feat(frontend): 添加部门管理和系统配置页面
- 添加 department.ts 部门管理服务 - 添加 DepartmentManagementView.vue 部门管理页面 - 添加 SystemConfigView.vue 系统配置页面 - 更新路由配置添加新页面 - 更新 App.vue 添加系统菜单入口 - 前端编译验证通过
This commit is contained in:
216
frontend/admin/src/views/DepartmentManagementView.vue
Normal file
216
frontend/admin/src/views/DepartmentManagementView.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<header class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="mos-title text-2xl font-semibold">部门管理</h1>
|
||||
<p class="mos-muted text-sm">管理系统部门组织架构。</p>
|
||||
</div>
|
||||
<button class="mos-btn mos-btn-accent" @click="openCreateDialog(0 as number)">
|
||||
新建部门
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 部门树形列表 -->
|
||||
<div class="mos-card p-5">
|
||||
<div v-if="loading" class="py-8 text-center text-mosquito-ink/60">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="!treeData.length" class="py-8 text-center text-mosquito-ink/60">
|
||||
暂无部门数据
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="dept in treeData"
|
||||
:key="dept.id"
|
||||
class="border border-mosquito-line rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="px-4 py-3 hover:bg-mosquito-bg/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">{{ dept.deptName }}</span>
|
||||
<span class="text-xs text-mosquito-ink/50">{{ dept.deptCode }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="openCreateDialog(dept.id || 0)">
|
||||
添加子部门
|
||||
</button>
|
||||
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="openEditDialog(dept)">
|
||||
编辑
|
||||
</button>
|
||||
<button class="mos-btn mos-btn-danger !py-1 !px-2 !text-xs" @click="handleDelete(dept)">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 子部门 -->
|
||||
<div v-if="dept.children?.length" class="mt-2 pl-6 space-y-1 bg-mosquito-bg/30 rounded-lg p-2">
|
||||
<div
|
||||
v-for="child in dept.children"
|
||||
:key="child.id"
|
||||
class="flex items-center justify-between py-2 px-3 bg-white rounded border border-mosquito-line"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ child.deptName }}</span>
|
||||
<span class="text-xs text-mosquito-ink/50">{{ child.deptCode }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="openEditDialog(child)">编辑</button>
|
||||
<button class="mos-btn mos-btn-danger !py-1 !px-2 !text-xs" @click="handleDelete(child)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<div v-if="dialogVisible" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="mos-card w-[500px] p-6">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ isEdit ? '编辑部门' : '新建部门' }}</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-mosquito-ink/70">部门名称</label>
|
||||
<input v-model="form.deptName" class="mos-input mt-2 w-full" placeholder="如: 运营部" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-mosquito-ink/70">部门编码</label>
|
||||
<input v-model="form.deptCode" class="mos-input mt-2 w-full" placeholder="如: DEPT001" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-mosquito-ink/70">排序</label>
|
||||
<input v-model.number="form.sortOrder" type="number" class="mos-input mt-2 w-full" placeholder="0" />
|
||||
</div>
|
||||
<div v-if="isEdit">
|
||||
<label class="text-xs font-semibold text-mosquito-ink/70">状态</label>
|
||||
<select v-model="form.status" class="mos-input mt-2 w-full">
|
||||
<option :value="1">启用</option>
|
||||
<option :value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<button class="mos-btn mos-btn-secondary" @click="dialogVisible = false">取消</button>
|
||||
<button class="mos-btn mos-btn-accent" @click="handleSubmit">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { type Department } from '../services/department'
|
||||
import departmentService from '../services/department'
|
||||
|
||||
interface DepartmentWithChildren extends Department {
|
||||
children?: DepartmentWithChildren[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const departments = ref<Department[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
const form = ref({
|
||||
id: 0,
|
||||
parentId: 0,
|
||||
deptName: '',
|
||||
deptCode: '',
|
||||
sortOrder: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 树形结构
|
||||
const treeData = computed(() => {
|
||||
const deptMap = new Map<number, DepartmentWithChildren>()
|
||||
const roots: DepartmentWithChildren[] = []
|
||||
|
||||
departments.value.forEach(dept => {
|
||||
deptMap.set(dept.id!, { ...dept, children: [] })
|
||||
})
|
||||
|
||||
departments.value.forEach(dept => {
|
||||
const node = deptMap.get(dept.id!)!
|
||||
if (dept.parentId && dept.parentId > 0 && deptMap.has(dept.parentId)) {
|
||||
deptMap.get(dept.parentId)!.children!.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
})
|
||||
|
||||
const loadDepartments = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
departments.value = await departmentService.getDepartments()
|
||||
} catch (error) {
|
||||
console.error('加载部门失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openCreateDialog = (parentId: number) => {
|
||||
isEdit.value = false
|
||||
form.value = {
|
||||
id: 0,
|
||||
parentId,
|
||||
deptName: '',
|
||||
deptCode: '',
|
||||
sortOrder: 0,
|
||||
status: 1
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditDialog = (dept: Department) => {
|
||||
isEdit.value = true
|
||||
form.value = {
|
||||
id: dept.id!,
|
||||
parentId: dept.parentId || 0,
|
||||
deptName: dept.deptName,
|
||||
deptCode: dept.deptCode || '',
|
||||
sortOrder: dept.sortOrder || 0,
|
||||
status: dept.status
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await departmentService.updateDepartment(form.value.id, form.value)
|
||||
alert('部门更新成功')
|
||||
} else {
|
||||
await departmentService.createDepartment(form.value)
|
||||
alert('部门创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadDepartments()
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (dept: Department) => {
|
||||
if (!confirm(`确定要删除部门 ${dept.deptName} 吗?`)) return
|
||||
try {
|
||||
if (dept.id) {
|
||||
await departmentService.deleteDepartment(dept.id)
|
||||
alert('部门删除成功')
|
||||
await loadDepartments()
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDepartments()
|
||||
})
|
||||
</script>
|
||||
197
frontend/admin/src/views/SystemConfigView.vue
Normal file
197
frontend/admin/src/views/SystemConfigView.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<header class="space-y-2">
|
||||
<h1 class="mos-title text-2xl font-semibold">系统配置</h1>
|
||||
<p class="mos-muted text-sm">管理系统参数和缓存配置。</p>
|
||||
</header>
|
||||
|
||||
<!-- 配置tabs -->
|
||||
<div class="flex gap-2 border-b border-mosquito-line pb-2">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="px-4 py-2 text-sm font-semibold rounded-t-lg transition"
|
||||
:class="activeTab === tab.key
|
||||
? 'bg-mosquito-accent/10 text-mosquito-brand border-b-2 border-mosquito-brand'
|
||||
: 'text-mosquito-ink/70 hover:bg-mosquito-bg'"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 系统参数 -->
|
||||
<div v-if="activeTab === 'params'" class="mos-card p-5">
|
||||
<div class="space-y-4">
|
||||
<div v-for="config in systemParams" :key="config.key" class="flex items-center justify-between py-3 border-b border-mosquito-line">
|
||||
<div>
|
||||
<div class="font-semibold">{{ config.label }}</div>
|
||||
<div class="text-xs text-mosquito-ink/70">{{ config.description }}</div>
|
||||
</div>
|
||||
<div class="w-64">
|
||||
<input
|
||||
v-if="config.type === 'string'"
|
||||
v-model="config.value"
|
||||
class="mos-input w-full"
|
||||
/>
|
||||
<input
|
||||
v-else-if="config.type === 'number'"
|
||||
v-model.number="config.value"
|
||||
type="number"
|
||||
class="mos-input w-full"
|
||||
/>
|
||||
<select
|
||||
v-else-if="config.type === 'boolean'"
|
||||
v-model="config.value"
|
||||
class="mos-input w-full"
|
||||
>
|
||||
<option :value="true">启用</option>
|
||||
<option :value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button class="mos-btn mos-btn-accent" @click="saveParams">保存配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缓存管理 -->
|
||||
<div v-if="activeTab === 'cache'" class="mos-card p-5">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-3 border-b border-mosquito-line">
|
||||
<div>
|
||||
<div class="font-semibold">活动数据缓存</div>
|
||||
<div class="text-xs text-mosquito-ink/70">缓存活动列表和统计数据</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-secondary" @click="clearCache('activity')">清除缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3 border-b border-mosquito-line">
|
||||
<div>
|
||||
<div class="font-semibold">用户数据缓存</div>
|
||||
<div class="text-xs text-mosquito-ink/70">缓存用户信息和权限数据</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-secondary" @click="clearCache('user')">清除缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3 border-b border-mosquito-line">
|
||||
<div>
|
||||
<div class="font-semibold">奖励数据缓存</div>
|
||||
<div class="text-xs text-mosquito-ink/70">缓存奖励配置和发放记录</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-secondary" @click="clearCache('reward')">清除缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<div class="font-semibold">全部缓存</div>
|
||||
<div class="text-xs text-mosquito-ink/70">清除所有系统缓存</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mos-btn mos-btn-danger" @click="clearCache('all')">清除全部缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API密钥 -->
|
||||
<div v-if="activeTab === 'apiKey'" class="mos-card p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="font-semibold">API密钥管理</div>
|
||||
<button class="mos-btn mos-btn-accent" @click="createApiKey">创建新密钥</button>
|
||||
</div>
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left text-xs text-mosquito-ink/70 border-b border-mosquito-line">
|
||||
<th class="pb-2">名称</th>
|
||||
<th class="pb-2">密钥</th>
|
||||
<th class="pb-2">状态</th>
|
||||
<th class="pb-2">创建时间</th>
|
||||
<th class="pb-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="key in apiKeys" :key="key.id" class="border-b border-mosquito-line">
|
||||
<td class="py-3">{{ key.name }}</td>
|
||||
<td class="py-3 font-mono text-sm">{{ showKeyId === key.id ? key.key : '••••••••••••••••' }}</td>
|
||||
<td class="py-3">
|
||||
<span :class="key.status === 1 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ key.status === 1 ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-sm">{{ key.createdAt }}</td>
|
||||
<td class="py-3">
|
||||
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="toggleShowKey(key.id)">
|
||||
{{ showKeyId === key.id ? '隐藏' : '显示' }}
|
||||
</button>
|
||||
<button class="mos-btn mos-btn-danger !py-1 !px-2 !text-xs ml-2" @click="deleteApiKey(key.id)">
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="!apiKeys.length" class="py-8 text-center text-mosquito-ink/60">
|
||||
暂无API密钥
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeTab = ref('params')
|
||||
const showKeyId = ref<number | null>(null)
|
||||
|
||||
const tabs = [
|
||||
{ key: 'params', label: '系统参数' },
|
||||
{ key: 'cache', label: '缓存管理' },
|
||||
{ key: 'apiKey', label: 'API密钥' }
|
||||
]
|
||||
|
||||
const systemParams = ref([
|
||||
{ key: 'reward.max.points', label: '单次奖励上限', description: '单次奖励发放的最大积分值', value: 10000, type: 'number' },
|
||||
{ key: 'activity.max.count', label: '最大活动数', description: '系统允许创建的最大活动数量', value: 100, type: 'number' },
|
||||
{ key: 'risk.callback.threshold', label: '回调失败阈值', description: '触发告警的回调失败率(%)', value: 5, type: 'number' },
|
||||
{ key: 'approval.auto.timeout', label: '审批超时时间', description: '审批自动通过的超时时间(小时)', value: 24, type: 'number' },
|
||||
{ key: 'user.invite.expire', label: '邀请链接有效期', description: '邀请链接有效天数', value: 7, type: 'number' },
|
||||
{ key: 'reward.batch.size', label: '批量发放大小', description: '奖励批量发放的每批数量', value: 200, type: 'number' }
|
||||
])
|
||||
|
||||
const apiKeys = ref([
|
||||
{ id: 1, name: '生产环境密钥', key: 'mk_prod_xxxxxxxxxxxxx', status: 1, createdAt: '2026-01-15' },
|
||||
{ id: 2, name: '测试环境密钥', key: 'mk_test_xxxxxxxxxxxxx', status: 1, createdAt: '2026-02-01' }
|
||||
])
|
||||
|
||||
const saveParams = () => {
|
||||
alert('配置保存成功(演示)')
|
||||
}
|
||||
|
||||
const clearCache = (type: string) => {
|
||||
if (confirm(`确定要清除${type === 'all' ? '全部' : type}缓存吗?`)) {
|
||||
alert('缓存清除成功(演示)')
|
||||
}
|
||||
}
|
||||
|
||||
const createApiKey = () => {
|
||||
const name = prompt('请输入密钥名称:')
|
||||
if (name) {
|
||||
alert('API密钥创建成功(演示)')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleShowKey = (id: number) => {
|
||||
showKeyId.value = showKeyId.value === id ? null : id
|
||||
}
|
||||
|
||||
const deleteApiKey = (id: number) => {
|
||||
if (confirm('确定要删除这个API密钥吗?')) {
|
||||
alert('API密钥删除成功(演示)')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user