From f9e6eb6fc2c2e0c2a2b17238b817fcbfcb6bab56 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 10 八月 2026 16:38:45 +0800
Subject: [PATCH] feat(erp): 添加采购来票管理和付款关联功能
---
src/views/hrm/employee/modules/contract-list.vue | 107 +++
src/views/hrm/employee/contract/modules/terminate.vue | 103 +++
src/views/erp/finance/payment/data.ts | 57 +
src/views/erp/purchase/invoice/index.vue | 221 ++++++
src/views/hrm/employee/contract/index.vue | 164 +++++
src/views/hrm/employee/contract/data.ts | 297 +++++++++
src/views/erp/purchase/invoice/modules/form.vue | 104 +++
src/views/erp/finance/payment/modules/form.vue | 4
src/views/erp/purchase/invoice/data.ts | 241 +++++++
src/api/hrm/employee/contract/index.ts | 130 ++++
src/views/hrm/employee/contract/modules/form.vue | 126 +++
src/views/erp/purchase/invoice/modules/purchase-order-select.vue | 185 +++++
src/api/erp/purchase/invoice/index.ts | 113 +++
src/views/erp/finance/payment/index.vue | 7
src/views/hrm/employee/modules/detail.vue | 4
src/packages/constants/src/dict-enum.ts | 4
src/api/erp/finance/payment/index.ts | 4
17 files changed, 1,869 insertions(+), 2 deletions(-)
diff --git a/src/api/erp/finance/payment/index.ts b/src/api/erp/finance/payment/index.ts
index 7f8586d..6efa6e3 100644
--- a/src/api/erp/finance/payment/index.ts
+++ b/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; // 浼樻儬閲戦
diff --git a/src/api/erp/purchase/invoice/index.ts b/src/api/erp/purchase/invoice/index.ts
new file mode 100644
index 0000000..cac70f5
--- /dev/null
+++ b/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}`,
+ );
+}
diff --git a/src/api/hrm/employee/contract/index.ts b/src/api/hrm/employee/contract/index.ts
new file mode 100644
index 0000000..38f1d73
--- /dev/null
+++ b/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;
+ /** 缁涓婁竴浠藉悎鍚孖D */
+ 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;
+ /** 瑙i櫎缁堟鐘舵�侊細0-姝e父 1-宸茶В闄� 2-宸茬粓姝� */
+ terminateStatus?: number;
+ /** 瑙i櫎/缁堟鏃ユ湡 */
+ terminateDate?: string;
+ /** 瑙i櫎/缁堟鍘熷洜 */
+ 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;
+ }
+
+ /** 瑙i櫎/缁堟鍚堝悓璇锋眰 */
+ 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);
+}
+
+/** 瑙i櫎/缁堟鍛樺伐鍚堝悓 */
+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');
+}
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index fa057e0..b7c45f8 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/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 鍚堝悓瑙i櫎缁堟鐘舵��
} as const;
/** 瀛楀吀绫诲瀷鏋氫妇 - 缁熶竴瀵煎嚭 */
diff --git a/src/views/erp/finance/payment/data.ts b/src/views/erp/finance/payment/data.ts
index 22aa220..85cf9ad 100644
--- a/src/views/erp/finance/payment/data.ts
+++ b/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,
diff --git a/src/views/erp/finance/payment/index.vue b/src/views/erp/finance/payment/index.vue
index 3c5637d..a2aa1bc 100644
--- a/src/views/erp/finance/payment/index.vue
+++ b/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="[
diff --git a/src/views/erp/finance/payment/modules/form.vue b/src/views/erp/finance/payment/modules/form.vue
index 9d92bb2..d00de4a 100644
--- a/src/views/erp/finance/payment/modules/form.vue
+++ b/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),
diff --git a/src/views/erp/purchase/invoice/data.ts b/src/views/erp/purchase/invoice/data.ts
new file mode 100644
index 0000000..e222159
--- /dev/null
+++ b/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銆乯peg銆乸ng銆乸df鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃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' },
+ },
+ ];
+}
diff --git a/src/views/erp/purchase/invoice/index.vue b/src/views/erp/purchase/invoice/index.vue
new file mode 100644
index 0000000..6c1dd21
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/invoice/modules/form.vue b/src/views/erp/purchase/invoice/modules/form.vue
new file mode 100644
index 0000000..3450581
--- /dev/null
+++ b/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 浠f浛 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>
diff --git a/src/views/erp/purchase/invoice/modules/purchase-order-select.vue b/src/views/erp/purchase/invoice/modules/purchase-order-select.vue
new file mode 100644
index 0000000..1c6ca0b
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/employee/contract/data.ts b/src/views/hrm/employee/contract/data.ts
new file mode 100644
index 0000000..a978d4c
--- /dev/null
+++ b/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: '璇疯緭鍏ヨ浆姝e伐璧�',
+ 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: '瑙i櫎缁堟鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERMINATE_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瑙i櫎缁堟鐘舵��',
+ },
+ },
+ {
+ 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' },
+ },
+ ];
+}
diff --git a/src/views/hrm/employee/contract/index.vue b/src/views/hrm/employee/contract/index.vue
new file mode 100644
index 0000000..8f69373
--- /dev/null
+++ b/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();
+}
+
+/** 瑙i櫎/缁堟鍚堝悓 */
+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: '瑙i櫎/缁堟',
+ 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>
diff --git a/src/views/hrm/employee/contract/modules/form.vue b/src/views/hrm/employee/contract/modules/form.vue
new file mode 100644
index 0000000..d48678e
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/employee/contract/modules/terminate.vue b/src/views/hrm/employee/contract/modules/terminate.vue
new file mode 100644
index 0000000..c554c0c
--- /dev/null
+++ b/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>();
+
+/** 瑙i櫎/缁堟琛ㄥ崟 */
+const schema: VbenFormSchema[] = [
+ {
+ fieldName: 'terminateStatus',
+ label: '鎿嶄綔绫诲瀷',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '瑙i櫎', value: 1 },
+ { label: '缁堟', value: 2 },
+ ],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'terminateDate',
+ label: '瑙i櫎/缁堟鏃ユ湡',
+ 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="瑙i櫎/缁堟鍚堝悓" class="w-[520px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/employee/modules/contract-list.vue b/src/views/hrm/employee/modules/contract-list.vue
new file mode 100644
index 0000000..8f95f77
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/employee/modules/detail.vue b/src/views/hrm/employee/modules/detail.vue
index 4f30a8b..010621b 100644
--- a/src/views/hrm/employee/modules/detail.vue
+++ b/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>
--
Gitblit v1.9.3