import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { fetchEmployeeMeta, fetchEmployees } from '../../services/employees.js' const DEFAULT_STATUS_TABS = ['全部员工', '在职', '试用中', '停用'] const FALLBACK_ROLE_OPTIONS = [ { id: 'manager', code: 'manager', label: '管理员', desc: '可以维护员工档案、组织结构和角色权限。' }, { id: 'finance', code: 'finance', label: '财务人员', desc: '可以处理复核、查看财务知识与风险校验结果。' }, { id: 'approver', code: 'approver', label: '审批负责人', desc: '可以处理审批中心中的待审单据。' }, { id: 'executive', code: 'executive', label: '高级管理人员', desc: '可以查看跨部门数据看板与关键审批结果。' }, { id: 'auditor', code: 'auditor', label: '审计观察员', desc: '可以查看变更记录和权限调整历史。' }, { id: 'user', code: 'user', label: '使用者', desc: '可以发起报销、查看个人单据和使用 AI 助手。' } ] function matchKeyword(employee, keyword) { if (!keyword) { return true } const haystack = [ employee.name, employee.employeeNo, employee.department, employee.position, employee.email, employee.manager, employee.financeOwner, employee.syncState, ...(employee.roles || []) ] .filter(Boolean) .join(' ') .toLowerCase() return haystack.includes(keyword) } function uniqueSorted(values) { return [...new Set(values.filter(Boolean))].sort((left, right) => { return String(left).localeCompare(String(right), 'zh-CN') }) } function resolveRoleOptions(metaRoles, employees) { const options = Array.isArray(metaRoles) && metaRoles.length ? metaRoles : FALLBACK_ROLE_OPTIONS const existingLabels = new Set(options.map((item) => item.label)) const unknownRoles = uniqueSorted(employees.flatMap((item) => item.roles || [])).filter( (label) => !existingLabels.has(label) ) return [ ...options, ...unknownRoles.map((label) => ({ id: label, code: label, label, desc: '该角色来自当前员工数据。' })) ] } function buildStatusTabs(employees) { return DEFAULT_STATUS_TABS.map((label) => ({ label, count: label === '全部员工' ? employees.length : employees.filter((item) => item.status === label).length })) } function buildEmployeeSummary(employees) { return { total: employees.length, active: employees.filter((item) => item.status === '在职').length, onboarding: employees.filter((item) => item.status === '试用中').length, disabled: employees.filter((item) => item.status === '停用').length, followUp: employees.filter((item) => item.syncState !== '已同步').length, departments: uniqueSorted(employees.map((item) => item.department)).length } } export default { name: 'EmployeeManagementView', emits: ['overview-change'], setup(_, { emit }) { const activeTab = ref(DEFAULT_STATUS_TABS[0]) const selectedEmployee = ref(null) const roleOptions = ref([...FALLBACK_ROLE_OPTIONS]) const employees = ref([]) const searchKeyword = ref('') const selectedDepartment = ref('') const selectedGrade = ref('') const selectedRole = ref('') const activeFilterPopover = ref('') const currentPage = ref(1) const pageSize = ref(10) const pageSizes = [10, 20, 50] const pageSizeOpen = ref(false) const loading = ref(false) const errorMessage = ref('') const tabs = computed(() => buildStatusTabs(employees.value)) const employeeSummary = computed(() => buildEmployeeSummary(employees.value)) const departmentOptions = computed(() => uniqueSorted(employees.value.map((item) => item.department)) ) const gradeOptions = computed(() => uniqueSorted(employees.value.map((item) => item.grade))) const roleFilterOptions = computed(() => uniqueSorted( roleOptions.value.map((item) => item.label).concat( employees.value.flatMap((item) => item.roles || []) ) ) ) const filteredEmployees = computed(() => { const keyword = searchKeyword.value.trim().toLowerCase() return employees.value.filter((item) => { const matchesStatus = activeTab.value === '全部员工' ? true : item.status === activeTab.value const matchesDepartment = selectedDepartment.value ? item.department === selectedDepartment.value : true const matchesGrade = selectedGrade.value ? item.grade === selectedGrade.value : true const matchesRole = selectedRole.value ? (item.roles || []).includes(selectedRole.value) : true return ( matchesStatus && matchesDepartment && matchesGrade && matchesRole && matchKeyword(item, keyword) ) }) }) const totalCount = computed(() => filteredEmployees.value.length) const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value))) const visibleEmployees = computed(() => { const start = (currentPage.value - 1) * pageSize.value return filteredEmployees.value.slice(start, start + pageSize.value) }) const activeFilterTokens = computed(() => { const tokens = [] if (selectedDepartment.value) { tokens.push(`部门:${selectedDepartment.value}`) } if (selectedGrade.value) { tokens.push(`职级:${selectedGrade.value}`) } if (selectedRole.value) { tokens.push(`角色:${selectedRole.value}`) } if (searchKeyword.value.trim()) { tokens.push(`搜索:${searchKeyword.value.trim()}`) } return tokens }) const hasActiveFilters = computed(() => activeFilterTokens.value.length > 0) watch( employeeSummary, (summary) => { emit('overview-change', summary) }, { immediate: true } ) watch(filteredEmployees, () => { currentPage.value = 1 pageSizeOpen.value = false }) function resetFilters() { searchKeyword.value = '' selectedDepartment.value = '' selectedGrade.value = '' selectedRole.value = '' activeTab.value = DEFAULT_STATUS_TABS[0] activeFilterPopover.value = '' pageSizeOpen.value = false } function changePageSize(size) { pageSize.value = size pageSizeOpen.value = false currentPage.value = 1 } function togglePageSizeOpen() { pageSizeOpen.value = !pageSizeOpen.value } function toggleFilterPopover(name) { activeFilterPopover.value = activeFilterPopover.value === name ? '' : name } function closeFilterPopover() { activeFilterPopover.value = '' } function selectFilter(name, value) { if (name === 'department') { selectedDepartment.value = value } if (name === 'grade') { selectedGrade.value = value } if (name === 'role') { selectedRole.value = value } closeFilterPopover() } function handleDocumentClick(event) { const target = event.target if (!(target instanceof Element)) { closeFilterPopover() pageSizeOpen.value = false return } if (!target.closest('.picker-filter')) { closeFilterPopover() } if (!target.closest('.page-size-wrap')) { pageSizeOpen.value = false } if (target.closest('.picker-filter') || target.closest('.page-size-wrap')) { return } } async function loadEmployees() { loading.value = true errorMessage.value = '' const [employeesResult, metaResult] = await Promise.allSettled([ fetchEmployees(), fetchEmployeeMeta() ]) if (employeesResult.status !== 'fulfilled') { employees.value = [] roleOptions.value = [...FALLBACK_ROLE_OPTIONS] selectedEmployee.value = null errorMessage.value = employeesResult.reason?.message || '员工数据加载失败,请稍后重试。' loading.value = false return } employees.value = Array.isArray(employeesResult.value) ? employeesResult.value : [] if (metaResult.status === 'fulfilled') { roleOptions.value = resolveRoleOptions(metaResult.value?.roleOptions, employees.value) } else { roleOptions.value = resolveRoleOptions([], employees.value) } if (!DEFAULT_STATUS_TABS.includes(activeTab.value)) { activeTab.value = DEFAULT_STATUS_TABS[0] } if (selectedEmployee.value) { selectedEmployee.value = employees.value.find((item) => item.id === selectedEmployee.value.id) || null } loading.value = false } onMounted(() => { document.addEventListener('click', handleDocumentClick) loadEmployees().catch((error) => { employees.value = [] roleOptions.value = [...FALLBACK_ROLE_OPTIONS] selectedEmployee.value = null errorMessage.value = error?.message || '员工数据加载失败,请稍后重试。' loading.value = false }) }) onBeforeUnmount(() => { document.removeEventListener('click', handleDocumentClick) }) return { tabs, activeTab, selectedEmployee, roleOptions, employees, visibleEmployees, searchKeyword, selectedDepartment, selectedGrade, selectedRole, activeFilterPopover, currentPage, pageSize, pageSizes, pageSizeOpen, departmentOptions, gradeOptions, roleFilterOptions, activeFilterTokens, hasActiveFilters, totalCount, totalPages, resetFilters, changePageSize, togglePageSizeOpen, toggleFilterPopover, closeFilterPopover, selectFilter, loading, errorMessage, loadEmployees } } }