feat: add employee management, backend health check, and UI improvements

This commit is contained in:
2026-05-07 11:50:10 +08:00
parent a5db09f41e
commit c00db75c13
59 changed files with 3926 additions and 5796 deletions

View File

@@ -0,0 +1,39 @@
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useBackendHealth } from '../../composables/useBackendHealth.js'
import { useSystemState } from '../../composables/useSystemState.js'
export default {
name: 'BackendUnavailableRouteView',
setup() {
const router = useRouter()
const { backendChecking, backendError, checkBackendHealth } = useBackendHealth()
const { loggedIn, resolveEntryRoute } = useSystemState()
const retrying = ref(false)
const statusMessage = computed(() => {
return backendError.value || '后端服务尚未就绪,请先检查 FastAPI 和数据库连接。'
})
async function retry() {
retrying.value = true
try {
const ok = await checkBackendHealth({ force: true })
if (ok) {
await router.replace(loggedIn.value ? resolveEntryRoute() : { name: 'login' })
}
} finally {
retrying.value = false
}
}
return {
backendChecking,
retrying,
statusMessage,
retry
}
}
}

View File

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