spring
2026-07-03 f10cf167372f2d8c4c0e14f42f361d7ab96d8347
Merge branch 'dev_pro2.0' of http://114.132.189.42:9002/r/mom-pro2-before into dev_pro2.0
已添加12个文件
已修改37个文件
2961 ■■■■ 文件已修改
pnpm-lock.yaml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/contract/index.ts 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/report/index.ts 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/saleQuotation/index.ts 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/saleTrace/index.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/request/index.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/sale/order/index.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/crm-customer-select.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table-action/table-action.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table-action/typing.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/icons/package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/icons/src/offline-icons.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/preferences.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/data.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/data.ts 84 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/data.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/modules/info.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/index.vue 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/modules/form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/product/components/data.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/product/components/edit-table.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/report/index.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/data.ts 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/index.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/modules/form.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/modules/item-form.vue 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/data.ts 102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/index.vue 100 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/form.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/item-form.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/order-form.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/process-select-modal.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/supplier/data.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/supplier/modules/form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/item-form.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/out/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/out/modules/item-form.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/return/data.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/return/modules/item-form.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/subject/modules/form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml
@@ -1620,6 +1620,9 @@
  src/packages/icons:
    dependencies:
      '@iconify/json':
        specifier: ^2.2.483
        version: 2.2.489
      '@vben-core/icons':
        specifier: workspace:*
        version: link:../@core/base/icons
src/api/crm/contract/index.ts
@@ -26,6 +26,7 @@
    totalProductPrice: number;
    discountPercent: number;
    totalPrice: number;
    depositPrice?: number; // å®šé‡‘金额,单位:元
    totalReceivablePrice: number;
    signContactId: number;
    signContactName?: string;
@@ -38,19 +39,31 @@
    updateTime?: Date;
    products?: ContractProduct[];
    contactName?: string;
    orderId?: number; // å…³è” ERP é”€å”®è®¢å•编号
    orderNo?: string; // ERP é”€å”®è®¢å•单号
  }
  /** åˆåŒäº§å“ä¿¡æ¯ */
  export interface ContractProduct {
    id: number;
    productId: number;
    productName: string;
    productNo: string;
    productUnit: number;
    productPrice: number;
    itemId: number;
    itemName: string;
    itemCode: string;
    itemBarCode: string;
    itemSpecification?: string;
    itemUnitName: string;
    itemUnitName2?: string;
    itemUnitName3?: string;
    itemPrice: number;
    contractPrice: number;
    count: number;
    totalPrice: number;
  }
  /** å®¡æ‰¹æµç¨‹ */
  export interface ApproveProcess {
    key: string;
    name: string;
  }
}
@@ -113,8 +126,17 @@
}
/** æäº¤å®¡æ ¸ */
export function submitContract(id: number) {
  return requestClient.put(`/crm/contract/submit?id=${id}`);
export function submitContract(id: number, processDefinitionKey: string) {
  return requestClient.put('/crm/contract/submit', null, {
    params: { id, processDefinitionKey },
  });
}
/** èŽ·å–åˆåŒå®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getContractApproveProcessList() {
  return requestClient.get<CrmContractApi.ApproveProcess[]>(
    '/crm/contract/approve-process-list',
  );
}
/** åˆåŒè½¬ç§» */
@@ -131,3 +153,8 @@
export function getRemindContractCount() {
  return requestClient.get<number>('/crm/contract/remind-count');
}
/** æ ¹æ®åˆåŒç”Ÿæˆé”€å”®è®¢å• */
export function generateSaleOrder(id: number) {
  return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } });
}
src/api/crm/report/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
import { requestClient } from '#/api/request';
export namespace CrmReportApi {
  /** å®¢æˆ·åˆ†æžç»Ÿè®¡ */
  export interface CustomerAnalyze {
    totalCount: number;
    todayCount: number;
    dealCount: number;
    dealRate: number;
    lockCount: number;
    poolCount: number;
  }
  /** å•†æœºåˆ†æžç»Ÿè®¡ */
  export interface BusinessAnalyze {
    totalCount: number;
    todayCount: number;
    totalPrice: number;
    successCount: number;
    successPrice: number;
    successRate: number;
    processingCount: number;
    failCount: number;
  }
  /** åˆåŒåˆ†æžç»Ÿè®¡ */
  export interface ContractAnalyze {
    totalCount: number;
    todayCount: number;
    totalPrice: number;
    approveCount: number;
    approvePrice: number;
    processCount: number;
    receivablePrice: number;
    receivableRate: number;
  }
  /** é”€å”®æ¼æ–—阶段 */
  export interface SalesFunnelStage {
    name: string;
    count: number | null;
    price: number;
    conversionRate: number;
  }
  /** é”€å”®æ¼æ–—统计 */
  export interface SalesFunnel {
    stages: SalesFunnelStage[];
  }
  /** é”€å”®ä¸šç»©é¡¹ */
  export interface SalesPerformanceItem {
    userId: number;
    userName: string;
    deptName: string;
    contractCount: number;
    contractPrice: number;
    receivablePrice: number;
    businessCount: number;
    businessPrice: number;
  }
  /** é”€å”®ä¸šç»©ç»Ÿè®¡ */
  export interface SalesPerformance {
    items: SalesPerformanceItem[];
  }
}
/** èŽ·å–å®¢æˆ·åˆ†æžç»Ÿè®¡ */
export function getCustomerAnalyze(startTime?: string, endTime?: string) {
  return requestClient.get<CrmReportApi.CustomerAnalyze>(
    '/crm/report/get-customer-analyze',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å–å•†æœºåˆ†æžç»Ÿè®¡ */
export function getBusinessAnalyze(startTime?: string, endTime?: string) {
  return requestClient.get<CrmReportApi.BusinessAnalyze>(
    '/crm/report/get-business-analyze',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å–åˆåŒåˆ†æžç»Ÿè®¡ */
export function getContractAnalyze(startTime?: string, endTime?: string) {
  return requestClient.get<CrmReportApi.ContractAnalyze>(
    '/crm/report/get-contract-analyze',
    { params: { startTime, endTime } },
  );
}
/** èŽ·å–é”€å”®æ¼æ–—ç»Ÿè®¡ */
export function getSalesFunnel() {
  return requestClient.get<CrmReportApi.SalesFunnel>(
    '/crm/report/get-sales-funnel',
  );
}
/** èŽ·å–é”€å”®ä¸šç»©ç»Ÿè®¡ */
export function getSalesPerformance(startTime?: string, endTime?: string) {
  return requestClient.get<CrmReportApi.SalesPerformance>(
    '/crm/report/get-sales-performance',
    { params: { startTime, endTime } },
  );
}
src/api/crm/saleQuotation/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
import type { PageParam, PageResult } from '../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace CrmSaleQuotationApi {
  /** æŠ¥ä»·å•信息 */
  export interface SaleQuotation {
    id: number;
    name: string;
    no: string;
    customerId: number;
    customerName?: string;
    businessId?: number;
    businessName?: string;
    contactId?: number;
    contactName?: string;
    quotationTime: Date;
    validUntil?: Date;
    totalProductPrice: number;
    discountPercent: number;
    totalPrice: number;
    taxRate?: number;
    fileUrl?: string;
    remark?: string;
    status: number;
    contractId?: number;
    contractNo?: string;
    ownerUserId: number;
    ownerUserName?: string;
    creator: string;
    creatorName: string;
    createTime?: Date;
    updateTime?: Date;
    items?: SaleQuotationItem[];
  }
  /** æŠ¥ä»·å•物料明细 */
  export interface SaleQuotationItem {
    id: number;
    itemId: number;
    itemName: string;
    itemCode: string;
    itemBarCode?: string;
    itemSpecification?: string;
    itemUnitName: string;
    itemUnitName2?: string;
    itemUnitName3?: string;
    itemPrice: number;
    quotationPrice: number;
    count: number;
    totalPrice: number;
    taxPercent?: number;
    taxPrice?: number;
    remark?: string;
  }
}
/** æŸ¥è¯¢æŠ¥ä»·å•分页列表 */
export function getSaleQuotationPage(params: PageParam) {
  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
    '/crm/sale-quotation/page',
    { params },
  );
}
/** æŸ¥è¯¢æŠ¥ä»·å•分页列表(基于客户) */
export function getSaleQuotationPageByCustomer(params: PageParam) {
  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
    '/crm/sale-quotation/page-by-customer',
    { params },
  );
}
/** æŸ¥è¯¢æŠ¥ä»·å•分页列表(基于商机) */
export function getSaleQuotationPageByBusiness(params: PageParam) {
  return requestClient.get<PageResult<CrmSaleQuotationApi.SaleQuotation>>(
    '/crm/sale-quotation/page-by-business',
    { params },
  );
}
/** æŸ¥è¯¢æŠ¥ä»·å•详情 */
export function getSaleQuotation(id: number) {
  return requestClient.get<CrmSaleQuotationApi.SaleQuotation>(
    `/crm/sale-quotation/get?id=${id}`,
  );
}
/** æ–°å¢žæŠ¥ä»·å• */
export function createSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) {
  return requestClient.post('/crm/sale-quotation/create', data);
}
/** ä¿®æ”¹æŠ¥ä»·å• */
export function updateSaleQuotation(data: CrmSaleQuotationApi.SaleQuotation) {
  return requestClient.put('/crm/sale-quotation/update', data);
}
/** åˆ é™¤æŠ¥ä»·å• */
export function deleteSaleQuotation(id: number) {
  return requestClient.delete(`/crm/sale-quotation/delete?id=${id}`);
}
/** è½¬ä¸ºåˆåŒ */
export function convertSaleQuotationToContract(id: number) {
  return requestClient.post<number>('/crm/sale-quotation/convert-contract', null, {
    params: { id },
  });
}
src/api/crm/saleTrace/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
import { requestClient } from '#/api/request';
export namespace CrmSaleTraceApi {
  /** é”€å”®è¿½æº¯ä¿¡æ¯ */
  export interface SaleTrace {
    businessId?: number;
    businessName?: string;
    businessPrice?: number;
    businessCreateTime?: Date;
    customerId?: number;
    customerName?: string;
    contractId?: number;
    contractName?: string;
    contractNo?: string;
    contractPrice?: number;
    contractCreateTime?: Date;
    contractStatus?: number;
    orderId?: number;
    orderNo?: string;
    orderPrice?: number;
    orderCreateTime?: Date;
    receivablePrice?: number;
    receivableItems?: ReceivableItem[];
  }
  /** å›žæ¬¾æ˜Žç»† */
  export interface ReceivableItem {
    receivableId: number;
    receivableNo: string;
    price: number;
    createTime?: Date;
  }
}
/** æ ¹æ®å•†æœºç¼–号获取销售流程追溯信息 */
export function getTraceByBusinessId(businessId: number) {
  return requestClient.get<CrmSaleTraceApi.SaleTrace>(
    '/crm/sale-trace/get-by-business-id',
    { params: { businessId } },
  );
}
/** æ ¹æ®åˆåŒç¼–号获取销售流程追溯信息 */
export function getTraceByContractId(contractId: number) {
  return requestClient.get<CrmSaleTraceApi.SaleTrace>(
    '/crm/sale-trace/get-by-contract-id',
    { params: { contractId } },
  );
}
/** æ ¹æ®å®¢æˆ·ç¼–号获取销售流程追溯列表 */
export function getTraceListByCustomerId(customerId: number) {
  return requestClient.get<CrmSaleTraceApi.SaleTrace[]>(
    '/crm/sale-trace/get-list-by-customer-id',
    { params: { customerId } },
  );
}
src/api/erp/purchase/request/index.ts
@@ -7,15 +7,23 @@
  export interface PurchaseRequest {
    id?: number; // ç¼–号
    no?: string; // ç”³è¯·ç¼–号
    status?: number; // çŠ¶æ€ï¼š0未提交 10审批中 20审核通过 30审核不通过 40已取消
    requestUserId?: number; // ç”³è¯·äººç¼–号
    requestUserName?: string; // ç”³è¯·äººåç§°
    requestDeptId?: number; // ç”³è¯·éƒ¨é—¨ç¼–号
    requestDeptName?: string; // ç”³è¯·éƒ¨é—¨åç§°
    requestTime: string; // ç”³è¯·æ—¶é—´
    requestReason?: string; // ç”³è¯·ç†ç”±
    supplierId?: number; // ä¾›åº”商编号
    supplierName?: string; // ä¾›åº”商名称
    requestTime: string; // ç”³è¯·æ—¶é—´
    requesterId?: number; // ç”³è¯·äººç¼–号
    requesterName?: string; // ç”³è¯·äººåç§°
    reason?: string; // ç”³è¯·ç†ç”±
    estimatedAmount?: number; // é¢„估金额
    status?: number; // çŠ¶æ€
    discountPercent?: number; // ä¼˜æƒ çއ
    fileUrl?: string; // é™„件地址
    remark?: string; // å¤‡æ³¨
    totalCount?: number; // æ€»æ•°é‡
    totalPrice?: number; // æ€»é‡‘额
    productNames?: string; // äº§å“åç§°åˆ—表
    orderId?: number; // ç”Ÿæˆçš„采购订单编号
    orderNo?: string; // ç”Ÿæˆçš„采购订单号
    items?: PurchaseRequestItem[]; // ç”³è¯·æ˜Žç»†
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
@@ -25,11 +33,13 @@
    id?: number; // ç¼–号
    productId: number; // äº§å“ç¼–号
    productName?: string; // äº§å“åç§°
    productUnitName?: string; // äº§å“å•位
    productUnitId?: number; // äº§å“å•位编号
    productUnitName?: string; // äº§å“å•位名称
    productBarCode?: string; // äº§å“æ¡ç 
    requestCount: number; // ç”³è¯·æ•°é‡
    estimatedPrice?: number; // é¢„估单价
    requiredDate?: string; // éœ€æ±‚日期
    productPrice?: number; // äº§å“å•价(参考价)
    count: number; // æ•°é‡
    taxPercent?: number; // ç¨Žçއ
    demandTime?: string; // éœ€æ±‚日期
    remark?: string; // å¤‡æ³¨
  }
}
@@ -44,18 +54,14 @@
  return requestClient.put('/erp/purchase-request/update', data);
}
/** å®¡æ‰¹é‡‡è´­ç”³è¯· */
export function approvePurchaseRequest(params: {
  id: number;
  status: number;
  remark?: string;
}) {
  return requestClient.put('/erp/purchase-request/approve', null, { params });
/** æäº¤é‡‡è´­ç”³è¯·å®¡æ‰¹ */
export function submitPurchaseRequest(id: number, processDefinitionKey: string) {
  return requestClient.put('/erp/purchase-request/submit', null, { params: { id, processDefinitionKey } });
}
/** è½¬ä¸ºé‡‡è´­è®¢å• */
export function convertToPurchaseOrder(id: number) {
  return requestClient.put('/erp/purchase-request/convert', null, { params: { id } });
/** ç”Ÿæˆé‡‡è´­è®¢å• */
export function generatePurchaseOrder(id: number, supplierId: number) {
  return requestClient.post<number>('/erp/purchase-request/generate-order', null, { params: { id, supplierId } });
}
/** åˆ é™¤é‡‡è´­ç”³è¯· */
@@ -82,3 +88,8 @@
export function exportPurchaseRequest(params: any) {
  return requestClient.download('/erp/purchase-request/export-excel', { params });
}
/** èŽ·å–å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getApproveProcessList(id: number) {
  return requestClient.get(`/erp/purchase-request/approve-process-list`, { params: { id } });
}
src/api/erp/sale/order/index.ts
@@ -8,6 +8,7 @@
    id?: number; // è®¢å•工单编号
    no: string; // é”€å”®è®¢å•号
    customerId: number; // å®¢æˆ·ç¼–号
    customerName?: string; // å®¢æˆ·åç§°
    accountId?: number; // æ”¶æ¬¾è´¦æˆ·ç¼–号
    orderTime: Date; // è®¢å•æ—¶é—´
    totalCount: number; // åˆè®¡æ•°é‡
@@ -22,6 +23,8 @@
    discountPercent?: number; // ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
    discountPrice?: number; // ä¼˜æƒ é‡‘额,单位:元
    depositPrice?: number; // å®šé‡‘金额,单位:元
    contractId?: number; // å…³è” CRM åˆåŒç¼–号
    contractNo?: string; // CRM åˆåŒå•号
    items?: SaleOrderItem[]; // é”€å”®è®¢å•产品明细列表
  }
@@ -97,3 +100,17 @@
export function exportSaleOrder(params: any) {
  return requestClient.download('/erp/sale-order/export-excel', { params });
}
/** ä½œåºŸé”€å”®è®¢å• */
export function cancelSaleOrder(id: number) {
  return requestClient.put('/erp/sale-order/cancel', null, {
    params: { id },
  });
}
/** å¯ç”¨é”€å”®è®¢å•(恢复已作废订单) */
export function enableSaleOrder(id: number) {
  return requestClient.put('/erp/sale-order/enable', null, {
    params: { id },
  });
}
src/api/mdm/item/index.ts
@@ -14,6 +14,12 @@
    categoryName?: string;
    unitMeasureId?: number;
    unitMeasureName?: string;
    unitMeasureId2?: number;
    unitMeasureName2?: string;
    unitMeasureRate1?: number;
    unitMeasureId3?: number;
    unitMeasureName3?: string;
    unitMeasureRate2?: number;
    brandId?: number;
    brandName?: string;
    itemType: number;
@@ -26,6 +32,9 @@
    maxStock?: number;
    isBatchManaged: boolean;
    expiryDay?: number;
    warehouseId?: number;
    warehouseCode?: string;
    warehouseName?: string;
    remark?: string;
    createTime?: string;
  }
src/components/crm-customer-select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { computed, ref, watch } from 'vue';
import { Select } from 'ant-design-vue';
import { getCustomerSimpleList } from '#/api/crm/customer';
interface Props {
  value?: number;
  disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  value: undefined,
  disabled: false,
});
const emit = defineEmits(['update:value', 'change']);
const customerList = ref<CrmCustomerApi.Customer[]>([]);
const loading = ref(false);
const options = computed(() => {
  return customerList.value.map((item) => ({
    label: item.name,
    value: item.id,
  }));
});
const selectedValue = ref(props.value);
watch(
  () => props.value,
  (val) => {
    selectedValue.value = val;
  },
);
watch(selectedValue, (val) => {
  emit('update:value', val);
  const customer = customerList.value.find((item) => item.id === val);
  emit('change', customer);
});
async function loadCustomers() {
  if (customerList.value.length > 0) return;
  loading.value = true;
  try {
    customerList.value = await getCustomerSimpleList();
  } finally {
    loading.value = false;
  }
}
function filterOption(input: string, option: any) {
  return option.label.toLowerCase().includes(input.toLowerCase());
}
loadCustomers();
</script>
<template>
  <Select
    v-model:value="selectedValue"
    :options="options"
    :loading="loading"
    :disabled="disabled"
    placeholder="请选择客户"
    show-search
    :filter-option="filterOption"
    allow-clear
  />
