From 8f114ae7618f7a105c9f64e99c3819d3935ddc26 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 10 八月 2026 15:41:33 +0800
Subject: [PATCH] 银川 1.新增开票管理页面 2.联调回款管理和回款计划页面 3.新增设备类型的子级时,没有展示出来 4.部署不同公司时配置公司api逻辑添加
---
src/views/crm/invoice/data.ts | 315 +++++++++++++
src/api/crm/contract/index.ts | 2
src/views/crm/invoice/detail/data.ts | 134 +++++
src/views/crm/receivable/data.ts | 51 ++
src/router/routes/modules/crm.ts | 9
src/views/crm/contract/detail/data.ts | 15
src/api/crm/receivable/index.ts | 11
src/views/crm/invoice/index.vue | 259 +++++++++++
src/api/crm/permission/index.ts | 1
company.config.ts | 9
src/views/crm/receivable/plan/data.ts | 3
src/views/crm/contract/components/data.ts | 12
src/views/crm/contract/data.ts | 12
src/views/crm/invoice/detail/index.vue | 128 +++++
src/views/crm/receivable/plan/modules/form.vue | 15
src/views/mes/dv/machinery/type/index.vue | 3
vite.config.ts | 26
src/views/crm/invoice/detail/modules/info.vue | 49 ++
src/api/crm/invoice/index.ts | 122 +++++
src/views/crm/receivable/detail/data.ts | 8
src/packages/constants/src/dict-enum.ts | 1
src/views/crm/receivable/modules/form.vue | 20
src/views/crm/receivable/index.vue | 17
src/views/crm/invoice/modules/form.vue | 128 +++++
24 files changed, 1,327 insertions(+), 23 deletions(-)
diff --git a/company.config.ts b/company.config.ts
index 04c0ed2..f4436c3 100644
--- a/company.config.ts
+++ b/company.config.ts
@@ -5,6 +5,8 @@
logo: string;
/** 缃戦〉鏍囬鍥炬爣璺緞锛岀浉瀵逛簬 public 鐩綍 */
favicon: string;
+ /** API 鎺ュ彛鍦板潃 */
+ apiUrl: string;
}
/**
@@ -15,5 +17,10 @@
* 绀轰緥锛歴et "COMPANY=TEST"
*/
export const companies: Record<string, CompanyConfig> = {
- TEST: { name: '娴嬭瘯鍏徃', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico' },
+ TEST: {
+ name: '娴嬭瘯鍏徃',
+ logo: '/logos/TEST.png',
+ favicon: '/logos/TEST.ico',
+ apiUrl: 'http://42.63.70.90:9003/admin-api',
+ },
};
diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
index 4626920..7bf66e4 100644
--- a/src/api/crm/contract/index.ts
+++ b/src/api/crm/contract/index.ts
@@ -28,6 +28,8 @@
totalPrice: number;
depositPrice?: number; // 瀹氶噾閲戦锛屽崟浣嶏細鍏�
totalReceivablePrice: number;
+ totalInvoicePrice?: number; // 宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛夛紝鍗曚綅锛氬厓
+ unInvoicePrice?: number; // 鏈紑绁ㄩ噾棰濓紝鍗曚綅锛氬厓
signContactId: number;
signContactName?: string;
signUserId: number;
diff --git a/src/api/crm/invoice/index.ts b/src/api/crm/invoice/index.ts
new file mode 100644
index 0000000..121f6ec
--- /dev/null
+++ b/src/api/crm/invoice/index.ts
@@ -0,0 +1,122 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmInvoiceApi {
+ /** 鍚堝悓淇℃伅 */
+ export interface Contract {
+ id?: number;
+ name?: string;
+ no: string;
+ }
+
+ /** 闄勪欢淇℃伅 */
+ export interface Attachment {
+ id: number;
+ url?: string;
+ name?: string;
+ }
+
+ /** 寮�绁ㄤ俊鎭� */
+ export interface Invoice {
+ id?: number;
+ no?: string;
+ contractId?: number;
+ contract?: Contract;
+ customerId?: number;
+ customerName?: string;
+ invoiceType?: number;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ invoiceTime?: string;
+ auditStatus?: number;
+ remark?: string;
+ ownerUserId?: number;
+ ownerUserName?: string;
+ ownerUserDeptName?: string;
+ processInstanceId?: string;
+ attachmentList?: Attachment[];
+ blobIds?: number[];
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ creator?: string;
+ creatorName?: string;
+ createTime?: string;
+ updateTime?: string;
+ }
+
+ /** 寮�绁ㄧ畝瑕佷俊鎭紙鐢ㄤ簬鍥炴鍏宠仈閫夋嫨锛� */
+ export interface InvoiceSimple {
+ id: number;
+ no: string;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ customerId?: number;
+ contractId?: number;
+ auditStatus?: number;
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ }
+}
+
+/** 鏌ヨ寮�绁ㄥ垎椤� */
+export function getInvoicePage(params: PageParam) {
+ return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>(
+ '/crm/invoice/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ寮�绁ㄥ垎椤碉紝鍩轰簬鎸囧畾瀹㈡埛 */
+export function getInvoicePageByCustomer(params: PageParam) {
+ return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>(
+ '/crm/invoice/page-by-customer',
+ { params },
+ );
+}
+
+/** 鏌ヨ寮�绁ㄨ鎯� */
+export function getInvoice(id: number) {
+ return requestClient.get<CrmInvoiceApi.Invoice>(
+ `/crm/invoice/get?id=${id}`,
+ );
+}
+
+/** 鏂板寮�绁� */
+export function createInvoice(data: CrmInvoiceApi.Invoice) {
+ return requestClient.post('/crm/invoice/create', data);
+}
+
+/** 淇敼寮�绁� */
+export function updateInvoice(data: CrmInvoiceApi.Invoice) {
+ return requestClient.put('/crm/invoice/update', data);
+}
+
+/** 鍒犻櫎寮�绁� */
+export function deleteInvoice(id: number) {
+ return requestClient.delete(`/crm/invoice/delete?id=${id}`);
+}
+
+/** 瀵煎嚭寮�绁� */
+export function exportInvoice(params: Record<string, unknown>) {
+ return requestClient.download('/crm/invoice/export-excel', { params });
+}
+
+/** 鎻愪氦瀹℃牳 */
+export function submitInvoice(id: number) {
+ return requestClient.put(`/crm/invoice/submit?id=${id}`);
+}
+
+/** 鑾峰緱寰呭鏍稿紑绁ㄦ暟閲� */
+export function getAuditInvoiceCount() {
+ return requestClient.get<number>('/crm/invoice/audit-count');
+}
+
+/** 鏌ヨ寮�绁ㄤ笅鎷夊垪琛紙鎸夊悎鍚屾煡鍙�夊紑绁級 */
+export function getInvoiceSimpleList(contractId: number) {
+ return requestClient.get<CrmInvoiceApi.InvoiceSimple[]>(
+ `/crm/invoice/simple-list?contractId=${contractId}`,
+ );
+}
diff --git a/src/api/crm/permission/index.ts b/src/api/crm/permission/index.ts
index b95e142..dad85ce 100644
--- a/src/api/crm/permission/index.ts
+++ b/src/api/crm/permission/index.ts
@@ -43,6 +43,7 @@
CRM_PRODUCT = 6, // 浜у搧
CRM_RECEIVABLE = 7, // 鍥炴
CRM_RECEIVABLE_PLAN = 8, // 鍥炴璁″垝
+ CRM_INVOICE = 9, // 寮�绁�
}
/**
diff --git a/src/api/crm/receivable/index.ts b/src/api/crm/receivable/index.ts
index 36b3db0..35940b4 100644
--- a/src/api/crm/receivable/index.ts
+++ b/src/api/crm/receivable/index.ts
@@ -14,6 +14,8 @@
contractId?: number;
contractNo?: string;
contract?: Contract;
+ invoiceId?: number;
+ invoice?: InvoiceInfo;
auditStatus: number;
processInstanceId: number;
returnTime: Date;
@@ -36,6 +38,15 @@
totalPrice: number;
}
+ /** 寮�绁ㄤ俊鎭� */
+ export interface InvoiceInfo {
+ id?: number;
+ no?: string;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ }
+
export interface ReceivablePageParam extends PageParam {
contractId?: number;
customerId?: number;
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index ab5df28..fa057e0 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -123,6 +123,7 @@
CRM_PRODUCT_STATUS: 'crm_product_status', // CRM 鍟嗗搧鐘舵��
CRM_PRODUCT_UNIT: 'crm_product_unit', // CRM 浜у搧鍗曚綅
CRM_RECEIVABLE_RETURN_TYPE: 'crm_receivable_return_type', // CRM 鍥炴鐨勮繕娆炬柟寮�
+ CRM_INVOICE_TYPE: 'crm_invoice_type', // CRM 鍙戠エ绫诲瀷
} as const;
/** ========== AFTERSALES - 鍞悗鏈嶅姟妯″潡 ========== */
diff --git a/src/router/routes/modules/crm.ts b/src/router/routes/modules/crm.ts
index cdd75fe..c463b0b 100644
--- a/src/router/routes/modules/crm.ts
+++ b/src/router/routes/modules/crm.ts
@@ -83,6 +83,15 @@
},
component: () => import('#/views/crm/product/detail/index.vue'),
},
+ {
+ path: 'invoice/detail/:id',
+ name: 'CrmInvoiceDetail',
+ meta: {
+ title: '寮�绁ㄨ鎯�',
+ activePath: '/crm/invoice',
+ },
+ component: () => import('#/views/crm/invoice/detail/index.vue'),
+ },
],
},
];
diff --git a/src/views/crm/contract/components/data.ts b/src/views/crm/contract/components/data.ts
index c314faa..9305a46 100644
--- a/src/views/crm/contract/components/data.ts
+++ b/src/views/crm/contract/components/data.ts
@@ -53,6 +53,18 @@
},
},
{
+ title: '宸插紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'totalInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'unInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
title: '璐熻矗浜�',
field: 'ownerUserName',
minWidth: 150,
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index 2903856..7dabc09 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -415,6 +415,18 @@
},
},
{
+ title: '宸插紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'totalInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'unInvoicePrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
title: '鏈�鍚庤窡杩涙椂闂�',
field: 'contactLastTime',
minWidth: 180,
diff --git a/src/views/crm/contract/detail/data.ts b/src/views/crm/contract/detail/data.ts
index 63bc602..ae82b00 100644
--- a/src/views/crm/contract/detail/data.ts
+++ b/src/views/crm/contract/detail/data.ts
@@ -35,6 +35,11 @@
render: (val) => erpPriceInputFormatter(val) as string,
},
{
+ field: 'totalInvoicePrice',
+ label: '宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
field: 'ownerUserName',
label: '璐熻矗浜�',
},
@@ -66,6 +71,16 @@
render: (val) => erpPriceInputFormatter(val) as string,
},
{
+ field: 'totalInvoicePrice',
+ label: '宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'unInvoicePrice',
+ label: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
field: 'depositPrice',
label: '瀹氶噾閲戦锛堝厓锛�',
render: (val) => erpPriceInputFormatter(val) as string,
diff --git a/src/views/crm/invoice/data.ts b/src/views/crm/invoice/data.ts
new file mode 100644
index 0000000..b2578d9
--- /dev/null
+++ b/src/views/crm/invoice/data.ts
@@ -0,0 +1,315 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '寮�绁ㄧ紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '淇濆瓨鏃惰嚜鍔ㄧ敓鎴�',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '璐熻矗浜�',
+ component: 'ApiSelect',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '璇烽�夋嫨璐熻矗浜�',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛鍚嶇О',
+ component: markRaw(CrmCustomerSelect),
+ rules: 'required',
+ componentProps: (_values, form) => ({
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: () => {
+ form.setFieldValue('contractId', undefined);
+ form.setFieldValue('price', undefined);
+ },
+ }),
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ },
+ {
+ fieldName: 'contractId',
+ label: '鍚堝悓鍚嶇О',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId || !!values.id,
+ async componentProps(values, form) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '璇峰厛閫夋嫨瀹㈡埛',
+ };
+ }
+ const contracts = await getContractSimpleList(values.customerId);
+ const approvedContracts = contracts.filter(
+ (item) => item.auditStatus === 20,
+ );
+ return {
+ options: approvedContracts.map((item) => {
+ const remain =
+ (item.totalPrice ?? 0) - (item.totalInvoicePrice ?? 0);
+ return {
+ label: `${item.name}锛埪�${item.totalPrice ?? 0}锛屽彲寮�绁� 楼${remain.toFixed(2)}锛塦,
+ value: item.id,
+ };
+ }),
+ placeholder: '璇烽�夋嫨鍚堝悓',
+ onChange: () => {
+ form.setFieldValue('price', undefined);
+ },
+ } as any;
+ },
+ },
+ },
+ {
+ fieldName: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_INVOICE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙戠エ绫诲瀷',
+ },
+ },
+ {
+ 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: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '寮�绁ㄧ紪鍙�',
+ field: 'no',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'no' },
+ },
+ {
+ title: '瀹㈡埛鍚嶇О',
+ field: 'customerName',
+ minWidth: 150,
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '鍚堝悓缂栧彿',
+ field: 'contract',
+ minWidth: 160,
+ slots: { default: 'contractNo' },
+ },
+ {
+ title: '鍙戠エ绫诲瀷',
+ field: 'invoiceType',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_INVOICE_TYPE },
+ },
+ },
+ {
+ title: '鍙戠エ鍙风爜',
+ field: 'invoiceNo',
+ minWidth: 150,
+ },
+ {
+ title: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ field: 'remainingInvoicePrice',
+ minWidth: 180,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '寮�绁ㄦ棩鏈�',
+ field: 'invoiceTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '澶囨敞',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '璐熻矗浜�',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '鎵�灞為儴闂�',
+ field: 'ownerUserDeptName',
+ minWidth: 150,
+ },
+ {
+ title: '鏇存柊鏃堕棿',
+ field: 'updateTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鍒涘缓浜�',
+ field: 'creatorName',
+ 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/crm/invoice/detail/data.ts b/src/views/crm/invoice/detail/data.ts
new file mode 100644
index 0000000..0b2e323
--- /dev/null
+++ b/src/views/crm/invoice/detail/data.ts
@@ -0,0 +1,134 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 璇︽儏椤电殑瀛楁 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ },
+ {
+ field: 'price',
+ label: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'remainingInvoicePrice',
+ label: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val ?? 0),
+ },
+ {
+ field: 'invoiceTime',
+ label: '寮�绁ㄦ棩鏈�',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_INVOICE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ },
+ {
+ field: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ },
+ {
+ field: 'ownerUserName',
+ label: '璐熻矗浜�',
+ },
+ ];
+}
+
+/** 璇︽儏椤电殑鍩虹瀛楁 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '寮�绁ㄧ紪鍙�',
+ },
+ {
+ field: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ },
+ {
+ field: 'contract',
+ label: '鍚堝悓缂栧彿',
+ render: (val, data) =>
+ val && data?.contract?.no ? data?.contract?.no : '',
+ },
+ {
+ field: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_INVOICE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ },
+ {
+ field: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ },
+ {
+ field: 'price',
+ label: '寮�绁ㄩ噾棰�',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'remainingInvoicePrice',
+ label: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val ?? 0),
+ },
+ {
+ field: 'invoiceTime',
+ label: '寮�绁ㄦ棩鏈�',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '澶囨敞',
+ },
+ ];
+}
+
+/** 绯荤粺淇℃伅瀛楁 */
+export function useDetailSystemSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'ownerUserName',
+ label: '璐熻矗浜�',
+ },
+ {
+ field: 'creatorName',
+ label: '鍒涘缓浜�',
+ },
+ {
+ field: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '鏇存柊鏃堕棿',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/src/views/crm/invoice/detail/index.vue b/src/views/crm/invoice/detail/index.vue
new file mode 100644
index 0000000..daf9836
--- /dev/null
+++ b/src/views/crm/invoice/detail/index.vue
@@ -0,0 +1,128 @@
+<script setup lang="ts">
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+import type { SystemOperateLogApi } from '#/api/system/operate-log';
+
+import { onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { useTabs } from '@vben/hooks';
+
+import { Card, Tabs } from 'ant-design-vue';
+
+import { getInvoice } from '#/api/crm/invoice';
+import { getOperateLogPage } from '#/api/crm/operateLog';
+import { BizTypeEnum } from '#/api/crm/permission';
+import { useDescription } from '#/components/description';
+import { OperateLog } from '#/components/operate-log';
+import { ACTION_ICON, TableAction } from '#/components/table-action';
+import { $t } from '#/locales';
+import { PermissionList } from '#/views/crm/permission';
+
+import InvoiceForm from '../modules/form.vue';
+import { useDetailSchema } from './data';
+import Info from './modules/info.vue';
+
+const props = defineProps<{ id?: number }>();
+
+const route = useRoute();
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const invoiceId = ref(0);
+const invoice = ref<CrmInvoiceApi.Invoice>({} as CrmInvoiceApi.Invoice);
+const logList = ref<SystemOperateLogApi.OperateLog[]>([]);
+const permissionListRef = ref<InstanceType<typeof PermissionList>>();
+
+const [Descriptions] = useDescription({
+ bordered: false,
+ column: 4,
+ schema: useDetailSchema(),
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: InvoiceForm,
+ destroyOnClose: true,
+});
+
+/** 鍔犺浇寮�绁ㄨ鎯� */
+async function loadInvoiceDetail() {
+ loading.value = true;
+ try {
+ invoice.value = await getInvoice(invoiceId.value);
+ const res = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_INVOICE,
+ bizId: invoiceId.value,
+ });
+ logList.value = res.list;
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 杩斿洖鍒楄〃椤� */
+function handleBack() {
+ tabs.closeCurrentTab();
+ router.push({ name: 'CrmInvoice' });
+}
+
+/** 缂栬緫寮�绁� */
+function handleEdit() {
+ formModalApi.setData({ invoice: { id: invoiceId.value } }).open();
+}
+
+/** 鍔犺浇鏁版嵁 */
+onMounted(() => {
+ invoiceId.value = Number(props.id || route.params.id);
+ loadInvoiceDetail();
+});
+</script>
+
+<template>
+ <Page auto-content-height :title="invoice?.no" :loading="loading">
+ <FormModal @success="loadInvoiceDetail" />
+ <template #extra>
+ <TableAction
+ :actions="[
+ {
+ label: '杩斿洖',
+ type: 'default',
+ icon: 'lucide:arrow-left',
+ onClick: handleBack,
+ },
+ {
+ label: $t('ui.actionTitle.edit'),
+ type: 'primary',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:invoice:update'],
+ ifShow: permissionListRef?.validateWrite,
+ onClick: handleEdit,
+ },
+ ]"
+ />
+ </template>
+ <Card class="min-h-[10%]">
+ <Descriptions :data="invoice" />
+ </Card>
+ <Card class="mt-4 min-h-[60%]">
+ <Tabs>
+ <Tabs.TabPane tab="璇︾粏璧勬枡" key="1" :force-render="true">
+ <Info :invoice="invoice" />
+ </Tabs.TabPane>
+ <Tabs.TabPane tab="鎿嶄綔鏃ュ織" key="2" :force-render="true">
+ <OperateLog :log-list="logList" />
+ </Tabs.TabPane>
+ <Tabs.TabPane tab="鍥㈤槦鎴愬憳" key="3" :force-render="true">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="invoiceId"
+ :biz-type="BizTypeEnum.CRM_INVOICE"
+ :show-action="true"
+ @quit-team="handleBack"
+ />
+ </Tabs.TabPane>
+ </Tabs>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/crm/invoice/detail/modules/info.vue b/src/views/crm/invoice/detail/modules/info.vue
new file mode 100644
index 0000000..9134afd
--- /dev/null
+++ b/src/views/crm/invoice/detail/modules/info.vue
@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { Divider } from 'ant-design-vue';
+
+import { useDescription } from '#/components/description';
+
+import { useDetailBaseSchema, useDetailSystemSchema } from '../data';
+
+const props = defineProps<{
+ invoice: CrmInvoiceApi.Invoice;
+}>();
+
+const [BaseDescriptions] = useDescription({
+ title: '鍩烘湰淇℃伅',
+ bordered: false,
+ column: 4,
+ class: 'mx-4',
+ schema: useDetailBaseSchema(),
+});
+
+const [SystemDescriptions] = useDescription({
+ title: '绯荤粺淇℃伅',
+ bordered: false,
+ column: 3,
+ class: 'mx-4',
+ schema: useDetailSystemSchema(),
+});
+</script>
+
+<template>
+ <div>
+ <BaseDescriptions :data="invoice" />
+ <Divider />
+ <div v-if="props.invoice?.attachmentList?.length" class="mx-4 mb-4">
+ <h4 class="mb-2 text-base font-medium">鍙戠エ闄勪欢</h4>
+ <div
+ v-for="file in props.invoice.attachmentList"
+ :key="file.id"
+ class="flex items-center gap-2 py-1"
+ >
+ <a :href="file.url" target="_blank" rel="noopener noreferrer">
+ {{ file.name || '闄勪欢' }}
+ </a>
+ </div>
+ </div>
+ <SystemDescriptions :data="invoice" />
+ </div>
+</template>
diff --git a/src/views/crm/invoice/index.vue b/src/views/crm/invoice/index.vue
new file mode 100644
index 0000000..6adafea
--- /dev/null
+++ b/src/views/crm/invoice/index.vue
@@ -0,0 +1,259 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Tag, Tabs } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteInvoice,
+ exportInvoice,
+ getInvoicePage,
+ submitInvoice,
+} from '#/api/crm/invoice';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const { push } = useRouter();
+const sceneType = ref('1');
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 澶勭悊鍦烘櫙绫诲瀷鐨勫垏鎹� */
+function handleChangeSceneType(key: number | string) {
+ sceneType.value = key.toString();
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportInvoice({
+ sceneType: sceneType.value,
+ ...formValues,
+ });
+ downloadFileFromBlobPart({ fileName: '寮�绁�.xls', source: data });
+}
+
+/** 鍒涘缓寮�绁� */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫寮�绁� */
+function handleEdit(row: CrmInvoiceApi.Invoice) {
+ formModalApi.setData({ invoice: row }).open();
+}
+
+/** 鍒犻櫎寮�绁� */
+async function handleDelete(row: CrmInvoiceApi.Invoice) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.no]),
+ duration: 0,
+ });
+ try {
+ await deleteInvoice(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鎻愪氦瀹℃牳 */
+async function handleSubmit(row: CrmInvoiceApi.Invoice) {
+ const hideLoading = message.loading({
+ content: '鎻愪氦瀹℃牳涓�...',
+ duration: 0,
+ });
+ try {
+ await submitInvoice(row.id!);
+ message.success('鎻愪氦瀹℃牳鎴愬姛');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鏌ョ湅寮�绁ㄨ鎯� */
+function handleDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmInvoiceDetail', params: { id: row.id } });
+}
+
+/** 鏌ョ湅瀹㈡埛璇︽儏 */
+function handleCustomerDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
+}
+
+/** 鏌ョ湅鍚堝悓璇︽儏 */
+function handleContractDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmContractDetail', params: { id: row.contractId } });
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: CrmInvoiceApi.Invoice) {
+ 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 getInvoicePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ sceneType: sceneType.value,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmInvoiceApi.Invoice>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid>
+ <template #toolbar-actions>
+ <Tabs class="w-full" @change="handleChangeSceneType">
+ <Tabs.TabPane tab="鎴戣礋璐g殑" key="1" />
+ <Tabs.TabPane tab="鎴戝弬涓庣殑" key="2" />
+ <Tabs.TabPane tab="涓嬪睘璐熻矗鐨�" key="3" />
+ </Tabs>
+ </template>
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板寮�绁ㄧ敵璇�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['crm:invoice:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:invoice:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #no="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.no }}
+ </Button>
+ </template>
+ <template #customerName="{ row }">
+ <Button type="link" @click="handleCustomerDetail(row)">
+ {{ row.customerName }}
+ </Button>
+ </template>
+ <template #contractNo="{ row }">
+ <Button
+ v-if="row.contract"
+ type="link"
+ @click="handleContractDetail(row)"
+ >
+ {{ row.contract.no }}
+ </Button>
+ <span v-else>--</span>
+ </template>
+ <template #attachment="{ row }">
+ <Tag v-if="!row.hasAttachment" color="warning">
+ 鏈笂浼�
+ </Tag>
+ <Tag
+ v-else
+ color="success"
+ style="cursor: pointer"
+ @click="handleDetail(row)"
+ >
+ 宸蹭笂浼�
+ </Tag>
+ </template>
+ <template #auditStatus="{ row }">
+ <Tag v-if="row.auditStatus === 0" color="default">鏈彁浜�</Tag>
+ <Tag v-if="row.auditStatus === 10" color="warning">瀹℃壒涓�</Tag>
+ <Tag v-if="row.auditStatus === 20" color="success">瀹℃牳閫氳繃</Tag>
+ <Tag v-if="row.auditStatus === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:invoice:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃牳',
+ type: 'link',
+ auth: ['crm:invoice:update'],
+ onClick: handleSubmit.bind(null, row),
+ ifShow: row.auditStatus === 0,
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ auth: ['crm:invoice:update'],
+ onClick: handleProcessDetail.bind(null, row),
+ ifShow: row.auditStatus !== 0 && !!row.processInstanceId,
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:invoice:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/invoice/modules/form.vue b/src/views/crm/invoice/modules/form.vue
new file mode 100644
index 0000000..1cc9274
--- /dev/null
+++ b/src/views/crm/invoice/modules/form.vue
@@ -0,0 +1,128 @@
+<script lang="ts" setup>
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import {
+ createInvoice,
+ getInvoice,
+ updateInvoice,
+} from '#/api/crm/invoice';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type InvoicePrefillData = Partial<
+ Pick<CrmInvoiceApi.Invoice, 'contractId' | 'customerId'>
+>;
+
+type InvoiceFormModalData = InvoicePrefillData & {
+ invoice?: Pick<CrmInvoiceApi.Invoice, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmInvoiceApi.Invoice>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['寮�绁�'])
+ : $t('ui.actionTitle.create', ['寮�绁�']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+/** 鏋勫缓鏂板寮�绁ㄧ殑棰勫~琛ㄥ崟 */
+async function buildCreateFormData(
+ prefill: InvoicePrefillData,
+): Promise<Partial<CrmInvoiceApi.Invoice>> {
+ const values: Partial<CrmInvoiceApi.Invoice> = {
+ contractId: prefill?.contractId,
+ customerId: prefill?.customerId,
+ };
+ if (values.customerId && values.contractId) {
+ const contracts = await getContractSimpleList(values.customerId);
+ const contract = contracts.find((item) => item.id === values.contractId);
+ if (contract) {
+ values.invoiceTitle = contract.customerName;
+ }
+ }
+ return values;
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as CrmInvoiceApi.Invoice;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateInvoice(data)
+ : createInvoice(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;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | InvoiceFormModalData;
+ if (!data) {
+ return;
+ }
+ const { invoice } = data;
+ const prefill = data.customerId || data.contractId ? data : undefined;
+ modalApi.lock();
+ try {
+ if (invoice?.id) {
+ formData.value = await getInvoice(invoice.id!);
+ // 鐢� attachmentList 浠f浛 blobIds锛岃涓婁紶缁勪欢鑳藉睍绀烘枃浠跺悕鍜岄瑙堥摼鎺�
+ if (formData.value?.attachmentList?.length) {
+ (formData.value as Record<string, unknown>).blobIds =
+ formData.value.attachmentList;
+ }
+ } else if (prefill) {
+ formData.value = await buildCreateFormData(prefill);
+ }
+ if (formData.value) {
+ 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/crm/receivable/data.ts b/src/views/crm/receivable/data.ts
index 300d02b..5e3a32d 100644
--- a/src/views/crm/receivable/data.ts
+++ b/src/views/crm/receivable/data.ts
@@ -6,6 +6,7 @@
import { useUserStore } from '@vben/stores';
import { getContractSimpleList } from '#/api/crm/contract';
+import { getInvoiceSimpleList } from '#/api/crm/invoice';
import {
getReceivablePlan,
getReceivablePlanSimpleList,
@@ -63,6 +64,7 @@
placeholder: '璇烽�夋嫨瀹㈡埛',
onChange: () => {
form.setFieldValue('contractId', undefined);
+ form.setFieldValue('invoiceId', undefined);
form.setFieldValue('planId', undefined);
form.setFieldValue('price', undefined);
form.setFieldValue('returnTime', undefined);
@@ -97,6 +99,7 @@
})),
placeholder: '璇烽�夋嫨鍚堝悓',
onChange: (value: number) => {
+ form.setFieldValue('invoiceId', undefined);
form.setFieldValue('planId', undefined);
form.setFieldValue('returnTime', undefined);
form.setFieldValue('returnType', undefined);
@@ -107,6 +110,48 @@
? contract.totalPrice - contract.totalReceivablePrice
: undefined,
);
+ },
+ } as any;
+ },
+ },
+ },
+ {
+ fieldName: 'invoiceId',
+ label: '鍏宠仈寮�绁�',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['contractId', 'invoiceId'],
+ disabled: (values) => !values.contractId || !!values.id,
+ async componentProps(values, form) {
+ if (!values.contractId) {
+ return {
+ options: [],
+ placeholder: '璇峰厛閫夋嫨鍚堝悓',
+ };
+ }
+ const invoices = await getInvoiceSimpleList(values.contractId);
+ 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: '璇烽�夋嫨鍏宠仈寮�绁�',
+ onChange: (value: number) => {
+ form.setFieldValue('planId', undefined);
+ form.setFieldValue('returnTime', undefined);
+ form.setFieldValue('returnType', undefined);
+ const invoice = invoices.find((item) => item.id === value);
+ if (invoice) {
+ form.setFieldValue(
+ 'price',
+ invoice.remainingInvoicePrice ?? invoice.price ?? undefined,
+ );
+ form.setFieldValue('customerId', invoice.customerId);
+ }
},
} as any;
},
@@ -246,6 +291,12 @@
slots: { default: 'contractNo' },
},
{
+ title: '鍏宠仈寮�绁�',
+ field: 'invoice',
+ minWidth: 200,
+ slots: { default: 'invoiceNo' },
+ },
+ {
title: '鍥炴鏃ユ湡',
field: 'returnTime',
minWidth: 150,
diff --git a/src/views/crm/receivable/detail/data.ts b/src/views/crm/receivable/detail/data.ts
index dcceec5..8d1ad4d 100644
--- a/src/views/crm/receivable/detail/data.ts
+++ b/src/views/crm/receivable/detail/data.ts
@@ -55,6 +55,14 @@
val && data?.contract?.no ? data?.contract?.no : '',
},
{
+ field: 'invoice',
+ label: '鍏宠仈寮�绁�',
+ render: (val, data) =>
+ val && data?.invoice?.no
+ ? `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / 楼${data.invoice.price ?? 0}`
+ : '',
+ },
+ {
field: 'returnTime',
label: '鍥炴鏃ユ湡',
render: (val) => formatDateTime(val) as string,
diff --git a/src/views/crm/receivable/index.vue b/src/views/crm/receivable/index.vue
index 42b319b..329fdec 100644
--- a/src/views/crm/receivable/index.vue
+++ b/src/views/crm/receivable/index.vue
@@ -106,6 +106,11 @@
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
}
+/** 鏌ョ湅寮�绁ㄨ鎯� */
+function handleInvoiceDetail(row: CrmReceivableApi.Receivable) {
+ push({ name: 'CrmInvoiceDetail', params: { id: row.invoiceId } });
+}
+
/** 鏌ョ湅瀹℃壒璇︽儏 */
function handleProcessDetail(row: CrmReceivableApi.Receivable) {
push({
@@ -186,6 +191,18 @@
{{ row.customerName }}
</Button>
</template>
+ <template #invoiceNo="{ row }">
+ <template v-if="row.invoice">
+ <Button type="link" @click="handleInvoiceDetail(row)">
+ {{ row.invoice.no }}
+ </Button>
+ <span class="mx-1">/</span>
+ <span>{{ row.invoice.invoiceNo ?? '-' }}</span>
+ <span class="mx-1">/</span>
+ <span>楼{{ row.invoice.price ?? 0 }}</span>
+ </template>
+ <span v-else>--</span>
+ </template>
<template #contractNo="{ row }">
<Button
v-if="row.contract"
diff --git a/src/views/crm/receivable/modules/form.vue b/src/views/crm/receivable/modules/form.vue
index e826402..d8a3672 100644
--- a/src/views/crm/receivable/modules/form.vue
+++ b/src/views/crm/receivable/modules/form.vue
@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { CrmReceivableApi } from '#/api/crm/receivable';
-import { computed, ref } from 'vue';
+import { computed, ref, watch } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
-import { message } from 'ant-design-vue';
+import { Alert, message } from 'ant-design-vue';
import { getContractSimpleList } from '#/api/crm/contract';
import {
@@ -31,6 +31,7 @@
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmReceivableApi.Receivable>>();
+const showInvoiceHint = ref(false);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['鍥炴'])
@@ -48,6 +49,14 @@
schema: useFormSchema(),
showDefaultActions: false,
});
+
+/** 鐩戝惉鍚堝悓閫夋嫨锛屾彁绀哄厛寮�绁ㄥ悗鍥炴 */
+watch(
+ () => formApi.form?.values?.contractId,
+ (val) => {
+ showInvoiceHint.value = !!val;
+ },
+);
/** 鏋勫缓鏂板鍥炴鐨勯濉〃鍗� */
async function buildCreateFormData(
@@ -135,6 +144,13 @@
<template>
<Modal :title="getTitle" class="w-2/5">
+ <Alert
+ v-if="showInvoiceHint"
+ type="warning"
+ message="璇峰厛瀹屾垚寮�绁ㄥ苟涓婁紶鍙戠エ闄勪欢锛屽啀杩涜鍥炴鎿嶄綔"
+ show-icon
+ class="mb-4"
+ />
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/crm/receivable/plan/data.ts b/src/views/crm/receivable/plan/data.ts
index cfd71c4..ee60405 100644
--- a/src/views/crm/receivable/plan/data.ts
+++ b/src/views/crm/receivable/plan/data.ts
@@ -10,6 +10,7 @@
import { getSimpleUserList } from '#/api/system/user';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+import dayjs from 'dayjs';
import { markRaw } from 'vue';
/** 鏂板/淇敼鐨勮〃鍗� */
@@ -112,7 +113,7 @@
valueFormat: 'YYYY-MM-DD',
format: 'YYYY-MM-DD',
},
- defaultValue: new Date(),
+ defaultValue: dayjs().format('YYYY-MM-DD'),
},
{
fieldName: 'remindDays',
diff --git a/src/views/crm/receivable/plan/modules/form.vue b/src/views/crm/receivable/plan/modules/form.vue
index 882f313..2110d0d 100644
--- a/src/views/crm/receivable/plan/modules/form.vue
+++ b/src/views/crm/receivable/plan/modules/form.vue
@@ -26,6 +26,7 @@
const [Form, formApi] = useVbenForm({
commonConfig: {
+ labelWidth: 120,
componentProps: {
class: 'w-full',
},
@@ -67,17 +68,19 @@
return;
}
// 鍔犺浇鏁版嵁
+ formData.value = undefined;
+ await formApi.resetForm();
const data = modalApi.getData<CrmReceivablePlanApi.Plan>();
- if (!data || !data.id) {
- // 璁剧疆鍒� values
- await formApi.setValues(data);
+ if (!data) {
return;
}
modalApi.lock();
try {
- formData.value = await getReceivablePlan(data.id);
+ if (data.id) {
+ formData.value = await getReceivablePlan(data.id);
+ }
// 璁剧疆鍒� values
- await formApi.setValues(formData.value);
+ await formApi.setValues(formData.value ?? data);
} finally {
modalApi.unlock();
}
@@ -86,7 +89,7 @@
</script>
<template>
- <Modal :title="getTitle" class="w-2/5">
+ <Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/mes/dv/machinery/type/index.vue b/src/views/mes/dv/machinery/type/index.vue
index e12ad96..1c47fbe 100644
--- a/src/views/mes/dv/machinery/type/index.vue
+++ b/src/views/mes/dv/machinery/type/index.vue
@@ -5,7 +5,6 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
-import { handleTree } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -81,7 +80,7 @@
proxyConfig: {
ajax: {
query: async (_, formValues) =>
- handleTree(await getMachineryTypeList(formValues)),
+ await getMachineryTypeList(formValues),
},
},
rowConfig: {
diff --git a/vite.config.ts b/vite.config.ts
index 825d4d4..10f4514 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,27 +1,31 @@
import { resolve } from "node:path";
import { loadEnv } from "vite";
import { defineConfig } from "./internal/vite-config/src";
-
-/** 鍏徃閰嶇疆锛氭柊澧炲叕鍙稿彧闇�鍔犱竴琛岋紝logo 鍥剧墖鏀� public/logos/ 鐩綍 */
-const companyConfigs: Record<string, { favicon: string; logo: string; name: string }> = {
- TEST: { name: '娴嬭瘯鍏徃', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico' },
-};
+import { companies } from "./company.config";
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
const companyKey = env.VITE_COMPANY || '';
- const company = companyConfigs[companyKey];
+ const company = companies[companyKey];
const appLogo = company?.logo ?? '';
const appFavicon = company?.favicon ?? '';
- console.log('[company]', companyKey, '-> logo:', appLogo, 'favicon:', appFavicon);
+ const appApiUrl = company?.apiUrl ?? '';
+ const isDev = mode === 'development';
+ console.log('[company]', companyKey, '-> name:', company?.name, 'logo:', appLogo, 'api:', appApiUrl || '(from env)');
+
+ const defineVars: Record<string, string> = {
+ __APP_LOGO__: JSON.stringify(appLogo),
+ __APP_FAVICON__: JSON.stringify(appFavicon),
+ };
+ if (!isDev && appApiUrl) {
+ defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);
+ defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify(new URL(appApiUrl).origin);
+ }
return {
application: {},
vite: {
- define: {
- __APP_LOGO__: JSON.stringify(appLogo),
- __APP_FAVICON__: JSON.stringify(appFavicon),
- },
+ define: defineVars,
resolve: {
alias: {
'@vue-office/pdf': resolve(__dirname, 'node_modules/@vue-office/pdf/lib/v3/vue-office-pdf.mjs'),
--
Gitblit v1.9.3