feat: 售后工单按条绑定部门与原因类型;新增加班工资管理;员工附件留档
- 售后工单明细:独立单号/部门/原因类型/具体原因,销售订单改为可选
- 新增加班工资管理页面与员工类别字段
- 员工表单附件上传 + 详情附件留档展示
| | |
| | | export interface TicketItem { |
| | | id?: number; |
| | | ticketId?: number; |
| | | /** æç»å®ååå·ï¼å·¥åå·-åºå·ï¼ */ |
| | | itemNo?: string; |
| | | saleOrderItemId?: number; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | |
| | | returnQuantity?: number; |
| | | needRepair?: boolean; |
| | | needReturn?: boolean; |
| | | /** é¨é¨ID */ |
| | | deptId?: number; |
| | | /** é¨é¨åç§° */ |
| | | deptName?: string; |
| | | /** åå ç±»å */ |
| | | reasonType?: number; |
| | | /** å
·ä½åå */ |
| | | reason?: string; |
| | | remark?: string; |
| | | } |
| | | |
| | |
| | | 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 }[]; |
| | | } |
| | | |
| | | /** æè²ç»å */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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}`); |
| | | } |
| | |
| | | AFTERSALE_ISSUE_TYPE: 'aftersale_issue_type', // å®åé®é¢ç±»å |
| | | AFTERSALE_RETURN_TYPE: 'aftersale_return_type', // å®åéè´§ç±»å |
| | | AFTERSALE_SEVERITY_LEVEL: 'aftersale_severity_level', // å®å严éç¨åº¦ |
| | | AFTERSALE_REASON_TYPE: 'aftersale_reason_type', // å®ååå ç±»å |
| | | } as const; |
| | | |
| | | /** ========== ERP - ä¼ä¸èµæºè®¡å模å ========== */ |
| | |
| | | const HRM_DICT = { |
| | | HRM_DEGREE: 'hrm_degree', // HRM å¦å |
| | | HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM åå·¥ç¶æ |
| | | HRM_EMPLOYEE_CATEGORY: 'hrm_employee_category', // HRM å工类å«ï¼æ®é/æå·¥/管çï¼ |
| | | HRM_GENDER: 'hrm_gender', // HRM æ§å« |
| | | HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM 审æ¹ç¶æ |
| | | HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM 请åç±»å |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { WmBatchSelect } from '#/views/wls/batch/components'; |
| | | import { UserSelect } from '#/views/system/user/components'; |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type TicketFormType = 'create' | 'detail' | 'update'; |
| | |
| | | } |
| | | |
| | | /** å·¥å表å */ |
| | | export function useTicketFormSchema( |
| | | formType: TicketFormType, |
| | | formApi?: VbenFormApi, |
| | | onSaleOrderChange?: (order: Record<string, unknown> | undefined) => void, |
| | | ): VbenFormSchema[] { |
| | | export function useTicketFormSchema(formType: TicketFormType): VbenFormSchema[] { |
| | | const readonly = isHeaderReadonly(formType); |
| | | return [ |
| | | { |
| | |
| | | placeholder: '请è¾å
¥å·¥å主é¢', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'saleOrderId', |
| | | label: 'éå®è®¢åå·', |
| | | component: markRaw(ErpSaleOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©éå®è®¢å', |
| | | outStatus: 3, |
| | | onChange: (item: Record<string, unknown> | undefined) => { |
| | | if (formApi) { |
| | | formApi.setFieldValue('saleOrderNo', item?.no ?? ''); |
| | | if (item?.customerId) { |
| | | formApi.setFieldValue('customerId', item.customerId); |
| | | } |
| | | } |
| | | onSaleOrderChange?.(item); |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'saleOrderNo', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'saleOrderNo', |
| | | title: 'éå®è®¢åå·', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'issueType', |
| | | title: 'é®é¢ç±»å', |
| | | width: 100, |
| | |
| | | ): VxeTableGridOptions<AftersalesTicketApi.TicketItem>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'itemNo', |
| | | title: 'æç»åå·', |
| | | minWidth: 170, |
| | | }, |
| | | { |
| | | field: 'itemCode', |
| | | title: 'ç©æç¼ç ', |
| | | minWidth: 120, |
| | |
| | | title: 'éè¦éè´§', |
| | | width: 90, |
| | | slots: { default: 'needReturn' }, |
| | | }, |
| | | { |
| | | field: 'deptName', |
| | | title: 'é¨é¨', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'reasonType', |
| | | title: 'åå ç±»å', |
| | | width: 120, |
| | | slots: { default: 'reasonType' }, |
| | | }, |
| | | { |
| | | field: 'reason', |
| | | title: 'å
·ä½åå ', |
| | | minWidth: 160, |
| | | }, |
| | | ...(editable |
| | | ? [ |
| | |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'deptId', |
| | | label: 'é¨é¨', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | api: async () => handleTree(await getDeptList()), |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | childrenField: 'children', |
| | | placeholder: 'è¯·éæ©é¨é¨', |
| | | treeDefaultExpandAll: true, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'reasonType', |
| | | label: 'åå ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.AFTERSALE_REASON_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©åå ç±»å', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'reason', |
| | | label: 'å
·ä½åå ', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-3', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å
·ä½åå ', |
| | | rows: 2, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | import type { TicketFormType } from '../data'; |
| | | |
| | | import type { AftersalesTicketApi } from '#/api/aftersales/ticket'; |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants'; |
| | | import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | |
| | | import { Button, Divider, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue'; |
| | | |
| | |
| | | resolveTicket, |
| | | updateTicket, |
| | | } from '#/api/aftersales/ticket'; |
| | | import { getSaleOrder } from '#/api/erp/sale/order'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useTicketFormSchema, useTicketItemFormSchema, useTicketItemGridColumns } from '../data'; |
| | |
| | | return $t('ui.actionTitle.create', ['å®åå·¥å']); |
| | | }); |
| | | |
| | | // éå®è®¢åç©æè¿æ»¤ |
| | | const orderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); |
| | | |
| | | function handleSaleOrderChange(item: Record<string, unknown> | undefined) { |
| | | if (!item?.id) { |
| | | orderItems.value = []; |
| | | return; |
| | | } |
| | | orderItems.value = (item.items as ErpSaleOrderApi.SaleOrderItem[]) || []; |
| | | } |
| | | |
| | | // 主表å |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 110 }, |
| | |
| | | // Modalï¼å·¥å表åå¼¹æ¡ï¼å¿
é¡»å¨ ItemModal ä¹åï¼ç¡®ä¿ connectedComponent 注å
¥å°æ£ç¡®çå¼¹æ¡ |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { formData.value = undefined; items.value = []; orderItems.value = []; showClassify.value = false; return; } |
| | | if (!isOpen) { formData.value = undefined; items.value = []; showClassify.value = false; return; } |
| | | const data = modalApi.getData<{ formType: TicketFormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | itemsGridApi.setGridOptions({ columns: useTicketItemGridColumns(isEdit.value) }); |
| | | formApi.setState({ schema: useTicketFormSchema(formType.value, formApi, handleSaleOrderChange) }); |
| | | formApi.setState({ schema: useTicketFormSchema(formType.value) }); |
| | | formApi.setDisabled(!isEdit.value); |
| | | modalApi.setState({ showConfirmButton: false, showCancelButton: false }); |
| | | if (data?.id) { |
| | |
| | | status: 'done', id: item.id, |
| | | })); |
| | | await formApi.setFieldValue('blobIds', blobIds); |
| | | } |
| | | if (formData.value?.saleOrderId) { |
| | | const order = await getSaleOrder(formData.value.saleOrderId); |
| | | orderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || []; |
| | | } |
| | | } finally { modalApi.unlock(); } |
| | | } else { |
| | |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { editingItemIndex.value = null; return; } |
| | | const d = itemModalApi.getData<AftersalesTicketApi.TicketItem | null>(); |
| | | itemFormApi.setState({ schema: useTicketItemFormSchema(formData.value?.customerId, formData.value?.saleOrderNo, orderItems.value, itemFormApi) }); |
| | | itemFormApi.setState({ schema: useTicketItemFormSchema(formData.value?.customerId, undefined, [], itemFormApi) }); |
| | | if (d) { itemFormApi.setValues(d as Record<string, unknown>); } else { itemFormApi.resetForm(); } |
| | | }, |
| | | }); |
| | |
| | | <ItemsGrid> |
| | | <template #needRepair="{ row }"><Tag :color="row.needRepair ? 'blue' : 'default'">{{ row.needRepair ? 'æ¯' : 'å¦' }}</Tag></template> |
| | | <template #needReturn="{ row }"><Tag :color="row.needReturn ? 'orange' : 'default'">{{ row.needReturn ? 'æ¯' : 'å¦' }}</Tag></template> |
| | | <template #reasonType="{ row }">{{ getDictLabel(DICT_TYPE.AFTERSALE_REASON_TYPE, row.reasonType) }}</template> |
| | | <template v-if="isEdit" #itemActions="{ row, rowIndex }"> |
| | | <TableAction :actions="[ |
| | | { label: $t('common.edit'), type: 'link', icon: ACTION_ICON.EDIT, onClick: () => editItem(rowIndex) }, |
| | |
| | | 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', |
| | |
| | | {{ 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="[ |
| | |
| | | <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(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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> |