完成前端部署内容

This commit is contained in:
super
2026-01-15 13:17:41 +08:00
parent 8fa07e4952
commit 183b295e40
34 changed files with 3564 additions and 655 deletions

View File

@@ -221,11 +221,13 @@ import {
import type { ApprovalTemplate, ApprovalScenario, ApprovalNode } from '@/types'
import { ApprovalScenarioMap } from '@/types'
import {
mockGetApprovalStats,
mockGetApprovalTemplateList,
mockToggleApprovalTemplate,
mockDeleteApprovalTemplate
} from '@/mock'
getApprovalStats,
getApprovalTemplateList,
toggleApprovalTemplate,
deleteApprovalTemplate,
createApprovalTemplate,
updateApprovalTemplate
} from '@/api/system/approval'
import { formatDateTime } from '@/utils/common'
import FlowEditor from '@/components/FlowEditor/index.vue'
@@ -290,7 +292,7 @@ function getScenarioColor(scenario: ApprovalScenario): string {
async function loadStats() {
try {
const data = await mockGetApprovalStats()
const data = await getApprovalStats()
Object.assign(stats, data)
} catch (error) {
console.error(error)
@@ -300,10 +302,8 @@ async function loadStats() {
async function loadTemplates() {
loading.value = true
try {
const res = await mockGetApprovalTemplateList({
scenario: filterScenario.value
})
templateList.value = res.list
const list = await getApprovalTemplateList(filterScenario.value)
templateList.value = list
} catch (error) {
console.error(error)
message.error('加载数据失败')
@@ -346,12 +346,27 @@ async function handleSubmit() {
formData.nodes = flowEditorRef.value.toApprovalNodes()
}
// 这里应该调用创建/更新API
const requestData = {
name: formData.name,
description: formData.description,
scenario: formData.scenario!,
enabled: formData.enabled,
nodes: formData.nodes
}
if (editingTemplate.value) {
await updateApprovalTemplate(editingTemplate.value.id, requestData)
} else {
await createApprovalTemplate(requestData)
}
message.success(editingTemplate.value ? '更新成功' : '创建成功')
drawerVisible.value = false
loadTemplates()
} catch {
// 验证失败
loadStats()
} catch (error) {
console.error(error)
message.error('保存失败')
} finally {
submitting.value = false
}
@@ -359,7 +374,7 @@ async function handleSubmit() {
async function handleToggle(id: number) {
try {
await mockToggleApprovalTemplate(id)
await toggleApprovalTemplate(id)
message.success('状态已更新')
loadTemplates()
loadStats()
@@ -371,7 +386,7 @@ async function handleToggle(id: number) {
async function handleDelete(id: number) {
try {
await mockDeleteApprovalTemplate(id)
await deleteApprovalTemplate(id)
message.success('删除成功')
loadTemplates()
loadStats()

View File

@@ -203,7 +203,7 @@ import {
} from '@ant-design/icons-vue'
import type { ApprovalInstance, ApprovalScenario, ApprovalInstanceStatus } from '@/types'
import { ApprovalScenarioMap, ApprovalInstanceStatusMap, ApprovalInstanceStatusBadgeMap } from '@/types'
import { mockGetApprovalInstanceList } from '@/mock'
import { getApprovalInstancePage } from '@/api/system/approval'
import { formatDateTime } from '@/utils/common'
const loading = ref(false)
@@ -274,16 +274,14 @@ function getActionText(action: string): string {
async function loadData() {
loading.value = true
try {
const res = await mockGetApprovalInstanceList({
const res = await getApprovalInstancePage({
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword.value || undefined,
scenario: filterScenario.value,
status: filterStatus.value,
startDate: dateRange.value?.[0]?.format('YYYY-MM-DD'),
endDate: dateRange.value?.[1]?.format('YYYY-MM-DD')
status: filterStatus.value
})
instanceList.value = res.list
instanceList.value = res.records
pagination.total = res.total
} catch (error) {
console.error(error)

View File

@@ -0,0 +1,290 @@
<template>
<div class="dept-page">
<a-page-header title="部门管理" sub-title="管理组织架构和部门信息">
<template #extra>
<a-button type="primary" @click="showCreateModal()">
<PlusOutlined /> 新增部门
</a-button>
</template>
</a-page-header>
<a-card class="main-card" :loading="loading">
<a-table
:columns="columns"
:data-source="deptTree"
:pagination="false"
row-key="id"
:default-expand-all-rows="true"
:indent-size="24"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="dept-name">
<ApartmentOutlined class="dept-icon" />
<span>{{ record.name }}</span>
</div>
</template>
<template v-else-if="column.key === 'leader'">
<span v-if="record.leaderName">{{ record.leaderName }}</span>
<span v-else class="empty-text">-</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '正常' : '停用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="showCreateModal(record.id)">
<PlusOutlined /> 新增子部门
</a-button>
<a-button type="link" size="small" @click="showEditModal(record as DeptRecord)">
<EditOutlined /> 编辑
</a-button>
<a-popconfirm
title="确定删除此部门?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined /> 删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="editingDept ? '编辑部门' : '新增部门'"
:confirm-loading="submitting"
@ok="handleSubmit"
@cancel="modalVisible = false"
>
<a-form
ref="formRef"
:model="formData"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="上级部门" name="parentId">
<a-tree-select
v-model:value="formData.parentId"
:tree-data="parentTreeData"
placeholder="请选择上级部门"
tree-default-expand-all
:field-names="{ children: 'children', label: 'name', value: 'id' }"
allow-clear
/>
</a-form-item>
<a-form-item label="部门名称" name="name" :rules="[{ required: true, message: '请输入部门名称' }]">
<a-input v-model:value="formData.name" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item label="部门编码" name="code">
<a-input v-model:value="formData.code" placeholder="请输入部门编码" />
</a-form-item>
<a-form-item label="负责人" name="leaderName">
<a-input v-model:value="formData.leaderName" placeholder="请输入负责人姓名" />
</a-form-item>
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="显示排序" name="sort">
<a-input-number v-model:value="formData.sort" :min="0" style="width: 100%" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">正常</a-radio>
<a-radio :value="0">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="formData.remark" :rows="3" placeholder="请输入备注" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ApartmentOutlined
} from '@ant-design/icons-vue'
import { getDeptTree, createDept, updateDept, deleteDept, type DeptRecord, type DeptFormData } from '@/api/system/dept'
const loading = ref(false)
const submitting = ref(false)
const deptTree = ref<DeptRecord[]>([])
const modalVisible = ref(false)
const editingDept = ref<DeptRecord | null>(null)
const formRef = ref<FormInstance>()
const formData = reactive<DeptFormData>({
parentId: 0,
name: '',
code: '',
leaderName: '',
phone: '',
email: '',
sort: 0,
status: 1,
remark: ''
})
const columns = [
{ title: '部门名称', key: 'name', width: 280 },
{ title: '部门编码', dataIndex: 'code', width: 120 },
{ title: '负责人', key: 'leader', width: 100 },
{ title: '联系电话', dataIndex: 'phone', width: 120 },
{ title: '状态', key: 'status', width: 80 },
{ title: '排序', dataIndex: 'sort', width: 80 },
{ title: '操作', key: 'action', width: 240 }
]
// 构建上级部门下拉数据(添加顶级选项)
const parentTreeData = computed(() => {
return [
{ id: 0, name: '无(顶级部门)', children: deptTree.value }
]
})
async function loadData() {
loading.value = true
try {
deptTree.value = await getDeptTree()
} catch (error) {
console.error(error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
function resetForm() {
Object.assign(formData, {
parentId: 0,
name: '',
code: '',
leaderName: '',
phone: '',
email: '',
sort: 0,
status: 1,
remark: ''
})
}
function showCreateModal(parentId?: number) {
editingDept.value = null
resetForm()
if (parentId) {
formData.parentId = parentId
}
modalVisible.value = true
}
function showEditModal(dept: DeptRecord) {
editingDept.value = dept
Object.assign(formData, {
parentId: dept.parentId,
name: dept.name,
code: dept.code || '',
leaderName: dept.leaderName || '',
phone: dept.phone || '',
email: dept.email || '',
sort: dept.sort,
status: dept.status,
remark: dept.remark || ''
})
modalVisible.value = true
}
async function handleSubmit() {
try {
await formRef.value?.validate()
submitting.value = true
if (editingDept.value) {
await updateDept(editingDept.value.id, formData)
message.success('更新成功')
} else {
await createDept(formData)
message.success('创建成功')
}
modalVisible.value = false
loadData()
} catch (error) {
console.error(error)
} finally {
submitting.value = false
}
}
async function handleDelete(id: number) {
try {
await deleteDept(id)
message.success('删除成功')
loadData()
} catch (error) {
console.error(error)
message.error('删除失败')
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.dept-page {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
overflow-y: auto;
padding-right: 8px;
}
.main-card {
border-radius: 12px;
}
.dept-name {
display: flex;
align-items: center;
gap: 8px;
}
.dept-icon {
color: #1890ff;
font-size: 16px;
}
.empty-text {
color: #bfbfbf;
}
</style>

View File

@@ -49,8 +49,8 @@
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleViewItems(record)">字典项</a-button>
<a-button type="link" size="small" @click="handleEdit(record as DictRecord)">编辑</a-button>
<a-button type="link" size="small" @click="handleViewItems(record as DictRecord)">字典项</a-button>
<a-popconfirm
title="确定删除此字典吗?"
@confirm="handleDelete(record.id)"
@@ -84,6 +84,7 @@ import { message } from 'ant-design-vue'
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import DictFormModal from '@/components/system/dict/DictFormModal.vue'
import DictItemDrawer from '@/components/system/dict/DictItemDrawer.vue'
import { getDictPage, deleteDict } from '@/api/system/dict'
// 类型定义
interface DictRecord {
@@ -119,7 +120,7 @@ const columns = [
{ title: '备注', dataIndex: 'remark', ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 80 },
{ title: '创建时间', dataIndex: 'createdAt', width: 170 },
{ title: '操作', dataIndex: 'action', width: 160, fixed: 'right' }
{ title: '操作', dataIndex: 'action', width: 160, fixed: 'right' as const }
]
// 字典弹窗
@@ -134,18 +135,17 @@ const currentDict = ref<DictRecord | null>(null)
async function loadData() {
loading.value = true
try {
// TODO: 接入真实API
// const res = await getDictList({ ...searchForm, page: pagination.current, pageSize: pagination.pageSize })
// tableData.value = res.data.data.records
// pagination.total = res.data.data.total
// 模拟数据
tableData.value = [
{ id: 1, name: '性别', code: 'gender', remark: '用户性别', status: 1, createdAt: '2026-01-08 10:00:00' },
{ id: 2, name: '状态', code: 'status', remark: '通用状态', status: 1, createdAt: '2026-01-08 10:00:00' },
{ id: 3, name: '审批状态', code: 'approval_status', remark: '审批流程状态', status: 1, createdAt: '2026-01-08 10:00:00' }
]
pagination.total = 3
const res = await getDictPage({
page: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
code: searchForm.code || undefined
})
tableData.value = res.data.data.records || []
pagination.total = res.data.data.total || 0
} catch (error) {
console.error('加载字典数据失败:', error)
tableData.value = []
} finally {
loading.value = false
}
@@ -184,10 +184,15 @@ function handleViewItems(record: DictRecord) {
}
async function handleDelete(id: number) {
// TODO: 接入真实API
// await deleteDict(id)
message.success('删除成功')
loadData()
try {
await deleteDict(id)
message.success('删除成功')
loadData()
} catch (error: any) {
if (error?.response?.data?.message) {
message.error(error.response.data.message)
}
}
}
onMounted(() => {

View File

@@ -119,7 +119,7 @@ async function loadData() {
loading.value = true
try {
const res = await getRolePage({ pageSize: 100 })
tableData.value = res.data.data.records || []
tableData.value = res.records || []
} catch (error) {
console.error('加载数据失败:', error)
} finally {
@@ -161,7 +161,7 @@ async function handleAssignMenus(record: RoleRecord) {
// 加载角色已有的菜单
const roleMenuRes = await getRoleMenuIds(record.id)
checkedMenuIds.value = roleMenuRes.data.data || []
checkedMenuIds.value = roleMenuRes || []
} catch (error) {
console.error('加载数据失败:', error)
} finally {

View File

@@ -6,6 +6,17 @@
<a-form-item label="关键词">
<a-input v-model:value="searchForm.keyword" placeholder="用户名/昵称/手机号" allow-clear style="width: 200px" />
</a-form-item>
<a-form-item label="部门">
<a-tree-select
v-model:value="searchForm.deptId"
:tree-data="deptTreeData"
placeholder="请选择部门"
tree-default-expand-all
:field-names="{ children: 'children', label: 'name', value: 'id' }"
allow-clear
style="width: 180px"
/>
</a-form-item>
<a-form-item label="角色">
<a-select v-model:value="searchForm.role" placeholder="请选择角色" allow-clear style="width: 150px">
<a-select-option v-for="role in roleOptions" :key="role.code" :value="role.code">
@@ -68,8 +79,8 @@
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleResetPassword(record)">重置密码</a-button>
<a-button type="link" size="small" @click="handleEdit(record as UserRecord)">编辑</a-button>
<a-button type="link" size="small" @click="handleResetPassword(record as UserRecord)">重置密码</a-button>
<a-popconfirm title="确定删除该用户吗?" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
@@ -106,6 +117,7 @@ import {
import { getUserList, deleteUser, updateUserStatus } from '@/api/system/user'
import { getRoleList } from '@/api/system/role'
import { getDeptTree, type DeptRecord } from '@/api/system/dept'
import type { UserRecord, UserFormData } from '@/types/system/user'
import type { RoleRecord } from '@/types/system/role'
@@ -116,10 +128,12 @@ import ResetPasswordModal from '@/components/system/user/ResetPasswordModal.vue'
const loading = ref(false)
const tableData = ref<UserRecord[]>([])
const roleOptions = ref<{ code: string; name: string }[]>([]) // Role List for display/select
const deptTreeData = ref<DeptRecord[]>([])
// 搜索表单
const searchForm = reactive({
keyword: '',
deptId: undefined as number | undefined,
role: undefined as string | undefined,
status: undefined as number | undefined
})
@@ -139,11 +153,11 @@ const columns = [
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '部门', dataIndex: 'deptName', key: 'deptName', width: 120 },
{ title: '角色', dataIndex: 'role', key: 'role', width: 120 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 },
{ title: '操作', key: 'action', width: 200, fixed: 'right' }
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const }
]
// Modal State
@@ -190,16 +204,22 @@ async function loadData() {
// 加载角色列表
async function loadRoles() {
try {
// 这里如果后端没有 list-all我们可以用 list 且不传参(默认第一页),或者传大 pageSize
// 假设后端支持 list 并返回 records
const res = await getRoleList()
// Mapping RoleRecord to Option { code, name }
roleOptions.value = (res.data.data || []).map((r: RoleRecord) => ({ code: r.code, name: r.name }))
const roles = await getRoleList()
roleOptions.value = (roles || []).map((r: any) => ({ code: r.code, name: r.name }))
} catch (error) {
console.error('加载角色列表失败:', error)
}
}
// 加载部门树
async function loadDeptTree() {
try {
deptTreeData.value = await getDeptTree()
} catch (error) {
console.error('加载部门列表失败:', error)
}
}
function handleSearch() {
pagination.current = 1
loadData()
@@ -207,6 +227,7 @@ function handleSearch() {
function handleReset() {
searchForm.keyword = ''
searchForm.deptId = undefined
searchForm.role = undefined
searchForm.status = undefined
handleSearch()
@@ -258,6 +279,7 @@ function handleResetPassword(record: UserRecord) {
onMounted(() => {
loadRoles()
loadDeptTree()
loadData()
})
</script>