银川
1.新增开票管理页面
2.联调回款管理和回款计划页面
3.新增设备类型的子级时,没有展示出来
4.部署不同公司时配置公司api逻辑添加
已修改17个文件
已添加7个文件
1348 ■■■■■ 文件已修改
company.config.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/contract/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/invoice/index.ts 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/permission/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/receivable/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/crm.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/components/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/data.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/data.ts 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/detail/data.ts 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/detail/index.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/detail/modules/info.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/index.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/modules/form.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/data.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/detail/data.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/modules/form.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/plan/data.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/plan/modules/form.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/type/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts
@@ -5,6 +5,8 @@
  logo: string;
  /** ç½‘页标题图标路径,相对于 public ç›®å½• */
  favicon: string;
  /** API æŽ¥å£åœ°å€ */
  apiUrl: string;
}
/**
@@ -15,5 +17,10 @@
 * ç¤ºä¾‹ï¼šset "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',
  },
};
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;
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}`,
  );
}
src/api/crm/permission/index.ts
@@ -43,6 +43,7 @@
  CRM_PRODUCT = 6, // äº§å“
  CRM_RECEIVABLE = 7, // å›žæ¬¾
  CRM_RECEIVABLE_PLAN = 8, // å›žæ¬¾è®¡åˆ’
  CRM_INVOICE = 9, // å¼€ç¥¨
}
/**
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;
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 - å”®åŽæœåŠ¡æ¨¡å— ========== */
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'),
      },
    ],
  },
];
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,
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,
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,
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、jpeg、png、pdf格式,单个文件不超过2MB',
      },
      formItemClass: 'md:col-span-2',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 4,
      },
      formItemClass: 'md:col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '开票编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入开票编号',
        allowClear: true,
      },
    },
    {
      fieldName: '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' },
    },
  ];
}
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,
    },
  ];
}
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>
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>
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="我负责的" 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>
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 ä»£æ›¿ 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>
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,
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,
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"
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>
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',
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 {
      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>
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: {
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'),