2026-08-10 f9e6eb6fc2c2e0c2a2b17238b817fcbfcb6bab56
feat(erp): 添加采购来票管理和付款关联功能

- 新增采购来票管理模块,支持来票信息录入和附件上传
- 实现付款单与来票关联选择功能,供应商变更时自动清空已选来票
- 在付款列表中显示关联来票信息,完善财务付款流程
- 添加员工合同管理功能,支持合同类型和状态管理
- 优化表单组件属性配置,提升用户体验和数据验证逻辑
已修改6个文件
已添加11个文件
1871 ■■■■■ 文件已修改
src/api/erp/finance/payment/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/invoice/index.ts 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/contract/index.ts 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/data.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/modules/form.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/data.ts 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/index.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/form.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/purchase-order-select.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/data.ts 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/index.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/modules/form.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/modules/terminate.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/contract-list.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/detail.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/finance/payment/index.ts
@@ -1,5 +1,7 @@
import type { PageParam, PageResult } from '@vben/request';
import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
import { requestClient } from '#/api/request';
export namespace ErpFinancePaymentApi {
@@ -9,6 +11,8 @@
    no: string; // ä»˜æ¬¾å•号
    supplierId?: number; // ä¾›åº”商编号
    supplierName?: string; // ä¾›åº”商名称
    invoiceId?: number; // å…³è”来票编号
    invoice?: ErpPurchaseInvoiceApi.PurchaseInvoice; // å…³è”来票信息
    paymentTime?: Date; // ä»˜æ¬¾æ—¶é—´
    totalPrice: number; // åˆè®¡é‡‘额,单位:元
    discountPrice: number; // ä¼˜æƒ é‡‘额
src/api/erp/purchase/invoice/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace ErpPurchaseInvoiceApi {
  /** é‡‡è´­è®¢å•信息(来票关联) */
  export interface PurchaseOrder {
    id?: number;
    no?: string;
    supplierId?: number;
    supplierName?: string;
    orderTime?: Date | string;
    totalPrice?: number;
    status?: number;
  }
  /** æ¥ç¥¨ä¿¡æ¯ */
  export interface PurchaseInvoice {
    id?: number;
    no?: string;
    supplierId?: number;
    supplierName?: string;
    purchaseOrderId?: number;
    order?: PurchaseOrder;
    invoiceNo?: string;
    invoiceTitle?: string;
    price?: number;
    invoiceTime?: string;
    auditStatus?: number;
    processInstanceId?: string;
    remark?: string;
    attachmentList?: { id: number; name?: string; url?: string }[];
    blobIds?: number[];
    hasAttachment?: boolean;
    remainingInvoicePrice?: number;
    creator?: string;
    creatorName?: string;
    createTime?: string;
    updateTime?: string;
  }
  /** æ¥ç¥¨ç®€è¦ä¿¡æ¯ï¼ˆç”¨äºŽä»˜æ¬¾å…³è”选择) */
  export interface PurchaseInvoiceSimple {
    id: number;
    no: string;
    invoiceNo?: string;
    invoiceTitle?: string;
    price?: number;
    supplierId?: number;
    purchaseOrderId?: number;
    auditStatus?: number;
    hasAttachment?: boolean;
    remainingInvoicePrice?: number;
  }
}
/** æŸ¥è¯¢æ¥ç¥¨åˆ†é¡µ */
export function getPurchaseInvoicePage(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchaseInvoiceApi.PurchaseInvoice>>(
    '/erp/purchase-invoice/page',
    { params },
  );
}
/** æŸ¥è¯¢æ¥ç¥¨è¯¦æƒ… */
export function getPurchaseInvoice(id: number) {
  return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoice>(
    `/erp/purchase-invoice/get?id=${id}`,
  );
}
/** æ–°å¢žæ¥ç¥¨ */
export function createPurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  return requestClient.post('/erp/purchase-invoice/create', data);
}
/** ä¿®æ”¹æ¥ç¥¨ */
export function updatePurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  return requestClient.put('/erp/purchase-invoice/update', data);
}
/** åˆ é™¤æ¥ç¥¨ */
export function deletePurchaseInvoice(id: number) {
  return requestClient.delete(`/erp/purchase-invoice/delete?id=${id}`);
}
/** å¯¼å‡ºæ¥ç¥¨ */
export function exportPurchaseInvoice(params: Record<string, unknown>) {
  return requestClient.download('/erp/purchase-invoice/export-excel', { params });
}
/** æäº¤æ¥ç¥¨å®¡æ ¸ */
export function submitPurchaseInvoice(id: number) {
  return requestClient.put(`/erp/purchase-invoice/submit?id=${id}`);
}
/** èŽ·å¾—å¾…å®¡æ ¸æ¥ç¥¨æ•°é‡ */
export function getAuditPurchaseInvoiceCount() {
  return requestClient.get<number>('/erp/purchase-invoice/audit-count');
}
/** æŸ¥è¯¢æ¥ç¥¨ä¸‹æ‹‰åˆ—表(按供应商可选来票,仅审核通过且已上传发票附件) */
export function getPurchaseInvoiceSimpleList(
  supplierId: number,
  purchaseOrderId?: number,
) {
  const params = `supplierId=${supplierId}${
    purchaseOrderId ? `&purchaseOrderId=${purchaseOrderId}` : ''
  }`;
  return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoiceSimple[]>(
    `/erp/purchase-invoice/simple-list?${params}`,
  );
}
src/api/hrm/employee/contract/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmEmployeeContractApi {
  /** å‘˜å·¥åˆåŒ */
  export interface EmployeeContract {
    id?: number;
    /** ç»­ç­¾ä¸Šä¸€ä»½åˆåŒID */
    parentId?: number;
    /** ç»­ç­¾ä¸Šä¸€ä»½åˆåŒç¼–号 */
    parentNo?: string;
    /** æ˜¯å¦å½“前生效合同 */
    isCurrent?: boolean;
    /** åˆåŒç¼–号(后端自动生成) */
    contractNo?: string;
    /** å‘˜å·¥ID */
    employeeId?: number;
    /** å‘˜å·¥å§“名 */
    employeeName?: string;
    /** å‘˜å·¥å·¥å· */
    employeeNo?: string;
    /** éƒ¨é—¨åç§° */
    deptName?: string;
    /** åˆåŒç±»åž‹ï¼š1-劳动合同 2-劳务合同 3-实习协议 4-劳务派遣 5-其他 */
    contractType?: number;
    /** åˆåŒæœŸé™ç±»åž‹ï¼š1-固定期限 2-无固定期限 3-以完成一定工作任务为期限 */
    contractTermType?: number;
    /** ç­¾çº¦ä¸»ä½“ */
    signCompany?: string;
    /** ç­¾è®¢æ—¥æœŸ */
    signDate?: string;
    /** åˆåŒå¼€å§‹æ—¥æœŸ */
    startDate?: string;
    /** åˆåŒç»“束日期 */
    endDate?: string;
    /** è¯•用期开始日期 */
    probationStartDate?: string;
    /** è¯•用期结束日期 */
    probationEndDate?: string;
    /** è¯•用期工资 */
    probationSalary?: number;
    /** è½¬æ­£å·¥èµ„ */
    regularSalary?: number;
    /** è§£é™¤ç»ˆæ­¢çŠ¶æ€ï¼š0-正常 1-已解除 2-已终止 */
    terminateStatus?: number;
    /** è§£é™¤/终止日期 */
    terminateDate?: string;
    /** è§£é™¤/终止原因 */
    terminateReason?: string;
    /** åˆåŒçŠ¶æ€ï¼ˆåŠ¨æ€è®¡ç®—ï¼‰ï¼š1-待生效 2-生效中 3-即将到期 4-已到期 5-已解除 6-已终止 */
    status?: number;
    /** é™„ä»¶ */
    attachmentList?: AttachmentItem[];
    /** é™„ä»¶ blobId åˆ—表 */
    blobIds?: number[];
    remark?: string;
    createTime?: string;
  }
  /** é™„件项 */
  export interface AttachmentItem {
    id: number;
    name?: string;
    url?: string;
    uid?: string;
  }
  /** å‘˜å·¥åˆåŒæŸ¥è¯¢å‚æ•° */
  export interface EmployeeContractPageParam extends PageParam {
    contractNo?: string;
    employeeId?: number;
    contractType?: number;
    contractTermType?: number;
    terminateStatus?: number;
    status?: number;
    /** åˆ°æœŸç±»åž‹ï¼š1-即将到期 2-已到期 */
    expiryType?: number;
  }
  /** è§£é™¤/终止合同请求 */
  export interface TerminateReq {
    id: number;
    terminateStatus: number;
    terminateDate: string;
    terminateReason: string;
  }
}
/** èŽ·å–å‘˜å·¥åˆåŒåˆ†é¡µåˆ—è¡¨ */
export function getEmployeeContractPage(
  params: HrmEmployeeContractApi.EmployeeContractPageParam,
) {
  return requestClient.get<PageResult<HrmEmployeeContractApi.EmployeeContract>>(
    '/hrm/employee-contract/page',
    { params },
  );
}
/** èŽ·å–å‘˜å·¥åˆåŒè¯¦æƒ… */
export function getEmployeeContract(id: number) {
  return requestClient.get<HrmEmployeeContractApi.EmployeeContract>(
    `/hrm/employee-contract/get?id=${id}`,
  );
}
/** åˆ›å»ºå‘˜å·¥åˆåŒ */
export function createEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) {
  return requestClient.post('/hrm/employee-contract/create', data);
}
/** æ›´æ–°å‘˜å·¥åˆåŒ */
export function updateEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) {
  return requestClient.put('/hrm/employee-contract/update', data);
}
/** è§£é™¤/终止员工合同 */
export function terminateEmployeeContract(data: HrmEmployeeContractApi.TerminateReq) {
  return requestClient.put('/hrm/employee-contract/terminate', data);
}
/** åˆ é™¤å‘˜å·¥åˆåŒ */
export function deleteEmployeeContract(id: number) {
  return requestClient.delete(`/hrm/employee-contract/delete?id=${id}`);
}
/** èŽ·å–å³å°†åˆ°æœŸåˆåŒæ•°é‡ */
export function getExpiringContractCount() {
  return requestClient.get<number>('/hrm/employee-contract/expiring-count');
}
src/packages/constants/src/dict-enum.ts
@@ -321,6 +321,10 @@
  HRM_SALARY_STATUS: 'hrm_salary_status', // HRM è–ªèµ„状态
  HRM_HANDOVER_STATUS: 'hrm_handover_status', // HRM äº¤æŽ¥çŠ¶æ€
  HRM_PAYMENT_METHOD: 'hrm_payment_method', // HRM å‘放方式
  HRM_CONTRACT_TYPE: 'hrm_contract_type', // HRM åˆåŒç±»åž‹
  HRM_CONTRACT_TERM_TYPE: 'hrm_contract_term_type', // HRM åˆåŒæœŸé™ç±»åž‹
  HRM_CONTRACT_STATUS: 'hrm_contract_status', // HRM åˆåŒçŠ¶æ€
  HRM_CONTRACT_TERMINATE_STATUS: 'hrm_contract_terminate_status', // HRM åˆåŒè§£é™¤ç»ˆæ­¢çŠ¶æ€
} as const;
/** å­—典类型枚举 - ç»Ÿä¸€å¯¼å‡º */
src/views/erp/finance/payment/data.ts
@@ -6,6 +6,7 @@
import { erpPriceInputFormatter } from '@vben/utils';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getPurchaseInvoiceSimpleList } from '#/api/erp/purchase/invoice';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -50,7 +51,7 @@
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
      componentProps: (_values, form) => ({
        disabled: formType === 'detail',
        placeholder: '请选择供应商',
        allowClear: true,
@@ -58,8 +59,54 @@
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
        onChange: () => {
          // ä¾›åº”商变化后,清空已选的关联来票
          form.setFieldValue('invoiceId', undefined);
        },
      }),
      rules: 'required',
    },
    {
      fieldName: 'invoiceId',
      label: '关联来票',
      component: formType === 'detail' ? 'Input' : 'Select',
      componentProps:
        formType === 'detail'
          ? {
              disabled: true,
              placeholder: '--',
            }
          : undefined,
      rules: 'required',
      dependencies: {
        // ä¾èµ– invoiceId è‡ªèº«ï¼Œç¡®ä¿ç¼–辑回显时重新加载选项以匹配已选来票
        triggerFields: ['supplierId', 'invoiceId'],
        disabled: (values) => formType === 'detail' || !values.supplierId,
        async componentProps(values) {
          if (formType === 'detail') {
            return {};
          }
          if (!values.supplierId) {
            return {
              options: [],
              placeholder: '请先选择供应商',
            };
          }
          const invoices = await getPurchaseInvoiceSimpleList(
            values.supplierId,
          );
          return {
            options: invoices.map((item) => {
              const remain = item.remainingInvoicePrice ?? item.price ?? 0;
              return {
                label: `${item.no} / ${item.invoiceNo ?? '-'} / Â¥${item.price ?? 0} / å‰©ä½™å¯ä»˜ Â¥${remain.toFixed(2)}`,
                value: item.id,
              };
            }),
            placeholder: '请选择关联来票',
          } as any;
        },
      },
    },
    {
      fieldName: 'financeUserId',
@@ -335,6 +382,12 @@
      minWidth: 120,
    },
    {
      field: 'invoice',
      title: '关联来票',
      minWidth: 200,
      slots: { default: 'invoice' },
    },
    {
      field: 'paymentTime',
      title: '付款时间',
      width: 160,
src/views/erp/finance/payment/index.vue
@@ -167,6 +167,13 @@
          ]"
        />
      </template>
      <template #invoice="{ row }">
        <span v-if="row.invoice">
          {{ row.invoice.no
          }}{{ row.invoice.invoiceNo ? ` / ${row.invoice.invoiceNo}` : '' }}
        </span>
        <span v-else>--</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/erp/finance/payment/modules/form.vue
