feat: 增强员工管理与报销单全流程功能

- 新增员工Excel导入服务(employee_spreadsheet)及导入/导出API端点
- 员工服务增加批量创建、邮箱唯一校验、组织架构关联等能力
- 报销单提交补充身份回填、部门信息透传及预审结果展示优化
- 认证流程增加部门信息(departmentName)并在schema中同步扩展
- 用户Agent服务增加部门关联与报销单回填逻辑
- 前端员工管理页面全面重构,新增导入导出、搜索过滤、分页等功能
- 前端审批中心、审计、差旅报销等视图交互与样式优化
- 新增TableLoadingState共享组件及员工导入测试用例
This commit is contained in:
caoxiaozhu
2026-05-20 14:21:56 +08:00
parent 57957d11a0
commit d7e98a58b9
46 changed files with 4022 additions and 305 deletions

View File

@@ -1,9 +1,13 @@
import { computed, ref } from 'vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import { mapExpenseClaimToRequest } from '../../composables/useRequests.js'
import { useSystemState } from '../../composables/useSystemState.js'
import { fetchExpenseClaims } from '../../services/reimbursements.js'
import { useToast } from '../../composables/useToast.js'
import { deleteExpenseClaim, fetchExpenseClaims, returnExpenseClaim } from '../../services/reimbursements.js'
import { canManageExpenseClaims } from '../../utils/accessControl.js'
const DEFAULT_SLA_HOURS = 24
const tabs = ['全部待审', '高风险', '即将超时', '已处理']
@@ -195,7 +199,6 @@ function buildFlowItems(request) {
function canCurrentUserProcessRequest(request, currentUser) {
const node = String(request?.workflowNode || '').trim()
const roleCodes = Array.isArray(currentUser?.roleCodes) ? currentUser.roleCodes.filter(Boolean) : []
const currentName = String(currentUser?.name || '').trim()
const applicantName = String(request?.person || request?.employeeName || '').trim()
@@ -203,8 +206,8 @@ function canCurrentUserProcessRequest(request, currentUser) {
return false
}
if (currentUser?.isAdmin || roleCodes.includes('finance')) {
return node.includes('财务')
if (canManageExpenseClaims(currentUser)) {
return true
}
return (
@@ -251,10 +254,13 @@ function buildApprovalRow(request) {
export default {
name: 'ApprovalCenterView',
components: {
ConfirmDialog,
TableLoadingState,
TableEmptyState
},
setup() {
const { currentUser } = useSystemState()
const { toast } = useToast()
const activeTab = ref('全部待审')
const selectedClaimId = ref('')
const expandedExpenseId = ref(null)
@@ -262,6 +268,9 @@ export default {
const rows = ref([])
const loading = ref(false)
const error = ref('')
const actionBusy = ref(false)
const returnDialogOpen = ref(false)
const deleteDialogOpen = ref(false)
const selectedRow = computed({
get() {
@@ -303,6 +312,7 @@ export default {
})
const showTable = computed(() => !loading.value && !error.value && visibleRows.value.length > 0)
const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0)
const canManageClaims = computed(() => canManageExpenseClaims(currentUser.value))
const approvalEmptyState = computed(() => {
if (!rows.value.length) {
return {
@@ -381,6 +391,76 @@ export default {
activeTab.value = '全部待审'
}
function handleReturnSelected() {
if (!selectedRow.value?.claimId || !canManageClaims.value || actionBusy.value) {
return
}
returnDialogOpen.value = true
}
function handleDeleteSelected() {
if (!selectedRow.value?.claimId || !canManageClaims.value || actionBusy.value) {
return
}
deleteDialogOpen.value = true
}
function closeReturnDialog() {
if (!actionBusy.value) {
returnDialogOpen.value = false
}
}
function closeDeleteDialog() {
if (!actionBusy.value) {
deleteDialogOpen.value = false
}
}
async function confirmReturnSelected() {
const row = selectedRow.value
if (!row?.claimId || actionBusy.value) {
return
}
actionBusy.value = true
try {
await returnExpenseClaim(row.claimId, {
reason: '审批中心退回,请申请人补充后重新提交。'
})
toast(`${row.id} 已退回待补充。`)
returnDialogOpen.value = false
selectedClaimId.value = ''
await reload()
} catch (nextError) {
toast(nextError?.message || '退回单据失败,请稍后重试。')
} finally {
actionBusy.value = false
}
}
async function confirmDeleteSelected() {
const row = selectedRow.value
if (!row?.claimId || actionBusy.value) {
return
}
actionBusy.value = true
try {
const payload = await deleteExpenseClaim(row.claimId)
toast(payload?.message || `${row.id} 报销单已删除。`)
deleteDialogOpen.value = false
selectedClaimId.value = ''
await reload()
} catch (nextError) {
toast(nextError?.message || '删除单据失败,请稍后重试。')
} finally {
actionBusy.value = false
}
}
async function reload() {
loading.value = true
error.value = ''
@@ -420,8 +500,15 @@ export default {
visibleRows,
showTable,
showEmpty,
actionBusy,
approvalEmptyState,
approvalSteps,
canManageClaims,
closeDeleteDialog,
closeReturnDialog,
confirmDeleteSelected,
confirmReturnSelected,
deleteDialogOpen,
summaryItems,
heroSummaryItems,
currentProgressRingMotion,
@@ -434,8 +521,11 @@ export default {
riskItems,
flowItems,
handleEmptyAction,
handleDeleteSelected,
handleReturnSelected,
loading,
error,
returnDialogOpen,
reload
}
}