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