gaoluyang
5 天以前 55c89a667220d8dd5fb99e2d8219d3aee15cb594
refactor(srm): 重构供应商采购界面,新增询价比价与保证金页面

- 移除供应商准入/资质/绩效/招投标/统计页面及路由
- 新增询价比价、投标保证金、供应商对账单页面
- 供应商档案表单支持银行账户
- 采购订单/入库/请购/退货表单适配

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加11个文件
已删除24个文件
已修改14个文件
6763 ■■■■■ 文件已修改
src/api/erp/purchase/order/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/statement/index.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/bidBond/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/inquiry/index.ts 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/statistics/index.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplier/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierApply/index.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierCertificate/index.ts 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierEvaluation/index.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender-config/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender/index.ts 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/srm.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/data.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/modules/item-form.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/item-form.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/detail/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/return/data.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/return/modules/item-form.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statement/index.vue 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/data.ts 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/modules/form.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/enums.ts 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/inquiry/data.ts 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/inquiry/detail.vue 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/inquiry/index.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/inquiry/modules/form.vue 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/statistics/index.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/data.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplier/modules/form.vue 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/data.ts 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/detail/index.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/index.vue 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/modules/form.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/modules/process-select-modal.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/data.ts 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/index.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierCertificate/modules/form.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | 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/config/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/data.ts 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/detail.vue 1084 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/index.vue 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/modules/form.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts
@@ -88,6 +88,16 @@
  });
}
/** æäº¤é‡‡è´­è®¢å•审批(草稿/未审核 -> å®¡æ‰¹ä¸­ï¼‰ */
export function submitPurchaseOrder(
  id: number,
  processDefinitionKey = 'purchase_order_approve',
) {
  return requestClient.post('/erp/purchase-order/submit', null, {
    params: { id, processDefinitionKey },
  });
}
/** åˆ é™¤é‡‡è´­è®¢å• */
export function deletePurchaseOrder(ids: number[]) {
  return requestClient.delete('/erp/purchase-order/delete', {
src/api/erp/purchase/statement/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
import { requestClient } from '#/api/request';
export namespace ErpSupplierStatementApi {
  export interface Summary {
    totalPurchaseAmount?: number;
    totalInvoiceAmount?: number;
    totalPaymentAmount?: number;
    unpaidAmount?: number;
  }
  export interface OrderRow {
    id?: number;
    no?: string;
    orderTime?: string;
    orderAmount?: number;
    invoicedAmount?: number;
    uninvoicedAmount?: number;
    status?: number;
    inStatus?: number;
  }
  export interface InvoiceRow {
    id?: number;
    no?: string;
    invoiceNo?: string;
    invoiceTime?: string;
    invoiceAmount?: number;
    paidAmount?: number;
    remainingPayable?: number;
    paymentStatus?: number;
    purchaseOrderId?: number;
    purchaseOrderNo?: string;
  }
  export interface PaymentRow {
    id?: number;
    no?: string;
    paymentTime?: string;
    paymentAmount?: number;
    status?: number;
    invoiceId?: number;
    invoiceNo?: string;
  }
  export interface Statement {
    supplierId?: number;
    supplierName?: string;
    summary?: Summary;
    orders?: OrderRow[];
    invoices?: InvoiceRow[];
    payments?: PaymentRow[];
  }
}
/** æŸ¥è¯¢ä¾›åº”商往来对账 */
export function getSupplierStatement(params: {
  supplierId: number;
  beginTime?: string;
  endTime?: string;
  includeUnapprovedOrder?: boolean;
}) {
  return requestClient.get<ErpSupplierStatementApi.Statement>(
    '/erp/supplier-statement/get',
    { params },
  );
}
src/api/srm/bidBond/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmBidBondApi {
  /** æŠ•标保证金 */
  export interface BidBond {
    id?: number;
    no?: string;
    supplierId?: number;
    supplierName?: string;
    projectName?: string;
    inquiryId?: number;
    inquiryNo?: string;
    purchaseOrderId?: number;
    purchaseOrderNo?: string;
    bondAmount?: number;
    payDate?: string;
    won?: boolean;
    refundStatus?: number;
    refundAmount?: number;
    refundDate?: string;
    remark?: string;
    createTime?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢æŠ•标保证金 */
export function getBidBondPage(params: PageParam) {
  return requestClient.get<PageResult<SrmBidBondApi.BidBond>>(
    '/srm/bid-bond/page',
    { params },
  );
}
/** æŸ¥è¯¢æŠ•标保证金详情 */
export function getBidBond(id: number) {
  return requestClient.get<SrmBidBondApi.BidBond>(
    `/srm/bid-bond/get?id=${id}`,
  );
}
/** åˆ›å»ºæŠ•标保证金 */
export function createBidBond(data: SrmBidBondApi.BidBond) {
  return requestClient.post<number>('/srm/bid-bond/create', data);
}
/** æ›´æ–°æŠ•标保证金 */
export function updateBidBond(data: SrmBidBondApi.BidBond) {
  return requestClient.put('/srm/bid-bond/update', data);
}
/** åˆ é™¤æŠ•标保证金 */
export function deleteBidBond(id: number) {
  return requestClient.delete('/srm/bid-bond/delete', { params: { id } });
}
src/api/srm/inquiry/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace SrmInquiryApi {
  /** è¯¢ä»·äº§å“è¡Œ */
  export interface Item {
    id?: number;
    inquiryId?: number;
    productId?: number;
    productUnitId?: number;
    productCode?: string;
    productName?: string;
    productSpec?: string;
    unitName?: string;
    quantity?: number;
    estimatedPrice?: number;
    selectedSupplierId?: number;
    selectedQuoteId?: number;
    selectedPrice?: number;
    /** æ–°å¢ž/编辑询价单时随产品行一并提交的供应商报价 */
    quotes?: ItemQuote[];
    remark?: string;
    sort?: number;
  }
  /** äº§å“è¡Œå†…的供应商报价 */
  export interface ItemQuote {
    supplierId: number;
    price?: number;
    taxRate?: number;
    deliveryCycle?: number;
    paymentTerms?: string;
    remark?: string;
  }
  /** å‚与供应商 */
  export interface Supplier {
    id?: number;
    supplierId: number;
    supplierName?: string;
    supplierCode?: string;
    quoteStatus?: number;
    quoteTime?: string;
    remark?: string;
  }
  /** äº§å“æŠ¥ä»· */
  export interface Quote {
    id?: number;
    inquiryId?: number;
    inquiryItemId?: number;
    supplierId?: number;
    price?: number;
    taxRate?: number;
    deliveryCycle?: number;
    paymentTerms?: string;
    remark?: string;
    isSelected?: boolean;
  }
  /** è¯¢ä»·è½¬è®¢å•记录 */
  export interface Order {
    id?: number;
    supplierId?: number;
    supplierName?: string;
    purchaseOrderId?: number;
    purchaseOrderNo?: string;
  }
  /** è¯¢ä»·å• */
  export interface Inquiry {
    id?: number;
    no?: string;
    title: string;
    status?: number;
    inquiryDate?: string;
    deadline?: string;
    purchaserUserId?: number;
    remark?: string;
    createTime?: string;
    items?: Item[];
    suppliers?: Supplier[];
    quotes?: Quote[];
    orders?: Order[];
  }
  /** ä¿å­˜æŠ¥ä»· */
  export interface QuoteSaveItem {
    inquiryItemId: number;
    supplierId: number;
    price?: number;
    taxRate?: number;
    deliveryCycle?: number;
    paymentTerms?: string;
    remark?: string;
  }
  /** ä¸­æ ‡é€‰æ‹© */
  export interface Selection {
    inquiryItemId: number;
    quoteId: number;
  }
  /** ç¡®è®¤è½¬è®¢å•结果 */
  export interface ConfirmResult {
    inquiryId: number;
    orders: {
      purchaseOrderId: number;
      purchaseOrderNo: string;
      supplierId: number;
      supplierName: string;
    }[];
  }
}
/** åˆ†é¡µæŸ¥è¯¢è¯¢ä»·å• */
export function getInquiryPage(params: PageParam) {
  return requestClient.get<PageResult<SrmInquiryApi.Inquiry>>(
    '/srm/inquiry/page',
    { params },
  );
}
/** æŸ¥è¯¢è¯¢ä»·å•详情 */
export function getInquiry(id: number) {
  return requestClient.get<SrmInquiryApi.Inquiry>(
    `/srm/inquiry/get?id=${id}`,
  );
}
/** æŸ¥è¯¢è¯¢ä»·æ¯”价视图 */
export function getInquiryCompare(id: number) {
  return requestClient.get<SrmInquiryApi.Inquiry>(
    `/srm/inquiry/compare?id=${id}`,
  );
}
/** æŸ¥è¯¢è¯¢ä»·å•精简列表 */
export function getInquirySimpleList() {
  return requestClient.get<SrmInquiryApi.Inquiry[]>(
    '/srm/inquiry/simple-list',
  );
}
/** åˆ›å»ºè¯¢ä»·å• */
export function createInquiry(data: SrmInquiryApi.Inquiry) {
  return requestClient.post<number>('/srm/inquiry/create', data);
}
/** æ›´æ–°è¯¢ä»·å• */
export function updateInquiry(data: SrmInquiryApi.Inquiry) {
  return requestClient.put('/srm/inquiry/update', data);
}
/** åˆ é™¤è¯¢ä»·å• */
export function deleteInquiry(id: number) {
  return requestClient.delete('/srm/inquiry/delete', { params: { id } });
}
/** å‘布询价单 */
export function publishInquiry(id: number) {
  return requestClient.put('/srm/inquiry/publish', null, { params: { id } });
}
/** å–消询价单 */
export function cancelInquiry(id: number) {
  return requestClient.put('/srm/inquiry/cancel', null, { params: { id } });
}
/** æ‰¹é‡ä¿å­˜æŠ¥ä»· */
export function saveQuotes(
  inquiryId: number,
  quotes: SrmInquiryApi.QuoteSaveItem[],
) {
  return requestClient.post('/srm/inquiry/save-quotes', {
    inquiryId,
    quotes,
  });
}
/** ç¡®è®¤ä¸­æ ‡å¹¶ç”Ÿæˆé‡‡è´­è®¢å•草稿 */
export function confirmInquiry(
  id: number,
  selections: SrmInquiryApi.Selection[],
) {
  return requestClient.post<SrmInquiryApi.ConfirmResult>(
    '/srm/inquiry/confirm',
    { id, selections },
  );
}
src/api/srm/statistics/index.ts
ÎļþÒÑɾ³ý
src/api/srm/supplier/index.ts
@@ -3,6 +3,29 @@
import { requestClient } from '#/api/request';
export namespace SrmSupplierApi {
  /** ä¾›åº”商联系人 */
  export interface SupplierContact {
    id?: number;
    name: string;
    position?: string;
    mobile?: string;
    telephone?: string;
    email?: string;
    wechat?: string;
    isDefault?: boolean;
  }
  /** ä¾›åº”商银行账户 */
  export interface SupplierBank {
    id?: number;
    bankName: string;
    bankAccount: string;
    bankNo?: string;
    bankAddress?: string;
    isDefault?: boolean;
    remark?: string;
  }
  export interface SupplierVO {
    id?: number;
    code: string;
@@ -18,6 +41,8 @@
    taxNo?: string;
    remark?: string;
    createTime?: string;
    contacts?: SupplierContact[];
    banks?: SupplierBank[];
  }
}
src/api/srm/supplierApply/index.ts
ÎļþÒÑɾ³ý
src/api/srm/supplierCertificate/index.ts
ÎļþÒÑɾ³ý
src/api/srm/supplierEvaluation/index.ts
ÎļþÒÑɾ³ý
src/api/srm/tender-config/index.ts
ÎļþÒÑɾ³ý
src/api/srm/tender/index.ts
ÎļþÒÑɾ³ý
src/router/routes/modules/srm.ts
ÎļþÒÑɾ³ý
src/views/erp/purchase/in/data.ts
@@ -258,17 +258,10 @@
    },
    {
      field: 'productPrice',
      title: '产品单价',
      title: '含税单价',
      fixed: 'right',
      minWidth: 120,
      slots: { default: 'productPrice' },
    },
    {
      field: 'totalProductPrice',
      fixed: 'right',
      title: '产品金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      fixed: 'right',
@@ -280,14 +273,14 @@
    {
      fixed: 'right',
      field: 'taxPrice',
      title: '税额',
      title: '税额(价内含)',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      field: 'totalPrice',
      fixed: 'right',
      title: '合计金额',
      title: '价税合计',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
@@ -595,13 +588,13 @@
    },
    {
      field: 'totalProductPrice',
      title: '金额合计',
      title: '优惠前金额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'totalPrice',
      title: '含税金额',
      title: '价税合计',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
src/views/erp/purchase/in/modules/item-form.vue
@@ -149,7 +149,7 @@
/** å¤„理行数据变更 */
function handleRowChange(row: any) {
  // TODO èŠ‹è‰¿
  calcRow(row);
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index === -1) {
    tableData.value.push(row);
@@ -159,14 +159,30 @@
  emit('update:items', [...tableData.value]);
}
/**
 * è®¡ç®—行金额
 * å•价为含税单价:金额(价税合计)= å•ä»· * æ•°é‡ï¼Œç¨Žé¢ä»Žå«ç¨Žé‡‘额中反算,不再额外加税
 */
function calcRow(row: ErpPurchaseInApi.PurchaseInItem) {
  if (!row.productPrice || !row.count) {
    return;
  }
  const totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
  const taxPercent = Number(row.taxPercent) || 0;
  row.totalProductPrice = totalProductPrice;
  row.taxPrice =
    taxPercent > 0
      ? (erpPriceMultiply(
          totalProductPrice,
          taxPercent / (100 + taxPercent),
        ) ?? 0)
      : 0;
  row.totalPrice = totalProductPrice;
}
/** åˆå§‹åŒ–行数据 */
function initRow(row: ErpPurchaseInApi.PurchaseInItem) {
  if (row.productPrice && row.count) {
    row.totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
    row.taxPrice =
      erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0;
    row.totalPrice = row.totalProductPrice + row.taxPrice;
  }
  calcRow(row);
}
/** è¡¨å•校验 */
@@ -269,11 +285,10 @@
          <div class="flex space-x-4">
            <span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>
              é‡‘额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
              ç¨Žé¢ï¼ˆä»·å†…含):{{ erpPriceInputFormatter(summaries.taxPrice) }}
            </span>
            <span>税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}</span>
            <span>
              ç¨Žé¢åˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
              ä»·ç¨Žåˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
            </span>
          </div>
        </div>
src/views/erp/purchase/order/data.ts
@@ -210,17 +210,10 @@
    },
    {
      field: 'productPrice',
      title: '产品单价',
      title: '含税单价',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'productPrice' },
    },
    {
      field: 'totalProductPrice',
      title: '金额',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
    },
    {
      field: 'taxPercent',
@@ -231,14 +224,14 @@
    },
    {
      field: 'taxPrice',
      title: '税额',
      title: '税额(价内含)',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
    },
    {
      field: 'totalPrice',
      title: '税额合计',
      title: '价税合计',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
@@ -316,7 +309,7 @@
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number').filter(
          (opt) => opt.label !== '草稿' && opt.label !== '审批不通过',
          (opt) => opt.label !== '审批不通过',
        ),
        placeholder: '请选择状态',
        allowClear: true,
@@ -404,13 +397,13 @@
    },
    {
      field: 'totalProductPrice',
      title: '金额合计',
      title: '优惠前金额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'totalPrice',
      title: '含税金额',
      title: '价税合计',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
src/views/erp/purchase/order/index.vue
@@ -16,6 +16,7 @@
  deletePurchaseOrder,
  exportPurchaseOrder,
  getPurchaseOrderPage,
  submitPurchaseOrder,
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
@@ -73,6 +74,21 @@
  try {
    await deletePurchaseOrder(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤è‰ç¨¿è®¢å•审批 */
async function handleSubmit(row: ErpPurchaseOrderApi.PurchaseOrder) {
  const hideLoading = message.loading({
    content: '提交审批中...',
    duration: 0,
  });
  try {
    await submitPurchaseOrder(row.id!);
    message.success(`采购订单 ${row.no} å·²æäº¤å®¡æ‰¹`);
    handleRefresh();
  } finally {
    hideLoading();
@@ -217,6 +233,17 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交审批',
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['erp:purchase-order:update-status'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: `提交采购订单${row.no}审批吗?`,
                confirm: handleSubmit.bind(null, row),
              },
            },
            {
              label: '确认收货',
              type: 'link',
              icon: ACTION_ICON.VIEW,
src/views/erp/purchase/order/modules/item-form.vue
@@ -164,6 +164,7 @@
/** å¤„理行数据变更 */
function handleRowChange(row: any) {
  calcRow(row);
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index === -1) {
    tableData.value.push(row);
@@ -173,14 +174,30 @@
  emit('update:items', [...tableData.value]);
}
/**
 * è®¡ç®—行金额
 * å•价为含税单价:金额(价税合计)= å•ä»· * æ•°é‡ï¼Œç¨Žé¢ä»Žå«ç¨Žé‡‘额中反算,不再额外加税
 */
function calcRow(row: ErpPurchaseOrderApi.PurchaseOrderItem) {
  if (!row.productPrice || !row.count) {
    return;
  }
  const totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
  const taxPercent = Number(row.taxPercent) || 0;
  row.totalProductPrice = totalProductPrice;
  row.taxPrice =
    taxPercent > 0
      ? (erpPriceMultiply(
          totalProductPrice,
          taxPercent / (100 + taxPercent),
        ) ?? 0)
      : 0;
  row.totalPrice = totalProductPrice;
}
/** åˆå§‹åŒ–行数据 */
function initRow(row: ErpPurchaseOrderApi.PurchaseOrderItem) {
  if (row.productPrice && row.count) {
    row.totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
    row.taxPrice =
      erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0;
    row.totalPrice = row.totalProductPrice + row.taxPrice;
  }
  calcRow(row);
}
/** è¡¨å•校验 */
@@ -195,7 +212,7 @@
        throw new Error(`第 ${i + 1} è¡Œï¼šäº§å“æ•°é‡ä¸èƒ½ä¸ºç©º`);
      }
      if (!item.productPrice || item.productPrice <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šäº§å“å•价不能为空`);
        throw new Error(`第 ${i + 1} è¡Œï¼šå«ç¨Žå•价不能为空`);
      }
    }
  }
@@ -294,11 +311,10 @@
          <div class="flex space-x-4">
            <span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>
              é‡‘额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
              ç¨Žé¢ï¼ˆä»·å†…含):{{ erpPriceInputFormatter(summaries.taxPrice) }}
            </span>
            <span>税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}</span>
            <span>
              ç¨Žé¢åˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
              ä»·ç¨Žåˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
            </span>
          </div>
        </div>
src/views/erp/purchase/request/data.ts
@@ -167,14 +167,14 @@
    },
    {
      field: 'productPrice',
      title: '单价(参考价)',
      title: '含税单价',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'productPrice' },
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      title: '价税合计',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
@@ -339,7 +339,7 @@
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      title: '价税合计',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
src/views/erp/purchase/request/detail/index.vue
@@ -42,13 +42,13 @@
    },
    {
      field: 'productPrice',
      title: '单价(参考价)',
      title: '含税单价',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      title: '价税合计',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
@@ -119,7 +119,7 @@
      render: (val) => erpCountInputFormatter(val) || '-',
    },
    {
      label: '合计金额',
      label: '价税合计',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
src/views/erp/purchase/return/data.ts
@@ -258,17 +258,10 @@
    },
    {
      field: 'productPrice',
      title: '产品单价',
      title: '含税单价',
      fixed: 'right',
      minWidth: 120,
      slots: { default: 'productPrice' },
    },
    {
      field: 'totalProductPrice',
      fixed: 'right',
      title: '产品金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      fixed: 'right',
@@ -280,14 +273,14 @@
    {
      fixed: 'right',
      field: 'taxPrice',
      title: '税额',
      title: '税额(价内含)',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      field: 'totalPrice',
      fixed: 'right',
      title: '合计金额',
      title: '价税合计',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
@@ -582,13 +575,13 @@
    },
    {
      field: 'totalProductPrice',
      title: '金额合计',
      title: '优惠前金额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'totalPrice',
      title: '含税金额',
      title: '价税合计',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
src/views/erp/purchase/return/modules/item-form.vue
@@ -152,7 +152,7 @@
/** å¤„理行数据变更 */
function handleRowChange(row: any) {
  // TODO èŠ‹è‰¿
  calcRow(row);
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index === -1) {
    tableData.value.push(row);
@@ -162,14 +162,30 @@
  emit('update:items', [...tableData.value]);
}
/**
 * è®¡ç®—行金额
 * å•价为含税单价:金额(价税合计)= å•ä»· * æ•°é‡ï¼Œç¨Žé¢ä»Žå«ç¨Žé‡‘额中反算,不再额外加税
 */
function calcRow(row: ErpPurchaseReturnApi.PurchaseReturnItem) {
  if (!row.productPrice || !row.count) {
    return;
  }
  const totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
  const taxPercent = Number(row.taxPercent) || 0;
  row.totalProductPrice = totalProductPrice;
  row.taxPrice =
    taxPercent > 0
      ? (erpPriceMultiply(
          totalProductPrice,
          taxPercent / (100 + taxPercent),
        ) ?? 0)
      : 0;
  row.totalPrice = totalProductPrice;
}
/** åˆå§‹åŒ–行数据 */
function initRow(row: ErpPurchaseReturnApi.PurchaseReturnItem) {
  if (row.productPrice && row.count) {
    row.totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
    row.taxPrice =
      erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0;
    row.totalPrice = row.totalProductPrice + row.taxPrice;
  }
  calcRow(row);
}
/** è¡¨å•校验 */
@@ -272,11 +288,10 @@
          <div class="flex space-x-4">
            <span>数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>
              é‡‘额:{{ erpPriceInputFormatter(summaries.totalProductPrice) }}
              ç¨Žé¢ï¼ˆä»·å†…含):{{ erpPriceInputFormatter(summaries.taxPrice) }}
            </span>
            <span>税额:{{ erpPriceInputFormatter(summaries.taxPrice) }}</span>
            <span>
              ç¨Žé¢åˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
              ä»·ç¨Žåˆè®¡ï¼š{{ erpPriceInputFormatter(summaries.totalPrice) }}
            </span>
          </div>
        </div>
src/views/erp/purchase/statement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,270 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { ErpSupplierStatementApi } from '#/api/erp/purchase/statement';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { message } from 'ant-design-vue';
import { getSupplierStatement } from '#/api/erp/purchase/statement';
import { getSupplierSimpleList } from '#/api/srm/supplier';
defineOptions({ name: 'ErpSupplierStatement' });
const supplierId = ref<number>();
const dateRange = ref<[Dayjs, Dayjs]>();
const supplierOptions = ref<SrmSupplierApi.SupplierVO[]>([]);
const statement = ref<ErpSupplierStatementApi.Statement>();
const loading = ref(false);
const orderColumns = [
  { title: '采购订单号', dataIndex: 'no', key: 'no', width: 170 },
  { title: '订单时间', dataIndex: 'orderTime', key: 'orderTime', width: 150 },
  { title: '订单金额', dataIndex: 'orderAmount', key: 'orderAmount', width: 120 },
  {
    title: '已开票金额',
    dataIndex: 'invoicedAmount',
    key: 'invoicedAmount',
    width: 120,
  },
  {
    title: '未开票金额',
    dataIndex: 'uninvoicedAmount',
    key: 'uninvoicedAmount',
    width: 120,
  },
  { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
];
const invoiceColumns = [
  { title: '来票单号', dataIndex: 'no', key: 'no', width: 160 },
  { title: '发票号', dataIndex: 'invoiceNo', key: 'invoiceNo', width: 140 },
  { title: '来票时间', dataIndex: 'invoiceTime', key: 'invoiceTime', width: 150 },
  {
    title: '开票金额',
    dataIndex: 'invoiceAmount',
    key: 'invoiceAmount',
    width: 120,
  },
  { title: '已付金额', dataIndex: 'paidAmount', key: 'paidAmount', width: 120 },
  {
    title: '剩余应付',
    dataIndex: 'remainingPayable',
    key: 'remainingPayable',
    width: 120,
  },
  { title: '采购订单', dataIndex: 'purchaseOrderNo', key: 'purchaseOrderNo', width: 160 },
];
const paymentColumns = [
  { title: '付款单号', dataIndex: 'no', key: 'no', width: 160 },
  { title: '付款时间', dataIndex: 'paymentTime', key: 'paymentTime', width: 150 },
  {
    title: '付款金额',
    dataIndex: 'paymentAmount',
    key: 'paymentAmount',
    width: 120,
  },
  { title: '关联发票', dataIndex: 'invoiceNo', key: 'invoiceNo', width: 150 },
  { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
];
function formatMoney(value?: number) {
  return value === undefined || value === null ? '-' : Number(value).toFixed(2);
}
function getStatusLabel(status?: number) {
  if (status === undefined || status === null) {
    return '-';
  }
  return getDictLabel(DICT_TYPE.ERP_AUDIT_STATUS, status) || '-';
}
function getPaymentStatusLabel(status?: number) {
  return (
    ['未付款', '部分付款', '全部付款'][status ?? 0] ?? '-'
  );
}
async function handleQuery() {
  if (!supplierId.value) {
    message.warning('请选择供应商');
    return;
  }
  loading.value = true;
  try {
    const params: {
      supplierId: number;
      beginTime?: string;
      endTime?: string;
    } = { supplierId: supplierId.value };
    if (dateRange.value?.length === 2) {
      params.beginTime = dateRange.value[0]!.startOf('day').format('YYYY-MM-DD HH:mm:ss');
      params.endTime = dateRange.value[1]!.endOf('day').format('YYYY-MM-DD HH:mm:ss');
    }
    statement.value = await getSupplierStatement(params);
  } finally {
    loading.value = false;
  }
}
onMounted(async () => {
  supplierOptions.value = (await getSupplierSimpleList()) ?? [];
});
</script>
<template>
  <Page auto-content-height>
    <div class="p-4" :class="{ 'pointer-events-none opacity-60': loading }">
      <div class="mb-4 flex flex-wrap items-center gap-3">
        <span class="text-sm text-gray-500">供应商</span>
        <a-select
          v-model:value="supplierId"
          show-search
          option-filter-prop="label"
          placeholder="请选择供应商"
          style="width: 260px"
          :options="
            supplierOptions.map((item) => ({
              label: item.name,
              value: item.id,
            }))
          "
        />
        <a-range-picker v-model:value="dateRange" />
        <a-button type="primary" :loading="loading" @click="handleQuery">
          æŸ¥è¯¢
        </a-button>
        <span v-if="statement?.supplierName" class="text-sm text-gray-600">
          å½“前供应商:{{ statement.supplierName }}
        </span>
      </div>
      <a-row :gutter="16" class="mb-4">
        <a-col :span="6">
          <a-card size="small">
            <div class="text-gray-500">累计采购额</div>
            <div class="text-2xl font-semibold">
              {{ formatMoney(statement?.summary?.totalPurchaseAmount) }}
            </div>
          </a-card>
        </a-col>
        <a-col :span="6">
          <a-card size="small">
            <div class="text-gray-500">累计开票额</div>
            <div class="text-2xl font-semibold text-blue-600">
              {{ formatMoney(statement?.summary?.totalInvoiceAmount) }}
            </div>
          </a-card>
        </a-col>
        <a-col :span="6">
          <a-card size="small">
            <div class="text-gray-500">累计付款额</div>
            <div class="text-2xl font-semibold text-green-600">
              {{ formatMoney(statement?.summary?.totalPaymentAmount) }}
            </div>
          </a-card>
        </a-col>
        <a-col :span="6">
          <a-card size="small">
            <div class="text-gray-500">未付余额(开票 - ä»˜æ¬¾ï¼‰</div>
            <div class="text-2xl font-semibold text-red-600">
              {{ formatMoney(statement?.summary?.unpaidAmount) }}
            </div>
          </a-card>
        </a-col>
      </a-row>
      <a-row :gutter="16">
        <a-col :span="12">
          <a-card title="采购订单流水(含开票金额)" size="small">
            <a-table
              :columns="orderColumns"
              :data-source="statement?.orders ?? []"
              :pagination="false"
              size="small"
              bordered
              :scroll="{ x: 800 }"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'orderAmount'">
                  {{ formatMoney(record.orderAmount) }}
                </template>
                <template v-else-if="column.key === 'invoicedAmount'">
                  <span class="text-blue-600">
                    {{ formatMoney(record.invoicedAmount) }}
                  </span>
                </template>
                <template v-else-if="column.key === 'uninvoicedAmount'">
                  <span class="text-red-600">
                    {{ formatMoney(record.uninvoicedAmount) }}
                  </span>
                </template>
                <template v-else-if="column.key === 'status'">
                  {{ getStatusLabel(record.status) }}
                </template>
              </template>
            </a-table>
          </a-card>
        </a-col>
        <a-col :span="12">
          <a-card title="采购来票流水(开票金额 / å·²ä»˜ / å‰©ä½™åº”付)" size="small">
            <a-table
              :columns="invoiceColumns"
              :data-source="statement?.invoices ?? []"
              :pagination="false"
              size="small"
              bordered
              :scroll="{ x: 900 }"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'invoiceAmount'">
                  <span class="text-blue-600">
                    {{ formatMoney(record.invoiceAmount) }}
                  </span>
                </template>
                <template v-else-if="column.key === 'paidAmount'">
                  {{ formatMoney(record.paidAmount) }}
                </template>
                <template v-else-if="column.key === 'remainingPayable'">
                  <span class="text-red-600">
                    {{ formatMoney(record.remainingPayable) }}
                  </span>
                </template>
                <template v-else-if="column.key === 'paymentStatus'">
                  {{ getPaymentStatusLabel(record.paymentStatus) }}
                </template>
              </template>
            </a-table>
          </a-card>
          <a-card title="付款流水" size="small" class="mt-4">
            <a-table
              :columns="paymentColumns"
              :data-source="statement?.payments ?? []"
              :pagination="false"
              size="small"
              bordered
              :scroll="{ x: 700 }"
            >
              <template #bodyCell="{ column, record }">
                <template v-if="column.key === 'paymentAmount'">
                  <span class="text-green-600">
                    {{ formatMoney(record.paymentAmount) }}
                  </span>
                </template>
                <template v-else-if="column.key === 'status'">
                  {{ getStatusLabel(record.status) }}
                </template>
              </template>
            </a-table>
          </a-card>
        </a-col>
      </a-row>
    </div>
  </Page>
</template>
src/views/srm/bidBond/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,255 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getPurchaseOrderPage } from '#/api/erp/purchase/order';
import { getInquirySimpleList } from '#/api/srm/inquiry';
import { getSupplierSimpleList } from '#/api/srm/supplier';
export const REFUND_STATUS_OPTIONS = [
  { label: '未退回', value: 0, color: 'warning' },
  { label: '部分退回', value: 1, color: 'processing' },
  { label: '已退回', value: 2, color: 'success' },
  { label: '无需退回', value: 3, color: 'default' },
];
export function getRefundStatusLabel(status?: number) {
  return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '-';
}
export function getRefundStatusColor(status?: number) {
  return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.color ?? 'default';
}
/** é‡‡è´­è®¢å•下拉数据 */
async function getPurchaseOrderOptions() {
  const result = await getPurchaseOrderPage({ pageNo: 1, pageSize: 200 });
  return result.list ?? [];
}
/** æ–°å¢ž/修改投标保证金的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      rules: 'required',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'projectName',
      label: '项目名称',
      component: 'Input',
      componentProps: { placeholder: '选择询价单后自动带出,也可手工填写' },
    },
    {
      fieldName: 'inquiryId',
      label: '关联询价单',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择询价单',
        allowClear: true,
        showSearch: true,
        api: getInquirySimpleList,
        labelField: 'title',
        valueField: 'id',
      },
    },
    {
      fieldName: 'purchaseOrderId',
      label: '关联采购订单',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择采购订单',
        allowClear: true,
        showSearch: true,
        api: getPurchaseOrderOptions,
        labelField: 'no',
        valueField: 'id',
      },
    },
    {
      fieldName: 'bondAmount',
      label: '保证金金额',
      component: 'InputNumber',
      rules: 'required',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入保证金金额',
        min: 0,
        precision: 2,
      },
    },
    {
      fieldName: 'payDate',
      label: '缴纳日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择缴纳日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'won',
      label: '是否中标',
      component: 'Switch',
      componentProps: {
        checkedChildren: '中标',
        unCheckedChildren: '未中标',
      },
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'refundStatus',
      label: '退回状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择退回状态',
        options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
    {
      fieldName: 'refundAmount',
      label: '退回金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入退回金额',
        min: 0,
        precision: 2,
      },
    },
    {
      fieldName: 'refundDate',
      label: '退回日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择退回日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 2 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '保证金单号',
      component: 'Input',
      componentProps: { placeholder: '请输入保证金单号', allowClear: true },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'projectName',
      label: '项目名称',
      component: 'Input',
      componentProps: { placeholder: '请输入项目名称', allowClear: true },
    },
    {
      fieldName: 'won',
      label: '是否中标',
      component: 'Select',
      componentProps: {
        placeholder: '请选择',
        allowClear: true,
        options: [
          { label: '中标', value: true },
          { label: '未中标', value: false },
        ],
      },
    },
    {
      fieldName: 'refundStatus',
      label: '退回状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择退回状态',
        allowClear: true,
        options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'no', title: '保证金单号', minWidth: 150 },
    { field: 'supplierName', title: '供应商', minWidth: 150 },
    { field: 'projectName', title: '项目名称', minWidth: 180 },
    { field: 'inquiryNo', title: '关联询价单', minWidth: 150 },
    { field: 'purchaseOrderNo', title: '关联采购订单', minWidth: 160 },
    {
      field: 'bondAmount',
      title: '保证金金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    { field: 'payDate', title: '缴纳日期', minWidth: 110 },
    {
      field: 'won',
      title: '是否中标',
      minWidth: 90,
      slots: { default: 'won' },
    },
    {
      field: 'refundStatus',
      title: '退回状态',
      minWidth: 110,
      slots: { default: 'refundStatus' },
    },
    {
      field: 'refundAmount',
      title: '退回金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    { field: 'refundDate', title: '退回日期', minWidth: 110 },
    { field: 'remark', title: '备注', minWidth: 150 },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/bidBond/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmBidBondApi } from '#/api/srm/bidBond';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteBidBond,
  getBidBondPage,
} from '#/api/srm/bidBond';
import { $t } from '#/locales';
import {
  getRefundStatusColor,
  getRefundStatusLabel,
  useGridColumns,
  useGridFormSchema,
} from './data';
import BidBondForm from './modules/form.vue';
defineOptions({ name: 'SrmBidBond' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: BidBondForm,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmBidBondApi.BidBond) {
  formModalApi.setData({ id: row.id }).open();
}
async function handleDelete(row: SrmBidBondApi.BidBond) {
  await deleteBidBond(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getBidBondPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmBidBondApi.BidBond>,
});
</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:bid-bond:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #won="{ row }">
        <Tag :color="row.won ? 'success' : 'default'">
          {{ row.won ? '中标' : '未中标' }}
        </Tag>
      </template>
      <template #refundStatus="{ row }">
        <Tag :color="getRefundStatusColor(row.refundStatus)">
          {{ getRefundStatusLabel(row.refundStatus) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:bid-bond:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:bid-bond:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/bidBond/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import type { SrmBidBondApi } from '#/api/srm/bidBond';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getPurchaseOrder } from '#/api/erp/purchase/order';
import {
  createBidBond,
  getBidBond,
  updateBidBond,
} from '#/api/srm/bidBond';
import { getInquiry } from '#/api/srm/inquiry';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<SrmBidBondApi.BidBond>();
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: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
  handleValuesChange: async (values, changedFields) => {
    if (changedFields.includes('inquiryId') && values.inquiryId) {
      try {
        const inquiry = await getInquiry(values.inquiryId);
        await formApi.setValues({
          projectName: inquiry.title,
        });
      } catch {
        // è¯¢ä»·å•读取失败时不阻断手工录入
      }
    }
    if (changedFields.includes('purchaseOrderId') && values.purchaseOrderId) {
      try {
        const order: ErpPurchaseOrderApi.PurchaseOrder =
          await getPurchaseOrder(values.purchaseOrderId);
        await formApi.setValues({
          supplierId: order.supplierId,
          projectName: values.projectName || order.no,
          won: true,
        });
      } catch {
        // é‡‡è´­è®¢å•读取失败时不阻断手工录入
      }
    }
  },
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = (await formApi.getValues()) as SrmBidBondApi.BidBond;
    if (data.won && data.refundStatus === undefined) {
      data.refundStatus = 3;
    }
    modalApi.lock();
    try {
      await (formData.value?.id
        ? updateBidBond({ ...data, id: formData.value.id })
        : createBidBond(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<{ id?: number }>();
    if (!data?.id) {
      formData.value = undefined;
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getBidBond(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-3/4">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/enums.ts
@@ -1,6 +1,6 @@
// ============================================================
// SRM ç»Ÿä¸€æžšä¸¾å¸¸é‡
// æ‰€æœ‰å€¼ä¸¥æ ¼å¯¹é½åŽç«¯æžšä¸¾å®šä¹‰ (yudao-module-srm/enums/)
// ä»…保留供应商档案在用常量;准入/资质/绩效/招投标已移除
// ============================================================
/** é€‰é¡¹ç±»åž‹ (Select/RadioGroup通用) */
@@ -28,7 +28,10 @@
  { label: '关闭', value: 1 },
];
export const SUPPLIER_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
export const SUPPLIER_STATUS_CELLTAG: {
  data: CellTagItem[];
  labels: Record<number, string>;
} = {
  data: [
    { value: 0, color: 'green' },
    { value: 1, color: 'red' },
@@ -52,237 +55,15 @@
  { label: '其他', value: '4' },
];
export const SUPPLIER_TYPE_CELLTAG: { data: CellTagItem[]; labels: Record<string, string> } = {
export const SUPPLIER_TYPE_CELLTAG: {
  data: CellTagItem[];
  labels: Record<string, string>;
} = {
  data: [
    { value: '1', color: 'blue' },
    { value: '2', color: 'green' },
    { value: '3', color: 'orange' },
    { value: '4', color: 'default' },
  ],
  labels: { '1': '生产型', '2': '贸易型', '3': '服务型', '4': '其他' },
  labels: { 1: '生产型', 2: '贸易型', 3: '服务型', 4: '其他' },
};
// ========== å‡†å…¥ç”³è¯· â€” å¯¹é½ SupplierApplyStatusEnum (0/10/30/40/50)
// æŽ¥å…¥ååŒåŠžå…¬åŽç®€åŒ–ä¸º 5 æ€ï¼šå¾…提交 / å®¡æ‰¹ä¸­ / å·²å–消 / å·²é€šè¿‡ / å·²é©³å›ž
// åŽŸå¾…é‡‡è´­å®¡æ ¸(10)/待质量审核(20)/待财务审核(30)已废弃 ==========
export const APPLY_STATUS = {
  DRAFT: 0,
  PROCESS: 10,
  CANCEL: 30,
  APPROVED: 40,
  REJECTED: 50,
} as const;
export const APPLY_STATUS_OPTIONS: EnumOption[] = [
  { label: '待提交', value: 0 },
  { label: '审批中', value: 10 },
  { 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: 30, color: 'purple' },
    { value: 40, color: 'green' },
    { value: 50, color: 'red' },
  ],
  labels: { 0: '待提交', 10: '审批中', 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=已关闭 10=比价中 20=已确认) */
export const TENDER_STATUS = {
  DRAFT: 0,
  PUBLISHED: 1,
  BIDDING: 2,
  EVALUATING: 3,
  AWARDED: 4,
  CLOSED: 5,
  PRICE_COMPARING: 10,
  CONFIRMED: 20,
} 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 },
  { label: '比价中', value: 10 },
  { label: '已确认', value: 20 },
];
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' },
    { value: 10, color: 'purple' },
    { value: 20, color: 'green' },
  ],
  labels: { 0: '草稿', 1: '发布中', 2: '投标中', 3: '评标中', 4: '已定标', 5: '已关闭', 10: '比价中', 20: '已确认' },
};
/** æ‹›æ ‡çŠ¶æ€ 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',
  [TENDER_STATUS.PRICE_COMPARING]: 'purple',
  [TENDER_STATUS.CONFIRMED]: 'green',
};
// ========== æ‹›æŠ•标模式 â€” FULL=完整版 SIMPLE=简易版 ==========
export const BIDDING_MODE = {
  FULL: 'FULL',
  SIMPLE: 'SIMPLE',
} as const;
export const BIDDING_MODE_OPTIONS: EnumOption[] = [
  { label: '完整版', value: 'FULL' },
  { label: '简易版', value: 'SIMPLE' },
];
export const BIDDING_MODE_MAP: Record<string, string> = {
  FULL: '完整版',
  SIMPLE: '简易版',
};
// ========== æŠ•æ ‡ â€” å¯¹é½ 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/inquiry/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSimpleUserList } from '#/api/system/user';
/** è¯¢ä»·çŠ¶æ€ */
export const INQUIRY_STATUS_OPTIONS = [
  { label: '草稿', value: 0, color: 'default' },
  { label: '询价中', value: 10, color: 'processing' },
  { label: '已转订单', value: 20, color: 'success' },
  { label: '已取消', value: 30, color: 'error' },
];
export function getInquiryStatusLabel(status?: number) {
  return INQUIRY_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '-';
}
/** æ–°å¢ž/修改询价单的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'title',
      label: '询价标题',
      component: 'Input',
      rules: 'required',
      componentProps: { placeholder: '请输入询价标题' },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'inquiryDate',
      label: '询价日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择询价日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'deadline',
      label: '报价截止日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择报价截止日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'purchaserUserId',
      label: '采购员',
      component: 'ApiSelect',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择采购员',
        allowClear: true,
        showSearch: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 2 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '询价单号',
      component: 'Input',
      componentProps: { placeholder: '请输入询价单号', allowClear: true },
    },
    {
      fieldName: 'title',
      label: '询价标题',
      component: 'Input',
      componentProps: { placeholder: '请输入询价标题', allowClear: true },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: INQUIRY_STATUS_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'no', title: '询价单号', minWidth: 160 },
    { field: 'title', title: '询价标题', minWidth: 200 },
    {
      field: 'status',
      title: '状态',
      minWidth: 110,
      slots: { default: 'status' },
    },
    { field: 'inquiryDate', title: '询价日期', minWidth: 120 },
    { field: 'deadline', title: '报价截止', minWidth: 120 },
    { field: 'createTime', title: '创建时间', minWidth: 160 },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/srm/inquiry/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,232 @@
<script lang="ts" setup>
import type { SrmInquiryApi } from '#/api/srm/inquiry';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { Button, Tag } from 'ant-design-vue';
import { getInquiryCompare } from '#/api/srm/inquiry';
import { getInquiryStatusLabel, INQUIRY_STATUS_OPTIONS } from './data';
interface QuoteCell {
  price?: number;
  taxRate?: number;
  deliveryCycle?: number;
}
const router = useRouter();
const inquiry = ref<SrmInquiryApi.Inquiry>();
const quoteCells = ref<Record<string, QuoteCell>>({});
const orders = ref<SrmInquiryApi.ConfirmResult['orders']>([]);
const statusOption = computed(() =>
  INQUIRY_STATUS_OPTIONS.find((item) => item.value === inquiry.value?.status),
);
const supplierColumns = computed(() => {
  return (inquiry.value?.suppliers ?? []).map((item) => ({
    title: item.supplierName || `供应商 ${item.supplierId}`,
    key: `s_${item.supplierId}`,
    supplierId: item.supplierId,
    width: 210,
  }));
});
const columns = computed(() => [
  {
    title: '产品',
    key: 'product',
    width: 240,
    fixed: 'left',
    supplierId: undefined as number | undefined,
  },
  ...supplierColumns.value,
  {
    title: '中标供应商',
    key: 'winner',
    width: 140,
    supplierId: undefined as number | undefined,
  },
]);
function quoteKey(itemId?: number, supplierId?: number) {
  return `${itemId}_${supplierId}`;
}
function initQuotes(data: SrmInquiryApi.Inquiry) {
  const cells: Record<string, QuoteCell> = {};
  for (const quote of data.quotes ?? []) {
    cells[quoteKey(quote.inquiryItemId, quote.supplierId)] = {
      price: quote.price,
      taxRate: quote.taxRate,
      deliveryCycle: quote.deliveryCycle,
    };
  }
  quoteCells.value = cells;
}
async function loadData(id: number) {
  inquiry.value = await getInquiryCompare(id);
  orders.value = (inquiry.value.orders ?? []).map((item) => ({
    supplierId: item.supplierId!,
    supplierName: item.supplierName ?? '',
    purchaseOrderId: item.purchaseOrderId!,
    purchaseOrderNo: item.purchaseOrderNo ?? '',
  }));
  initQuotes(inquiry.value);
}
function getSupplierName(supplierId: number) {
  return (
    inquiry.value?.suppliers?.find((item) => item.supplierId === supplierId)
      ?.supplierName ?? `供应商 ${supplierId}`
  );
}
function getWinnerName(row: SrmInquiryApi.Item) {
  return row.selectedSupplierId
    ? getSupplierName(row.selectedSupplierId)
    : '-';
}
function amountOf(row: SrmInquiryApi.Item, supplierId: number) {
  const price = quoteCells.value[quoteKey(row.id, supplierId)]?.price;
  if (!price) {
    return '-';
  }
  return (price * (row.quantity ?? 0)).toFixed(2);
}
function goPurchaseOrder() {
  router.push({ path: '/purchase/order' });
}
const [Modal, modalApi] = useVbenModal({
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      inquiry.value = undefined;
      quoteCells.value = {};
      orders.value = [];
      return;
    }
    const data = modalApi.getData<{ id?: number }>();
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    loadData(data.id).finally(() => modalApi.unlock());
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal title="询价详情" class="w-11/12" :show-confirm-button="false">
    <div v-if="inquiry" class="mx-3">
      <div class="mb-3 flex flex-wrap items-center gap-6 text-sm">
        <span>
          <span class="text-gray-500">询价单号:</span>{{ inquiry.no }}
        </span>
        <span>
          <span class="text-gray-500">标题:</span>{{ inquiry.title }}
        </span>
        <Tag :color="statusOption?.color ?? 'default'">
          {{ getInquiryStatusLabel(inquiry.status) }}
        </Tag>
        <span>
          <span class="text-gray-500">询价日期:</span>{{
            inquiry.inquiryDate || '-'
          }}
        </span>
        <span>
          <span class="text-gray-500">报价截止:</span>{{
            inquiry.deadline || '-'
          }}
        </span>
      </div>
      <div class="mb-2 flex flex-wrap items-center gap-2">
        <Button v-if="orders.length > 0" @click="goPurchaseOrder">
          æŸ¥çœ‹é‡‡è´­è®¢å•
        </Button>
        <span class="text-xs text-gray-400">
          æŠ¥ä»·ä¸Žä¸­æ ‡ç»“果在新增/编辑询价单时一并录入;此处仅展示报价与中标结果。
        </span>
      </div>
      <a-table
        :columns="columns"
        :data-source="inquiry.items ?? []"
        :pagination="false"
        size="small"
        bordered
        row-key="id"
        :scroll="{ x: 1000 }"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'product'">
            <div class="font-medium">{{ record.productName }}</div>
            <div class="text-xs text-gray-500">
              {{ record.productCode || '-' }} / {{ record.productSpec || '-' }}
            </div>
            <div class="text-xs text-gray-500">
              æ•°é‡ï¼š{{ record.quantity }} {{ record.unitName || '' }}
            </div>
            <div class="text-xs text-gray-500">
              ç›®æ ‡å•价:{{ record.estimatedPrice ?? '-' }}
            </div>
          </template>
          <template
            v-else-if="String(column.key).startsWith('s_')"
          >
            <template
              v-if="quoteCells[quoteKey(record.id, column.supplierId)]?.price"
            >
              <div class="font-medium">
                å«ç¨Žå•价:{{ quoteCells[quoteKey(record.id, column.supplierId)]?.price }}
              </div>
              <div class="text-xs text-gray-500">
                ç¨ŽçŽ‡ï¼š{{
                  quoteCells[quoteKey(record.id, column.supplierId)]?.taxRate ?? '-'
                }}% / äº¤æœŸï¼š{{
                  quoteCells[quoteKey(record.id, column.supplierId)]?.deliveryCycle ?? '-'
                }} å¤©
              </div>
              <div class="text-xs text-gray-500">
                ä»·ç¨Žåˆè®¡ï¼š{{ amountOf(record, column.supplierId) }}
              </div>
            </template>
            <div v-else class="text-xs text-gray-400">未报价</div>
            <Tag
              v-if="record.selectedSupplierId === column.supplierId"
              color="success"
              class="mt-1"
            >
              ä¸­æ ‡
            </Tag>
          </template>
          <template v-else-if="column.key === 'winner'">
            {{ getWinnerName(record) }}
          </template>
        </template>
      </a-table>
      <div v-if="orders.length > 0" class="mt-4 rounded bg-gray-50 p-3">
        <div class="mb-1 font-medium">已生成的采购订单草稿</div>
        <div
          v-for="order in orders"
          :key="order.purchaseOrderId"
          class="text-sm"
        >
          {{ order.supplierName }}:{{ order.purchaseOrderNo }}(草稿,需在采购订单页提交审批)
        </div>
      </div>
    </div>
  </Modal>
</template>
src/views/srm/inquiry/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmInquiryApi } from '#/api/srm/inquiry';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelInquiry,
  confirmInquiry,
  deleteInquiry,
  getInquiryPage,
} from '#/api/srm/inquiry';
import { $t } from '#/locales';
import {
  getInquiryStatusLabel,
  INQUIRY_STATUS_OPTIONS,
  useGridColumns,
  useGridFormSchema,
} from './data';
import CompareModal from './detail.vue';
import InquiryForm from './modules/form.vue';
defineOptions({ name: 'SrmInquiry' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: InquiryForm,
  destroyOnClose: true,
});
const [CompareView, compareModalApi] = useVbenModal({
  connectedComponent: CompareModal,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmInquiryApi.Inquiry) {
  formModalApi.setData({ id: row.id }).open();
}
function handleCompare(row: SrmInquiryApi.Inquiry) {
  compareModalApi.setData({ id: row.id }).open();
}
async function handleConfirm(row: SrmInquiryApi.Inquiry) {
  const result = await confirmInquiry(row.id!, []);
  const orders = result.orders ?? [];
  message.success(
    `询价单 ${row.no} å·²ç¡®è®¤ä¸­æ ‡ï¼Œç”Ÿæˆ ${orders.length} å¼ é‡‡è´­è®¢å•草稿:${orders
      .map((item) => item.purchaseOrderNo)
      .join('、')}(需在采购订单页提交审批)`,
  );
  handleRefresh();
}
async function handleCancel(row: SrmInquiryApi.Inquiry) {
  await cancelInquiry(row.id!);
  message.success(`询价单 ${row.no} å·²å–消`);
  handleRefresh();
}
async function handleDelete(row: SrmInquiryApi.Inquiry) {
  await deleteInquiry(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getInquiryPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmInquiryApi.Inquiry>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <CompareView @success="handleRefresh" />
    <Grid table-title="询价比价">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['询价单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:inquiry:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag
          :color="
            INQUIRY_STATUS_OPTIONS.find((item) => item.value === row.status)
              ?.color ?? 'default'
          "
        >
          {{ getInquiryStatusLabel(row.status) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '详情',
              type: 'link',
              icon: ACTION_ICON.VIEW,
              ifShow: () => row.status !== 30,
              onClick: handleCompare.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:inquiry:update'],
              ifShow: () => row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '确认转订单',
              type: 'link',
              auth: ['srm:inquiry:confirm'],
              ifShow: () => row.status === 0 || row.status === 10,
              popConfirm: {
                title: `确认询价单 ${row.no} çš„中标结果并生成采购订单草稿吗?生成后不可再修改该询价单。`,
                confirm: handleConfirm.bind(null, row),
              },
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
              auth: ['srm:inquiry:update'],
              ifShow: () => row.status === 0 || row.status === 10,
              popConfirm: {
                title: `取消询价单 ${row.no} å—?`,
                confirm: handleCancel.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:inquiry:delete'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/srm/inquiry/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,457 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { SrmInquiryApi } from '#/api/srm/inquiry';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { computed, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getItemSimpleList } from '#/api/mdm/item';
import {
  createInquiry,
  getInquiry,
  updateInquiry,
} from '#/api/srm/inquiry';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
interface ItemQuoteRow extends SrmInquiryApi.ItemQuote {
  price?: number;
}
interface ItemRow extends SrmInquiryApi.Item {
  key: string;
  quotes: ItemQuoteRow[];
}
const emit = defineEmits(['success']);
const formData = ref<SrmInquiryApi.Inquiry>();
const itemRows = ref<ItemRow[]>([]);
const supplierIds = ref<number[]>([]);
const itemOptions = ref<MdmItemApi.Item[]>([]);
const supplierOptions = ref<SrmSupplierApi.SupplierVO[]>([]);
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['询价单'])
    : $t('ui.actionTitle.create', ['询价单']);
});
const itemColumns = [
  { title: '产品', key: 'productId', width: 220 },
  { title: '规格型号', key: 'productSpec', width: 140 },
  { title: '单位', key: 'unitName', width: 70 },
  { title: '数量', key: 'quantity', width: 110 },
  { title: '目标单价', key: 'estimatedPrice', width: 110 },
  { title: '备注', key: 'remark', width: 140 },
  { title: '操作', key: 'action', width: 60, fixed: 'right' },
];
const quoteColumns = computed(() =>
  supplierIds.value.map((supplierId) => ({
    title: getSupplierName(supplierId),
    key: `supplier_${supplierId}`,
    width: 200,
  })),
);
const tableColumns = computed(() => {
  const columns = itemColumns.filter((column) => column.key !== 'action');
  return [
    ...columns,
    ...quoteColumns.value,
    { title: '中标供应商', key: 'winner', width: 160, fixed: 'right' },
    itemColumns.find((column) => column.key === 'action'),
  ];
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
onMounted(async () => {
  const [items, suppliers] = await Promise.all([
    getItemSimpleList(),
    getSupplierSimpleList(),
  ]);
  itemOptions.value = items ?? [];
  supplierOptions.value = suppliers ?? [];
});
function getSupplierName(supplierId: number) {
  return (
    supplierOptions.value.find((item) => item.id === supplierId)?.name ??
    `供应商 ${supplierId}`
  );
}
/** ä¾›åº”商标识来自列 key:supplier_{id} */
function supplierIdOfColumn(key: unknown) {
  return Number(String(key).replace('supplier_', ''));
}
/** ä¿è¯æ¯ä¸ªäº§å“è¡Œå¯¹æ¯ä¸ªå‚与供应商都有报价对象,供 v-model ç›´æŽ¥ç»‘定 */
function syncQuotes() {
  for (const row of itemRows.value) {
    const existing = new Map<number, ItemQuoteRow>(
      (row.quotes ?? []).map((quote) => [quote.supplierId, quote]),
    );
    row.quotes = supplierIds.value.map(
      (supplierId) => existing.get(supplierId) ?? { supplierId },
    );
    if (
      row.selectedSupplierId &&
      !supplierIds.value.includes(row.selectedSupplierId)
    ) {
      row.selectedSupplierId = undefined;
    }
  }
}
function quoteOf(row: ItemRow, supplierId: number) {
  let quote = row.quotes?.find((item) => item.supplierId === supplierId);
  if (!quote) {
    quote = { supplierId };
    row.quotes = [...(row.quotes ?? []), quote];
  }
  return quote;
}
function quotePriceOf(row: ItemRow, supplierId: number) {
  return (row.quotes ?? []).find((item) => item.supplierId === supplierId)?.price;
}
function hasQuote(row: ItemRow, supplierId: number) {
  const price = quotePriceOf(row, supplierId);
  return price !== undefined && price !== null && price > 0;
}
function handleSupplierChange() {
  syncQuotes();
}
function handleAddItem() {
  itemRows.value.push({
    key: `${Date.now()}_${itemRows.value.length}`,
    productId: undefined,
    quantity: 1,
    estimatedPrice: undefined,
    quotes: [],
  });
  syncQuotes();
}
function handleRemoveItem(index: number) {
  itemRows.value.splice(index, 1);
}
function handleProductChange(productId: number, record: ItemRow) {
  const item = itemOptions.value.find((option) => option.id === productId);
  if (!item) {
    return;
  }
  record.productCode = item.code;
  record.productName = item.name;
  record.productSpec = item.specification;
  record.productUnitId = item.unitMeasureId;
  record.unitName = item.unitMeasureName;
  if (!record.estimatedPrice) {
    record.estimatedPrice = item.purchasePrice;
  }
}
function productLabel(row: ItemRow, index: number) {
  return row.productName || `第 ${index + 1} è¡Œ`;
}
/** æ ¡éªŒå¹¶ç»„装提交数据 */
function buildPayload(values: SrmInquiryApi.Inquiry): SrmInquiryApi.Inquiry {
  if (itemRows.value.length === 0) {
    throw new Error('请至少添加一个询价产品');
  }
  const items: SrmInquiryApi.Item[] = itemRows.value.map((row, index) => {
    const label = productLabel(row, index);
    if (!row.productId) {
      throw new Error(`第 ${index + 1} è¡Œï¼šè¯·é€‰æ‹©äº§å“`);
    }
    if (!row.quantity || row.quantity <= 0) {
      throw new Error(`产品【${label}】:采购数量必须大于 0`);
    }
    const quotes = (row.quotes ?? []).filter(
      (quote) => quote.price !== undefined && quote.price !== null,
    );
    for (const quote of quotes) {
      if (quote.price! <= 0) {
        throw new Error(
          `产品【${label}】供应商【${getSupplierName(quote.supplierId)}】:报价单价必须大于 0`,
        );
      }
    }
    if (quotes.length === 0) {
      throw new Error(
        `产品【${label}】:还没有录入任何供应商报价,请至少填写一个供应商的报价单价`,
      );
    }
    if (row.selectedSupplierId) {
      const winnerQuote = quotes.find(
        (quote) => quote.supplierId === row.selectedSupplierId,
      );
      if (!winnerQuote) {
        throw new Error(
          `产品【${label}】:中标供应商【${getSupplierName(row.selectedSupplierId)}】没有填写报价单价,请补填报价或改选其他供应商`,
        );
      }
    }
    return {
      id: row.id,
      productId: row.productId,
      productUnitId: row.productUnitId,
      productCode: row.productCode,
      productName: row.productName,
      productSpec: row.productSpec,
      unitName: row.unitName,
      quantity: row.quantity,
      estimatedPrice: row.estimatedPrice,
      selectedSupplierId: row.selectedSupplierId,
      remark: row.remark,
      quotes: quotes.map((quote) => ({
        supplierId: quote.supplierId,
        price: quote.price,
        taxRate: quote.taxRate,
        deliveryCycle: quote.deliveryCycle,
        paymentTerms: quote.paymentTerms,
        remark: quote.remark,
      })),
    };
  });
  if (supplierIds.value.length === 0) {
    throw new Error('请至少选择一个参与询价的供应商');
  }
  return {
    ...values,
    items,
    suppliers: supplierIds.value.map((supplierId) => ({ supplierId })),
  };
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const values = (await formApi.getValues()) as SrmInquiryApi.Inquiry;
    let payload: SrmInquiryApi.Inquiry;
    try {
      payload = buildPayload(values);
    } catch (error: unknown) {
      message.error(error instanceof Error ? error.message : '请检查询价单数据');
      return;
    }
    modalApi.lock();
    try {
      if (formData.value?.id) {
        await updateInquiry({ ...payload, id: formData.value.id });
      } else {
        await createInquiry(payload);
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      itemRows.value = [];
      supplierIds.value = [];
      return;
    }
    const data = modalApi.getData<{ id?: number }>();
    if (!data?.id) {
      formData.value = undefined;
      itemRows.value = [];
      supplierIds.value = [];
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getInquiry(data.id);
      supplierIds.value = (formData.value.suppliers ?? []).map(
        (item) => item.supplierId,
      );
      const quotes = formData.value.quotes ?? [];
      itemRows.value = (formData.value.items ?? []).map((item, index) => ({
        ...item,
        key: String(item.id ?? index),
        quotes: quotes
          .filter((quote) => quote.inquiryItemId === item.id)
          .map((quote) => ({
            supplierId: quote.supplierId!,
            price: quote.price,
            taxRate: quote.taxRate,
            deliveryCycle: quote.deliveryCycle,
            paymentTerms: quote.paymentTerms,
            remark: quote.remark,
          })),
      }));
      syncQuotes();
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-11/12">
    <Form class="mx-4" />
    <div class="mx-4 mt-2">
      <div class="mb-2 font-medium">参与询价的供应商</div>
      <a-select
        v-model:value="supplierIds"
        mode="multiple"
        show-search
        option-filter-prop="label"
        placeholder="请选择参与询价的供应商(可多选)"
        style="width: 100%"
        :options="supplierOptions.map((item) => ({
          label: item.name,
          value: item.id,
        }))"
        @change="handleSupplierChange"
      />
    </div>
    <div class="mx-4 mt-4">
      <div class="mb-2 flex items-center justify-between">
        <span class="font-medium">询价产品与供应商报价</span>
        <Button type="primary" size="small" @click="handleAddItem">
          æ·»åŠ äº§å“
        </Button>
      </div>
      <div class="mb-2 text-xs text-gray-400">
        é€‰æ‹©å‚与供应商后,每个供应商会生成一列报价;报价单价为含税单价,价税合计 = å•ä»· Ã— æ•°é‡ï¼›å¹¶åœ¨ã€Œä¸­æ ‡ä¾›åº”商」列选择该产品的中标方。
      </div>
      <a-table
        :columns="tableColumns"
        :data-source="itemRows"
        :pagination="false"
        size="small"
        bordered
        row-key="key"
        :scroll="{ x: 1400 }"
      >
        <template #bodyCell="{ column, index, record }">
          <template v-if="column.key === 'productId'">
            <a-select
              v-model:value="record.productId"
              show-search
              option-filter-prop="label"
              placeholder="请选择产品"
              style="width: 100%"
              :options="itemOptions.map((item) => ({
                label: item.name,
                value: item.id,
                code: item.code,
              }))"
              @change="(value: number) => handleProductChange(value, record)"
            />
          </template>
          <template v-else-if="column.key === 'productSpec'">
            {{ record.productSpec || '-' }}
          </template>
          <template v-else-if="column.key === 'unitName'">
            {{ record.unitName || '-' }}
          </template>
          <template v-else-if="column.key === 'quantity'">
            <a-input-number
              v-model:value="record.quantity"
              :min="0"
              :precision="3"
              style="width: 100%"
            />
          </template>
          <template v-else-if="column.key === 'estimatedPrice'">
            <a-input-number
              v-model:value="record.estimatedPrice"
              :min="0"
              :precision="2"
              style="width: 100%"
            />
          </template>
          <template v-else-if="column.key === 'remark'">
            <a-input v-model:value="record.remark" placeholder="备注" />
          </template>
          <template
            v-else-if="String(column.key).startsWith('supplier_')"
          >
            <div class="space-y-1">
              <a-input-number
                v-model:value="quoteOf(record, supplierIdOfColumn(column.key)).price"
                size="small"
                :min="0"
                :precision="2"
                placeholder="含税单价"
                style="width: 100%"
              />
              <div class="flex gap-1">
                <a-input-number
                  v-model:value="quoteOf(record, supplierIdOfColumn(column.key)).taxRate"
                  size="small"
                  :min="0"
                  :precision="2"
                  placeholder="税率%"
                  style="width: 50%"
                />
                <a-input-number
                  v-model:value="quoteOf(record, supplierIdOfColumn(column.key)).deliveryCycle"
                  size="small"
                  :min="0"
                  :precision="0"
                  placeholder="交期天"
                  style="width: 50%"
                />
              </div>
            </div>
          </template>
          <template v-else-if="column.key === 'winner'">
            <a-select
              v-model:value="record.selectedSupplierId"
              allow-clear
              placeholder="请选择中标供应商"
              style="width: 100%"
              :options="supplierIds.map((supplierId) => ({
                label: getSupplierName(supplierId),
                value: supplierId,
                disabled: !hasQuote(record, supplierId),
              }))"
            />
          </template>
          <template v-else-if="column.key === 'action'">
            <a-button type="link" danger size="small" @click="handleRemoveItem(index)">
              åˆ é™¤
            </a-button>
          </template>
        </template>
      </a-table>
    </div>
  </Modal>
</template>
src/views/srm/statistics/index.vue
ÎļþÒÑɾ³ý
src/views/srm/supplier/data.ts
@@ -54,25 +54,7 @@
        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: '请输入邮箱' },
      rules: z.number().default(0),
    },
    {
      fieldName: 'address',
@@ -80,18 +62,6 @@
      component: 'Input',
      componentProps: { placeholder: '请输入地址' },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'bankName',
      label: '开户银行',
      component: 'Input',
      componentProps: { placeholder: '请输入开户银行' },
    },
    {
      fieldName: 'bankAccount',
      label: '银行账号',
      component: 'Input',
      componentProps: { placeholder: '请输入银行账号' },
    },
    {
      fieldName: 'taxNo',
src/views/srm/supplier/modules/form.vue
@@ -5,7 +5,7 @@
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -19,11 +19,43 @@
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierApi.SupplierVO>();
interface ContactRow extends SrmSupplierApi.SupplierContact { _key: string }
interface BankRow extends SrmSupplierApi.SupplierBank { _key: string }
let rowSeed = 0;
function nextKey() {
  rowSeed += 1;
  return `row_${rowSeed}`;
}
const contacts = ref<ContactRow[]>([]);
const banks = ref<BankRow[]>([]);
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['供应商'])
    : $t('ui.actionTitle.create', ['供应商']);
});
const contactColumns = [
  { title: '姓名', key: 'name', width: 110 },
  { title: '职位', key: 'position', width: 110 },
  { title: '手机', key: 'mobile', width: 140 },
  { title: '固定电话', key: 'telephone', width: 140 },
  { title: '邮箱', key: 'email', width: 180 },
  { title: '微信', key: 'wechat', width: 120 },
  { title: '默认', key: 'isDefault', width: 60 },
  { title: '操作', key: 'action', width: 60 },
];
const bankColumns = [
  { title: '开户行', key: 'bankName', width: 200 },
  { title: '银行账号', key: 'bankAccount', width: 200 },
  { title: '银行行号', key: 'bankNo', width: 160 },
  { title: '开户行地址', key: 'bankAddress', width: 200 },
  { title: '备注', key: 'remark', width: 150 },
  { title: '默认', key: 'isDefault', width: 60 },
  { title: '操作', key: 'action', width: 60 },
];
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -36,16 +68,117 @@
  showDefaultActions: false,
});
function addContact() {
  contacts.value.push({
    _key: nextKey(),
    name: '',
    position: '',
    mobile: '',
    telephone: '',
    email: '',
    wechat: '',
    isDefault: contacts.value.length === 0,
  });
}
function removeContact(index: number) {
  const wasDefault = contacts.value[index]?.isDefault;
  contacts.value.splice(index, 1);
  if (wasDefault && contacts.value.length > 0) {
    contacts.value[0]!.isDefault = true;
  }
}
function setDefaultContact(index: number) {
  contacts.value.forEach((item, i) => {
    item.isDefault = i === index;
  });
}
function addBank() {
  banks.value.push({
    _key: nextKey(),
    bankName: '',
    bankAccount: '',
    bankNo: '',
    bankAddress: '',
    remark: '',
    isDefault: banks.value.length === 0,
  });
}
function removeBank(index: number) {
  const wasDefault = banks.value[index]?.isDefault;
  banks.value.splice(index, 1);
  if (wasDefault && banks.value.length > 0) {
    banks.value[0]!.isDefault = true;
  }
}
function setDefaultBank(index: number) {
  banks.value.forEach((item, i) => {
    item.isDefault = i === index;
  });
}
/** æäº¤å‰æ ¡éªŒå¤šè¡Œæ˜Žç»†ï¼Œå¹¶åŽ»æŽ‰å®Œå…¨ç©ºç™½çš„è¡Œ */
function buildPayload(
  values: SrmSupplierApi.SupplierVO,
): SrmSupplierApi.SupplierVO {
  const validContacts = contacts.value.filter(
    (item) =>
      item.name?.trim() ||
      item.mobile?.trim() ||
      item.telephone?.trim() ||
      item.email?.trim(),
  );
  for (let i = 0; i < validContacts.length; i++) {
    if (!validContacts[i]!.name?.trim()) {
      throw new Error(`第 ${i + 1} ä¸ªè”系人的姓名不能为空`);
    }
  }
  const validBanks = banks.value.filter(
    (item) =>
      item.bankName?.trim() ||
      item.bankAccount?.trim() ||
      item.bankNo?.trim(),
  );
  for (let i = 0; i < validBanks.length; i++) {
    if (!validBanks[i]!.bankName?.trim()) {
      throw new Error(`第 ${i + 1} ä¸ªé“¶è¡Œè´¦æˆ·çš„开户行不能为空`);
    }
    if (!validBanks[i]!.bankAccount?.trim()) {
      throw new Error(`第 ${i + 1} ä¸ªé“¶è¡Œè´¦æˆ·çš„银行账号不能为空`);
    }
  }
  if (validContacts.length > 0 && !validContacts.some((item) => item.isDefault)) {
    validContacts[0]!.isDefault = true;
  }
  if (validBanks.length > 0 && !validBanks.some((item) => item.isDefault)) {
    validBanks[0]!.isDefault = true;
  }
  return { ...values, contacts: validContacts, banks: validBanks };
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as SrmSupplierApi.SupplierVO;
    let payload: SrmSupplierApi.SupplierVO;
    try {
      await (formData.value?.id ? updateSupplier(data) : createSupplier(data));
      payload = buildPayload(data);
    } catch (error: unknown) {
      message.error(error instanceof Error ? error.message : '请检查明细数据');
      return;
    }
    modalApi.lock();
    try {
      await (formData.value?.id
        ? updateSupplier({ ...payload, id: formData.value.id })
        : createSupplier(payload));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
@@ -56,15 +189,28 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      contacts.value = [];
      banks.value = [];
      return;
    }
    const data = modalApi.getData<SrmSupplierApi.SupplierVO>();
    if (!data || !data.id) {
      formData.value = undefined;
      contacts.value = [];
      banks.value = [];
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getSupplier(data.id);
      contacts.value = (formData.value.contacts ?? []).map((item) => ({
        ...item,
        _key: nextKey(),
      }));
      banks.value = (formData.value.banks ?? []).map((item) => ({
        ...item,
        _key: nextKey(),
      }));
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
@@ -76,7 +222,104 @@
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
  <Modal :title="getTitle" class="w-3/4">
    <Form class="mx-4" />
    <div class="mx-4 mt-2">
      <div class="mb-2 flex items-center justify-between">
        <span class="font-medium">联系人及联系方式</span>
        <Button type="primary" size="small" @click="addContact">
          æ·»åŠ è”ç³»äºº
        </Button>
      </div>
      <a-table
        :columns="contactColumns"
        :data-source="contacts"
        :pagination="false"
        size="small"
        bordered
        row-key="_key"
      >
        <template #bodyCell="{ column, index, record }">
          <template v-if="column.key === 'name'">
            <a-input v-model:value="record.name" placeholder="姓名(必填)" />
          </template>
          <template v-else-if="column.key === 'position'">
            <a-input v-model:value="record.position" placeholder="职位" />
          </template>
          <template v-else-if="column.key === 'mobile'">
            <a-input v-model:value="record.mobile" placeholder="手机" />
          </template>
          <template v-else-if="column.key === 'telephone'">
            <a-input v-model:value="record.telephone" placeholder="固定电话" />
          </template>
          <template v-else-if="column.key === 'email'">
            <a-input v-model:value="record.email" placeholder="邮箱" />
          </template>
          <template v-else-if="column.key === 'wechat'">
            <a-input v-model:value="record.wechat" placeholder="微信号" />
          </template>
          <template v-else-if="column.key === 'isDefault'">
            <a-radio
              :checked="record.isDefault"
              @change="setDefaultContact(index)"
            />
          </template>
          <template v-else-if="column.key === 'action'">
            <a-button type="link" danger size="small" @click="removeContact(index)">
              åˆ é™¤
            </a-button>
          </template>
        </template>
      </a-table>
    </div>
    <div class="mx-4 mt-4">
      <div class="mb-2 flex items-center justify-between">
        <span class="font-medium">银行账户</span>
        <Button type="primary" size="small" @click="addBank">
          æ·»åŠ é“¶è¡Œè´¦æˆ·
        </Button>
      </div>
      <a-table
        :columns="bankColumns"
        :data-source="banks"
        :pagination="false"
        size="small"
        bordered
        row-key="_key"
      >
        <template #bodyCell="{ column, index, record }">
          <template v-if="column.key === 'bankName'">
            <a-input v-model:value="record.bankName" placeholder="开户行(必填)" />
          </template>
          <template v-else-if="column.key === 'bankAccount'">
            <a-input
              v-model:value="record.bankAccount"
              placeholder="银行账号(必填)"
            />
          </template>
          <template v-else-if="column.key === 'bankNo'">
            <a-input v-model:value="record.bankNo" placeholder="银行行号" />
          </template>
          <template v-else-if="column.key === 'bankAddress'">
            <a-input v-model:value="record.bankAddress" placeholder="开户行地址" />
          </template>
          <template v-else-if="column.key === 'remark'">
            <a-input v-model:value="record.remark" placeholder="备注" />
          </template>
          <template v-else-if="column.key === 'isDefault'">
            <a-radio
              :checked="record.isDefault"
              @change="setDefaultBank(index)"
            />
          </template>
          <template v-else-if="column.key === 'action'">
            <a-button type="link" danger size="small" @click="removeBank(index)">
              åˆ é™¤
            </a-button>
          </template>
        </template>
      </a-table>
    </div>
  </Modal>
</template>
src/views/srm/supplierApply/data.ts
ÎļþÒÑɾ³ý
src/views/srm/supplierApply/detail/index.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierApply/index.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierApply/modules/form.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierApply/modules/process-select-modal.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierCertificate/data.ts
ÎļþÒÑɾ³ý
src/views/srm/supplierCertificate/index.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierCertificate/modules/form.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierEvaluation/data.ts
ÎļþÒÑɾ³ý
src/views/srm/supplierEvaluation/index.vue
ÎļþÒÑɾ³ý
src/views/srm/supplierEvaluation/modules/form.vue
ÎļþÒÑɾ³ý
src/views/srm/tender/config/index.vue
ÎļþÒÑɾ³ý
src/views/srm/tender/data.ts
ÎļþÒÑɾ³ý
src/views/srm/tender/detail.vue
ÎļþÒÑɾ³ý
src/views/srm/tender/index.vue
ÎļþÒÑɾ³ý
src/views/srm/tender/modules/form.vue
ÎļþÒÑɾ³ý