</template>
src/components/table-action/table-action.vue
@@ -97,6 +97,8 @@
    attrs.onCancel = popConfirm.cancel;
  }
  // ä¸è®¾ç½® getPopupContainer,让它默认挂载到 body,避免被遮挡
  return attrs;
}
@@ -269,6 +271,12 @@
}
.ant-popconfirm {
  width: 210px;
  //.ant-popconfirm-message {
  //  white-space: nowrap;
  //}
  .ant-popconfirm-buttons {
    .ant-btn {
      margin-inline-start: 4px !important;
src/components/table-action/typing.ts
@@ -12,6 +12,8 @@
  cancel?: () => void;
  icon?: string;
  disabled?: boolean;
  placement?: string;
  getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
}
export interface ActionItem extends ButtonProps {
src/packages/icons/package.json
@@ -17,6 +17,7 @@
    }
  },
  "dependencies": {
    "@iconify/json": "^2.2.483",
    "@vben-core/icons": "workspace:*"
  }
}
src/packages/icons/src/offline-icons.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
/**
 * ç¦»çº¿å›¾æ ‡é¢„加载配置
 * æš‚时禁用,使用在线加载方式
 */
import { addCollection } from '@iconify/vue';
/**
 * åˆå§‹åŒ–离线图标
 * ç›®å‰ä½¿ç”¨åœ¨çº¿åŠ è½½ï¼Œæ­¤å‡½æ•°ä¸ºç©º
 */
