7 小时以前 908fae82bbf0476df66c9213b49a8199571e8333
fix(datepicker): 修复日期选择器时间格式化问题

- 将所有 DatePicker 组件的 valueFormat 从 'x' 修改为标准格式 'YYYY-MM-DD HH:mm:ss' 或 'YYYY-MM-DD'
- 统一了日期时间格式以确保数据一致性和正确显示
- 修复了 BPM、CRM、ERP、MES、WLS 等模块中的日期时间字段格式问题
- 更新了 leave、business、clue、contact、contract、customer、followup 等业务模块的日期格式
- 调整了 finance、purchase、sale、stock、checkplan、checkrecord、repair 等模块的时间格式
- 修正了项目管理、生产制造、质量控制等模块中的日期时间显示问题
- 统一了系统租户、到货通知、物料收货等页面的日期格式化方式
已添加28个文件
已修改77个文件
4432 ■■■■■ 文件已修改
src/adapter/vxe-table.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/statistics/index.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplier/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierApply/index.ts 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierCertificate/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierEvaluation/index.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender/index.ts 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/srm.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/oa/leave/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/business/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/clue/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contact/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/data.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/customer/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/followup/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/plan/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/receipt/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/order-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/return/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/supplier.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/uncompleted.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/supplier/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/out/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/return/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/stock/check/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/stock/in/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/stock/move/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/stock/out/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/demo/demo01/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/demo/demo03/erp/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/demo/demo03/inner/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/demo/demo03/normal/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/cal/plan/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/checkplan/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/checkrecord/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/maintenrecord/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/repair/data.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/task/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/andon/record/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/card/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/tm/tool/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/ProductIssueFormModal.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/ProductIssueTab.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/ReturnIssueFormModal.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/ReturnIssueTab.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/enums.ts 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/statistics/index.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/components/select-dialog.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/components/select.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/data.ts 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/index.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/modules/form.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/data.ts 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/index.vue 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/modules/form.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/data.ts 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/modules/form.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierEvaluation/data.ts 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierEvaluation/index.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierEvaluation/modules/form.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/data.ts 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/detail.vue 811 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/index.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/modules/form.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenant/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/arrivalnotice/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/itemreceipt/data.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscissue/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscreceipt/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/outsourceissue/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/outsourcereceipt/data.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productissue/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnissue/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/plan/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/task/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wms/order/check/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wms/order/movement/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wms/order/receipt/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wms/order/shipment/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/adapter/vxe-table.ts
@@ -120,11 +120,42 @@
    });
    // è¡¨æ ¼é…ç½®é¡¹å¯ä»¥ç”¨ cellRender: { name: 'CellTag' },
    // props: { data: [{value, color}], labels?: {[value]: label} }
    // æ”¯æŒä¸¤ç§åŒ¹é…æ¨¡å¼:
    //   1. ç²¾ç¡®åŒ¹é…: data å’Œ labels ä¸­éƒ½æ˜¯ç²¾ç¡®å€¼ (状态/类型枚举)
    //   2. é˜ˆå€¼åŒ¹é…: data ä¸ºé™åºé˜ˆå€¼, label æ˜¾ç¤ºåŽŸå§‹æ•°å€¼ (评分)
    vxeUI.renderer.add('CellTag', {
      renderTableDefault(renderOpts, params) {
        const { props } = renderOpts;
        const { column, row } = params;
        return h(Tag, { color: props?.color }, () => row[column.field]);
        const rawValue = row[column.field];
        // æŸ¥æ‰¾æ˜¾ç¤ºæ–‡æœ¬
        let label = rawValue;
        if (props?.labels && rawValue !== undefined && rawValue !== null) {
          label = props.labels[rawValue] ?? rawValue;
        }
        // æŸ¥æ‰¾é¢œè‰²: ä¼˜å…ˆç²¾ç¡®åŒ¹é…ï¼Œå¦åˆ™é˜ˆå€¼åŒ¹é…
        let color: string | undefined;
        if (props?.data) {
          const exact = props.data.find(
            (item: any) => item.value === rawValue,
          );
          if (exact) {
            color = exact.color;
          } else {
            const sorted = [...props.data]
              .filter((item: any) => typeof item.value === 'number')
              .sort((a: any, b: any) => b.value - a.value);
            const threshold = sorted.find(
              (item: any) => Number(rawValue) >= item.value,
            );
            if (threshold) color = threshold.color;
          }
        }
        return h(Tag, { color }, () => label);
      },
    });
src/api/mdm/item/index.ts
@@ -67,9 +67,14 @@
  });
}
/** æŸ¥è¯¢ç‰©æ–™åˆ—表 */
export function getItemList(params?: Partial<MdmItemApi.Item>) {
  return requestClient.get<MdmItemApi.Item[]>('/mdm/item/list', { params });
/** æŸ¥è¯¢ç‰©æ–™åˆ—表(按ID列表) */
export function getItemList(ids: number[]) {
  return requestClient.get<MdmItemApi.Item[]>('/mdm/item/list-by-ids', { params: { ids: ids.join(',') } });
}
/** èŽ·å–ç‰©æ–™ç²¾ç®€åˆ—è¡¨ï¼ˆå¯ç”¨çŠ¶æ€ï¼Œç”¨äºŽä¸‹æ‹‰é€‰æ‹©ï¼‰ */
export function getItemSimpleList() {
  return requestClient.get<MdmItemApi.Item[]>('/mdm/item/simple-list');
}
/** æŸ¥è¯¢ç‰©æ–™è¯¦æƒ… */
src/api/srm/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import { requestClient } from '#/api/request';
export namespace SrmStatisticsApi {
  export interface TenderStatisticsVO {
    totalTenderCount?: number;
    totalTenderAmount?: number;
    totalAwardAmount?: number;
    savedAmount?: number;
    winRate?: number;
    supplierCount?: number;
    period?: string;
  }
}
/** èŽ·å–æ‹›æ ‡ç»Ÿè®¡ */
export function getTenderStatistics(year: string) {
  return requestClient.get<SrmStatisticsApi.TenderStatisticsVO>(`/srm/statistics/tender?year=${year}`);
}
src/api/srm/supplier/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmSupplierApi {
  export interface SupplierVO {
    id?: number;
    code: string;
    name: string;
    type: number;
    status: number;
    contactName?: string;
    contactMobile?: string;
    contactEmail?: string;
    address?: string;
    bankName?: string;
    bankAccount?: string;
    taxNo?: string;
    remark?: string;
    createTime?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢ä¾›åº”商 */
export function getSupplierPage(params: PageParam) {
  return requestClient.get<PageResult<SrmSupplierApi.SupplierVO>>('/srm/supplier/page', { params });
}
/** èŽ·å–ä¾›åº”å•†ç²¾ç®€åˆ—è¡¨ï¼ˆä»…è¿”å›žå·²é€šè¿‡å‡†å…¥å®¡æ‰¹çš„ä¾›åº”å•†ï¼‰ */
export function getSupplierSimpleList() {
  return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/simple-list');
}
/** èŽ·å–ä¾›åº”å•†è¯¦æƒ… */
export function getSupplier(id: number) {
  return requestClient.get<SrmSupplierApi.SupplierVO>(`/srm/supplier/get?id=${id}`);
}
/** åˆ›å»ºä¾›åº”商 */
export function createSupplier(data: SrmSupplierApi.SupplierVO) {
  return requestClient.post('/srm/supplier/create', data);
}
/** æ›´æ–°ä¾›åº”商 */
export function updateSupplier(data: SrmSupplierApi.SupplierVO) {
  return requestClient.put('/srm/supplier/update', data);
}
/** åˆ é™¤ä¾›åº”商 */
export function deleteSupplier(id: number) {
  return requestClient.delete(`/srm/supplier/delete?id=${id}`);
}
src/api/srm/supplierApply/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmSupplierApplyApi {
  export interface ApplyVO {
    id?: number;
    applyNo?: string;
    supplierId?: number;
    supplierName: string;
    contactName?: string;
    contactMobile?: string;
    contactEmail?: string;
    address?: string;
    bankName?: string;
    bankAccount?: string;
    taxNo?: string;
    businessLicense?: string;
    taxCertificate?: string;
    orgCodeCertificate?: string;
    applyStatus?: number;
    applyStatusLabel?: string;
    auditComment?: string;
    remark?: string;
    createTime?: string;
  }
  export interface ReviewVO {
    id: number;
    passed: boolean;
    comment?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢å‡†å…¥ç”³è¯· */
export function getApplyPage(params: PageParam) {
  return requestClient.get<PageResult<SrmSupplierApplyApi.ApplyVO>>('/srm/supplier-apply/page', { params });
}
/** èŽ·å–å‡†å…¥ç”³è¯·è¯¦æƒ… */
export function getApply(id: number) {
  return requestClient.get<SrmSupplierApplyApi.ApplyVO>(`/srm/supplier-apply/get?id=${id}`);
}
/** åˆ›å»ºå‡†å…¥ç”³è¯· */
export function createApply(data: SrmSupplierApplyApi.ApplyVO) {
  return requestClient.post('/srm/supplier-apply/create', data);
}
/** æ›´æ–°å‡†å…¥ç”³è¯· */
export function updateApply(data: SrmSupplierApplyApi.ApplyVO) {
  return requestClient.put('/srm/supplier-apply/update', data);
}
/** åˆ é™¤å‡†å…¥ç”³è¯· */
export function deleteApply(id: number) {
  return requestClient.delete(`/srm/supplier-apply/delete?id=${id}`);
}
/** æäº¤å®¡æ ¸ */
export function submitApply(id: number) {
  return requestClient.put(`/srm/supplier-apply/submit?id=${id}`);
}
/** é‡‡è´­å®¡æ ¸ */
export function reviewPurchase(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-purchase', data);
}
/** è´¨é‡å®¡æ ¸ */
export function reviewQuality(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-quality', data);
}
/** è´¢åŠ¡å®¡æ ¸ */
export function reviewFinance(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-finance', data);
}
src/api/srm/supplierCertificate/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmSupplierCertificateApi {
  export interface CertificateVO {
    id?: number;
    supplierId: number;
    supplierName?: string;
    certificateName: string;
    certificateNo?: string;
    certificateType: number;
    issuingAuthority?: string;
    issueDate: string;
    expireDate: string;
    expireStatus?: number;
    remark?: string;
    createTime?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢èµ„è´¨ */
export function getCertificatePage(params: PageParam) {
  return requestClient.get<PageResult<SrmSupplierCertificateApi.CertificateVO>>('/srm/supplier-certificate/page', { params });
}
/** èŽ·å–èµ„è´¨è¯¦æƒ… */
export function getCertificate(id: number) {
  return requestClient.get<SrmSupplierCertificateApi.CertificateVO>(`/srm/supplier-certificate/get?id=${id}`);
}
/** åˆ›å»ºèµ„è´¨ */
export function createCertificate(data: SrmSupplierCertificateApi.CertificateVO) {
  return requestClient.post('/srm/supplier-certificate/create', data);
}
/** æ›´æ–°èµ„è´¨ */
export function updateCertificate(data: SrmSupplierCertificateApi.CertificateVO) {
  return requestClient.put('/srm/supplier-certificate/update', data);
}
/** åˆ é™¤èµ„è´¨ */
export function deleteCertificate(id: number) {
  return requestClient.delete(`/srm/supplier-certificate/delete?id=${id}`);
}
/** æŒ‰ä¾›åº”商获取资质列表 */
export function getCertificateListBySupplier(supplierId: number) {
  return requestClient.get<SrmSupplierCertificateApi.CertificateVO[]>(`/srm/supplier-certificate/list-by-supplier?supplierId=${supplierId}`);
}
/** èŽ·å–å³å°†åˆ°æœŸçš„èµ„è´¨åˆ—è¡¨ */
export function getExpiringCertificates() {
  return requestClient.get<SrmSupplierCertificateApi.CertificateVO[]>('/srm/supplier-certificate/expiring-list');
}
src/api/srm/supplierEvaluation/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmSupplierEvaluationApi {
  export interface EvaluationVO {
    id?: number;
    supplierId: number;
    supplierName?: string;
    evaluationPeriod: string;
    qualityScore: number;
    deliveryScore: number;
    priceScore: number;
    serviceScore: number;
    compositeScore?: number;
    grade?: string;
    evaluatorId?: number;
    evaluatorName?: string;
    remark?: string;
    createTime?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢ç»©æ•ˆè¯„ä»· */
export function getEvaluationPage(params: PageParam) {
  return requestClient.get<PageResult<SrmSupplierEvaluationApi.EvaluationVO>>('/srm/supplier-evaluation/page', { params });
}
/** èŽ·å–ç»©æ•ˆè¯„ä»·è¯¦æƒ… */
export function getEvaluation(id: number) {
  return requestClient.get<SrmSupplierEvaluationApi.EvaluationVO>(`/srm/supplier-evaluation/get?id=${id}`);
}
/** åˆ›å»ºç»©æ•ˆè¯„ä»· */
export function createEvaluation(data: SrmSupplierEvaluationApi.EvaluationVO) {
  return requestClient.post('/srm/supplier-evaluation/create', data);
}
/** æ›´æ–°ç»©æ•ˆè¯„ä»· */
export function updateEvaluation(data: SrmSupplierEvaluationApi.EvaluationVO) {
  return requestClient.put('/srm/supplier-evaluation/update', data);
}
/** åˆ é™¤ç»©æ•ˆè¯„ä»· */
export function deleteEvaluation(id: number) {
  return requestClient.delete(`/srm/supplier-evaluation/delete?id=${id}`);
}
src/api/srm/tender/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,232 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmTenderApi {
  export interface TenderProjectVO {
    id?: number;
    tenderNo?: string;
    tenderName: string;
    tenderType: number;
    purchaseOrg?: string;
    projectLeaderId?: number;
    projectLeaderName?: string;
    tenderStatus?: number;
    budgetAmount?: number;
    startTime?: string;
    endTime?: string;
    bidOpenTime?: string;
    tenderDesc?: string;
    qualificationRequirements?: string;
    purchaseRequestId?: number;
    awardId?: number;
    purchaseOrderNo?: string;
    createTime?: string;
  }
  export interface TenderMaterialVO {
    id?: number;
    tenderProjectId: number;
    productId?: number;
    productCode?: string;
    productName: string;
    productSpec?: string;
    unit?: string;
    quantity?: number;
    techRequirement?: string;
    estimatedPrice?: number;
    sort?: number;
  }
  export interface TenderBidVO {
    id?: number;
    tenderProjectId: number;
    supplierId: number;
    supplierName?: string;
    bidNo: string;
    bidStatus?: number;
    bidTime?: string;
    bidTotalAmount?: number;
    bidFileName?: string;
    bidFileUrl?: string;
    createTime?: string;
  }
  export interface QuoteVO {
    id?: number;
    tenderProjectId?: number;
    supplierId?: number;
    bidId: number;
    tenderMaterialId: number;
    materialName?: string;
    quotePrice?: number;
    taxRate?: number;
    deliveryCycle?: number;
    paymentTerms?: string;
    warrantyPeriod?: number;
    remark?: string;
  }
  export interface BidEvaluationVO {
    id?: number;
    tenderProjectId: number;
    supplierId: number;
    supplierName?: string;
    bidId: number;
    priceScore?: number;
    techScore?: number;
    deliveryScore?: number;
    serviceScore?: number;
    compositeScore?: number;
    rank?: number;
    evaluatorId?: number;
    evaluatorName?: string;
    evaluationOpinion?: string;
  }
  export interface BidAwardVO {
    id?: number;
    tenderProjectId: number;
    supplierId: number;
    supplierName?: string;
    bidId: number;
    awardNo: string;
    awardStatus?: number;
    awardAmount?: number;
    awardTime?: string;
    noticeFileName?: string;
    noticeFileUrl?: string;
    purchaseOrderId?: number;
    remark?: string;
  }
}
// ========== æ‹›æ ‡é¡¹ç›® ==========
export function getTenderProjectPage(params: PageParam) {
  return requestClient.get<PageResult<SrmTenderApi.TenderProjectVO>>('/srm/tender-project/page', { params });
}
export function getTenderProject(id: number) {
  return requestClient.get<SrmTenderApi.TenderProjectVO>(`/srm/tender-project/get?id=${id}`);
}
export function createTenderProject(data: SrmTenderApi.TenderProjectVO) {
  return requestClient.post('/srm/tender-project/create', data);
}
export function updateTenderProject(data: SrmTenderApi.TenderProjectVO) {
  return requestClient.put('/srm/tender-project/update', data);
}
export function deleteTenderProject(id: number) {
  return requestClient.delete(`/srm/tender-project/delete?id=${id}`);
}
export function publishTender(id: number) {
  return requestClient.put(`/srm/tender-project/publish?id=${id}`);
}
export function closeTender(id: number) {
  return requestClient.put(`/srm/tender-project/close?id=${id}`);
}
export function startBidding(id: number) {
  return requestClient.put(`/srm/tender-project/start-bidding?id=${id}`);
}
export function startEvaluation(id: number) {
  return requestClient.put(`/srm/tender-project/start-evaluation?id=${id}`);
}
// ========== ç‰©æ–™å­è¡¨ ==========
export function getMaterialList(tenderProjectId: number) {
  return requestClient.get<SrmTenderApi.TenderMaterialVO[]>(`/srm/tender-project/material/list?tenderProjectId=${tenderProjectId}`);
}
export function createMaterial(data: SrmTenderApi.TenderMaterialVO) {
  return requestClient.post('/srm/tender-project/material/create', data);
}
export function updateMaterial(data: SrmTenderApi.TenderMaterialVO) {
  return requestClient.put('/srm/tender-project/material/update', data);
}
export function deleteMaterial(id: number) {
  return requestClient.delete(`/srm/tender-project/material/delete?id=${id}`);
}
// ========== æŠ•标管理 ==========
export function getBidListByProject(tenderProjectId: number) {
  return requestClient.get<SrmTenderApi.TenderBidVO[]>(`/srm/tender-bid/list-by-project?tenderProjectId=${tenderProjectId}`);
}
export function getBid(id: number) {
  return requestClient.get<SrmTenderApi.TenderBidVO>(`/srm/tender-bid/get?id=${id}`);
}
export function createBid(data: SrmTenderApi.TenderBidVO) {
  return requestClient.post('/srm/tender-bid/create', data);
}
export function withdrawBid(id: number) {
  return requestClient.put(`/srm/tender-bid/withdraw?id=${id}`);
}
// ========== æŠ¥ä»·ç®¡ç† ==========
export function getQuoteList(bidId: number) {
  return requestClient.get<SrmTenderApi.QuoteVO[]>(`/srm/tender-bid/quote/list-by-bid?bidId=${bidId}`);
}
export function createQuote(data: SrmTenderApi.QuoteVO) {
  return requestClient.post('/srm/tender-bid/quote/create', data);
}
export function updateQuote(data: SrmTenderApi.QuoteVO) {
  return requestClient.put('/srm/tender-bid/quote/update', data);
}
// ========== è¯„标管理 ==========
export function getEvaluationList(tenderProjectId: number) {
  return requestClient.get<SrmTenderApi.BidEvaluationVO[]>(`/srm/bid-evaluation/list-by-project?tenderProjectId=${tenderProjectId}`);
}
export function createBidEvaluation(data: SrmTenderApi.BidEvaluationVO) {
  return requestClient.post('/srm/bid-evaluation/create', data);
}
export function calculateRanking(tenderProjectId: number) {
  return requestClient.put(`/srm/bid-evaluation/calculate-ranking?tenderProjectId=${tenderProjectId}`);
}
// ========== å¼€æ ‡ ==========
export function createBidOpen(tenderProjectId: number) {
  return requestClient.post(`/srm/bid-evaluation/bid-open/create?tenderProjectId=${tenderProjectId}`);
}
export function getBidOpen(id: number) {
  return requestClient.get(`/srm/bid-evaluation/bid-open/get?id=${id}`);
}
// ========== å®šæ ‡ç®¡ç† ==========
export function createAward(data: SrmTenderApi.BidAwardVO) {
  return requestClient.post('/srm/bid-award/create', data);
}
export function getAward(id: number) {
  return requestClient.get<SrmTenderApi.BidAwardVO>(`/srm/bid-award/get?id=${id}`);
}
export function approveAward(id: number) {
  return requestClient.put(`/srm/bid-award/approve?id=${id}`);
}
export function generatePurchaseOrder(id: number) {
  return requestClient.put(`/srm/bid-award/generate-purchase-order/${id}`);
}
src/router/routes/modules/srm.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
  {
    path: '/srm',
    name: 'SrmCenter',
    meta: {
      title: 'SRM ä¾›åº”商管理',
      icon: 'lucide:package-search',
      keepAlive: true,
      hideInMenu: true,
    },
    children: [
      {
        path: 'tender/detail',
        name: 'SrmTenderDetail',
        meta: {
          title: '招标详情',
          activePath: '/srm/tender',
        },
        component: () => import('#/views/srm/tender/detail.vue'),
      },
    ],
  },
];
export default routes;
src/views/bpm/oa/leave/data.ts
@@ -41,7 +41,7 @@
      componentProps: {
        placeholder: '请选择开始时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        format: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
@@ -53,7 +53,7 @@
      componentProps: {
        placeholder: '请选择结束时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        format: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
src/views/crm/business/data.ts
@@ -98,7 +98,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择预计成交日期',
      },
    },
src/views/crm/clue/data.ts
@@ -144,7 +144,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择下次联系时间',
      },
    },
src/views/crm/contact/data.ts
@@ -165,7 +165,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择下次联系时间',
      },
    },
src/views/crm/contract/data.ts
@@ -79,7 +79,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择下单日期',
      },
    },
@@ -128,7 +128,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择合同开始时间',
      },
    },
