feat(hrm): 前端同步新疆中亿装配的人力资源模块
配套后端提交 52feee3e。新增加班工资配置、合同附件、薪资计算表单等页面,
调整考勤/员工/薪资/离职/调岗等页面以匹配新接口与新字段
(员工类别、考勤规则生效区间、社保大病医疗/长期护理比例等)。
| | |
| | | export interface Holiday { |
| | | id?: number; |
| | | name?: string; |
| | | /** å¼å§æ¥æ */ |
| | | startDate?: string; |
| | | /** ç»ææ¥æ */ |
| | | endDate?: string; |
| | | date?: string; |
| | | type?: number; |
| | | typeName?: string; |
| | | isLegal?: number; |
| | |
| | | lateAllowMinutes?: number; |
| | | earlyAllowMinutes?: number; |
| | | overtimeThreshold?: number; |
| | | effectiveStartDate?: string; |
| | | effectiveEndDate?: string; |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | |
| | | { file, updateSupport }, |
| | | ); |
| | | } |
| | | |
| | | /** çæå¹¶ä¸è½½ååææ¡£ï¼docxï¼ |
| | | * @param templateType 1-å³å¨åå(ç®ç) 2-å³å¨åå(æ å) 3-å³å¡åå |
| | | */ |
| | | export function generateContractDocx(id: number, templateType: number) { |
| | | return requestClient.download( |
| | | `/hrm/employee-contract/generate-docx?id=${id}&templateType=${templateType}`, |
| | | ); |
| | | } |
| | | |
| | | /** çæååå¹¶åå为ååéä»¶ï¼è¿å blobId */ |
| | | export function generateContractArchive(id: number, templateType: number) { |
| | | return requestClient.post<number>( |
| | | `/hrm/employee-contract/generate?id=${id}&templateType=${templateType}`, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½ååç©ºç½æ¨¡æ¿ |
| | | * @param type 1-å³å¨åå(ç®ç) 2-å³å¨åå(æ å) 3-å³å¡åå 4-éä¼è¿èåå |
| | | */ |
| | | export function downloadContractTemplate(type: number) { |
| | | return requestClient.download( |
| | | `/hrm/employee-contract/download-template?type=${type}`, |
| | | ); |
| | | } |
| | |
| | | regularDate?: string; |
| | | leaveDate?: string; |
| | | employeeStatus?: number; |
| | | /** å工类å«ï¼1-æ®éåå·¥ï¼2-æå·¥ï¼3-管ç */ |
| | | employeeCategory?: number; |
| | | annualLeaveBalance?: number; |
| | | sickLeaveBalance?: number; |
| | | salaryStructureId?: number; |
| | |
| | | workHistories?: WorkHistory[]; |
| | | /** ç´§æ¥è系人å表 */ |
| | | emergencyContacts?: EmergencyContact[]; |
| | | /** éä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | blobIds?: number[]; |
| | | /** éä»¶å表ï¼è¯¦æ
æ¶è¿åï¼ */ |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; |
| | | } |
| | | |
| | | /** æè²ç»å */ |
| | |
| | | id: number; |
| | | employeeNo: string; |
| | | name: string; |
| | | /** é¨é¨ID */ |
| | | deptId?: number; |
| | | deptName?: string; |
| | | /** å²ä½IDå表 */ |
| | | postIds?: number[]; |
| | | postName?: string; |
| | | employeeStatus?: number; |
| | | } |
| | | |
| | | /** å工导å
¥ç»æ */ |
| | |
| | | export function deleteEmergencyContact(id: number) { |
| | | return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`); |
| | | } |
| | | |
| | | /** å工转æ£ï¼ä»
å³å¨ååå¯è½¬æ£ï¼ */ |
| | | export function regularizeEmployee(id: number) { |
| | | return requestClient.put(`/hrm/employee/regular?id=${id}`); |
| | | } |
| | | |
| | | /** å¾
è½¬æ£æéå表ï¼è¯ç¨ä¸ãå³å¨ååãè¯ç¨æå³å°å°æï¼ */ |
| | | export function getRegularizeReminders(days = 7) { |
| | | return requestClient.get<HrmEmployeeApi.Employee[]>( |
| | | `/hrm/employee/regularize-reminders?days=${days}`, |
| | | ); |
| | | } |
| | |
| | | |
| | | /** æäº¤ç¦»èç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitResignation(id: number) { |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}&processDefinitionKey=hrm_resignation_approve`); |
| | | return requestClient.post(`/hrm/resignation/submit?id=${id}`); |
| | | } |
| | | |
| | | /** æ¤é离èç³è¯· */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmOvertimeWageApi { |
| | | /** å²ä½å çå·¥èµé
ç½® */ |
| | | export interface OvertimeWageConfig { |
| | | id?: number; |
| | | /** å²ä½ID */ |
| | | postId?: number; |
| | | /** å²ä½åç§° */ |
| | | postName?: string; |
| | | /** å çæ¶èªï¼å
/å°æ¶ï¼ï¼é»è®¤ 20 */ |
| | | hourlyRate?: number; |
| | | /** æ¯ææå¤§å çæ¶é¿ä¸éï¼å°æ¶ï¼ï¼ä¸ºç©ºè¡¨ç¤ºä¸é */ |
| | | maxOvertimeHours?: number; |
| | | /** ç¶æï¼0-å¯ç¨ï¼1-ç¦ç¨ */ |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** æ¥è¯¢åæ° */ |
| | | export interface OvertimeWageConfigQueryParam { |
| | | postId?: number; |
| | | status?: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å²ä½å çå·¥èµé
ç½®å表 */ |
| | | export function getOvertimeWageConfigList( |
| | | params?: HrmOvertimeWageApi.OvertimeWageConfigQueryParam, |
| | | ) { |
| | | return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig[]>( |
| | | '/hrm/salary/overtime-wage/list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å²ä½å çå·¥èµé
置详æ
*/ |
| | | export function getOvertimeWageConfig(id: number) { |
| | | return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig>( |
| | | `/hrm/salary/overtime-wage/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢å²ä½å çå·¥èµé
ç½® */ |
| | | export function createOvertimeWageConfig( |
| | | data: HrmOvertimeWageApi.OvertimeWageConfig, |
| | | ) { |
| | | return requestClient.post('/hrm/salary/overtime-wage/create', data); |
| | | } |
| | | |
| | | /** æ´æ°å²ä½å çå·¥èµé
ç½® */ |
| | | export function updateOvertimeWageConfig( |
| | | data: HrmOvertimeWageApi.OvertimeWageConfig, |
| | | ) { |
| | | return requestClient.put('/hrm/salary/overtime-wage/update', data); |
| | | } |
| | | |
| | | /** å é¤å²ä½å çå·¥èµé
ç½® */ |
| | | export function deleteOvertimeWageConfig(id: number) { |
| | | return requestClient.delete(`/hrm/salary/overtime-wage/delete?id=${id}`); |
| | | } |
| | |
| | | unemploymentPersonalRatio?: number; |
| | | workInjuryRatio?: number; |
| | | maternityRatio?: number; |
| | | /** 大ç
å»çä¼ä¸ç¼´çº³æ¯ä¾ */ |
| | | seriousIllnessCompanyRatio?: number; |
| | | /** 大ç
å»ç个人缴纳æ¯ä¾ */ |
| | | seriousIllnessPersonalRatio?: number; |
| | | /** é¿ææ¤çä¼ä¸ç¼´çº³æ¯ä¾ */ |
| | | longTermCareCompanyRatio?: number; |
| | | /** é¿ææ¤ç个人缴纳æ¯ä¾ */ |
| | | longTermCarePersonalRatio?: number; |
| | | housingFundCompanyRatio?: number; |
| | | housingFundPersonalRatio?: number; |
| | | status?: number; |
| | |
| | | /** è°å²ç³è¯· */ |
| | | export interface Transfer { |
| | | id?: number; |
| | | employeeId?: number; |
| | | employeeName?: string; |
| | | /** è°å²äººï¼åå·¥IDï¼ */ |
| | | userId?: number; |
| | | /** è°å²äººå§å */ |
| | | userName?: string; |
| | | currentDeptId?: number; |
| | | currentDeptName?: string; |
| | | currentPostId?: number; |
| | |
| | | |
| | | /** æäº¤è°å²ç³è¯·ï¼å起审æ¹ï¼ */ |
| | | export function submitTransfer(id: number) { |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}&processDefinitionKey=hrm_transfer_approve`); |
| | | return requestClient.post(`/hrm/transfer/submit?id=${id}`); |
| | | } |
| | | |
| | | /** æ¤éè°å²ç³è¯· */ |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime, formatDate } from '@vben/utils'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception'; |
| | | |
| | |
| | | |
| | | /** æäº¤èå¤å¼å¸¸ç³è¯· */ |
| | | async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) { |
| | | const res = automaticApproval.value ? [] : await getAttendanceExceptionApproveProcessList(); |
| | | const res = await getAttendanceExceptionApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_attendance_exception'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitAttendanceException(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'dateRange', |
| | | label: 'æ¥æåºé´', |
| | | component: 'RangePicker', |
| | | fieldName: 'date', |
| | | label: 'æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'éæ©æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | |
| | | export function useGridColumns(): VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>['columns'] { |
| | | return [ |
| | | { field: 'name', title: 'è忥åç§°', minWidth: 150 }, |
| | | { |
| | | field: 'startDate', |
| | | title: 'æ¥æåºé´', |
| | | minWidth: 200, |
| | | slots: { |
| | | default: 'dateRange', |
| | | }, |
| | | }, |
| | | { field: 'date', title: 'æ¥æ', width: 120 }, |
| | | { |
| | | field: 'type', |
| | | title: 'ç±»å', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #dateRange="{ row }"> |
| | | <span v-if="!row.startDate && !row.endDate">-</span> |
| | | <span v-else-if="row.startDate === row.endDate">{{ row.startDate }}</span> |
| | | <span v-else>{{ row.startDate }} ~ {{ row.endDate }}</span> |
| | | </template> |
| | | <template #isLegal="{ row }"> |
| | | <Tag v-if="row.isLegal === 1" color="red">æ³å®</Tag> |
| | | <span v-else>-</span> |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const values = (await formApi.getValues()) as HrmAttendanceHolidayApi.Holiday & { |
| | | dateRange?: string[]; |
| | | }; |
| | | const { dateRange, ...rest } = values; |
| | | const data: HrmAttendanceHolidayApi.Holiday = { |
| | | ...rest, |
| | | startDate: dateRange?.[0], |
| | | endDate: dateRange?.[1], |
| | | }; |
| | | const data = (await formApi.getValues()) as HrmAttendanceHolidayApi.Holiday; |
| | | try { |
| | | if (formType.value === 'update') { |
| | | await updateAttendanceHoliday(data); |
| | |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const detail = await getAttendanceHoliday(data.id); |
| | | await formApi.setValues({ |
| | | ...detail, |
| | | dateRange: |
| | | detail.startDate && detail.endDate |
| | | ? [detail.startDate, detail.endDate] |
| | | : undefined, |
| | | }); |
| | | await formApi.setValues(await getAttendanceHoliday(data.id)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | emit('success'); |
| | | // å端è¿å导å
¥ç»æï¼å«å¤±è´¥åå ï¼ä¾å¦â以ä¸åå·¥ä¸åå¨...ï¼éè¡¥å
ææºå·...âï¼ |
| | | if (res && res.length > 0) { |
| | | // 失败æ¶åç«¯ä¼æ¼ä¸ãæªè½å¯¼å
¥ï¼ãæç»æ®µï¼ç¨å®å¤å®ï¼é¿å
æåææ¡éåºç°ãæªã就误æ¥è¦å |
| | | message[res.includes('æªè½å¯¼å
¥') ? 'warning' : 'success'](res); |
| | | message[res.includes('æª') || res.includes('ä¸å®æ´') ? 'warning' : 'success'](res); |
| | | } else { |
| | | message.success('导å
¥æå'); |
| | | } |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'effectiveStartDate', |
| | | label: 'çæå¼å§æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'ä¸é表示ä¸é', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'effectiveEndDate', |
| | | label: 'çæç»ææ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'ä¸éè¡¨ç¤ºé¿æææ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | |
| | | { field: 'earlyAllowMinutes', title: 'æ©éå
许(åé)', width: 120 }, |
| | | { field: 'overtimeThreshold', title: 'å çèµ·ç®(å°æ¶)', width: 120 }, |
| | | { |
| | | field: 'effectiveStartDate', |
| | | title: 'çæåºé´', |
| | | minWidth: 200, |
| | | formatter: ({ row }) => |
| | | `${row.effectiveStartDate || 'ä¸é'} ~ ${row.effectiveEndDate || 'ä¸é'}`, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 80, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { Button, Dropdown, Menu, message, Modal, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteEmployeeContract, |
| | | downloadContractTemplate, |
| | | generateContractArchive, |
| | | generateContractDocx, |
| | | getEmployeeContractPage, |
| | | } from '#/api/hrm/employee/contract'; |
| | | import { $t } from '#/locales'; |
| | |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | import Terminate from './modules/terminate.vue'; |
| | | import Attachment from './modules/attachment-modal.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | |
| | | const [TerminateModal, terminateModalApi] = useVbenModal({ |
| | | connectedComponent: Terminate, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [AttachmentModal, attachmentModalApi] = useVbenModal({ |
| | | connectedComponent: Attachment, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** ä¸è½½ååç©ºç½æ¨¡æ¿ï¼type 1-å³å¨ååç®ç 2-å³å¨ååæ å 3-å³å¡åå */ |
| | | const TEMPLATE_NAMES: Record<number, string> = { |
| | | 1: 'å³å¨ååï¼ç®çï¼.docx', |
| | | 2: 'å³å¨ååï¼æ åï¼.docx', |
| | | 3: 'å³å¡åå.doc', |
| | | }; |
| | | async function handleDownloadTemplate(type: number) { |
| | | const data = await downloadContractTemplate(type); |
| | | downloadFileFromBlobPart({ |
| | | fileName: TEMPLATE_NAMES[type] || 'å忍¡æ¿', |
| | | source: data, |
| | | }); |
| | | message.success('模æ¿å·²ä¸è½½'); |
| | | } |
| | | |
| | | /** ç¼è¾åå */ |
| | | function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | |
| | | /** è§£é¤/ç»æ¢åå */ |
| | | function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | terminateModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çååéä»¶ */ |
| | | function handleAttachments(row: HrmEmployeeContractApi.EmployeeContract) { |
| | | attachmentModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** çæååï¼docxï¼ï¼templateType 1-å³å¨ååç®ç 2-å³å¨ååæ å 3-å³å¡åå */ |
| | | async function handleGenerate( |
| | | row: HrmEmployeeContractApi.EmployeeContract, |
| | | templateType: number, |
| | | ) { |
| | | // 1. çæå¹¶åå为ååéä»¶ |
| | | await generateContractArchive(row.id!, templateType); |
| | | // 2. åæ¶ä¸è½½ä¸ä»½ |
| | | const data = await generateContractDocx(row.id!, templateType); |
| | | const genName = |
| | | { 1: 'å³å¨åå(ç®ç).docx', 2: 'å³å¨åå(æ å).docx', 3: 'å³å¡åå.docx' }[ |
| | | templateType |
| | | ] || 'åå.docx'; |
| | | downloadFileFromBlobPart({ |
| | | fileName: `${row.contractNo || row.id}-${genName}`, |
| | | source: data, |
| | | }); |
| | | message.success('ååå·²çæï¼å¹¶å½æ¡£å°è¯¥ååéä»¶'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** âæ´å¤âèåï¼ä¿®æ¹ / ç»ç¾ / è§£é¤ç»æ¢ / å é¤ */ |
| | | function handleMore({ key }: { key: number | string }, row: HrmEmployeeContractApi.EmployeeContract) { |
| | | switch (key) { |
| | | case 'edit': { |
| | | handleEdit(row); |
| | | break; |
| | | } |
| | | case 'renew': { |
| | | handleRenew(row); |
| | | break; |
| | | } |
| | | case 'terminate': { |
| | | handleTerminate(row); |
| | | break; |
| | | } |
| | | case 'delete': { |
| | | Modal.confirm({ |
| | | title: `确认å é¤ååã${row.contractNo || row.id}ãåï¼`, |
| | | okText: 'å é¤', |
| | | okType: 'danger', |
| | | cancelText: 'åæ¶', |
| | | onOk: () => handleDelete(row), |
| | | }); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** å é¤åå */ |
| | |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <TerminateModal @success="handleRefresh" /> |
| | | <AttachmentModal /> |
| | | <Grid table-title="åå·¥ååå表"> |
| | | <template #parentNo="{ row }"> |
| | | <span>{{ row.parentNo || '-' }}</span> |
| | |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #toolbar-tools> |
| | | <Dropdown> |
| | | <Button class="mr-2" type="primary"> |
| | | ä¸è½½å忍¡æ¿ <IconifyIcon class="size-3" icon="lucide:chevron-down" /> |
| | | </Button> |
| | | <template #overlay> |
| | | <Menu @click="({ key }) => handleDownloadTemplate(Number(key))"> |
| | | <Menu.Item key="1">å³å¨ååï¼ç®çï¼</Menu.Item> |
| | | <Menu.Item key="2">å³å¨ååï¼æ åï¼</Menu.Item> |
| | | <Menu.Item key="3">å³å¡åå</Menu.Item> |
| | | <Menu.Item key="4">éä¼è¿èåå</Menu.Item> |
| | | </Menu> |
| | | </template> |
| | | </Dropdown> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç»ç¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:employee-contract:create'], |
| | | ifShow: isTerminable(row), |
| | | onClick: handleRenew.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:employee-contract:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'è§£é¤/ç»æ¢', |
| | | type: 'link', |
| | | icon: ACTION_ICON.CLOSE, |
| | | auth: ['hrm:employee-contract:update'], |
| | | ifShow: isTerminable(row), |
| | | onClick: handleTerminate.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:employee-contract:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['åå·¥åå']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | <Dropdown> |
| | | <a class="text-primary" @click.prevent>çæåå <IconifyIcon class="size-3" icon="lucide:chevron-down" /></a> |
| | | <template #overlay> |
| | | <Menu @click="({ key }) => handleGenerate(row, Number(key))"> |
| | | <Menu.Item key="1">å³å¨ååï¼ç®çï¼</Menu.Item> |
| | | <Menu.Item key="2">å³å¨ååï¼æ åï¼</Menu.Item> |
| | | <Menu.Item key="3">å³å¡åå</Menu.Item> |
| | | </Menu> |
| | | </template> |
| | | </Dropdown> |
| | | <a class="ml-3 text-primary" @click="handleAttachments(row)">éä»¶</a> |
| | | <Dropdown> |
| | | <a class="ml-3 text-primary" @click.prevent>æ´å¤ <IconifyIcon class="size-3" icon="lucide:chevron-down" /></a> |
| | | <template #overlay> |
| | | <Menu @click="(e) => handleMore(e, row)"> |
| | | <Menu.Item key="edit">ä¿®æ¹</Menu.Item> |
| | | <Menu.Item v-if="isTerminable(row)" key="renew">ç»ç¾</Menu.Item> |
| | | <Menu.Item v-if="isTerminable(row)" key="terminate">è§£é¤/ç»æ¢</Menu.Item> |
| | | <Menu.Divider /> |
| | | <Menu.Item danger key="delete">å é¤</Menu.Item> |
| | | </Menu> |
| | | </template> |
| | | </Dropdown> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { getEmployeeContract } from '#/api/hrm/employee/contract'; |
| | | |
| | | const attachments = ref<HrmEmployeeContractApi.AttachmentItem[]>([]); |
| | | const loading = ref(false); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id?: number }>(); |
| | | if (!data?.id) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const detail = await getEmployeeContract(data.id); |
| | | attachments.value = detail.attachmentList || []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ååéä»¶" class="w-[560px]"> |
| | | <div v-if="loading" class="py-6 text-center text-gray-400">å è½½ä¸...</div> |
| | | <div v-else-if="attachments.length === 0" class="py-6 text-center text-gray-400"> |
| | | ææ éä»¶ï¼å¯å¨ãæ´å¤ â ä¿®æ¹ãéä¸ä¼ ï¼æç¨ãçæååãçæåèªå¨å½æ¡£ï¼ |
| | | </div> |
| | | <ul v-else class="max-h-[50vh] overflow-auto pl-5"> |
| | | <li v-for="f in attachments" :key="f.id" class="mb-2 list-disc"> |
| | | <a :href="f.url" target="_blank" rel="noopener" class="text-primary"> |
| | | {{ f.name || `éä»¶${f.id}` }} |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | </Modal> |
| | | </template> |
| | |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'employeeCategory', |
| | | label: 'å工类å«', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'), |
| | | }, |
| | | rules: z.number().default(1), |
| | | help: 'æ®éåå·¥æå²ä½å çæ¶èªï¼æå·¥å çæ 1:1ï¼ç®¡çä¸è®¡å çå·¥èµ', |
| | | }, |
| | | { |
| | | fieldName: 'birthday', |
| | | label: 'åºçæ¥æ', |
| | | component: 'DatePicker', |
| | |
| | | placeholder: '请è¾å
¥å±
ä½å°å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | help: 'å¯ä¸ä¼ 身份è¯ãå¦åè¯ä¹¦ã使£æ¥åçèµæä½ä¸ºçæ¡£', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | placeholder: 'è¯·éæ©åå·¥ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'employeeCategory', |
| | | label: 'å工类å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'), |
| | | placeholder: 'è¯·éæ©å工类å«', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | default: 'employeeStatus', |
| | | }, |
| | | }, |
| | | { |
| | | field: 'employeeCategory', |
| | | title: 'å工类å«', |
| | | width: 100, |
| | | slots: { |
| | | default: 'employeeCategory', |
| | | }, |
| | | }, |
| | | { field: 'hireDate', title: 'å
¥èæ¥æ', width: 110 }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Tag } from 'ant-design-vue'; |
| | | import { Button, message, Progress, Tag, Alert } from 'ant-design-vue'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteEmployee, exportEmployee, getEmployeePage } from '#/api/hrm/employee'; |
| | | import { deleteEmployee, exportEmployee, getEmployeePage, getRegularizeReminders, regularizeEmployee } from '#/api/hrm/employee'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** å¾
è½¬æ£æéæ° */ |
| | | const regularizeCount = ref(0); |
| | | async function refreshRegularizeReminders() { |
| | | try { |
| | | const list = await getRegularizeReminders(7); |
| | | regularizeCount.value = list?.length ?? 0; |
| | | } catch { |
| | | regularizeCount.value = 0; |
| | | } |
| | | } |
| | | onMounted(refreshRegularizeReminders); |
| | | |
| | | /** å工转æ£ï¼ä»
å³å¨ååå¯è½¬æ£ï¼ */ |
| | | async function handleRegularize(row: HrmEmployeeApi.Employee) { |
| | | await regularizeEmployee(row.id!); |
| | | message.success(`ã${row.name}ã已转æ£`); |
| | | handleRefresh(); |
| | | refreshRegularizeReminders(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <Page auto-content-height> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Alert |
| | | v-if="regularizeCount > 0" |
| | | class="mb-3" |
| | | :message="`å¾
è½¬æ£æéï¼æ ${regularizeCount} ååå·¥è¯ç¨æå³å°å°æï¼è¯·åæ¶è½¬æ£`" |
| | | show-icon |
| | | type="warning" |
| | | /> |
| | | <Grid table-title="åå·¥å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_STATUS, row.employeeStatus) }} |
| | | </Tag> |
| | | </template> |
| | | <template #employeeCategory="{ row }"> |
| | | <Tag :color="row.employeeCategory === 2 ? 'orange' : row.employeeCategory === 3 ? 'purple' : 'blue'"> |
| | | {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, row.employeeCategory) }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '转æ£', |
| | | type: 'link', |
| | | auth: ['hrm:employee:update'], |
| | | ifShow: () => row.employeeStatus === 2, |
| | | popConfirm: { |
| | | title: `确认å°ã${row.name}ã转æ£åï¼ï¼ä»
å³å¨ååå¯è½¬æ£ï¼ä¸å«è°å²ï¼`, |
| | | confirm: handleRegularize.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | <DescriptionsItem label="å±
ä½å°å" :span="2">{{ employee.address || '-' }}</DescriptionsItem> |
| | | </Descriptions> |
| | | |
| | | <!-- éä»¶çæ¡£ --> |
| | | <Descriptions title="éä»¶çæ¡£" bordered :column="1" class="mb-4"> |
| | | <DescriptionsItem label="éä»¶"> |
| | | <template v-if="employee.attachmentList && employee.attachmentList.length"> |
| | | <div v-for="file in employee.attachmentList" :key="file.id" class="mb-1"> |
| | | <a :href="file.url" target="_blank" rel="noopener"> |
| | | {{ file.name || `éä»¶${file.id}` }} |
| | | </a> |
| | | </div> |
| | | </template> |
| | | <span v-else>-</span> |
| | | </DescriptionsItem> |
| | | </Descriptions> |
| | | |
| | | <!-- 夿³¨ --> |
| | | <Descriptions title="å
¶ä»ä¿¡æ¯" bordered :column="1" class="mb-4"> |
| | | <DescriptionsItem label="夿³¨">{{ employee.remark || '-' }}</DescriptionsItem> |
| | |
| | | // æäº¤è¡¨å |
| | | const data = (await formApi.getValues()) as HrmEmployeeApi.Employee; |
| | | data.remark = remark.value; |
| | | // éä»¶ï¼æ FileUpload çå¼è½¬æ blobId æ°ç» |
| | | if (Array.isArray(data.blobIds)) { |
| | | data.blobIds = (data.blobIds as any[]).map((item) => item?.id ?? item); |
| | | } |
| | | // è·ååè¡¨æ°æ® |
| | | data.educations = educationListRef.value?.getData(); |
| | | data.workHistories = workHistoryListRef.value?.getData(); |
| | |
| | | employeeData.value = formData; |
| | | remark.value = formData.remark; |
| | | if (data.formType !== 'detail') { |
| | | await formApi.setValues(formData); |
| | | const blobIds = (formData.attachmentList || []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...formData, blobIds }); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime, formatDate } from '@vben/utils'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmLeaveApi } from '#/api/hrm/leave'; |
| | | |
| | |
| | | |
| | | /** æäº¤è¯·åç³è¯·ï¼å
éæ©å®¡æ¹æµç¨ï¼ */ |
| | | async function handleSubmit(row: HrmLeaveApi.Leave) { |
| | | processList.value = automaticApproval.value ? [] : await getLeaveApproveProcessDefinitionList(); |
| | | processList.value = await getLeaveApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_leave'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDate } from '@vben/utils'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | field: 'status', |
| | | render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }), |
| | | }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: (value) => formatDate(value) as string }, |
| | | { label: 'å建æ¶é´', field: 'createTime', render: (value) => formatDateTime(value) as string }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | |
| | | <script lang="ts" setup> |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeApi } from '#/api/hrm/overtime'; |
| | | |
| | |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: HrmOvertimeApi.Overtime) { |
| | | processList.value = automaticApproval.value ? [] : await getOvertimeApproveProcessDefinitionList(); |
| | | processList.value = await getOvertimeApproveProcessDefinitionList(); |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'hrm_overtime'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitOvertime(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | message.success('å·²æäº¤è³åååå
¬å®¡æ¹'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDate } from '@vben/utils'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | { |
| | | label: 'é¢è®¡ç¦»èæ¥æ', |
| | | field: 'expectedDate', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '离èåå ', field: 'reason', render: textRender }, |
| | | { label: '交æ¥äºº', field: 'handoverUserName', render: textRender }, |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | { |
| | | label: '交æ¥', |
| | | type: 'link', |
| | | auth: ['hrm:resignation:update'], |
| | | auth: ['hrm:resignation:handover'], |
| | | ifShow: row.status === 20 && row.handoverStatus !== 20, |
| | | popConfirm: { |
| | | title: row.handoverStatus === 0 ? '确认å¼å§äº¤æ¥åï¼' : 'ç¡®è®¤å®æäº¤æ¥åï¼', |
| | |
| | | { field: 'period', title: 'æ ¸ç®å¨æ', width: 100 }, |
| | | { field: 'userName', title: 'åå·¥å§å', minWidth: 100 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'baseSalary', title: 'åºæ¬å·¥èµ', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'performanceSalary', title: '绩æå·¥èµ', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'overtimePay', title: 'å çè´¹', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'mealAllowance', title: 'é¤è¡¥', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'transportAllowance', title: '交é补贴', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'otherAllowance', title: 'å
¶ä»è¡¥è´´', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'socialSecurityDeduction', title: 'ç¤¾ä¿æ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'housingFundDeduction', title: 'å
¬ç§¯éæ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'taxDeduction', title: 'ä¸ªç¨æ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'otherDeduction', title: 'å
¶ä»æ£æ¬¾', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'leaveDeduction', title: 'è¯·åæ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'actualSalary', title: 'å®åå·¥èµ', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'workDays', title: 'åºå¤å¤©æ°', width: 80 }, |
| | | { field: 'overtimeHours', title: 'å çæ¶é¿(h)', width: 100 }, |
| | | { field: 'totalWorkHours', title: 'æçå·¥æ¶(h)', width: 100 }, |
| | | { field: 'baseSalary', title: 'åºæ¬å·¥èµ', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'performanceSalary', title: '绩æå·¥èµ', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'overtimePay', title: 'å çè´¹', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'mealAllowance', title: 'é¤è¡¥', width: 110, slots: { default: 'editableCell' } }, |
| | | { field: 'transportAllowance', title: '交é补贴', width: 110, slots: { default: 'editableCell' } }, |
| | | { field: 'otherAllowance', title: 'å
¶ä»è¡¥è´´', width: 110, slots: { default: 'editableCell' } }, |
| | | { field: 'socialSecurityDeduction', title: 'ç¤¾ä¿æ£æ¬¾', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'housingFundDeduction', title: 'å
¬ç§¯éæ£æ¬¾', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'taxDeduction', title: 'ä¸ªç¨æ£æ¬¾', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'otherDeduction', title: 'å
¶ä»æ£æ¬¾', width: 110, slots: { default: 'editableCell' } }, |
| | | { field: 'leaveDeduction', title: 'è¯·åæ£æ¬¾', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'actualSalary', title: 'å®åå·¥èµ', width: 110, formatter: 'formatAmount2' }, |
| | | { field: 'workDays', title: 'åºå¤å¤©æ°', width: 110, slots: { default: 'editableCell' } }, |
| | | { field: 'overtimeHours', title: 'å çæ¶é¿(h)', width: 120, slots: { default: 'editableCell' } }, |
| | | { field: 'totalWorkHours', title: 'æçå·¥æ¶(h)', width: 110 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 120, |
| | | width: 220, |
| | | fixed: 'right', |
| | | slots: { |
| | | default: 'actions', |
| | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert, Tag } from 'ant-design-vue'; |
| | | import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, Input, InputNumber, Alert, Tag } from 'ant-design-vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | |
| | | previewSalaryCalculation, |
| | | revokeSalaryCalculation, |
| | | saveSalaryCalculation, |
| | | updateSalaryCalculation, |
| | | } from '#/api/hrm/salary/calculation'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema, usePreviewColumns } from './data'; |
| | | import FormModal from './modules/form.vue'; |
| | | |
| | | // æ ¸ç®å¼¹çªç¸å
³ |
| | | const formModalRef = ref(); |
| | | const saving = ref(false); |
| | | const calculateVisible = ref(false); |
| | | const calculatePeriod = ref<string>(); |
| | | const calculateDeptId = ref<number>(); |
| | |
| | | previewData.value = []; |
| | | } |
| | | |
| | | /** 确认èªé
¬æ ¸ç® */ |
| | | /** æè¡å
ç¼è¾çææ¬å¼å½ä¸ä¸ºæ°ååæäº¤ä¿å */ |
| | | async function saveRowEdits(row: any) { |
| | | const payload: any = { ...row }; |
| | | EDITABLE_NUMBER_FIELDS.forEach((f) => { |
| | | payload[f] = toNumberOrZero(payload[f]); |
| | | }); |
| | | await updateSalaryCalculation(payload); |
| | | } |
| | | |
| | | /** 确认èªé
¬æ ¸ç®ï¼æ¹æ¡Aï¼ç¡®è®¤åå
ä¿åå½åè¡çä¿®æ¹ï¼é¿å
ãæ¹äºæ²¡ä¿å就确认ã导è´éé¢ä¸¢å¤±ï¼ */ |
| | | async function handleConfirm(row: HrmSalaryCalculationApi.SalaryCalculation) { |
| | | if (row.status === 0) { |
| | | await saveRowEdits(row); |
| | | } |
| | | await confirmSalary([row.id!]); |
| | | message.success('确认æå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ¹é确认éä¸çè®°å½ */ |
| | | /** æ¹é确认éä¸çè®°å½ï¼åæ ·å
ä¿åå¾éçå¾
确认记å½ï¼åç¡®è®¤ï¼ */ |
| | | async function handleBatchConfirm() { |
| | | const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[]; |
| | | if (records.length === 0) { |
| | | message.warning('è¯·éæ©è¦ç¡®è®¤çè®°å½'); |
| | | return; |
| | | } |
| | | const pending = records.filter((r) => r.status === 0); |
| | | for (const row of pending) { |
| | | await saveRowEdits(row); |
| | | } |
| | | const ids = records.map((r) => r.id!); |
| | | await confirmSalary(ids); |
| | |
| | | async function handleExport() { |
| | | const data = await exportSalaryCalculation(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'èªé
¬æ ¸ç®è¡¨.xls', source: data }); |
| | | } |
| | | |
| | | /** å表å¯ç¼è¾çæ°å¼å段 */ |
| | | const EDITABLE_NUMBER_FIELDS = [ |
| | | 'baseSalary', |
| | | 'performanceSalary', |
| | | 'overtimePay', |
| | | 'mealAllowance', |
| | | 'transportAllowance', |
| | | 'otherAllowance', |
| | | 'socialSecurityDeduction', |
| | | 'housingFundDeduction', |
| | | 'taxDeduction', |
| | | 'otherDeduction', |
| | | 'leaveDeduction', |
| | | 'workDays', |
| | | 'overtimeHours', |
| | | ]; |
| | | |
| | | /** ææ¬è¾å
¥è½¬æ°åï¼ç©ºå¼æ 0ï¼ */ |
| | | function toNumberOrZero(val: any) { |
| | | if (val === '' || val === null || val === undefined) { |
| | | return 0; |
| | | } |
| | | const num = Number(String(val).replace(/,/g, '')); |
| | | return Number.isNaN(num) ? 0 : num; |
| | | } |
| | | |
| | | /** ä¿åå表å
èä¿®æ¹ï¼ä»
å¾
ç¡®è®¤ç¶æï¼ */ |
| | | async function handleSaveEdits() { |
| | | const rows = (gridApi.grid.getData() as any[]).filter((r) => r.status === 0); |
| | | if (rows.length === 0) { |
| | | message.warning('å½å页没æå¾
确认çè®°å½å¯ä¿®æ¹'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | for (const row of rows) { |
| | | const payload: any = { ...row }; |
| | | EDITABLE_NUMBER_FIELDS.forEach((f) => { |
| | | payload[f] = toNumberOrZero(payload[f]); |
| | | }); |
| | | await updateSalaryCalculation(payload); |
| | | } |
| | | message.success(`å·²ä¿å ${rows.length} æ¡æ ¸ç®è®°å½`); |
| | | handleRefresh(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | </div> |
| | | </Modal> |
| | | |
| | | <FormModal ref="formModalRef" @success="handleRefresh" /> |
| | | <Grid table-title="èªé
¬æ ¸ç®å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | onClick: handleBatchSave, |
| | | }, |
| | | { |
| | | label: 'ä¿åä¿®æ¹', |
| | | type: 'primary', |
| | | auth: ['hrm:salary-calculation:calculate'], |
| | | loading: saving, |
| | | onClick: handleSaveEdits, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #editableCell="{ row, column }"> |
| | | <Input |
| | | v-if="row.status === 0" |
| | | v-model:value="row[column.field]" |
| | | size="small" |
| | | :style="{ width: '100%' }" |
| | | /> |
| | | <span v-else>{{ row[column.field] ?? '' }}</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag :color="row.status === 0 ? 'warning' : row.status === 10 ? 'success' : 'processing'"> |
| | | {{ getDictLabel(DICT_TYPE.HRM_SALARY_STATUS, row.status) }} |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ä¿®æ¹', |
| | | type: 'link', |
| | | auth: ['hrm:salary-calculation:calculate'], |
| | | ifShow: row.status === 0, |
| | | onClick: () => formModalRef.value?.open(row), |
| | | }, |
| | | { |
| | | label: '确认', |
| | | type: 'link', |
| | | auth: ['hrm:salary-calculation:confirm'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { message, Modal, Form, FormItem, Input, InputNumber } from 'ant-design-vue'; |
| | | |
| | | import { updateSalaryCalculation } from '#/api/hrm/salary/calculation'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | /** å½åç¼è¾çè®°å½ï¼å«ææå¯ç¼è¾åæ®µï¼ */ |
| | | const current = ref<Record<string, any>>({}); |
| | | |
| | | /** æ¶å
¥é¡¹ */ |
| | | const incomeFields = [ |
| | | { key: 'baseSalary', label: 'åºæ¬å·¥èµ' }, |
| | | { key: 'performanceSalary', label: '绩æå·¥èµ' }, |
| | | { key: 'overtimePay', label: 'å çè´¹' }, |
| | | { key: 'mealAllowance', label: 'é¤è¡¥' }, |
| | | { key: 'transportAllowance', label: '交é补贴' }, |
| | | { key: 'otherAllowance', label: 'å
¶ä»è¡¥è´´' }, |
| | | ]; |
| | | |
| | | /** æ£é¤é¡¹ */ |
| | | const deductionFields = [ |
| | | { key: 'socialSecurityDeduction', label: 'ç¤¾ä¿æ£æ¬¾' }, |
| | | { key: 'housingFundDeduction', label: 'å
¬ç§¯éæ£æ¬¾' }, |
| | | { key: 'taxDeduction', label: 'ä¸ªç¨æ£æ¬¾' }, |
| | | { key: 'otherDeduction', label: 'å
¶ä»æ£æ¬¾' }, |
| | | { key: 'leaveDeduction', label: 'è¯·åæ£æ¬¾' }, |
| | | ]; |
| | | |
| | | /** å®åå·¥èµï¼å®æ¶è®¡ç®ï¼ä¸å端å£å¾ä¸è´ï¼ */ |
| | | const actualSalary = computed(() => { |
| | | const num = (v: any) => (v === undefined || v === null || v === '' ? 0 : Number(v)); |
| | | const income = incomeFields.reduce((sum, f) => sum + num(current.value[f.key]), 0); |
| | | const deduction = deductionFields.reduce((sum, f) => sum + num(current.value[f.key]), 0); |
| | | return Number((income - deduction).toFixed(2)); |
| | | }); |
| | | |
| | | function open(row: HrmSalaryCalculationApi.SalaryCalculation) { |
| | | current.value = { ...row }; |
| | | visible.value = true; |
| | | } |
| | | |
| | | async function handleOk() { |
| | | loading.value = true; |
| | | try { |
| | | await updateSalaryCalculation({ |
| | | ...(current.value as HrmSalaryCalculationApi.SalaryCalculation), |
| | | actualSalary: actualSalary.value, |
| | | }); |
| | | message.success('ä¿®æ¹æå'); |
| | | visible.value = false; |
| | | emit('success'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | defineExpose({ open }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="visible" |
| | | :title="`ä¿®æ¹èªé
¬æ ¸ç®${current.no ? ' - ' + current.no : ''}`" |
| | | :width="760" |
| | | :confirm-loading="loading" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | @ok="handleOk" |
| | | > |
| | | <Form layout="vertical" class="mt-2"> |
| | | <div class="mb-2 text-sm font-semibold text-gray-700">æ¶å
¥é¡¹</div> |
| | | <div class="grid grid-cols-3 gap-x-4"> |
| | | <FormItem v-for="f in incomeFields" :key="f.key" :label="f.label"> |
| | | <InputNumber |
| | | v-model:value="current[f.key]" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | /> |
| | | </FormItem> |
| | | </div> |
| | | |
| | | <div class="mb-2 mt-2 text-sm font-semibold text-gray-700">æ£é¤é¡¹</div> |
| | | <div class="grid grid-cols-3 gap-x-4"> |
| | | <FormItem v-for="f in deductionFields" :key="f.key" :label="f.label"> |
| | | <InputNumber |
| | | v-model:value="current[f.key]" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | /> |
| | | </FormItem> |
| | | </div> |
| | | |
| | | <div class="mb-2 mt-2 text-sm font-semibold text-gray-700">èå¤ / å
¶ä»</div> |
| | | <div class="grid grid-cols-3 gap-x-4"> |
| | | <FormItem label="åºå¤å¤©æ°"> |
| | | <InputNumber v-model:value="current.workDays" :min="0" :max="31" :precision="0" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å çæ¶é¿(h)"> |
| | | <InputNumber v-model:value="current.overtimeHours" :min="0" :precision="1" style="width: 100%" /> |
| | | </FormItem> |
| | | <FormItem label="å®åå·¥èµï¼èªå¨è®¡ç®ï¼"> |
| | | <Input :value="actualSalary.toFixed(2)" disabled class="font-bold text-green-600" /> |
| | | </FormItem> |
| | | </div> |
| | | |
| | | <FormItem label="夿³¨"> |
| | | <Input.TextArea v-model:value="current.remark" :rows="2" placeholder="请è¾å
¥å¤æ³¨" /> |
| | | </FormItem> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage'; |
| | | |
| | | import { getSimplePostList } from '#/api/system/post'; |
| | | |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | const statusOptions = [ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]; |
| | | |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'postId', |
| | | label: 'å²ä½', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimplePostList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©å²ä½ï¼å·¥ç§ï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'hourlyRate', |
| | | label: 'å çæ¶èª', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å
/å°æ¶', |
| | | min: 0, |
| | | precision: 2, |
| | | step: 1, |
| | | placeholder: 'é»è®¤ 20ï¼æå²ä½å¯ä¸å', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'maxOvertimeHours', |
| | | label: 'æå¤§å çæ¶é¿', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å°æ¶/æ', |
| | | min: 0, |
| | | precision: 1, |
| | | step: 1, |
| | | placeholder: 'ç空表示ä¸é', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { options: statusOptions }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | maxlength: 500, |
| | | placeholder: 'å¯å¡«åéç¨è¯´æï¼å¦ãä»
æ®éåå·¥ããæå·¥æ 1:1ãç', |
| | | rows: 3, |
| | | showCount: true, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'postId', |
| | | label: 'å²ä½', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimplePostList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©å²ä½', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: statusOptions, |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | export function useGridColumns(): VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>['columns'] { |
| | | return [ |
| | | { field: 'postName', title: 'å²ä½ï¼å·¥ç§ï¼', minWidth: 160 }, |
| | | { |
| | | field: 'hourlyRate', |
| | | title: 'å çæ¶èª', |
| | | width: 130, |
| | | slots: { default: 'hourlyRate' }, |
| | | }, |
| | | { |
| | | field: 'maxOvertimeHours', |
| | | title: 'æå¤§å çæ¶é¿', |
| | | width: 140, |
| | | slots: { default: 'maxOvertimeHours' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { field: 'remark', title: '夿³¨', minWidth: 180 }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | | title: 'æä½', |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | width: 150, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage'; |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Alert, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteOvertimeWageConfig, |
| | | getOvertimeWageConfigList, |
| | | } from '#/api/hrm/salary/overtime-wage'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'HrmOvertimeWage' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: HrmOvertimeWageApi.OvertimeWageConfig) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: HrmOvertimeWageApi.OvertimeWageConfig) { |
| | | await deleteOvertimeWageConfig(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['å çå·¥èµé
ç½®'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { schema: useGridFormSchema() }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async (_params, formValues) => |
| | | await getOvertimeWageConfigList(formValues), |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-4" |
| | | description="æãå²ä½ï¼å·¥ç§ï¼ãé
ç½®å çæ¶èªï¼æ®éåå·¥ææ¤å¤æ¶èªè®¡å çå·¥èµï¼å¯è®¾ç½®æ¯ææå¤§å çæ¶é¿ä¸éï¼æå·¥æå¹³æ¶æ¶èª 1:1ï¼ç®¡çä¸è®¡å çå·¥èµãå çå·¥èµå审æ¹éè¿çå çåæ ¸ç®ã" |
| | | message="å çå·¥èµæå²ä½åå" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å çå·¥èµç®¡ç"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å çå·¥èµ']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:overtime-wage:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #hourlyRate="{ row }"> |
| | | <Tag color="blue">{{ row.hourlyRate ?? 0 }} å
/å°æ¶</Tag> |
| | | </template> |
| | | <template #maxOvertimeHours="{ row }"> |
| | | <span v-if="row.maxOvertimeHours === null || row.maxOvertimeHours === undefined">ä¸é</span> |
| | | <span v-else>{{ row.maxOvertimeHours }} å°æ¶/æ</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag :color="row.status === 0 ? 'success' : 'default'"> |
| | | {{ row.status === 0 ? 'å¯ç¨' : 'ç¦ç¨' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:overtime-wage:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:overtime-wage:delete'], |
| | | popConfirm: { |
| | | title: '确认å é¤è¯¥å²ä½çå çå·¥èµé
ç½®åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage'; |
| | | import type { FormType } from '../data'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createOvertimeWageConfig, |
| | | getOvertimeWageConfig, |
| | | updateOvertimeWageConfig, |
| | | } from '#/api/hrm/salary/overtime-wage'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => |
| | | formType.value === 'update' ? 'ä¿®æ¹å çå·¥èµ' : 'æ°å¢å çå·¥èµ', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = |
| | | (await formApi.getValues()) as HrmOvertimeWageApi.OvertimeWageConfig; |
| | | if (data.id) { |
| | | await updateOvertimeWageConfig(data); |
| | | } else { |
| | | await createOvertimeWageConfig(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | await formApi.resetForm(); |
| | | if (!data.id) { |
| | | await formApi.setValues({ hourlyRate: 20, status: 0 }); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await formApi.setValues(await getOvertimeWageConfig(data.id)); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[640px]"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | description="æ®éåå·¥ï¼å çå·¥èµ = æ¬å²ä½é
ç½®çæ¶èª Ã å çæ¶é¿ï¼è¶
è¿æå¤§æ¶é¿ä¸éçé¨åä¸è®¡ï¼ï¼æå·¥æå¹³æ¶æ¶èª 1:1ï¼ç®¡çä¸è®¡å çå·¥èµã" |
| | | message="å çå·¥èµè®¡ç®è¯´æ" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | /** æ ¼å¼åæ¯ä¾è¾å
¥ */ |
| | | /** æ ¼å¼åæ¯ä¾è¾å
¥ï¼ç»ä¸æ¾ç¤ºä¸¤ä½å°æ°ç¾åæ¯ï¼å¦ 8.25%ï¼ */ |
| | | function createRatioFormatter(step: number = 0.01) { |
| | | return { |
| | | formatter: (value: number) => value !== undefined && value !== null ? `${(value * 100).toFixed(step === 0.001 ? 1 : 0)}%` : '', |
| | | formatter: (value: number) => |
| | | value !== undefined && value !== null ? `${(value * 100).toFixed(2)}%` : '', |
| | | parser: (value: string) => { |
| | | const num = parseFloat(String(value).replace('%', '')); |
| | | return isNaN(num) ? 0 : num / 100; |
| | | }, |
| | | }; |
| | | } |
| | | |
| | | /** æ ¼å¼åæ¯ä¾è¾å
¥ï¼æ¾ç¤ºä¸¤ä½å°æ°ï¼å¦ 0.50%ï¼ */ |
| | | function createRatioFormatter2() { |
| | | return { |
| | | formatter: (value: number) => |
| | | value !== undefined && value !== null ? `${(value * 100).toFixed(2)}%` : '', |
| | | parser: (value: string) => { |
| | | const num = parseFloat(value.replace('%', '')); |
| | | return isNaN(num) ? 0 : num / 100; |
| | |
| | | ...createRatioFormatter(0.001), |
| | | }, |
| | | }, |
| | | // 大ç
å»ç |
| | | { |
| | | fieldName: 'divider_serious_illness', |
| | | label: '大ç
å»ç', |
| | | component: 'Divider', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'seriousIllnessCompanyRatio', |
| | | label: 'ä¼ä¸æ¯ä¾', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: 'ä¼ä¸ç¼´çº³æ¯ä¾', |
| | | min: 0, |
| | | max: 1, |
| | | step: 0.0001, |
| | | precision: 4, |
| | | style: { width: '100%' }, |
| | | ...createRatioFormatter2(), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'seriousIllnessPersonalRatio', |
| | | label: '个人æ¯ä¾', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '个人缴纳æ¯ä¾', |
| | | min: 0, |
| | | max: 1, |
| | | step: 0.0001, |
| | | precision: 4, |
| | | style: { width: '100%' }, |
| | | ...createRatioFormatter2(), |
| | | }, |
| | | }, |
| | | // é¿ææ¤ç |
| | | { |
| | | fieldName: 'divider_long_term_care', |
| | | label: 'é¿ææ¤ç', |
| | | component: 'Divider', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'longTermCareCompanyRatio', |
| | | label: 'ä¼ä¸æ¯ä¾', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: 'ä¼ä¸ç¼´çº³æ¯ä¾', |
| | | min: 0, |
| | | max: 1, |
| | | step: 0.0001, |
| | | precision: 4, |
| | | style: { width: '100%' }, |
| | | ...createRatioFormatter2(), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'longTermCarePersonalRatio', |
| | | label: '个人æ¯ä¾', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '个人缴纳æ¯ä¾', |
| | | min: 0, |
| | | max: 1, |
| | | step: 0.0001, |
| | | precision: 4, |
| | | style: { width: '100%' }, |
| | | ...createRatioFormatter2(), |
| | | }, |
| | | }, |
| | | // å
¬ç§¯é |
| | | { |
| | | fieldName: 'divider_housing', |
| | |
| | | slots: { default: 'housingFundPersonalRatio' }, |
| | | }, |
| | | { |
| | | field: 'seriousIllnessPersonalRatio', |
| | | title: '大ç
å»ç(个人)', |
| | | width: 120, |
| | | slots: { default: 'seriousIllnessPersonalRatio' }, |
| | | }, |
| | | { |
| | | field: 'longTermCarePersonalRatio', |
| | | title: 'é¿ææ¤ç(个人)', |
| | | width: 120, |
| | | slots: { default: 'longTermCarePersonalRatio' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 80, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ ¼å¼åæ¯ä¾æ¾ç¤º */ |
| | | /** æ ¼å¼åæ¯ä¾æ¾ç¤ºï¼ä¿ç两ä½å°æ°ï¼ */ |
| | | function formatRatio(ratio: number | undefined) { |
| | | if (ratio === undefined || ratio === null) return '-'; |
| | | return `${(ratio * 100).toFixed(1)}%`; |
| | | return `${(ratio * 100).toFixed(2)}%`; |
| | | } |
| | | |
| | | /** æ ¼å¼åæ¯ä¾æ¾ç¤ºï¼ä¿ç两ä½å°æ°ï¼ */ |
| | | function formatRatio2(ratio: number | undefined) { |
| | | if (ratio === undefined || ratio === null) return '-'; |
| | | return `${(ratio * 100).toFixed(2)}%`; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | <template #housingFundPersonalRatio="{ row }"> |
| | | {{ formatRatio(row.housingFundPersonalRatio) }} |
| | | </template> |
| | | <template #seriousIllnessPersonalRatio="{ row }"> |
| | | {{ formatRatio2(row.seriousIllnessPersonalRatio) }} |
| | | </template> |
| | | <template #longTermCarePersonalRatio="{ row }"> |
| | | {{ formatRatio2(row.longTermCarePersonalRatio) }} |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag :color="row.status === 0 ? 'success' : 'default'"> |
| | | {{ row.status === 0 ? 'å¯ç¨' : 'ç¦ç¨' }} |
| | |
| | | maternityRatio: 0.005, |
| | | housingFundCompanyRatio: 0.12, |
| | | housingFundPersonalRatio: 0.12, |
| | | seriousIllnessCompanyRatio: 0, |
| | | seriousIllnessPersonalRatio: 0, |
| | | longTermCareCompanyRatio: 0, |
| | | longTermCarePersonalRatio: 0, |
| | | status: 0, |
| | | }); |
| | | return; |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'actions', |
| | |
| | | |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { getSimplePostList } from '#/api/system/post'; |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | /** 表åç±»å */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'è°å²äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | showSearch: true, |
| | | filterOption: (input: string, option: any) => |
| | | `${option?.label ?? ''}`.toLowerCase().includes(input.toLowerCase()), |
| | | allowClear: true, |
| | | placeholder: 'è¯·éæ©è¦è°å²çåå·¥ï¼éæ©åèªå¨å¸¦åºåé¨é¨/åå²ä½ï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'currentDeptName', |
| | | label: 'åé¨é¨', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'èªå¨è·å', |
| | | placeholder: 'éæ©è°å²äººåèªå¨å¸¦åº', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'èªå¨è·å', |
| | | placeholder: 'éæ©è°å²äººåèªå¨å¸¦åº', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | export function useGridColumns(): VxeTableGridOptions<HrmTransferApi.Transfer>['columns'] { |
| | | return [ |
| | | { field: 'id', title: 'ç³è¯·ç¼å·', width: 80 }, |
| | | { field: 'employeeName', title: 'ç³è¯·äºº', minWidth: 100 }, |
| | | { field: 'userName', title: 'è°å²äºº', minWidth: 100 }, |
| | | { field: 'currentDeptName', title: 'åé¨é¨', minWidth: 120 }, |
| | | { field: 'currentPostName', title: 'åå²ä½', minWidth: 120 }, |
| | | { field: 'targetDeptName', title: 'ç®æ é¨é¨', minWidth: 120 }, |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDate } from '@vben/utils'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'åå·¥å§å', field: 'employeeName', render: textRender }, |
| | | { label: 'å½åé¨é¨', field: 'currentDeptName', render: textRender }, |
| | | { label: 'å½åèä½', field: 'currentPostName', render: textRender }, |
| | | { label: 'è°å²äºº', field: 'userName', render: textRender }, |
| | | { label: 'åé¨é¨', field: 'currentDeptName', render: textRender }, |
| | | { label: 'åå²ä½', field: 'currentPostName', render: textRender }, |
| | | { label: 'ç®æ é¨é¨', field: 'targetDeptName', render: textRender }, |
| | | { label: 'ç®æ èä½', field: 'targetPostName', render: textRender }, |
| | | { |
| | |
| | | { |
| | | label: 'çææ¥æ', |
| | | field: 'effectiveDate', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: 'è°å²åå ', field: 'reason', render: textRender }, |
| | | { |
| | |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDate(val) as string, |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const currentEmployeeId = ref<number>(); |
| | | /** å工精ç®å表ç¼åï¼é人æ¶ç¨äºå¸¦åºåé¨é¨ / åå²ä½ï¼ */ |
| | | let employeeCache: any[] = []; |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const getTitle = computed(() => { |
| | |
| | | } |
| | | return formType.value === 'update' ? 'ä¿®æ¹è°å²ç³è¯·' : 'æ°å¢è°å²ç³è¯·'; |
| | | }); |
| | | |
| | | /** |
| | | * æ ¹æ®éä¸çåå·¥ï¼èªå¨å¸¦åºåé¨é¨ / åå²ä½ï¼å« IDï¼ä¾¿äºå端è½åºï¼ |
| | | */ |
| | | async function fillCurrentPosition(employeeId?: number) { |
| | | if (!employeeId) { |
| | | await formApi.setValues({ |
| | | currentDeptId: undefined, |
| | | currentDeptName: undefined, |
| | | currentPostId: undefined, |
| | | currentPostName: undefined, |
| | | }); |
| | | return; |
| | | } |
| | | if (employeeCache.length === 0) { |
| | | employeeCache = (await getEmployeeSimpleList()) || []; |
| | | } |
| | | const emp = employeeCache.find((item) => item.id === employeeId); |
| | | if (!emp) { |
| | | return; |
| | | } |
| | | await formApi.setValues({ |
| | | currentDeptId: emp.deptId, |
| | | currentDeptName: emp.deptName, |
| | | currentPostId: emp.postIds?.[0], |
| | | currentPostName: emp.postName, |
| | | }); |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | // éæ©ãè°å²äººãåï¼èªå¨å¸¦åºå
¶å½åé¨é¨ / å²ä½ |
| | | async handleValuesChange(values: any, changedFields: string[]) { |
| | | if (changedFields?.includes('userId')) { |
| | | await fillCurrentPosition(values?.userId); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as HrmTransferApi.Transfer; |
| | | data.employeeId = currentEmployeeId.value; |
| | | try { |
| | | if (data.id) { |
| | | await updateTransfer(data); |
| | |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | employeeCache = []; |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(data.formType) }); |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | modalApi.setState({ showConfirmButton: data.formType !== 'detail' }); |
| | | if (!data?.id) { |
| | | // æ°å¢æ¶ï¼èªå¨è·åå½åç»å½åå·¥çé¨é¨åå²ä½ä¿¡æ¯ |
| | | const employees = await getEmployeeSimpleList(); |
| | | // TODO: è¿éåºè¯¥æ ¹æ®ç»å½ç¨æ·è·å对åºçå工信æ¯ï¼ææ¶å第ä¸ä¸ªåå·¥ä½ä¸ºç¤ºä¾ |
| | | const currentEmployee = employees[0]; |
| | | if (currentEmployee) { |
| | | currentEmployeeId.value = currentEmployee.id; |
| | | await formApi.setValues({ |
| | | currentDeptName: currentEmployee.deptName, |
| | | currentPostName: currentEmployee.postName, |
| | | }); |
| | | } |
| | | // æ°å¢ï¼æ¸
空表åï¼ç±ç¨æ·éæ©è°å²äºº |
| | | await formApi.setValues({ |
| | | userId: undefined, |
| | | currentDeptId: undefined, |
| | | currentDeptName: undefined, |
| | | currentPostId: undefined, |
| | | currentPostName: undefined, |
| | | }); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const transfer = await getTransfer(data.id); |
| | | currentEmployeeId.value = transfer.employeeId; |
| | | await formApi.setValues(transfer); |
| | | // å岿°æ®å¯è½æ²¡æåé¨é¨/å²ä½ï¼æè°å²äººè¡¥é½ |
| | | if (transfer.userId && !transfer.currentDeptId) { |
| | | await fillCurrentPosition(transfer.userId); |
| | | } |
| | | if (transfer.attachmentList?.length) { |
| | | await formApi.setFieldValue('blobIds', transfer.attachmentList.map(item => ({ |
| | | uid: String(item.id), |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[600px]"> |
| | | <Modal :title="getTitle" class="w-[700px]"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | | </template> |