2026-05-07 11:50:10 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
export default {
|
2026-05-07 11:50:10 +08:00
|
|
|
name: 'EmployeeManagementView',
|
|
|
|
|
emits: ['overview-change'],
|
|
|
|
|
setup(_, { emit }) {
|
|
|
|
|
const activeTab = ref(DEFAULT_STATUS_TABS[0])
|
2026-05-06 11:00:38 +08:00
|
|
|
const selectedEmployee = ref(null)
|
2026-05-07 11:50:10 +08:00
|
|
|
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('')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
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)
|
2026-05-06 11:00:38 +08:00
|
|
|
},
|
2026-05-07 11:50:10 +08:00
|
|
|
{ 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
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
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)
|
2026-05-06 11:00:38 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tabs,
|
|
|
|
|
activeTab,
|
|
|
|
|
selectedEmployee,
|
|
|
|
|
roleOptions,
|
|
|
|
|
employees,
|
2026-05-07 11:50:10 +08:00
|
|
|
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
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|