@@ -162,6 +162,10 @@
      formData.value = await getFinancePayment(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value, false);
      // è¯¦æƒ…模式下,关联来票字段用 Input å±•示来票编号
      if (formType.value === 'detail' && formData.value?.invoice) {
        await formApi.setFieldValue('invoiceId', formData.value.invoice.no);
      }
      if (formData.value?.attachmentList?.length) {
        const blobIds = formData.value.attachmentList.map((item) => ({
          uid: String(item.id),
src/views/erp/purchase/invoice/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,241 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { markRaw } from 'vue';
import PurchaseOrderSelect from './modules/purchase-order-select.vue';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'no',
      label: '来票编号',
      component: 'Input',
      componentProps: {
        placeholder: '保存时自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'purchaseOrderId',
      label: '采购订单',
      component: markRaw(PurchaseOrderSelect),
      rules: 'required',
      dependencies: {
        triggerFields: ['id'],
        disabled: (values) => !!values.id,
      },
      componentProps: (_values, form) => ({
        placeholder: '请选择采购订单',
        onChange: (order: { supplierName?: string } | undefined) => {
          form.setFieldValue('supplierName', order?.supplierName);
        },
      }),
    },
    {
      fieldName: 'supplierName',
      label: '供应商',
      component: 'Input',
      componentProps: {
        placeholder: '选择采购订单后自动带出',
        disabled: true,
      },
    },
    {
      fieldName: 'invoiceNo',
      label: '发票号码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入发票号码',
        allowClear: true,
      },
    },
    {
      fieldName: 'invoiceTitle',
      label: '发票抬头',
      component: 'Input',
      componentProps: {
        placeholder: '请输入发票抬头',
        allowClear: true,
      },
    },
    {
      fieldName: 'price',
      label: '来票金额(元)',
      component: 'InputNumber',
      rules: 'required',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入来票金额',
        min: 0,
        precision: 2,
      },
    },
    {
      fieldName: 'invoiceTime',
      label: '来票日期',
      component: 'DatePicker',
      rules: 'required',
      componentProps: {
        placeholder: '请选择来票日期',
        showTime: false,
        valueFormat: 'YYYY-MM-DD',
        format: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'blobIds',
      label: '发票附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        accept: ['jpg', 'jpeg', 'png', 'pdf'],
        helpText: '支持jpg、jpeg、png、pdf格式,单个文件不超过2MB',
      },
      formItemClass: 'md:col-span-2',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 4,
      },
      formItemClass: 'md:col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '来票编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入来票编号',
        allowClear: true,
      },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'auditStatus',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '未提交', value: 0 },
          { label: '审批中', value: 10 },
          { label: '审核通过', value: 20 },
          { label: '审核不通过', value: 30 },
          { label: '已作废', value: 40 },
        ],
        placeholder: '请选择审批状态',
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
      title: '来票编号',
      field: 'no',
      minWidth: 160,
      fixed: 'left',
      slots: { default: 'no' },
    },
    {
      title: '供应商',
      field: 'supplierName',
      minWidth: 150,
    },
    {
      title: '采购订单编号',
      field: 'order',
      minWidth: 160,
      slots: { default: 'orderNo' },
    },
    {
      title: '发票号码',
      field: 'invoiceNo',
      minWidth: 150,
    },
    {
      title: '来票金额(元)',
      field: 'price',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
    {
      title: '剩余可付金额',
      field: 'remainingInvoicePrice',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
    {
      title: '来票日期',
      field: 'invoiceTime',
      minWidth: 150,
      formatter: 'formatDateTime',
    },
    {
      title: '备注',
      field: 'remark',
      minWidth: 150,
    },
    {
      title: '附件',
      field: 'attachmentList',
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'attachment' },
    },
    {
      title: '审批状态',
      field: 'auditStatus',
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'auditStatus' },
    },
    {
      title: '操作',
      field: 'actions',
      minWidth: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/erp/purchase/invoice/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,221 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deletePurchaseInvoice,
  exportPurchaseInvoice,
  getPurchaseInvoicePage,
  submitPurchaseInvoice,
} from '#/api/erp/purchase/invoice';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'ErpPurchaseInvoice' });
const { push } = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const formValues = await gridApi.formApi.getValues();
  const data = await exportPurchaseInvoice(formValues);
  downloadFileFromBlobPart({ fileName: '来票.xls', source: data });
}
/** åˆ›å»ºæ¥ç¥¨ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑来票 */
function handleEdit(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
/** æŸ¥çœ‹æ¥ç¥¨è¯¦æƒ… */
function handleDetail(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** åˆ é™¤æ¥ç¥¨ */
async function handleDelete(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.no]),
    duration: 0,
  });
  try {
    await deletePurchaseInvoice(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤å®¡æ ¸ */
async function handleSubmit(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  const hideLoading = message.loading({
    content: '提交审核中...',
    duration: 0,
  });
  try {
    await submitPurchaseInvoice(row.id!);
    message.success('提交审核成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
  push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPurchaseInvoicePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseInvoiceApi.PurchaseInvoice>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="来票列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['来票']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['erp:purchase-invoice:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['erp:purchase-invoice:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #no="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.no }}
        </Button>
      </template>
      <template #orderNo="{ row }">
        <span>{{ row.order?.no ?? '--' }}</span>
      </template>
      <template #attachment="{ row }">
        <Tag v-if="!row.hasAttachment" color="warning">未上传</Tag>
        <Tag v-else color="success">已上传</Tag>
      </template>
      <template #auditStatus="{ row }">
        <Tag v-if="row.auditStatus === 0" color="default">未提交</Tag>
        <Tag v-else-if="row.auditStatus === 10" color="warning">审批中</Tag>
        <Tag v-else-if="row.auditStatus === 20" color="success">审核通过</Tag>
        <Tag v-else-if="row.auditStatus === 30" color="error">审核不通过</Tag>
        <Tag v-else-if="row.auditStatus === 40" color="default">已作废</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['erp:purchase-invoice:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['erp:purchase-invoice:update'],
              ifShow: row.auditStatus === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交审核',
              type: 'link',
              auth: ['erp:purchase-invoice:update'],
              onClick: handleSubmit.bind(null, row),
              ifShow: row.auditStatus === 0,
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['erp:purchase-invoice:query'],
              onClick: handleProcessDetail.bind(null, row),
              ifShow: row.auditStatus !== 0 && !!row.processInstanceId,
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-invoice:delete'],
              ifShow: row.auditStatus !== 20,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/erp/purchase/invoice/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createPurchaseInvoice,
  getPurchaseInvoice,
  updatePurchaseInvoice,
} from '#/api/erp/purchase/invoice';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['来票']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['来票']);
  } else {
    return '来票详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  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();
    const data = (await formApi.getValues()) as ErpPurchaseInvoiceApi.PurchaseInvoice;
    if (formData.value?.id) {
      data.id = formData.value.id;
    }
    try {
      await (formData.value?.id
        ? updatePurchaseInvoice(data)
        : createPurchaseInvoice(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema());
    await formApi.resetForm();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPurchaseInvoice(data.id);
      if (formData.value?.attachmentList?.length) {
        // ç”¨ attachmentList ä»£æ›¿ blobIds,让上传组件能展示文件名和预览链接
        (formData.value as Record<string, unknown>).blobIds =
          formData.value.attachmentList;
      }
      await formApi.setValues(formData.value as any);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/erp/purchase/invoice/modules/purchase-order-select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, nextTick, ref, watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { Input, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  getPurchaseOrder,
  getPurchaseOrderPage,
} from '#/api/erp/purchase/order';
defineOptions({ name: 'ErpPurchaseInvoiceOrderSelect' });
const props = withDefaults(
  defineProps<{
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
  }>(),
  {
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择采购订单',
  },
);
const emit = defineEmits<{
  change: [order: ErpPurchaseOrderApi.PurchaseOrder | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const open = ref(false); // é€‰æ‹©å¼¹çª—是否打开
const order = ref<ErpPurchaseOrderApi.PurchaseOrder>(); // å½“前选中采购订单
const displayNo = computed(() => order.value?.no ?? '');
/** æ ¹æ®è®¢å•编号回显选择器 */
async function resolveOrderById(id: number | undefined) {
  if (!id) {
    order.value = undefined;
    return;
  }
  if (order.value?.id === id) {
    return;
  }
  order.value = await getPurchaseOrder(id);
}
watch(
  () => props.modelValue,
  (value) => {
    resolveOrderById(value);
  },
  { immediate: true },
);
/** è¡¨æ ¼é…ç½® */
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: [
      {
        fieldName: 'no',
        label: '订单单号',
        component: 'Input',
        componentProps: {
          placeholder: '请输入订单单号',
          allowClear: true,
        },
      },
    ],
  },
  gridOptions: {
    columns: [
      { field: 'no', title: '订单单号', width: 200, fixed: 'left' },
      { field: 'supplierName', title: '供应商', minWidth: 120 },
      {
        field: 'orderTime',
        title: '订单时间',
        width: 160,
        formatter: 'formatDate',
      },
      {
        field: 'totalPrice',
        title: '含税金额',
        formatter: 'formatAmount2',
        minWidth: 120,
      },
      {
        field: 'status',
        title: '状态',
        minWidth: 100,
        cellRender: {
          name: 'CellDict',
          props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
        },
      },
    ],
    height: 520,
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPurchaseOrderPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            status: 20, // ä»…展示已审核通过的采购订单
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    radioConfig: {
      trigger: 'row',
      highlight: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseOrderApi.PurchaseOrder>,
  gridEvents: {
    radioChange: ({ row }: { row: ErpPurchaseOrderApi.PurchaseOrder }) => {
      order.value = row;
    },
  },
});
/** æ‰“开选择弹窗 */
async function handleOpen() {
  if (props.disabled) {
    return;
  }
  open.value = true;
  await nextTick();
  gridApi.query();
}
/** ç¡®è®¤é€‰æ‹©é‡‡è´­è®¢å• */
function handleOk() {
  if (!order.value?.id) {
    return;
  }
  emit('update:modelValue', order.value.id);
  emit('change', order.value);
  open.value = false;
}
</script>
<template>
  <div>
    <Input
      readonly
      :disabled="disabled"
      :placeholder="placeholder"
      :value="displayNo"
      @click="handleOpen"
    >
      <template #suffix>
        <IconifyIcon
          class="size-4 cursor-pointer"
          icon="lucide:search"
          :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
        />
      </template>
    </Input>
    <Modal
      v-model:open="open"
      title="选择采购订单(仅已审核通过)"
      width="80%"
      @cancel.stop="open = false"
      @ok.stop="handleOk"
    >
      <Grid table-title="采购订单列表" />
    </Modal>
  </div>
</template>
src/views/hrm/employee/contract/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,297 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
/** è¡¨å•类型 */
export type FormType = 'create' | 'update';
/** æ–°å¢ž/修改员工合同的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'employeeId',
      label: '员工',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        showSearch: true,
        filterOption: (input: string, option: any) =>
          option.label?.toLowerCase().includes(input.toLowerCase()),
        placeholder: '请选择员工',
      },
      rules: 'selectRequired',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'contractType',
      label: '合同类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择合同类型',
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'contractTermType',
      label: '期限类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择期限类型',
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'signCompany',
      label: '签约主体',
      component: 'Input',
      componentProps: {
        placeholder: '请输入签约主体(公司全称)',
      },
    },
    {
      fieldName: 'signDate',
      label: '签订日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择签订日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'startDate',
      label: '开始日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择开始日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'endDate',
      label: '结束日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '无固定期限可不填',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'probationStartDate',
      label: '试用期开始',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择试用期开始日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'probationEndDate',
      label: '试用期结束',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择试用期结束日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'probationSalary',
      label: '试用期工资',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入试用期工资',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'regularSalary',
      label: '转正工资',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入转正工资',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      formItemClass: 'col-span-2',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'contractNo',
      label: '合同编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入合同编号',
      },
    },
    {
      fieldName: 'employeeId',
      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: '请选择员工',
      },
    },
    {
      fieldName: 'contractType',
      label: '合同类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'),
        placeholder: '请选择合同类型',
      },
    },
    {
      fieldName: 'contractTermType',
      label: '期限类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'),
        placeholder: '请选择期限类型',
      },
    },
    {
      fieldName: 'terminateStatus',
      label: '解除终止状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERMINATE_STATUS, 'number'),
        placeholder: '请选择解除终止状态',
      },
    },
    {
      fieldName: 'status',
      label: '合同状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CONTRACT_STATUS, 'number'),
        placeholder: '请选择合同状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>['columns'] {
  return [
    { field: 'contractNo', title: '合同编号', minWidth: 160 },
    {
      field: 'parentNo',
      title: '续签自',
      minWidth: 140,
      slots: { default: 'parentNo' },
    },
    { field: 'employeeName', title: '员工姓名', minWidth: 100 },
    { field: 'employeeNo', title: '员工工号', minWidth: 130 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    {
      field: 'contractType',
      title: '合同类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CONTRACT_TYPE },
      },
    },
    {
      field: 'contractTermType',
      title: '期限类型',
      width: 130,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CONTRACT_TERM_TYPE },
      },
    },
    { field: 'signDate', title: '签订日期', width: 110 },
    { field: 'startDate', title: '开始日期', width: 110 },
    { field: 'endDate', title: '结束日期', width: 110 },
    {
      field: 'status',
      title: '合同状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CONTRACT_STATUS },
      },
    },
    {
      field: 'isCurrent',
      title: '当前合同',
      width: 90,
      slots: { default: 'isCurrent' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/employee/contract/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEmployeeContract,
  getEmployeeContractPage,
} from '#/api/hrm/employee/contract';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import Terminate from './modules/terminate.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [TerminateModal, terminateModalApi] = useVbenModal({
  connectedComponent: Terminate,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žåˆåŒ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑合同 */
function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** ç»­ç­¾åˆåŒï¼šå¸¦å…¥ä¸Šä»½åˆåŒåŸºç¡€ä¿¡æ¯ï¼Œåˆ›å»ºæ–°åˆåŒï¼ˆåŽç«¯è‡ªåŠ¨å…³è” parentId) */
function handleRenew(row: HrmEmployeeContractApi.EmployeeContract) {
  formModalApi.setData({ formType: 'create', renewFrom: row }).open();
}
/** è§£é™¤/终止合同 */
function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) {
  terminateModalApi.setData({ id: row.id }).open();
}
/** åˆ é™¤åˆåŒ */
async function handleDelete(row: HrmEmployeeContractApi.EmployeeContract) {
  await deleteEmployeeContract(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['员工合同']));
  handleRefresh();
}
/** æ˜¯å¦å¯è§£é™¤/终止:仅未解除/未终止的合同可操作 */
function isTerminable(row: HrmEmployeeContractApi.EmployeeContract) {
  return row.terminateStatus === 0;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getEmployeeContractPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <TerminateModal @success="handleRefresh" />
    <Grid table-title="员工合同列表">
      <template #parentNo="{ row }">
        <span>{{ row.parentNo || '-' }}</span>
      </template>
      <template #isCurrent="{ row }">
        <Tag v-if="row.isCurrent" color="processing">当前</Tag>
        <span v-else>-</span>
      </template>
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['员工合同']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:employee-contract:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </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),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/employee/contract/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEmployeeContract,
  getEmployeeContract,
  updateEmployeeContract,
} from '#/api/hrm/employee/contract';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isRenew = ref(false);
const getTitle = computed(() => {
  if (formType.value === 'update') {
    return '修改员工合同';
  }
  return isRenew.value ? '续签员工合同' : '新增员工合同';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  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();
    const data =
      (await formApi.getValues()) as HrmEmployeeContractApi.EmployeeContract;
    try {
      // FileUpload ç»„件的值需转换为 blobId æ•°ç»„提交
      data.blobIds = (data.blobIds ?? []).map((item: any) => item.id ?? item);
      if (data.id) {
        await updateEmployeeContract(data);
      } else {
        await createEmployeeContract(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;
      renewFrom?: HrmEmployeeContractApi.EmployeeContract;
    }>();
    formType.value = data.formType;
    isRenew.value = !data?.id && !!data?.renewFrom;
    // ç»­ç­¾ï¼šå¸¦å…¥ä¸Šä»½åˆåŒçš„基础信息(员工、类型、签约主体、工资、续签来源),日期需重新填写
    if (data?.renewFrom) {
      await formApi.resetForm();
      await formApi.setValues({
        parentId: data.renewFrom.id,
        employeeId: data.renewFrom.employeeId,
        contractType: data.renewFrom.contractType,
        contractTermType: data.renewFrom.contractTermType,
        signCompany: data.renewFrom.signCompany,
        probationSalary: data.renewFrom.probationSalary,
        regularSalary: data.renewFrom.regularSalary,
      });
      return;
    }
    if (!data?.id) {
      await formApi.resetForm();
      return;
    }
    modalApi.lock();
    try {
      const detail = await getEmployeeContract(data.id);
      await formApi.setValues(detail);
      if (detail.attachmentList?.length) {
        await formApi.setFieldValue(
          'blobIds',
          detail.attachmentList.map((item) => ({
            uid: String(item.id),
            name: item.name || '',
            url: item.url || '',
            status: 'done',
            id: item.id,
          })),
        );
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[720px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/employee/contract/modules/terminate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { terminateEmployeeContract } from '#/api/hrm/employee/contract';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const contractId = ref<number>();
/** è§£é™¤/终止表单 */
const schema: VbenFormSchema[] = [
  {
    fieldName: 'terminateStatus',
    label: '操作类型',
    component: 'RadioGroup',
    componentProps: {
      options: [
        { label: '解除', value: 1 },
        { label: '终止', value: 2 },
      ],
    },
    rules: 'required',
  },
  {
    fieldName: 'terminateDate',
    label: '解除/终止日期',
    component: 'DatePicker',
    componentProps: {
      placeholder: '请选择日期',
      valueFormat: 'YYYY-MM-DD',
      style: { width: '100%' },
    },
    rules: 'required',
  },
  {
    fieldName: 'terminateReason',
    label: '原因',
    component: 'Textarea',
    formItemClass: 'col-span-2',
    componentProps: {
      placeholder: '请输入解除/终止原因',
      rows: 3,
      maxlength: 255,
      showCount: true,
    },
    rules: 'required',
  },
];
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema,
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    try {
      await terminateEmployeeContract({ id: contractId.value!, ...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<{ id: number }>();
    contractId.value = data.id;
    await formApi.resetForm();
  },
});
</script>
<template>
  <Modal title="解除/终止合同" class="w-[520px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/employee/modules/contract-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
<script lang="ts" setup>
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { ref, watch } from 'vue';
import { Table, Tag } from 'ant-design-vue';
import { getEmployeeContractPage } from '#/api/hrm/employee/contract';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
const props = defineProps<{
  employeeId?: number;
}>();
const list = ref<HrmEmployeeContractApi.EmployeeContract[]>([]);
const loading = ref(false);
/** åˆåŒçŠ¶æ€é¢œè‰²ï¼š1-待生效 2-生效中 3-即将到期 4-已到期 5-已解除 6-已终止 */
const statusColorMap: Record<number, string> = {
  1: 'processing',
  2: 'success',
  3: 'warning',
  4: 'default',
  5: 'error',
  6: 'error',
};
/** åˆ·æ–°åˆ—表 */
async function handleRefresh() {
  if (!props.employeeId) return;
  loading.value = true;
  try {
    const res = await getEmployeeContractPage({
      pageNo: 1,
      pageSize: 100,
      employeeId: props.employeeId,
    });
    list.value = res.list;
  } finally {
    loading.value = false;
  }
}
/** ç›‘听 employeeId å˜åŒ– */
watch(
  () => props.employeeId,
  (val) => {
    if (val) {
      handleRefresh();
    }
  },
  { immediate: true },
);
const columns = [
  { title: '合同编号', dataIndex: 'contractNo', width: 150 },
  { title: '续签自', dataIndex: 'parentNo', width: 130, key: 'parentNo' },
  { title: '合同类型', dataIndex: 'contractType', width: 100, key: 'contractType' },
  { title: '期限类型', dataIndex: 'contractTermType', width: 110, key: 'contractTermType' },
  { title: '签订日期', dataIndex: 'signDate', width: 110 },
  { title: '开始日期', dataIndex: 'startDate', width: 110 },
  { title: '结束日期', dataIndex: 'endDate', width: 110 },
  { title: '合同状态', dataIndex: 'status', width: 100, key: 'status' },
  { title: '当前合同', dataIndex: 'isCurrent', width: 90, key: 'isCurrent' },
];
</script>
<template>
  <div class="contract-list">
    <Table
      :columns="columns"
      :data-source="list"
      :loading="loading"
      :pagination="false"
      size="small"
      row-key="id"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'parentNo'">
          {{ record.parentNo || '-' }}
        </template>
        <template v-else-if="column.key === 'contractType'">
          {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TYPE, record.contractType) }}
        </template>
        <template v-else-if="column.key === 'contractTermType'">
          {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, record.contractTermType) }}
        </template>
        <template v-else-if="column.key === 'status'">
          <Tag :color="statusColorMap[record.status!] || 'default'">
            {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_STATUS, record.status) }}
          </Tag>
        </template>
        <template v-else-if="column.key === 'isCurrent'">
          <Tag v-if="record.isCurrent" color="processing">当前</Tag>
          <span v-else>-</span>
        </template>
      </template>
    </Table>
  </div>
</template>
<style scoped>
.contract-list {
  padding: 0;
}
</style>
src/views/hrm/employee/modules/detail.vue
@@ -12,6 +12,7 @@
import WorkHistoryList from './work-history-list.vue';
import EmergencyContactList from './emergency-contact-list.vue';
import SocialSecurityList from './social-security-list.vue';
import ContractList from './contract-list.vue';
const props = defineProps<{
  employee: HrmEmployeeApi.Employee;
@@ -96,6 +97,9 @@
      <Tabs.TabPane key="socialSecurity" tab="社保公积金档案">
        <SocialSecurityList form-type="detail" :employee-id="employee.id" :user-id="employee.userId" />
      </Tabs.TabPane>
      <Tabs.TabPane key="contract" tab="员工合同">
        <ContractList :employee-id="employee.id" />
      </Tabs.TabPane>
    </Tabs>
  </div>
</template>