feat: enhance employee CRUD with search, filters, and security module
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { fetchEmployeeMeta, fetchEmployees } from '../../services/employees.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { disableEmployee, fetchEmployeeMeta, fetchEmployees, updateEmployee } from '../../services/employees.js'
|
||||
|
||||
const DEFAULT_STATUS_TABS = ['全部员工', '在职', '试用中', '停用']
|
||||
const FALLBACK_ROLE_OPTIONS = [
|
||||
@@ -42,6 +43,92 @@ const FALLBACK_ROLE_OPTIONS = [
|
||||
}
|
||||
]
|
||||
|
||||
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) : ''
|
||||
}
|
||||
|
||||
function matchKeyword(employee, keyword) {
|
||||
if (!keyword) {
|
||||
return true
|
||||
@@ -114,8 +201,10 @@ export default {
|
||||
name: 'EmployeeManagementView',
|
||||
emits: ['overview-change'],
|
||||
setup(_, { emit }) {
|
||||
const { toast } = useToast()
|
||||
const activeTab = ref(DEFAULT_STATUS_TABS[0])
|
||||
const selectedEmployee = ref(null)
|
||||
const employeeForm = ref(createEmployeeForm())
|
||||
const roleOptions = ref([...FALLBACK_ROLE_OPTIONS])
|
||||
const employees = ref([])
|
||||
const searchKeyword = ref('')
|
||||
@@ -127,11 +216,26 @@ export default {
|
||||
const pageSize = ref(10)
|
||||
const pageSizes = [10, 20, 50]
|
||||
const pageSizeOpen = ref(false)
|
||||
const actionState = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const tabs = computed(() => buildStatusTabs(employees.value))
|
||||
const employeeSummary = computed(() => buildEmployeeSummary(employees.value))
|
||||
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 === '停用'
|
||||
)
|
||||
|
||||
const departmentOptions = computed(() =>
|
||||
uniqueSorted(employees.value.map((item) => item.department))
|
||||
@@ -211,6 +315,14 @@ export default {
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
selectedEmployee,
|
||||
(employee) => {
|
||||
employeeForm.value = buildEmployeeForm(employee)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(filteredEmployees, () => {
|
||||
currentPage.value = 1
|
||||
pageSizeOpen.value = false
|
||||
@@ -282,6 +394,167 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function disableEmployeeAccount() {
|
||||
if (!selectedEmployee.value || disableActionDisabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!window.confirm(`确认停用 ${selectedEmployee.value.name} 的账号吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'disable'
|
||||
|
||||
try {
|
||||
const updated = await disableEmployee(selectedEmployee.value.id)
|
||||
selectedEmployee.value = updated
|
||||
await loadEmployees()
|
||||
toast('员工账号已停用。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '停用账号失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmployees() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
@@ -339,6 +612,13 @@ export default {
|
||||
return {
|
||||
tabs,
|
||||
activeTab,
|
||||
employeeForm,
|
||||
detailAge,
|
||||
roleCount,
|
||||
selectedRoleLabels,
|
||||
actionState,
|
||||
actionBusy,
|
||||
disableActionDisabled,
|
||||
selectedEmployee,
|
||||
roleOptions,
|
||||
employees,
|
||||
@@ -360,6 +640,10 @@ export default {
|
||||
totalCount,
|
||||
totalPages,
|
||||
resetFilters,
|
||||
openEmployeeDetail,
|
||||
closeEmployeeDetail,
|
||||
saveEmployeeChanges,
|
||||
disableEmployeeAccount,
|
||||
changePageSize,
|
||||
togglePageSizeOpen,
|
||||
toggleFilterPopover,
|
||||
|
||||
Reference in New Issue
Block a user