export function initOfflineIcons() {
  // æš‚时不预加载,使用 Iconify åœ¨çº¿ API
  // å¦‚果需要离线加载,需要将 JSON æ–‡ä»¶å¤åˆ¶åˆ°é¡¹ç›®ä¸­
}
src/preferences.ts
@@ -32,6 +32,10 @@
    companyName: import.meta.env.VITE_APP_TITLE,
    companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
  },
  tabbar: {
    /** åˆ·æ–°æµè§ˆå™¨åŽåªä¿ç•™å½“前页面,不持久化所有标签页 */
    persist: false,
  },
});
export const preferencesExtension =
src/views/basicData/mdm/data.ts
@@ -6,6 +6,7 @@
import { z } from '#/adapter/form';
import { getUnitPage } from '#/api/mdm/unit';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
/** ç‰©æ–™ç±»åž‹é€‰é¡¹ */
export const ITEM_TYPE_OPTIONS = [
@@ -72,10 +73,19 @@
      rules: 'required',
    },
    {
      component: 'Input',
      fieldName: 'specification',
      label: '规格型号',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      component: 'ApiSelect',
      fieldName: 'unitMeasureId',
      label: '计量单位',
      rules: 'required',
      formItemClass: 'col-span-1',
      componentProps: {
        placeholder: '请选择计量单位',
        allowClear: true,
@@ -83,6 +93,74 @@
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'ApiSelect',
      fieldName: 'unitMeasureId2',
      label: '辅单位1',
      formItemClass: 'col-span-1',
      componentProps: {
        placeholder: '请选择辅单位1',
        allowClear: true,
        api: async () => {
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'unitMeasureRate1',
      label: '辅单位1换算比率',
      formItemClass: 'col-span-1',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 6,
        placeholder: '请输入换算比率',
      },
    },
    {
      component: 'ApiSelect',
      fieldName: 'unitMeasureId3',
      label: '辅单位2',
      formItemClass: 'col-span-1',
      componentProps: {
        placeholder: '请选择辅单位2',
        allowClear: true,
        api: async () => {
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'unitMeasureRate2',
      label: '辅单位2换算比率',
      formItemClass: 'col-span-1',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 6,
        placeholder: '请输入换算比率',
      },
    },
    {
      component: 'ApiSelect',
      fieldName: 'warehouseId',
      label: '默认仓库',
      componentProps: {
        placeholder: '请选择默认仓库',
        allowClear: true,
        api: getWarehouseSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
@@ -275,6 +353,31 @@
      minWidth: 80,
    },
    {
      field: 'unitMeasureName2',
      title: '辅单位1',
      minWidth: 80,
    },
    {
      field: 'unitMeasureRate1',
      title: '辅单位1换算比率',
      minWidth: 130,
    },
    {
      field: 'unitMeasureName3',
      title: '辅单位2',
      minWidth: 80,
    },
    {
      field: 'unitMeasureRate2',
      title: '辅单位2换算比率',
      minWidth: 130,
    },
    {
      field: 'warehouseName',
      title: '默认仓库',
      minWidth: 100,
    },
    {
      field: 'itemType',
      title: '物料类型',
      minWidth: 100,
src/views/basicData/mdm/index.vue
@@ -13,7 +13,7 @@
import {
  deleteItem,
  deleteItemList,
  exportItem,
  // exportItem,
  getItemPage,
  updateItemStatus,
} from '#/api/mdm/item';
@@ -35,10 +35,10 @@
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportItem(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '物料.xls', source: data });
}
// async function handleExport() {
//   const data = await exportItem(await gridApi.formApi.getValues());
//   downloadFileFromBlobPart({ fileName: '物料.xls', source: data });
// }
/** åˆ›å»ºç‰©æ–™ */
function handleCreate() {
@@ -156,13 +156,13 @@
              auth: ['mdm:item:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mdm:item:export'],
              onClick: handleExport,
            },
            // {
            //   label: $t('ui.actionTitle.export'),
            //   type: 'primary',
            //   icon: ACTION_ICON.DOWNLOAD,
            //   auth: ['mdm:item:export'],
            //   onClick: handleExport,
            // },
            {
              label: $t('ui.actionTitle.deleteBatch'),
              type: 'primary',
src/views/basicData/mdm/modules/form.vue
@@ -28,7 +28,7 @@
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 100,
    labelWidth: 110,
  },
  layout: 'horizontal',
  schema: useFormSchema(),
@@ -74,7 +74,7 @@
</script>
<template>
  <Modal class="w-[800px]" :title="getTitle">
  <Modal class="w-[1000px]" :title="getTitle">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/basicData/mdm/unit/data.ts
@@ -5,7 +5,6 @@
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
import { getUnitPage } from '#/api/mdm/unit';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -34,61 +33,6 @@
      rules: 'required',
      componentProps: {
        placeholder: '请输入单位名称',
      },
    },
    {
      component: 'Switch',
      fieldName: 'primaryFlag',
      label: '是否主单位',
      componentProps: {
        class: 'w-auto',
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
    },
    {
      component: 'ApiSelect',
      fieldName: 'primaryId',
      label: '主单位',
      dependencies: {
        triggerFields: ['primaryFlag'],
        show: (values) => {
          return values.primaryFlag === false;
        },
        required: (values) => {
          return values.primaryFlag === false;
        },
      },
      componentProps: {
        placeholder: '请选择主单位',
        allowClear: true,
        api: async () => {
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, primaryFlag: true, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'changeRate',
      label: '换算比例',
      dependencies: {
        triggerFields: ['primaryFlag'],
        show: (values) => {
          return values.primaryFlag === false;
        },
        required: (values) => {
          return values.primaryFlag === false;
        },
      },
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入换算比例',
      },
    },
    {
@@ -161,17 +105,6 @@
      field: 'name',
      title: '单位名称',
      minWidth: 150,
    },
    {
      field: 'primaryFlag',
      title: '主单位',
      minWidth: 80,
      slots: { default: 'primaryFlag' },
    },
    {
      field: 'changeRate',
      title: '换算比例',
      minWidth: 100,
    },
    {
      field: 'status',
src/views/crm/contract/data.ts
@@ -1,7 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '../../../packages/constants/src';
import { useUserStore } from '../../../packages/stores/src';
import { erpPriceInputFormatter, erpPriceMultiply } from '../../../packages/utils/src';
@@ -70,6 +69,18 @@
      },
    },
    {
      fieldName: 'orderDate',
      label: '下单日期',
      component: 'DatePicker',
      rules: 'required',
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        placeholder: '请选择下单日期',
      },
    },
    {
      fieldName: 'businessId',
      label: '商机名称',
      component: 'Select',
@@ -102,18 +113,6 @@
      },
    },
    {
      fieldName: 'orderDate',
      label: '下单日期',
      component: 'DatePicker',
      rules: 'required',
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        placeholder: '请选择下单日期',
      },
    },
    {
      fieldName: 'startTime',
      label: '合同开始时间',
      component: 'DatePicker',
@@ -134,6 +133,18 @@
        valueFormat: 'x',
        placeholder: '请选择合同结束时间',
      },
    },
    {
      fieldName: 'depositPrice',
      label: '定金金额(元)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入定金金额',
      },
      rules: z.number().min(0).optional(),
    },
    {
      fieldName: 'signUserId',
@@ -278,6 +289,32 @@
        allowClear: true,
      },
    },
    {
      fieldName: 'auditStatus',
      label: '合同状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '未提交', value: 0 },
          { label: '审批中', value: 10 },
          { label: '审核通过', value: 20 },
          { label: '审核不通过', value: 30 },
          { label: '已取消', value: 40 },
          { label: '已作废', value: 50 },
        ],
        placeholder: '请选择合同状态',
        allowClear: true,
      },
    },
    {
      fieldName: 'orderNo',
      label: '关联订单',
      component: 'Input',
      componentProps: {
        placeholder: '请输入订单号',
        allowClear: true,
      },
    },
  ];
}
@@ -288,6 +325,7 @@
      field: 'no',
      minWidth: 180,
      fixed: 'left',
      slots: { default: 'no' },
    },
    {
      title: '合同名称',
@@ -311,6 +349,12 @@
    {
      title: '合同金额(元)',
      field: 'totalPrice',
      minWidth: 140,
      formatter: 'formatAmount2',
    },
    {
      title: '定金金额(元)',
      field: 'depositPrice',
      minWidth: 140,
      formatter: 'formatAmount2',
    },
@@ -402,16 +446,20 @@
      field: 'auditStatus',
      fixed: 'right',
      minWidth: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
      },
      slots: { default: 'auditStatus' },
    },
    {
      title: '关联订单',
      field: 'orderNo',
      fixed: 'right',
      minWidth: 150,
      slots: { default: 'orderNo' },
    },
    {
      title: '操作',
      field: 'actions',
      fixed: 'right',
      minWidth: 130,
      minWidth: 220,
      slots: { default: 'actions' },
    },
  ];
src/views/crm/contract/detail/data.ts
@@ -20,6 +20,11 @@
      render: (val) => erpPriceInputFormatter(val) as string,
    },
    {
      field: 'depositPrice',
      label: '定金金额(元)',
      render: (val) => erpPriceInputFormatter(val) as string,
    },
    {
      field: 'orderDate',
      label: '下单时间',
      render: (val) => formatDateTime(val) as string,
@@ -61,6 +66,11 @@
      render: (val) => erpPriceInputFormatter(val) as string,
    },
    {
      field: 'depositPrice',
      label: '定金金额(元)',
      render: (val) => erpPriceInputFormatter(val) as string,
    },
    {
      field: 'orderDate',
      label: '下单时间',
      render: (val) => formatDateTime(val) as string,
@@ -84,6 +94,11 @@
      label: '公司签约人',
    },
    {
      field: 'orderNo',
      label: '销售订单',
      slot: 'orderNo', // ä½¿ç”¨ slot è‡ªå®šä¹‰æ¸²æŸ“
    },
    {
      field: 'remark',
      label: '备注',
    },