@@ -139,7 +139,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择合同结束时间',
      },
    },
src/views/crm/customer/data.ts
@@ -154,7 +154,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择下次联系时间',
        allowClear: true,
      },
src/views/crm/followup/data.ts
@@ -49,7 +49,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择下次联系时间',
      },
      rules: 'required',
src/views/crm/receivable/data.ts
@@ -183,7 +183,7 @@
      componentProps: {
        placeholder: '请选择回款日期',
        showTime: false,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        format: 'YYYY-MM-DD',
      },
    },
src/views/crm/receivable/plan/data.ts
@@ -109,7 +109,7 @@
      componentProps: {
        placeholder: '请选择计划回款日期',
        showTime: false,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        format: 'YYYY-MM-DD',
      },
      defaultValue: new Date(),
src/views/crm/saleQuotation/data.ts
@@ -128,7 +128,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择报价日期',
      },
    },
@@ -139,7 +139,7 @@
      componentProps: {
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择有效期',
      },
    },
src/views/erp/finance/payment/data.ts
@@ -6,7 +6,7 @@
import { erpPriceInputFormatter } from '@vben/utils';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -42,7 +42,7 @@
        placeholder: '选择付款时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/finance/receipt/data.ts
@@ -42,7 +42,7 @@
        placeholder: '选择收款时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/purchase/in/data.ts
@@ -8,7 +8,7 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -45,7 +45,7 @@
        placeholder: '选择入库时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/purchase/order/data.ts
@@ -9,7 +9,7 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { getRangePickerDefaultProps } from '#/utils';
@@ -45,7 +45,7 @@
        placeholder: '选择订单时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/purchase/request/data.ts
@@ -4,7 +4,7 @@
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -48,7 +48,7 @@
        placeholder: '选择申请时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        disabled: formType === 'detail',
      },
      rules: 'required',
src/views/erp/purchase/request/modules/order-form.vue
@@ -8,7 +8,7 @@
import { message, Select } from 'ant-design-vue';
import { generatePurchaseOrder } from '#/api/erp/purchase/request';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
const emit = defineEmits(['success']);
src/views/erp/purchase/return/data.ts
@@ -8,7 +8,7 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -45,7 +45,7 @@
        placeholder: '选择退货时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/purchase/statistics/supplier.vue
@@ -6,7 +6,7 @@
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSupplierStatistics } from '#/api/erp/purchase/statistics';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getRangePickerDefaultProps } from '#/utils';
import type { VbenFormSchema } from '#/adapter/form';
src/views/erp/purchase/statistics/uncompleted.vue
@@ -8,7 +8,7 @@
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getUncompletedOrders } from '#/api/erp/purchase/statistics';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getRangePickerDefaultProps } from '#/utils';
import type { VbenFormSchema } from '#/adapter/form';
src/views/erp/purchase/supplier/data.ts
@@ -89,7 +89,7 @@
      componentProps: {
        placeholder: '请选择合作开始时间',
        class: '!w-full',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
src/views/erp/sale/order/data.ts
@@ -72,7 +72,7 @@
        placeholder: '选择订单时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/sale/out/data.ts
@@ -45,7 +45,7 @@
        placeholder: '选择出库时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/sale/return/data.ts
@@ -45,7 +45,7 @@
        placeholder: '选择退货时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/stock/check/data.ts
@@ -40,7 +40,7 @@
        placeholder: '选择盘点时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/stock/in/data.ts
@@ -5,7 +5,7 @@
import { getDictOptions } from '@vben/hooks';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -41,7 +41,7 @@
        placeholder: '选择入库时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/stock/move/data.ts
@@ -40,7 +40,7 @@
        placeholder: '选择调度时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/erp/stock/out/data.ts
@@ -41,7 +41,7 @@
        placeholder: '选择出库时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/infra/demo/demo01/data.ts
@@ -46,7 +46,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择出生年',
      },
    },
src/views/infra/demo/demo03/erp/data.ts
@@ -46,7 +46,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择出生日期',
      },
    },
src/views/infra/demo/demo03/inner/data.ts
@@ -46,7 +46,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/infra/demo/demo03/normal/data.ts
@@ -46,7 +46,7 @@
      componentProps: {
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择出生日期',
      },
    },
src/views/mes/cal/plan/data.ts
@@ -97,7 +97,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择开始日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -108,7 +108,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择结束日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/dv/checkplan/data.ts
@@ -97,7 +97,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择开始日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -108,7 +108,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择结束日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/dv/checkrecord/data.ts
@@ -79,7 +79,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择点检时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/mes/dv/machinery/data.ts
@@ -129,7 +129,7 @@
        disabled: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['id'],
@@ -145,7 +145,7 @@
        disabled: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['id'],
src/views/mes/dv/maintenrecord/data.ts
@@ -79,7 +79,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择保养时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/mes/dv/repair/data.ts
@@ -109,7 +109,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择报修日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
@@ -141,7 +141,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择完成日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      // ç»´ä¿®ä¸­(≥CONFIRMED)态展示;仅"完成维修"弹窗可编辑并必填,其余态只读回显
      dependencies: {
@@ -182,7 +182,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择验收日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      // éªŒæ”¶ä¿¡æ¯ä¸ºå·²ç¡®è®¤(≥FINISHED)态自动产生的只读回显字段
      dependencies: {
src/views/mes/pd/project/data.ts
@@ -36,7 +36,7 @@
      document.body,
    inputReadOnly: true,
    placeholder: options?.placeholder ?? "请选择日期",
    valueFormat: "x",
    valueFormat: 'YYYY-MM-DD',
  };
}
src/views/mes/pd/project/modules/task/data.ts
@@ -106,7 +106,7 @@
      fieldName: 'planFinishDate',
      label: '计划完成',
      component: 'DatePicker',
      componentProps: { disabled: readonly, class: 'w-full', valueFormat: 'x' },
      componentProps: { disabled: readonly, class: 'w-full', valueFormat: 'YYYY-MM-DD' },
    },
    {
      fieldName: 'remark',
@@ -135,7 +135,7 @@
      fieldName: 'planFinishDate',
      label: '计划完成',
      component: 'DatePicker',
      componentProps: { class: 'w-full', valueFormat: 'x' },
      componentProps: { class: 'w-full', valueFormat: 'YYYY-MM-DD' },
    },
    {
      fieldName: 'remark',
src/views/mes/pro/andon/record/data.ts
@@ -275,7 +275,7 @@
              format: 'YYYY-MM-DD HH:mm:ss',
              placeholder: '请选择处置时间',
              showTime: true,
              valueFormat: 'x',
              valueFormat: 'YYYY-MM-DD HH:mm:ss',
            },
          },
          isUpdate
src/views/mes/pro/card/data.ts
@@ -347,7 +347,7 @@
        class: '!w-full',
        placeholder: '请选择进入工序时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
@@ -358,7 +358,7 @@
        class: '!w-full',
        placeholder: '请选择出工序时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/mes/pro/feedback/data.ts
@@ -554,7 +554,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择报工时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/mes/pro/mps/data.ts
@@ -108,7 +108,7 @@
        class: '!w-full',
        placeholder: '请选择需求日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        disabled: !isCreate && !isEdit,
      },
      rules: 'required',
