refactor(srm): 重构供应商采购界面,新增询价比价与保证金页面
- 移除供应商准入/资质/绩效/招投标/统计页面及路由
- 新增询价比价、投标保证金、供应商对账单页面
- 供应商档案表单支持银行账户
- 采购订单/入库/请购/退货表单适配
Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加11个文件
已删除24个文件
已修改14个文件
| | |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤éè´è®¢å审æ¹ï¼è稿/æªå®¡æ ¸ -> 审æ¹ä¸ï¼ */ |
| | | 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', { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 } }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }, |
| | | ); |
| | | } |
| | |
| | | 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; |
| | |
| | | taxNo?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | contacts?: SupplierContact[]; |
| | | banks?: SupplierBank[]; |
| | | } |
| | | } |
| | | |
| | |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: '产ååä»·', |
| | | title: 'å«ç¨åä»·', |
| | | fixed: 'right', |
| | | minWidth: 120, |
| | | slots: { default: 'productPrice' }, |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | fixed: 'right', |
| | | title: '产åéé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | fixed: 'right', |
| | |
| | | { |
| | | fixed: 'right', |
| | | field: 'taxPrice', |
| | | title: 'ç¨é¢', |
| | | title: 'ç¨é¢ï¼ä»·å
å«ï¼', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | fixed: 'right', |
| | | title: 'å计éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | title: 'éé¢å计', |
| | | title: '伿 åéé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å«ç¨éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | |
| | | |
| | | /** å¤çè¡æ°æ®åæ´ */ |
| | | function handleRowChange(row: any) { |
| | | // TODO èè¿ |
| | | calcRow(row); |
| | | const index = tableData.value.findIndex((item) => item.seq === row.seq); |
| | | if (index === -1) { |
| | | tableData.value.push(row); |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** è¡¨åæ ¡éª */ |
| | |
| | | <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> |
| | |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: '产ååä»·', |
| | | title: 'å«ç¨åä»·', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | slots: { default: 'productPrice' }, |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | title: 'éé¢', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'taxPercent', |
| | |
| | | }, |
| | | { |
| | | field: 'taxPrice', |
| | | title: 'ç¨é¢', |
| | | title: 'ç¨é¢ï¼ä»·å
å«ï¼', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'ç¨é¢å计', |
| | | title: 'ä»·ç¨å计', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | formatter: 'formatAmount2', |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number').filter( |
| | | (opt) => opt.label !== 'è稿' && opt.label !== '审æ¹ä¸éè¿', |
| | | (opt) => opt.label !== '审æ¹ä¸éè¿', |
| | | ), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | title: 'éé¢å计', |
| | | title: '伿 åéé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å«ç¨éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | |
| | | deletePurchaseOrder, |
| | | exportPurchaseOrder, |
| | | getPurchaseOrderPage, |
| | | submitPurchaseOrder, |
| | | } from '#/api/erp/purchase/order'; |
| | | import { $t } from '#/locales'; |
| | | |
| | |
| | | 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(); |
| | |
| | | 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, |
| | |
| | | |
| | | /** å¤çè¡æ°æ®åæ´ */ |
| | | function handleRowChange(row: any) { |
| | | calcRow(row); |
| | | const index = tableData.value.findIndex((item) => item.seq === row.seq); |
| | | if (index === -1) { |
| | | tableData.value.push(row); |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** è¡¨åæ ¡éª */ |
| | |
| | | throw new Error(`第 ${i + 1} è¡ï¼äº§åæ°éä¸è½ä¸ºç©º`); |
| | | } |
| | | if (!item.productPrice || item.productPrice <= 0) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼äº§ååä»·ä¸è½ä¸ºç©º`); |
| | | throw new Error(`第 ${i + 1} è¡ï¼å«ç¨åä»·ä¸è½ä¸ºç©º`); |
| | | } |
| | | } |
| | | } |
| | |
| | | <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> |
| | |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: 'åä»·ï¼åèä»·ï¼', |
| | | title: 'å«ç¨åä»·', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | slots: { default: 'productPrice' }, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'é¢ä¼°éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | minWidth: 120, |
| | | fixed: 'right', |
| | | formatter: 'formatAmount2', |
| | |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'é¢ä¼°éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: 'åä»·ï¼åèä»·ï¼', |
| | | title: 'å«ç¨åä»·', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'é¢ä¼°éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | |
| | | render: (val) => erpCountInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'å计éé¢', |
| | | label: 'ä»·ç¨å计', |
| | | field: 'totalPrice', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'productPrice', |
| | | title: '产ååä»·', |
| | | title: 'å«ç¨åä»·', |
| | | fixed: 'right', |
| | | minWidth: 120, |
| | | slots: { default: 'productPrice' }, |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | fixed: 'right', |
| | | title: '产åéé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | fixed: 'right', |
| | |
| | | { |
| | | fixed: 'right', |
| | | field: 'taxPrice', |
| | | title: 'ç¨é¢', |
| | | title: 'ç¨é¢ï¼ä»·å
å«ï¼', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | fixed: 'right', |
| | | title: 'å计éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | |
| | | }, |
| | | { |
| | | field: 'totalProductPrice', |
| | | title: 'éé¢å计', |
| | | title: '伿 åéé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å«ç¨éé¢', |
| | | title: 'ä»·ç¨å计', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | |
| | | |
| | | /** å¤çè¡æ°æ®åæ´ */ |
| | | function handleRowChange(row: any) { |
| | | // TODO èè¿ |
| | | calcRow(row); |
| | | const index = tableData.value.findIndex((item) => item.seq === row.seq); |
| | | if (index === -1) { |
| | | tableData.value.push(row); |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** è¡¨åæ ¡éª */ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | // ============================================================ |
| | | // SRM ç»ä¸æä¸¾å¸¸é |
| | | // ææå¼ä¸¥æ ¼å¯¹é½å端æä¸¾å®ä¹ (yudao-module-srm/enums/) |
| | | // ä»
ä¿çä¾åºåæ¡£æ¡å¨ç¨å¸¸éï¼åå
¥/èµè´¨/绩æ/æææ å·²ç§»é¤ |
| | | // ============================================================ |
| | | |
| | | /** é项类å (Select/RadioGroupéç¨) */ |
| | |
| | | { 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' }, |
| | |
| | | { 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; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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', |
| | |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥å°å' }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'bankName', |
| | | label: '弿·é¶è¡', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥å¼æ·é¶è¡' }, |
| | | }, |
| | | { |
| | | fieldName: 'bankAccount', |
| | | label: 'é¶è¡è´¦å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¶è¡è´¦å·' }, |
| | | }, |
| | | { |
| | | fieldName: 'taxNo', |
| | |
| | | |
| | | 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 { |
| | |
| | | |
| | | 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: { |
| | |
| | | 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')); |
| | |
| | | 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(); |
| | |
| | | </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> |