feat: add employee management, backend health check, and UI improvements
This commit is contained in:
@@ -1,202 +1,373 @@
|
||||
import { computed, ref } from 'vue'
|
||||
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' ,
|
||||
setup(props, { emit }) {
|
||||
const tabs = ['全部员工', '在职', '试用中', '停用']
|
||||
const filters = ['按部门筛选', '按职级筛选', '按系统角色筛选']
|
||||
const activeTab = ref(tabs[0])
|
||||
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 roleOptions = [
|
||||
{ id: 'user', label: '使用者', desc: '可以发起报销、查看个人单据和使用 AI 助手。' },
|
||||
{ id: 'finance', label: '财务人员', desc: '可以处理复核、查看财务知识与风险校验结果。' },
|
||||
{ id: 'manager', label: '管理员', desc: '可以维护员工档案、组织结构和角色权限。' },
|
||||
{ id: 'executive', label: '高级管理人员', desc: '可以查看跨部门数据看板与关键审批结果。' },
|
||||
{ id: 'approver', label: '审批负责人', desc: '可以处理审批中心中的待审单据。' },
|
||||
{ id: 'auditor', label: '审计观察员', desc: '可以查看变更记录和权限调整历史。' }
|
||||
]
|
||||
const tabs = computed(() => buildStatusTabs(employees.value))
|
||||
const employeeSummary = computed(() => buildEmployeeSummary(employees.value))
|
||||
|
||||
const employees = [
|
||||
{
|
||||
id: 'EMP-001',
|
||||
avatar: '张',
|
||||
name: '张晓晴',
|
||||
employeeNo: 'E10234',
|
||||
department: '财务共享中心',
|
||||
position: '费用运营经理',
|
||||
grade: 'M3',
|
||||
manager: '李文静',
|
||||
financeOwner: '华东财务组',
|
||||
roles: ['管理员', '财务人员', '审批负责人'],
|
||||
status: '在职',
|
||||
statusTone: 'success',
|
||||
gender: '女',
|
||||
age: '32',
|
||||
birthDate: '1994-08-12',
|
||||
email: 'xiaoqing.zhang@xfinance.com',
|
||||
phone: '138 1023 4567',
|
||||
joinDate: '2021-03-15',
|
||||
location: '上海',
|
||||
costCenter: 'CC-2108',
|
||||
updatedAt: '2026-05-06 10:24',
|
||||
lastSync: '2026-05-06 10:24',
|
||||
syncState: '待生效',
|
||||
spotlight: true,
|
||||
permissions: [
|
||||
'可查看审批中心全部待审单据',
|
||||
'可配置员工角色与部门归属',
|
||||
'可查看知识管理与技能中心配置'
|
||||
],
|
||||
history: [
|
||||
{ action: '新增“审批负责人”角色', owner: '系统管理员 · 王敏', time: '今天 10:24' },
|
||||
{ action: '调整财务归口为华东财务组', owner: '组织管理员 · 陈硕', time: '昨天 18:10' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EMP-002',
|
||||
avatar: '李',
|
||||
name: '李文静',
|
||||
employeeNo: 'E10018',
|
||||
department: '总经办',
|
||||
position: '高级财务总监',
|
||||
grade: 'D2',
|
||||
manager: 'CEO',
|
||||
financeOwner: '集团财务',
|
||||
roles: ['高级管理人员', '审批负责人'],
|
||||
status: '在职',
|
||||
statusTone: 'success',
|
||||
gender: '女',
|
||||
age: '39',
|
||||
birthDate: '1987-03-26',
|
||||
email: 'wenjing.li@xfinance.com',
|
||||
phone: '139 0018 7688',
|
||||
joinDate: '2018-06-21',
|
||||
location: '上海',
|
||||
costCenter: 'CC-1001',
|
||||
updatedAt: '2026-05-05 16:20',
|
||||
lastSync: '2026-05-05 16:20',
|
||||
syncState: '已同步',
|
||||
permissions: [
|
||||
'可查看集团层面的审批看板',
|
||||
'可处理高金额报销的最终审批',
|
||||
'可查看部门预算执行情况'
|
||||
],
|
||||
history: [
|
||||
{ action: '更新高级管理人员可见范围', owner: '系统管理员 · 王敏', time: '05-05 16:20' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EMP-003',
|
||||
avatar: '王',
|
||||
name: '王敏',
|
||||
employeeNo: 'E10867',
|
||||
department: '人力与组织',
|
||||
position: '组织发展主管',
|
||||
grade: 'P6',
|
||||
manager: '陈嘉',
|
||||
financeOwner: '总部财务',
|
||||
roles: ['管理员', '审计观察员'],
|
||||
status: '在职',
|
||||
statusTone: 'success',
|
||||
gender: '女',
|
||||
age: '30',
|
||||
birthDate: '1996-11-05',
|
||||
email: 'min.wang@xfinance.com',
|
||||
phone: '136 8867 1200',
|
||||
joinDate: '2022-08-08',
|
||||
location: '杭州',
|
||||
costCenter: 'CC-3206',
|
||||
updatedAt: '2026-05-05 09:18',
|
||||
lastSync: '2026-05-05 09:18',
|
||||
syncState: '已同步',
|
||||
permissions: [
|
||||
'可维护组织结构与岗位映射',
|
||||
'可查看员工角色分配历史'
|
||||
],
|
||||
history: [
|
||||
{ action: '新增“审计观察员”角色', owner: '系统管理员 · 张晓晴', time: '05-05 09:18' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EMP-004',
|
||||
avatar: '陈',
|
||||
name: '陈嘉',
|
||||
employeeNo: 'E11602',
|
||||
department: '销售运营',
|
||||
position: '区域销售经理',
|
||||
grade: 'M2',
|
||||
manager: '李文静',
|
||||
financeOwner: '华南财务组',
|
||||
roles: ['使用者', '审批负责人'],
|
||||
status: '试用中',
|
||||
statusTone: 'warning',
|
||||
gender: '男',
|
||||
age: '29',
|
||||
birthDate: '1997-02-18',
|
||||
email: 'jia.chen@xfinance.com',
|
||||
phone: '137 1602 9901',
|
||||
joinDate: '2026-03-01',
|
||||
location: '深圳',
|
||||
costCenter: 'CC-4102',
|
||||
updatedAt: '2026-05-04 14:12',
|
||||
lastSync: '2026-05-04 14:12',
|
||||
syncState: '已同步',
|
||||
permissions: [
|
||||
'可发起个人报销与出差申请',
|
||||
'可处理本部门基础审批'
|
||||
],
|
||||
history: [
|
||||
{ action: '完成试用期角色初始化', owner: '组织管理员 · 王敏', time: '05-04 14:12' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'EMP-005',
|
||||
avatar: '赵',
|
||||
name: '赵雨辰',
|
||||
employeeNo: 'E11991',
|
||||
department: '研发中心',
|
||||
position: '产品经理',
|
||||
grade: 'P5',
|
||||
manager: '陈嘉',
|
||||
financeOwner: '总部财务',
|
||||
roles: ['使用者'],
|
||||
status: '停用',
|
||||
statusTone: 'neutral',
|
||||
gender: '男',
|
||||
age: '27',
|
||||
birthDate: '1999-06-09',
|
||||
email: 'yuchen.zhao@xfinance.com',
|
||||
phone: '135 1991 3300',
|
||||
joinDate: '2023-11-18',
|
||||
location: '北京',
|
||||
costCenter: 'CC-5209',
|
||||
updatedAt: '2026-05-01 11:06',
|
||||
lastSync: '2026-05-01 11:06',
|
||||
syncState: '已同步',
|
||||
permissions: [
|
||||
'当前账号停用,仅保留历史单据查看记录'
|
||||
],
|
||||
history: [
|
||||
{ action: '账号状态变更为停用', owner: '系统管理员 · 王敏', time: '05-01 11:06' }
|
||||
]
|
||||
}
|
||||
]
|
||||
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(() => {
|
||||
if (activeTab.value === '全部员工') return employees
|
||||
return employees.filter((item) => item.status === activeTab.value)
|
||||
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,
|
||||
filters,
|
||||
activeTab,
|
||||
selectedEmployee,
|
||||
roleOptions,
|
||||
employees,
|
||||
visibleEmployees
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user