src/views/mes/pro/task/data.ts
@@ -281,7 +281,7 @@
        class: '!w-full',
        disabled: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
@@ -415,7 +415,7 @@
        class: '!w-full',
        placeholder: '请选择开始时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        // å¼€å§‹æ—¶é—´å˜æ›´ï¼šé‡æ–°è®¡ç®—结束时间
        onChange: () => recalcEndTime(formApi),
      },
@@ -443,7 +443,7 @@
        class: '!w-full',
        disabled: true,
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/mes/pro/workorder/data.ts
@@ -232,7 +232,7 @@
        disabled: headerReadonly,
        format: 'YYYY-MM-DD',
        placeholder: '请选择需求日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/qc/ipqc/data.ts
@@ -328,7 +328,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择检测日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/qc/iqc/data.ts
@@ -208,7 +208,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择来料日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -232,7 +232,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择检测日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/qc/oqc/data.ts
@@ -254,7 +254,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择出货日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -265,7 +265,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择检测日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/mes/qc/rqc/data.ts
@@ -219,7 +219,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择检测日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/mes/tm/tool/data.ts
@@ -183,7 +183,7 @@
      componentProps: {
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['maintenType'],
src/views/mes/workbench/components/tabs/ProductIssueFormModal.vue
@@ -98,7 +98,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择需求时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/mes/workbench/components/tabs/ProductIssueTab.vue
@@ -119,7 +119,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择需求时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/mes/workbench/components/tabs/ReturnIssueFormModal.vue
@@ -117,7 +117,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择退料日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/mes/workbench/components/tabs/ReturnIssueTab.vue
@@ -131,7 +131,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择退料日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/srm/enums.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
// ============================================================
// SRM ç»Ÿä¸€æžšä¸¾å¸¸é‡
// æ‰€æœ‰å€¼ä¸¥æ ¼å¯¹é½åŽç«¯æžšä¸¾å®šä¹‰ (yudao-module-srm/enums/)
// ============================================================
/** é€‰é¡¹ç±»åž‹ (Select/RadioGroup通用) */
export interface EnumOption {
  label: string;
  value: number | string;
}
/** CellTag æ•°æ®ç±»åž‹ */
export interface CellTagItem {
  value: number | string;
  color: string;
}
// ========== ä¾›åº”商 (字段值对齐 CommonStatusEnum + SupplierTypeEnum) ==========
/** ä¾›åº”商状态 â€” å¯¹é½ CommonStatusEnum (0=开启 1=关闭) */
export const SUPPLIER_STATUS = {
  ENABLE: 0,
  DISABLE: 1,
} as const;
export const SUPPLIER_STATUS_OPTIONS: EnumOption[] = [
  { label: '开启', value: 0 },
  { label: '关闭', value: 1 },
];
export const SUPPLIER_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 0, color: 'green' },
    { value: 1, color: 'red' },
  ],
  labels: { 0: '开启', 1: '关闭' },
};
/** ä¾›åº”商类型 â€” å¯¹é½ SupplierTypeEnum (1=生产型 2=贸易型 3=服务型 4=其他) */
export const SUPPLIER_TYPE = {
  PRODUCTION: 1,
  TRADING: 2,
  SERVICE: 3,
  OTHER: 4,
} as const;
export const SUPPLIER_TYPE_OPTIONS: EnumOption[] = [
  { label: '生产型', value: 1 },
  { label: '贸易型', value: 2 },
  { label: '服务型', value: 3 },
  { label: '其他', value: 4 },
];
export const SUPPLIER_TYPE_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 1, color: 'blue' },
    { value: 2, color: 'green' },
    { value: 3, color: 'orange' },
    { value: 4, color: 'default' },
  ],
  labels: { 1: '生产型', 2: '贸易型', 3: '服务型', 4: '其他' },
};
// ========== å‡†å…¥ç”³è¯· â€” å¯¹é½ SupplierApplyStatusEnum (0/10/20/30/40/50) ==========
export const APPLY_STATUS = {
  DRAFT: 0,
  PENDING_PURCHASE: 10,
  PENDING_QUALITY: 20,
  PENDING_FINANCE: 30,
  APPROVED: 40,
  REJECTED: 50,
} as const;
export const APPLY_STATUS_OPTIONS: EnumOption[] = [
  { label: '待提交', value: 0 },
  { label: '待采购审核', value: 10 },
  { label: '待质量审核', value: 20 },
  { label: '待财务审核', value: 30 },
  { label: '已通过', value: 40 },
  { label: '已拒绝', value: 50 },
];
export const APPLY_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 0, color: 'gray' },
    { value: 10, color: 'blue' },
    { value: 20, color: 'orange' },
    { value: 30, color: 'purple' },
    { value: 40, color: 'green' },
    { value: 50, color: 'red' },
  ],
  labels: { 0: '待提交', 10: '待采购审核', 20: '待质量审核', 30: '待财务审核', 40: '已通过', 50: '已拒绝' },
};
// ========== èµ„质证书 â€” å¯¹é½ CertificateTypeEnum (1=营业执照 2=认证证书 3=检测报告 4=其他) ==========
export const CERTIFICATE_TYPE = {
  BUSINESS_LICENSE: 1,
  CERTIFICATION: 2,
  INSPECTION_REPORT: 3,
  OTHER: 4,
} as const;
export const CERTIFICATE_TYPE_OPTIONS: EnumOption[] = [
  { label: '营业执照', value: 1 },
  { label: '认证证书', value: 2 },
  { label: '检测报告', value: 3 },
  { label: '其他', value: 4 },
];
export const CERTIFICATE_TYPE_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 1, color: 'blue' },
    { value: 2, color: 'green' },
    { value: 3, color: 'orange' },
    { value: 4, color: 'default' },
  ],
  labels: { 1: '营业执照', 2: '认证证书', 3: '检测报告', 4: '其他' },
};
// ========== ç»©æ•ˆè¯„ä»· â€” åŽç«¯ evaluationLevel ä¸º String ç±»åž‹ (A/B/C/D) ==========
export const EVALUATION_GRADE = {
  A: 'A',
  B: 'B',
  C: 'C',
  D: 'D',
} as const;
export const EVALUATION_GRADE_CELLTAG: { data: CellTagItem[] } = {
  data: [
    { value: 'A', color: 'green' },
    { value: 'B', color: 'blue' },
    { value: 'C', color: 'orange' },
    { value: 'D', color: 'red' },
  ],
};
/** ç»¼åˆè¯„分区间 â€” åŽç«¯è®¡ç®—: >=90→A, >=80→B, >=70→C, <70→D */
export const COMPOSITE_SCORE_CELLTAG: { data: CellTagItem[] } = {
  data: [
    { value: 90, color: 'green' },
    { value: 80, color: 'blue' },
    { value: 70, color: 'orange' },
    { value: 0, color: 'red' },
  ],
};
// ========== æ‹›æ ‡é¡¹ç›® â€” å¯¹é½ TenderStatusEnum (0/1/2/3/4/5) + TenderTypeEnum (1/2/3) ==========
/** æ‹›æ ‡ç±»åž‹ â€” å¯¹é½ TenderTypeEnum (1=公开招标 2=邀请招标 3=竞争性谈判) */
export const TENDER_TYPE = {
  PUBLIC: 1,
  INVITATION: 2,
  COMPETITIVE_NEGOTIATION: 3,
} as const;
export const TENDER_TYPE_OPTIONS: EnumOption[] = [
  { label: '公开招标', value: 1 },
  { label: '邀请招标', value: 2 },
  { label: '竞争性谈判', value: 3 },
];
export const TENDER_TYPE_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 1, color: 'blue' },
    { value: 2, color: 'blue' },
    { value: 3, color: 'green' },
  ],
  labels: { 1: '公开招标', 2: '邀请招标', 3: '竞争性谈判' },
};
/** æ‹›æ ‡é¡¹ç›®çŠ¶æ€ â€” å¯¹é½ TenderStatusEnum (0=草稿 1=发布中 2=投标中 3=评标中 4=已定标 5=已关闭) */
export const TENDER_STATUS = {
  DRAFT: 0,
  PUBLISHED: 1,
  BIDDING: 2,
  EVALUATING: 3,
  AWARDED: 4,
  CLOSED: 5,
} as const;
export const TENDER_STATUS_OPTIONS: EnumOption[] = [
  { label: '草稿', value: 0 },
  { label: '发布中', value: 1 },
  { label: '投标中', value: 2 },
  { label: '评标中', value: 3 },
  { label: '已定标', value: 4 },
  { label: '已关闭', value: 5 },
];
export const TENDER_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 0, color: 'gray' },
    { value: 1, color: 'blue' },
    { value: 2, color: 'cyan' },
    { value: 3, color: 'orange' },
    { value: 4, color: 'green' },
    { value: 5, color: 'red' },
  ],
  labels: { 0: '草稿', 1: '发布中', 2: '投标中', 3: '评标中', 4: '已定标', 5: '已关闭' },
};
/** æ‹›æ ‡çŠ¶æ€ Ant Design Tag é¢œè‰²æ˜ å°„ */
export const TENDER_STATUS_COLORS: Record<number, string> = {
  [TENDER_STATUS.DRAFT]: 'default',
  [TENDER_STATUS.PUBLISHED]: 'blue',
  [TENDER_STATUS.BIDDING]: 'cyan',
  [TENDER_STATUS.EVALUATING]: 'orange',
  [TENDER_STATUS.AWARDED]: 'green',
  [TENDER_STATUS.CLOSED]: 'red',
};
// ========== æŠ•æ ‡ â€” å¯¹é½ BidStatusEnum (0=报名 10=已投标 20=撤标 30=中标 40=未中标) ==========
export const BID_STATUS = {
  REGISTERED: 0,
  BID: 10,
  WITHDRAWN: 20,
  WIN: 30,
  LOSE: 40,
} as const;
export const BID_STATUS_MAP: Record<number, string> = {
  0: '报名',
  10: '已投标',
  20: '撤标',
  30: '中标',
  40: '未中标',
};
// ========== å®šæ ‡ â€” å¯¹é½ AwardStatusEnum (0=待审批 10=已定标 20=已生成订单) ==========
export const AWARD_STATUS = {
  PENDING_APPROVAL: 0,
  AWARDED: 10,
  ORDER_GENERATED: 20,
} as const;
export const AWARD_STATUS_MAP: Record<number, string> = {
  0: '待审批',
  10: '已定标',
  20: '已生成订单',
};
// ========== å·¥å…·å‡½æ•° ==========
/** æ ¹æ®å€¼èŽ·å–æ ‡ç­¾ */
export function getLabel(map: Record<number, string>, value?: number): string {
  return value !== undefined ? map[value] || String(value) : '';
}
/** å°† options æ•°ç»„转为 labels æ˜ å°„ */
export function optionsToLabels(options: EnumOption[]): Record<number, string> {
  const labels: Record<number, string> = {};
  for (const opt of options) {
    if (typeof opt.value === 'number') {
      labels[opt.value] = opt.label;
    }
  }
  return labels;
}
src/views/srm/statistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { EchartsUIType } from '@vben/plugins/echarts';
import type { SrmStatisticsApi } from '#/api/srm/statistics';
import { onMounted, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { getTenderStatistics } from '#/api/srm/statistics';
import { getSupplierSimpleList } from '#/api/srm/supplier';
defineOptions({ name: 'SrmStatistics' });
const year = ref(new Date().getFullYear().toString());
const stats = ref<SrmStatisticsApi.TenderStatisticsVO>({});
const supplierCount = ref(0);
async function loadStats() {
  stats.value = await getTenderStatistics(year.value);
}
async function loadSupplierCount() {
  const list = await getSupplierSimpleList();
  supplierCount.value = list?.length ?? 0;
}
// å›¾è¡¨
const pieChartRef = ref<EchartsUIType>();
const barChartRef = ref<EchartsUIType>();
const { renderEcharts: renderPie } = useEcharts(pieChartRef);
const { renderEcharts: renderBar } = useEcharts(barChartRef);
async function renderCharts() {
  // é¥¼å›¾ï¼šä¾›åº”商状态分布
  await renderPie({
    title: { text: '供应商状态分布', left: 'center' },
    tooltip: { trigger: 'item' },
    legend: { bottom: 0 },
    series: [{
      type: 'pie',
      radius: ['40%', '70%'],
      data: [
        { value: supplierCount.value, name: '已启用' },
        { value: 0, name: '已禁用' },
      ],
    }],
  });
  // æŸ±çŠ¶å›¾ï¼šæ‹›æ ‡ç»Ÿè®¡æ¦‚è§ˆ
  await renderBar({
    title: { text: `${year.value} å¹´æ‹›æ ‡ç»Ÿè®¡æ¦‚览`, left: 'center' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: ['招标总数', '参与供应商', '定标金额(万)', '节约金额(万)'] },
    yAxis: { type: 'value' },
    series: [{
      type: 'bar',
      data: [
        stats.value.totalTenderCount ?? 0,
        stats.value.supplierCount ?? 0,
        +(((stats.value.totalAwardAmount ?? 0) / 10000).toFixed(2)),
        +(((stats.value.savedAmount ?? 0) / 10000).toFixed(2)),
      ],
      itemStyle: {
        color: (params: any) => ['#3b82f6', '#ec4899', '#f97316', '#8b5cf6'][params.dataIndex],
      },
    }],
  });
}
watch([stats, supplierCount], () => renderCharts());
onMounted(() => { loadStats(); loadSupplierCount(); });
</script>
<template>
  <Page auto-content-height>
    <div class="p-4">
      <div class="mb-6 flex items-center gap-4">
        <span class="text-lg font-medium">SRM ç»Ÿè®¡çœ‹æ¿</span>
        <a-input v-model:value="year" placeholder="年份" style="width: 120px" @change="loadStats" />
      </div>
      <!-- 6 ä¸ªç»Ÿè®¡å¡ç‰‡ -->
      <div class="mb-6 grid grid-cols-3 gap-4">
        <div class="rounded-lg bg-blue-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-blue-600">{{ stats.totalTenderCount ?? 0 }}</div>
          <div class="mt-2 text-gray-500">招标总数</div>
        </div>
        <div class="rounded-lg bg-green-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-green-600">{{ stats.totalTenderAmount?.toLocaleString() ?? 0 }}</div>
          <div class="mt-2 text-gray-500">招标总金额(元)</div>
        </div>
        <div class="rounded-lg bg-orange-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-orange-600">{{ stats.totalAwardAmount?.toLocaleString() ?? 0 }}</div>
          <div class="mt-2 text-gray-500">定标总金额(元)</div>
        </div>
        <div class="rounded-lg bg-purple-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-purple-600">{{ stats.savedAmount?.toLocaleString() ?? 0 }}</div>
          <div class="mt-2 text-gray-500">节约金额(元)</div>
        </div>
        <div class="rounded-lg bg-teal-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-teal-600">{{ stats.winRate ?? 0 }}%</div>
          <div class="mt-2 text-gray-500">中标率</div>
        </div>
        <div class="rounded-lg bg-rose-50 p-6 text-center shadow-sm">
          <div class="text-3xl font-bold text-rose-600">{{ stats.supplierCount ?? 0 }}</div>
          <div class="mt-2 text-gray-500">参与招标供应商</div>
        </div>
      </div>
      <!-- å›¾è¡¨åŒºåŸŸ -->
      <div class="mb-6 grid grid-cols-2 gap-4">
        <div class="rounded-lg bg-white p-4 shadow-sm">
          <EchartsUI ref="pieChartRef" class="h-[350px] w-full" />
        </div>
        <div class="rounded-lg bg-white p-4 shadow-sm">
          <EchartsUI ref="barChartRef" class="h-[350px] w-full" />
        </div>
      </div>
      <!-- åº•部信息 -->
      <div class="rounded-lg bg-white p-4 shadow-sm">
        <h3 class="mb-3 text-base font-medium">供应商总数: {{ supplierCount }} å®¶</h3>
        <a-empty v-if="supplierCount === 0" description="暂无供应商数据" />
      </div>
    </div>
  </Page>
</template>
src/views/srm/supplier/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as SrmSupplierSelect } from './select.vue';
export { default as SrmSupplierSelectDialog } from './select-dialog.vue';
src/views/srm/supplier/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,259 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { nextTick, ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSupplierPage } from '#/api/srm/supplier';
defineOptions({ name: 'SrmSupplierSelectDialog' });
const emit = defineEmits<{
  selected: [rows: SrmSupplierApi.SupplierVO[]];
}>();
const open = ref(false);
const multiple = ref(true);
const approvalFilter = ref(true);
const selectedRows = ref<SrmSupplierApi.SupplierVO[]>([]);
const preSelectedIds = ref<number[]>([]);
function getTableRows() {
  return gridApi.grid.getTableData().fullData as SrmSupplierApi.SupplierVO[];
}
function getMultipleSelectedRows() {
  const selectedMap = new Map<number, SrmSupplierApi.SupplierVO>();
  const records = [
    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
  ] as SrmSupplierApi.SupplierVO[];
  records.forEach((row) => {
    const rowId = row.id;
    if (rowId) {
      selectedMap.set(rowId, row);
    }
  });
  return [...selectedMap.values()];
}
function handleCheckboxSelectChange() {
  selectedRows.value = getMultipleSelectedRows();
}
function handleRadioChange(row: SrmSupplierApi.SupplierVO) {
  selectedRows.value = [row];
}
async function toggleMultipleRow(row: SrmSupplierApi.SupplierVO) {
  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
  await gridApi.grid.setCheckboxRow(row, !selected);
  selectedRows.value = getMultipleSelectedRows();
}
async function handleCellDblclick({ row }: { row: SrmSupplierApi.SupplierVO }) {
  if (multiple.value) {
    await toggleMultipleRow(row);
    return;
  }
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
async function applyPreSelection() {
  if (preSelectedIds.value.length === 0) {
    return;
  }
  const rows = getTableRows();
  for (const row of rows) {
    if (
      row.id === undefined ||
      row.id === null ||
      !preSelectedIds.value.includes(row.id)
    ) {
      continue;
    }
    if (multiple.value) {
      await gridApi.grid.setCheckboxRow(row, true);
    } else {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
  if (multiple.value) {
    selectedRows.value = getMultipleSelectedRows();
  }
}
function useSelectGridFormSchema() {
  return [
    {
      fieldName: 'name',
      label: '供应商名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入供应商名称',
        allowClear: true,
      },
    },
    {
      fieldName: 'code',
      label: '供应商编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入供应商编码',
        allowClear: true,
      },
    },
  ];
}
function useSelectGridColumns(multiple = false): VxeTableGridOptions<SrmSupplierApi.SupplierVO>['columns'] {
  return [
    {
      type: multiple ? 'checkbox' : 'radio',
      width: 50,
    },
    {
      field: 'code',
      title: '编码',
      minWidth: 100,
    },
    {
      field: 'name',
      title: '供应商名称',
      minWidth: 150,
    },
    {
      field: 'contactName',
      title: '联系人',
      minWidth: 100,
    },
    {
      field: 'contactMobile',
      title: '联系电话',
      minWidth: 120,
    },
    {
      field: 'contactEmail',
      title: '邮箱',
      minWidth: 150,
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useSelectGridFormSchema(),
  },
  gridOptions: {
    columns: useSelectGridColumns(true),
    height: 520,
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      range: true,
      reserve: true,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          const params: Record<string, unknown> = {
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
            status: CommonStatusEnum.ENABLE,
          };
          if (approvalFilter.value) {
            params.approved = true;
          }
          return await getSupplierPage(params);
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<SrmSupplierApi.SupplierVO>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    checkboxAll: handleCheckboxSelectChange,
    checkboxChange: handleCheckboxSelectChange,
    radioChange: ({ row }: { row: SrmSupplierApi.SupplierVO }) => {
      handleRadioChange(row);
    },
  },
});
async function resetQueryState() {
  selectedRows.value = [];
  await gridApi.grid.clearCheckboxRow();
  await gridApi.grid.clearCheckboxReserve();
  await gridApi.grid.clearRadioRow();
  await gridApi.formApi.resetForm();
}
async function openModal(
  selectedIds?: number[],
  options?: { multiple?: boolean; approvalFilter?: boolean },
) {
  open.value = true;
  multiple.value = options?.multiple ?? true;
  approvalFilter.value = options?.approvalFilter ?? true;
  preSelectedIds.value = selectedIds || [];
  await nextTick();
  gridApi.setGridOptions({
    columns: useSelectGridColumns(multiple.value),
  });
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
function handleConfirm() {
  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
  if (rows.length === 0) {
    message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
    return;
  }
  emit('selected', multiple.value ? rows : [rows[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="SRM ä¾›åº”商选择"
    width="70%"
    :destroy-on-close="true"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <Grid table-title="供应商列表" />
  </Modal>
</template>
src/views/srm/supplier/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,117 @@
<script lang="ts" setup>
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { computed, ref, useAttrs } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import SrmSupplierSelectDialog from './select-dialog.vue';
defineOptions({ name: 'SrmSupplierSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    approvalFilter?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
    supplierName?: string;
  }>(),
  {
    allowClear: true,
    approvalFilter: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择供应商',
    supplierName: undefined,
  },
);
const emit = defineEmits<{
  change: [item: SrmSupplierApi.SupplierVO | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof SrmSupplierSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<SrmSupplierApi.SupplierVO>();
const displayLabel = computed(() => props.supplierName || selectedItem.value?.name || '');
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== null,
);
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  const selectedIds = (
    props.modelValue === null ? [] : [props.modelValue]
  ) as number[];
  dialogRef.value?.open(selectedIds, { multiple: false, approvalFilter: props.approvalFilter });
}
function handleSelected(rows: SrmSupplierApi.SupplierVO[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>编码:{{ selectedItem.code || '-' }}</div>
          <div>名称:{{ selectedItem.name || '-' }}</div>
          <div>联系人:{{ selectedItem.contactName || '-' }}</div>
          <div>电话:{{ selectedItem.contactMobile || '-' }}</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <SrmSupplierSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>
src/views/srm/supplier/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import {
  SUPPLIER_STATUS_OPTIONS,
  SUPPLIER_STATUS_CELLTAG,
  SUPPLIER_TYPE_OPTIONS,
  SUPPLIER_TYPE_CELLTAG,
} from '../enums';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '供应商编码',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '请输入供应商编码' },
    },
    {
      fieldName: 'name',
      label: '供应商名称',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '请输入供应商名称' },
    },
    {
      fieldName: 'type',
      label: '供应商类型',
      component: 'Select',
      rules: 'required',
      componentProps: {
        placeholder: '请选择供应商类型',
        options: SUPPLIER_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: SUPPLIER_STATUS_OPTIONS,
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'contactName',
      label: '联系人',
      component: 'Input',
      componentProps: { placeholder: '请输入联系人' },
    },
    {
      fieldName: 'contactMobile',
      label: '联系电话',
      component: 'Input',
      componentProps: { placeholder: '请输入联系电话' },
    },
    {
      fieldName: 'contactEmail',
      label: '邮箱',
      component: 'Input',
      componentProps: { placeholder: '请输入邮箱' },
    },
    {
      fieldName: 'address',
      label: '地址',
      component: 'Input',
      componentProps: { placeholder: '请输入地址' },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'bankName',
      label: '开户银行',
      component: 'Input',
      componentProps: { placeholder: '请输入开户银行' },
    },
    {
      fieldName: 'bankAccount',
      label: '银行账号',
      component: 'Input',
      componentProps: { placeholder: '请输入银行账号' },
    },
    {
      fieldName: 'taxNo',
      label: '税号',
      component: 'Input',
      componentProps: { placeholder: '请输入税号' },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 3 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '编码',
      component: 'Input',
      componentProps: { placeholder: '请输入编码', allowClear: true },
    },
    {
      fieldName: 'name',
      label: '供应商名称',
      component: 'Input',
      componentProps: { placeholder: '请输入供应商名称', allowClear: true },
    },
    {
      fieldName: 'type',
      label: '类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择类型',
        allowClear: true,
        options: SUPPLIER_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: SUPPLIER_STATUS_OPTIONS,
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'code', title: '编码', minWidth: 120 },
    { field: 'name', title: '供应商名称', minWidth: 150 },
    {
      field: 'type',
      title: '类型',
      minWidth: 100,
      cellRender: { name: 'CellTag', props: SUPPLIER_TYPE_CELLTAG },
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 80,
      cellRender: { name: 'CellTag', props: SUPPLIER_STATUS_CELLTAG },
    },
    { field: 'contactName', title: '联系人', minWidth: 100 },
    { field: 'contactMobile', title: '联系电话', minWidth: 120 },
    { field: 'contactEmail', title: '邮箱', minWidth: 150 },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/supplier/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteSupplier,
  getSupplierPage,
} from '#/api/srm/supplier';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import SupplierForm from './modules/form.vue';
defineOptions({ name: 'SrmSupplier' });
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmSupplierApi.SupplierVO) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: SrmSupplierApi.SupplierVO) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteSupplier(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: SupplierForm,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getSupplierPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmSupplierApi.SupplierVO>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="供应商列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['供应商']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:supplier:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:supplier:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/supplier/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createSupplier,
  getSupplier,
  updateSupplier,
} from '#/api/srm/supplier';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierApi.SupplierVO>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['供应商'])
    : $t('ui.actionTitle.create', ['供应商']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmSupplierApi.SupplierVO;
    try {
      await (formData.value?.id ? updateSupplier(data) : createSupplier(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<SrmSupplierApi.SupplierVO>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getSupplier(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/supplierApply/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { markRaw } from 'vue';
import { z } from '#/adapter/form';
import { SrmSupplierSelect } from '#/views/srm/supplier/components';
import { APPLY_STATUS_CELLTAG, APPLY_STATUS_OPTIONS } from '../enums';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'supplierId',
      label: '选择供应商',
      component: markRaw(SrmSupplierSelect),
      componentProps: {
        approvalFilter: false,
        placeholder: '请选择SRM供应商(或直接填写供应商名称)',
        onChange: async (supplier?: SrmSupplierApi.SupplierVO) => {
          if (!supplier || !formApi) return;
          await formApi.setValues({
            supplierName: supplier.name || '',
            contactName: supplier.contactName || '',
            contactMobile: supplier.contactMobile || '',
            contactEmail: supplier.contactEmail || '',
            taxNo: supplier.taxNo || '',
            address: supplier.address || '',
          });
        },
      },
      dependencies: {
        triggerFields: ['supplierName'],
        componentProps: (values) => ({
          approvalFilter: false,
          placeholder: '请选择SRM供应商(或直接填写供应商名称)',
          supplierName: values.supplierName as string,
        }),
      },
    },
    {
      fieldName: 'supplierName',
      label: '供应商名称',
      component: 'Input',
      componentProps: { placeholder: '请输入供应商名称' },
    },
    {
      fieldName: 'contactName',
      label: '联系人',
      component: 'Input',
      componentProps: { placeholder: '请输入联系人' },
    },
    {
      fieldName: 'contactMobile',
      label: '联系电话',
      component: 'Input',
      componentProps: { placeholder: '请输入联系电话' },
    },
    {
      fieldName: 'contactEmail',
      label: '邮箱',
      component: 'Input',
      componentProps: { placeholder: '请输入邮箱' },
    },
    {
      fieldName: 'address',
      label: '地址',
      component: 'Input',
      componentProps: { placeholder: '请输入地址' },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'bankName',
      label: '开户银行',
      component: 'Input',
      componentProps: { placeholder: '请输入开户银行' },
    },
    {
      fieldName: 'bankAccount',
      label: '银行账号',
      component: 'Input',
      componentProps: { placeholder: '请输入银行账号' },
    },
    {
      fieldName: 'taxNo',
      label: '税号',
      component: 'Input',
      componentProps: { placeholder: '请输入税号' },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 3 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'applyNo',
      label: '申请编号',
      component: 'Input',
      componentProps: { placeholder: '请输入申请编号', allowClear: true },
    },
    {
      fieldName: 'supplierName',
      label: '供应商名称',
      component: 'Input',
      componentProps: { placeholder: '请输入供应商名称', allowClear: true },
    },
    {
      fieldName: 'applyStatus',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: APPLY_STATUS_OPTIONS,
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'applyNo', title: '申请编号', minWidth: 140 },
    { field: 'supplierName', title: '供应商名称', minWidth: 150 },
    {
      field: 'applyStatus',
      title: '状态',
      minWidth: 100,
      cellRender: { name: 'CellTag', props: APPLY_STATUS_CELLTAG },
    },
    { field: 'contactName', title: '联系人', minWidth: 100 },
    { field: 'contactMobile', title: '联系电话', minWidth: 120 },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/supplierApply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import { computed, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteApply,
  getApplyPage,
  submitApply,
  reviewPurchase,
  reviewQuality,
  reviewFinance,
} from '#/api/srm/supplierApply';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import ApplyForm from './modules/form.vue';
import { APPLY_STATUS } from '../enums';
defineOptions({ name: 'SrmSupplierApply' });
// ---------- reviewModal ----------
const reviewFormData = ref({
  applyId: 0,
  reviewType: '' as 'purchase' | 'quality' | 'finance',
  passed: true,
  comment: '',
});
const reviewTitle = computed(() => {
  const labels: Record<string, string> = {
    purchase: '采购审核',
    quality: '质量审核',
    finance: '财务审核',
  };
  return labels[reviewFormData.value.reviewType] || '审核';
});
const [ReviewForm, reviewFormApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'passed',
      label: '审核结果',
      component: 'RadioGroup',
      rules: 'required',
      componentProps: {
        options: [
          { label: '通过', value: true },
          { label: '拒绝', value: false },
        ],
        buttonStyle: 'solid',
        optionType: 'button',
      },
    },
    {
      fieldName: 'comment',
      label: '审核意见',
      component: 'Textarea',
      componentProps: { placeholder: '请输入审核意见', rows: 3 },
    },
  ],
  showDefaultActions: false,
});
const [ReviewModal, reviewModalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await reviewFormApi.validate();
    if (!valid) {
      return;
    }
    reviewModalApi.lock();
    const data = (await reviewFormApi.getValues()) as { passed: boolean; comment?: string };
    try {
      const payload: SrmSupplierApplyApi.ReviewVO = {
        id: reviewFormData.value.applyId,
        passed: data.passed,
        comment: data.comment,
      };
      if (reviewFormData.value.reviewType === 'purchase') {
        await reviewPurchase(payload);
      } else if (reviewFormData.value.reviewType === 'quality') {
        await reviewQuality(payload);
      } else if (reviewFormData.value.reviewType === 'finance') {
        await reviewFinance(payload);
      }
      await reviewModalApi.close();
      handleRefresh();
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      reviewModalApi.unlock();
    }
  },
});
function openReviewModal(row: SrmSupplierApplyApi.ApplyVO, reviewType: 'purchase' | 'quality' | 'finance') {
  reviewFormData.value = {
    applyId: row.id!,
    reviewType,
    passed: true,
    comment: '',
  };
  reviewFormApi.resetForm();
  reviewModalApi.open();
}
// ---------- grid ----------
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmSupplierApplyApi.ApplyVO) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: SrmSupplierApplyApi.ApplyVO) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.supplierName]),
    duration: 0,
  });
  try {
    await deleteApply(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.supplierName]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handleSubmit(row: SrmSupplierApplyApi.ApplyVO) {
  const hideLoading = message.loading({ content: '提交中...', duration: 0 });
  try {
    await submitApply(row.id!);
    message.success($t('ui.actionMessage.operationSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: ApplyForm,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getApplyPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmSupplierApplyApi.ApplyVO>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ReviewModal :title="reviewTitle" class="w-1/3">
      <ReviewForm class="mx-4" />
    </ReviewModal>
    <Grid table-title="准入申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['准入申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:supplier-apply:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier-apply:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.DRAFT,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '采购审核',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_PURCHASE,
              onClick: openReviewModal.bind(null, row, 'purchase'),
            },
            {
              label: '质量审核',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_QUALITY,
              onClick: openReviewModal.bind(null, row, 'quality'),
            },
            {
              label: '财务审核',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_FINANCE,
              onClick: openReviewModal.bind(null, row, 'finance'),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:supplier-apply:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.supplierName]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/supplierApply/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createApply,
  getApply,
  updateApply,
} from '#/api/srm/supplierApply';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierApplyApi.ApplyVO>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['准入申请'])
    : $t('ui.actionTitle.create', ['准入申请']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
// æ³¨å…¥ formApi ä»¥æ”¯æŒé€‰æ‹© SRM ä¾›åº”商后自动填充表单字段
formApi.setState({ schema: useFormSchema(formApi) });
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmSupplierApplyApi.ApplyVO;
    try {
      await (formData.value?.id ? updateApply(data) : createApply(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<SrmSupplierApplyApi.ApplyVO>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getApply(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/supplierCertificate/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,153 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { CERTIFICATE_TYPE_CELLTAG, CERTIFICATE_TYPE_OPTIONS } from '../enums';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
      rules: 'required',
    },
    {
      fieldName: 'certificateName',
      label: '资质证书名称',
      component: 'Input',
      componentProps: { placeholder: '请输入资质证书名称' },
      rules: 'required',
    },
    {
      fieldName: 'certificateNo',
      label: '证书编号',
      component: 'Input',
      componentProps: { placeholder: '请输入证书编号' },
    },
    {
      fieldName: 'certificateType',
      label: '证书类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择证书类型',
        options: CERTIFICATE_TYPE_OPTIONS,
      },
      rules: 'required',
    },
    {
      fieldName: 'issuingAuthority',
      label: '颁发机构',
      component: 'Input',
      componentProps: { placeholder: '请输入颁发机构' },
    },
    {
      fieldName: 'issueDate',
      label: '颁发日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择颁发日期',
        allowClear: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'expireDate',
      label: '到期日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择到期日期',
        allowClear: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 3 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'certificateName',
      label: '资质证书名称',
      component: 'Input',
      componentProps: { placeholder: '请输入资质证书名称', allowClear: true },
    },
    {
      fieldName: 'certificateType',
      label: '证书类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择证书类型',
        allowClear: true,
        options: CERTIFICATE_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'supplierName', title: '供应商', minWidth: 120 },
    { field: 'certificateName', title: '资质证书名称', minWidth: 150 },
    { field: 'certificateNo', title: '证书编号', minWidth: 120 },
    {
      field: 'certificateType',
      title: '证书类型',
      minWidth: 120,
      cellRender: { name: 'CellTag', props: CERTIFICATE_TYPE_CELLTAG },
    },
    { field: 'issuingAuthority', title: '颁发机构', minWidth: 120 },
    { field: 'issueDate', title: '颁发日期', minWidth: 120 },
    { field: 'expireDate', title: '到期日期', minWidth: 120 },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/supplierCertificate/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierCertificateApi } from '#/api/srm/supplierCertificate';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteCertificate,
  getCertificatePage,
  getExpiringCertificates,
} from '#/api/srm/supplierCertificate';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import CertificateForm from './modules/form.vue';
defineOptions({ name: 'SrmSupplierCertificate' });
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmSupplierCertificateApi.CertificateVO) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: SrmSupplierCertificateApi.CertificateVO) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.certificateName]),
    duration: 0,
  });
  try {
    await deleteCertificate(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.certificateName]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handleExpiringCertificates() {
  try {
    const data = await getExpiringCertificates();
    if (data && data.length > 0) {
      message.warning(`有 ${data.length} ä¸ªèµ„质证书即将到期`);
    } else {
      message.success('没有即将到期的资质证书');
    }
  } catch {
    message.error('获取即将到期的资质证书失败');
  }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: CertificateForm,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getCertificatePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmSupplierCertificateApi.CertificateVO>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="资质证书列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['资质证书']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:supplier-certificate:create'],
              onClick: handleCreate,
            },
            {
              label: '到期提醒',
              type: 'warning',
              icon: 'ant-design:bell-outlined',
              auth: ['srm:supplier-certificate:query'],
              onClick: handleExpiringCertificates,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier-certificate:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:supplier-certificate:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.certificateName]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/supplierCertificate/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SrmSupplierCertificateApi } from '#/api/srm/supplierCertificate';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createCertificate,
  getCertificate,
  updateCertificate,
} from '#/api/srm/supplierCertificate';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierCertificateApi.CertificateVO>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['资质证书'])
    : $t('ui.actionTitle.create', ['资质证书']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmSupplierCertificateApi.CertificateVO;
    try {
      await (formData.value?.id ? updateCertificate(data) : createCertificate(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<SrmSupplierCertificateApi.CertificateVO>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getCertificate(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/supplierEvaluation/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { COMPOSITE_SCORE_CELLTAG, EVALUATION_GRADE_CELLTAG } from '../enums';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      rules: 'required',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'evaluationPeriod',
      label: '评价周期',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '如 2024-Q1' },
    },
    {
      fieldName: 'qualityScore',
      label: '质量评分',
      component: 'InputNumber',
      rules: 'required',
      componentProps: { min: 0, max: 100, placeholder: '请输入质量评分' },
    },
    {
      fieldName: 'deliveryScore',
      label: '交付评分',
      component: 'InputNumber',
      rules: 'required',
      componentProps: { min: 0, max: 100, placeholder: '请输入交付评分' },
    },
    {
      fieldName: 'priceScore',
      label: '价格评分',
      component: 'InputNumber',
      rules: 'required',
      componentProps: { min: 0, max: 100, placeholder: '请输入价格评分' },
    },
    {
      fieldName: 'serviceScore',
      label: '服务评分',
      component: 'InputNumber',
      rules: 'required',
      componentProps: { min: 0, max: 100, placeholder: '请输入服务评分' },
    },
    {
      fieldName: 'remark',
      label: '评价说明',
      component: 'Textarea',
      componentProps: { placeholder: '请输入评价说明', rows: 3 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'evaluationPeriod',
      label: '评价周期',
      component: 'Input',
      componentProps: { placeholder: '请输入评价周期', allowClear: true },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'supplierName', title: '供应商', minWidth: 150 },
    { field: 'evaluationPeriod', title: '评价周期', minWidth: 120 },
    { field: 'qualityScore', title: '质量评分', minWidth: 100 },
    { field: 'deliveryScore', title: '交付评分', minWidth: 100 },
    { field: 'priceScore', title: '价格评分', minWidth: 100 },
    { field: 'serviceScore', title: '服务评分', minWidth: 100 },
    {
      field: 'compositeScore',
      title: '综合评分',
      minWidth: 120,
      cellRender: { name: 'CellTag', props: COMPOSITE_SCORE_CELLTAG },
    },
    {
      field: 'grade',
      title: '等级',
      minWidth: 80,
      cellRender: { name: 'CellTag', props: EVALUATION_GRADE_CELLTAG },
    },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/supplierEvaluation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierEvaluationApi } from '#/api/srm/supplierEvaluation';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEvaluation,
  getEvaluationPage,
} from '#/api/srm/supplierEvaluation';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import EvaluationForm from './modules/form.vue';
defineOptions({ name: 'SrmSupplierEvaluation' });
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmSupplierEvaluationApi.EvaluationVO) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: SrmSupplierEvaluationApi.EvaluationVO) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.evaluationPeriod]),
    duration: 0,
  });
  try {
    await deleteEvaluation(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.evaluationPeriod]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: EvaluationForm,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getEvaluationPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmSupplierEvaluationApi.EvaluationVO>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="绩效评价列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['绩效评价']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:supplier-evaluation:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier-evaluation:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:supplier-evaluation:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.evaluationPeriod]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/supplierEvaluation/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { SrmSupplierEvaluationApi } from '#/api/srm/supplierEvaluation';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEvaluation,
  getEvaluation,
  updateEvaluation,
} from '#/api/srm/supplierEvaluation';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierEvaluationApi.EvaluationVO>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['绩效评价'])
    : $t('ui.actionTitle.create', ['绩效评价']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmSupplierEvaluationApi.EvaluationVO;
    try {
      await (formData.value?.id ? updateEvaluation(data) : createEvaluation(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<SrmSupplierEvaluationApi.EvaluationVO>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getEvaluation(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/tender/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import {
  TENDER_STATUS_CELLTAG,
  TENDER_STATUS_OPTIONS,
  TENDER_TYPE_CELLTAG,
  TENDER_TYPE_OPTIONS,
} from '../enums';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'tenderNo',
      label: '招标编号',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '请输入招标编号' },
    },
    {
      fieldName: 'tenderName',
      label: '招标项目名称',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '请输入招标项目名称' },
    },
    {
      fieldName: 'tenderType',
      label: '招标类型',
      component: 'Select',
      rules: 'required',
      componentProps: {
        placeholder: '请选择招标类型',
        options: TENDER_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'purchaseOrg',
      label: '采购组织',
      component: 'Input',
      componentProps: { placeholder: '请输入采购组织' },
    },
    {
      fieldName: 'projectLeaderName',
      label: '项目负责人',
      component: 'Input',
      componentProps: { placeholder: '请输入项目负责人' },
    },
    {
      fieldName: 'budgetAmount',
      label: '预算金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入预算金额',
      },
    },
    {
      fieldName: 'startTime',
      label: '开始时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        placeholder: '请选择开始时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'endTime',
      label: '截止时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        placeholder: '请选择截止时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'bidOpenTime',
      label: '开标时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        placeholder: '请选择开标时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'tenderDesc',
      label: '招标说明',
      component: 'Textarea',
      componentProps: { placeholder: '请输入招标说明', rows: 3 },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'qualificationRequirements',
      label: '资质要求',
      component: 'Textarea',
      componentProps: { placeholder: '请输入资质要求', rows: 3 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'tenderNo',
      label: '招标编号',
      component: 'Input',
      componentProps: { placeholder: '请输入招标编号', allowClear: true },
    },
    {
      fieldName: 'tenderName',
      label: '招标项目名称',
      component: 'Input',
      componentProps: { placeholder: '请输入招标项目名称', allowClear: true },
    },
    {
      fieldName: 'tenderType',
      label: '招标类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择招标类型',
        allowClear: true,
        options: TENDER_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'tenderStatus',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: TENDER_STATUS_OPTIONS,
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'tenderNo', title: '招标编号', minWidth: 140 },
    { field: 'tenderName', title: '招标项目名称', minWidth: 180 },
    {
      field: 'tenderType',
      title: '招标类型',
      minWidth: 120,
      cellRender: { name: 'CellTag', props: TENDER_TYPE_CELLTAG },
    },
    {
      field: 'tenderStatus',
      title: '状态',
      minWidth: 100,
      cellRender: { name: 'CellTag', props: TENDER_STATUS_CELLTAG },
    },
    { field: 'budgetAmount', title: '预算金额', minWidth: 120 },
    { field: 'purchaseOrg', title: '采购组织', minWidth: 100 },
    { field: 'startTime', title: '开始时间', minWidth: 160 },
    { field: 'endTime', title: '截止时间', minWidth: 160 },
    { field: 'purchaseOrderNo', title: '采购订单号', minWidth: 140 },
    {
      title: '操作',
      width: 250,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/tender/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,811 @@
<script lang="ts" setup>
import type { SrmTenderApi } from '#/api/srm/tender';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import {
  getTenderProject,
  getMaterialList, createMaterial, updateMaterial, deleteMaterial,
  getBidListByProject, createBid, withdrawBid,
  getQuoteList, createQuote, updateQuote,
  createBidOpen, getBidOpen,
  getEvaluationList, createBidEvaluation, calculateRanking,
  createAward, getAward, approveAward, generatePurchaseOrder,
} from '#/api/srm/tender';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getItemSimpleList } from '#/api/mdm/item';
import type { MdmItemApi } from '#/api/mdm/item';
import {
  TENDER_STATUS,
  TENDER_STATUS_CELLTAG,
  TENDER_STATUS_COLORS,
  TENDER_TYPE_CELLTAG,
  BID_STATUS,
  BID_STATUS_MAP,
  AWARD_STATUS,
  AWARD_STATUS_MAP,
  getLabel,
} from '../enums';
defineOptions({ name: 'SrmTenderDetail' });
const route = useRoute();
const router = useRouter();
const projectId = Number(route.query.id);
const project = ref<SrmTenderApi.TenderProjectVO>();
const activeTab = ref('info');
// ========== ä¸‹æ‹‰é€‰é¡¹æ•°æ® ==========
const mdmItemList = ref<MdmItemApi.Item[]>([]);
const supplierList = ref<{ id: number; name: string }[]>([]);
onMounted(async () => {
  const [items, suppliers] = await Promise.all([
    getItemSimpleList(),
    getSupplierSimpleList(),
  ]);
  mdmItemList.value = items;
  supplierList.value = suppliers;
});
const filterOption = (input: string, option: any) =>
  option.label?.toLowerCase().indexOf(input.toLowerCase()) >= 0;
// ========== åŸºæœ¬ä¿¡æ¯ ==========
const materialList = ref<SrmTenderApi.TenderMaterialVO[]>([]);
const materialLoading = ref(false);
const materialFormVisible = ref(false);
const materialForm = ref<SrmTenderApi.TenderMaterialVO>({ tenderProjectId: projectId });
async function loadProject() {
  project.value = await getTenderProject(projectId);
}
async function loadMaterials() {
  materialLoading.value = true;
  try { materialList.value = await getMaterialList(projectId); } finally { materialLoading.value = false; }
}
async function handleSaveMaterial() {
  if (materialForm.value.id) {
    await updateMaterial(materialForm.value);
  } else {
    await createMaterial(materialForm.value);
  }
  materialFormVisible.value = false;
  loadMaterials();
}
function onMaterialSelect(itemId: number) {
  const item = mdmItemList.value.find((i) => i.id === itemId);
  if (item) {
    materialForm.value.productCode = item.code;
    materialForm.value.productName = item.name;
    materialForm.value.productSpec = item.specification;
    materialForm.value.unit = item.unitMeasureName;
  }
}
function handleAddMaterial() {
  materialForm.value = { tenderProjectId: projectId };
  materialFormVisible.value = true;
}
function handleEditMaterial(row: SrmTenderApi.TenderMaterialVO) {
  materialForm.value = { ...row };
  materialFormVisible.value = true;
}
async function handleDeleteMaterial(id: number) {
  await deleteMaterial(id);
  loadMaterials();
}
// ========== æŠ•标管理 ==========
const bidList = ref<SrmTenderApi.TenderBidVO[]>([]);
const bidLoading = ref(false);
const bidFormVisible = ref(false);
const bidForm = ref<SrmTenderApi.TenderBidVO>({ tenderProjectId: projectId, supplierId: 0, bidNo: '' });
async function loadBids() {
  bidLoading.value = true;
  try { bidList.value = await getBidListByProject(projectId); } finally { bidLoading.value = false; }
}
async function handleCreateBid() {
  await createBid(bidForm.value);
  bidFormVisible.value = false;
  loadBids();
}
async function handleWithdrawBid(id: number) {
  await withdrawBid(id);
  loadBids();
}
// ========== æŠ¥ä»·ç®¡ç† ==========
const quoteList = ref<SrmTenderApi.QuoteVO[]>([]);
const quoteLoading = ref(false);
const quoteFormVisible = ref(false);
const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: 0, tenderMaterialId: 0 });
const selectedBidId = ref<number>();
const availableMaterials = computed(() => {
  const quotedIds = new Set(
    quoteList.value
      .filter((q) => q.tenderMaterialId !== undefined)
      .map((q) => q.tenderMaterialId),
  );
  // ç¼–辑时允许保留当前物料
  if (quoteForm.value.id && quoteForm.value.tenderMaterialId) {
    quotedIds.delete(quoteForm.value.tenderMaterialId);
  }
  return materialList.value.filter((m) => !quotedIds.has(m.id));
});
async function loadQuotes(bidId?: number) {
  if (!bidId) return;
  selectedBidId.value = bidId;
  quoteLoading.value = true;
  try { quoteList.value = await getQuoteList(bidId); } finally { quoteLoading.value = false; }
}
async function handleSaveQuote() {
  if (quoteForm.value.id) {
    await updateQuote(quoteForm.value);
  } else {
    await createQuote(quoteForm.value);
  }
  quoteFormVisible.value = false;
  loadQuotes(selectedBidId.value);
}
function handleAddQuote() {
  quoteForm.value = { bidId: selectedBidId.value!, tenderMaterialId: 0 };
  quoteFormVisible.value = true;
}
function handleEditQuote(row: SrmTenderApi.QuoteVO) {
  quoteForm.value = { ...row };
  quoteFormVisible.value = true;
}
// ========== å¼€æ ‡ ==========
const bidOpen = ref<any>(null);
async function loadBidOpen() {
  try {
    bidOpen.value = null;
  } catch { /* æš‚无开标记录 */ }
}
async function handleCreateBidOpen() {
  await createBidOpen(projectId);
  message.success('开标成功');
  loadBidOpen();
  loadProject();
}
// ========== è¯„标管理 ==========
const evaluationList = ref<SrmTenderApi.BidEvaluationVO[]>([]);
const evalLoading = ref(false);
const evalFormVisible = ref(false);
const evalForm = ref<SrmTenderApi.BidEvaluationVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0 });
async function loadEvaluations() {
  evalLoading.value = true;
  try { evaluationList.value = await getEvaluationList(projectId); } finally { evalLoading.value = false; }
}
async function handleCreateEvaluation() {
  await createBidEvaluation(evalForm.value);
  evalFormVisible.value = false;
  loadEvaluations();
  loadProject();
}
async function handleCalculateRanking() {
  await calculateRanking(projectId);
  message.success('排名计算完成');
  loadEvaluations();
}
// ========== å®šæ ‡ ==========
const award = ref<SrmTenderApi.BidAwardVO>();
const awardFormVisible = ref(false);
const awardForm = ref<SrmTenderApi.BidAwardVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' });
async function loadAward() {
  try { award.value = await getAward(projectId); } catch { award.value = undefined; }
}
async function handleCreateAward() {
  await createAward(awardForm.value);
  awardFormVisible.value = false;
  loadAward();
  loadProject();
}
async function handleApproveAward() {
  if (!award.value?.id) return;
  await approveAward(award.value.id);
  message.success('定标审批通过');
  loadAward();
  loadProject();
}
async function handleGeneratePurchaseOrder() {
  if (!award.value?.id) return;
  await generatePurchaseOrder(award.value.id);
  message.success('采购订单已生成');
  loadAward();
  loadProject();
}
// ========== åˆå§‹åŒ– ==========
loadProject();
loadMaterials();
loadBids();
loadEvaluations();
loadAward();
// ========== çŠ¶æ€åˆ¤æ–­ ==========
const statusLabel = computed(() => getLabel(TENDER_STATUS_CELLTAG.labels, project.value?.tenderStatus));
const statusColor = computed(() => TENDER_STATUS_COLORS[project.value?.tenderStatus!] || 'default');
const typeLabel = computed(() => getLabel(TENDER_TYPE_CELLTAG.labels, project.value?.tenderType));
const stepItems = [
  { title: '草稿', status: TENDER_STATUS.DRAFT },
  { title: '发布', status: TENDER_STATUS.PUBLISHED },
  { title: '投标中', status: TENDER_STATUS.BIDDING },
  { title: '评标中', status: TENDER_STATUS.EVALUATING },
  { title: '已定标', status: TENDER_STATUS.AWARDED },
];
const currentStep = computed(() => {
  const s = project.value?.tenderStatus ?? 0;
  if (s === TENDER_STATUS.CLOSED) return 4;
  const idx = stepItems.findIndex((item) => item.status >= s);
  return idx === -1 ? 0 : idx;
});
const stepStatus = computed(() => {
  const s = project.value?.tenderStatus;
  if (s === TENDER_STATUS.CLOSED) return 'error';
  if (s === TENDER_STATUS.AWARDED) return 'finish';
  return 'process';
});
const materialTotalAmount = computed(() =>
  materialList.value.reduce((sum, m) => sum + (m.estimatedPrice ?? 0) * (m.quantity ?? 0), 0),
);
const bidCount = computed(() => bidList.value.length);
const bidTotalAmount = computed(() =>
  bidList.value.reduce((sum, b) => sum + (b.bidTotalAmount ?? 0), 0),
);
const evalCount = computed(() => evaluationList.value.length);
const rankedCount = computed(() => evaluationList.value.filter((e) => e.rank != null).length);
const selectedBid = computed(() =>
  bidList.value.find((b) => b.id === selectedBidId.value),
);
const materialColumns = [
  { title: '物料编码', dataIndex: 'productCode', width: 120 },
  { title: '物料名称', dataIndex: 'productName', width: 150 },
  { title: '规格型号', dataIndex: 'productSpec', width: 120 },
  { title: '单位', dataIndex: 'unit', width: 80 },
  { title: '数量', dataIndex: 'quantity', width: 80 },
  { title: '技术要求', dataIndex: 'techRequirement', width: 150 },
  { title: '预估单价', dataIndex: 'estimatedPrice', width: 100 },
  { title: '操作', key: 'actions', width: 150 },
];
const bidColumns = [
  { title: '投标编号', dataIndex: 'bidNo', width: 140 },
  { title: '供应商', dataIndex: 'supplierName', width: 200 },
  { title: '投标金额', dataIndex: 'bidTotalAmount', width: 120 },
  { title: '投标时间', dataIndex: 'bidTime', width: 160 },
  { title: '状态', dataIndex: 'bidStatus', width: 100, customRender: ({ text }: any) => BID_STATUS_MAP[text] || text },
  { title: '操作', key: 'actions', width: 180 },
];
const quoteColumns = [
  { title: '物料', dataIndex: 'materialName', width: 120 },
  { title: '报价价格', dataIndex: 'quotePrice', width: 100 },
  { title: '税率(%)', dataIndex: 'taxRate', width: 80 },
  { title: '交期(天)', dataIndex: 'deliveryCycle', width: 80 },
  { title: '付款条件', dataIndex: 'paymentTerms', width: 120 },
  { title: '质保期(月)', dataIndex: 'warrantyPeriod', width: 80 },
  { title: '备注', dataIndex: 'remark', width: 150 },
  { title: '操作', key: 'actions', width: 120 },
];
const evalColumns = [
  { title: '投标ID', dataIndex: 'bidId', width: 80 },
  { title: '供应商', dataIndex: 'supplierName', width: 150 },
  { title: '价格评分', dataIndex: 'priceScore', width: 100 },
  { title: '技术评分', dataIndex: 'techScore', width: 100 },
  { title: '交付评分', dataIndex: 'deliveryScore', width: 100 },
  { title: '服务评分', dataIndex: 'serviceScore', width: 100 },
  { title: '综合评分', dataIndex: 'compositeScore', width: 100 },
  { title: '排名', dataIndex: 'rank', width: 80 },
  { title: '评委', dataIndex: 'evaluatorName', width: 100 },
  { title: '评语', dataIndex: 'evaluationOpinion', width: 150 },
  { title: '操作', key: 'actions', width: 120 },
];
</script>
<template>
  <Page auto-content-height>
    <!-- åŠ è½½ä¸­ -->
    <a-skeleton v-if="!project" active class="p-4" />
    <template v-else>
      <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
      <div class="flex items-center gap-2 px-4 pt-3 pb-2">
        <a-button type="text" size="small" @click="router.back()">
          <template #icon><IconifyIcon icon="ant-design:arrow-left-outlined" /></template>
        </a-button>
        <span class="text-lg font-bold truncate max-w-md">{{ project.tenderName }}</span>
        <span class="text-gray-400 text-xs">{{ project.tenderNo }}</span>
        <a-tag :color="statusColor">{{ statusLabel }}</a-tag>
        <a-tag color="blue">{{ typeLabel }}</a-tag>
        <div class="flex-1" />
        <a-button size="small" @click="loadProject(); loadMaterials(); loadBids(); loadEvaluations(); loadAward();">
          <template #icon><IconifyIcon icon="ant-design:reload-outlined" /></template>
          åˆ·æ–°
        </a-button>
      </div>
      <!-- å·¥ä½œæµè¿›åº¦ + å…³é”®ä¿¡æ¯ -->
      <div class="px-4 mb-3">
        <a-card size="small">
          <!-- æµç¨‹æ­¥éª¤ -->
          <a-steps :current="currentStep" :status="stepStatus" size="small" class="mb-3">
              <a-step v-for="item in stepItems" :key="item.status" :title="item.title" />
            </a-steps>
          <!-- å…³é”®å­—段 -->
          <div class="grid grid-cols-6 gap-3 text-xs">
            <div>
              <div class="text-gray-400 mb-0.5">预算金额</div>
              <div class="font-semibold text-blue-500">{{ project.budgetAmount?.toLocaleString() ?? '-' }} å…ƒ</div>
            </div>
            <div>
              <div class="text-gray-400 mb-0.5">负责人</div>
              <div class="font-semibold">{{ project.projectLeaderName || '-' }}</div>
            </div>
            <div>
              <div class="text-gray-400 mb-0.5">采购组织</div>
              <div class="font-semibold">{{ project.purchaseOrg || '-' }}</div>
            </div>
            <div>
              <div class="text-gray-400 mb-0.5">开标时间</div>
              <div class="font-semibold">{{ project.bidOpenTime || '待定' }}</div>
            </div>
            <div>
              <div class="text-gray-400 mb-0.5">开始时间</div>
              <div class="font-semibold">{{ project.startTime || '-' }}</div>
            </div>
            <div>
              <div class="text-gray-400 mb-0.5">截止时间</div>
              <div class="font-semibold">{{ project.endTime || '-' }}</div>
            </div>
          </div>
          <!-- æ‹›æ ‡è¯´æ˜Ž / èµ„质要求 -->
          <template v-if="project.tenderDesc || project.qualificationRequirements">
            <a-divider class="!my-2" />
            <a-collapse :bordered="false" ghost expand-icon-position="end">
              <a-collapse-panel v-if="project.tenderDesc" key="desc" header="招标说明">
                <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.tenderDesc }}</p>
              </a-collapse-panel>
              <a-collapse-panel v-if="project.qualificationRequirements" key="req" header="资质要求">
                <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
              </a-collapse-panel>
            </a-collapse>
          </template>
        </a-card>
      </div>
      <!-- Tab é¡µç­¾ -->
      <div class="px-4 pb-4">
        <a-card size="small">
          <a-tabs v-model:activeKey="activeTab" size="small">
            <!-- Tab1: ç‰©æ–™æ¸…单 -->
            <a-tab-pane key="info">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:unordered-list-outlined" />物料清单</a-space>
              </template>
              <div class="flex items-center justify-between mb-3">
                <a-space>
                  <span class="text-xs text-gray-400">共 <b class="text-gray-600">{{ materialList.length }}</b> é¡¹ï¼Œé¢„估总金额 <b class="text-blue-500">{{ materialTotalAmount.toLocaleString() }}</b> å…ƒ</span>
                </a-space>
                <a-button type="primary" size="small" @click="handleAddMaterial">
                  <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                  æ·»åŠ ç‰©æ–™
                </a-button>
              </div>
              <a-table :columns="materialColumns" :data-source="materialList" :loading="materialLoading" :pagination="false" row-key="id" size="small">
                <template #bodyCell="{ column, record }">
                  <template v-if="column.dataIndex === 'estimatedPrice'">
                    {{ record.estimatedPrice ? record.estimatedPrice.toLocaleString() : '-' }}
                  </template>
                  <template v-if="column.key === 'actions'">
                    <a-space>
                      <a-button size="small" type="link" @click="handleEditMaterial(record)"><IconifyIcon icon="ant-design:edit-outlined" />编辑</a-button>
                      <a-popconfirm title="确认删除该物料?" @confirm="handleDeleteMaterial(record.id)">
                        <a-button size="small" type="link" danger><IconifyIcon icon="ant-design:delete-outlined" />删除</a-button>
                      </a-popconfirm>
                    </a-space>
                  </template>
                </template>
              </a-table>
              <a-empty v-if="!materialLoading && materialList.length === 0" description="暂无物料" class="mt-8" />
            </a-tab-pane>
            <!-- Tab2: æŠ•标管理 -->
            <a-tab-pane key="bid">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:team-outlined" />投标管理</a-space>
              </template>
              <div class="flex items-center justify-between mb-3">
                <span class="text-xs text-gray-400">共 <b class="text-gray-600">{{ bidCount }}</b> ä¸ªæŠ•æ ‡</span>
                <a-button type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: 0, bidNo: '' }; bidFormVisible = true">
                  <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                  æ–°å¢žæŠ•æ ‡
                </a-button>
              </div>
              <a-table :columns="bidColumns" :data-source="bidList" :loading="bidLoading" :pagination="false" row-key="id" size="small">
                <template #bodyCell="{ column, record }">
                  <template v-if="column.dataIndex === 'bidTotalAmount'">
                    <span class="font-medium">{{ record.bidTotalAmount?.toLocaleString() }}</span>
                  </template>
                  <template v-if="column.dataIndex === 'bidStatus'">
                    <a-tag :color="record.bidStatus === BID_STATUS.WIN ? 'green' : record.bidStatus === BID_STATUS.LOSE ? 'red' : 'default'">
                      {{ BID_STATUS_MAP[record.bidStatus] }}
                    </a-tag>
                  </template>
                  <template v-if="column.key === 'actions'">
                    <a-space>
                      <a-button size="small" type="link" @click="loadQuotes(record.id); activeTab = 'quote'"><IconifyIcon icon="ant-design:dollar-outlined" />报价</a-button>
                      <a-popconfirm v-if="record.bidStatus === BID_STATUS.REGISTERED" title="确认撤标?" @confirm="handleWithdrawBid(record.id)">
                        <a-button size="small" type="link" danger><IconifyIcon icon="ant-design:close-circle-outlined" />撤标</a-button>
                      </a-popconfirm>
                    </a-space>
                  </template>
                </template>
              </a-table>
              <a-empty v-if="!bidLoading && bidList.length === 0" description="暂无投标记录" class="mt-8" />
            </a-tab-pane>
            <!-- Tab3: æŠ¥ä»·ç®¡ç† -->
            <a-tab-pane key="quote">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:dollar-outlined" />报价管理</a-space>
              </template>
              <template v-if="selectedBidId && selectedBid">
                <div class="flex items-center gap-3 mb-3 px-3 py-2 bg-gray-50 rounded text-xs">
                  <span class="text-gray-400">投标:<b class="text-gray-700">{{ selectedBid.bidNo }}</b></span>
                  <a-divider type="vertical" />
                  <span class="text-gray-400">供应商:<b class="text-gray-700">{{ selectedBid.supplierName }}</b></span>
                  <a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">切换投标</a-button>
                </div>
                <div class="text-right mb-3">
                  <a-button v-if="selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
                    <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                    æ–°å¢žæŠ¥ä»·
                  </a-button>
                </div>
                <a-table :columns="quoteColumns" :data-source="quoteList" :loading="quoteLoading" :pagination="false" row-key="id" size="small">
                  <template #bodyCell="{ column, record }">
                    <template v-if="column.dataIndex === 'quotePrice'">
                      <span class="font-medium text-blue-600">{{ record.quotePrice?.toLocaleString() }}</span>
                    </template>
                    <template v-if="column.dataIndex === 'taxRate'">
                      {{ record.taxRate != null ? `${record.taxRate}%` : '-' }}
                    </template>
                    <template v-if="column.dataIndex === 'deliveryCycle'">
                      {{ record.deliveryCycle != null ? `${record.deliveryCycle} å¤©` : '-' }}
                    </template>
                    <template v-if="column.dataIndex === 'warrantyPeriod'">
                      {{ record.warrantyPeriod != null ? `${record.warrantyPeriod} ä¸ªæœˆ` : '-' }}
                    </template>
                    <template v-if="column.key === 'actions'">
                      <a-button v-if="selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />编辑</a-button>
                    </template>
                  </template>
                </a-table>
                <a-empty v-if="!quoteLoading && quoteList.length === 0" description="暂无报价明细" class="mt-8" />
              </template>
              <a-empty v-else description="请先在「投标管理」中选择一个投标查看报价">
                <a-button type="primary" @click="activeTab = 'bid'">前往投标管理</a-button>
              </a-empty>
            </a-tab-pane>
            <!-- Tab4: å¼€æ ‡ -->
            <a-tab-pane key="bidOpen">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:unlock-outlined" />开标</a-space>
              </template>
              <template v-if="bidOpen">
                <a-result status="success" title="已开标">
                  <template #sub-title>
                    <a-descriptions :column="2" size="small" class="mt-4">
                      <a-descriptions-item label="开标编号">{{ bidOpen.id }}</a-descriptions-item>
                      <a-descriptions-item label="开标时间">{{ bidOpen.openTime }}</a-descriptions-item>
                    </a-descriptions>
                  </template>
                </a-result>
              </template>
              <template v-else>
                <div class="text-center py-8">
                  <a-result status="info" title="尚未开标" sub-title="执行开标后项目进入投标阶段,供应商可正式投标">
                    <template #extra>
                      <a-button type="primary" @click="handleCreateBidOpen">
                        <template #icon><IconifyIcon icon="ant-design:thunderbolt-outlined" /></template>
                        æ‰§è¡Œå¼€æ ‡
                      </a-button>
                    </template>
                  </a-result>
                </div>
              </template>
            </a-tab-pane>
            <!-- Tab5: è¯„æ ‡ -->
            <a-tab-pane key="evaluation">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:trophy-outlined" />评标</a-space>
              </template>
              <div class="flex items-center justify-between mb-3">
                <span class="text-xs text-gray-400">共 <b class="text-gray-600">{{ evalCount }}</b> æ¡ï¼Œå·²æŽ’名 <b class="text-green-500">{{ rankedCount }}</b> æ¡</span>
                <a-space>
                  <a-button size="small" @click="handleCalculateRanking">
                    <template #icon><IconifyIcon icon="ant-design:bar-chart-outlined" /></template>
                    è®¡ç®—排名
                  </a-button>
                  <a-button type="primary" size="small" @click="evalForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0 }; evalFormVisible = true">
                    <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                    æ–°å¢žè¯„æ ‡
                  </a-button>
                </a-space>
              </div>
              <a-table :columns="evalColumns" :data-source="evaluationList" :loading="evalLoading" :pagination="false" row-key="id" size="small">
                <template #bodyCell="{ column, record }">
                  <template v-if="['priceScore', 'techScore', 'deliveryScore', 'serviceScore'].includes(column.dataIndex)">
                    <div class="flex items-center gap-2">
                      <a-progress :percent="record[column.dataIndex] || 0" :show-info="false" size="small"
                        :stroke-color="record[column.dataIndex] >= 80 ? '#52c41a' : record[column.dataIndex] >= 60 ? '#faad14' : '#ff4d4f'"
                        class="flex-1" style="min-width: 50px;" />
                      <span class="text-xs w-8 text-right">{{ record[column.dataIndex] ?? '-' }}</span>
                    </div>
                  </template>
                  <template v-if="column.dataIndex === 'compositeScore'">
                    <span class="font-bold" :class="record.compositeScore >= 80 ? 'text-green-500' : record.compositeScore >= 60 ? 'text-orange-500' : 'text-red-500'">
                      {{ record.compositeScore ?? '-' }}
                    </span>
                  </template>
                  <template v-if="column.dataIndex === 'rank'">
                    <a-tag v-if="record.rank === 1" color="gold"><IconifyIcon icon="ant-design:trophy-outlined" /> ç¬¬1名</a-tag>
                    <a-tag v-else-if="record.rank" color="default">第{{ record.rank }}名</a-tag>
                    <span v-else class="text-gray-300">-</span>
                  </template>
                  <template v-if="column.key === 'actions'">
                    <a-button size="small" type="link" @click="evalForm = { ...record }; evalFormVisible = true"><IconifyIcon icon="ant-design:edit-outlined" />编辑</a-button>
                  </template>
                </template>
              </a-table>
              <a-empty v-if="!evalLoading && evaluationList.length === 0" description="暂无评标记录" class="mt-8" />
            </a-tab-pane>
            <!-- Tab6: å®šæ ‡ -->
            <a-tab-pane key="award">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:check-circle-outlined" />定标</a-space>
              </template>
              <template v-if="award">
                <a-row :gutter="16">
                  <a-col :span="16">
                    <a-descriptions :column="2" bordered size="small">
                      <a-descriptions-item label="定标编号">
                        <span class="font-mono">{{ award.awardNo }}</span>
                      </a-descriptions-item>
                      <a-descriptions-item label="中标供应商">
                        <a-tag color="blue">{{ award.supplierName || award.supplierId }}</a-tag>
                      </a-descriptions-item>
                      <a-descriptions-item label="定标金额">
                        <span class="font-bold text-blue-600">{{ award.awardAmount?.toLocaleString() }}</span> å…ƒ
                      </a-descriptions-item>
                      <a-descriptions-item label="定标时间">{{ award.awardTime || '-' }}</a-descriptions-item>
                      <a-descriptions-item label="状态">
                        <a-tag :color="award.awardStatus === AWARD_STATUS.AWARDED ? 'green' : 'orange'">{{ AWARD_STATUS_MAP[award.awardStatus] }}</a-tag>
                      </a-descriptions-item>
                      <a-descriptions-item label="采购订单">{{ award.purchaseOrderId || '待生成' }}</a-descriptions-item>
                      <a-descriptions-item label="备注" :span="2">{{ award.remark || '-' }}</a-descriptions-item>
                    </a-descriptions>
                  </a-col>
                  <a-col :span="8" class="flex items-center justify-center">
                    <template v-if="award.awardStatus === AWARD_STATUS.ORDER_GENERATED">
                      <a-result status="success" title="订单已生成" class="p-0">
                        <template #sub-title>
                          <span class="text-sm">采购订单 ID: {{ award.purchaseOrderId }}</span>
                        </template>
                      </a-result>
                    </template>
                    <template v-else-if="award.awardStatus === AWARD_STATUS.AWARDED">
                      <div class="text-center">
                        <a-result status="success" title="定标完成" class="p-0 mb-3" />
                        <a-button type="primary" block @click="handleGeneratePurchaseOrder">
                          <template #icon><IconifyIcon icon="ant-design:shopping-cart-outlined" /></template>
                          ç”Ÿæˆé‡‡è´­è®¢å•
                        </a-button>
                      </div>
                    </template>
                    <template v-else>
                      <div class="text-center">
                        <div class="text-gray-500 text-sm mb-3">定标信息已创建,请审批</div>
                        <a-button type="primary" block @click="handleApproveAward" style="background: #52c41a; border-color: #52c41a;">
                          <template #icon><IconifyIcon icon="ant-design:check-outlined" /></template>
                          å®¡æ‰¹é€šè¿‡
                        </a-button>
                      </div>
                    </template>
                  </a-col>
                </a-row>
              </template>
              <template v-else>
                <div class="text-center py-8">
                  <a-result status="info" title="尚未定标" sub-title="基于评标排名选择中标供应商并创建定标">
                    <template #extra>
                      <a-button type="primary" @click="awardForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' }; awardFormVisible = true">
                        <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                        åˆ›å»ºå®šæ ‡
                      </a-button>
                    </template>
                  </a-result>
                </div>
              </template>
            </a-tab-pane>
          </a-tabs>
        </a-card>
      </div>
      <!-- ========== å¼¹çª— ========== -->
      <!-- ç‰©æ–™è¡¨å• -->
      <a-modal v-model:open="materialFormVisible" :title="materialForm.id ? '编辑物料' : '添加物料'" :width="560" @ok="handleSaveMaterial">
        <a-form layout="vertical" class="mt-4">
          <a-form-item label="MDM物料" required>
            <a-select v-model:value="materialForm.productId" show-search placeholder="搜索并选择MDM物料" :filter-option="filterOption" option-label-prop="label" @change="onMaterialSelect">
              <a-select-option v-for="item in mdmItemList" :key="item.id" :value="item.id" :label="`${item.code} ${item.name}`">
                <div class="flex flex-col">
                  <span class="font-medium">{{ item.code }}</span>
                  <span class="text-xs text-gray-400">{{ item.name }}{{ item.specification ? ` Â· ${item.specification}` : '' }}</span>
                </div>
              </a-select-option>
            </a-select>
          </a-form-item>
          <a-row :gutter="12">
            <a-col :span="12">
              <a-form-item label="物料名称" required><a-input v-model:value="materialForm.productName" placeholder="自动带入" /></a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="规格型号"><a-input v-model:value="materialForm.productSpec" placeholder="自动带入" /></a-form-item>
            </a-col>
          </a-row>
          <a-row :gutter="12">
            <a-col :span="8">
              <a-form-item label="单位"><a-input v-model:value="materialForm.unit" placeholder="自动带入" /></a-form-item>
            </a-col>
            <a-col :span="8">
              <a-form-item label="数量" required><a-input-number v-model:value="materialForm.quantity" :min="0" class="!w-full" placeholder="数量" /></a-form-item>
            </a-col>
            <a-col :span="8">
              <a-form-item label="预估单价"><a-input-number v-model:value="materialForm.estimatedPrice" :min="0" class="!w-full" placeholder="单价" /></a-form-item>
            </a-col>
          </a-row>
          <a-form-item label="技术要求">
            <a-textarea v-model:value="materialForm.techRequirement" placeholder="如:符合GB/T标准" :rows="2" />
          </a-form-item>
        </a-form>
      </a-modal>
      <!-- æŠ•标表单 -->
      <a-modal v-model:open="bidFormVisible" title="新增投标" :width="460" @ok="handleCreateBid">
        <a-form layout="vertical" class="mt-4">
          <a-form-item label="投标编号" required><a-input v-model:value="bidForm.bidNo" placeholder="如 BID2024010001" /></a-form-item>
          <a-form-item label="供应商" required>
            <a-select v-model:value="bidForm.supplierId" show-search placeholder="搜索并选择供应商" :filter-option="filterOption">
              <a-select-option v-for="s in supplierList" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item label="投标总金额">
            <a-input-number v-model:value="bidForm.bidTotalAmount" :min="0" class="!w-full" placeholder="投标总金额"><template #addonAfter>元</template></a-input-number>
          </a-form-item>
        </a-form>
      </a-modal>
      <!-- æŠ¥ä»·è¡¨å• -->
      <a-modal v-model:open="quoteFormVisible" :title="quoteForm.id ? '编辑报价' : '新增报价'" :width="560" @ok="handleSaveQuote">
        <a-form layout="vertical" class="mt-4">
          <a-form-item label="招标物料" required>
            <a-select v-model:value="quoteForm.tenderMaterialId" show-search placeholder="搜索并选择招标物料" :filter-option="filterOption">
              <a-select-option v-for="m in availableMaterials" :key="m.id" :value="m.id" :label="m.productName">{{ m.productCode }} - {{ m.productName }}</a-select-option>
            </a-select>
          </a-form-item>
          <a-row :gutter="12">
            <a-col :span="8">
              <a-form-item label="报价价格" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="报价"><template #addonAfter>元</template></a-input-number></a-form-item>
            </a-col>
            <a-col :span="8">
              <a-form-item label="税率(%)"><a-input-number v-model:value="quoteForm.taxRate" :min="0" :max="100" class="!w-full" placeholder="如 13" /></a-form-item>
            </a-col>
            <a-col :span="8">
              <a-form-item label="交期(天)"><a-input-number v-model:value="quoteForm.deliveryCycle" :min="0" class="!w-full" placeholder="天数" /></a-form-item>
            </a-col>
          </a-row>
          <a-row :gutter="12">
            <a-col :span="12">
              <a-form-item label="付款条件"><a-input v-model:value="quoteForm.paymentTerms" placeholder="如:货到30天付款" /></a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="质保期(月)"><a-input-number v-model:value="quoteForm.warrantyPeriod" :min="0" class="!w-full" placeholder="月数" /></a-form-item>
            </a-col>
          </a-row>
          <a-form-item label="备注"><a-textarea v-model:value="quoteForm.remark" placeholder="如含运费" :rows="2" /></a-form-item>
        </a-form>
      </a-modal>
      <!-- è¯„标表单 -->
      <a-modal v-model:open="evalFormVisible" :title="evalForm.id ? '编辑评标' : '新增评标'" :width="600" @ok="handleCreateEvaluation">
        <a-form layout="vertical" class="mt-4">
          <a-row :gutter="12">
            <a-col :span="12">
              <a-form-item label="投标" required>
                <a-select v-model:value="evalForm.bidId" show-search placeholder="选择投标" :filter-option="filterOption"
                  @change="(val: number) => { const b = bidList.find(i => i.id === val); if (b) { evalForm.supplierId = b.supplierId; evalForm.supplierName = b.supplierName; } }">
                  <a-select-option v-for="b in bidList" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="供应商">
                <a-select v-model:value="evalForm.supplierId" show-search placeholder="选择供应商" :filter-option="filterOption">
                  <a-select-option v-for="s in supplierList" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>
          </a-row>
          <a-divider class="!my-2">评分项(百分制)</a-divider>
          <a-row :gutter="12">
            <a-col :span="12"><a-form-item label="价格评分"><a-input-number v-model:value="evalForm.priceScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
            <a-col :span="12"><a-form-item label="技术评分"><a-input-number v-model:value="evalForm.techScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
            <a-col :span="12"><a-form-item label="交付评分"><a-input-number v-model:value="evalForm.deliveryScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
            <a-col :span="12"><a-form-item label="服务评分"><a-input-number v-model:value="evalForm.serviceScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
          </a-row>
          <a-divider class="!my-2" />
          <a-row :gutter="12">
            <a-col :span="12"><a-form-item label="评委"><a-input v-model:value="evalForm.evaluatorName" placeholder="评委姓名" /></a-form-item></a-col>
          </a-row>
          <a-form-item label="评标意见"><a-textarea v-model:value="evalForm.evaluationOpinion" placeholder="供应商的技术方案、交付能力等综合评价" :rows="3" /></a-form-item>
        </a-form>
      </a-modal>
      <!-- å®šæ ‡è¡¨å• -->
      <a-modal v-model:open="awardFormVisible" title="创建定标" :width="500" @ok="handleCreateAward">
        <a-form layout="vertical" class="mt-4">
          <a-form-item label="定标编号" required><a-input v-model:value="awardForm.awardNo" placeholder="如 AWARD2024010001" /></a-form-item>
          <a-row :gutter="12">
            <a-col :span="12">
              <a-form-item label="供应商" required>
                <a-select v-model:value="awardForm.supplierId" show-search placeholder="选择供应商" :filter-option="filterOption">
                  <a-select-option v-for="s in supplierList" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="投标" required>
                <a-select v-model:value="awardForm.bidId" show-search placeholder="选择投标" :filter-option="filterOption">
                  <a-select-option v-for="b in bidList" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>
          </a-row>
          <a-form-item label="定标金额" required>
            <a-input-number v-model:value="awardForm.awardAmount" :min="0" class="!w-full" placeholder="定标金额"><template #addonAfter>元</template></a-input-number>
          </a-form-item>
          <a-form-item label="备注"><a-textarea v-model:value="awardForm.remark" placeholder="如:综合评标第一名" :rows="2" /></a-form-item>
        </a-form>
      </a-modal>
    </template>
  </Page>
