2026-05-07 11:50:10 +08:00
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
|
2026-05-13 03:35:44 +00:00
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
2026-05-07 13:48:00 +08:00
|
|
|
import { useToast } from '../../composables/useToast.js'
|
|
|
|
|
import { disableEmployee, fetchEmployeeMeta, fetchEmployees, updateEmployee } from '../../services/employees.js'
|
2026-05-07 11:50:10 +08:00
|
|
|
|
|
|
|
|
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 助手。'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
2026-05-07 13:48:00 +08:00
|
|
|
function createEmployeeForm() {
|
|
|
|
|
return {
|
|
|
|
|
name: '',
|
|
|
|
|
employeeNo: '',
|
|
|
|
|
gender: '',
|
|
|
|
|
birthDate: '',
|
|
|
|
|
phone: '',
|
|
|
|
|
email: '',
|
|
|
|
|
joinDate: '',
|
|
|
|
|
location: '',
|
|
|
|
|
position: '',
|
|
|
|
|
grade: '',
|
|
|
|
|
department: '',
|
|
|
|
|
manager: '',
|
|
|
|
|
financeOwner: '',
|
|
|
|
|
costCenter: '',
|
|
|
|
|
roleCodes: [],
|
|
|
|
|
password: ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildEmployeeForm(employee) {
|
|
|
|
|
if (!employee) {
|
|
|
|
|
return createEmployeeForm()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: employee.name || '',
|
|
|
|
|
employeeNo: employee.employeeNo || '',
|
|
|
|
|
gender: employee.gender || '',
|
|
|
|
|
birthDate: employee.birthDate || '',
|
|
|
|
|
phone: employee.phone || '',
|
|
|
|
|
email: employee.email || '',
|
|
|
|
|
joinDate: employee.joinDate || '',
|
|
|
|
|
location: employee.location || '',
|
|
|
|
|
position: employee.position || '',
|
|
|
|
|
grade: employee.grade || '',
|
|
|
|
|
department: employee.department || '',
|
|
|
|
|
manager: employee.manager || '',
|
|
|
|
|
financeOwner: employee.financeOwner || '',
|
|
|
|
|
costCenter: employee.costCenter || '',
|
|
|
|
|
roleCodes: [...(employee.roleCodes || [])],
|
|
|
|
|
password: ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeText(value) {
|
|
|
|
|
return String(value || '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeNullableText(value) {
|
|
|
|
|
const text = normalizeText(value)
|
|
|
|
|
return text || null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sameValues(left, right) {
|
|
|
|
|
if (left.length !== right.length) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return left.every((value, index) => value === right[index])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateAgeFromDate(dateString) {
|
|
|
|
|
if (!dateString) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const birthDate = new Date(`${dateString}T00:00:00`)
|
|
|
|
|
if (Number.isNaN(birthDate.getTime())) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const today = new Date()
|
|
|
|
|
let age = today.getFullYear() - birthDate.getFullYear()
|
|
|
|
|
const hasBirthdayPassed =
|
|
|
|
|
today.getMonth() > birthDate.getMonth() ||
|
|
|
|
|
(today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate())
|
|
|
|
|
|
|
|
|
|
if (!hasBirthdayPassed) {
|
|
|
|
|
age -= 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return age >= 0 ? String(age) : ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
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',
|
2026-05-13 03:35:44 +00:00
|
|
|
components: {
|
|
|
|
|
ConfirmDialog
|
|
|
|
|
},
|
2026-05-07 11:50:10 +08:00
|
|
|
emits: ['overview-change'],
|
|
|
|
|
setup(_, { emit }) {
|
2026-05-07 13:48:00 +08:00
|
|
|
const { toast } = useToast()
|
2026-05-07 11:50:10 +08:00
|
|
|
const activeTab = ref(DEFAULT_STATUS_TABS[0])
|
2026-05-06 11:00:38 +08:00
|
|
|
const selectedEmployee = ref(null)
|
2026-05-07 13:48:00 +08:00
|
|
|
const employeeForm = ref(createEmployeeForm())
|
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)
|
2026-05-07 13:48:00 +08:00
|
|
|
const actionState = ref('')
|
2026-05-07 11:50:10 +08:00
|
|
|
const loading = ref(false)
|
|
|
|
|
const errorMessage = ref('')
|
2026-05-13 03:35:44 +00:00
|
|
|
const disableDialogOpen = ref(false)
|
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))
|
2026-05-07 13:48:00 +08:00
|
|
|
const detailAge = computed(() => calculateAgeFromDate(employeeForm.value.birthDate))
|
|
|
|
|
const roleCount = computed(() => employeeForm.value.roleCodes.length)
|
|
|
|
|
const selectedRoleLabels = computed(() =>
|
|
|
|
|
roleOptions.value
|
|
|
|
|
.filter((role) => employeeForm.value.roleCodes.includes(role.code))
|
|
|
|
|
.map((role) => role.label)
|
|
|
|
|
)
|
|
|
|
|
const actionBusy = computed(() => actionState.value === 'save' || actionState.value === 'disable')
|
|
|
|
|
const disableActionDisabled = computed(
|
|
|
|
|
() =>
|
|
|
|
|
actionBusy.value ||
|
|
|
|
|
!selectedEmployee.value ||
|
|
|
|
|
selectedEmployee.value.status === '停用'
|
|
|
|
|
)
|
2026-05-07 11:50:10 +08:00
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-07 13:48:00 +08:00
|
|
|
watch(
|
|
|
|
|
selectedEmployee,
|
|
|
|
|
(employee) => {
|
|
|
|
|
employeeForm.value = buildEmployeeForm(employee)
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:48:00 +08:00
|
|
|
function openEmployeeDetail(employee) {
|
|
|
|
|
selectedEmployee.value = employee
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeEmployeeDetail() {
|
|
|
|
|
selectedEmployee.value = null
|
|
|
|
|
employeeForm.value = createEmployeeForm()
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildUpdatePayload() {
|
|
|
|
|
const current = selectedEmployee.value
|
|
|
|
|
const form = employeeForm.value
|
|
|
|
|
const payload = {}
|
|
|
|
|
|
|
|
|
|
if (!current) {
|
|
|
|
|
return payload
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextName = normalizeText(form.name)
|
|
|
|
|
if (nextName && nextName !== current.name) {
|
|
|
|
|
payload.name = nextName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextGender = normalizeNullableText(form.gender)
|
|
|
|
|
if (nextGender !== (current.gender || null)) {
|
|
|
|
|
payload.gender = nextGender
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextBirthDate = normalizeNullableText(form.birthDate)
|
|
|
|
|
if (nextBirthDate !== (current.birthDate || null)) {
|
|
|
|
|
payload.birth_date = nextBirthDate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextPhone = normalizeNullableText(form.phone)
|
|
|
|
|
if (nextPhone !== (current.phone || null)) {
|
|
|
|
|
payload.phone = nextPhone
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextEmail = normalizeText(form.email)
|
|
|
|
|
if (nextEmail && nextEmail !== current.email) {
|
|
|
|
|
payload.email = nextEmail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextJoinDate = normalizeNullableText(form.joinDate)
|
|
|
|
|
if (nextJoinDate !== (current.joinDate || null)) {
|
|
|
|
|
payload.join_date = nextJoinDate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextLocation = normalizeNullableText(form.location)
|
|
|
|
|
if (nextLocation !== (current.location || null)) {
|
|
|
|
|
payload.location = nextLocation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextPosition = normalizeText(form.position)
|
|
|
|
|
if (nextPosition && nextPosition !== current.position) {
|
|
|
|
|
payload.position = nextPosition
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextGrade = normalizeText(form.grade)
|
|
|
|
|
if (nextGrade && nextGrade !== current.grade) {
|
|
|
|
|
payload.grade = nextGrade
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextFinanceOwner = normalizeNullableText(form.financeOwner)
|
|
|
|
|
if (nextFinanceOwner !== (current.financeOwner || null)) {
|
|
|
|
|
payload.finance_owner_name = nextFinanceOwner
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextCostCenter = normalizeNullableText(form.costCenter)
|
|
|
|
|
if (nextCostCenter !== (current.costCenter || null)) {
|
|
|
|
|
payload.cost_center = nextCostCenter
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextRoleCodes = [...form.roleCodes].sort()
|
|
|
|
|
const currentRoleCodes = [...(current.roleCodes || [])].sort()
|
|
|
|
|
if (!sameValues(nextRoleCodes, currentRoleCodes)) {
|
|
|
|
|
payload.role_codes = form.roleCodes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextPassword = normalizeText(form.password)
|
|
|
|
|
if (nextPassword) {
|
|
|
|
|
payload.password = nextPassword
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveEmployeeChanges() {
|
|
|
|
|
if (!selectedEmployee.value || actionBusy.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!normalizeText(employeeForm.value.name)) {
|
|
|
|
|
toast('员工姓名不能为空。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!normalizeText(employeeForm.value.email)) {
|
|
|
|
|
toast('邮箱不能为空。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!normalizeText(employeeForm.value.position)) {
|
|
|
|
|
toast('岗位不能为空。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!normalizeText(employeeForm.value.grade)) {
|
|
|
|
|
toast('职级不能为空。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (normalizeText(employeeForm.value.password) && normalizeText(employeeForm.value.password).length < 5) {
|
|
|
|
|
toast('员工密码至少需要 5 位。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = buildUpdatePayload()
|
|
|
|
|
if (!Object.keys(payload).length) {
|
|
|
|
|
toast('未检测到需要保存的变更。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionState.value = 'save'
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const updated = await updateEmployee(selectedEmployee.value.id, payload)
|
|
|
|
|
selectedEmployee.value = updated
|
|
|
|
|
await loadEmployees()
|
|
|
|
|
toast('员工信息已保存并生效。')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast(error?.message || '员工信息保存失败,请稍后重试。')
|
|
|
|
|
} finally {
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 03:35:44 +00:00
|
|
|
function disableEmployeeAccount() {
|
2026-05-07 13:48:00 +08:00
|
|
|
if (!selectedEmployee.value || disableActionDisabled.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 03:35:44 +00:00
|
|
|
disableDialogOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeDisableDialog() {
|
|
|
|
|
if (actionState.value === 'disable') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disableDialogOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function confirmDisableEmployeeAccount() {
|
|
|
|
|
if (!selectedEmployee.value || disableActionDisabled.value) {
|
2026-05-07 13:48:00 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionState.value = 'disable'
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const updated = await disableEmployee(selectedEmployee.value.id)
|
2026-05-13 03:35:44 +00:00
|
|
|
disableDialogOpen.value = false
|
2026-05-07 13:48:00 +08:00
|
|
|
selectedEmployee.value = updated
|
|
|
|
|
await loadEmployees()
|
|
|
|
|
toast('员工账号已停用。')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast(error?.message || '停用账号失败,请稍后重试。')
|
|
|
|
|
} finally {
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
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,
|
2026-05-07 13:48:00 +08:00
|
|
|
employeeForm,
|
|
|
|
|
detailAge,
|
|
|
|
|
roleCount,
|
|
|
|
|
selectedRoleLabels,
|
|
|
|
|
actionState,
|
|
|
|
|
actionBusy,
|
|
|
|
|
disableActionDisabled,
|
2026-05-06 11:00:38 +08:00
|
|
|
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,
|
2026-05-07 13:48:00 +08:00
|
|
|
openEmployeeDetail,
|
|
|
|
|
closeEmployeeDetail,
|
2026-05-13 03:35:44 +00:00
|
|
|
closeDisableDialog,
|
|
|
|
|
confirmDisableEmployeeAccount,
|
2026-05-07 13:48:00 +08:00
|
|
|
saveEmployeeChanges,
|
2026-05-13 03:35:44 +00:00
|
|
|
disableDialogOpen,
|
2026-05-07 13:48:00 +08:00
|
|
|
disableEmployeeAccount,
|
2026-05-07 11:50:10 +08:00
|
|
|
changePageSize,
|
|
|
|
|
togglePageSizeOpen,
|
|
|
|
|
toggleFilterPopover,
|
|
|
|
|
closeFilterPopover,
|
|
|
|
|
selectFilter,
|
|
|
|
|
loading,
|
|
|
|
|
errorMessage,
|
|
|
|
|
loadEmployees
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|