src/views/crm/contract/detail/index.vue
@@ -38,6 +38,7 @@
const contract = ref<CrmContractApi.Contract>({} as CrmContractApi.Contract); // åˆåŒè¯¦æƒ…
const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // æ“ä½œæ—¥å¿—
const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // å›¢é˜Ÿæˆå‘˜åˆ—表 Ref
const activeTab = ref('1'); // å½“前激活的 Tab
const [Descriptions] = useDescription({
  bordered: false,
@@ -128,13 +129,13 @@
      <Descriptions :data="contract" />
    </Card>
    <Card class="mt-4 min-h-[60%]">
      <Tabs>
        <Tabs.TabPane tab="跟进记录" key="1" :force-render="true">
          <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
        </Tabs.TabPane>
        <Tabs.TabPane tab="基本信息" key="2" :force-render="true">
      <Tabs v-model:activeKey="activeTab">
        <Tabs.TabPane tab="基本信息" key="1" :force-render="true">
          <ContractDetailsInfo :contract="contract" />
        </Tabs.TabPane>
        <Tabs.TabPane tab="跟进记录" key="2" :force-render="true">
          <FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
        </Tabs.TabPane>
        <Tabs.TabPane tab="产品" key="3" :force-render="true">
          <ProductDetailsList
            :biz-id="contractId"
src/views/crm/contract/detail/modules/info.vue
@@ -1,16 +1,20 @@
<script lang="ts" setup>
import type { CrmContractApi } from '#/api/crm/contract';
import { Divider } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { Button, Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineProps<{
const props = defineProps<{
  contract: CrmContractApi.Contract; // åˆåŒä¿¡æ¯
}>();
const router = useRouter();
const [BaseDescriptions] = useDescription({
  title: '基本信息',
@@ -27,11 +31,28 @@
  class: 'mx-4',
  schema: useFollowUpDetailSchema(),
});
/** æŸ¥çœ‹é”€å”®è®¢å• */
function handleViewOrder() {
  if (props.contract.orderId) {
    router.push({
      path: '/erp/sale/order',
      query: { orderId: props.contract.orderId },
    });
  }
}
</script>
<template>
  <div>
    <BaseDescriptions :data="contract" />
    <BaseDescriptions :data="contract">
      <template #orderNo="{ data }">
        <Button v-if="data.orderId" type="link" @click="handleViewOrder">
          {{ data.orderNo }}
        </Button>
        <span v-else>-</span>
      </template>
    </BaseDescriptions>
    <Divider />
    <SystemDescriptions :data="contract" />
  </div>
src/views/crm/contract/index.vue
@@ -8,12 +8,14 @@
import { Page, useVbenModal } from '../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '../../../packages/utils/src';
import { Button, message, Tabs } from 'ant-design-vue';
import { Button, message, Modal, Select, Tabs, Tag, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteContract,
  exportContract,
  generateSaleOrder,
  getContractApproveProcessList,
  getContractPage,
  submitContract,
} from '#/api/crm/contract';
@@ -76,18 +78,34 @@
  }
}
/** æäº¤å®¡æ ¸ */
/** æäº¤å®¡æ‰¹å¼¹çª— */
const submitModalVisible = ref(false);
const currentRow = ref<CrmContractApi.Contract>();
const processList = ref<CrmContractApi.ApproveProcess[]>([]);
const selectedProcessKey = ref<string>();
const submitLoading = ref(false);
async function handleSubmit(row: CrmContractApi.Contract) {
  const hideLoading = message.loading({
    content: '提交审核中...',
    duration: 0,
  });
  const res = await getContractApproveProcessList();
  processList.value = res;
  currentRow.value = row;
  selectedProcessKey.value = undefined;
  submitModalVisible.value = true;
}
async function confirmSubmit() {
  if (!selectedProcessKey.value) {
    message.warning('请选择审批流程');
    return;
  }
  submitLoading.value = true;
  try {
    await submitContract(row.id!);
    message.success('提交审核成功');
    await submitContract(currentRow.value!.id!, selectedProcessKey.value);
    message.success('提交审批成功');
    submitModalVisible.value = false;
    handleRefresh();
  } finally {
    hideLoading();
    submitLoading.value = false;
  }
}
@@ -117,6 +135,39 @@
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** ç”Ÿæˆé”€å”®è®¢å• */
async function handleGenerateOrder(row: CrmContractApi.Contract) {
  if (row.auditStatus !== 20) {
    message.warning('只有已审批的合同才能生成销售订单');
    return;
  }
  if (row.orderId) {
    message.warning('该合同已生成销售订单');
    return;
  }
  const hideLoading = message.loading({
    content: '正在生成销售订单...',
    duration: 0,
  });
  try {
    const orderId = await generateSaleOrder(row.id!);
    message.success('生成销售订单成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æŸ¥çœ‹é”€å”®è®¢å• */
function handleViewOrder(row: CrmContractApi.Contract) {
  if (row.orderId) {
    push({
      name: 'ErpSaleOrder',
      query: { id: row.orderId },
    });
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -152,7 +203,8 @@
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid>
      <template #toolbar-actions>
        <Tabs class="w-full" @change="handleChangeSceneType">
@@ -186,6 +238,16 @@
          {{ row.name }}
        </Button>
      </template>
      <template #no="{ row }">
        <Tooltip v-if="row.auditStatus === 50" title="该合同已作废,订单已失效">
          <span class="text-red-500 cursor-pointer" @click="handleDetail(row)">
            {{ row.no }}
          </span>
        </Tooltip>
        <Button v-else type="link" @click="handleDetail(row)">
          {{ row.no }}
        </Button>
      </template>
      <template #customerName="{ row }">
        <Button type="link" @click="handleCustomerDetail(row)">
          {{ row.customerName }}
@@ -201,6 +263,20 @@
          {{ row.signContactName }}
        </Button>
      </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>
        <Tag v-if="row.auditStatus === 40" color="default">已取消</Tag>
        <Tag v-if="row.auditStatus === 50" color="error">已作废</Tag>
      </template>
      <template #orderNo="{ row }">
        <Button v-if="row.orderId" type="link" @click="handleViewOrder(row)">
          {{ row.orderNo }}
        </Button>
        <span v-else>-</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -213,7 +289,7 @@
              ifShow: row.auditStatus === 0,
            },
            {
              label: '提交审核',
              label: '提交审批',
              type: 'link',
              auth: ['crm:contract:update'],
              onClick: handleSubmit.bind(null, row),
@@ -227,6 +303,20 @@
              ifShow: row.auditStatus !== 0,
            },
            {
              label: '生成销售订单',
              type: 'link',
              auth: ['crm:contract:update'],
              onClick: handleGenerateOrder.bind(null, row),
              ifShow: row.auditStatus === 20 && !row.orderId,
            },
            {
              label: '查看订单',
              type: 'link',
              auth: ['erp:sale-order:query'],
              onClick: handleViewOrder.bind(null, row),
              ifShow: !!row.orderId,
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
@@ -235,10 +325,34 @@
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
              ifShow: row.auditStatus === 0,
            },
          ]"
        />
      </template>
    </Grid>
    <!-- æäº¤å®¡æ‰¹å¼¹çª— -->
    <Modal
      v-model:open="submitModalVisible"
      title="选择审批流程"
      width="400px"
      @ok="confirmSubmit"
      :confirmLoading="submitLoading"
    >
      <Select
        v-model:value="selectedProcessKey"
        placeholder="请选择审批流程"
        class="w-full"
      >
        <Select.Option
          v-for="item in processList"
          :key="item.key"
          :value="item.key"
        >
          {{ item.name }}
        </Select.Option>
      </Select>
    </Modal>
  </Page>
</template>
src/views/crm/contract/modules/form.vue
@@ -105,7 +105,7 @@
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
  <Modal :title="getTitle" class="w-[80%]">
    <Form class="mx-4">
      <template #product="slotProps">
        <ProductEditTable
src/views/crm/product/components/data.ts
@@ -1,31 +1,29 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '../../../../packages/constants/src';
/** äº§å“è¯¦æƒ…列表的列定义 */
export function useDetailListColumns(
  showBusinessPrice: boolean,
): VxeTableGridOptions['columns'] {
  return [
    {
      field: 'productName',
      field: 'itemName',
      title: '产品名称',
    },
    {
      field: 'productNo',
      title: '产品条码',
      field: 'itemCode',
      title: '物料编码',
    },
    {
      field: 'productUnit',
      title: '产品单位',
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
      },
      field: 'itemBarCode',
      title: '物料条码',
    },
    {
      field: 'productPrice',
      title: '产品价格(元)',
      field: 'itemUnitName',
      title: '单位',
    },
    {
      field: 'itemPrice',
      title: '原价(元)',
      formatter: 'formatAmount2',
    },
    {
@@ -58,36 +56,47 @@
  return [
    { type: 'seq', title: '序号', minWidth: 50 },
    {
      field: 'productId',
      field: 'itemId',
      title: '产品名称',
      minWidth: 100,
      slots: { default: 'productId' },
      slots: { default: 'itemId' },
    },
    {
      field: 'productNo',
      field: 'itemSpecification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'itemBarCode',
      title: '条码',
      minWidth: 150,
    },
    {
      field: 'productUnit',
      field: 'itemUnitName',
      title: '单位',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
      },
      minWidth: 80,
    },
    {
      field: 'productPrice',
      title: '价格(元)',
      field: 'itemUnitName2',
      title: '辅单位1',
      minWidth: 80,
    },
    {
      field: 'itemUnitName3',
      title: '辅单位2',
      minWidth: 80,
    },
    {
      field: 'itemPrice',
      title: '原价(元)',
      minWidth: 100,
      formatter: 'formatAmount2',
    },
    {
      field: 'sellingPrice',
      title: '售价(元)',
      field: 'contractPrice',
      title: '合同价(元)',
      minWidth: 100,
      slots: { default: 'sellingPrice' },
      slots: { default: 'contractPrice' },
    },
    {
      field: 'count',
src/views/crm/product/components/edit-table.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { CrmBusinessApi } from '#/api/crm/business';
import type { CrmContractApi } from '#/api/crm/contract';
import type { CrmProductApi } from '#/api/crm/product';
import type { MdmItemApi } from '#/api/mdm/item';
import { nextTick, onMounted, ref, watch } from 'vue';
@@ -11,7 +11,7 @@
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { BizTypeEnum } from '#/api/crm/permission';
import { getProductSimpleList } from '#/api/crm/product';
import { getItemPage } from '#/api/mdm/item';
import { $t } from '#/locales';
import { useProductEditTableColumns } from './data';
@@ -39,34 +39,36 @@
}
/** åˆ‡æ¢äº§å“æ—¶åŒæ­¥åŸºç¡€ä¿¡æ¯ */
function handleProductChange(productId: any, row: any) {
  const product = productOptions.value.find((p) => p.id === productId);
function handleProductChange(itemId: any, row: any) {
  const product = productOptions.value.find((p) => p.id === itemId);
  if (!product) {
    return;
  }
  row.productUnit = product.unit;
  row.productNo = product.no;
  row.productPrice = product.price;
  row.sellingPrice = product.price;
  row.count = 0;
  row.totalPrice = 0;
  row.itemId = itemId;
  row.itemName = product.name;
  row.itemCode = product.code;
  row.itemBarCode = product.barCode;
  row.itemUnitName = product.unitMeasureName;
  row.itemUnitName2 = product.unitMeasureName2;
  row.itemUnitName3 = product.unitMeasureName3;
  row.itemSpecification = product.specification;
  row.itemPrice = product.salesPrice;
  row.contractPrice = product.salesPrice;
  row.count = row.count || 1;
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  handleUpdateValue(row);
}
/** é‡‘额变动时重新计算合计 */
function handlePriceChange(row: any) {
  row.totalPrice = erpPriceMultiply(row.sellingPrice, row.count) ?? 0;
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  handleUpdateValue(row);
}
/** å°†æœ€æ–°æ•°æ®å†™å›žå¹¶é€šçŸ¥çˆ¶ç»„ä»¶ */
function handleUpdateValue(row: any) {
  const index = tableData.value.findIndex((item) => item.id === row.id);
  if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
    row.businessPrice = row.sellingPrice;
  } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
    row.contractPrice = row.sellingPrice;
  }
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  if (index === -1) {
    row.id = tableData.value.length + 1;
    tableData.value.push(row);
@@ -110,15 +112,6 @@
    }
    await nextTick();
    tableData.value = products;
    if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
      tableData.value.forEach((item) => {
        item.sellingPrice = item.businessPrice;
      });
    } else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
      tableData.value.forEach((item) => {
        item.sellingPrice = item.contractPrice;
      });
    }
    await gridApi.grid.reloadData(tableData.value);
  },
  {
@@ -127,26 +120,27 @@
);
/** åˆå§‹åŒ– */
const productOptions = ref<CrmProductApi.Product[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
const productOptions = ref<MdmItemApi.Item[]>([]); // ç‰©æ–™ä¸‹æ‹‰é€‰é¡¹
onMounted(async () => {
  productOptions.value = await getProductSimpleList();
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
});
</script>
<template>
  <Grid class="w-full">
    <template #productId="{ row }">
    <template #itemId="{ row }">
      <Select
        v-model:value="row.productId"
        v-model:value="row.itemId"
        :options="productOptions"
        :field-names="{ label: 'name', value: 'id' }"
        class="w-full"
        @change="handleProductChange($event, row)"
      />
    </template>
    <template #sellingPrice="{ row }">
    <template #contractPrice="{ row }">
      <InputNumber
        v-model:value="row.sellingPrice"
        v-model:value="row.contractPrice"
        :min="0.001"
        :precision="2"
        @change="handlePriceChange(row)"
@@ -180,7 +174,7 @@
            type: 'link',
            danger: true,
            popConfirm: {
              title: $t('ui.actionMessage.deleteConfirm', [row.name]),
              title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
              confirm: handleDelete.bind(null, row),
            },
          },
src/views/crm/report/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,174 @@
<script lang="ts" setup>
import type { CrmReportApi } from '#/api/crm/report';
import { computed, onMounted, ref } from 'vue';
import { Page } from '../../../packages/effects/common-ui/src';
import { Card, Col, DatePicker, Row, Statistic, Table } from 'ant-design-vue';
import dayjs from 'dayjs';
import {
  getBusinessAnalyze,
  getContractAnalyze,
  getCustomerAnalyze,
  getSalesFunnel,
  getSalesPerformance,
} from '#/api/crm/report';
defineOptions({ name: 'CrmReportDashboard' });
const loading = ref(false);
const dateRange = ref<[string, string] | null>(null);
const customerAnalyze = ref<CrmReportApi.CustomerAnalyze>();
const businessAnalyze = ref<CrmReportApi.BusinessAnalyze>();
const contractAnalyze = ref<CrmReportApi.ContractAnalyze>();
const salesFunnel = ref<CrmReportApi.SalesFunnel>();
const salesPerformance = ref<CrmReportApi.SalesPerformance>();
const startTime = computed(() =>
  dateRange.value?.[0] ? dayjs(dateRange.value[0]).format('YYYY-MM-DD') : undefined,
);
const endTime = computed(() =>
  dateRange.value?.[1] ? dayjs(dateRange.value[1]).format('YYYY-MM-DD') : undefined,
);
const performanceColumns = [
  { title: '姓名', dataIndex: 'userName', width: 120 },
  { title: '部门', dataIndex: 'deptName', width: 150 },
  { title: '合同数量', dataIndex: 'contractCount', width: 100 },
  { title: '合同金额', dataIndex: 'contractPrice', width: 120, formatter: 'formatAmount2' },
  { title: '回款金额', dataIndex: 'receivablePrice', width: 120, formatter: 'formatAmount2' },
  { title: '商机数量', dataIndex: 'businessCount', width: 100 },
  { title: '商机金额', dataIndex: 'businessPrice', width: 120, formatter: 'formatAmount2' },
];
async function loadData() {
  loading.value = true;
  try {
    const [customer, business, contract, funnel, performance] = await Promise.all([
      getCustomerAnalyze(startTime.value, endTime.value),
      getBusinessAnalyze(startTime.value, endTime.value),
      getContractAnalyze(startTime.value, endTime.value),
      getSalesFunnel(),
      getSalesPerformance(startTime.value, endTime.value),
    ]);
    customerAnalyze.value = customer;
    businessAnalyze.value = business;
    contractAnalyze.value = contract;
    salesFunnel.value = funnel;
    salesPerformance.value = performance;
  } finally {
    loading.value = false;
  }
}
function handleDateChange() {
  loadData();
}
onMounted(() => {
  loadData();
});
</script>
<template>
  <Page auto-content-height>
    <div class="p-4">
      <Card title="CRM æ•°æ®æ¦‚览" class="mb-4">
        <div class="mb-4">
          <DatePicker.RangePicker
            v-model:value="dateRange"
            :placeholder="['开始日期', '结束日期']"
            format="YYYY-MM-DD"
            @change="handleDateChange"
          />
        </div>
      </Card>
      <Row :gutter="16" class="mb-4">
        <Col :span="6">
          <Card title="客户分析" :loading="loading">
            <Statistic title="总客户数" :value="customerAnalyze?.totalCount || 0" />
            <Statistic title="今日新增" :value="customerAnalyze?.todayCount || 0" class="mt-2" />
            <Statistic title="成交客户" :value="customerAnalyze?.dealCount || 0" class="mt-2" />
            <Statistic
              title="成交率"
              :value="customerAnalyze?.dealRate || 0"
              :precision="2"
              suffix="%"
              class="mt-2"
            />
          </Card>
        </Col>
        <Col :span="6">
          <Card title="商机分析" :loading="loading">
            <Statistic title="总商机数" :value="businessAnalyze?.totalCount || 0" />
            <Statistic title="商机金额" :value="businessAnalyze?.totalPrice || 0" :precision="2" class="mt-2" />
            <Statistic title="成功商机" :value="businessAnalyze?.successCount || 0" class="mt-2" />
            <Statistic
              title="成功率"
              :value="businessAnalyze?.successRate || 0"
              :precision="2"
              suffix="%"
              class="mt-2"
            />
          </Card>
        </Col>
        <Col :span="6">
          <Card title="合同分析" :loading="loading">
            <Statistic title="总合同数" :value="contractAnalyze?.totalCount || 0" />
            <Statistic title="合同金额" :value="contractAnalyze?.totalPrice || 0" :precision="2" class="mt-2" />
            <Statistic title="审批中" :value="contractAnalyze?.processCount || 0" class="mt-2" />
            <Statistic
              title="回款率"
              :value="contractAnalyze?.receivableRate || 0"
              :precision="2"
              suffix="%"
              class="mt-2"
            />
          </Card>
        </Col>
        <Col :span="6">
          <Card title="待处理" :loading="loading">
            <Statistic title="客户公海" :value="customerAnalyze?.poolCount || 0" />
            <Statistic title="锁定客户" :value="customerAnalyze?.lockCount || 0" class="mt-2" />
            <Statistic title="进行中商机" :value="businessAnalyze?.processingCount || 0" class="mt-2" />
            <Statistic title="失败商机" :value="businessAnalyze?.failCount || 0" class="mt-2" />
          </Card>
        </Col>
      </Row>
      <Row :gutter="16">
        <Col :span="12">
          <Card title="销售漏斗" :loading="loading">
            <Table
              :dataSource="salesFunnel?.stages || []"
              :columns="[
                { title: '阶段', dataIndex: 'name', width: 150 },
                { title: '数量', dataIndex: 'count', width: 100 },
                { title: '金额', dataIndex: 'price', width: 120 },
                { title: '转化率', dataIndex: 'conversionRate', width: 100, customRender: ({ text }) => `${text}%` },
              ]"
              :pagination="false"
              size="small"
              rowKey="name"
            />
          </Card>
        </Col>
        <Col :span="12">
          <Card title="销售业绩" :loading="loading">
            <Table
              :dataSource="salesPerformance?.items || []"
              :columns="performanceColumns"
              :pagination="false"
              size="small"
              rowKey="userId"
            />
          </Card>
        </Col>
      </Row>
    </div>
  </Page>
</template>
src/views/crm/saleQuotation/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,413 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { erpPriceMultiply } from '#/packages/utils/src';
import { z } from '#/adapter/form';
import { getSimpleBusinessList } from '#/api/crm/business';
import { getSimpleContactList } from '#/api/crm/contact';
import { getCustomerSimpleList } from '#/api/crm/customer';
import { getItemPage } from '#/api/mdm/item';
import { getSimpleUserList } from '#/api/system/user';
import { useUserStore } from '#/packages/stores/src';
/** æŠ¥ä»·å•状态 */
export const QUOTATION_STATUS = {
  DRAFT: 0, // è‰ç¨¿
  CONVERTED: 10, // å·²è½¬åˆåŒ
};
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  const userStore = useUserStore();
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'no',
      label: '报价单号',
      component: 'Input',
      componentProps: {
        placeholder: '保存时自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '报价单名称',
      component: 'Input',
      rules: 'required',
      componentProps: {
        placeholder: '请输入报价单名称',
      },
    },
    {
      fieldName: 'ownerUserId',
      label: '负责人',
      component: 'ApiSelect',
      componentProps: {
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
      },
      defaultValue: userStore.userInfo?.id,
      rules: 'required',
    },
    {
      fieldName: 'customerId',
      label: '客户名称',
      component: 'ApiSelect',
      rules: 'required',
      componentProps: {
        api: getCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择客户',
      },
    },
    {
      fieldName: 'businessId',
      label: '商机名称',
      component: 'Select',
      componentProps: {
        options: [],
        placeholder: '请选择商机',
        allowClear: true,
      },
      dependencies: {
        triggerFields: ['customerId'],
        disabled: (values) => !values.customerId,
        async componentProps(values) {
          if (!values.customerId) {
            return { options: [], placeholder: '请选择客户' };
          }
          const res = await getSimpleBusinessList();
          const list = res.filter((item) => item.customerId === values.customerId);
          return {
            options: list.map((item) => ({ label: item.name, value: item.id })),
            placeholder: '请选择商机',
          };
        },
      },
    },
    {
      fieldName: 'contactId',
      label: '联系人',
      component: 'Select',
      componentProps: {
        options: [],
        placeholder: '请选择联系人',
        allowClear: true,
      },
      dependencies: {
        triggerFields: ['customerId'],
        disabled: (values) => !values.customerId,
        async componentProps(values) {
          if (!values.customerId) {
            return { options: [], placeholder: '请选择客户' };
          }
          const res = await getSimpleContactList();
          const list = res.filter((item) => item.customerId === values.customerId);
          return {
            options: list.map((item) => ({ label: item.name, value: item.id })),
            placeholder: '请选择联系人',
          };
        },
      },
    },
    {
      fieldName: 'quotationTime',
      label: '报价日期',
      component: 'DatePicker',
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        placeholder: '请选择报价日期',
      },
    },
    {
      fieldName: 'validUntil',
      label: '有效期至',
      component: 'DatePicker',
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        placeholder: '请选择有效期',
      },
    },
    {
      fieldName: 'taxRate',
      label: '税率(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        max: 100,
        precision: 2,
        placeholder: '请输入税率',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
    {
      fieldName: 'items',
      label: '物料清单',
      component: 'Input',
      formItemClass: 'col-span-3',
    },
    {
      fieldName: 'totalProductPrice',
      label: '物料总金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        disabled: true,
      },
      rules: z.number().min(0).optional().default(0),
    },
    {
      fieldName: 'discountPercent',
      label: '整单折扣(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入折扣',
      },
      rules: z.number().min(0).max(100).optional().default(0),
    },
    {
      fieldName: 'totalPrice',
      label: '折扣后金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        disabled: true,
      },
      dependencies: {
        triggerFields: ['totalProductPrice', 'discountPercent'],
        trigger(values, form) {
          const discountPrice =
            erpPriceMultiply(values.totalProductPrice, values.discountPercent / 100) ?? 0;
          form.setFieldValue('totalPrice', values.totalProductPrice - discountPrice);
        },
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '报价单号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入报价单号',
        allowClear: true,
      },
    },
    {
      fieldName: 'name',
      label: '报价单名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入报价单名称',
        allowClear: true,
      },
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: 'ApiSelect',
      componentProps: {
        api: getCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择客户',
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: [
          { label: '草稿', value: 0 },
          { label: '已转合同', value: 10 },
        ],
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
      type: 'checkbox',
      width: 50,
      fixed: 'left',
    },
    {
      field: 'no',
      title: '报价单号',
      minWidth: 180,
      fixed: 'left',
    },
    {
      field: 'name',
      title: '报价单名称',
      minWidth: 160,
      fixed: 'left',
    },
    {
      field: 'customerName',
      title: '客户名称',
      minWidth: 120,
    },
    {
      field: 'businessName',
      title: '商机名称',
      minWidth: 130,
    },
    {
      field: 'totalPrice',
      title: '报价金额(元)',
      minWidth: 140,
      formatter: 'formatAmount2',
    },
    {
      field: 'quotationTime',
      title: '报价日期',
      minWidth: 120,
      formatter: 'formatDateTime',
    },
    {
      field: 'validUntil',
      title: '有效期至',
      minWidth: 120,
      formatter: 'formatDateTime',
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 100,
      slots: { default: 'status' },
    },
    {
      field: 'contractNo',
      title: '关联合同',
      minWidth: 150,
    },
    {
      field: 'ownerUserName',
      title: '负责人',
      minWidth: 120,
    },
    {
      field: 'createTime',
      title: '创建时间',
      minWidth: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
/** ç‰©æ–™æ˜Žç»†è¡¨æ ¼åˆ— */
export function useItemColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
    {
      field: 'itemId',
      title: '物料名称',
      minWidth: 200,
      slots: { default: 'itemId' },
    },
    {
      field: 'itemSpecification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'itemBarCode',
      title: '条码',
      minWidth: 120,
    },
    {
      field: 'itemUnitName',
      title: '单位',
      minWidth: 80,
    },
    {
      field: 'itemUnitName2',
      title: '辅单位1',
      minWidth: 80,
    },
    {
      field: 'itemUnitName3',
      title: '辅单位2',
      minWidth: 80,
    },
    {
      field: 'itemPrice',
      title: '原价(元)',
      minWidth: 100,
      formatter: 'formatAmount2',
    },
    {
      field: 'quotationPrice',
      title: '报价(元)',
      minWidth: 100,
      slots: { default: 'quotationPrice' },
    },
    {
      field: 'count',
      title: '数量',
      minWidth: 100,
      slots: { default: 'count' },
    },
    {
      field: 'totalPrice',
      title: '合计',
      minWidth: 100,
      formatter: 'formatAmount2',
    },
    {
      title: '操作',
      width: 50,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/saleQuotation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { Button, message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  convertSaleQuotationToContract,
  deleteSaleQuotation,
  getSaleQuotationPage,
} from '#/api/crm/saleQuotation';
import { $t } from '#/locales';
import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const { push } = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºæŠ¥ä»·å• */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑报价单 */
function handleEdit(row: CrmSaleQuotationApi.SaleQuotation) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤æŠ¥ä»·å• */
async function handleDelete(row: CrmSaleQuotationApi.SaleQuotation) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteSaleQuotation(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æŸ¥çœ‹æŠ¥ä»·å•详情 */
function handleDetail(row: CrmSaleQuotationApi.SaleQuotation) {
  push({ name: 'CrmSaleQuotationDetail', params: { id: row.id } });
}
/** æŸ¥çœ‹å®¢æˆ·è¯¦æƒ… */
function handleCustomerDetail(row: CrmSaleQuotationApi.SaleQuotation) {
  push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** æŸ¥çœ‹å•†æœºè¯¦æƒ… */
function handleBusinessDetail(row: CrmSaleQuotationApi.SaleQuotation) {
  if (row.businessId) {
    push({ name: 'CrmBusinessDetail', params: { id: row.businessId } });
  }
}
/** æŸ¥çœ‹åˆåŒè¯¦æƒ… */
function handleContractDetail(row: CrmSaleQuotationApi.SaleQuotation) {
  if (row.contractId) {
    push({ name: 'CrmContractDetail', params: { id: row.contractId } });
  }
}
/** è½¬ä¸ºåˆåŒ */
async function handleConvertContract(row: CrmSaleQuotationApi.SaleQuotation) {
  Modal.confirm({
    title: '确认转合同',
    content: '确认将该报价单转为合同?',
    async onOk() {
      const contractId = await convertSaleQuotationToContract(row.id!);
      message.success('转合同成功');
      push({ name: 'CrmContractDetail', params: { id: contractId } });
    },
  });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getSaleQuotationPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<CrmSaleQuotationApi.SaleQuotation>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid>
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['报价单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:sale-quotation:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #name="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.name }}
        </Button>
      </template>
      <template #customerName="{ row }">
        <Button type="link" @click="handleCustomerDetail(row)">
          {{ row.customerName }}
        </Button>
      </template>
      <template #businessName="{ row }">
        <Button v-if="row.businessId" type="link" @click="handleBusinessDetail(row)">
          {{ row.businessName }}
        </Button>
        <span v-else>-</span>
      </template>
      <template #contractNo="{ row }">
        <Button v-if="row.contractId" type="link" @click="handleContractDetail(row)">
          {{ row.contractNo }}
        </Button>
        <span v-else>-</span>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === QUOTATION_STATUS.DRAFT" color="default">草稿</Tag>
        <Tag v-if="row.status === QUOTATION_STATUS.CONVERTED" color="success">已转合同</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:sale-quotation:update'],
              onClick: handleEdit.bind(null, row),
              ifShow: row.status === QUOTATION_STATUS.DRAFT,
            },
            {
              label: '转合同',
              type: 'link',
              auth: ['crm:sale-quotation:update'],
              onClick: handleConvertContract.bind(null, row),
              ifShow: row.status === QUOTATION_STATUS.DRAFT && !row.contractId,
            },
            {
              label: '查看合同',
              type: 'link',
              onClick: handleContractDetail.bind(null, row),
              ifShow: !!row.contractId,
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              auth: ['crm:sale-quotation:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
              ifShow: row.status === QUOTATION_STATUS.DRAFT,
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/crm/saleQuotation/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
<script lang="ts" setup>
import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { erpPriceMultiply } from '#/packages/utils/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createSaleQuotation,
  getSaleQuotation,
  updateSaleQuotation,
} from '#/api/crm/saleQuotation';
import { $t } from '#/locales';
import ItemForm from './item-form.vue';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmSaleQuotationApi.SaleQuotation>();
const itemFormRef = ref();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['报价单'])
    : $t('ui.actionTitle.create', ['报价单']);
});
const isEdit = computed(() => !!formData.value?.id);
const isDisabled = computed(() => isEdit.value && formData.value?.status !== 0);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(),
  showDefaultActions: false,
});
/** è®¡ç®—物料总金额 */
async function calculateTotalPrice() {
  const items = itemFormRef.value?.getData() || [];
  const totalProductPrice = items.reduce(
    (prev: number, curr: any) => prev + (curr.totalPrice || 0),
    0,
  );
  await formApi.setFieldValue('totalProductPrice', totalProductPrice);
  const discountPercent = (await formApi.getValues())?.discountPercent || 0;
  const discountPrice = erpPriceMultiply(totalProductPrice, discountPercent / 100) ?? 0;
  await formApi.setFieldValue('totalPrice', totalProductPrice - discountPrice);
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    try {
      await itemFormRef.value?.validate();
    } catch (e: any) {
      message.error(e.message);
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as CrmSaleQuotationApi.SaleQuotation;
    data.items = itemFormRef.value?.getData();
    try {
      await (formData.value?.id ? updateSaleQuotation(data) : createSaleQuotation(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<CrmSaleQuotationApi.SaleQuotation>();
    if (!data || !data.id) {
      formData.value = {} as CrmSaleQuotationApi.SaleQuotation;
      await formApi.resetForm();
      // æ–°å¢žæ—¶ï¼Œé‡ç½®å¹¶æ·»åŠ ä¸€è¡Œç©ºæ•°æ®
      itemFormRef.value?.resetData();
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getSaleQuotation(data.id);
      await formApi.setValues(formData.value);
      // ç¼–辑时设置物料数据
      itemFormRef.value?.setData(formData.value.items || []);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[80%]">
    <Form class="mx-4">
      <template #items>
        <ItemForm
          ref="itemFormRef"
          class="w-full"
          :disabled="isDisabled"
          @change="calculateTotalPrice"
        />
      </template>
    </Form>
  </Modal>
</template>
src/views/crm/saleQuotation/modules/item-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
import { computed, onMounted, ref } from 'vue';
import { erpPriceMultiply } from '#/packages/utils/src';
import { InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemPage } from '#/api/mdm/item';
import { useItemColumns } from '../data';
interface Props {
  disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  disabled: false,
});
const emit = defineEmits(['change']);
const tableData = ref<CrmSaleQuotationApi.SaleQuotationItem[]>([]);
const itemOptions = ref<MdmItemApi.Item[]>([]);
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
const summaries = computed(() => {
  return tableData.value.reduce(
    (acc, item) => {
      acc.count += item.count || 0;
      acc.totalPrice += item.totalPrice || 0;
      return acc;
    },
    { count: 0, totalPrice: 0 },
  );
});
/** è¡¨æ ¼é…ç½® */
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useItemColumns(),
    data: [],
    minHeight: 250,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'seq',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
/** è§¦å‘变更事件 */
function triggerChange() {
  emit('change');
}
/** å¤„理新增 */
function handleAdd() {
  tableData.value.push({
    itemId: undefined,
    itemName: undefined,
    itemCode: undefined,
    itemBarCode: undefined,
    itemSpecification: undefined,
    itemUnitName: undefined,
    itemUnitName2: undefined,
    itemUnitName3: undefined,
    itemPrice: undefined,
    quotationPrice: undefined,
    count: 1,
    totalPrice: 0,
  });
  gridApi.grid.loadData(tableData.value);
}
/** å¤„理删除 */
function handleDelete(row: CrmSaleQuotationApi.SaleQuotationItem) {
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index !== -1) {
    tableData.value.splice(index, 1);
    gridApi.grid.loadData(tableData.value);
    triggerChange();
  }
}
/** å¤„理物料变更 */
function handleItemChange(itemId: number, row: any) {
  const item = itemOptions.value.find((p) => p.id === itemId);
  if (!item) {
    return;
  }
  row.itemId = itemId;
  row.itemName = item.name;
  row.itemCode = item.code;
  row.itemBarCode = item.barCode;
  row.itemSpecification = item.specification;
  row.itemUnitName = item.unitMeasureName;
  row.itemUnitName2 = item.unitMeasureName2;
  row.itemUnitName3 = item.unitMeasureName3;
  row.itemPrice = item.salesPrice;
  row.quotationPrice = item.salesPrice;
  row.totalPrice = erpPriceMultiply(row.quotationPrice, row.count) ?? 0;
  triggerChange();
}
/** å¤„理报价变更 */
function handleQuotationPriceChange(value: number, row: any) {
  row.quotationPrice = value;
  row.totalPrice = erpPriceMultiply(value, row.count) ?? 0;
  triggerChange();
}
/** å¤„理数量变更 */
function handleCountChange(value: number, row: any) {
  row.count = value;
  row.totalPrice = erpPriceMultiply(row.quotationPrice, value) ?? 0;
  triggerChange();
}
/** è¡¨å•校验 */
function validate() {
  for (let i = 0; i < tableData.value.length; i++) {
    const item = tableData.value[i];
    if (item) {
      if (!item.itemId) {
        throw new Error(`第 ${i + 1} è¡Œï¼šç‰©æ–™ä¸èƒ½ä¸ºç©º`);
      }
      if (!item.count || item.count <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šæ•°é‡ä¸èƒ½ä¸ºç©º`);
      }
      if (!item.quotationPrice || item.quotationPrice <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šæŠ¥ä»·ä¸èƒ½ä¸ºç©º`);
      }
    }
  }
}
/** è®¾ç½®æ•°æ®ï¼ˆä¾›çˆ¶ç»„件调用) */
function setData(items: CrmSaleQuotationApi.SaleQuotationItem[]) {
  items.forEach((item) => {
    if (item.quotationPrice && item.count) {
      item.totalPrice = erpPriceMultiply(item.quotationPrice, item.count) ?? 0;
    }
  });
  tableData.value = [...items];
  gridApi.grid.loadData(tableData.value);
}
/** èŽ·å–æ•°æ® */
function getData() {
  return [...tableData.value];
}
/** é‡ç½®æ•°æ® */
function resetData() {
  tableData.value = [];
  handleAdd();
}
defineExpose({
  validate,
  setData,
  getData,
  resetData,
});
/** åˆå§‹åŒ– */
onMounted(async () => {
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  itemOptions.value = res.list || [];
});
</script>
<template>
  <Grid class="w-full">
    <template #itemId="{ row }">
      <Select
        v-if="!disabled"
        v-model:value="row.itemId"
        :options="itemOptions"
        :field-names="{ label: 'name', value: 'id' }"
        class="w-full"
        placeholder="请选择物料"
        @change="(val) => handleItemChange(val, row)"
      />
      <span v-else>{{ row.itemName || '-' }}</span>
    </template>
    <template #quotationPrice="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.quotationPrice"
        :min="0"
        :precision="2"
        @change="(val) => handleQuotationPriceChange(val, row)"
      />
      <span v-else>{{ row.quotationPrice || '-' }}</span>
    </template>
    <template #count="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.count"
        :min="0.001"
        :precision="3"
        @change="(val) => handleCountChange(val, row)"
      />
      <span v-else>{{ row.count || '-' }}</span>
    </template>
    <template #actions="{ row }">
      <TableAction
        v-if="!disabled"
        :actions="[
          {
            label: '删除',
            type: 'link',
            danger: true,
            popConfirm: {
              title: '确认删除该物料吗?',
              confirm: handleDelete.bind(null, row),
            },
          },
        ]"
      />
    </template>
    <template #bottom>
      <div class="mt-2 rounded border border-border bg-muted p-2">
        <div class="flex justify-between text-sm text-muted-foreground">
          <span class="font-medium text-foreground">合计:</span>
          <div class="flex space-x-4">
            <span>数量:{{ summaries.count }}</span>
            <span>金额:{{ summaries.totalPrice.toFixed(2) }}元</span>
          </div>
        </div>
      </div>
      <TableAction
        v-if="!disabled"
        class="mt-2 flex justify-center"
        :actions="[
          {
            label: '添加物料',
            type: 'default',
            onClick: handleAdd,
          },
        ]"
      />
    </template>
  </Grid>
</template>
src/views/erp/purchase/request/data.ts
@@ -14,10 +14,11 @@
/** çŠ¶æ€å¸¸é‡ */
export const PURCHASE_REQUEST_STATUS = {
  PENDING: 0, // å¾…审批
  APPROVED: 1, // å·²å®¡æ‰¹
  REJECTED: 2, // å·²æ‹’绝
  CONVERTED: 3, // å·²è½¬è®¢å•
  DRAFT: 0, // æœªæäº¤ï¼ˆè‰ç¨¿ï¼‰
  PROCESSING: 10, // å®¡æ‰¹ä¸­
  APPROVED: 20, // å®¡æ ¸é€šè¿‡
  REJECTED: 30, // å®¡æ ¸ä¸é€šè¿‡
  CANCELLED: 40, // å·²å–消
};
/** è¡¨å•的配置项 */
@@ -54,7 +55,7 @@
      rules: 'required',
    },
    {
      label: '供应商',
      label: '供应商(可选)',
      fieldName: 'supplierId',
      component: 'ApiSelect',
      componentProps: {
@@ -68,12 +69,37 @@
      },
    },
    {
      fieldName: 'reason',
      fieldName: 'discountPercent',
      label: '优惠率(%)',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入优惠率',
        min: 0,
        max: 100,
        precision: 2,
        disabled: formType === 'detail',
      },
    },
    {
      fieldName: 'requestReason',
      label: '申请理由',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入申请理由',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: formType === 'detail',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'fileUrl',
      label: '附件',
      component: 'FileUpload',
      componentProps: {
        placeholder: '请上传附件',
        maxNumber: 5,
        maxSize: 20,
        accept: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png'],
        disabled: formType === 'detail',
      },
      formItemClass: 'col-span-2',
@@ -121,18 +147,18 @@
      minWidth: 80,
    },
    {
      field: 'requestCount',
      title: '申请数量',
      field: 'count',
      title: '数量',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'requestCount' },
      slots: { default: 'count' },
    },
    {
      field: 'estimatedPrice',
      title: '预估单价',
      field: 'productPrice',
      title: '单价(参考价)',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'estimatedPrice' },
      slots: { default: 'productPrice' },
    },
    {
      field: 'totalPrice',
@@ -142,11 +168,18 @@
      formatter: 'formatAmount2',
    },
    {
      field: 'requiredDate',
      field: 'taxPercent',
      title: '税率(%)',
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'taxPercent' },
    },
    {
      field: 'demandTime',
      title: '需求日期',
      minWidth: 140,
      fixed: 'right',
      slots: { default: 'requiredDate' },
      slots: { default: 'demandTime' },
    },
    {
      field: 'remark',
@@ -199,7 +232,7 @@
      },
    },
    {
      fieldName: 'requesterId',
      fieldName: 'requestUserId',
      label: '申请人',
      component: 'ApiSelect',
      componentProps: {
@@ -217,10 +250,11 @@
      component: 'Select',
      componentProps: {
        options: [
          { label: '待审批', value: 0 },
          { label: '已审批', value: 1 },
          { label: '已拒绝', value: 2 },
          { label: '已转订单', value: 3 },
          { label: '未提交', value: 0 },
          { label: '审批中', value: 10 },
          { label: '审核通过', value: 20 },
          { label: '审核不通过', value: 30 },
          { label: '已取消', value: 40 },
        ],
        placeholder: '请选择状态',
        allowClear: true,
@@ -255,15 +289,37 @@
      formatter: 'formatDate',
    },
    {
      field: 'requesterName',
      field: 'requestUserName',
      title: '申请人',
      minWidth: 120,
    },
    {
      field: 'estimatedAmount',
      field: 'requestDeptName',
      title: '申请部门',
      minWidth: 120,
    },
    {
      field: 'totalCount',
      title: '申请数量',
      minWidth: 120,
      formatter: 'formatAmount3',
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'productNames',
      title: '申请产品',
      minWidth: 150,
      showOverflow: 'tooltip',
    },
    {
      field: 'orderNo',
      title: '采购订单',
      minWidth: 180,
    },
    {
      field: 'status',
@@ -272,7 +328,7 @@
      slots: { default: 'status' },
    },
    {
      field: 'reason',
      field: 'requestReason',
      title: '申请理由',
      minWidth: 150,
      showOverflow: 'tooltip',
@@ -285,7 +341,7 @@
    },
    {
      title: '操作',
      width: 200,
      width: 240,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/erp/purchase/request/index.vue
@@ -4,29 +4,41 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { confirm, Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  approvePurchaseRequest,
  convertToPurchaseOrder,
  deletePurchaseRequest,
  exportPurchaseRequest,
  getApproveProcessList,
  getPurchaseRequestPage,
  submitPurchaseRequest,
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema, PURCHASE_REQUEST_STATUS } from './data';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import OrderForm from './modules/order-form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
/** ERP é‡‡è´­ç”³è¯·åˆ—表 */
defineOptions({ name: 'ErpPurchaseRequest' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [OrderFormModal, orderFormModalApi] = useVbenModal({
  connectedComponent: OrderForm,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -66,34 +78,29 @@
  }
}
/** å®¡æ‰¹é‡‡è´­ç”³è¯· */
async function handleApprove(row: ErpPurchaseRequestApi.PurchaseRequest, status: number) {
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit(row: ErpPurchaseRequestApi.PurchaseRequest) {
  const hideLoading = message.loading({
    content: `确定${status === 1 ? '审批通过' : '审批拒绝'}该申请吗?`,
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    await approvePurchaseRequest({ id: row.id!, status, remark: '' });
    message.success(`${status === 1 ? '审批通过' : '审批拒绝'}成功`);
    handleRefresh();
  } finally {
    const processList = await getApproveProcessList(row.id!);
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: row.id, processList }).open();
  } catch {
    hideLoading();
  }
}
/** è½¬ä¸ºé‡‡è´­è®¢å• */
async function handleConvert(row: ErpPurchaseRequestApi.PurchaseRequest) {
  const hideLoading = message.loading({
    content: '正在转为采购订单...',
    duration: 0,
  });
  try {
    await convertToPurchaseOrder(row.id!);
    message.success('已成功转为采购订单');
    handleRefresh();
  } finally {
    hideLoading();
  }
/** ç”Ÿæˆé‡‡è´­è®¢å• */
function handleGenerateOrder(row: ErpPurchaseRequestApi.PurchaseRequest) {
  orderFormModalApi.setData({ id: row.id, supplierId: row.supplierId }).open();
}
const checkedIds = ref<number[]>([]);
@@ -148,6 +155,8 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <OrderFormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="采购申请列表">
      <template #toolbar-tools>
        <TableAction
@@ -182,10 +191,11 @@
        />
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 0" color="warning">待审批</Tag>
        <Tag v-else-if="row.status === 1" color="success">已审批</Tag>
        <Tag v-else-if="row.status === 2" color="error">已拒绝</Tag>
        <Tag v-else-if="row.status === 3" color="default">已转订单</Tag>
        <Tag v-if="row.status === 0" color="default">未提交</Tag>
        <Tag v-else-if="row.status === 10" color="processing">审批中</Tag>
        <Tag v-else-if="row.status === 20" color="success">审核通过</Tag>
        <Tag v-else-if="row.status === 30" color="error">审核不通过</Tag>
        <Tag v-else-if="row.status === 40" color="warning">已取消</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
@@ -206,35 +216,17 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '审批通过',
              label: '提交',
              type: 'link',
              auth: ['erp:purchase-request:approve'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: '确认审批通过该申请吗?',
                confirm: handleApprove.bind(null, row, 1),
              },
              ifShow: () => row.status === 0 || row.status === 30,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '审批拒绝',
              label: '生成订单',
              type: 'link',
              danger: true,
              auth: ['erp:purchase-request:approve'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: '确认审批拒绝该申请吗?',
                confirm: handleApprove.bind(null, row, 2),
              },
            },
            {
              label: '转订单',
              type: 'link',
              auth: ['erp:purchase-request:convert'],
              ifShow: () => row.status === 1,
              popConfirm: {
                title: '确认转为采购订单吗?',
                confirm: handleConvert.bind(null, row),
              },
              auth: ['erp:purchase-request:generate-order'],
              ifShow: () => row.status === 20 && !row.orderId,
              onClick: handleGenerateOrder.bind(null, row),
            },
            {
              label: $t('common.delete'),
@@ -253,4 +245,4 @@
      </template>
    </Grid>
  </Page>
</template>
</template>
src/views/erp/purchase/request/modules/form.vue
@@ -7,7 +7,7 @@
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -17,7 +17,7 @@
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
import { useFormSchema, PURCHASE_REQUEST_STATUS } from '../data';
import { useFormSchema } from '../data';
import PurchaseRequestItemForm from './item-form.vue';
const emit = defineEmits(['success']);
@@ -78,8 +78,13 @@
    const data =
      (await formApi.getValues()) as ErpPurchaseRequestApi.PurchaseRequest;
    data.items = formData.value?.items?.map((item) => ({
      ...item,
      id: undefined,
      productId: item.productId,
      productUnitId: item.productUnitId,
      productPrice: item.productPrice,
      count: item.count,
      taxPercent: item.taxPercent,
      demandTime: item.demandTime,
      remark: item.remark,
    }));
    try {
      await (formType.value === 'create'
src/views/erp/purchase/request/modules/item-form.vue
@@ -35,10 +35,10 @@
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
const summaries = computed(() => {
  return {
    count: tableData.value.reduce((sum, item) => sum + (item.requestCount || 0), 0),
    count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0),
    totalPrice: tableData.value.reduce(
      (sum, item) => {
        const price = erpPriceMultiply(item.estimatedPrice || 0, item.requestCount || 0) ?? 0;
        const price = erpPriceMultiply(item.productPrice || 0, item.count || 0) ?? 0;
        return sum + price;
      },
      0,
@@ -89,11 +89,13 @@
  const newRow = {
    id: undefined,
    productId: undefined,
    productUnitId: undefined,
    productUnitName: undefined,
    productBarCode: undefined,
    requestCount: 1,
    estimatedPrice: undefined,
    requiredDate: undefined,
    count: 1,
    productPrice: undefined,
    taxPercent: undefined,
    demandTime: undefined,
    remark: undefined,
    totalPrice: 0,
  };
@@ -118,10 +120,11 @@
  }
  row.productId = productId;
  row.productBarCode = product.barCode;
  row.productUnitId = product.unitMeasureId;
  row.productUnitName = product.unitMeasureName;
  row.productName = product.name;
  row.estimatedPrice = product.purchasePrice || 0;
  row.requestCount = row.requestCount || 1;
  row.productPrice = product.purchasePrice || 0;
  row.count = row.count || 1;
  handleRowChange(row);
}
@@ -134,16 +137,16 @@
    tableData.value[index] = row;
  }
  // è®¡ç®—预估金额
  if (row.estimatedPrice && row.requestCount) {
    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
  if (row.productPrice && row.count) {
    row.totalPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
  }
  emit('update:items', [...tableData.value]);
}
/** åˆå§‹åŒ–行数据 */
function initRow(row: ErpPurchaseRequestApi.PurchaseRequestItem) {
  if (row.estimatedPrice && row.requestCount) {
    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
  if (row.productPrice && row.count) {
    row.totalPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
  }
}
@@ -155,8 +158,8 @@
      if (!item.productId) {
        throw new Error(`第 ${i + 1} è¡Œï¼šäº§å“ä¸èƒ½ä¸ºç©º`);
      }
      if (!item.requestCount || item.requestCount <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šç”³è¯·æ•°é‡ä¸èƒ½ä¸ºç©º`);
      if (!item.count || item.count <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šæ•°é‡ä¸èƒ½ä¸ºç©º`);
      }
    }
  }
@@ -191,35 +194,46 @@
      />
      <span v-else>{{ row.productName || '-' }}</span>
    </template>
    <template #requestCount="{ row }">
    <template #count="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.requestCount"
        v-model:value="row.count"
        :min="0"
        :precision="3"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ erpCountInputFormatter(row.requestCount) || '-' }}</span>
      <span v-else>{{ erpCountInputFormatter(row.count) || '-' }}</span>
    </template>
    <template #estimatedPrice="{ row }">
    <template #productPrice="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.estimatedPrice"
        v-model:value="row.productPrice"
        :min="0"
        :precision="2"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ erpPriceInputFormatter(row.estimatedPrice) || '-' }}</span>
      <span v-else>{{ erpPriceInputFormatter(row.productPrice) || '-' }}</span>
    </template>
    <template #requiredDate="{ row }">
    <template #taxPercent="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.taxPercent"
        :min="0"
        :max="100"
        :precision="2"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ row.taxPercent ? `${row.taxPercent}%` : '-' }}</span>
    </template>
    <template #demandTime="{ row }">
      <DatePicker
        v-if="!disabled"
        v-model:value="row.requiredDate"
        v-model:value="row.demandTime"
        valueFormat="YYYY-MM-DD"
        class="w-full"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ row.requiredDate || '-' }}</span>
      <span v-else>{{ row.demandTime || '-' }}</span>
    </template>
    <template #remark="{ row }">
      <Input v-if="!disabled" v-model:value="row.remark" class="w-full" @change="handleRowChange(row)" />