</template>
src/views/srm/tender/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmTenderApi } from '#/api/srm/tender';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  closeTender,
  deleteTenderProject,
  generatePurchaseOrder,
  getTenderProjectPage,
  publishTender,
  startBidding,
  startEvaluation,
} from '#/api/srm/tender';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import TenderForm from './modules/form.vue';
import { TENDER_STATUS } from '../enums';
defineOptions({ name: 'SrmTender' });
const router = useRouter();
function handleDetail(row: SrmTenderApi.TenderProjectVO) {
  router.push({ path: '/srm/tender/detail', query: { id: row.id } });
}
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmTenderApi.TenderProjectVO) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: SrmTenderApi.TenderProjectVO) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.tenderName]),
    duration: 0,
  });
  try {
    await deleteTenderProject(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.tenderName]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handlePublish(row: SrmTenderApi.TenderProjectVO) {
  await publishTender(row.id!);
  message.success('发布成功');
  handleRefresh();
}
async function handleStartBidding(row: SrmTenderApi.TenderProjectVO) {
  await startBidding(row.id!);
  message.success('已开始投标');
  handleRefresh();
}
async function handleStartEvaluation(row: SrmTenderApi.TenderProjectVO) {
  await startEvaluation(row.id!);
  message.success('已开始评标');
  handleRefresh();
}
async function handleClose(row: SrmTenderApi.TenderProjectVO) {
  await closeTender(row.id!);
  message.success('已关闭');
  handleRefresh();
}
async function handleGeneratePurchaseOrder(row: SrmTenderApi.TenderProjectVO) {
  if (!row.awardId) return;
  await generatePurchaseOrder(row.awardId);
  message.success('采购订单已生成');
  handleRefresh();
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: TenderForm,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getTenderProjectPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmTenderApi.TenderProjectVO>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="招标项目列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['招标项目']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:tender-project:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '详情',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              onClick: handleDetail.bind(null, row),
            },
            {
              label: '发布',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.DRAFT,
              popConfirm: {
                title: '确认要发布该项目吗?发布后无法修改。',
                confirm: handlePublish.bind(null, row),
              },
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:tender-project:delete'],
              ifShow: row.tenderStatus === TENDER_STATUS.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.tenderName]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: '开始投标',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED,
              popConfirm: {
                title: '确认要开始投标吗?',
                confirm: handleStartBidding.bind(null, row),
              },
            },
            {
              label: '关闭',
              type: 'link',
              danger: true,
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED || row.tenderStatus === TENDER_STATUS.BIDDING,
              popConfirm: {
                title: '确认要关闭该项目吗?',
                confirm: handleClose.bind(null, row),
              },
            },
            {
              label: '开始评标',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.BIDDING,
              popConfirm: {
                title: '确认要开始评标吗?',
                confirm: handleStartEvaluation.bind(null, row),
              },
            },
            {
              label: '生成采购订单',
              type: 'link',
              auth: ['srm:bid-award:update'],
              ifShow: !!row.awardId && !row.purchaseOrderNo,
              popConfirm: {
                title: '确认要生成采购订单吗?',
                confirm: handleGeneratePurchaseOrder.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/tender/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { SrmTenderApi } from '#/api/srm/tender';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createTenderProject,
  getTenderProject,
  updateTenderProject,
} from '#/api/srm/tender';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmTenderApi.TenderProjectVO>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['招标项目'])
    : $t('ui.actionTitle.create', ['招标项目']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmTenderApi.TenderProjectVO;
    try {
      await (formData.value?.id
        ? updateTenderProject(data)
        : createTenderProject(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<SrmTenderApi.TenderProjectVO>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getTenderProject(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/system/tenant/data.ts
@@ -102,7 +102,7 @@
      component: 'DatePicker',
      componentProps: {
        format: 'YYYY-MM-DD',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择过期时间',
      },
      rules: 'required',
src/views/wls/arrivalnotice/data.ts
@@ -125,7 +125,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择到货日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/itemreceipt/data.ts
@@ -101,7 +101,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择入库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -385,7 +385,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择生产日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
@@ -396,7 +396,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择有效期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
src/views/wls/miscissue/data.ts
@@ -101,7 +101,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择出库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/miscreceipt/data.ts
@@ -116,7 +116,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择入库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/outsourceissue/data.ts
@@ -100,7 +100,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择发料日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
src/views/wls/outsourcereceipt/data.ts
@@ -122,7 +122,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择入库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -363,7 +363,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择生产日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
@@ -374,7 +374,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择有效期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
src/views/wls/packages/data.ts
@@ -78,7 +78,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择装箱日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
@@ -455,7 +455,7 @@
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择有效期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
src/views/wls/productissue/data.ts
@@ -101,7 +101,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择需求时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/wls/productreceipt/data.ts
@@ -98,7 +98,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择入库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/productsales/data.ts
@@ -182,7 +182,7 @@
        disabled: isHeaderReadonly(formType),
        format: 'YYYY-MM-DD',
        placeholder: '请选择出库日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/returnissue/data.ts
@@ -128,7 +128,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择退料日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
src/views/wls/returnsales/data.ts
@@ -129,7 +129,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择退货日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/returnvendor/data.ts
@@ -137,7 +137,7 @@
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: '请选择退货日期',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
src/views/wls/salesnotice/data.ts
@@ -114,7 +114,7 @@
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择发货日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
src/views/wls/stocktaking/plan/data.ts
@@ -91,7 +91,7 @@
      componentProps: {
        placeholder: '请选择开始时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['type'],
@@ -105,7 +105,7 @@
      componentProps: {
        placeholder: '请选择结束时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['type'],
src/views/wls/stocktaking/task/data.ts
@@ -134,7 +134,7 @@
      componentProps: {
        placeholder: '请选择开始时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['type'],
@@ -148,7 +148,7 @@
      componentProps: {
        placeholder: '请选择结束时间',
        showTime: true,
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      dependencies: {
        triggerFields: ['type'],
src/views/wms/order/check/data.ts
@@ -396,7 +396,7 @@
        class: 'w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择单据日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      fieldName: 'orderTime',
      label: '单据日期',
src/views/wms/order/movement/data.ts
@@ -319,7 +319,7 @@
        class: 'w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择单据日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      fieldName: 'orderTime',
      label: '单据日期',
src/views/wms/order/receipt/data.ts
@@ -359,7 +359,7 @@
        class: 'w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择单据日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      fieldName: 'orderTime',
      label: '单据日期',
src/views/wms/order/shipment/data.ts
@@ -369,7 +369,7 @@
        class: 'w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择单据日期',
        valueFormat: 'x',
        valueFormat: 'YYYY-MM-DD',
      },
      fieldName: 'orderTime',
      label: '单据日期',