Files
X-Financial/web/src/views/scripts/EmployeeManagementView.js

374 lines
9.9 KiB
JavaScript
Raw Normal View History

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
}
}
}