@@ -246,7 +260,7 @@
        <div class="flex justify-between text-sm text-muted-foreground">
          <span class="font-medium text-foreground">合计:</span>
          <div class="flex space-x-4">
            <span>申请数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>预估金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}</span>
          </div>
        </div>
@@ -264,4 +278,4 @@
      />
    </template>
  </Grid>
</template>
</template>
src/views/erp/purchase/request/modules/order-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import { ref, computed } from 'vue';
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { message, Select } from 'ant-design-vue';
import { generatePurchaseOrder } from '#/api/erp/purchase/request';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
const emit = defineEmits(['success']);
const requestId = ref<number>();
const supplierId = ref<number>();
const supplierOptions = ref<{ id: number; name: string }[]>([]);
const supplierSelectOptions = computed(() => {
  return supplierOptions.value.map((s) => ({
    label: s.name,
    value: s.id,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!supplierId.value) {
      message.warning('请选择供应商');
      return;
    }
    modalApi.lock();
    try {
      const orderId = await generatePurchaseOrder(requestId.value!, supplierId.value!);
      await modalApi.close();
      emit('success');
      message.success(`生成采购订单成功,订单编号:${orderId}`);
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      requestId.value = undefined;
      supplierId.value = undefined;
      return;
    }
    const data = modalApi.getData<{ id: number; supplierId?: number }>();
    requestId.value = data.id;
    supplierId.value = data.supplierId;
    // èŽ·å–ä¾›åº”å•†åˆ—è¡¨
    supplierOptions.value = await getSupplierSimpleList();
  },
});
</script>
<template>
  <Modal title="生成采购订单" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">选择供应商:</span>
      <Select
        v-model:value="supplierId"
        :options="supplierSelectOptions"
        style="width: 200px"
        placeholder="请选择供应商"
        show-search
        :filter-option="(input: string, option: any) => option.label.toLowerCase().includes(input.toLowerCase())"
      />
    </div>
  </Modal>
</template>
src/views/erp/purchase/request/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
<script lang="ts" setup>
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import { ref, computed } from 'vue';
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { message, Select } from 'ant-design-vue';
import { submitPurchaseRequest } from '#/api/erp/purchase/request';
interface ProcessDefinition {
  id: string;
  name: string;
  version: number;
  key: string;
}
const emit = defineEmits(['success']);
const requestId = ref<number>();
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: `${p.name} (V${p.version})`,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await submitPurchaseRequest(requestId.value!, selectedProcessKey.value);
      await modalApi.close();
      emit('success');
      message.success('提交成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      requestId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
    requestId.value = data.id;
    processList.value = data.processList || [];
    // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªæµç¨‹
    if (processList.value.length > 0) {
      selectedProcessKey.value = processList.value[0].key;
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">审批流程:</span>
      <Select
        v-model:value="selectedProcessKey"
        :options="processOptions"
        style="width: 200px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>
src/views/erp/purchase/supplier/data.ts
@@ -116,6 +116,17 @@
      rules: z.number().default(0),
    },
    {
      fieldName: 'sort',
      label: '排序',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入排序',
        min: 0,
        class: '!w-full',
      },
      rules: z.number().default(0),
    },
    {
      fieldName: 'qualification',
      label: '资质信息',
      component: 'Textarea',
src/views/erp/purchase/supplier/modules/form.vue
@@ -30,7 +30,7 @@
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 100,
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
src/views/erp/sale/order/data.ts
@@ -1,14 +1,12 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '../../../../packages/constants/src';
import { getDictOptions } from '../../../../packages/effects/hooks/src';
import { erpPriceInputFormatter } from '../../../../packages/utils/src';
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getCustomerSimpleList } from '#/api/erp/sale/customer';
import { getItemPage } from '#/api/mdm/item';
import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -55,7 +53,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
@@ -192,6 +190,11 @@
      slots: { default: 'productId' },
    },
    {
      field: 'productSpecification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'stockCount',
      title: '库存',
      minWidth: 80,
@@ -205,6 +208,16 @@
    {
      field: 'productUnitName',
      title: '单位',
      minWidth: 80,
    },
    {
      field: 'productUnitName2',
      title: '辅单位1',
      minWidth: 80,
    },
    {
      field: 'productUnitName3',
      title: '辅单位2',
      minWidth: 80,
    },
    {
@@ -285,7 +298,10 @@
        placeholder: '请选择产品',
        allowClear: true,
        showSearch: true,
        api: getProductSimpleList,
        api: async () => {
          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
@@ -307,7 +323,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
@@ -330,7 +346,11 @@
      label: '状态',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
        options: [
          { label: '未审核', value: 10 },
          { label: '已审核', value: 20 },
          { label: '已作废', value: 30 },
        ],
        placeholder: '请选择状态',
        allowClear: true,
      },
@@ -388,6 +408,7 @@
      title: '订单单号',
      width: 200,
      fixed: 'left',
      slots: { default: 'no' },
    },
    {
      field: 'productNames',
@@ -399,6 +420,12 @@
      field: 'customerName',
      title: '客户',
      minWidth: 120,
    },
    {
      field: 'contractNo',
      title: '关联合同',
      minWidth: 150,
      slots: { default: 'contractNo' },
    },
    {
      field: 'orderTime',
@@ -450,11 +477,9 @@
    {
      field: 'status',
      title: '状态',
      minWidth: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
      },
      minWidth: 100,
      fixed: 'right',
      slots: { default: 'status' },
    },
    {
      title: '操作',
src/views/erp/sale/order/index.vue
@@ -3,15 +3,18 @@
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
import { message } from 'ant-design-vue';
import { message, Tag, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelSaleOrder,
  deleteSaleOrder,
  enableSaleOrder,
  exportSaleOrder,
  getSaleOrderPage,
  updateSaleOrderStatus,
@@ -23,6 +26,8 @@
/** ERP é”€å”®è®¢å•列表 */
defineOptions({ name: 'ErpSaleOrder' });
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -83,6 +88,36 @@
  }
}
/** ä½œåºŸé”€å”®è®¢å• */
async function handleCancel(row: ErpSaleOrderApi.SaleOrder) {
  const hideLoading = message.loading({
    content: '正在作废该订单...',
    duration: 0,
  });
  try {
    await cancelSaleOrder(row.id!);
    message.success('作废成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å¯ç”¨é”€å”®è®¢å•(恢复已作废订单) */
async function handleEnable(row: ErpSaleOrderApi.SaleOrder) {
  const hideLoading = message.loading({
    content: '正在启用该订单...',
    duration: 0,
  });
  try {
    await enableSaleOrder(row.id!);
    message.success('启用成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
@@ -95,6 +130,16 @@
/** æŸ¥çœ‹è¯¦æƒ… */
function handleDetail(row: ErpSaleOrderApi.SaleOrder) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** æŸ¥çœ‹å…³è”合同 */
function handleViewContract(row: ErpSaleOrderApi.SaleOrder) {
  if (row.contractId) {
    router.push({
      path: '/crm/contract/detail',
      query: { id: row.contractId },
    });
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -167,6 +212,27 @@
          ]"
        />
      </template>
      <template #no="{ row }">
        <Tooltip v-if="row.status === 30" title="该订单已作废,订单已失效">
          <span class="text-red-500 cursor-pointer" @click="handleDetail(row)">
            {{ row.no }}
          </span>
        </Tooltip>
        <a v-else class="text-primary cursor-pointer" @click="handleDetail(row)">
          {{ row.no }}
        </a>
      </template>
      <template #contractNo="{ row }">
        <a v-if="row.contractId" class="text-primary cursor-pointer" @click="handleViewContract(row)">
          {{ row.contractNo }}
        </a>
        <span v-else>-</span>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 10" color="warning">未审核</Tag>
        <Tag v-if="row.status === 20" color="success">已审核</Tag>
        <Tag v-if="row.status === 30" color="error">已作废</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -182,21 +248,51 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['erp:sale-order:update'],
              ifShow: () => row.status !== 20,
              ifShow: () => row.status === 10,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: row.status === 10 ? '审批' : '反审批',
              label: '审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['erp:sale-order:update-status'],
              ifShow: () => row.status === 10,
              popConfirm: {
                title: `确认${row.status === 10 ? '审批' : '反审批'}${row.no}吗?`,
                confirm: handleUpdateStatus.bind(
                  null,
                  row,
                  row.status === 10 ? 20 : 10,
                ),
                title: `确认审批${row.no}吗?`,
                confirm: handleUpdateStatus.bind(null, row, 20),
              },
            },
            {
              label: '反审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['erp:sale-order:update-status'],
              ifShow: () => row.status === 20,
              popConfirm: {
                title: `确认反审批${row.no}吗?`,
                confirm: handleUpdateStatus.bind(null, row, 10),
              },
            },
            {
              label: '作废',
              type: 'link',
              icon: 'ant-design:stop-outlined',
              auth: ['erp:sale-order:cancel'],
              ifShow: () => row.status === 20,
              popConfirm: {
                title: `确认作废${row.no}吗?作废后不可恢复!`,
                confirm: handleCancel.bind(null, row),
              },
            },
            {
              label: '启用',
              type: 'link',
              icon: 'ant-design:check-circle-outlined',
              auth: ['erp:sale-order:enable'],
              ifShow: () => row.status === 30,
              popConfirm: {
                title: `确认启用${row.no}吗?`,
                confirm: handleEnable.bind(null, row),
              },
            },
            {
@@ -205,6 +301,7 @@
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['erp:sale-order:delete'],
              ifShow: () => row.status === 10,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, [row.id!]),
src/views/erp/sale/order/modules/item-form.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ErpProductApi } from '#/api/erp/product/product';
import type { MdmItemApi } from '#/api/mdm/item';
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -13,7 +13,7 @@
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getItemPage } from '#/api/mdm/item';
import { getStockCount } from '#/api/erp/stock/stock';
import { useFormItemColumns } from '../data';
@@ -37,7 +37,7 @@
]);
const tableData = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); // è¡¨æ ¼æ•°æ®
const productOptions = ref<ErpProductApi.Product[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
const productOptions = ref<MdmItemApi.Item[]>([]); // ç‰©æ–™ä¸‹æ‹‰é€‰é¡¹
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
const summaries = computed(() => {
@@ -158,12 +158,15 @@
    return;
  }
  row.productId = productId;
  row.productUnitId = product.unitId;
  row.productUnitId = product.unitMeasureId;
  row.productBarCode = product.barCode;
  row.productUnitName = product.unitName;
  row.productUnitName = product.unitMeasureName;
  row.productUnitName2 = product.unitMeasureName2;
  row.productUnitName3 = product.unitMeasureName3;
  row.productSpecification = product.specification;
  row.productName = product.name;
  row.stockCount = (await getStockCount(productId)) || 0;
  row.productPrice = product.salePrice || 0;
  row.productPrice = product.salesPrice || 0;
  row.count = row.count || 1;
  handleRowChange(row);
}
@@ -213,7 +216,8 @@
/** åˆå§‹åŒ– */
onMounted(async () => {
  productOptions.value = await getProductSimpleList();
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
  // ç›®çš„:新增时,默认添加一行
  if (tableData.value.length === 0) {
    handleAdd();
@@ -225,6 +229,7 @@
  <Grid class="w-full">
    <template #productId="{ row }">
      <Select
        v-if="!disabled"
        v-model:value="row.productId"
        :options="productOptions"
        :field-names="{ label: 'name', value: 'id' }"
@@ -233,6 +238,7 @@
        show-search
        @change="handleProductChange($event, row)"
      />
      <span v-else>{{ row.productName || '-' }}</span>
    </template>
    <template #count="{ row }">
      <InputNumber
src/views/erp/sale/out/data.ts
@@ -7,8 +7,8 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getCustomerSimpleList } from '#/api/erp/sale/customer';
import { getItemPage } from '#/api/mdm/item';
import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -69,7 +69,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        fieldNames: {
          label: 'name',
          value: 'id',
@@ -350,11 +350,12 @@
        placeholder: '请选择产品',
        allowClear: true,
        showSearch: true,
        api: getProductSimpleList,
        fieldNames: {
          label: 'name',
          value: 'id',
        api: async () => {
          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
@@ -374,7 +375,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        fieldNames: {
          label: 'name',
          value: 'id',
@@ -554,7 +555,10 @@
        placeholder: '请选择产品',
        allowClear: true,
        showSearch: true,
        api: getProductSimpleList,
        api: async () => {
          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
@@ -572,6 +576,8 @@
}
/** åˆ—表的字段 */
/** åˆ—表的字段 */
export function useOrderGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
src/views/erp/sale/out/modules/item-form.vue
@@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { ErpSaleOutApi } from '#/api/erp/sale/out';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -12,7 +13,7 @@
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getItemPage } from '#/api/mdm/item';
import { getWarehouseStockCount } from '#/api/erp/stock/stock';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
@@ -40,7 +41,7 @@
]);
const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); // è¡¨æ ¼æ•°æ®
const productOptions = ref<any[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
const productOptions = ref<MdmItemApi.Item[]>([]); // ç‰©æ–™ä¸‹æ‹‰é€‰é¡¹
const warehouseOptions = ref<any[]>([]); // ä»“库下拉选项
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
@@ -192,7 +193,8 @@
/** åˆå§‹åŒ– */
onMounted(async () => {
  productOptions.value = await getProductSimpleList();
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
  warehouseOptions.value = await getWarehouseSimpleList();
});
</script>
src/views/erp/sale/return/data.ts
@@ -7,8 +7,8 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getCustomerSimpleList } from '#/api/erp/sale/customer';
import { getItemPage } from '#/api/mdm/item';
import { getCustomerSimpleList as getCrmCustomerSimpleList } from '#/api/crm/customer';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -69,7 +69,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
@@ -343,7 +343,10 @@
        placeholder: '请选择产品',
        allowClear: true,
        showSearch: true,
        api: getProductSimpleList,
        api: async () => {
          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
@@ -365,7 +368,7 @@
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        api: getCrmCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
@@ -541,7 +544,10 @@
        placeholder: '请选择产品',
        allowClear: true,
        showSearch: true,
        api: getProductSimpleList,
        api: async () => {
          const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
src/views/erp/sale/return/modules/item-form.vue
@@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { ErpSaleReturnApi } from '#/api/erp/sale/return';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -12,7 +13,7 @@
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getItemPage } from '#/api/mdm/item';
import { getWarehouseStockCount } from '#/api/erp/stock/stock';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
@@ -40,7 +41,7 @@
]);
const tableData = ref<ErpSaleReturnApi.SaleReturnItem[]>([]); // è¡¨æ ¼æ•°æ®
const productOptions = ref<any[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
const productOptions = ref<MdmItemApi.Item[]>([]); // ç‰©æ–™ä¸‹æ‹‰é€‰é¡¹
const warehouseOptions = ref<any[]>([]); // ä»“库下拉选项
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
@@ -192,7 +193,8 @@
/** åˆå§‹åŒ– */
onMounted(async () => {
  productOptions.value = await getProductSimpleList();
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
  warehouseOptions.value = await getWarehouseSimpleList();
});
</script>
src/views/mes/dv/subject/modules/form.vue
@@ -86,7 +86,7 @@
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
  <Modal :title="getTitle" class="w-[900px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/system/menu/data.ts
@@ -101,7 +101,7 @@
      component: 'IconPicker',
      componentProps: {
        placeholder: '请选择菜单图标',
        prefix: 'carbon',
        prefix: 'ep',
      },
      rules: 'required',
      dependencies: {
vite.config.ts
@@ -9,6 +9,9 @@
      },
      server: {
        allowedHosts: true,
        watch: {
          ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
        },
        proxy: {
          "/admin-api": {
            changeOrigin: true,