feat: 同步质检、生产、销售、财务与人事前端功能
- MES:IPQC 工单驱动与附件、盘号展示,新增质检统计、巡检、工时与计件工资、
销售通知台账、盘点/入库相关页面;移除旧排班日历页面
- CRM:回款审批、发票录入与付款节点、报价单 OCR/模板、客户地址、客户对账单
- ERP:采购发票、财务对账与核销页面
- HRM:社保与薪资核算页面调整
- SRM:询价页面调整
- 公共:业务枚举、质量规则引擎与单据渲染同步
Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加29个文件
已删除31个文件
已重命名1个文件
已修改76个文件
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmCustomerAddressApi { |
| | | /** å®¢æ·æ¶è´§å°å */ |
| | | export interface CustomerAddress { |
| | | id?: number; |
| | | customerId: number; |
| | | receiverName: string; |
| | | receiverMobile: string; |
| | | areaId?: number; |
| | | detailAddress: string; |
| | | defaultFlag?: boolean; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å®¢æ·æ¶è´§å°åå表 */ |
| | | export function getCustomerAddressList(customerId: number) { |
| | | return requestClient.get<CrmCustomerAddressApi.CustomerAddress[]>( |
| | | '/crm/customer-address/list', |
| | | { params: { customerId } }, |
| | | ); |
| | | } |
| | | |
| | | /** åå»ºå®¢æ·æ¶è´§å°å */ |
| | | export function createCustomerAddress( |
| | | data: CrmCustomerAddressApi.CustomerAddress, |
| | | ) { |
| | | return requestClient.post<number>('/crm/customer-address/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹å®¢æ·æ¶è´§å°å */ |
| | | export function updateCustomerAddress( |
| | | data: CrmCustomerAddressApi.CustomerAddress, |
| | | ) { |
| | | return requestClient.put('/crm/customer-address/update', data); |
| | | } |
| | | |
| | | /** å é¤å®¢æ·æ¶è´§å°å */ |
| | | export function deleteCustomerAddress(id: number) { |
| | | return requestClient.delete(`/crm/customer-address/delete?id=${id}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmFinanceReportApi { |
| | | /** 客æ·å¾æ¥-æ¥è¯¢åæ° */ |
| | | export interface CustomerStatementParams { |
| | | beginTime?: string; |
| | | customerId: number; |
| | | endTime?: string; |
| | | includeUnapprovedOrder?: boolean; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥-æ±æ»å表æ¥è¯¢åæ° */ |
| | | export interface CustomerStatementSummaryParams { |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | includeUnapprovedOrder?: boolean; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥æ±æ»ï¼å·¦ä¾§å®¢æ·æ»è§ï¼ */ |
| | | export interface CustomerStatementSummary { |
| | | customerId: number; |
| | | customerName: string; |
| | | orderCount: number; |
| | | invoiceCount: number; |
| | | receiptCount: number; |
| | | totalOrderAmount: number; |
| | | totalInvoiceAmount: number; |
| | | totalCollectedAmount: number; |
| | | uncollectedAmount: number; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥å¯¹è´¦å */ |
| | | export interface CustomerStatement { |
| | | customerId: number; |
| | | customerName: string; |
| | | orderCount: number; |
| | | orderTotalAmount: number; |
| | | shippedAmount: number; |
| | | invoicedAmount: number; |
| | | collectedAmount: number; |
| | | shippedNotInvoicedAmount: number; |
| | | outstandingAmount: number; |
| | | nextMonthPlanAmount: number; |
| | | orders: CustomerStatementOrderRow[]; |
| | | invoices: CustomerStatementInvoiceRow[]; |
| | | receipts: CustomerStatementReceiptRow[]; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥å¯¹è´¦å-订åè¡ */ |
| | | export interface CustomerStatementOrderRow { |
| | | saleOrderId: number; |
| | | saleOrderNo: string; |
| | | contractId?: number; |
| | | orderTime?: string; |
| | | orderAmount: number; |
| | | shippedAmount: number; |
| | | invoicedAmount: number; |
| | | collectedAmount: number; |
| | | shippedNotInvoicedAmount: number; |
| | | uninvoicedAmount: number; |
| | | outstandingAmount: number; |
| | | status?: number; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥å¯¹è´¦å-å¼ç¥¨è¡ */ |
| | | export interface CustomerStatementInvoiceRow { |
| | | id: number; |
| | | no: string; |
| | | invoiceNo?: string; |
| | | invoiceTime?: string; |
| | | invoiceAmount: number; |
| | | collectedAmount: number; |
| | | remainingReceivable: number; |
| | | saleOrderId?: number; |
| | | saleOrderNo?: string; |
| | | status?: number; |
| | | } |
| | | |
| | | /** 客æ·å¾æ¥å¯¹è´¦å-忬¾è¡ */ |
| | | export interface CustomerStatementReceiptRow { |
| | | id: number; |
| | | no: string; |
| | | returnTime?: string; |
| | | amount: number; |
| | | invoiceId?: number; |
| | | invoiceNo?: string; |
| | | status?: number; |
| | | } |
| | | |
| | | /** å·²åè´§æªå¼ç¥¨æç» */ |
| | | export interface ShippedNotInvoiced { |
| | | saleOutId: number; |
| | | saleOutCode: string; |
| | | salesDate?: string; |
| | | saleOrderId?: number; |
| | | saleOrderNo?: string; |
| | | customerId: number; |
| | | customerName: string; |
| | | outAmount: number; |
| | | invoicedAmount: number; |
| | | remainInvoiceAmount: number; |
| | | } |
| | | |
| | | /** æªæ ¸éåºæ¶æç» */ |
| | | export interface UnwrittenOff { |
| | | invoiceId: number; |
| | | invoiceNo: string; |
| | | taxInvoiceNo?: string; |
| | | customerId: number; |
| | | customerName: string; |
| | | contractId?: number; |
| | | saleOrderId?: number; |
| | | invoiceTime?: string; |
| | | invoiceAmount: number; |
| | | collectedAmount: number; |
| | | remainAmount: number; |
| | | agingDays: number; |
| | | agingBucket: string; |
| | | } |
| | | |
| | | /** è´¢å¡æåº¦è¶å¿ */ |
| | | export interface FinanceTrend { |
| | | month: string; |
| | | invoicedAmount: number; |
| | | collectedAmount: number; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾å客æ·å¾æ¥æ±æ»å表ï¼å·¦ä¾§å®¢æ·æ»è§ï¼ */ |
| | | export function getCustomerStatementSummaryList( |
| | | params?: CrmFinanceReportApi.CustomerStatementSummaryParams, |
| | | ) { |
| | | return requestClient.get<CrmFinanceReportApi.CustomerStatementSummary[]>( |
| | | '/crm/finance/report/customer-statement/summary-list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾å®¢æ·å¾æ¥å¯¹è´¦æ°æ® */ |
| | | export function getCustomerStatement( |
| | | params: CrmFinanceReportApi.CustomerStatementParams, |
| | | ) { |
| | | return requestClient.get<CrmFinanceReportApi.CustomerStatement>( |
| | | '/crm/finance/report/customer-statement', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾å·²åè´§æªå¼ç¥¨æç» */ |
| | | export function getShippedNotInvoicedList(customerId?: number) { |
| | | return requestClient.get<CrmFinanceReportApi.ShippedNotInvoiced[]>( |
| | | '/crm/finance/report/shipped-not-invoiced', |
| | | { params: { customerId } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾æªæ ¸éåºæ¶æç» */ |
| | | export function getUnwrittenOffList(customerId?: number) { |
| | | return requestClient.get<CrmFinanceReportApi.UnwrittenOff[]>( |
| | | '/crm/finance/report/unwritten-off', |
| | | { params: { customerId } }, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾è´¢å¡æåº¦è¶å¿ */ |
| | | export function getFinanceMonthlyTrend(months = 6) { |
| | | return requestClient.get<CrmFinanceReportApi.FinanceTrend[]>( |
| | | '/crm/finance/report/monthly-trend', |
| | | { params: { months } }, |
| | | ); |
| | | } |
| | |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | | saleOrderId?: number; |
| | | saleOrderNo?: string; |
| | | saleOutId?: number; |
| | | saleOutCode?: string; |
| | | contractAmount?: number; |
| | | prepaymentAmount?: number; |
| | | deliveryAmount?: number; |
| | | acceptanceAmount?: number; |
| | | paymentNodes?: PaymentNode[]; |
| | | entryStatus?: number; |
| | | invoiceTime?: string; |
| | | auditStatus?: number; |
| | | remark?: string; |
| | |
| | | creatorName?: string; |
| | | createTime?: string; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | /** 款项æ¶é´èç¹ */ |
| | | export interface PaymentNode { |
| | | id?: number; |
| | | /** é¶æ®µï¼1å忬¾ 2é¢ä»æ¬¾ 3å°è´§æ¬¾ 4éªæ¶æ¬¾ 5è´¨ä¿é */ |
| | | stage: number; |
| | | amount?: number; |
| | | returnTime?: string; |
| | | receiptedPrice?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å¼ç¥¨å¯éåºåºå */ |
| | | export interface SaleOut { |
| | | id: number; |
| | | code: string; |
| | | saleOrderId?: number; |
| | | salesOrderCode?: string; |
| | | salesDate?: string; |
| | | status?: number; |
| | | statusName?: string; |
| | | totalPrice?: number; |
| | | invoicedPrice?: number; |
| | | remainInvoicePrice?: number; |
| | | } |
| | | |
| | | /** å¼ç¥¨ç®è¦ä¿¡æ¯ï¼ç¨äºå款å
³èéæ©ï¼ */ |
| | |
| | | file, |
| | | }); |
| | | } |
| | | |
| | | /** è·å¾å¼ç¥¨å¯éçéå®åºåºåï¼æéå®è®¢åï¼ */ |
| | | export function getInvoiceSaleOutList(saleOrderId: number, id?: number) { |
| | | return requestClient.get<CrmInvoiceApi.SaleOut[]>( |
| | | '/crm/invoice/sale-out-list', |
| | | { params: { saleOrderId, id } }, |
| | | ); |
| | | } |
| | | |
| | | /** å¼ç¥¨ç³è¯·å½å
¥å票ï¼å®¡æ¹éè¿åå¡«åçå®å票信æ¯ä¸æ¬¾é¡¹æ¶é´èç¹ï¼ */ |
| | | export function entryInvoice(data: { |
| | | id: number; |
| | | invoiceNo: string; |
| | | invoiceTitle?: string; |
| | | invoiceTime: string; |
| | | blobIds?: number[]; |
| | | paymentNodes?: CrmInvoiceApi.PaymentNode[]; |
| | | remark?: string; |
| | | }) { |
| | | return requestClient.put('/crm/invoice/entry', data); |
| | | } |
| | |
| | | invoiceId?: number; |
| | | invoice?: InvoiceInfo; |
| | | auditStatus: number; |
| | | processInstanceId: number; |
| | | processInstanceId: string; |
| | | returnTime: Date; |
| | | returnType: number; |
| | | price: number; |
| | |
| | | return requestClient.download('/crm/receivable/export-excel', { params }); |
| | | } |
| | | |
| | | /** æäº¤åæ¬¾å®¡æ¹ */ |
| | | export function submitReceivable(id: number, remark?: string) { |
| | | return requestClient.put('/crm/receivable/submit', null, { |
| | | params: { id, remark }, |
| | | }); |
| | | } |
| | | |
| | | /** æ´æ°å款çç¶æï¼å®¡æ ¸/åå®¡æ ¸ï¼ */ |
| | | export function updateReceivableStatus(id: number, status: number) { |
| | | return requestClient.put('/crm/receivable/update-status', null, { |
| | |
| | | items?: SaleQuotationItem[]; |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ApproveProcess { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | version: number; |
| | | } |
| | | |
| | | /** æ¥ä»·åç©ææç» */ |
| | | export interface SaleQuotationItem { |
| | | id: number; |
| | |
| | | return requestClient.post<number>('/crm/sale-quotation/convert-contract', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤æ¥ä»·åå®¡æ¹ */ |
| | | export function submitSaleQuotation(id: number, processDefinitionKey: string) { |
| | | return requestClient.put('/crm/sale-quotation/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åæ¥ä»·åå®¡æ¹æµç¨å表 */ |
| | | export function getSaleQuotationApproveProcessList() { |
| | | return requestClient.get<CrmSaleQuotationApi.ApproveProcess[]>( |
| | | '/crm/sale-quotation/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å¸¦ç« æ¥ä»·å Word */ |
| | | export function downloadSaleQuotationDocument(id: number) { |
| | | return requestClient.download('/crm/sale-quotation/export-word', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | |
| | | supplierName?: string; |
| | | purchaseOrderId?: number; |
| | | order?: PurchaseOrder; |
| | | itemReceiptId?: number; |
| | | itemReceiptCode?: string; |
| | | invoiceNo?: string; |
| | | invoiceTitle?: string; |
| | | price?: number; |
| | |
| | | creatorName?: string; |
| | | createTime?: string; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | /** æ¥ç¥¨å¯éå
¥åºå */ |
| | | export interface ItemReceipt { |
| | | id: number; |
| | | code: string; |
| | | name?: string; |
| | | receiptDate?: string; |
| | | status?: number; |
| | | totalPrice?: number; |
| | | invoicedPrice?: number; |
| | | remainInvoicePrice?: number; |
| | | } |
| | | |
| | | /** æ¥ç¥¨ç®è¦ä¿¡æ¯ï¼ç¨äºä»æ¬¾å
³èéæ©ï¼ */ |
| | |
| | | return requestClient.download('/erp/purchase-invoice/export-excel', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ç¥¨å¯éå
¥åºååè¡¨ï¼æéè´è®¢åè¿æ»¤ï¼è¿åæªåæ¶å
¥åºåå坿¥ç¥¨éé¢ï¼ */ |
| | | export function getItemReceiptList(purchaseOrderId: number, id?: number) { |
| | | return requestClient.get<ErpPurchaseInvoiceApi.ItemReceipt[]>( |
| | | '/erp/purchase-invoice/item-receipt-list', |
| | | { params: { purchaseOrderId, id } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥ç¥¨ä¸æåè¡¨ï¼æä¾åºåå¯éæ¥ç¥¨ï¼å·²ä¸ä¼ å票éä»¶ä¸å©ä½å¯ä»éé¢å¤§äº 0ï¼ */ |
| | | export function getPurchaseInvoiceSimpleList( |
| | | supplierId: number, |
| | |
| | | mealAllowance?: number; |
| | | transportAllowance?: number; |
| | | otherAllowance?: number; |
| | | /** æ¥å·¥å·¥èµï¼æç产æ¥å·¥æ±æ»ï¼å¯äººå·¥è°æ´ï¼ */ |
| | | workWage?: number; |
| | | socialSecurityDeduction?: number; |
| | | housingFundDeduction?: number; |
| | | taxDeduction?: number; |
| | |
| | | period: string; |
| | | deptId?: number; |
| | | } |
| | | |
| | | /** æ¥å·¥å·¥èµå段æç» */ |
| | | export interface WorkWageField { |
| | | code?: string; |
| | | name?: string; |
| | | /** 计ç®è§è²ï¼1-ç³»æ° 2-å åéé¢ 3-计价æ°é 4-ä»
è®°å½ */ |
| | | role?: number; |
| | | value?: string; |
| | | unitPrice?: number; |
| | | /** æ¬å段计å
¥å·¥èµçéé¢ */ |
| | | amount?: number; |
| | | } |
| | | |
| | | /** æ¥å·¥å·¥èµæç»ï¼ä¸æ¡æ¥å·¥åï¼ */ |
| | | export interface WorkWageDetail { |
| | | feedbackId?: number; |
| | | feedbackNo?: string; |
| | | processId?: number; |
| | | processName?: string; |
| | | feedbackTime?: string; |
| | | status?: number; |
| | | billingType?: number; |
| | | workHours?: number; |
| | | feedbackQuantity?: number; |
| | | qualifiedQuantity?: number; |
| | | mainWage?: number; |
| | | auxWage?: number; |
| | | /** èªå®ä¹å·¥èµåæ®µè°æ´å计 */ |
| | | fieldWage?: number; |
| | | preQualityWage?: number; |
| | | /** è´¨éè°æ´ï¼æ£æ¬¾ä¸ºè´ï¼ */ |
| | | qualityAdjustment?: number; |
| | | finalWage?: number; |
| | | fields?: WorkWageField[]; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢èªé
¬æ ¸ç®å页å表 */ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢åå·¥ææ ¸ç®å¨æçæ¥å·¥å·¥èµæç» */ |
| | | export function getWorkWageDetail(params: { period: string; userId: number }) { |
| | | return requestClient.get<HrmSalaryCalculationApi.WorkWageDetail[]>( |
| | | '/hrm/salary/calculation/work-wage-detail', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¹éä¿åèªé
¬æ ¸ç®è®°å½ */ |
| | | export function saveSalaryCalculation(data: HrmSalaryCalculationApi.SalarySaveParam) { |
| | | return requestClient.post('/hrm/salary/calculation/save', data); |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmEmployeeSocialSecurityApi { |
| | | /** å工社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | /** é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export interface EmployeeSocialSecurity { |
| | | id?: number; |
| | | userId?: number; |
| | | userName?: string; |
| | | deptId?: number; |
| | | deptName?: string; |
| | | socialSecuritySchemeId?: number; |
| | | schemeName?: string; |
| | |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å工社ä¿å
¬ç§¯éæ¡£æ¡æ¥è¯¢åæ° */ |
| | | /** é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡æ¥è¯¢åæ° */ |
| | | export interface EmployeeSocialSecurityPageParam extends PageParam { |
| | | userName?: string; |
| | | deptId?: number; |
| | | status?: number; |
| | | effectiveDate?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·åå工社ä¿å
¬ç§¯éæ¡£æ¡å页å表 */ |
| | | /** è·åé¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡å页å表 */ |
| | | export function getEmployeeSocialSecurityPage( |
| | | params: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurityPageParam, |
| | | ) { |
| | |
| | | ); |
| | | } |
| | | |
| | | /** è·åå工社ä¿å
¬ç§¯éæ¡£æ¡è¯¦æ
*/ |
| | | /** è·åé¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡è¯¦æ
*/ |
| | | export function getEmployeeSocialSecurity(id: number) { |
| | | return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>( |
| | | `/hrm/salary/employee-social-security/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·åå工社ä¿å
¬ç§¯éæ¡£æ¡åå²å表 */ |
| | | export function getEmployeeSocialSecurityListByUser(userId: number) { |
| | | /** è·åé¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡åå²å表 */ |
| | | export function getEmployeeSocialSecurityListByDept(deptId: number) { |
| | | return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>( |
| | | `/hrm/salary/employee-social-security/list-by-user?userId=${userId}`, |
| | | `/hrm/salary/employee-social-security/list-by-dept?deptId=${deptId}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååå·¥ææ°çæç社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export function getLatestEmployeeSocialSecurity(userId: number) { |
| | | /** è·åé¨é¨ææ°çæç社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export function getLatestEmployeeSocialSecurity(deptId: number) { |
| | | return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>( |
| | | `/hrm/salary/employee-social-security/get-latest?userId=${userId}`, |
| | | `/hrm/salary/employee-social-security/get-latest?deptId=${deptId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å建å工社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | /** å建é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export function createEmployeeSocialSecurity( |
| | | data: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity, |
| | | ) { |
| | | return requestClient.post('/hrm/salary/employee-social-security/create', data); |
| | | } |
| | | |
| | | /** æ´æ°å工社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | /** æ´æ°é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export function updateEmployeeSocialSecurity( |
| | | data: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity, |
| | | ) { |
| | | return requestClient.put('/hrm/salary/employee-social-security/update', data); |
| | | } |
| | | |
| | | /** å é¤å工社ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | /** å é¤é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ */ |
| | | export function deleteEmployeeSocialSecurity(id: number) { |
| | | return requestClient.delete(`/hrm/salary/employee-social-security/delete?id=${id}`); |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è®¾å¤ç²¾ç®å表 */ |
| | | export function getMachinerySimpleList() { |
| | | /** æ¥è¯¢è®¾å¤ç²¾ç®å表ï¼ä¼ workstationId æ¶åªè¿å该工ä½ç«ç»å®çè®¾å¤ */ |
| | | export function getMachinerySimpleList(workstationId?: number) { |
| | | return requestClient.get<MesDvMachineryApi.Machinery[]>( |
| | | '/mes/dv/machinery/simple-list', |
| | | { params: workstationId ? { workstationId } : undefined }, |
| | | ); |
| | | } |
| | | |
| | |
| | | return requestClient.download('/mes/pd/project/export-excel', { params }); |
| | | } |
| | | |
| | | /** 导åºé¡¹ç®è·è¸ªè¡¨ Wordï¼è¡¨å¤´èªå¨å¡«å
ï¼è·è¸ªè®°å½çç©ºå¯æå¡«ï¼ */ |
| | | export function exportPdProjectTrackingWord(id: number) { |
| | | return requestClient.download('/mes/pd/project/export-word', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åå¸ï¼å¾
åå¸âå¾
ä¸ä¼ ï¼ */ |
| | | export function publishPdProject(id: number) { |
| | | return requestClient.put('/mes/pd/project/publish', undefined, { |
| | |
| | | otherScrapQuantity?: number; // å
¶ä»åºåæ°é |
| | | feedbackUserId?: number; // æ¥å·¥ç¨æ·ç¼å· |
| | | feedbackUserNickname?: string; // æ¥å·¥äººæµç§° |
| | | billingType?: number; // 计费类åå¿«ç
§ï¼1-è®¡æ¶ 2-计件 |
| | | wageRate?: number; // å·¥èµå®é¢å¿«ç
§ï¼è®¡æ¶å
/å°æ¶ï¼è®¡ä»¶å
/ä»¶ |
| | | workHours?: number; // æ¬æ¬¡æ¥å·¥å·¥æ¶ï¼å°æ¶ï¼ï¼è®¡æ¶è®¡èªå¿
å¡« |
| | | wageAmount?: number; // æ¬æ¬¡æ¥å·¥å·¥èµ |
| | | machineryId?: number; // æ¬æ¬¡æ¥å·¥ä¸»ç¨è®¾å¤ç¼å· |
| | | machineryName?: string; // 主ç¨è®¾å¤åç§° |
| | | wageRuleId?: number; // å½ä¸çå·¥åºå·¥èµåä»·è§åç¼å· |
| | | auxPaidHours?: number; // è¾
å©è®¡èªå·¥æ¶ï¼å°æ¶ï¼ |
| | | auxWageAmount?: number; // æ¬æ¬¡è¾
å©å·¥èµ |
| | | qualityPassRate?: number; // è¿ç¨æ£éªåæ ¼çï¼%ï¼ |
| | | qualityCoefficient?: number; // è´¨éç³»æ° |
| | | qualityDeduction?: number; // è´¨éæ£æ¬¾éé¢ |
| | | wageBreakdown?: string; // å·¥èµè®¡ç®æç»å¿«ç
§ï¼JSONï¼ |
| | | productionLength?: number; // æ¬æ¬¡ç产é¿åº¦ï¼ç±³ï¼ï¼ä»
ç¨äºè®¡ç®æ åå·¥æ¶ |
| | | standardWorkHours?: number; // æ åå·¥æ¶ï¼å°æ¶ï¼ï¼åªç¨äºèæ ¸å¯¹æ¯ |
| | | standardPrimaryHours?: number; // æ åå·¥æ¶-主工æ¶ï¼å°æ¶ï¼ |
| | | standardAuxHours?: number; // æ åå·¥æ¶-è¾
å©å·¥æ¶ï¼å°æ¶ï¼ |
| | | stdCalcNote?: string; // æ åå·¥æ¶è®¡ç®ä¾æ® |
| | | workHoursDiff?: number; // å·¥æ¶å·®å¼ = å®é
- æ å |
| | | workHoursDiffRate?: number; // å·¥æ¶å·®å¼çï¼%ï¼ |
| | | standardWarnFlag?: boolean; // æ¯å¦è¶
åºå·¥æ¶èæ ¸éå¼ |
| | | standardWarnMessage?: string; // å·¥æ¶èæ ¸æç¤º |
| | | auxList?: AuxValue[]; // è¾
å©å·¥æ¶å½å
¥æç» |
| | | approveUserId?: number; // å®¡æ ¸ç¨æ·ç¼å· |
| | | approveUserNickname?: string; // å®¡æ ¸äººæµç§° |
| | | status?: number; // ç¶æ |
| | |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ç¼å· |
| | | processDefinitionKey?: string; // BPM æµç¨å®ä¹ Key |
| | | paramValues?: ParamValue[]; |
| | | } |
| | | |
| | | export interface AuxValue { |
| | | auxItemId: number; |
| | | quantity?: number; |
| | | materialFrom?: string; |
| | | materialTo?: string; |
| | | remark?: string; |
| | | // ---- 忾忮µ ---- |
| | | itemName?: string; |
| | | measureType?: number; |
| | | minutes?: number; |
| | | } |
| | | |
| | | export interface ParamValue { |
| | |
| | | taskId: number; |
| | | paramValues?: MesProFeedbackApi.ParamValue[]; |
| | | feedbackQuantity: number; |
| | | /** æ¬æ¬¡æ¥å·¥å·¥æ¶ï¼å°æ¶ï¼ï¼è®¡æ¶è®¡èªçå·¥åºå¿
å¡« */ |
| | | workHours?: number; |
| | | feedbackTime?: string; |
| | | remark?: string; |
| | | } |
| | |
| | | mergeFlag?: number; // æ¯å¦åå¹¶ä¸åï¼0-å¦ï¼1-æ¯ï¼ |
| | | mergeBatchNo?: string; // åå¹¶æ¹æ¬¡å· |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // 计åå建人ï¼éå®è®¢å计ååéå®è®¢ååå»ºäººï¼ |
| | | canIssue?: boolean; // å½åç»å½ç¨æ·æ¯å¦å¯ä¸å |
| | | createTime?: number; // å建æ¶é´ |
| | | } |
| | | |
| | |
| | | code?: string; |
| | | name?: string; |
| | | attention?: string; |
| | | /** 计费类åï¼1-è®¡æ¶ 2-计件 */ |
| | | billingType?: number; |
| | | /** å·¥èµå®é¢ï¼è®¡æ¶ä¸ºå
/å°æ¶ï¼è®¡ä»¶ä¸ºå
/ä»¶ */ |
| | | wageRate?: number; |
| | | /** è¾
å©å·¥æ¶å°æ¶å·¥èµï¼å
/å°æ¶ï¼ï¼è¾
å©é¡¹æå·¥åºè®¡èªæ¶çåä»· */ |
| | | auxHourlyRate?: number; |
| | | /** æ åå·¥æ¶è®¡ç®æ¹å¼ï¼0-æªå¯ç¨ 1-转éÃèè· 2-线é度ç´å 3-满éç¾åæ¯Ãèè· */ |
| | | stdTimeMode?: number; |
| | | /** æ åå·¥æ¶æç®ç³»æ°ï¼æä¸ 1.8ï¼å
¶ä½ 1ï¼ */ |
| | | stdTimeFactor?: number; |
| | | /** æºå°æ»¡è½¬éï¼r/minï¼ */ |
| | | maxRpm?: number; |
| | | /** 转éåæ°ç¼ç */ |
| | | stdRpmParamCode?: string; |
| | | /** èè·åæ°ç¼ç */ |
| | | stdPitchParamCode?: string; |
| | | /** 线é度ï¼ç±³/åï¼åæ°ç¼ç */ |
| | | stdSpeedParamCode?: string; |
| | | /** 转éç¾åæ¯åæ°ç¼ç */ |
| | | stdPercentParamCode?: string; |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: Date; |
| | |
| | | |
| | | /** æ°å¢çäº§å·¥åº */ |
| | | export function createProcess(data: MesProProcessApi.Process) { |
| | | return requestClient.post('/mes/pro/process/create', data); |
| | | return requestClient.post<number>('/mes/pro/process/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹çäº§å·¥åº */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWageApi { |
| | | /** å·¥åºå·¥èµå段 */ |
| | | export interface WageField { |
| | | id?: number; |
| | | processId?: number; |
| | | /** åæ®µç¼ç */ |
| | | fieldCode?: string; |
| | | /** åæ®µåç§° */ |
| | | fieldName?: string; |
| | | /** åæ®µç±»åï¼1-æ°å¼ 2-ææ¬ 3-䏿 4-å¸å° */ |
| | | fieldType?: number; |
| | | /** åä½ */ |
| | | unit?: string; |
| | | /** å·¥åºçº§åºå®å¼ */ |
| | | fieldValue?: string; |
| | | /** 计ç®è§è²ï¼1-ç³»æ° 2-å åéé¢ 3-计价æ°é 4-ä»
è®°å½ */ |
| | | calcRole?: number; |
| | | /** 计价æ°éåä»· */ |
| | | unitPrice?: number; |
| | | sort?: number; |
| | | /** ç¶æï¼0-å¯ç¨ 1-ç¦ç¨ */ |
| | | status?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å·¥åºå·¥èµåä»·è§å */ |
| | | export interface WageRule { |
| | | id?: number; |
| | | processId?: number; |
| | | /** å·¥èºè·¯çº¿ç¼å·ï¼ç©º=ä¸é */ |
| | | routeId?: number; |
| | | /** 设å¤ç¼å·ï¼ç©º=ä¸é */ |
| | | machineryId?: number; |
| | | /** 计费类åï¼1-è®¡æ¶ 2-计件 */ |
| | | billingType?: number; |
| | | /** å·¥èµåä»·ï¼è®¡æ¶=å
/å°æ¶ï¼è®¡ä»¶=å
/ä»¶ */ |
| | | wageRate?: number; |
| | | /** çæå¼å§æ¥æ */ |
| | | effectiveFrom?: string; |
| | | /** çæç»ææ¥æ */ |
| | | effectiveTo?: string; |
| | | /** ä¼å
级ï¼è¶å¤§è¶ä¼å
*/ |
| | | priority?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** è´¨éå·¥èµè§å */ |
| | | export interface QualityRule { |
| | | id?: number; |
| | | processId?: number; |
| | | /** å·¥èºè·¯çº¿ç¼å·ï¼ç©º=ä¸é */ |
| | | routeId?: number; |
| | | /** è§åç±»åï¼1-åæ ¼çç³»æ° 2-ä¸åæ ¼æ£æ¬¾ */ |
| | | ruleType?: number; |
| | | /** åæ ¼çä¸éï¼%ï¼å«ï¼ */ |
| | | minRate?: number; |
| | | /** åæ ¼çä¸éï¼%ï¼ä¸å«ï¼ */ |
| | | maxRate?: number; |
| | | /** å·¥èµç³»æ°ï¼è§åç±»å=1ï¼ */ |
| | | coefficient?: number; |
| | | /** æ¯ä»¶ä¸åæ ¼æ£æ¬¾éé¢ï¼è§åç±»å=2ï¼ */ |
| | | deductionPerUnit?: number; |
| | | priority?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å·¥åºå·¥èµå段å表 */ |
| | | export function getWageFieldList(processId: number) { |
| | | return requestClient.get<MesProWageApi.WageField[]>( |
| | | '/mes/pro/process-wage-field/list', |
| | | { params: { processId } }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åå·¥åºå·¥èµå段ï¼å
¨éè¦çï¼ */ |
| | | export function saveWageFieldList( |
| | | processId: number, |
| | | list: MesProWageApi.WageField[], |
| | | ) { |
| | | return requestClient.post('/mes/pro/process-wage-field/save-list', { |
| | | processId, |
| | | list, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢å·¥åºå·¥èµåä»·è§åå表 */ |
| | | export function getWageRuleList(processId: number) { |
| | | return requestClient.get<MesProWageApi.WageRule[]>( |
| | | '/mes/pro/process-wage-rule/list', |
| | | { params: { processId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢å·¥åºå·¥èµåä»·è§å */ |
| | | export function createWageRule(data: MesProWageApi.WageRule) { |
| | | return requestClient.post('/mes/pro/process-wage-rule/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹å·¥åºå·¥èµåä»·è§å */ |
| | | export function updateWageRule(data: MesProWageApi.WageRule) { |
| | | return requestClient.put('/mes/pro/process-wage-rule/update', data); |
| | | } |
| | | |
| | | /** å é¤å·¥åºå·¥èµåä»·è§å */ |
| | | export function deleteWageRule(id: number) { |
| | | return requestClient.delete(`/mes/pro/process-wage-rule/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨éå·¥èµè§åå表 */ |
| | | export function getQualityRuleList(processId: number) { |
| | | return requestClient.get<MesProWageApi.QualityRule[]>( |
| | | '/mes/pro/quality-wage-rule/list', |
| | | { params: { processId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è´¨éå·¥èµè§å */ |
| | | export function createQualityRule(data: MesProWageApi.QualityRule) { |
| | | return requestClient.post('/mes/pro/quality-wage-rule/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¨éå·¥èµè§å */ |
| | | export function updateQualityRule(data: MesProWageApi.QualityRule) { |
| | | return requestClient.put('/mes/pro/quality-wage-rule/update', data); |
| | | } |
| | | |
| | | /** å é¤è´¨éå·¥èµè§å */ |
| | | export function deleteQualityRule(id: number) { |
| | | return requestClient.delete(`/mes/pro/quality-wage-rule/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æ¥å·¥å·¥èµè¯ç®ï¼ä¸è½åºï¼ */ |
| | | export namespace MesProFeedbackWagePreviewApi { |
| | | export interface FieldSnapshot { |
| | | code?: string; |
| | | name?: string; |
| | | role?: number; |
| | | value?: string; |
| | | unitPrice?: number; |
| | | amount?: number; |
| | | } |
| | | |
| | | export interface Quality { |
| | | passRate?: number; |
| | | coefficient?: number; |
| | | deduction?: number; |
| | | } |
| | | |
| | | export interface PreviewReq { |
| | | taskId: number; |
| | | machineryId?: number; |
| | | workHours?: number; |
| | | feedbackQuantity?: number; |
| | | qualifiedQuantity?: number; |
| | | productionLength?: number; |
| | | feedbackDate?: string; |
| | | auxList?: MesProFeedbackApi.AuxValue[]; |
| | | } |
| | | |
| | | export interface PreviewResp { |
| | | billingType?: number; |
| | | wageRate?: number; |
| | | wageRuleId?: number; |
| | | matchLevel?: string; |
| | | workHours?: number; |
| | | pieceQuantity?: number; |
| | | mainWage?: number; |
| | | auxPaidHours?: number; |
| | | auxWage?: number; |
| | | fields?: FieldSnapshot[]; |
| | | preQualityWage?: number; |
| | | quality?: Quality; |
| | | finalWage?: number; |
| | | warnings?: string[]; |
| | | } |
| | | } |
| | | |
| | | /** æ¥å·¥å·¥èµè¯ç® */ |
| | | export function previewFeedbackWage(data: MesProFeedbackWagePreviewApi.PreviewReq) { |
| | | return requestClient.post<MesProFeedbackWagePreviewApi.PreviewResp>( |
| | | '/mes/pro/feedback/preview-wage', |
| | | data, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWorktimeApi { |
| | | /** è¾
å©å·¥æ¶é¡¹ç® */ |
| | | export interface AuxItem { |
| | | id?: number; |
| | | processId?: number; |
| | | processName?: string; |
| | | workstationId?: number; |
| | | workstationName?: string; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | /** è®¡éæ¹å¼ï¼1-ææ¬¡ 2-æä¸ª 3-æç 4-æç±³æç® */ |
| | | measureType?: number; |
| | | /** è®¡èªæ¹å¼ï¼0-ä¸è®¡èª 1-æå·¥åºè¾
å©å°æ¶å·¥èµ 2-ææ¬é¡¹åä»· */ |
| | | wageMode?: number; |
| | | /** 计èªåä»·ï¼å
/å°æ¶ï¼ï¼è®¡èªæ¹å¼=2 æ¶ä½¿ç¨ */ |
| | | wageRate?: number; |
| | | durationMinutes?: number; |
| | | speedMPerMin?: number; |
| | | materialFrom?: string; |
| | | materialTo?: string; |
| | | status?: number; |
| | | sort?: number; |
| | | remark?: string; |
| | | createTime?: Date; |
| | | } |
| | | |
| | | /** è¾
å©å·¥æ¶è¯ç®å
¥å */ |
| | | export interface AuxInput { |
| | | auxItemId: number; |
| | | quantity?: number; |
| | | materialFrom?: string; |
| | | materialTo?: string; |
| | | remark?: string; |
| | | } |
| | | |
| | | export interface CalcReq { |
| | | taskId: number; |
| | | productionLength?: number; |
| | | feedbackQuantity?: number; |
| | | auxList?: AuxInput[]; |
| | | } |
| | | |
| | | export interface AuxResult { |
| | | auxItemId?: number; |
| | | itemName?: string; |
| | | measureType?: number; |
| | | quantity?: number; |
| | | materialFrom?: string; |
| | | materialTo?: string; |
| | | minutes?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | export interface CalcResp { |
| | | productionLength?: number; |
| | | standardWorkHours?: number; |
| | | primaryHours?: number; |
| | | auxHours?: number; |
| | | note?: string; |
| | | missingReasons?: string[]; |
| | | auxList?: AuxResult[]; |
| | | } |
| | | |
| | | export interface ReportRow { |
| | | dimKey?: string; |
| | | dimName?: string; |
| | | feedbackCount?: number; |
| | | productionLength?: number; |
| | | actualHours?: number; |
| | | standardHours?: number; |
| | | workHoursDiff?: number; |
| | | workHoursDiffRate?: number; |
| | | wageAmount?: number; |
| | | } |
| | | |
| | | export interface ReportResp { |
| | | summary?: ReportRow; |
| | | rows?: ReportRow[]; |
| | | } |
| | | |
| | | export interface ReportParams { |
| | | beginTime: string; |
| | | endTime: string; |
| | | /** 1-æäºº 2-æå·¥åº 3-ææºå° 4-ææ¥æ */ |
| | | dimType: number; |
| | | feedbackUserId?: number; |
| | | processId?: number; |
| | | workstationId?: number; |
| | | } |
| | | |
| | | export interface PageParams extends PageParam { |
| | | processId?: number; |
| | | workstationId?: number; |
| | | itemName?: string; |
| | | status?: number; |
| | | } |
| | | } |
| | | |
| | | /** è¾
å©å·¥æ¶é¡¹ç®å页 */ |
| | | export function getWorktimeAuxPage(params: MesProWorktimeApi.PageParams) { |
| | | return requestClient.get<PageResult<MesProWorktimeApi.AuxItem>>( |
| | | '/mes/pro/worktime-aux/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è¾
å©å·¥æ¶é¡¹ç®è¯¦æ
*/ |
| | | export function getWorktimeAux(id: number) { |
| | | return requestClient.get<MesProWorktimeApi.AuxItem>( |
| | | `/mes/pro/worktime-aux/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å·¥åº + æºå°å¯ç¨çè¾
å©å·¥æ¶é¡¹ç® */ |
| | | export function getWorktimeAuxApplicable( |
| | | processId: number, |
| | | workstationId?: number, |
| | | ) { |
| | | return requestClient.get<MesProWorktimeApi.AuxItem[]>( |
| | | '/mes/pro/worktime-aux/list-applicable', |
| | | { params: { processId, workstationId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è¾
å©å·¥æ¶é¡¹ç® */ |
| | | export function createWorktimeAux(data: MesProWorktimeApi.AuxItem) { |
| | | return requestClient.post<number>('/mes/pro/worktime-aux/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è¾
å©å·¥æ¶é¡¹ç® */ |
| | | export function updateWorktimeAux(data: MesProWorktimeApi.AuxItem) { |
| | | return requestClient.put('/mes/pro/worktime-aux/update', data); |
| | | } |
| | | |
| | | /** å é¤è¾
å©å·¥æ¶é¡¹ç® */ |
| | | export function deleteWorktimeAux(id: number) { |
| | | return requestClient.delete(`/mes/pro/worktime-aux/delete?id=${id}`); |
| | | } |
| | | |
| | | /** è¯ç®æ åå·¥æ¶ï¼ä¸è½åºï¼ */ |
| | | export function calculateWorktime(data: MesProWorktimeApi.CalcReq) { |
| | | return requestClient.post<MesProWorktimeApi.CalcResp>( |
| | | '/mes/pro/worktime/calculate', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** å·¥æ¶èæ ¸æ¥è¡¨ */ |
| | | export function getWorktimeReport(params: MesProWorktimeApi.ReportParams) { |
| | | return requestClient.get<MesProWorktimeApi.ReportResp>( |
| | | '/mes/pro/worktime/report', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | createList?: string[]; // æ°å¢æåçæ£éªåç¼å· |
| | | failureList?: Record<string, string>; // 导å
¥å¤±è´¥æç»ï¼key ä¸ºãæ£éªåç¼å· Xãæã第 N è¡ã |
| | | } |
| | | |
| | | /** å·¥åºé»è®¤è´¨æ£æ¹æ¡åæ£æµææ */ |
| | | export interface TemplateResolve { |
| | | processId?: number; // å·¥åºç¼å· |
| | | qcType?: number; // è´¨æ£ç§ç±» |
| | | templateId?: number; // è´¨æ£æ¹æ¡ç¼å·ï¼ç©º=è¯¥å·¥åºæªç»å®æ¹æ¡ |
| | | templateCode?: string; // è´¨æ£æ¹æ¡ç¼ç |
| | | templateName?: string; // è´¨æ£æ¹æ¡åç§° |
| | | indicators?: TemplateIndicator[]; // æ£æµææ åæ®µ |
| | | } |
| | | |
| | | /** æ£æµææ åæ®µ */ |
| | | export interface TemplateIndicator { |
| | | templateIndicatorId?: number; // æ¹æ¡ææ 项ç¼å· |
| | | indicatorId?: number; // æ£æµææ ç¼å· |
| | | indicatorCode?: string; // æ£æµææ ç¼ç |
| | | indicatorName?: string; // æ£æµææ åç§° |
| | | itemType?: number; // ææ é¡¹ç®ç±»å |
| | | resultType?: number; // ç»æå¼ç±»å |
| | | resultSpecification?: string; // ç»æè§æ ¼ |
| | | unitText?: string; // åä½ææ¬ |
| | | tool?: string; // æ£æµå·¥å
· |
| | | checkMethod?: string; // æ£æµæ¹æ³ |
| | | standardValue?: number; // æ åå¼ |
| | | unitMeasureId?: number; // 计éåä½ç¼å· |
| | | thresholdMax?: number; // ä¸é |
| | | thresholdMin?: number; // ä¸é |
| | | } |
| | | |
| | | /** è¿ç¨æ£éªåå·¥èµå段ï¼å·¥åºèªå®ä¹å·¥èµåæ®µï¼ */ |
| | | export interface WageField { |
| | | fieldId?: number; // å·¥åºå·¥èµå段ç¼å· |
| | | fieldCode?: string; // åæ®µç¼ç |
| | | fieldName?: string; // åæ®µåç§° |
| | | fieldType?: number; // åæ®µç±»åï¼1-æ°å¼ 2-ææ¬ 3-䏿 4-å¸å° |
| | | unit?: string; // åä½ |
| | | calcRole?: number; // 计ç®è§è²ï¼1-ç³»æ° 2-å åéé¢ 3-计价æ°é 4-ä»
è®°å½ |
| | | unitPrice?: number; // 计价æ°éåä»· |
| | | sort?: number; // æåº |
| | | defaultValue?: string; // å·¥åºåºå®å¼ï¼æªå¡«åæ¶å
åºï¼ |
| | | value?: string; // è´¨æ£å¡«åçå¼ |
| | | remark?: string; // 夿³¨ |
| | | } |
| | | |
| | | /** å·¥èµå段å¼ä¿å项 */ |
| | | export interface WageFieldValueItem { |
| | | fieldId: number; // å·¥åºå·¥èµå段ç¼å· |
| | | value?: string; // è´¨æ£å¡«åçå¼ |
| | | remark?: string; // 夿³¨ |
| | | } |
| | | } |
| | | |
| | | /** æå·¥åºè·åè¿ç¨æ£éªåå·¥èµåæ®µï¼æ°å»ºæ¶åªä¼ processIdï¼ç¼è¾æ¶åä¼ ipqcId 带åºå·²å¡«åçå¼ï¼ */ |
| | | export function getIpqcWageFieldList(params: { |
| | | ipqcId?: number; |
| | | processId?: number; |
| | | }) { |
| | | return requestClient.get<MesQcIpqcApi.WageField[]>( |
| | | '/mes/qc/ipqc/wage-field/list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åè¿ç¨æ£éªåå·¥èµåæ®µå¼ */ |
| | | export function saveIpqcWageFieldValues( |
| | | ipqcId: number, |
| | | list: MesQcIpqcApi.WageFieldValueItem[], |
| | | ) { |
| | | return requestClient.post('/mes/qc/ipqc/wage-field/save', { |
| | | ipqcId, |
| | | list, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢è¿ç¨æ£éªåå页 */ |
| | |
| | | return requestClient.get<MesQcIpqcApi.Ipqc>(`/mes/qc/ipqc/get?id=${id}`); |
| | | } |
| | | |
| | | /** æå·¥åºè·åé»è®¤è´¨æ£æ¹æ¡åæ£æµææ åæ®µ */ |
| | | export function getTemplateByProcess(processId: number, type?: number) { |
| | | return requestClient.get<MesQcIpqcApi.TemplateResolve>( |
| | | '/mes/qc/ipqc/template-by-process', |
| | | { params: { processId, type } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è¿ç¨æ£éªå */ |
| | | export function createIpqc(data: MesQcIpqcApi.Ipqc) { |
| | | return requestClient.post('/mes/qc/ipqc/create', data); |
| | |
| | | closeDate?: string; |
| | | closeOpinion?: string; |
| | | status?: number; |
| | | processInstanceId?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | |
| | | } |
| | | |
| | | /** å¤ç½®ä¸åæ ¼åå¤çå */ |
| | | export function disposeNcr(data: { handleResult?: string; handlerUserId?: number; id: number }) { |
| | | export function disposeNcr(data: { |
| | | disposition: number; |
| | | handleResult?: string; |
| | | handlerUserId?: number; |
| | | id: number; |
| | | }) { |
| | | return requestClient.put('/mes/qc/ncr/dispose', data); |
| | | } |
| | | |
| | | /** æäº¤ä¸åæ ¼åå¤çåå®¡æ¹ */ |
| | | export function submitNcrApproval(id: number) { |
| | | return requestClient.put('/mes/qc/ncr/submit-approval', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** å
³éä¸åæ ¼åå¤çå */ |
| | | export function closeNcr(data: { closeOpinion?: string; id: number }) { |
| | | return requestClient.put('/mes/qc/ncr/close', data); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | import { normalizeQcDateFilter } from '../date-filter'; |
| | | |
| | | export namespace MesQcPatrolApi { |
| | | /** MES è´¨éå·¡æ£è®°å½ */ |
| | | export interface Patrol { |
| | | id?: number; // ç¼å· |
| | | code?: string; // å·¡æ£åç¼å· |
| | | name?: string; // å·¡æ£ååç§° |
| | | itemId?: number; // 产åç©æç¼å· |
| | | itemCode?: string; // 产åç©æç¼ç ï¼å
³èæ¥è¯¢ï¼ |
| | | itemName?: string; // 产åç©æåç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | itemSpecification?: string; // è§æ ¼åå·ï¼å
³èæ¥è¯¢ï¼ |
| | | unitName?: string; // åä½åç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | reelNo?: string; // çå· |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | qualifiedQuantity?: number; // åæ ¼æ°é |
| | | unqualifiedQuantity?: number; // ä¸åæ ¼æ°é |
| | | inspectDate?: string; // å·¡æ£æ¥æ |
| | | inspectorUserId?: number; // å·¡æ£äººåç¼å· |
| | | inspectorNickname?: string; // å·¡æ£äººåæµç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨éå·¡æ£è®°å½å页 */ |
| | | export function getPatrolPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcPatrolApi.Patrol>>( |
| | | '/mes/qc/patrol/page', |
| | | { params: normalizeQcDateFilter(params) }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨éå·¡æ£è®°å½è¯¦æ
*/ |
| | | export function getPatrol(id: number) { |
| | | return requestClient.get<MesQcPatrolApi.Patrol>( |
| | | `/mes/qc/patrol/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è´¨éå·¡æ£è®°å½ */ |
| | | export function createPatrol(data: MesQcPatrolApi.Patrol) { |
| | | return requestClient.post<number>('/mes/qc/patrol/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¨éå·¡æ£è®°å½ */ |
| | | export function updatePatrol(data: MesQcPatrolApi.Patrol) { |
| | | return requestClient.put('/mes/qc/patrol/update', data); |
| | | } |
| | | |
| | | /** å é¤è´¨éå·¡æ£è®°å½ */ |
| | | export function deletePatrol(id: number) { |
| | | return requestClient.delete(`/mes/qc/patrol/delete?id=${id}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesQcStatisticsApi { |
| | | /** ææ£éªç±»åç»è®¡ */ |
| | | export interface TypeStat { |
| | | qcType: string; // æ£éªç±»åç¼ç ï¼IQC / IPQC / OQC / RQC |
| | | qcTypeName: string; // æ£éªç±»ååç§° |
| | | qcCount: number; // è´¨æ£åæ°é |
| | | checkQuantity: number; // æ£æµæ»æ° |
| | | qualifiedQuantity: number; // åæ ¼æ° |
| | | unqualifiedQuantity: number; // ä¸åæ ¼æ° |
| | | qualifiedRate: number; // åæ ¼çï¼ç¾åæ¯ï¼ |
| | | unqualifiedRate: number; // ä¸åæ ¼çï¼ç¾åæ¯ï¼ |
| | | } |
| | | |
| | | /** æ´ä½ç»è®¡ */ |
| | | export interface Overall { |
| | | totalQcCount: number; // è´¨æ£åæ»æ° |
| | | checkQuantity: number; // æ£æµæ»æ° |
| | | qualifiedQuantity: number; // åæ ¼æ° |
| | | unqualifiedQuantity: number; // ä¸åæ ¼æ° |
| | | qualifiedRate: number; // åæ ¼çï¼ç¾åæ¯ï¼ |
| | | unqualifiedRate: number; // ä¸åæ ¼çï¼ç¾åæ¯ï¼ |
| | | types: TypeStat[]; // 忣éªç±»åç»è®¡ |
| | | } |
| | | |
| | | /** æå·¥åºç»è®¡ï¼ä»
IPQCï¼ */ |
| | | export interface ByProcess { |
| | | processId?: number; // å·¥åºç¼å· |
| | | processName?: string; // å·¥åºåç§° |
| | | checkQuantity: number; // æ£æµæ»æ° |
| | | qualifiedQuantity: number; // åæ ¼æ° |
| | | unqualifiedQuantity: number; // ä¸åæ ¼æ° |
| | | qualifiedRate: number; // åæ ¼çï¼ç¾åæ¯ï¼ |
| | | } |
| | | |
| | | /** æç产工åç»è®¡ï¼ä»
IPQCï¼ */ |
| | | export interface ByWorkOrder { |
| | | workOrderId?: number; // ç产工åç¼å· |
| | | workOrderCode?: string; // å·¥åå· |
| | | productName?: string; // 产ååç§° |
| | | checkQuantity: number; // æ£æµæ»æ° |
| | | qualifiedQuantity: number; // åæ ¼æ° |
| | | unqualifiedQuantity: number; // ä¸åæ ¼æ° |
| | | qualifiedRate: number; // åæ ¼çï¼ç¾åæ¯ï¼ |
| | | } |
| | | |
| | | /** è´¨éç»è®¡æ±æ» */ |
| | | export interface Summary { |
| | | overall: Overall; |
| | | byProcess: ByProcess[]; |
| | | byWorkOrder: ByWorkOrder[]; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨éç»è®¡æ±æ»ï¼beginTime / endTime å¯éï¼æ ¼å¼ yyyy-MM-dd HH:mm:ssï¼ */ |
| | | export function getQcStatisticsSummary(params?: { |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | }) { |
| | | return requestClient.get<MesQcStatisticsApi.Summary>( |
| | | '/mes/qc/statistics/summary', |
| | | { params }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesQcTemplateProcessApi { |
| | | /** è´¨æ£æ¹æ¡-å·¥åºç»å® */ |
| | | export interface Binding { |
| | | id?: number; |
| | | processId?: number; |
| | | processName?: string; |
| | | templateId?: number; |
| | | templateCode?: string; |
| | | templateName?: string; |
| | | /** è´¨æ£ç§ç±»ï¼1-æ¥ææ£éª 2-è¿ç¨æ£éª 3-åºè´§æ£éª 4-éè´§æ£éª */ |
| | | qcType?: number; |
| | | /** æ¯å¦é»è®¤æ¹æ¡ */ |
| | | defaultFlag?: boolean; |
| | | sort?: number; |
| | | status?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** ææ¹æ¡ä¿åéç¨å·¥åºçå
¥å */ |
| | | export interface SaveByTemplateReq { |
| | | templateId: number; |
| | | qcType: number; |
| | | list: Array<{ |
| | | processId: number; |
| | | defaultFlag?: boolean; |
| | | sort?: number; |
| | | }>; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨æ£æ¹æ¡éç¨çå·¥åº */ |
| | | export function getTemplateProcessListByTemplate(templateId: number) { |
| | | return requestClient.get<MesQcTemplateProcessApi.Binding[]>( |
| | | '/mes/qc/template-process/list-by-template', |
| | | { params: { templateId } }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åè´¨æ£æ¹æ¡éç¨çå·¥åºï¼å·®å¼å¢å ï¼ */ |
| | | export function saveTemplateProcessByTemplate( |
| | | data: MesQcTemplateProcessApi.SaveByTemplateReq, |
| | | ) { |
| | | return requestClient.post('/mes/qc/template-process/save-by-template', data); |
| | | } |
| | |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** å¯¼åºæååºåºå Wordï¼å¸¦çµåç« ï¼å¯æå¡«ç¾åå彿¡£ï¼ */ |
| | | export function exportProductSalesWord(id: number) { |
| | | return requestClient.download('/mes/wm/product-sales/export-word', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | |
| | | recipientName?: string; // æ¶è´§äºº |
| | | recipientTelephone?: string; // èç³»æ¹å¼ |
| | | recipientAddress?: string; // æ¶è´§å°å |
| | | logisticsCompany?: string; // ç©æµå
¬å¸ |
| | | expressFee?: number; // å¿«éè´¹ |
| | | invoiceStatus?: number; // å¼ç¥¨ç¶æï¼0-æªå¼ç¥¨ï¼1-é¨åå¼ç¥¨ï¼2-å·²å¼ç¥¨ |
| | | status?: number; // åæ®ç¶æ |
| | | outStatus?: number; // åºåºç¶æï¼0-æªåºåºï¼1-é¨ååºåºï¼2-å
¨é¨åºåº |
| | | remark?: string; // 夿³¨ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | | |
| | | /** åè´§å°è´¦ï¼æäº§åå¹³éºï¼ä¸è¡ = ä¸ä¸ªäº§åæç»ï¼ */ |
| | | export interface SalesNoticeLedger { |
| | | noticeId: number; // åè´§éç¥åç¼å· |
| | | noticeCode: string; // åè´§åå· |
| | | saleOrderCode?: string; // éå®è®¢åå· |
| | | clientId?: number; // 客æ·ç¼å· |
| | | clientName?: string; // 客æ·åç§° |
| | | salesDate?: number; // åè´§æ¥æ |
| | | recipientName?: string; // æ¶è´§äºº |
| | | recipientAddress?: string; // æ¶è´§å°å |
| | | logisticsCompany?: string; // ç©æµå
¬å¸ |
| | | expressFee?: number; // å¿«éè´¹ |
| | | invoiceStatus?: number; // å¼ç¥¨ç¶æ |
| | | status?: number; // åæ®ç¶æ |
| | | outStatus?: number; // åºåºç¶æ |
| | | lineId: number; // æç»è¡ç¼å· |
| | | itemId?: number; // 产åç¼å· |
| | | itemCode?: string; // 产åç¼ç |
| | | itemName?: string; // 产ååç§° |
| | | itemSpecification?: string; // è§æ ¼åå· |
| | | unitName?: string; // åä½ |
| | | quantity?: number; // åè´§æ°é |
| | | outCount?: number; // å·²åºåºæ°é |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | lineRemark?: string; // æç»å¤æ³¨ |
| | | } |
| | | } |
| | | |
| | |
| | | export function getSalesNoticePage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesWmSalesNoticeApi.SalesNotice>>( |
| | | '/mes/wm/sales-notice/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢åè´§å°è´¦åé¡µï¼æäº§åå¹³éºï¼ */ |
| | | export function getSalesNoticeLedgerPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesWmSalesNoticeApi.SalesNoticeLedger>>( |
| | | '/mes/wm/sales-notice/ledger-page', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | } |
| | | |
| | | /** æäº¤åè´§éç¥å */ |
| | | export function submitSalesNotice(id: number) { |
| | | return requestClient.put(`/mes/wm/sales-notice/submit?id=${id}`); |
| | | export function submitSalesNotice(id: number, processDefinitionKey?: string) { |
| | | return requestClient.put('/mes/wm/sales-notice/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·ååè´§å®¡æ¹æµç¨å表 */ |
| | | export function getSalesNoticeApproveProcessList() { |
| | | return requestClient.get< |
| | | Array<{ id: string; key: string; name: string; version: number }> |
| | | >('/mes/wm/sales-notice/approve-process-list'); |
| | | } |
| | | |
| | | /** 导åºåè´§éç¥å */ |
| | |
| | | }); |
| | | } |
| | | |
| | | /** 导åºåè´§å Wordï¼å¸¦çµåç« ï¼å¯æå¡«ç¾åå彿¡£ï¼ */ |
| | | export function exportSalesNoticeWord(id: number) { |
| | | return requestClient.download('/mes/wm/sales-notice/export-word', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** ä»éå®è®¢åçæåè´§éç¥å */ |
| | | export function generateFromSaleOrder(saleOrderId: number) { |
| | | return requestClient.post<number>('/mes/wm/sales-notice/generate-from-sale-order', { |
| | |
| | | bizType?: number; // ä¸å¡ç±»åç¼ç |
| | | bizTypeName?: string; // ä¸å¡ç±»ååç§° |
| | | bizCode?: string; // ä¸å¡åå· |
| | | workOrderId?: number; // ç产订åï¼å·¥åï¼ID |
| | | workOrderCode?: string; // ç产订åå· |
| | | itemId?: number; // ç©æID |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | |
| | | export function confirmInquiry( |
| | | id: number, |
| | | selections: SrmInquiryApi.Selection[], |
| | | paymentDate: string, |
| | | ) { |
| | | return requestClient.post<SrmInquiryApi.ConfirmResult>( |
| | | '/srm/inquiry/confirm', |
| | | { id, selections }, |
| | | { id, selections, paymentDate }, |
| | | ); |
| | | } |
| | |
| | | const total = items.length; |
| | | /** åæ ¼çç忝åªç®ãè½å¤å®ãçé¡¹ï¼æ å¤å®è§åç项æ¢ä¸å åä¹ä¸åå */ |
| | | const decidableCount = total - undecidableCount; |
| | | const hasPendingItem = items.some((item) => item.resultText === PENDING_TEXT); |
| | | |
| | | let reportResult: QualityResult | undefined; |
| | | if (reportRules.length > 0) { |
| | | // ç¼ºå°æ£éªä¾æ®æ¶ï¼å³ä½¿æ¥å级è§åè½ç®åºå¸å°å¼ï¼ä¹ä¸è½çæç¡®å®ç»è®ºã |
| | | if (!hasPendingItem && reportRules.length > 0) { |
| | | let passed = true; |
| | | let failed = false; |
| | | reportRules.forEach((rule) => { |
| | |
| | | } |
| | | }); |
| | | reportResult = failed ? undefined : passed ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL; |
| | | } else if (decidableCount > 0) { |
| | | } else if (!hasPendingItem && decidableCount > 0) { |
| | | // åªæãå
¨é¨å¯å¤å®é¡¹é½åæ ¼ãæç»åæ ¼ç»è®ºãæ å¤å®è§åç项ä¸åä¸ï¼å®ä»¬æ²¡æå¯¹éå¯è¨ã |
| | | // åå¨å¾
å¤å®é¡¹ï¼è§åè·æï¼æ¶ä¸ç»ç»è®ºï¼æ²¡éªè¿ä¸è½è¯´åæ ¼ï¼æ²¡è¯æ®ä¹ä¸è½è¯´ä¸åæ ¼ï¼ |
| | | // å¦åä¼åºç°ãç»è®ºåæ ¼ãåæ ¼ç 0%ãè¿ç§èªç¸çç¾çæ¥åã |
| | |
| | | return leftText === rightText ? 0 : leftText > rightText ? 1 : -1; |
| | | } |
| | | |
| | | function compareNumbers(left: unknown, right: unknown): number { |
| | | const leftNumber = toNumber(left); |
| | | const rightNumber = toNumber(right); |
| | | if (!Number.isFinite(leftNumber) || !Number.isFinite(rightNumber)) { |
| | | throw new RuleRuntimeError('æ°å¼æ¯è¾ç¼ºå°ææå®æµå¼æè§æ ¼éå¼'); |
| | | } |
| | | return leftNumber === rightNumber ? 0 : leftNumber > rightNumber ? 1 : -1; |
| | | } |
| | | |
| | | /** å¤å®ç»æç±»åå¼ç»ä¸æ PASS / FAILï¼å
¼å®¹ä¸æãå¸å°ä¸æ°å 1/0 */ |
| | | const PASS_TOKENS = new Set(['PASS', 'TRUE', 'åæ ¼', 'OK', '1', 'æ¯']); |
| | | const FAIL_TOKENS = new Set(['FAIL', 'FALSE', 'ä¸åæ ¼', 'NG', '0', 'å¦']); |
| | |
| | | case 'between': { |
| | | const value = evaluateNode(node.value, context); |
| | | const hit = |
| | | compare(value, evaluateNode(node.lower, context)) >= 0 && |
| | | compare(value, evaluateNode(node.upper, context)) <= 0; |
| | | compareNumbers(value, evaluateNode(node.lower, context)) >= 0 && |
| | | compareNumbers(value, evaluateNode(node.upper, context)) <= 0; |
| | | return node.negated ? !hit : hit; |
| | | } |
| | | case 'in': { |
| | |
| | | return compare(left, right) !== 0; |
| | | } |
| | | case '>': { |
| | | return compare(left, right) > 0; |
| | | return compareNumbers(left, right) > 0; |
| | | } |
| | | case '>=': { |
| | | return compare(left, right) >= 0; |
| | | return compareNumbers(left, right) >= 0; |
| | | } |
| | | case '<': { |
| | | return compare(left, right) < 0; |
| | | return compareNumbers(left, right) < 0; |
| | | } |
| | | case '<=': { |
| | | return compare(left, right) <= 0; |
| | | return compareNumbers(left, right) <= 0; |
| | | } |
| | | case '+': { |
| | | const leftNumber = toNumber(left); |
| | |
| | | visit(node); |
| | | // ç»è®¡å½æ°ä½ç¨å¨æ°ç»è·¯å¾ä¸ï¼è¿é廿æ«ç«¯å段åªä¿çæ°ç»æ¬èº« |
| | | return [...new Set(paths.map((path) => parsePath(path).join('.')))]; |
| | | } |
| | | } |
| | |
| | | MAINTENANCE: 3, |
| | | } as const; |
| | | |
| | | /** MES åæç±»åæä¸¾ */ |
| | | export const HolidayType = { |
| | | WORKDAY: 1, |
| | | HOLIDAY: 2, |
| | | } as const; |
| | | |
| | | /** MES æç计åç¶ææä¸¾ */ |
| | | export const MesCalPlanStatusEnum = { |
| | | PREPARE: 0, |
| | | CONFIRMED: 1, |
| | | } as const; |
| | | |
| | | /** MES è½®çæ¹å¼æä¸¾ */ |
| | | export const MesCalShiftTypeEnum = { |
| | | SINGLE: 1, |
| | | TWO: 2, |
| | | THREE: 3, |
| | | } as const; |
| | | |
| | | /** MES åçæ¹å¼æä¸¾ */ |
| | | export const MesCalShiftMethodEnum = { |
| | | QUARTER: 1, |
| | | MONTH: 2, |
| | | WEEK: 3, |
| | | DAY: 4, |
| | | } as const; |
| | | |
| | | /** MES ç¹æ£ä¿å
»é¡¹ç®ç±»åæä¸¾ */ |
| | | export const MesDvSubjectTypeEnum = { |
| | | CHECK: 1, |
| | |
| | | QC_IPQC_CODE: 'QC_IPQC_CODE', |
| | | QC_OQC_CODE: 'QC_OQC_CODE', |
| | | QC_RQC_CODE: 'QC_RQC_CODE', |
| | | QC_PATROL_CODE: 'QC_PATROL_CODE', |
| | | QC_TEMPLATE_CODE: 'QC_TEMPLATE_CODE', |
| | | TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE', |
| | | TM_TOOL_CODE: 'TM_TOOL_CODE', |
| | |
| | | /** MES åè´§éç¥åç¶ææä¸¾ */ |
| | | export const MesWmSalesNoticeStatusEnum = { |
| | | PREPARE: MesOrderStatusConstants.DRAFT, |
| | | APPROVING: MesOrderStatusConstants.APPROVING, |
| | | APPROVED: MesOrderStatusConstants.APPROVED, |
| | | } as const; |
| | | |
| | |
| | | ERP_STOCK_RECORD_BIZ_TYPE: 'erp_stock_record_biz_type', // åºåæç»çä¸å¡ç±»å |
| | | ERP_PURCHASE_TYPE: 'erp_purchase_type', // éè´å°è´¦-éè´ç±»åï¼1ç产 2åå
¬ç¨å 3å
¶ä» |
| | | ERP_PAYMENT_METHOD: 'erp_payment_method', // éè´å°è´¦-仿¬¾æ¹å¼ï¼1çµæ± 2æ±ç¥¨ 3ç°é 4ææºæ¯ä» |
| | | ERP_SALE_TYPE: 'erp_sale_type', // éå®å°è´¦-éå®ç±»åï¼1å
é 2å¤é 3æ ·å 4å
¶ä» |
| | | } as const; |
| | | |
| | | /** ========== FINANCE - è´¢å¡ç®¡ç模å ========== */ |
| | |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { downloadContractDocument } from '#/api/crm/contract'; |
| | | import { downloadSaleQuotationDocument } from '#/api/crm/saleQuotation'; |
| | | |
| | | export async function downloadContractWord(id: number, no?: string) { |
| | | const source = await downloadContractDocument(id); |
| | | const suffix = (no || String(id)).replaceAll(/[\\/:*?"<>|]/g, '_'); |
| | | downloadFileFromBlobPart({ fileName: `è´éåå-${suffix}.docx`, source }); |
| | | } |
| | | |
| | | export async function downloadSaleQuotationWord(id: number, no?: string) { |
| | | const source = await downloadSaleQuotationDocument(id); |
| | | const suffix = (no || String(id)).replaceAll(/[\\/:*?"<>|]/g, '_'); |
| | | downloadFileFromBlobPart({ fileName: `æ¥ä»·å-${suffix}.docx`, source }); |
| | | } |
| | |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReceivablePage, updateReceivableStatus } from '#/api/crm/receivable'; |
| | | import { getReceivablePage } from '#/api/crm/receivable'; |
| | | import { useGridColumns } from '#/views/crm/receivable/data'; |
| | | |
| | | import { AUDIT_STATUS } from '../data'; |
| | | |
| | | const { push } = useRouter(); |
| | | const emit = defineEmits<{ refresh: [] }>(); |
| | | |
| | | /** å®¡æ ¸ */ |
| | | async function handleAudit(row: CrmReceivableApi.Receivable) { |
| | | const hideLoading = message.loading({ |
| | | content: 'å®¡æ ¸ä¸...', |
| | | duration: 0, |
| | | /** æ¥ç审æ¹è¿åº¦ */ |
| | | function handleProcessDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | try { |
| | | await updateReceivableStatus(row.id!, 20); |
| | | message.success('å®¡æ ¸æå'); |
| | | await gridApi.query(); |
| | | emit('refresh'); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æå¼å款详æ
*/ |
| | |
| | | if (row.invoiceId) push({ name: 'CrmInvoiceDetail', params: { id: row.invoiceId } }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | const [Grid] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction :actions="[{ |
| | | label: 'å®¡æ ¸', |
| | | label: 'æ¥ç审æ¹è¿åº¦', |
| | | type: 'link', |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 10, |
| | | onClick: handleAudit.bind(null, row), |
| | | ifShow: row.auditStatus === 10 && !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }]" /> |
| | | </template> |
| | | </Grid> |
| | |
| | | |
| | | import Form from '../modules/form.vue'; |
| | | import { useDetailSchema } from './data'; |
| | | import AddressList from './modules/address-list.vue'; |
| | | import DistributeForm from './modules/distribute-form.vue'; |
| | | import Info from './modules/info.vue'; |
| | | |
| | |
| | | <ReceivablePlanDetailsList :customer-id="customerId" /> |
| | | <ReceivableDetailsList :customer-id="customerId" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æ¶è´§å°å" key="9" :force-render="true"> |
| | | <AddressList :customer-id="customerId" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="8" :force-render="true"> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmCustomerAddressApi } from '#/api/crm/customerAddress'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | createCustomerAddress, |
| | | deleteCustomerAddress, |
| | | getCustomerAddressList, |
| | | updateCustomerAddress, |
| | | } from '#/api/crm/customerAddress'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | defineOptions({ name: 'CustomerAddressList' }); |
| | | |
| | | const props = defineProps<{ customerId: number }>(); |
| | | |
| | | const editingRow = ref<CrmCustomerAddressApi.CustomerAddress>(); |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { formItemClass: 'col-span-1', labelWidth: 100 }, |
| | | layout: 'horizontal', |
| | | schema: [ |
| | | { |
| | | fieldName: 'receiverName', |
| | | label: 'æ¶è´§äºº', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { placeholder: '请è¾å
¥æ¶è´§äººå§å', maxlength: 64 }, |
| | | }, |
| | | { |
| | | fieldName: 'receiverMobile', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { placeholder: '请è¾å
¥æ¶è´§äººææºå·', maxlength: 32 }, |
| | | }, |
| | | { |
| | | fieldName: 'detailAddress', |
| | | label: 'æ¶è´§å°å', |
| | | component: 'Textarea', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥çãå¸ãåºå详ç»å°å', |
| | | rows: 2, |
| | | maxlength: 255, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'defaultFlag', |
| | | label: 'é»è®¤å°å', |
| | | component: 'Switch', |
| | | defaultValue: false, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2, maxlength: 500 }, |
| | | }, |
| | | ], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-1', |
| | | }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | const values = |
| | | (await formApi.getValues()) as CrmCustomerAddressApi.CustomerAddress; |
| | | formModalApi.lock(); |
| | | try { |
| | | if (editingRow.value?.id) { |
| | | await updateCustomerAddress({ |
| | | ...values, |
| | | id: editingRow.value.id, |
| | | customerId: props.customerId, |
| | | }); |
| | | } else { |
| | | await createCustomerAddress({ |
| | | ...values, |
| | | customerId: props.customerId, |
| | | }); |
| | | } |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | await formModalApi.close(); |
| | | await refresh(); |
| | | } finally { |
| | | formModalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | editingRow.value = undefined; |
| | | return; |
| | | } |
| | | await formApi.resetForm(); |
| | | if (editingRow.value) { |
| | | await formApi.setValues(editingRow.value); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { field: 'receiverName', title: 'æ¶è´§äºº', width: 120 }, |
| | | { field: 'receiverMobile', title: 'èç³»çµè¯', width: 150 }, |
| | | { field: 'detailAddress', title: 'æ¶è´§å°å', minWidth: 260 }, |
| | | { |
| | | field: 'defaultFlag', |
| | | title: 'é»è®¤å°å', |
| | | width: 100, |
| | | slots: { default: 'defaultFlag' }, |
| | | }, |
| | | { field: 'remark', title: '夿³¨', minWidth: 160 }, |
| | | { title: 'æä½', width: 160, fixed: 'right', slots: { default: 'actions' } }, |
| | | ], |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<CrmCustomerAddressApi.CustomerAddress>, |
| | | }); |
| | | |
| | | /** å è½½å°åå表 */ |
| | | async function refresh() { |
| | | const list = await getCustomerAddressList(props.customerId); |
| | | gridApi.setGridOptions({ data: list }); |
| | | } |
| | | |
| | | /** æ°å¢ */ |
| | | function handleCreate() { |
| | | editingRow.value = undefined; |
| | | formModalApi.setState({ title: 'æ°å¢æ¶è´§å°å' }); |
| | | void formModalApi.open(); |
| | | } |
| | | |
| | | /** ç¼è¾ */ |
| | | function handleEdit(row: CrmCustomerAddressApi.CustomerAddress) { |
| | | editingRow.value = row; |
| | | formModalApi.setState({ title: 'ç¼è¾æ¶è´§å°å' }); |
| | | void formModalApi.open(); |
| | | } |
| | | |
| | | /** å é¤ */ |
| | | async function handleDelete(row: CrmCustomerAddressApi.CustomerAddress) { |
| | | await deleteCustomerAddress(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.receiverName])); |
| | | await refresh(); |
| | | } |
| | | |
| | | onMounted(refresh); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="mb-2 flex justify-end"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢æ¶è´§å°å', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['crm:customer:update'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid table-title="æ¶è´§å°åå表"> |
| | | <template #defaultFlag="{ row }"> |
| | | <Tag v-if="row.defaultFlag" color="green">é»è®¤</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:customer:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['crm:customer:update'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.receiverName]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <FormModal class="w-[560px]"> |
| | | <Form class="mx-4" /> |
| | | </FormModal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import type { CrmFinanceReportApi } from '#/api/crm/financeReport'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | |
| | | import { |
| | | Button, |
| | | Card, |
| | | Checkbox, |
| | | Col, |
| | | Empty, |
| | | Input, |
| | | RangePicker, |
| | | Row, |
| | | Spin, |
| | | Table, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getCustomerStatement, |
| | | getCustomerStatementSummaryList, |
| | | } from '#/api/crm/financeReport'; |
| | | |
| | | defineOptions({ name: 'CrmCustomerStatement' }); |
| | | |
| | | const dateRange = ref<[Dayjs, Dayjs]>(); |
| | | const includeUnapproved = ref(false); |
| | | const keyword = ref(''); |
| | | |
| | | /** 左侧ï¼å客æ·å¾æ¥æ±æ» */ |
| | | const summaries = ref<CrmFinanceReportApi.CustomerStatementSummary[]>([]); |
| | | const listLoading = ref(false); |
| | | /** å½åéä¸çå®¢æ· */ |
| | | const selectedId = ref<number>(); |
| | | /** å³ä¾§ï¼å½å客æ·å¾æ¥æç» */ |
| | | const statement = ref<CrmFinanceReportApi.CustomerStatement>(); |
| | | const detailLoading = ref(false); |
| | | |
| | | const filteredSummaries = computed(() => { |
| | | const kw = keyword.value.trim().toLowerCase(); |
| | | if (!kw) { |
| | | return summaries.value; |
| | | } |
| | | return summaries.value.filter((item) => |
| | | (item.customerName ?? '').toLowerCase().includes(kw), |
| | | ); |
| | | }); |
| | | |
| | | const selectedSummary = computed(() => |
| | | summaries.value.find((item) => item.customerId === selectedId.value), |
| | | ); |
| | | |
| | | const totalUncollected = computed(() => |
| | | summaries.value.reduce( |
| | | (sum, item) => sum + (item.uncollectedAmount ?? 0), |
| | | 0, |
| | | ), |
| | | ); |
| | | |
| | | const orderColumns = [ |
| | | { title: 'éå®è®¢åå·', dataIndex: 'saleOrderNo', key: 'saleOrderNo', 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: 'collectedAmount', |
| | | key: 'collectedAmount', |
| | | width: 120, |
| | | }, |
| | | { |
| | | title: 'å©ä½åºæ¶', |
| | | dataIndex: 'remainingReceivable', |
| | | key: 'remainingReceivable', |
| | | width: 120, |
| | | }, |
| | | { |
| | | title: 'éå®è®¢å', |
| | | dataIndex: 'saleOrderNo', |
| | | key: 'saleOrderNo', |
| | | width: 160, |
| | | }, |
| | | ]; |
| | | |
| | | const receiptColumns = [ |
| | | { title: '忬¾åå·', dataIndex: 'no', key: 'no', width: 160 }, |
| | | { title: '忬¾æ¶é´', dataIndex: 'returnTime', key: 'returnTime', width: 150 }, |
| | | { title: '忬¾éé¢', dataIndex: 'amount', key: 'amount', width: 120 }, |
| | | { title: 'å
³èå票', dataIndex: 'invoiceNo', key: 'invoiceNo', width: 150 }, |
| | | { title: 'ç¶æ', dataIndex: 'status', key: 'status', width: 100 }, |
| | | ]; |
| | | |
| | | function formatMoney(value?: number) { |
| | | if (value === undefined || value === null) { |
| | | return '-'; |
| | | } |
| | | return Number(value).toLocaleString('zh-CN', { |
| | | maximumFractionDigits: 2, |
| | | minimumFractionDigits: 2, |
| | | }); |
| | | } |
| | | |
| | | function getOrderStatusLabel(status?: number) { |
| | | if (status === undefined || status === null) { |
| | | return '-'; |
| | | } |
| | | return getDictLabel(DICT_TYPE.ERP_AUDIT_STATUS, status) || '-'; |
| | | } |
| | | |
| | | function getAuditStatusLabel(status?: number) { |
| | | if (status === undefined || status === null) { |
| | | return '-'; |
| | | } |
| | | return getDictLabel(DICT_TYPE.CRM_AUDIT_STATUS, status) || '-'; |
| | | } |
| | | |
| | | /** ç»è£
æ¶é´èå´åæ° */ |
| | | function buildTimeParams() { |
| | | const params: { |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | } = {}; |
| | | 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'); |
| | | } |
| | | return params; |
| | | } |
| | | |
| | | /** å è½½å·¦ä¾§å®¢æ·æ±æ»ï¼é»è®¤éä¸ç¬¬ä¸ä¸ªå®¢æ· */ |
| | | async function loadSummaries() { |
| | | listLoading.value = true; |
| | | try { |
| | | summaries.value = |
| | | (await getCustomerStatementSummaryList({ |
| | | ...buildTimeParams(), |
| | | includeUnapprovedOrder: includeUnapproved.value, |
| | | })) ?? []; |
| | | const stillExists = summaries.value.some( |
| | | (item) => item.customerId === selectedId.value, |
| | | ); |
| | | if (!stillExists) { |
| | | selectedId.value = summaries.value[0]?.customerId; |
| | | } |
| | | const targetId = selectedId.value; |
| | | if (targetId === undefined) { |
| | | statement.value = undefined; |
| | | return; |
| | | } |
| | | await loadDetail(targetId); |
| | | } finally { |
| | | listLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** ç¹å»å·¦ä¾§å®¢æ·ï¼å è½½å³ä¾§æç» */ |
| | | async function handleSelect(customerId: number) { |
| | | if (selectedId.value === customerId && statement.value) { |
| | | return; |
| | | } |
| | | selectedId.value = customerId; |
| | | await loadDetail(customerId); |
| | | } |
| | | |
| | | /** å è½½æå®å®¢æ·ç徿¥æç» */ |
| | | async function loadDetail(customerId: number) { |
| | | detailLoading.value = true; |
| | | try { |
| | | statement.value = await getCustomerStatement({ |
| | | ...buildTimeParams(), |
| | | includeUnapprovedOrder: includeUnapproved.value, |
| | | customerId, |
| | | }); |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(loadSummaries); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex h-full gap-4"> |
| | | <!-- 左侧ï¼å®¢æ·æ»è§ï¼ç¹å»æ¥çå³ä¾§æç» --> |
| | | <div class="flex w-[360px] shrink-0 flex-col rounded-lg border"> |
| | | <div class="border-b p-3"> |
| | | <Input v-model:value="keyword" allow-clear placeholder="æç´¢å®¢æ·åç§°" /> |
| | | <div class="mt-2 flex items-center gap-2"> |
| | | <RangePicker |
| | | v-model:value="dateRange" |
| | | class="min-w-0 flex-1" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 flex items-center justify-between"> |
| | | <Checkbox v-model:checked="includeUnapproved"> |
| | | 嫿ªå®¡æ ¸éå®è®¢å |
| | | </Checkbox> |
| | | <Button |
| | | size="small" |
| | | type="primary" |
| | | :loading="listLoading" |
| | | @click="loadSummaries" |
| | | > |
| | | æ¥è¯¢ |
| | | </Button> |
| | | </div> |
| | | <div class="mt-2 text-xs text-gray-500"> |
| | | å
± {{ filteredSummaries.length }} 个客æ·ï¼æªæ¶å计 |
| | | <span class="font-medium text-red-600"> |
| | | ¥{{ formatMoney(totalUncollected) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="min-h-0 flex-1 overflow-y-auto p-2"> |
| | | <Spin :spinning="listLoading"> |
| | | <div |
| | | v-if="filteredSummaries.length === 0" |
| | | class="p-6 text-center text-sm text-gray-500" |
| | | > |
| | | {{ |
| | | summaries.length > 0 |
| | | ? '没æå¹é
ç客æ·' |
| | | : 'æéæ¶é´èå´å
没æå®¢æ·å¾æ¥' |
| | | }} |
| | | </div> |
| | | <div |
| | | v-for="item in filteredSummaries" |
| | | :key="item.customerId" |
| | | class="mb-2 cursor-pointer rounded-lg border p-3 transition-colors" |
| | | :class=" |
| | | item.customerId === selectedId |
| | | ? 'border-blue-500 bg-blue-50' |
| | | : 'hover:border-blue-300 hover:bg-gray-50' |
| | | " |
| | | @click="handleSelect(item.customerId)" |
| | | > |
| | | <div class="flex items-start justify-between gap-2"> |
| | | <div class="min-w-0"> |
| | | <div class="truncate text-sm font-medium"> |
| | | {{ item.customerName }} |
| | | </div> |
| | | <div class="text-xs text-gray-400"> |
| | | 订å {{ item.orderCount }} å / å¼ç¥¨ |
| | | {{ item.invoiceCount }} å¼ / 忬¾ {{ item.receiptCount }} ç¬ |
| | | </div> |
| | | </div> |
| | | <div class="shrink-0 text-right"> |
| | | <div class="text-xs text-gray-500">æªæ¶ä½é¢</div> |
| | | <div |
| | | class="text-sm font-semibold" |
| | | :class=" |
| | | (item.uncollectedAmount ?? 0) > 0 |
| | | ? 'text-red-600' |
| | | : 'text-gray-700' |
| | | " |
| | | > |
| | | {{ formatMoney(item.uncollectedAmount) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mt-2 grid grid-cols-3 gap-1 text-xs text-gray-500"> |
| | | <span>订å {{ formatMoney(item.totalOrderAmount) }}</span> |
| | | <span>å¼ç¥¨ {{ formatMoney(item.totalInvoiceAmount) }}</span> |
| | | <span>忬¾ {{ formatMoney(item.totalCollectedAmount) }}</span> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å³ä¾§ï¼å½å客æ·å¾æ¥æç» --> |
| | | <div class="min-w-0 flex-1 overflow-y-auto"> |
| | | <Empty |
| | | v-if="!selectedId" |
| | | class="mt-20" |
| | | description="请å¨å·¦ä¾§éæ©å®¢æ·ï¼æ¥ç徿¥æç»" |
| | | /> |
| | | <Spin v-else :spinning="detailLoading"> |
| | | <div class="mb-4 flex flex-wrap items-center gap-2"> |
| | | <span class="text-lg font-semibold"> |
| | | {{ statement?.customerName ?? selectedSummary?.customerName }} |
| | | </span> |
| | | <span class="text-xs text-gray-400"> |
| | | 订å {{ selectedSummary?.orderCount ?? 0 }} å / å¼ç¥¨ |
| | | {{ selectedSummary?.invoiceCount ?? 0 }} å¼ / 忬¾ |
| | | {{ selectedSummary?.receiptCount ?? 0 }} ç¬ |
| | | </span> |
| | | </div> |
| | | |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">累计订åé¢</div> |
| | | <div class="text-2xl font-semibold"> |
| | | {{ formatMoney(statement?.orderTotalAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">累计å¼ç¥¨é¢</div> |
| | | <div class="text-2xl font-semibold text-blue-600"> |
| | | {{ formatMoney(statement?.invoicedAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">ç´¯è®¡åæ¬¾é¢</div> |
| | | <div class="text-2xl font-semibold text-green-600"> |
| | | {{ formatMoney(statement?.collectedAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">æªæ¶ä½é¢ï¼å¼ç¥¨ - 忬¾ï¼</div> |
| | | <div class="text-2xl font-semibold text-red-600"> |
| | | {{ formatMoney(statement?.outstandingAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <Row :gutter="16"> |
| | | <Col :span="12"> |
| | | <Card title="éå®è®¢åæµæ°´ï¼å«å¼ç¥¨éé¢ï¼" size="small"> |
| | | <Table |
| | | :columns="orderColumns" |
| | | :data-source="statement?.orders ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :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'"> |
| | | {{ getOrderStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="12"> |
| | | <Card title="å¼ç¥¨æµæ°´ï¼å¼ç¥¨éé¢ / 已忬¾ / å©ä½åºæ¶ï¼" size="small"> |
| | | <Table |
| | | :columns="invoiceColumns" |
| | | :data-source="statement?.invoices ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :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 === 'collectedAmount'"> |
| | | {{ formatMoney(record.collectedAmount) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'remainingReceivable'"> |
| | | <span class="text-red-600"> |
| | | {{ formatMoney(record.remainingReceivable) }} |
| | | </span> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | <Card class="mt-4" size="small" title="忬¾æµæ°´"> |
| | | <Table |
| | | :columns="receiptColumns" |
| | | :data-source="statement?.receipts ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :scroll="{ x: 700 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'amount'"> |
| | | <span class="text-green-600"> |
| | | {{ formatMoney(record.amount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | {{ getAuditStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { getContractSimpleList } from '#/api/crm/contract'; |
| | | import { getSaleOrderPage } from '#/api/erp/sale/order'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import PaymentNodesEditor from './modules/payment-nodes-editor.vue'; |
| | | import SaleOutSelect from './modules/sale-out-select.vue'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | |
| | | { |
| | | fieldName: 'saleOrderId', |
| | | label: 'éå®è®¢åå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥æå
³èéå®è®¢åå·' }, |
| | | component: 'ApiSelect', |
| | | componentProps: (_values, form) => ({ |
| | | placeholder: 'è¯·éæ©å·²å®¡æ ¸çéå®è®¢å', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: async () => { |
| | | const result = await getSaleOrderPage({ |
| | | pageNo: 1, |
| | | pageSize: 200, |
| | | status: 20, |
| | | customerId: form.getValues().customerId, |
| | | }); |
| | | return result.list; |
| | | }, |
| | | labelField: 'no', |
| | | valueField: 'id', |
| | | onChange: () => { |
| | | form.setFieldValue('saleOutId', undefined); |
| | | }, |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'saleOutId', |
| | | label: 'åºåºåå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥æå
³èåºåºåå·' }, |
| | | component: markRaw(SaleOutSelect), |
| | | help: 'éæ©éå®è®¢ååï¼ä»è¯¥è®¢å已宿åºåºçåæ®ä¸éæ©ï¼ä¸éä¹å¯ä¿å', |
| | | componentProps: (values) => ({ |
| | | saleOrderId: values.saleOrderId, |
| | | id: values.id, |
| | | placeholder: 'è¯·éæ©åºåºå', |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'contractAmount', |
| | |
| | | label: 'éªæ¶æ¬¾éé¢', |
| | | component: 'InputNumber', |
| | | componentProps: { class: '!w-full', min: 0, precision: 2 }, |
| | | }, |
| | | { |
| | | fieldName: 'paymentNodes', |
| | | label: '款项æ¶é´èç¹', |
| | | component: markRaw(PaymentNodesEditor), |
| | | formItemClass: 'md:col-span-2', |
| | | help: 'æåå约å®å¡«ååé¶æ®µè®¡åéé¢ä¸è®¡å忬¾æ¶é´ï¼å½å
¥å票æ¶è¿è½è¡¥å¡«å·²å款æ
åµ', |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTime', |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | title: 'å票å½å
¥', |
| | | field: 'entryStatus', |
| | | minWidth: 110, |
| | | slots: { default: 'entryStatus' }, |
| | | }, |
| | | { |
| | | title: 'å¼ç¥¨éé¢ï¼å
ï¼', |
| | | field: 'price', |
| | | minWidth: 150, |
| | |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import EntryForm from './modules/entry-form.vue'; |
| | | import Form from './modules/form.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [EntryModal, entryModalApi] = useVbenModal({ |
| | | connectedComponent: EntryForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportForm, |
| | | destroyOnClose: true, |
| | |
| | | /** æå¼å¯¼å
¥å¼ç¥¨å¼¹çª */ |
| | | function handleImport() { |
| | | importModalApi.open(); |
| | | } |
| | | |
| | | /** å½å
¥å票 */ |
| | | function handleEntry(row: CrmInvoiceApi.Invoice) { |
| | | entryModalApi.setData({ invoice: row }).open(); |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <EntryModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-actions> |
| | |
| | | å·²ä¸ä¼ |
| | | </Tag> |
| | | </template> |
| | | <template #entryStatus="{ row }"> |
| | | <Tag v-if="row.entryStatus === 10" color="success">å·²å½å
¥</Tag> |
| | | <Tag v-else color="default">å¾
å½å
¥</Tag> |
| | | </template> |
| | | <template #auditStatus="{ row }"> |
| | | <Tag v-if="row.auditStatus === 0" color="default">æªæäº¤</Tag> |
| | | <Tag v-if="row.auditStatus === 10" color="warning">审æ¹ä¸</Tag> |
| | |
| | | ifShow: row.auditStatus === 0, |
| | | }, |
| | | { |
| | | label: 'å½å
¥å票', |
| | | type: 'link', |
| | | auth: ['crm:invoice:update'], |
| | | onClick: handleEntry.bind(null, row), |
| | | ifShow: row.auditStatus === 20 && row.entryStatus !== 10, |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | auth: ['crm:invoice:update'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { entryInvoice, getInvoice } from '#/api/crm/invoice'; |
| | | |
| | | import PaymentNodesEditor from './payment-nodes-editor.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const invoice = ref<CrmInvoiceApi.Invoice>(); |
| | | |
| | | const getTitle = computed(() => |
| | | invoice.value |
| | | ? `å½å
¥å票 - ${invoice.value.no ?? ''}` |
| | | : 'å½å
¥å票', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | showDefaultActions: false, |
| | | schema: [ |
| | | { |
| | | fieldName: 'invoiceNo', |
| | | label: 'å票å·ç ', |
| | | component: 'Input', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票ä¸ççå®å票å·ç ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTime', |
| | | label: 'å¼ç¥¨æ¥æ', |
| | | component: 'DatePicker', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å票ä¸çå¼ç¥¨æ¥æ', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD', |
| | | format: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceTitle', |
| | | label: 'å票æ¬å¤´', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å票æ¬å¤´', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'å票éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | accept: ['jpg', 'jpeg', 'png', 'pdf'], |
| | | helpText: 'æ¯æjpgãjpegãpngãpdfæ ¼å¼ï¼å个æä»¶ä¸è¶
è¿50MB', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'paymentNodes', |
| | | label: '款项æ¶é´èç¹', |
| | | component: PaymentNodesEditor, |
| | | formItemClass: 'md:col-span-2', |
| | | help: 'æåå约å®å¡«å计åéé¢ã计å忬¾æ¶é´ï¼å·²æå款计åä¼å¸¦åºå·²å款éé¢', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 3 }, |
| | | formItemClass: 'md:col-span-2', |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid || !invoice.value?.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const data = await formApi.getValues(); |
| | | await entryInvoice({ |
| | | id: invoice.value.id, |
| | | invoiceNo: data.invoiceNo as string, |
| | | invoiceTime: data.invoiceTime as string, |
| | | invoiceTitle: data.invoiceTitle as string | undefined, |
| | | blobIds: data.blobIds as number[] | undefined, |
| | | paymentNodes: data.paymentNodes as CrmInvoiceApi.PaymentNode[], |
| | | remark: data.remark as string | undefined, |
| | | }); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('å票信æ¯å·²å½å
¥'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | invoice.value = undefined; |
| | | return; |
| | | } |
| | | await formApi.resetForm(); |
| | | const data = modalApi.getData() as |
| | | | { invoice?: Partial<CrmInvoiceApi.Invoice> } |
| | | | undefined; |
| | | if (!data?.invoice?.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | invoice.value = await getInvoice(data.invoice.id); |
| | | const values: Record<string, unknown> = { |
| | | invoiceNo: invoice.value.invoiceNo, |
| | | invoiceTime: invoice.value.invoiceTime, |
| | | invoiceTitle: invoice.value.invoiceTitle, |
| | | paymentNodes: invoice.value.paymentNodes ?? [], |
| | | remark: invoice.value.remark, |
| | | }; |
| | | if (invoice.value.attachmentList?.length) { |
| | | values.blobIds = invoice.value.attachmentList; |
| | | } |
| | | await formApi.setValues(values); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { Button, DatePicker, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; |
| | | |
| | | defineOptions({ name: 'CrmInvoicePaymentNodesEditor' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | /** æ¯å¦åªå±ç¤ºå·²å款信æ¯ï¼è¯¦æ
/å½å
¥å票æ¶ï¼ */ |
| | | readonly?: boolean; |
| | | }>(), |
| | | { disabled: false, readonly: false }, |
| | | ); |
| | | |
| | | const nodes = defineModel<CrmInvoiceApi.PaymentNode[]>({ default: () => [] }); |
| | | |
| | | const STAGE_OPTIONS = [ |
| | | { label: 'å忬¾', value: 1 }, |
| | | { label: 'é¢ä»æ¬¾', value: 2 }, |
| | | { label: 'å°è´§æ¬¾', value: 3 }, |
| | | { label: 'éªæ¶æ¬¾', value: 4 }, |
| | | { label: 'è´¨ä¿é', value: 5 }, |
| | | ]; |
| | | |
| | | function addNode() { |
| | | nodes.value = [ |
| | | ...(nodes.value ?? []), |
| | | { stage: 1, amount: undefined, returnTime: undefined, remark: '' }, |
| | | ]; |
| | | } |
| | | |
| | | function removeNode(index: number) { |
| | | const list = [...(nodes.value ?? [])]; |
| | | list.splice(index, 1); |
| | | nodes.value = list; |
| | | } |
| | | |
| | | function updateNode( |
| | | index: number, |
| | | key: keyof CrmInvoiceApi.PaymentNode, |
| | | value: unknown, |
| | | ) { |
| | | const list = [...(nodes.value ?? [])]; |
| | | list[index] = { ...list[index], [key]: value } as CrmInvoiceApi.PaymentNode; |
| | | nodes.value = list; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full"> |
| | | <div v-if="!nodes?.length" class="mb-2 text-sm text-gray-400"> |
| | | ææªè®¾ç½®æ¬¾é¡¹æ¶é´èç¹ï¼å¯ç¹å»ä¸æ¹ãæ°å¢èç¹ãæåå约å®å¡«å |
| | | </div> |
| | | <div |
| | | v-for="(node, index) in nodes" |
| | | :key="index" |
| | | class="mb-2 flex items-center gap-2" |
| | | > |
| | | <Select |
| | | :value="node.stage" |
| | | :options="STAGE_OPTIONS" |
| | | :disabled="props.disabled || props.readonly" |
| | | class="w-32" |
| | | placeholder="é¶æ®µ" |
| | | @change="(value) => updateNode(index, 'stage', value)" |
| | | /> |
| | | <InputNumber |
| | | :value="node.amount" |
| | | :disabled="props.disabled || props.readonly" |
| | | :min="0" |
| | | :precision="2" |
| | | class="w-40" |
| | | placeholder="计åéé¢" |
| | | @change="(value) => updateNode(index, 'amount', value)" |
| | | /> |
| | | <DatePicker |
| | | :value="node.returnTime" |
| | | :disabled="props.disabled || props.readonly" |
| | | class="w-44" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | placeholder="计å忬¾æ¶é´" |
| | | @change="(value) => updateNode(index, 'returnTime', value)" |
| | | /> |
| | | <Input |
| | | :value="node.remark" |
| | | :disabled="props.disabled || props.readonly" |
| | | class="flex-1" |
| | | placeholder="夿³¨" |
| | | @change="(e) => updateNode(index, 'remark', e.target.value)" |
| | | /> |
| | | <div v-if="node.receiptedPrice != null" class="w-40 text-sm text-gray-500"> |
| | | 已忬¾ï¼{{ Number(node.receiptedPrice).toFixed(2) }} |
| | | </div> |
| | | <Button |
| | | v-if="!props.disabled && !props.readonly" |
| | | type="text" |
| | | danger |
| | | @click="removeNode(index)" |
| | | > |
| | | <DeleteOutlined /> |
| | | </Button> |
| | | </div> |
| | | <Button |
| | | v-if="!props.disabled && !props.readonly" |
| | | type="dashed" |
| | | block |
| | | @click="addNode" |
| | | > |
| | | <PlusOutlined /> |
| | | æ°å¢èç¹ |
| | | </Button> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmInvoiceApi } from '#/api/crm/invoice'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, message, Modal } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getInvoiceSaleOutList } from '#/api/crm/invoice'; |
| | | |
| | | defineOptions({ name: 'CrmInvoiceSaleOutSelect' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | /** ç¼è¾æ¶ä¼ å
¥å¼ç¥¨åç¼å·ï¼ç¨äºæé¤èªèº«å·²å ç¨éé¢ */ |
| | | id?: number; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | | saleOrderId?: number; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | id: undefined, |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©åºåºå', |
| | | saleOrderId: undefined, |
| | | }, |
| | | ); |
| | | const emit = defineEmits<{ |
| | | change: [saleOut: CrmInvoiceApi.SaleOut | undefined]; |
| | | 'update:modelValue': [value: number | undefined]; |
| | | }>(); |
| | | |
| | | const open = ref(false); |
| | | const saleOut = ref<CrmInvoiceApi.SaleOut>(); |
| | | const displayCode = computed(() => saleOut.value?.code ?? ''); |
| | | |
| | | watch( |
| | | () => props.saleOrderId, |
| | | () => { |
| | | saleOut.value = undefined; |
| | | }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | async (value) => { |
| | | if (!value || !props.saleOrderId) { |
| | | if (!value) { |
| | | saleOut.value = undefined; |
| | | } |
| | | return; |
| | | } |
| | | if (saleOut.value?.id === value) { |
| | | return; |
| | | } |
| | | const list = await getInvoiceSaleOutList(props.saleOrderId, props.id); |
| | | saleOut.value = list.find((item) => item.id === value); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'radio', width: 50, fixed: 'left' }, |
| | | { field: 'code', title: 'åºåºåå·', width: 200, fixed: 'left' }, |
| | | { |
| | | field: 'salesDate', |
| | | title: 'åºåºæ¥æ', |
| | | width: 150, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'åºåºéé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'invoicedPrice', |
| | | title: 'å·²å¼ç¥¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'remainInvoicePrice', |
| | | title: 'å©ä½å¯å¼ç¥¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 140, |
| | | }, |
| | | ], |
| | | height: 460, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async () => { |
| | | if (!props.saleOrderId) { |
| | | return { list: [], total: 0 }; |
| | | } |
| | | const list = await getInvoiceSaleOutList( |
| | | props.saleOrderId, |
| | | props.id, |
| | | ); |
| | | return { list, total: list.length }; |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | radioConfig: { trigger: 'row', highlight: true }, |
| | | toolbarConfig: { refresh: false, search: false }, |
| | | } as VxeTableGridOptions<CrmInvoiceApi.SaleOut>, |
| | | gridEvents: { |
| | | radioChange: ({ row }: { row: CrmInvoiceApi.SaleOut }) => { |
| | | saleOut.value = row; |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | function handleOpen() { |
| | | if (props.disabled) { |
| | | return; |
| | | } |
| | | if (!props.saleOrderId) { |
| | | message.warning('请å
éæ©éå®è®¢åï¼å鿩坹åºçåºåºå'); |
| | | return; |
| | | } |
| | | open.value = true; |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleOk() { |
| | | if (!saleOut.value?.id) { |
| | | message.warning('è¯·éæ©åºåºå'); |
| | | return; |
| | | } |
| | | emit('update:modelValue', saleOut.value.id); |
| | | emit('change', saleOut.value); |
| | | open.value = false; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <Input |
| | | readonly |
| | | :disabled="disabled || !saleOrderId" |
| | | :placeholder="saleOrderId ? placeholder : '请å
éæ©éå®è®¢å'" |
| | | :value="displayCode" |
| | | @click="handleOpen" |
| | | > |
| | | <template #suffix> |
| | | <IconifyIcon |
| | | class="size-4" |
| | | :style="{ cursor: disabled || !saleOrderId ? 'not-allowed' : 'pointer' }" |
| | | icon="lucide:search" |
| | | /> |
| | | </template> |
| | | </Input> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="éæ©éå®åºåºåï¼æéå®è®¢åè¿æ»¤ï¼" |
| | | width="70%" |
| | | @cancel.stop="open = false" |
| | | @ok.stop="handleOk" |
| | | > |
| | | <Grid table-title="éå®åºåºåå表" /> |
| | | </Modal> |
| | | </div> |
| | | </template> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | title: '审æ¹ç¶æ', |
| | | field: 'auditStatus', |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.CRM_AUDIT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | title: '夿³¨', |
| | | field: 'remark', |
| | | minWidth: 150, |
| | |
| | | render: (val) => erpPriceInputFormatter(val), |
| | | }, |
| | | { |
| | | field: 'auditStatus', |
| | | label: '审æ¹ç¶æ', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.CRM_AUDIT_STATUS, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | label: 'è´è´£äºº', |
| | | }, |
| | |
| | | <script setup lang="ts"> |
| | | import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance'; |
| | | import type { CrmReceivableApi } from '#/api/crm/receivable'; |
| | | import type { SystemOperateLogApi } from '#/api/system/operate-log'; |
| | | |
| | |
| | | |
| | | import { Card, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { getApprovalDetail } from '#/api/bpm/processInstance'; |
| | | import { getOperateLogPage } from '#/api/crm/operateLog'; |
| | | import { BizTypeEnum } from '#/api/crm/permission'; |
| | | import { getReceivable } from '#/api/crm/receivable'; |
| | |
| | | import { ACTION_ICON, TableAction } from '#/components/table-action'; |
| | | import { $t } from '#/locales'; |
| | | import { PermissionList } from '#/views/crm/permission'; |
| | | import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue'; |
| | | |
| | | import ReceivableForm from '../modules/form.vue'; |
| | | import { useDetailSchema } from './data'; |
| | |
| | | {} as CrmReceivableApi.Receivable, |
| | | ); // 忬¾è¯¦æ
|
| | | const logList = ref<SystemOperateLogApi.OperateLog[]>([]); // æä½æ¥å¿ |
| | | const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]); // 审æ¹è®°å½ |
| | | const permissionListRef = ref<InstanceType<typeof PermissionList>>(); // å¢éæåå表 Ref |
| | | |
| | | const [Descriptions] = useDescription({ |
| | |
| | | bizId: receivableId.value, |
| | | }); |
| | | logList.value = res.list; |
| | | // 审æ¹è®°å½ï¼å·²æäº¤å®¡æ¹æ¶å è½½ BPM 审æ¹èç¹ |
| | | activityNodes.value = []; |
| | | if (receivable.value.processInstanceId) { |
| | | try { |
| | | const approval = await getApprovalDetail({ |
| | | processInstanceId: receivable.value.processInstanceId, |
| | | }); |
| | | activityNodes.value = approval?.activityNodes ?? []; |
| | | } catch { |
| | | activityNodes.value = []; |
| | | } |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail() { |
| | | if (!receivable.value.processInstanceId) { |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: receivable.value.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | } |
| | | |
| | | /** è¿åå表页 */ |
| | |
| | | onClick: handleBack, |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'default', |
| | | icon: ACTION_ICON.VIEW, |
| | | ifShow: !!receivable.processInstanceId, |
| | | onClick: handleProcessDetail, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.edit'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: permissionListRef?.validateWrite, |
| | | ifShow: |
| | | permissionListRef?.validateWrite && |
| | | (receivable.auditStatus === 0 || receivable.auditStatus === 30), |
| | | onClick: handleEdit, |
| | | }, |
| | | ]" |
| | |
| | | <Tabs.TabPane tab="详ç»èµæ" key="1" :force-render="true"> |
| | | <Info :receivable="receivable" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="审æ¹è®°å½" key="4" :force-render="true"> |
| | | <div |
| | | v-if="!receivable.processInstanceId" |
| | | class="py-8 text-center text-muted-foreground" |
| | | > |
| | | è¯¥åæ¬¾å°æªæäº¤å®¡æ¹ |
| | | </div> |
| | | <ProcessInstanceTimeline v-else :activity-nodes="activityNodes" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane tab="æä½æ¥å¿" key="2" :force-render="true"> |
| | | <OperateLog :log-list="logList" date-format="YYYY-MM-DD" /> |
| | | </Tabs.TabPane> |
| | |
| | | deleteReceivable, |
| | | exportReceivable, |
| | | getReceivablePage, |
| | | submitReceivable, |
| | | updateReceivableStatus, |
| | | } from '#/api/crm/receivable'; |
| | | import { $t } from '#/locales'; |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ´æ°åæ¬¾ç¶æï¼å®¡æ ¸/åå®¡æ ¸ï¼ */ |
| | | /** æäº¤å®¡æ¹ */ |
| | | async function handleSubmit(row: CrmReceivableApi.Receivable) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ¹ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitReceivable(row.id!); |
| | | message.success('æäº¤å®¡æ¹æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** åå®¡æ ¸ */ |
| | | async function handleUpdateStatus( |
| | | row: CrmReceivableApi.Receivable, |
| | | status: number, |
| | |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ */ |
| | | function handleProcessDetail(row: CrmReceivableApi.Receivable) { |
| | | push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥ç忬¾è¯¦æ
*/ |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 10, |
| | | ifShow: row.auditStatus === 0 || row.auditStatus === 30, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å®¡æ ¸', |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 10, |
| | | onClick: handleUpdateStatus.bind(null, row, 20), |
| | | ifShow: row.auditStatus === 0 || row.auditStatus === 30, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹è¿åº¦', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | ifShow: row.auditStatus === 10 && !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå®¡æ ¸', |
| | |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['crm:receivable:update'], |
| | | ifShow: row.auditStatus === 20, |
| | | onClick: handleUpdateStatus.bind(null, row, 10), |
| | | // åå®¡æ ¸åå°ãæªæäº¤ãï¼ä¾¿äºä¿®æ¹åéæ°æäº¤ BPM å®¡æ¹ |
| | | onClick: handleUpdateStatus.bind(null, row, 0), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | /** æ¥ä»·åç¶æ */ |
| | | export const QUOTATION_STATUS = { |
| | | DRAFT: 0, // è稿 |
| | | PROCESS: 1, // 审æ¹ä¸ |
| | | APPROVE: 2, // 审æ¹éè¿ |
| | | REJECT: 3, // 审æ¹ä¸éè¿ |
| | | CANCEL: 4, // 已忶 |
| | | CONVERTED: 10, // 已转åå |
| | | }; |
| | | |
| | |
| | | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { Button, message, Modal, Tag } from 'ant-design-vue'; |
| | | import { Button, message, Modal, Select, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | convertSaleQuotationToContract, |
| | | deleteSaleQuotation, |
| | | getSaleQuotationApproveProcessList, |
| | | getSaleQuotationPage, |
| | | submitSaleQuotation, |
| | | } from '#/api/crm/saleQuotation'; |
| | | import { $t } from '#/locales'; |
| | | import { downloadContractWord } from '#/utils/contract-document'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { |
| | | downloadContractWord, |
| | | downloadSaleQuotationWord, |
| | | } from '#/utils/contract-document'; |
| | | |
| | | import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai'; |
| | | |
| | |
| | | function handleContractDetail(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | if (row.contractId) { |
| | | push({ name: 'CrmContractDetail', params: { id: row.contractId } }); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ¹å¼¹çª */ |
| | | const submitModalVisible = ref(false); |
| | | const currentRow = ref<CrmSaleQuotationApi.SaleQuotation>(); |
| | | const processList = ref<CrmSaleQuotationApi.ApproveProcess[]>([]); |
| | | const selectedProcessKey = ref<string>(); |
| | | const submitLoading = ref(false); |
| | | |
| | | async function handleSubmit(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | const res = automaticApproval.value ? [] : await getSaleQuotationApproveProcessList(); |
| | | processList.value = res; |
| | | currentRow.value = row; |
| | | selectedProcessKey.value = undefined; |
| | | if (automaticApproval.value) { |
| | | selectedProcessKey.value = 'sale_quotation_approve'; |
| | | await confirmSubmit(); |
| | | return; |
| | | } |
| | | submitModalVisible.value = true; |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | submitLoading.value = true; |
| | | try { |
| | | await submitSaleQuotation(currentRow.value!.id!, selectedProcessKey.value); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | submitLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** ä¸è½½å¸¦ç« æ¥ä»·å */ |
| | | async function handleDownloadQuotation(row: CrmSaleQuotationApi.SaleQuotation) { |
| | | try { |
| | | await downloadSaleQuotationWord(row.id!, row.no); |
| | | } catch { |
| | | message.error('æ¥ä»·åä¸è½½å¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | } |
| | | } |
| | | |
| | |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.DRAFT" color="default">è稿</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.PROCESS" color="processing">审æ¹ä¸</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.APPROVE" color="success">审æ¹éè¿</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.REJECT" color="error">审æ¹ä¸éè¿</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.CANCEL" color="warning">已忶</Tag> |
| | | <Tag v-if="row.status === QUOTATION_STATUS.CONVERTED" color="success">已转åå</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT, |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | auth: ['crm:sale-quotation:update'], |
| | | onClick: handleSubmit.bind(null, row), |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT, |
| | | }, |
| | | { |
| | | label: '转åå', |
| | | type: 'link', |
| | | auth: ['crm:sale-quotation:update'], |
| | | onClick: handleConvertContract.bind(null, row), |
| | | ifShow: row.status === QUOTATION_STATUS.DRAFT && !row.contractId, |
| | | ifShow: |
| | | !row.contractId && |
| | | [QUOTATION_STATUS.DRAFT, QUOTATION_STATUS.APPROVE].includes( |
| | | row.status, |
| | | ), |
| | | }, |
| | | { |
| | | label: 'ä¸è½½æ¥ä»·å', |
| | | type: 'link', |
| | | auth: ['crm:sale-quotation:export'], |
| | | onClick: handleDownloadQuotation.bind(null, row), |
| | | ifShow: [QUOTATION_STATUS.APPROVE, QUOTATION_STATUS.CONVERTED].includes( |
| | | row.status, |
| | | ), |
| | | }, |
| | | { |
| | | label: 'æ¥çåå', |
| | |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <Modal |
| | | v-model:open="submitModalVisible" |
| | | title="éæ©å®¡æ¹æµç¨" |
| | | width="400px" |
| | | :confirm-loading="submitLoading" |
| | | @ok="confirmSubmit" |
| | | > |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | class="w-full" |
| | | > |
| | | <Select.Option v-for="item in processList" :key="item.key" :value="item.key"> |
| | | {{ item.name }} |
| | | </Select.Option> |
| | | </Select> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | step.value = 1; |
| | | processingTip.value = 'æ£å¨è¿è¡æ¥ä»·åè¯å«ï¼è¯·ç¨å...'; |
| | | const ocrData = await ocrSaleQuotation(blobId); |
| | | // æ£æ¥é误 |
| | | if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) { |
| | | // ææå段为 nullï¼rawText å
å«é误æç¤º |
| | | const hasData = !!( |
| | | ocrData.name || ocrData.customerName || ocrData.quotationTime |
| | | || ocrData.validUntil || ocrData.taxRate != null || ocrData.discountPercent != null |
| | | || ocrData.remark || ocrData.items?.length |
| | | ); |
| | | if (ocrData.rawText && !hasData) { |
| | | processingTip.value = ''; |
| | | step.value = 0; |
| | | message.error(ocrData.rawText); |
| | | return; |
| | | } |
| | | // æ£æ¥æ¯å¦å
¨é¨ä¸ºç©º |
| | | const hasData = ocrData.name || ocrData.customerName || ocrData.quotationTime |
| | | || ocrData.validUntil || ocrData.taxRate || ocrData.discountPercent |
| | | || ocrData.remark || (ocrData.items && ocrData.items.length > 0); |
| | | if (!hasData) { |
| | | processingTip.value = ''; |
| | | step.value = 0; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmFinanceReportApi } from '#/api/crm/financeReport'; |
| | | |
| | | /** éå®å¯¹è´¦-订åæç»å */ |
| | | export function useOrderColumns(): VxeTableGridOptions<CrmFinanceReportApi.CustomerStatementOrderRow>['columns'] { |
| | | return [ |
| | | { field: 'saleOrderNo', title: 'éå®è®¢åå·', minWidth: 160 }, |
| | | { field: 'orderTime', title: 'ä¸åæ¶é´', width: 160, formatter: 'formatDate' }, |
| | | { |
| | | field: 'orderAmount', |
| | | title: '订åéé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'shippedAmount', |
| | | title: 'åºåºéé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'invoicedAmount', |
| | | title: 'å¼ç¥¨éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'collectedAmount', |
| | | title: '忬¾éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'shippedNotInvoicedAmount', |
| | | title: 'å·²åè´§æªå¼ç¥¨', |
| | | width: 140, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'outstandingAmount', |
| | | title: 'æªæ ¸éåºæ¶', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** éå®å¯¹è´¦-å·²åè´§æªå¼ç¥¨å */ |
| | | export function useShippedColumns(): VxeTableGridOptions<CrmFinanceReportApi.ShippedNotInvoiced>['columns'] { |
| | | return [ |
| | | { field: 'saleOutCode', title: 'éå®åºåºåå·', minWidth: 160 }, |
| | | { field: 'salesDate', title: 'åºåºæ¥æ', width: 160, formatter: 'formatDate' }, |
| | | { field: 'saleOrderNo', title: 'éå®è®¢åå·', minWidth: 150 }, |
| | | { field: 'customerName', title: '客æ·åç§°', minWidth: 160 }, |
| | | { |
| | | field: 'outAmount', |
| | | title: 'åºåºéé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'invoicedAmount', |
| | | title: 'å·²å¼ç¥¨éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'remainInvoiceAmount', |
| | | title: 'æªå¼ç¥¨éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ ¸éæç»å */ |
| | | export function useWriteOffColumns(): VxeTableGridOptions<CrmFinanceReportApi.UnwrittenOff>['columns'] { |
| | | return [ |
| | | { field: 'invoiceNo', title: 'å票å·', minWidth: 150 }, |
| | | { field: 'taxInvoiceNo', title: 'ç¨å¡å票å·', minWidth: 150 }, |
| | | { field: 'customerName', title: '客æ·åç§°', minWidth: 160 }, |
| | | { field: 'invoiceTime', title: 'å¼ç¥¨æ¶é´', width: 160, formatter: 'formatDate' }, |
| | | { |
| | | field: 'invoiceAmount', |
| | | title: 'å¼ç¥¨éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'collectedAmount', |
| | | title: '已忬¾éé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { |
| | | field: 'remainAmount', |
| | | title: 'æªæ ¸ééé¢', |
| | | width: 130, |
| | | align: 'right', |
| | | formatter: ({ cellValue }) => formatAmount(cellValue), |
| | | }, |
| | | { field: 'agingDays', title: 'è´¦é¾(天)', width: 100, align: 'right' }, |
| | | { field: 'agingBucket', title: 'è´¦é¾åºé´', width: 110 }, |
| | | ]; |
| | | } |
| | | |
| | | /** é颿 ¼å¼å */ |
| | | export function formatAmount(value?: null | number | string) { |
| | | const num = Number(value ?? 0); |
| | | return Number.isNaN(num) ? '0.00' : num.toFixed(2); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmFinanceReportApi } from '#/api/crm/financeReport'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Button, Card, message, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | getCustomerStatement, |
| | | getShippedNotInvoicedList, |
| | | } from '#/api/crm/financeReport'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import { |
| | | formatAmount, |
| | | useOrderColumns, |
| | | useShippedColumns, |
| | | } from './data'; |
| | | |
| | | defineOptions({ name: 'FinSaleReconciliation' }); |
| | | |
| | | const customerId = ref<number>(); |
| | | const loading = ref(false); |
| | | const activeTab = ref('statement'); |
| | | const statement = ref<CrmFinanceReportApi.CustomerStatement>(); |
| | | |
| | | const [OrderGrid, orderGridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useOrderColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | rowConfig: { keyField: 'saleOrderId', isHover: true }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<CrmFinanceReportApi.CustomerStatementOrderRow>, |
| | | }); |
| | | |
| | | const [ShippedGrid, shippedGridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useShippedColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | rowConfig: { keyField: 'saleOutId', isHover: true }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<CrmFinanceReportApi.ShippedNotInvoiced>, |
| | | }); |
| | | |
| | | /** æ¥è¯¢å¯¹è´¦æ°æ® */ |
| | | async function handleQuery() { |
| | | loading.value = true; |
| | | try { |
| | | if (activeTab.value === 'statement') { |
| | | if (!customerId.value) { |
| | | message.warning('请å
鿩客æ·'); |
| | | return; |
| | | } |
| | | statement.value = await getCustomerStatement({ |
| | | customerId: customerId.value, |
| | | }); |
| | | orderGridApi.setGridOptions({ data: statement.value?.orders ?? [] }); |
| | | } else { |
| | | const list = await getShippedNotInvoicedList(customerId.value); |
| | | shippedGridApi.setGridOptions({ data: list }); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** 页ç¾åæ¢æ¶æ¸
空å¦ä¸é¡µç¾çæ§æ°æ®ï¼é¿å
çå°ä¸ä¸å®¢æ·çæ°æ® */ |
| | | function handleTabChange(key: any) { |
| | | activeTab.value = key; |
| | | void handleQuery(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Card class="mb-3"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="text-sm">客æ·</span> |
| | | <CrmCustomerSelect |
| | | v-model="customerId" |
| | | class="w-[280px]" |
| | | placeholder="è¯·éæ©å®¢æ·ï¼å·²åè´§æªå¼ç¥¨å¯ä¸éï¼" |
| | | /> |
| | | <Button type="primary" :loading="loading" @click="handleQuery"> |
| | | æ¥è¯¢ |
| | | </Button> |
| | | <template v-if="statement"> |
| | | <span class="text-muted-foreground text-sm"> |
| | | è®¢åæ»é¢ {{ formatAmount(statement.orderTotalAmount) }} å
/ |
| | | æªæ ¸éåºæ¶ |
| | | <span class="text-red-500"> |
| | | {{ formatAmount(statement.outstandingAmount) }} |
| | | </span> |
| | | å
|
| | | </span> |
| | | </template> |
| | | </div> |
| | | </Card> |
| | | |
| | | <Card size="small"> |
| | | <Tabs v-model:active-key="activeTab" @change="handleTabChange"> |
| | | <Tabs.TabPane key="statement" tab="客æ·å¯¹è´¦"> |
| | | <OrderGrid v-if="statement" table-title="客æ·å¯¹è´¦æç»" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="shipped" tab="å·²åè´§æªå¼ç¥¨"> |
| | | <ShippedGrid table-title="å·²åè´§æªå¼ç¥¨æç»" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Card> |
| | | </Page> |
| | | </template> |
| | |
| | | import { Button, Card, DatePicker, Col, Row, Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getShippedNotInvoicedList, |
| | | getUnwrittenOffList, |
| | | } from '#/api/crm/financeReport'; |
| | | import { |
| | | getStatisticsOverview, |
| | | getStatisticsTrend, |
| | | } from '#/api/erp/finance/statistics'; |
| | |
| | | const loading = ref(false); |
| | | const overview = ref<FinStatisticsApi.StatisticsOverview>(); |
| | | const trendList = ref<FinStatisticsApi.StatisticsTrend[]>([]); |
| | | /** åºæ¶é£é©ï¼å·²åè´§æªå¼ç¥¨ãæªæ ¸éåºæ¶ */ |
| | | const shippedNotInvoicedAmount = ref(0); |
| | | const unwrittenOffAmount = ref(0); |
| | | const receivableRiskCards = computed(() => [ |
| | | { |
| | | title: 'å·²åè´§æªå¼ç¥¨', |
| | | value: shippedNotInvoicedAmount.value, |
| | | color: '#f59e0b', |
| | | tip: 'éå®å·²åºåºä½å°æªå¼ç¥¨çéé¢å计', |
| | | }, |
| | | { |
| | | title: 'æªæ ¸éåºæ¶', |
| | | value: unwrittenOffAmount.value, |
| | | color: '#d03050', |
| | | tip: '审æ¹éè¿å¼ç¥¨ä¸å°æªå款çéé¢å计', |
| | | }, |
| | | ]); |
| | | |
| | | /** æ¥è¯¢æ¡ä»¶ï¼é»è®¤æ¬å¹´ 1 æè³å½åæ */ |
| | | const startMonth = ref<string>(); |
| | |
| | | startMonth: startMonth.value, |
| | | endMonth: endMonth.value, |
| | | }; |
| | | const [overviewData, trendData] = await Promise.all([ |
| | | getStatisticsOverview(params), |
| | | getStatisticsTrend(params), |
| | | ]); |
| | | const [overviewData, trendData, shippedList, unwrittenList] = |
| | | await Promise.all([ |
| | | getStatisticsOverview(params), |
| | | getStatisticsTrend(params), |
| | | getShippedNotInvoicedList(), |
| | | getUnwrittenOffList(), |
| | | ]); |
| | | overview.value = overviewData; |
| | | trendList.value = trendData ?? []; |
| | | shippedNotInvoicedAmount.value = (shippedList ?? []).reduce( |
| | | (sum, row) => sum + Number(row.remainInvoiceAmount ?? 0), |
| | | 0, |
| | | ); |
| | | unwrittenOffAmount.value = (unwrittenList ?? []).reduce( |
| | | (sum, row) => sum + Number(row.remainAmount ?? 0), |
| | | 0, |
| | | ); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | |
| | | </Col> |
| | | </Row> |
| | | |
| | | <!-- åºæ¶é£é© --> |
| | | <div> |
| | | <div class="mb-2 text-muted-foreground">åºæ¶é£é©ï¼æ¶ç¹æ°ï¼</div> |
| | | <div class="grid grid-cols-2 gap-4 max-sm:grid-cols-1"> |
| | | <Card |
| | | v-for="item in receivableRiskCards" |
| | | :key="item.title" |
| | | :body-style="{ padding: '12px 16px 16px' }" |
| | | class="h-full shadow-sm" |
| | | :style="{ borderTop: `3px solid ${item.color}` }" |
| | | > |
| | | <div class="mb-2 text-muted-foreground">{{ item.title }}</div> |
| | | <div class="text-2xl font-bold" :style="{ color: item.color }"> |
| | | {{ formatAmount(item.value) }} |
| | | </div> |
| | | <div class="mt-2 text-xs text-muted-foreground"> |
| | | {{ item.tip }} |
| | | </div> |
| | | </Card> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æåº¦è¶å¿ --> |
| | | <TrendChart :list="trendList" :loading="loading" /> |
| | | </div> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { CrmFinanceReportApi } from '#/api/crm/financeReport'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Button, Card, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getUnwrittenOffList } from '#/api/crm/financeReport'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | |
| | | import { formatAmount, useWriteOffColumns } from '../reconciliation/data'; |
| | | |
| | | defineOptions({ name: 'FinWriteOff' }); |
| | | |
| | | const customerId = ref<number>(); |
| | | const loading = ref(false); |
| | | const list = ref<CrmFinanceReportApi.UnwrittenOff[]>([]); |
| | | |
| | | /** æ±æ» */ |
| | | const summary = computed(() => { |
| | | const invoiceAmount = list.value.reduce( |
| | | (sum, row) => sum + Number(row.invoiceAmount ?? 0), |
| | | 0, |
| | | ); |
| | | const collectedAmount = list.value.reduce( |
| | | (sum, row) => sum + Number(row.collectedAmount ?? 0), |
| | | 0, |
| | | ); |
| | | const remainAmount = list.value.reduce( |
| | | (sum, row) => sum + Number(row.remainAmount ?? 0), |
| | | 0, |
| | | ); |
| | | return { invoiceAmount, collectedAmount, remainAmount, count: list.value.length }; |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useWriteOffColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | pagerConfig: { enabled: false }, |
| | | rowConfig: { keyField: 'invoiceId', isHover: true }, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<CrmFinanceReportApi.UnwrittenOff>, |
| | | }); |
| | | |
| | | /** æ¥è¯¢æªæ ¸éåºæ¶ */ |
| | | async function handleQuery() { |
| | | loading.value = true; |
| | | try { |
| | | list.value = await getUnwrittenOffList(customerId.value); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | if (list.value.length === 0) { |
| | | message.info('å½åç鿡件䏿²¡ææªæ ¸éçåºæ¶'); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(handleQuery); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Card class="mb-3"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="text-sm">客æ·</span> |
| | | <CrmCustomerSelect |
| | | v-model="customerId" |
| | | class="w-[280px]" |
| | | placeholder="ä¸éåç»è®¡å
¨é¨å®¢æ·" |
| | | /> |
| | | <Button type="primary" :loading="loading" @click="handleQuery"> |
| | | æ¥è¯¢ |
| | | </Button> |
| | | <span class="text-muted-foreground text-sm"> |
| | | å
± {{ summary.count }} å¼ å票ï¼å¼ç¥¨ |
| | | {{ formatAmount(summary.invoiceAmount) }} å
ï¼å·²å款 |
| | | {{ formatAmount(summary.collectedAmount) }} å
ï¼æªæ ¸é |
| | | <span class="text-red-500">{{ formatAmount(summary.remainAmount) }}</span> |
| | | å
|
| | | </span> |
| | | </div> |
| | | </Card> |
| | | <Grid table-title="æªæ ¸éåºæ¶æç»" /> |
| | | </Page> |
| | | </template> |
| | |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import ItemReceiptSelect from './modules/item-receipt-select.vue'; |
| | | import PurchaseOrderSelect from './modules/purchase-order-select.vue'; |
| | | |
| | | /** 表åç±»å */ |
| | |
| | | | undefined, |
| | | ) => { |
| | | form.setFieldValue('supplierName', order?.supplierName); |
| | | form.setFieldValue('itemReceiptId', undefined); |
| | | if (orderRemainInvoicePrice) { |
| | | orderRemainInvoicePrice.value = order?.remainInvoicePrice; |
| | | } |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'arrivalNoticeId', |
| | | fieldName: 'itemReceiptId', |
| | | label: 'å
¥åºå', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥æå
³èå
¥åºåå·' }, |
| | | component: markRaw(ItemReceiptSelect), |
| | | help: 'å
ééè´è®¢åï¼åä»è¯¥è®¢åå·²å
¥åºãæªåæ¶çå
¥åºåä¸éæ©ï¼ä¸éä¹å¯ä¿åï¼ä»
è¡¨ç¤ºæ¬æ¬¡æ¥ç¥¨ä¸ç»å®å
·ä½å
¥åºå', |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | disabled: (values) => !!values.id, |
| | | }, |
| | | componentProps: (values) => ({ |
| | | purchaseOrderId: values.purchaseOrderId, |
| | | id: values.id, |
| | | placeholder: 'è¯·éæ©å
¥åºå', |
| | | }), |
| | | }, |
| | | { |
| | | fieldName: 'invoiceNo', |
| | |
| | | slots: { default: 'orderNo' }, |
| | | }, |
| | | { |
| | | title: 'å
¥åºåå·', |
| | | field: 'itemReceiptCode', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | title: 'å票å·ç ', |
| | | field: 'invoiceNo', |
| | | minWidth: 150, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, message, Modal } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getItemReceiptList } from '#/api/erp/purchase/invoice'; |
| | | |
| | | defineOptions({ name: 'ErpPurchaseInvoiceItemReceiptSelect' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | id?: number; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | | purchaseOrderId?: number; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©å
¥åºå', |
| | | purchaseOrderId: undefined, |
| | | }, |
| | | ); |
| | | const emit = defineEmits<{ |
| | | change: [receipt: ErpPurchaseInvoiceApi.ItemReceipt | undefined]; |
| | | 'update:modelValue': [value: number | undefined]; |
| | | }>(); |
| | | |
| | | const open = ref(false); |
| | | const receipt = ref<ErpPurchaseInvoiceApi.ItemReceipt>(); |
| | | const displayCode = computed(() => receipt.value?.code ?? ''); |
| | | |
| | | /** éè´è®¢ååå忏
空已éå
¥åºå */ |
| | | watch( |
| | | () => props.purchaseOrderId, |
| | | () => { |
| | | receipt.value = undefined; |
| | | }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | async (value) => { |
| | | if (!value) { |
| | | receipt.value = undefined; |
| | | return; |
| | | } |
| | | if (receipt.value?.id === value) { |
| | | return; |
| | | } |
| | | if (!props.purchaseOrderId) { |
| | | return; |
| | | } |
| | | const list = await getItemReceiptList(props.purchaseOrderId, props.id); |
| | | receipt.value = list.find((item) => item.id === value); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'radio', width: 50, fixed: 'left' }, |
| | | { field: 'code', title: 'å
¥åºåå·', width: 200, fixed: 'left' }, |
| | | { field: 'name', title: 'å
¥åºååç§°', minWidth: 160 }, |
| | | { |
| | | field: 'receiptDate', |
| | | title: 'å
¥åºæ¥æ', |
| | | width: 160, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'totalPrice', |
| | | title: 'å
¥åºéé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'invoicedPrice', |
| | | title: 'å·²æ¥ç¥¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'remainInvoicePrice', |
| | | title: '坿¥ç¥¨éé¢', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | ], |
| | | height: 480, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async () => { |
| | | if (!props.purchaseOrderId) { |
| | | return { list: [], total: 0 }; |
| | | } |
| | | const list = await getItemReceiptList( |
| | | props.purchaseOrderId, |
| | | props.id, |
| | | ); |
| | | return { list, total: list.length }; |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | radioConfig: { |
| | | trigger: 'row', |
| | | highlight: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: false, |
| | | search: false, |
| | | }, |
| | | } as VxeTableGridOptions<ErpPurchaseInvoiceApi.ItemReceipt>, |
| | | gridEvents: { |
| | | radioChange: ({ row }: { row: ErpPurchaseInvoiceApi.ItemReceipt }) => { |
| | | receipt.value = row; |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | function handleOpen() { |
| | | if (props.disabled || !props.purchaseOrderId) { |
| | | if (!props.purchaseOrderId) { |
| | | message.warning('请å
éæ©éè´è®¢åï¼å鿩坹åºçå
¥åºå'); |
| | | } |
| | | return; |
| | | } |
| | | open.value = true; |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleOk() { |
| | | if (!receipt.value?.id) { |
| | | message.warning('è¯·éæ©å
¥åºå'); |
| | | return; |
| | | } |
| | | emit('update:modelValue', receipt.value.id); |
| | | emit('change', receipt.value); |
| | | open.value = false; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <Input |
| | | readonly |
| | | :disabled="disabled || !purchaseOrderId" |
| | | :placeholder="purchaseOrderId ? placeholder : '请å
éæ©éè´è®¢å'" |
| | | :value="displayCode" |
| | | @click="handleOpen" |
| | | > |
| | | <template #suffix> |
| | | <IconifyIcon |
| | | class="size-4" |
| | | icon="lucide:search" |
| | | :style="{ |
| | | cursor: |
| | | disabled || !purchaseOrderId ? 'not-allowed' : 'pointer', |
| | | }" |
| | | /> |
| | | </template> |
| | | </Input> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="éæ©å
¥åºåï¼ææééè´è®¢åè¿æ»¤ï¼" |
| | | width="75%" |
| | | @cancel.stop="open = false" |
| | | @ok.stop="handleOk" |
| | | > |
| | | <Grid table-title="å
¥åºåå表" /> |
| | | </Modal> |
| | | </div> |
| | | </template> |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE, getDictOptions } from '@vben/constants'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | |
| | | allowClear: true, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'saleType', |
| | | label: 'éå®ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©éå®ç±»å', |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.ERP_SALE_TYPE, 'number'), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractNo', |
| | | label: 'ååå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '坿工填åååå·ï¼å
³è CRM ååæ¶èªå¨å¸¦åºï¼', |
| | | maxlength: 64, |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'saleUserId', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractNo', |
| | | label: 'ååå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ååå·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'saleType', |
| | | label: 'éå®ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.ERP_SALE_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©éå®ç±»å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'creator', |
| | | label: 'å建人', |
| | | component: 'ApiSelect', |
| | |
| | | }, |
| | | { |
| | | field: 'contractNo', |
| | | title: 'å
³èåå', |
| | | title: 'ååå·', |
| | | minWidth: 150, |
| | | slots: { default: 'contractNo' }, |
| | | }, |
| | | { |
| | | field: 'saleType', |
| | | title: 'éå®ç±»å', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.ERP_SALE_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'productCodes', |
| | | title: '产åç¼å·', |
| | | minWidth: 140, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { |
| | | field: 'producedCount', |
| | | title: 'ç产æ°é', |
| | | minWidth: 110, |
| | | formatter: 'formatAmount3', |
| | | }, |
| | | { |
| | | field: 'orderTime', |
| | | title: 'è®¢åæ¶é´', |
| | | width: 160, |
| | |
| | | <a v-if="row.contractId" class="text-primary cursor-pointer" @click="handleViewContract(row)"> |
| | | {{ row.contractNo }} |
| | | </a> |
| | | <span v-else>-</span> |
| | | <span v-else>{{ row.contractNo || '-' }}</span> |
| | | </template> |
| | | <template #receiptDate="{ row }"> |
| | | <Tooltip |
| | |
| | | <Tabs.TabPane key="emergencyContact" tab="ç´§æ¥è系人"> |
| | | <EmergencyContactList form-type="detail" :employee-id="employee.id" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="socialSecurity" tab="社ä¿å
¬ç§¯éæ¡£æ¡"> |
| | | <SocialSecurityList form-type="detail" :employee-id="employee.id" :user-id="employee.userId" /> |
| | | <Tabs.TabPane key="socialSecurity" tab="é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡"> |
| | | <SocialSecurityList :dept-id="employee.deptId" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="contract" tab="åå·¥åå"> |
| | | <ContractList :employee-id="employee.id" /> |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from './form.vue'; |
| | | import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security'; |
| | | |
| | | import { ref, computed, watch } from 'vue'; |
| | | import { ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { Alert, Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { message, Table, Tag, Button } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction } from '#/adapter/vxe-table'; |
| | | import { |
| | | createEmployeeSocialSecurity, |
| | | deleteEmployeeSocialSecurity, |
| | | getEmployeeSocialSecurityListByUser, |
| | | updateEmployeeSocialSecurity, |
| | | } from '#/api/hrm/salary/employee-social-security'; |
| | | import { getSocialSecuritySchemeList } from '#/api/hrm/salary/social-security-scheme'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import SocialSecurityForm from './social-security-form.vue'; |
| | | import { getEmployeeSocialSecurityListByDept } from '#/api/hrm/salary/employee-social-security'; |
| | | |
| | | const props = defineProps<{ |
| | | employeeId?: number; |
| | | userId?: number; |
| | | formType?: FormType; |
| | | deptId?: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | const list = ref<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>([]); |
| | | const schemeList = ref<any[]>([]); |
| | | const loading = ref(false); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: SocialSecurityForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°å表 */ |
| | | /** å è½½åå·¥æå¨é¨é¨ç社ä¿å
¬ç§¯éæ¡£æ¡ï¼æé¨é¨å
±ç¨ï¼æ¤å¤åªè¯»ï¼ */ |
| | | async function handleRefresh() { |
| | | if (!props.userId) return; |
| | | if (!props.deptId) { |
| | | list.value = []; |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | list.value = await getEmployeeSocialSecurityListByUser(props.userId); |
| | | list.value = await getEmployeeSocialSecurityListByDept(props.deptId); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | emit('refresh'); |
| | | } |
| | | |
| | | /** æ°å¢æ¡£æ¡ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ |
| | | formType: 'create', |
| | | userId: props.userId, |
| | | schemeList: schemeList.value, |
| | | }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æ¡£æ¡ */ |
| | | function handleEdit(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) { |
| | | formModalApi.setData({ |
| | | formType: 'update', |
| | | id: row.id, |
| | | userId: props.userId, |
| | | schemeList: schemeList.value, |
| | | }).open(); |
| | | } |
| | | |
| | | /** å 餿¡£æ¡ */ |
| | | async function handleDelete(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) { |
| | | await deleteEmployeeSocialSecurity(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['社ä¿å
¬ç§¯éæ¡£æ¡'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** è·åæ¹æ¡å表 */ |
| | | async function getSchemeList() { |
| | | schemeList.value = await getSocialSecuritySchemeList({ status: 0 }); |
| | | } |
| | | |
| | | /** çå¬ userId åå */ |
| | | watch( |
| | | () => props.userId, |
| | | (val) => { |
| | | if (val) { |
| | | handleRefresh(); |
| | | getSchemeList(); |
| | | } |
| | | }, |
| | | () => props.deptId, |
| | | () => handleRefresh(), |
| | | { immediate: true }, |
| | | ); |
| | | |
| | |
| | | width: 80, |
| | | key: 'status', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | key: 'action', |
| | | width: 150, |
| | | fixed: 'right', |
| | | }, |
| | | ]; |
| | | |
| | | const isDetail = computed(() => props.formType === 'detail'); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="social-security-list"> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Alert |
| | | message="社ä¿å
¬ç§¯éæ¡£æ¡æé¨é¨ç»´æ¤ï¼é¨é¨ä¸ææåå·¥å
±ç¨ææ°çæçä¸ä»½ï¼å¦éè°æ´ï¼è¯·å°ãèªé
¬ç®¡ç - é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ãä¿®æ¹ã" |
| | | type="info" |
| | | show-icon |
| | | style="margin-bottom: 12px" |
| | | /> |
| | | |
| | | <!-- å·¥å
·æ --> |
| | | <div v-if="!isDetail" class="mb-3"> |
| | | <Button type="primary" @click="handleCreate"> |
| | | æ°å¢ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ |
| | | </Button> |
| | | </div> |
| | | |
| | | <!-- è¡¨æ ¼ --> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="list" |
| | |
| | | {{ record.status === 0 ? 'å¯ç¨' : 'ç¦ç¨' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action' && !isDetail"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | onClick: handleEdit.bind(null, record), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['社ä¿å
¬ç§¯éæ¡£æ¡']), |
| | | confirm: handleDelete.bind(null, record), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | |
| | | .social-security-list { |
| | | padding: 0; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['hrm:resignation:update'], |
| | | ifShow: row.status === 0, |
| | | ifShow: row.status === 0 || row.status === 30, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤', |
| | | type: 'link', |
| | | auth: ['hrm:resignation:submit'], |
| | | ifShow: row.status === 0, |
| | | ifShow: row.status === 0 || row.status === 30, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | | </template> |
| | |
| | | { field: 'mealAllowance', title: 'é¤è¡¥', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'transportAllowance', title: '交é补贴', width: 80, formatter: 'formatAmount2' }, |
| | | { field: 'otherAllowance', title: 'å
¶ä»è¡¥è´´', width: 80, formatter: 'formatAmount2' }, |
| | | { |
| | | field: 'workWage', |
| | | title: 'æ¥å·¥å·¥èµ', |
| | | width: 130, |
| | | slots: { |
| | | default: 'workWage', |
| | | }, |
| | | }, |
| | | { field: 'socialSecurityDeduction', title: 'ç¤¾ä¿æ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'housingFundDeduction', title: 'å
¬ç§¯éæ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'taxDeduction', title: 'ä¸ªç¨æ£æ¬¾', width: 100, formatter: 'formatAmount2' }, |
| | |
| | | { title: 'é¤è¡¥', dataIndex: 'mealAllowance', width: 100 }, |
| | | { title: '交é补贴', dataIndex: 'transportAllowance', width: 100 }, |
| | | { title: 'å
¶ä»è¡¥è´´', dataIndex: 'otherAllowance', width: 100 }, |
| | | { title: 'æ¥å·¥å·¥èµ', dataIndex: 'workWage', width: 140 }, |
| | | { title: 'ç¤¾ä¿æ£æ¬¾', dataIndex: 'socialSecurityDeduction', width: 110 }, |
| | | { title: 'å
¬ç§¯éæ£æ¬¾', dataIndex: 'housingFundDeduction', width: 110 }, |
| | | { title: 'ä¸ªç¨æ£æ¬¾', dataIndex: 'taxDeduction', width: 110 }, |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert, Tag } from 'ant-design-vue'; |
| | | import { Alert, Button, DatePicker, Form, FormItem, InputNumber, message, Modal, Table, Tag, TreeSelect } from 'ant-design-vue'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictLabel } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | calculateSalary, |
| | | confirmSalary, |
| | | deleteSalaryCalculation, |
| | | exportSalaryCalculation, |
| | | getSalaryCalculationPage, |
| | | getWorkWageDetail, |
| | | previewSalaryCalculation, |
| | | revokeSalaryCalculation, |
| | | saveSalaryCalculation, |
| | |
| | | const previewLoading = ref(false); |
| | | const previewData = ref<HrmSalaryCalculationApi.SalaryCalculation[]>([]); |
| | | const submitLoading = ref(false); |
| | | const calculateLoading = ref(false); |
| | | /** 已宿é¢è§çæ¡ä»¶ï¼å¨æ+é¨é¨ï¼ï¼ä¸ºç©ºè¡¨ç¤ºè¿æ²¡é¢è§ææ¡ä»¶å·²åå */ |
| | | const previewSignature = ref(''); |
| | | const canSubmit = computed( |
| | | () => previewSignature.value !== '' && previewData.value.length > 0, |
| | | ); |
| | | |
| | | function buildQuerySignature() { |
| | | return `${calculatePeriod.value ?? ''}|${calculateDeptId.value ?? ''}`; |
| | | } |
| | | |
| | | /** æ¸
空é¢è§ç»æï¼å¼ºå¶éæ°é¢è§åæè½ä¿å */ |
| | | function invalidatePreview() { |
| | | previewData.value = []; |
| | | previewSignature.value = ''; |
| | | } |
| | | |
| | | watch([calculatePeriod, calculateDeptId], () => { |
| | | if (buildQuerySignature() !== previewSignature.value) { |
| | | invalidatePreview(); |
| | | } |
| | | }); |
| | | |
| | | // æå¼æ ¸ç®å¼¹çª |
| | | async function openCalculateModal() { |
| | | const formValues = await gridApi.formApi.getValues(); |
| | | calculatePeriod.value = formValues.period; |
| | | calculateDeptId.value = formValues.deptId; |
| | | previewData.value = []; |
| | | invalidatePreview(); |
| | | // å è½½é¨é¨æ |
| | | const data = await getDeptList(); |
| | | deptTreeData.value = handleTree(data); |
| | |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ§è¡èªé
¬æ ¸ç®ï¼èªå¨è®¡ç®å¹¶ä¿åï¼ */ |
| | | async function handleCalculate() { |
| | | if (!calculatePeriod.value) { |
| | | message.warning('è¯·éæ©æ ¸ç®å¨æ'); |
| | | return; |
| | | } |
| | | calculateLoading.value = true; |
| | | try { |
| | | await calculateSalary({ |
| | | period: calculatePeriod.value, |
| | | deptId: calculateDeptId.value, |
| | | }); |
| | | message.success('èªé
¬æ ¸ç®æ§è¡æå'); |
| | | calculateVisible.value = false; |
| | | handleRefresh(); |
| | | } finally { |
| | | calculateLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** é¢è§æ ¸ç®æ°æ® */ |
| | | /** é¢è§æ ¸ç®æ°æ®ï¼ä¿ååå¿
é¡»å
宿é¢è§ */ |
| | | async function handlePreview() { |
| | | if (!calculatePeriod.value) { |
| | | message.warning('è¯·éæ©æ ¸ç®å¨æ'); |
| | | message.warning('请å
éæ©æ ¸ç®å¨æï¼åé¢è§æ ¸ç®æ°æ®'); |
| | | return; |
| | | } |
| | | const signature = buildQuerySignature(); |
| | | previewLoading.value = true; |
| | | try { |
| | | previewData.value = await previewSalaryCalculation({ |
| | | const list = await previewSalaryCalculation({ |
| | | period: calculatePeriod.value, |
| | | deptId: calculateDeptId.value, |
| | | }); |
| | | // é¢è§æé´æ¡ä»¶è¢«ä¿®æ¹æ¶ä¸¢å¼æ¬æ¬¡ç»æï¼é¿å
ç¨æ§æ¡ä»¶çæ°æ®ä¿å |
| | | if (signature !== buildQuerySignature()) { |
| | | return; |
| | | } |
| | | previewData.value = list ?? []; |
| | | if (previewData.value.length === 0) { |
| | | message.info('该æ¡ä»¶ä¸æ²¡æéè¦æ ¸ç®çåå·¥'); |
| | | previewSignature.value = ''; |
| | | message.info('è¯¥å¨æ/é¨é¨ä¸æ²¡æéè¦æ ¸ç®çåå·¥ï¼è¯·è°æ´æ¡ä»¶åéæ°é¢è§'); |
| | | } else { |
| | | previewSignature.value = signature; |
| | | message.success(`é¢è§å®æï¼å
± ${previewData.value.length} 人ï¼è¯·æ ¸å¯¹åä¿å`); |
| | | } |
| | | } finally { |
| | | previewLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | // ==================== æ¥å·¥å·¥èµæç» ==================== |
| | | const wageDetailVisible = ref(false); |
| | | const wageDetailLoading = ref(false); |
| | | const wageDetailList = ref<HrmSalaryCalculationApi.WorkWageDetail[]>([]); |
| | | /** é»è®¤å±å¼æææ¥å·¥è¡ï¼ç´æ¥å±ç¤ºèªå®ä¹å·¥èµå段æç» */ |
| | | const wageDetailExpandedKeys = ref<Array<number | string>>([]); |
| | | const wageDetailTitle = ref(''); |
| | | const wageDetailPeriod = ref(''); |
| | | /** å½åæ ¸ç®è¡ä¸çãæ¥å·¥å·¥èµãï¼ç¨äºåæç»å计æ¯å¯¹ */ |
| | | const wageDetailExpected = ref(0); |
| | | |
| | | /** 计ç®è§è²ææ¡ï¼ä¸å·¥åºå·¥èµå段ç计ç®è§è²ä¿æä¸è´ */ |
| | | const WAGE_ROLE_LABELS: Record<number, string> = { |
| | | 1: 'ç³»æ°', |
| | | 2: 'å åéé¢', |
| | | 3: '计价æ°é', |
| | | 4: 'ä»
è®°å½', |
| | | }; |
| | | const WAGE_ROLE_COLORS: Record<number, string> = { |
| | | 1: 'blue', |
| | | 2: 'orange', |
| | | 3: 'green', |
| | | 4: 'default', |
| | | }; |
| | | |
| | | const wageDetailColumns = [ |
| | | { title: 'æ¥å·¥åå·', dataIndex: 'feedbackNo', width: 170 }, |
| | | { title: 'å·¥åº', dataIndex: 'processName', width: 150 }, |
| | | { title: 'æ¥å·¥æ¶é´', dataIndex: 'feedbackTime', width: 160 }, |
| | | { title: 'æ¥å·¥æ°é', dataIndex: 'feedbackQuantity', width: 100 }, |
| | | { title: 'åæ ¼æ°é', dataIndex: 'qualifiedQuantity', width: 100 }, |
| | | { title: '主工èµ', dataIndex: 'mainWage', width: 100 }, |
| | | { title: 'è¾
å©å·¥èµ', dataIndex: 'auxWage', width: 100 }, |
| | | { title: 'åæ®µè°æ´', dataIndex: 'fieldWage', width: 100 }, |
| | | { title: 'è´¨éè°æ´', dataIndex: 'qualityAdjustment', width: 100 }, |
| | | { title: 'æç»å·¥èµ', dataIndex: 'finalWage', width: 110 }, |
| | | ]; |
| | | |
| | | const wageFieldColumns = [ |
| | | { title: 'åæ®µç¼ç ', dataIndex: 'code', width: 140 }, |
| | | { title: 'åæ®µåç§°', dataIndex: 'name', width: 160 }, |
| | | { title: '计ç®è§è²', dataIndex: 'role', width: 110 }, |
| | | { title: 'åæ®µå¼', dataIndex: 'value', width: 120 }, |
| | | { title: 'åä»·', dataIndex: 'unitPrice', width: 110 }, |
| | | { title: '计å
¥éé¢', dataIndex: 'amount', width: 110 }, |
| | | ]; |
| | | |
| | | /** éé¢å±ç¤ºï¼ç©ºå¼æ¾ç¤º 0.00 */ |
| | | function fmtAmount(value?: null | number) { |
| | | return Number(value ?? 0).toFixed(2); |
| | | } |
| | | |
| | | /** æç»å计ï¼åæ¥å·¥åæç»å·¥èµä¹åï¼ */ |
| | | const wageDetailTotal = computed(() => |
| | | wageDetailList.value.reduce((sum, item) => sum + Number(item.finalWage ?? 0), 0), |
| | | ); |
| | | |
| | | /** |
| | | * æå¼æ¥å·¥å·¥èµæç» |
| | | * @param record æ ¸ç®è¡ï¼æ¥èªé¢è§å¼¹çªæåè¡¨ï¼ |
| | | */ |
| | | async function openWageDetail(record: HrmSalaryCalculationApi.SalaryCalculation) { |
| | | const period = record.period || calculatePeriod.value; |
| | | if (!period) { |
| | | message.warning('ç¼ºå°æ ¸ç®å¨æï¼æ æ³æ¥çæ¥å·¥å·¥èµæç»ï¼è¯·å
éæ©æ ¸ç®å¨æåéè¯'); |
| | | return; |
| | | } |
| | | if (!record.userId) { |
| | | message.warning('缺å°å工信æ¯ï¼æ æ³æ¥çæ¥å·¥å·¥èµæç»ï¼è¯·å·æ°å表åéè¯'); |
| | | return; |
| | | } |
| | | wageDetailTitle.value = `${record.userName ?? ''} ${period} æ¥å·¥å·¥èµæç»`; |
| | | wageDetailPeriod.value = period; |
| | | wageDetailExpected.value = Number(record.workWage ?? 0); |
| | | wageDetailList.value = []; |
| | | wageDetailExpandedKeys.value = []; |
| | | wageDetailVisible.value = true; |
| | | wageDetailLoading.value = true; |
| | | try { |
| | | wageDetailList.value = (await getWorkWageDetail({ period, userId: record.userId })) ?? []; |
| | | // é»è®¤å±å¼å
¨é¨æ¥å·¥åï¼å段æç»æ éåç¹å¼ |
| | | wageDetailExpandedKeys.value = wageDetailList.value |
| | | .map((item) => item.feedbackId) |
| | | .filter((id): id is number => id !== undefined && id !== null); |
| | | } finally { |
| | | wageDetailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | |
| | | (record.overtimePay || 0) + |
| | | (record.mealAllowance || 0) + |
| | | (record.transportAllowance || 0) + |
| | | (record.otherAllowance || 0); |
| | | (record.otherAllowance || 0) + |
| | | (record.workWage || 0); |
| | | const deduction = (record.socialSecurityDeduction || 0) + |
| | | (record.housingFundDeduction || 0) + |
| | | (record.taxDeduction || 0) + |
| | |
| | | message.warning('è¯·éæ©æ ¸ç®å¨æ'); |
| | | return; |
| | | } |
| | | if (previewData.value.length === 0) { |
| | | message.warning('请å
é¢è§æ ¸ç®æ°æ®'); |
| | | if (!canSubmit.value) { |
| | | message.warning('请å
ç¹å»âé¢è§æ ¸ç®âçææ°æ®ï¼æ ¸å¯¹æ 误ååä¿å'); |
| | | return; |
| | | } |
| | | submitLoading.value = true; |
| | |
| | | /** åæ¶å¼¹çª */ |
| | | function handleCancel() { |
| | | calculateVisible.value = false; |
| | | previewData.value = []; |
| | | invalidatePreview(); |
| | | } |
| | | |
| | | /** 确认èªé
¬æ ¸ç® */ |
| | |
| | | <Modal |
| | | v-model:open="calculateVisible" |
| | | title="èªé
¬æ ¸ç®" |
| | | width="1200px" |
| | | width="1800px" |
| | | :footer="null" |
| | | :maskClosable="false" |
| | | > |
| | | <Alert |
| | | message="ä¸¤ç§æ ¸ç®æ¹å¼ï¼1. æ§è¡æ ¸ç® - èªå¨è®¡ç®å¹¶ä¿åï¼2. é¢è§æ°æ® - æå¨è°æ´åä¿å" |
| | | message="æä½æ¥éª¤ï¼éæ©æ ¸ç®å¨æï¼åé¨é¨ï¼â ç¹å»âé¢è§æ ¸ç®âçææ°æ® â æ ¸å¯¹å¹¶è°æ´ â ä¿åæ ¸ç®ãæªé¢è§æä¿®æ¹æ¡ä»¶åä¸è½ä¿åã" |
| | | type="info" |
| | | show-icon |
| | | style="margin-bottom: 16px" |
| | |
| | | /> |
| | | </FormItem> |
| | | <FormItem> |
| | | <a-button type="primary" :loading="calculateLoading" @click="handleCalculate"> |
| | | æ§è¡æ ¸ç® |
| | | </a-button> |
| | | <Button type="primary" :loading="previewLoading" @click="handlePreview"> |
| | | é¢è§æ ¸ç® |
| | | </Button> |
| | | </FormItem> |
| | | <FormItem> |
| | | <a-button :loading="previewLoading" @click="handlePreview"> |
| | | é¢è§æ°æ® |
| | | </a-button> |
| | | <FormItem v-if="canSubmit"> |
| | | <span class="text-xs text-green-600"> |
| | | å·²æå½åæ¡ä»¶é¢è§ {{ previewData.length }} 人ï¼å¯è°æ´åä¿å |
| | | </span> |
| | | </FormItem> |
| | | </Form> |
| | | |
| | |
| | | :columns="usePreviewColumns()" |
| | | :data-source="previewData" |
| | | :pagination="{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `å
± ${t} 人` }" |
| | | :scroll="{ x: 1800, y: 400 }" |
| | | :scroll="{ x: 1920, y: 420 }" |
| | | size="small" |
| | | row-key="userId" |
| | | bordered |
| | |
| | | style="width: 80px" |
| | | @change="(val: number) => updateFieldValue(record, 'otherAllowance', val)" |
| | | /> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'workWage'"> |
| | | <div class="flex items-center gap-1"> |
| | | <InputNumber |
| | | v-model:value="record.workWage" |
| | | :min="0" |
| | | :precision="2" |
| | | size="small" |
| | | style="width: 82px" |
| | | @change="(val) => updateFieldValue(record, 'workWage', Number(val ?? 0))" |
| | | /> |
| | | <a class="text-xs whitespace-nowrap" @click="openWageDetail(record)">æç»</a> |
| | | </div> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'socialSecurityDeduction'"> |
| | | <InputNumber |
| | |
| | | </template> |
| | | </Table> |
| | | |
| | | <div class="flex justify-end gap-2" v-if="previewData.length > 0"> |
| | | <a-button @click="handleCancel">åæ¶</a-button> |
| | | <a-button type="primary" :loading="submitLoading" @click="handleSubmit"> |
| | | <div class="flex items-center justify-end gap-2"> |
| | | <span v-if="!canSubmit" class="mr-auto text-xs text-gray-500"> |
| | | 请å
éæ©æ ¸ç®å¨æå¹¶ç¹å»âé¢è§æ ¸ç®âï¼é¢è§å®æåæè½ä¿å |
| | | </span> |
| | | <Button @click="handleCancel">åæ¶</Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="submitLoading" |
| | | :disabled="!canSubmit" |
| | | @click="handleSubmit" |
| | | > |
| | | ä¿åæ ¸ç® |
| | | </a-button> |
| | | </Button> |
| | | </div> |
| | | </Modal> |
| | | |
| | | <!-- æ¥å·¥å·¥èµæç»å¼¹çª --> |
| | | <Modal |
| | | v-model:open="wageDetailVisible" |
| | | :title="`æ¥å·¥å·¥èµæç» - ${wageDetailTitle}`" |
| | | width="1200px" |
| | | :footer="null" |
| | | > |
| | | <Alert |
| | | type="info" |
| | | show-icon |
| | | class="mb-4" |
| | | :message="`ç»è®¡å£å¾ï¼æ ¸ç®å¨æ ${wageDetailPeriod} å
ï¼è¯¥åå·¥å䏿æå·²å®¡æ ¸ï¼æªå®¡æ ¸/å·²å®¡æ ¸ï¼åå·²å®æçæ¥å·¥åå·¥èµãé颿æ = ä¸»å·¥èµ + è¾
å©å·¥èµ + èªå®ä¹å·¥èµåæ®µè°æ´ + è´¨éè°æ´ã`" |
| | | /> |
| | | |
| | | <div class="mb-4 flex flex-wrap items-center gap-x-8 gap-y-2 text-sm"> |
| | | <span> |
| | | æ¥å·¥åï¼ |
| | | <span class="font-bold">{{ wageDetailList.length }}</span> æ¡ |
| | | </span> |
| | | <span> |
| | | æç»åè®¡ï¼ |
| | | <span class="font-bold text-green-600">Â¥{{ fmtAmount(wageDetailTotal) }}</span> |
| | | </span> |
| | | <span> |
| | | æ ¸ç®ä¸çæ¥å·¥å·¥èµï¼ |
| | | <span class="font-bold">Â¥{{ fmtAmount(wageDetailExpected) }}</span> |
| | | </span> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="!wageDetailLoading && Math.abs(wageDetailTotal - wageDetailExpected) > 0.005" |
| | | type="warning" |
| | | show-icon |
| | | class="mb-4" |
| | | :message="`æç»å计 Â¥${fmtAmount(wageDetailTotal)} 䏿 ¸ç®ä¸çæ¥å·¥å·¥èµ Â¥${fmtAmount(wageDetailExpected)} ä¸ä¸è´ï¼è¯¥å¼å¯è½å·²äººå·¥è°æ´ï¼å¦éæ¢å¤ç³»ç»è®¡ç®å¼ï¼è¯·éæ°é¢è§æ ¸ç®ã`" |
| | | /> |
| | | |
| | | <Table |
| | | v-model:expanded-row-keys="wageDetailExpandedKeys" |
| | | :columns="wageDetailColumns" |
| | | :data-source="wageDetailList" |
| | | :loading="wageDetailLoading" |
| | | :pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (t) => `å
± ${t} æ¡` }" |
| | | :scroll="{ x: 1200 }" |
| | | size="small" |
| | | row-key="feedbackId" |
| | | bordered |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.dataIndex === 'mainWage'"> |
| | | {{ fmtAmount(record.mainWage) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'auxWage'"> |
| | | {{ fmtAmount(record.auxWage) }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'fieldWage'"> |
| | | <span :class="Number(record.fieldWage ?? 0) === 0 ? 'text-gray-400' : ''"> |
| | | {{ fmtAmount(record.fieldWage) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'qualityAdjustment'"> |
| | | <span |
| | | :class="Number(record.qualityAdjustment ?? 0) < 0 ? 'text-red-500' : 'text-gray-500'" |
| | | > |
| | | {{ |
| | | Number(record.qualityAdjustment ?? 0) === 0 |
| | | ? '-' |
| | | : fmtAmount(record.qualityAdjustment) |
| | | }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'finalWage'"> |
| | | <span class="font-bold text-green-600">{{ fmtAmount(record.finalWage) }}</span> |
| | | </template> |
| | | </template> |
| | | |
| | | <template #expandedRowRender="{ record }"> |
| | | <div class="px-2 py-1"> |
| | | <div class="mb-2 text-xs text-gray-500"> |
| | | èªå®ä¹å·¥èµå段ï¼å¼ä¼å
åè¿ç¨æ£éªå¡«åå¼ï¼å
¶æ¬¡åå·¥åºåºå®å¼ï¼ |
| | | </div> |
| | | <Table |
| | | v-if="record.fields && record.fields.length > 0" |
| | | :columns="wageFieldColumns" |
| | | :data-source="record.fields" |
| | | :pagination="false" |
| | | size="small" |
| | | bordered |
| | | > |
| | | <template #bodyCell="{ column, record: field }"> |
| | | <template v-if="column.dataIndex === 'role'"> |
| | | <Tag :color="WAGE_ROLE_COLORS[field.role] ?? 'default'"> |
| | | {{ WAGE_ROLE_LABELS[field.role] ?? 'æªè®¾ç½®' }} |
| | | </Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'unitPrice'"> |
| | | {{ |
| | | field.unitPrice === undefined || field.unitPrice === null |
| | | ? '-' |
| | | : fmtAmount(field.unitPrice) |
| | | }} |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'amount'"> |
| | | <span :class="Number(field.amount ?? 0) < 0 ? 'text-red-500' : ''"> |
| | | {{ fmtAmount(field.amount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'value'"> |
| | | {{ |
| | | field.value === undefined || field.value === null || field.value === '' |
| | | ? '-' |
| | | : field.value |
| | | }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | <div v-else class="text-xs text-gray-400"> |
| | | 该æ¥å·¥åçå·¥åºæ²¡æé
ç½®èªå®ä¹å·¥èµå段 |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </Table> |
| | | </Modal> |
| | | |
| | | <Grid table-title="èªé
¬æ ¸ç®å表"> |
| | |
| | | {{ getDictLabel(DICT_TYPE.HRM_SALARY_STATUS, row.status) }} |
| | | </Tag> |
| | | </template> |
| | | <template #workWage="{ row }"> |
| | | <div class="flex items-center gap-1"> |
| | | <span>{{ fmtAmount(row.workWage) }}</span> |
| | | <a class="text-xs text-blue-600" @click="openWageDetail(row)">æç»</a> |
| | | </div> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security'; |
| | | |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import { getSocialSecuritySchemeList } from '#/api/hrm/salary/social-security-scheme'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹å工社ä¿å
¬ç§¯éæ¡£æ¡ç表å */ |
| | | /** æ°å¢/ä¿®æ¹é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'åå·¥', |
| | | component: 'ApiSelect', |
| | | fieldName: 'deptId', |
| | | label: 'é¨é¨', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | api: getEmployeeSimpleList, |
| | | api: async () => { |
| | | const data = await getDeptList(); |
| | | return handleTree(data); |
| | | }, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | childrenField: 'children', |
| | | showSearch: true, |
| | | filterOption: (input: string, option: any) => |
| | | option.label?.toLowerCase().includes(input.toLowerCase()), |
| | | placeholder: 'è¯·éæ©åå·¥', |
| | | allowClear: true, |
| | | treeDefaultExpandAll: true, |
| | | placeholder: 'è¯·éæ©é¨é¨', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | formItemClass: 'col-span-2', |
| | |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'userName', |
| | | label: 'åå·¥å§å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥åå·¥å§å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'deptId', |
| | | label: 'é¨é¨', |
| | | component: 'ApiTreeSelect', |
| | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>['columns'] { |
| | | return [ |
| | | { field: 'userName', title: 'åå·¥å§å', minWidth: 100 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 120 }, |
| | | { field: 'deptName', title: 'é¨é¨', minWidth: 160 }, |
| | | { field: 'schemeName', title: 'ç¤¾ä¿æ¹æ¡', minWidth: 180 }, |
| | | { field: 'socialSecurityBase', title: '社ä¿åºæ°', width: 100, formatter: 'formatAmount2' }, |
| | | { field: 'housingFundBase', title: 'å
¬ç§¯éåºæ°', width: 100, formatter: 'formatAmount2' }, |
| | |
| | | |
| | | /** æ¥çåå² */ |
| | | function handleHistory(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) { |
| | | historyModalApi.setData({ userId: row.userId, userName: row.userName }).open(); |
| | | historyModalApi.setData({ deptId: row.deptId, deptName: row.deptName }).open(); |
| | | } |
| | | |
| | | /** å 餿¡£æ¡ */ |
| | | async function handleDelete(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) { |
| | | await deleteEmployeeSocialSecurity(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['å工社ä¿å
¬ç§¯éæ¡£æ¡'])); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡'])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <HistoryModal /> |
| | | <Grid table-title="å工社ä¿å
¬ç§¯éæ¡£æ¡å表"> |
| | | <Grid table-title="é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å工社ä¿å
¬ç§¯éæ¡£æ¡']), |
| | | label: $t('ui.actionTitle.create', ['é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['hrm:employee-social-security:create'], |
| | |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['hrm:employee-social-security:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['å工社ä¿å
¬ç§¯éæ¡£æ¡']), |
| | | title: $t('ui.actionMessage.deleteConfirm', ['é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => |
| | | formType.value === 'update' ? 'ä¿®æ¹å工社ä¿å
¬ç§¯éæ¡£æ¡' : 'æ°å¢å工社ä¿å
¬ç§¯éæ¡£æ¡', |
| | | formType.value === 'update' ? 'ä¿®æ¹é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡' : 'æ°å¢é¨é¨ç¤¾ä¿å
¬ç§¯éæ¡£æ¡', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | |
| | | |
| | | import { Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getEmployeeSocialSecurityListByUser } from '#/api/hrm/salary/employee-social-security'; |
| | | import { getEmployeeSocialSecurityListByDept } from '#/api/hrm/salary/employee-social-security'; |
| | | |
| | | const historyList = ref<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>([]); |
| | | const userName = ref<string>(''); |
| | | const deptName = ref<string>(''); |
| | | |
| | | const columns = [ |
| | | { title: 'ç¤¾ä¿æ¹æ¡', dataIndex: 'schemeName', width: 180 }, |
| | |
| | | historyList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ userId: number; userName: string }>(); |
| | | userName.value = data.userName; |
| | | const data = modalApi.getData<{ deptId: number; deptName: string }>(); |
| | | deptName.value = data.deptName; |
| | | modalApi.lock(); |
| | | try { |
| | | historyList.value = await getEmployeeSocialSecurityListByUser(data.userId); |
| | | historyList.value = await getEmployeeSocialSecurityListByDept(data.deptId); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`${userName} - 社ä¿å
¬ç§¯éåå²è®°å½`" class="w-[600px]" :footer="null"> |
| | | <Modal :title="`${deptName} - 社ä¿å
¬ç§¯éåå²è®°å½`" class="w-[600px]" :footer="null"> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="historyList" |
| | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { message, Tag, DatePicker } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | DatePicker, |
| | | Form, |
| | | FormItem, |
| | | message, |
| | | Modal, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | </FormItem> |
| | | </Form> |
| | | <template #footer> |
| | | <a-button @click="generateVisible = false">åæ¶</a-button> |
| | | <a-button type="primary" :loading="generateLoading" @click="handleGenerate"> |
| | | <Button @click="generateVisible = false">åæ¶</Button> |
| | | <Button type="primary" :loading="generateLoading" @click="handleGenerate"> |
| | | ç¡®å® |
| | | </a-button> |
| | | </Button> |
| | | </template> |
| | | </Modal> |
| | | |
| | |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message, Table, Tag, InputNumber, Alert } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction } from '#/adapter/vxe-table'; |
| | | import { getSalaryPaymentDetailList, paySalaryDetail } from '#/api/hrm/salary/payment'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | |
| | | |
| | | const isPaid = computed(() => status.value === 10); |
| | | |
| | | // å宽å计å¿
é¡»ä¸ scroll.xï¼1400ï¼ä¸è´ï¼å¦åè¡¨æ ¼æä¼¸å fixed ååç§»ä¼ç®éï¼ç«çº¿å¯¹ä¸ä¸ |
| | | const columns = [ |
| | | { title: 'åå·¥å§å', dataIndex: 'userName', width: 100, fixed: 'left' }, |
| | | { title: 'é¨é¨', dataIndex: 'deptName', width: 120 }, |
| | | { title: 'åºæ¬å·¥èµ', dataIndex: 'baseSalary', width: 100 }, |
| | | { title: '绩æå·¥èµ', dataIndex: 'performanceSalary', width: 100 }, |
| | | { title: 'ç¤¾ä¿æ£æ¬¾', dataIndex: 'socialSecurityDeduction', width: 100 }, |
| | | { title: 'å
¬ç§¯éæ£æ¬¾', dataIndex: 'housingFundDeduction', width: 100 }, |
| | | { title: 'ä¸ªç¨æ£æ¬¾', dataIndex: 'taxDeduction', width: 100 }, |
| | | { title: 'è¯·åæ£æ¬¾', dataIndex: 'leaveDeduction', width: 100 }, |
| | | { title: 'åæ¾æ¶é´', dataIndex: 'paymentTime', width: 160 }, |
| | | { title: 'å®åå·¥èµ', dataIndex: 'actualSalary', width: 100, fixed: 'right' }, |
| | | { title: 'åå·¥å§å', dataIndex: 'userName', width: 110, fixed: 'left' }, |
| | | { title: 'é¨é¨', dataIndex: 'deptName', width: 140 }, |
| | | { title: 'åºæ¬å·¥èµ', dataIndex: 'baseSalary', width: 120 }, |
| | | { title: '绩æå·¥èµ', dataIndex: 'performanceSalary', width: 120 }, |
| | | { title: 'ç¤¾ä¿æ£æ¬¾', dataIndex: 'socialSecurityDeduction', width: 120 }, |
| | | { title: 'å
¬ç§¯éæ£æ¬¾', dataIndex: 'housingFundDeduction', width: 130 }, |
| | | { title: 'ä¸ªç¨æ£æ¬¾', dataIndex: 'taxDeduction', width: 120 }, |
| | | { title: 'è¯·åæ£æ¬¾', dataIndex: 'leaveDeduction', width: 120 }, |
| | | { title: 'åæ¾æ¶é´', dataIndex: 'paymentTime', width: 200 }, |
| | | { title: 'å®åå·¥èµ', dataIndex: 'actualSalary', width: 130, fixed: 'right' }, |
| | | { |
| | | title: 'ç¶æ', |
| | | dataIndex: 'status', |
| | | width: 80, |
| | | width: 90, |
| | | fixed: 'right', |
| | | key: 'status', |
| | | }, |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`${period} - èªèµåæ¾æç»`" class="w-[1100px]" :footer="null"> |
| | | <Modal :title="`${period} - èªèµåæ¾æç»`" class="w-[1424px]" :footer="null"> |
| | | <Alert |
| | | v-if="!isPaid" |
| | | message="å¾ééè¦åæ¾çåå·¥æç»ï¼ç¹å»æ¹éåæ¾æé®è¿è¡åæ¾" |
| | |
| | | /> |
| | | |
| | | <div v-if="!isPaid" class="mb-3"> |
| | | <a-button type="primary" :loading="payLoading" :disabled="selectedIds.length === 0" @click="handleBatchPay"> |
| | | <Button type="primary" :loading="payLoading" :disabled="selectedIds.length === 0" @click="handleBatchPay"> |
| | | æ¹éåæ¾ (å·²é {{ selectedIds.length }} æ¡) |
| | | </a-button> |
| | | </Button> |
| | | </div> |
| | | |
| | | <Table |
| | |
| | | :data-source="detailList" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | :scroll="{ x: 1200, y: 400 }" |
| | | :scroll="{ x: 1400, y: 460 }" |
| | | :row-selection="!isPaid ? { selectedRowKeys: selectedIds, onChange: onSelectChange } : undefined" |
| | | size="small" |
| | | row-key="id" |
| | |
| | | |
| | | deletePdProject, |
| | | exportPdProject, |
| | | exportPdProjectTrackingWord, |
| | | getPdProjectApproveProcessList, |
| | | getPdProjectPage, |
| | | publishPdProject, |
| | |
| | | } |
| | | } |
| | | |
| | | async function handleExportTracking(row: { id?: number; taskCode?: string }) { |
| | | const data = await exportPdProjectTrackingWord(row.id!); |
| | | downloadFileFromBlobPart({ |
| | | fileName: `项ç®è·è¸ªè¡¨-${row.taskCode ?? row.id}.docx`, |
| | | source: data, |
| | | }); |
| | | } |
| | | |
| | | async function handleExport() { |
| | | const data = await exportPdProject(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: '设计任å¡.xls', source: data }); |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: 'ä¸è½½è·è¸ªè¡¨', |
| | | type: 'link', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:pd-project:export'], |
| | | onClick: handleExportTracking.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getMachinerySimpleList } from '#/api/mes/dv/machinery'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getProcess } from '#/api/mes/pro/process'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { loadParamDetails } from './param-state'; |
| | | |
| | |
| | | |
| | | /** ç产æ¥å·¥è¡¨åç±»å */ |
| | | export type FormType = 'approve' | 'create' | 'detail' | 'submit' | 'update'; |
| | | |
| | | /** å·¥åºè®¡è´¹ç±»åï¼1-è®¡æ¶ 2-计件 */ |
| | | const FEEDBACK_BILLING_TYPE_OPTIONS = [ |
| | | { label: '计æ¶ï¼æå·¥æ¶ï¼', value: 1 }, |
| | | { label: 'è®¡ä»¶ï¼ææ°éï¼', value: 2 }, |
| | | ]; |
| | | |
| | | /** å·¥èµå®é¢åä½ */ |
| | | function getWageRateUnit(billingType?: number) { |
| | | return billingType === 1 ? 'å
/å°æ¶' : 'å
/ä»¶'; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | |
| | | { field: 'itemSpecification', title: 'è§æ ¼åå·', width: 120 }, |
| | | { field: 'unitMeasureName', title: 'åä½', width: 80 }, |
| | | { field: 'feedbackQuantity', title: 'æ¥å·¥æ°é', width: 100 }, |
| | | { |
| | | field: 'billingType', |
| | | title: '计费类å', |
| | | width: 100, |
| | | formatter: ({ cellValue }) => |
| | | FEEDBACK_BILLING_TYPE_OPTIONS.find((item) => item.value === cellValue) |
| | | ?.label ?? '-', |
| | | }, |
| | | { field: 'workHours', title: 'æ¥å·¥å·¥æ¶(h)', width: 110 }, |
| | | { field: 'productionLength', title: 'ç产é¿åº¦(m)', width: 120 }, |
| | | { field: 'standardWorkHours', title: 'æ åå·¥æ¶(h)', width: 110 }, |
| | | { |
| | | field: 'workHoursDiff', |
| | | title: 'å·¥æ¶å·®å¼(h)', |
| | | width: 110, |
| | | formatter: ({ cellValue }) => |
| | | cellValue == null |
| | | ? '-' |
| | | : `${Number(cellValue) > 0 ? '+' : ''}${cellValue}`, |
| | | }, |
| | | { |
| | | field: 'workHoursDiffRate', |
| | | title: 'å·®å¼ç', |
| | | width: 100, |
| | | formatter: ({ cellValue, row }) => |
| | | cellValue == null |
| | | ? '-' |
| | | : `${Number(cellValue) > 0 ? '+' : ''}${cellValue}%`, |
| | | }, |
| | | { |
| | | field: 'wageAmount', |
| | | title: 'æ¥å·¥å·¥èµ', |
| | | width: 110, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { field: 'qualifiedQuantity', title: 'è´¨æ£åæ ¼æ°é', width: 120 }, |
| | | { field: 'unqualifiedQuantity', title: 'è´¨æ£ä¸åæ ¼æ°é', width: 130 }, |
| | | { field: 'feedbackUserNickname', title: 'æ¥å·¥äºº', width: 100 }, |
| | |
| | | * æ¥å·¥åªå¡«å"æ¥å·¥æ°é"ï¼åæ ¼å/ä¸è¯åæ°éç±è´¨æ£ç¡®å®ï¼æ¥å·¥ç¯èä¸ééã |
| | | * æ¥å·¥å®¡æ¹å产åºåå¾
æ£éªï¼åæ ¼åç»æ£éªåå
¥åºã |
| | | */ |
| | | /** æ¥å·¥è¡¨åä¸å·¥æ¶è¯ç®ç»ä»¶çèå¨åè° */ |
| | | export interface FeedbackFormHooks { |
| | | /** ä»»å¡éæ©ååï¼æä»»å¡ä¿¡æ¯åæ¥ç»è¾
å©å·¥æ¶å½å
¥ç»ä»¶ */ |
| | | onTaskChange?: (task?: MesProTaskApi.Task) => void; |
| | | /** ç产é¿åº¦ååï¼ç¨äºè¾
å©å·¥æ¶è¯ç® */ |
| | | onProductionLengthChange?: (value?: number) => void; |
| | | /** å®é
å·¥æ¶ååï¼ç¨äºå®æ¶å±ç¤ºå·¥æ¶å·®å¼ */ |
| | | onWorkHoursChange?: (value?: number) => void; |
| | | /** 表åå
³é®å段ååï¼è§¦åå端工èµè¯ç®ï¼å«è¾
å©å·¥èµä¸è´¨éè°æ´ï¼ */ |
| | | onPreviewWage?: () => void; |
| | | } |
| | | |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | formApi?: VbenFormApi, |
| | | hooks?: FeedbackFormHooks, |
| | | ): VbenFormSchema[] { |
| | | const isHeaderReadonly = ['approve', 'detail', 'submit'].includes(formType); |
| | | |
| | | /** |
| | | * å端æå·¥åºå¿«ç
§é¢è§æ¥å·¥å·¥èµï¼è®¡æ¶=å·¥æ¶Ãå®é¢ï¼è®¡ä»¶=æ¥å·¥æ°éÃå®é¢ã |
| | | * 以å端ä¿å/å®¡æ¹æ¶éç®çç»æä¸ºåï¼è¿éåªç¨äºå½å
¥æ¶å³æ¶å±ç¤ºã |
| | | */ |
| | | const recalculateWagePreview = async () => { |
| | | if (!formApi) { |
| | | return; |
| | | } |
| | | const values = await formApi.getValues(); |
| | | const wageRate = Number(values?.wageRate ?? 0); |
| | | const quantity = Number(values?.feedbackQuantity ?? 0); |
| | | const hours = Number(values?.workHours ?? 0); |
| | | const wage = values?.billingType === 1 ? hours * wageRate : quantity * wageRate; |
| | | await formApi.setFieldValue( |
| | | 'wageAmount', |
| | | Number.isFinite(wage) ? Math.round(wage * 100) / 100 : 0, |
| | | ); |
| | | hooks?.onPreviewWage?.(); |
| | | }; |
| | | |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | workstationId: task.workstationId, |
| | | reportableQuantity: task.reportableQuantity, |
| | | }); |
| | | // 带åºå·¥åºç计费类åä¸å·¥èµå®é¢ï¼ç¨äºå±ç¤ºå峿¶é¢è§æ¥å·¥å·¥èµ |
| | | if (task.processId) { |
| | | const process = await getProcess(task.processId); |
| | | const billingType = process?.billingType ?? 2; |
| | | const wageRate = process?.wageRate ?? 0; |
| | | await formApi?.setValues({ billingType, wageRate }); |
| | | await recalculateWagePreview(); |
| | | } |
| | | hooks?.onTaskChange?.(task); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | placeholder: 'è¯·éæ©å·¥ä½ç«', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'machineryId', |
| | | label: '使ç¨è®¾å¤', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getMachinerySimpleList, |
| | | disabled: isHeaderReadonly, |
| | | labelField: 'name', |
| | | placeholder: 'æ¬æ¬¡æ¥å·¥ä¸»ç¨è®¾å¤ï¼å½±åå·¥èµåä»·ï¼', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | valueField: 'id', |
| | | onChange: () => recalculateWagePreview(), |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId', 'workstationId'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | // åªååºå½åä»»å¡å·¥ä½ç«å·²ç»å®ç设å¤ï¼é¿å
误éå
¶ä»å·¥åºçè®¾å¤ |
| | | api: () => getMachinerySimpleList(values.workstationId), |
| | | disabled: isHeaderReadonly, |
| | | labelField: 'name', |
| | | placeholder: values.workstationId |
| | | ? 'æ¬æ¬¡æ¥å·¥ä¸»ç¨è®¾å¤ï¼ä»
æ¾ç¤ºè¯¥å·¥ä½ç«ç»å®ç设å¤ï¼' |
| | | : '请å
鿩工ä½ç«', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | valueField: 'id', |
| | | onChange: () => recalculateWagePreview(), |
| | | }), |
| | | show: (values) => !!values.taskId, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemCode', |
| | |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥æ¥å·¥æ°é', |
| | | onChange: () => recalculateWagePreview(), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'productionLength', |
| | | label: 'ç产é¿åº¦(ç±³)', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: 'ç¨äºæ åå·¥æ¶è®¡ç®ï¼åä½ä¸ºç±³æ¶é»è®¤çäºæ¥å·¥æ°é', |
| | | onChange: (value?: number) => |
| | | hooks?.onProductionLengthChange?.(value), |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId'], |
| | | show: (values) => |
| | | !!values.taskId && ['create', 'update'].includes(formType), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'billingType', |
| | | label: '计费类å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | disabled: true, |
| | | options: FEEDBACK_BILLING_TYPE_OPTIONS, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId'], |
| | | show: (values) => !!values.taskId, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'wageRate', |
| | | label: 'å·¥èµå®é¢', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | disabled: true, |
| | | precision: 2, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId', 'billingType'], |
| | | show: (values) => !!values.taskId, |
| | | componentProps: (values) => ({ |
| | | addonAfter: getWageRateUnit(values.billingType), |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workHours', |
| | | label: 'æ¥å·¥å·¥æ¶(h)', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥æ¬æ¬¡æ¥å·¥å·¥æ¶', |
| | | onChange: (value?: number) => { |
| | | hooks?.onWorkHoursChange?.(value); |
| | | recalculateWagePreview(); |
| | | }, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId', 'billingType'], |
| | | show: (values) => !!values.taskId && values.billingType === 1, |
| | | rules: (values) => |
| | | values.billingType === 1 |
| | | ? z.number().min(0.01, '计æ¶è®¡èªçå·¥åºå¿
须填åå¤§äº 0 çæ¥å·¥å·¥æ¶') |
| | | : null, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'wageAmount', |
| | | label: 'æ¥å·¥å·¥èµ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | disabled: true, |
| | | precision: 2, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['taskId', 'billingType'], |
| | | show: (values) => !!values.taskId, |
| | | componentProps: (values) => ({ |
| | | addonAfter: 'å
', |
| | | placeholder: |
| | | values.billingType === 1 |
| | | ? 'æå·¥æ¶ à 工èµå®é¢èªå¨è®¡ç®' |
| | | : 'ææ¥å·¥æ°é à 工èµå®é¢èªå¨è®¡ç®', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'feedbackUserId', |
| | |
| | | { field: 'workOrderId', label: 'ç产订å', render: (_val, data) => data?.workOrderCode }, |
| | | { field: 'taskId', label: 'ç产任å¡', render: (_val, data) => data?.taskCode }, |
| | | { field: 'workstationName', label: 'å·¥ä½ç«' }, |
| | | { field: 'machineryName', label: '使ç¨è®¾å¤' }, |
| | | { field: 'itemCode', label: '产åç¼ç ' }, |
| | | { field: 'itemName', label: '产ååç§°' }, |
| | | { field: 'unitMeasureName', label: 'åä½' }, |
| | | { field: 'itemSpecification', label: 'è§æ ¼' }, |
| | | { field: 'reportableQuantity', label: '坿¥å·¥æ°é' }, |
| | | { field: 'feedbackQuantity', label: 'æ¥å·¥æ°é' }, |
| | | { |
| | | field: 'billingType', |
| | | label: '计费类å', |
| | | render: (val) => |
| | | FEEDBACK_BILLING_TYPE_OPTIONS.find((item) => item.value === val) |
| | | ?.label ?? '-', |
| | | }, |
| | | { field: 'wageRate', label: 'å·¥èµå®é¢' }, |
| | | { field: 'workHours', label: 'æ¥å·¥å·¥æ¶(h)' }, |
| | | { field: 'auxPaidHours', label: 'è¾
å©è®¡èªå·¥æ¶(h)' }, |
| | | { field: 'wageAmount', label: 'æ¥å·¥å·¥èµï¼å«è´¨éè°æ´ï¼' }, |
| | | { field: 'auxWageAmount', label: 'è¾
å©å·¥èµ' }, |
| | | { field: 'qualityPassRate', label: 'è¿ç¨æ£éªåæ ¼ç(%)' }, |
| | | { field: 'productionLength', label: 'ç产é¿åº¦(ç±³)' }, |
| | | { field: 'standardWorkHours', label: 'æ åå·¥æ¶(h)' }, |
| | | { field: 'standardPrimaryHours', label: 'æ å主工æ¶(h)' }, |
| | | { field: 'standardAuxHours', label: 'æ åè¾
å©å·¥æ¶(h)' }, |
| | | { field: 'workHoursDiff', label: 'å·¥æ¶å·®å¼(h)' }, |
| | | { field: 'workHoursDiffRate', label: 'å·¥æ¶å·®å¼ç(%)' }, |
| | | { field: 'stdCalcNote', label: 'æ åå·¥æ¶è®¡ç®ä¾æ®' }, |
| | | { field: 'qualifiedQuantity', label: 'åæ ¼åæ°é' }, |
| | | { field: 'unqualifiedQuantity', label: 'ä¸è¯åæ°é' }, |
| | | { field: 'laborScrapQuantity', label: 'å·¥åºæ°é' }, |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { FormType, FeedbackFormHooks } from '../data'; |
| | | |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | import type { MesProTaskApi } from '#/api/mes/pro/task'; |
| | | import type { MesProFeedbackWagePreviewApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | |
| | | import { MesAutoCodeRuleCode, MesProFeedbackStatusEnum } from '@vben/constants'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { Button, Input, message, Popconfirm, Tabs } from 'ant-design-vue'; |
| | | import { Alert, Button, Input, message, Popconfirm, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | |
| | | updateFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { previewFeedbackWage } from '#/api/mes/pro/wage'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | |
| | | import BatchInputDialog from './batch-input-dialog.vue'; |
| | | import ItemConsumeList from './item-consume-list.vue'; |
| | | import ProductProduceList from './product-produce-list.vue'; |
| | | import WorktimeAuxInput from './worktime-aux-input.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | |
| | | const userStore = useUserStore(); |
| | | const subTabsName = ref('itemConsume'); |
| | | const originalSnapshot = ref(''); // 表ååå§æ°æ®å¿«ç
§ï¼ç¨äº submit æ¶è·³è¿æªåæ´çä¿åè¯·æ± |
| | | /** å½åéä¸çç产任å¡ï¼ç¨äºè¾
å©å·¥æ¶ç»ä»¶æå·¥åº/æºå°å è½½é
ç½® */ |
| | | const currentTask = ref<Partial<MesProTaskApi.Task>>(); |
| | | const productionLength = ref<number>(); |
| | | const workHours = ref<number>(); |
| | | const auxRef = ref<InstanceType<typeof WorktimeAuxInput>>(); |
| | | /** å端工èµè¯ç®ç»æï¼å«è¾
å©å·¥èµãè´¨éè°æ´ï¼ */ |
| | | const wagePreview = ref<MesProFeedbackWagePreviewApi.PreviewResp>(); |
| | | let previewTimer: ReturnType<typeof setTimeout> | undefined; |
| | | |
| | | /** è°ç¨å端è¯ç®å·¥èµï¼æªæå®è®¾å¤æ¶æå·¥åºé»è®¤åä»·ï¼æå®è®¾å¤æè®¾å¤ç»´åº¦è§å */ |
| | | async function refreshWagePreview() { |
| | | if (!isEditable.value) { |
| | | wagePreview.value = undefined; |
| | | return; |
| | | } |
| | | const values = (await formApi.getValues()) as MesProFeedbackApi.Feedback; |
| | | if (!values.taskId) { |
| | | wagePreview.value = undefined; |
| | | return; |
| | | } |
| | | try { |
| | | wagePreview.value = await previewFeedbackWage({ |
| | | taskId: values.taskId, |
| | | machineryId: values.machineryId, |
| | | workHours: values.workHours, |
| | | feedbackQuantity: values.feedbackQuantity, |
| | | productionLength: values.productionLength, |
| | | auxList: auxRef.value?.getAuxList(), |
| | | }); |
| | | } catch { |
| | | // è¯ç®å¤±è´¥ä¸é»æå½å
¥ï¼ä¿åæ¶å端ä¼ç»åºç²¾ç¡®æ ¡éªæç¤º |
| | | wagePreview.value = undefined; |
| | | } |
| | | } |
| | | |
| | | /** é²æè§¦åå·¥èµè¯ç®ï¼é¿å
è¿ç»å½å
¥æ¶é¢ç¹è¯·æ± */ |
| | | function schedulePreview() { |
| | | if (previewTimer) { |
| | | clearTimeout(previewTimer); |
| | | } |
| | | previewTimer = setTimeout(() => { |
| | | refreshWagePreview(); |
| | | }, 500); |
| | | } |
| | | |
| | | const formHooks: FeedbackFormHooks = { |
| | | onTaskChange: (task) => { |
| | | currentTask.value = task; |
| | | productionLength.value = undefined; |
| | | workHours.value = undefined; |
| | | }, |
| | | onProductionLengthChange: (value) => { |
| | | productionLength.value = value; |
| | | }, |
| | | onWorkHoursChange: (value) => { |
| | | workHours.value = value; |
| | | }, |
| | | onPreviewWage: () => schedulePreview(), |
| | | }; |
| | | |
| | | const isEditable = computed(() => |
| | | ['create', 'submit', 'update'].includes(formType.value), |
| | |
| | | const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback; |
| | | syncParamValues(data); |
| | | alignQuantity(data); |
| | | // è¾
å©é¡¹æ¬¡æ°ç±å·¥æ¶ç»ä»¶ç»´æ¤ï¼æ åå·¥æ¶ä»¥å端ä¿åæ¶éç®ä¸ºå |
| | | if (isEditable.value && auxRef.value) { |
| | | data.auxList = auxRef.value.getAuxList(); |
| | | } |
| | | return data; |
| | | } |
| | | |
| | |
| | | status: MesProFeedbackStatusEnum.PREPARE, |
| | | }; |
| | | formType.value = 'update'; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, formHooks), |
| | | }); |
| | | await formApi.setFieldValue('id', id); |
| | | message.success($t('common.createSuccess')); |
| | | } else { |
| | |
| | | subTabsName.value = 'itemConsume'; |
| | | originalSnapshot.value = ''; |
| | | paramDetails.value = []; |
| | | currentTask.value = undefined; |
| | | productionLength.value = undefined; |
| | | workHours.value = undefined; |
| | | wagePreview.value = undefined; |
| | | if (previewTimer) { |
| | | clearTimeout(previewTimer); |
| | | previewTimer = undefined; |
| | | } |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ formType: FormType; id?: number; showSubmitButton?: boolean }>(); |
| | | formType.value = data.formType; |
| | | showSubmitButton.value = data.showSubmitButton !== false; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, formHooks), |
| | | }); |
| | | // 审æ¹/详æ
æ´è¡¨ç¦ç¨ï¼é¿å
å®¡æ ¸äººè¯¯æ¹æªæäº¤çåæ®µ |
| | | formApi.setDisabled( |
| | | formType.value === 'approve' || formType.value === 'detail', |
| | |
| | | await loadParamDetails(formData.value.taskId); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues({ ...formData.value }); |
| | | // 忥工æ¶ç»ä»¶æéçä¸ä¸æï¼ç¼è¾æ¶æå·²ææ¬¡æ°åæ¾ |
| | | currentTask.value = { |
| | | id: formData.value.taskId, |
| | | processId: formData.value.processId, |
| | | workstationId: formData.value.workstationId, |
| | | }; |
| | | productionLength.value = formData.value.productionLength; |
| | | workHours.value = formData.value.workHours; |
| | | // è®°å½åå§å¿«ç
§ï¼å«æ°é对é½ç»æï¼ï¼submit æ¶ç¨äºè·³è¿æ åæ´çä¿åè¯·æ± |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | schedulePreview(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | v-if="formData?.standardWarnFlag && formData?.standardWarnMessage" |
| | | class="mx-4 mb-3" |
| | | :description="formData.standardWarnMessage" |
| | | message="å·¥æ¶è¶
åºèæ ¸éå¼" |
| | | show-icon |
| | | type="warning" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | <div |
| | | v-if="isEditable && wagePreview" |
| | | class="mx-4 mt-4 rounded border border-gray-200 p-4" |
| | | > |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <div class="font-medium"> |
| | | å·¥èµè¯ç® |
| | | <span class="ml-2 text-xs font-normal text-gray-400"> |
| | | å«è¾
å©å·¥èµä¸è´¨éè°æ´ï¼ä¿å/å®¡æ¹æ¶ä»¥ç³»ç»éç®ç»æä¸ºå |
| | | </span> |
| | | </div> |
| | | <Button size="small" @click="refreshWagePreview">å·æ°è¯ç®</Button> |
| | | </div> |
| | | <div class="flex flex-wrap gap-x-6 gap-y-1 text-sm"> |
| | | <span> |
| | | è®¡èªæ¹å¼ï¼<b>{{ wagePreview.billingType === 1 ? '计æ¶' : '计件' }}</b> |
| | | </span> |
| | | <span> |
| | | åä»·ï¼<b>{{ wagePreview.wageRate ?? 0 }}</b> |
| | | {{ wagePreview.billingType === 1 ? 'å
/å°æ¶' : 'å
/ä»¶' }} |
| | | </span> |
| | | <span v-if="wagePreview.billingType === 1"> |
| | | å·¥æ¶ï¼<b>{{ wagePreview.workHours ?? 0 }}</b> å°æ¶ |
| | | </span> |
| | | <span v-else> |
| | | 计件æ°éï¼<b>{{ wagePreview.pieceQuantity ?? 0 }}</b> |
| | | </span> |
| | | <span>主工èµï¼<b>{{ wagePreview.mainWage ?? 0 }}</b> å
</span> |
| | | <span> |
| | | è¾
å©å·¥èµï¼<b>{{ wagePreview.auxWage ?? 0 }}</b> å
|
| | | <span class="text-xs text-gray-400"> |
| | | ï¼{{ wagePreview.auxPaidHours ?? 0 }} å°æ¶ï¼ |
| | | </span> |
| | | </span> |
| | | <span v-if="wagePreview.quality?.passRate !== undefined"> |
| | | åæ ¼çï¼<b>{{ wagePreview.quality?.passRate }}</b>% |
| | | </span> |
| | | <span> |
| | | æç»å·¥èµï¼<b class="text-blue-600">{{ wagePreview.finalWage ?? 0 }}</b> å
|
| | | </span> |
| | | </div> |
| | | <div |
| | | v-if="wagePreview.fields?.length" |
| | | class="mt-2 text-xs text-gray-500" |
| | | > |
| | | å·¥èµåæ®µï¼ |
| | | <span v-for="(field, index) in wagePreview.fields" :key="field.code"> |
| | | {{ index > 0 ? 'ï¼' : '' }}{{ field.name }}={{ field.value ?? '-' }} |
| | | <template v-if="field.amount !== undefined"> |
| | | ï¼{{ field.amount }} å
ï¼ |
| | | </template> |
| | | </span> |
| | | </div> |
| | | <Alert |
| | | v-if="wagePreview.warnings?.length" |
| | | class="mt-2" |
| | | :message="wagePreview.warnings.join('ï¼')" |
| | | show-icon |
| | | type="warning" |
| | | /> |
| | | </div> |
| | | <WorktimeAuxInput |
| | | v-if="isEditable && (currentTask?.id || formData?.taskId)" |
| | | ref="auxRef" |
| | | :initial-aux-list="formData?.auxList" |
| | | :process-id="currentTask?.processId ?? formData?.processId" |
| | | :production-length="productionLength ?? formData?.productionLength" |
| | | :task-id="currentTask?.id ?? formData?.taskId" |
| | | :work-hours="workHours ?? formData?.workHours" |
| | | :workstation-id="currentTask?.workstationId ?? formData?.workstationId" |
| | | /> |
| | | <div v-if="paramDetails.length" class="mx-4 mt-4 rounded border border-gray-200 p-4"> |
| | | <div class="mb-3 font-medium">å·¥èºåæ°</div> |
| | | <div class="grid grid-cols-2 gap-3"> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | import type { MesProWorktimeApi } from '#/api/mes/pro/worktime'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { Button, Input, InputNumber, message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | calculateWorktime, |
| | | getWorktimeAuxApplicable, |
| | | } from '#/api/mes/pro/worktime'; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | taskId?: number; |
| | | processId?: number; |
| | | workstationId?: number; |
| | | productionLength?: number; |
| | | workHours?: number; |
| | | initialAuxList?: MesProFeedbackApi.AuxValue[]; |
| | | }>(), |
| | | { |
| | | taskId: undefined, |
| | | processId: undefined, |
| | | workstationId: undefined, |
| | | productionLength: undefined, |
| | | workHours: undefined, |
| | | initialAuxList: () => [], |
| | | }, |
| | | ); |
| | | |
| | | interface AuxRow extends MesProWorktimeApi.AuxItem { |
| | | quantity?: number; |
| | | materialFrom?: string; |
| | | materialTo?: string; |
| | | minutes?: number; |
| | | } |
| | | |
| | | const items = ref<AuxRow[]>([]); |
| | | const loading = ref(false); |
| | | const calculating = ref(false); |
| | | const calcResult = ref<MesProWorktimeApi.CalcResp>(); |
| | | let calcTimer: ReturnType<typeof setTimeout> | undefined; |
| | | |
| | | const measureUnit = (measureType?: number) => { |
| | | if (measureType === 2) return '个'; |
| | | if (measureType === 3) return 'ç'; |
| | | if (measureType === 4) return 'ç±³'; |
| | | return '次'; |
| | | }; |
| | | |
| | | function mergeInitial(quantity: boolean) { |
| | | const initialMap = new Map( |
| | | (props.initialAuxList ?? []).map((item) => [item.auxItemId, item]), |
| | | ); |
| | | items.value.forEach((item) => { |
| | | if (item.id === undefined) return; |
| | | const initial = initialMap.get(item.id); |
| | | if (!initial) return; |
| | | if (quantity && item.quantity === undefined) { |
| | | item.quantity = initial.quantity ?? 0; |
| | | } |
| | | if (!item.materialFrom && initial.materialFrom) { |
| | | item.materialFrom = initial.materialFrom; |
| | | } |
| | | if (!item.materialTo && initial.materialTo) { |
| | | item.materialTo = initial.materialTo; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function loadItems() { |
| | | if (!props.processId) { |
| | | items.value = []; |
| | | calcResult.value = undefined; |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const list = await getWorktimeAuxApplicable( |
| | | props.processId, |
| | | props.workstationId, |
| | | ); |
| | | items.value = (list ?? []).map((item) => ({ |
| | | ...item, |
| | | quantity: 0, |
| | | materialFrom: item.materialFrom, |
| | | materialTo: item.materialTo, |
| | | })); |
| | | mergeInitial(true); |
| | | if (items.value.length > 0) { |
| | | await calculate(); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function calculate() { |
| | | if (!props.taskId || items.value.length === 0) { |
| | | calcResult.value = undefined; |
| | | return; |
| | | } |
| | | calculating.value = true; |
| | | try { |
| | | const result = await calculateWorktime({ |
| | | taskId: props.taskId, |
| | | productionLength: props.productionLength, |
| | | auxList: getAuxList(), |
| | | }); |
| | | calcResult.value = result; |
| | | const minuteMap = new Map( |
| | | (result?.auxList ?? []).map((aux) => [aux.auxItemId, aux.minutes]), |
| | | ); |
| | | items.value.forEach((item) => { |
| | | item.minutes = item.id ? minuteMap.get(item.id) : undefined; |
| | | }); |
| | | } catch { |
| | | // è¯ç®å¤±è´¥ä¸é»ææ¥å·¥ä¿åï¼å端ä¿åæ¶ä¼å次计ç®å¹¶ç»åºè¯´æ |
| | | message.warning('æ åå·¥æ¶è¯ç®å¤±è´¥ï¼å¯ç´æ¥ä¿åï¼ç³»ç»ä¼å¨ä¿åæ¶éç®'); |
| | | } finally { |
| | | calculating.value = false; |
| | | } |
| | | } |
| | | |
| | | function scheduleCalculate() { |
| | | if (calcTimer) { |
| | | clearTimeout(calcTimer); |
| | | } |
| | | calcTimer = setTimeout(() => { |
| | | calculate(); |
| | | }, 400); |
| | | } |
| | | |
| | | /** æäº¤ç»å端çè¾
å©é¡¹ï¼åªæäº¤å½å
¥äºæ¬¡æ°çé¡¹ï¼ */ |
| | | function getAuxList(): MesProFeedbackApi.AuxValue[] { |
| | | return items.value |
| | | .filter((item) => item.id && Number(item.quantity ?? 0) > 0) |
| | | .map((item) => ({ |
| | | auxItemId: item.id!, |
| | | quantity: item.quantity, |
| | | materialFrom: item.materialFrom, |
| | | materialTo: item.materialTo, |
| | | })); |
| | | } |
| | | |
| | | const standardHours = computed(() => calcResult.value?.standardWorkHours ?? 0); |
| | | const actualHours = computed(() => Number(props.workHours ?? 0)); |
| | | const diffHours = computed(() => { |
| | | if (!calcResult.value?.standardWorkHours) return undefined; |
| | | return Math.round((actualHours.value - standardHours.value) * 100) / 100; |
| | | }); |
| | | const diffRate = computed(() => { |
| | | if (!calcResult.value?.standardWorkHours) return undefined; |
| | | return ( |
| | | Math.round( |
| | | ((actualHours.value - standardHours.value) / standardHours.value) * |
| | | 100 * |
| | | 100, |
| | | ) / 100 |
| | | ); |
| | | }); |
| | | const warnFlag = computed( |
| | | () => diffRate.value !== undefined && diffRate.value > 20, |
| | | ); |
| | | |
| | | watch( |
| | | () => [props.processId, props.workstationId], |
| | | () => loadItems(), |
| | | { immediate: true }, |
| | | ); |
| | | watch( |
| | | () => props.initialAuxList, |
| | | () => mergeInitial(false), |
| | | ); |
| | | watch( |
| | | () => props.productionLength, |
| | | () => scheduleCalculate(), |
| | | ); |
| | | watch( |
| | | () => props.taskId, |
| | | () => scheduleCalculate(), |
| | | ); |
| | | |
| | | defineExpose({ calculate, getAuxList }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div |
| | | v-if="items.length > 0" |
| | | class="mx-4 mt-4 rounded border border-gray-200 p-4" |
| | | > |
| | | <div class="mb-3 flex items-center justify-between"> |
| | | <div class="font-medium"> |
| | | è¾
å©å·¥æ¶ |
| | | <span class="ml-2 text-xs font-normal text-gray-400"> |
| | | æå®é
åççæ¬¡æ°å½å
¥ï¼ç¨äºè®¡ç®æ åå·¥æ¶ï¼ä¸å½±åå·¥èµï¼ |
| | | </span> |
| | | </div> |
| | | <Button :loading="calculating || loading" size="small" @click="calculate"> |
| | | è¯ç®æ åå·¥æ¶ |
| | | </Button> |
| | | </div> |
| | | <div class="space-y-2"> |
| | | <div |
| | | v-for="item in items" |
| | | :key="item.id" |
| | | class="flex flex-wrap items-center gap-3" |
| | | > |
| | | <span class="w-52 text-right text-sm text-gray-600"> |
| | | {{ item.itemName }} |
| | | <span class="text-xs text-gray-400"> |
| | | ï¼{{ item.measureType === 4 ? 'æç±³æç®' : `æ${measureUnit(item.measureType)}` }}ï¼ |
| | | </span> |
| | | </span> |
| | | <InputNumber |
| | | v-model:value="item.quantity" |
| | | :min="0" |
| | | :precision="2" |
| | | class="!w-28" |
| | | :addon-after="measureUnit(item.measureType)" |
| | | @change="scheduleCalculate" |
| | | /> |
| | | <template v-if="item.materialFrom || item.materialTo"> |
| | | <Input |
| | | v-model:value="item.materialFrom" |
| | | class="!w-32" |
| | | placeholder="åè²/ææ" |
| | | @change="scheduleCalculate" |
| | | /> |
| | | <span class="text-gray-400">â</span> |
| | | <Input |
| | | v-model:value="item.materialTo" |
| | | class="!w-32" |
| | | placeholder="ç®æ è²/ææ" |
| | | @change="scheduleCalculate" |
| | | /> |
| | | </template> |
| | | <span v-if="item.minutes !== undefined" class="text-xs text-gray-400"> |
| | | æç® {{ item.minutes }} åé |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-if="calcResult" |
| | | class="mt-3 flex flex-wrap items-center gap-x-6 gap-y-1 border-t border-gray-100 pt-2 text-sm" |
| | | > |
| | | <span> |
| | | æ åå·¥æ¶ï¼<b>{{ standardHours }}</b> å°æ¶ |
| | | <span class="text-xs text-gray-400"> |
| | | ï¼ä¸» {{ calcResult.primaryHours ?? 0 }} + è¾
|
| | | {{ calcResult.auxHours ?? 0 }}ï¼ |
| | | </span> |
| | | </span> |
| | | <span v-if="props.workHours !== undefined"> |
| | | å®é
å·¥æ¶ï¼<b>{{ actualHours }}</b> å°æ¶ |
| | | </span> |
| | | <span v-if="diffHours !== undefined" :class="warnFlag ? 'text-red-500' : ''"> |
| | | å·®å¼ï¼<b>{{ diffHours > 0 ? '+' : '' }}{{ diffHours }}</b> å°æ¶ |
| | | <template v-if="diffRate !== undefined"> |
| | | ï¼{{ diffRate > 0 ? '+' : '' }}{{ diffRate }}%ï¼ |
| | | </template> |
| | | </span> |
| | | </div> |
| | | <div v-if="calcResult?.note" class="mt-1 text-xs text-gray-400"> |
| | | {{ calcResult.note }} |
| | | </div> |
| | | <div |
| | | v-if="calcResult?.missingReasons?.length" |
| | | class="mt-1 text-xs text-orange-500" |
| | | > |
| | | {{ calcResult.missingReasons.join('ï¼') }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | message.error('è¯·éæ©èç¨¿ç¶æçç产计å'); |
| | | return; |
| | | } |
| | | const forbidden = checkedRows.value.filter((row) => row.canIssue !== true); |
| | | if (forbidden.length > 0) { |
| | | message.error( |
| | | `以ä¸è®¡å䏿¯ç±ä½ å建ï¼ä¸è½ä¸åï¼${forbidden |
| | | .map((row) => row.code) |
| | | .join('ã')}ã请ç±è®¡åå建人æä½ï¼æè系管çåã`, |
| | | ); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ¹éä¸åä¸...', |
| | | duration: 0, |
| | |
| | | { |
| | | label: 'æ¹éä¸å', |
| | | type: 'primary', |
| | | disabled: isEmpty(checkedIds), |
| | | disabled: |
| | | isEmpty(checkedIds) || |
| | | !checkedRows.every((row) => row.canIssue), |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['mes:pro-mps:issue'], |
| | | popConfirm: { |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['mes:pro-mps:issue'], |
| | | ifShow: () => row.status === MpsStatusEnum.DRAFT, |
| | | // ä»
计åå建人ï¼éå®è®¡åå订ååå»ºäººï¼æè¶
管å¯è§ï¼åç«¯åæ ·å¼ºæ ¡éª |
| | | ifShow: () => |
| | | row.status === MpsStatusEnum.DRAFT && row.canIssue === true, |
| | | popConfirm: { |
| | | title: `确认ä¸å${row.code}åï¼`, |
| | | confirm: handleIssue.bind(null, row), |
| | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | /** 计费类åï¼1-è®¡æ¶ 2-计件 */ |
| | | export const PROCESS_BILLING_TYPE_OPTIONS = [ |
| | | { label: '计æ¶ï¼å
/å°æ¶ï¼', value: 1 }, |
| | | { label: '计件ï¼å
/ä»¶ï¼', value: 2 }, |
| | | ]; |
| | | |
| | | /** 计费类ååç§° */ |
| | | export function getBillingTypeLabel(billingType?: number) { |
| | | return ( |
| | | PROCESS_BILLING_TYPE_OPTIONS.find((item) => item.value === billingType) |
| | | ?.label ?? '-' |
| | | ); |
| | | } |
| | | |
| | | /** å·¥èµå®é¢åä½ */ |
| | | export function getWageRateUnit(billingType?: number) { |
| | | return billingType === 1 ? 'å
/å°æ¶' : 'å
/ä»¶'; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç产工åºç表å */ |
| | | export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { |
| | | return [ |
| | |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | }, |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'billingType', |
| | | label: '计费类å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: PROCESS_BILLING_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è®¡è´¹ç±»å', |
| | | }, |
| | | rules: z.number().default(2), |
| | | }, |
| | | { |
| | | fieldName: 'wageRate', |
| | | label: 'å·¥èµå®é¢', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['billingType'], |
| | | componentProps: (values: Record<string, unknown>) => ({ |
| | | addonAfter: getWageRateUnit(values.billingType as number), |
| | | placeholder: |
| | | values.billingType === 1 |
| | | ? '请è¾å
¥å°æ¶å·¥èµå®é¢' |
| | | : '请è¾å
¥è®¡ä»¶åä»·', |
| | | }), |
| | | }, |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | rules: z.number().min(0, 'å·¥èµå®é¢ä¸è½å°äº 0'), |
| | | }, |
| | | { |
| | | fieldName: 'attention', |
| | |
| | | }, |
| | | { field: 'name', title: 'å·¥åºåç§°', minWidth: 180 }, |
| | | { |
| | | field: 'billingType', |
| | | title: '计费类å', |
| | | width: 110, |
| | | formatter: ({ cellValue }) => getBillingTypeLabel(cellValue), |
| | | }, |
| | | { |
| | | field: 'wageRate', |
| | | title: 'å·¥èµå®é¢', |
| | | width: 140, |
| | | formatter: ({ cellValue, row }) => |
| | | cellValue == null |
| | | ? '-' |
| | | : `${Number(cellValue).toFixed(2)} ${getWageRateUnit(row.billingType)}`, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'checkFlag', |
| | | label: 'æ¯å¦è´¨æ£ç¡®è®¤', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¥å·¥èªå¨æ£æ', |
| | | component: 'Switch', |
| | |
| | | field: 'keyFlag', |
| | | title: 'å
³é®å·¥åº', |
| | | width: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'checkFlag', |
| | | title: 'è´¨æ£ç¡®è®¤', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | |
| | | { field: 'workstationCode', title: 'å·¥ä½ç«ç¼ç ', width: 140 }, |
| | | { field: 'workstationName', title: 'å·¥ä½ç«åç§°', width: 140 }, |
| | | { field: 'processName', title: 'å·¥åº', width: 120 }, |
| | | { |
| | | field: 'checkFlag', |
| | | title: 'æ¯å¦è´¨æ£', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { field: 'itemCode', title: 'ç©æç¼ç ', width: 140 }, |
| | | { field: 'itemName', title: 'ç©æåç§°', width: 140 }, |
| | | { field: 'itemSpecification', title: 'è§æ ¼åå·', width: 120 }, |
| | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { quickCreateFeedback } from '#/api/mes/pro/feedback'; |
| | | import { getProcess } from '#/api/mes/pro/process'; |
| | | import { getTask } from '#/api/mes/pro/task'; |
| | | import { |
| | | loadParamDetails, |
| | | paramDetails, |
| | |
| | | selectedTask.value = undefined; |
| | | formData.value = { |
| | | feedbackQuantity: undefined, |
| | | workHours: undefined, |
| | | feedbackTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | remark: '', |
| | | }; |
| | | // é¢è®¾ä»»å¡æ¶å è½½å
¶å·¥èºåæ°ï¼å¦åçå¾
鿩任å¡ååå è½½ |
| | | // é¢è®¾ä»»å¡æ¶å è½½å
¶å·¥èºåæ°ä¸å·¥åºè®¡è´¹æ¹å¼ï¼å¦åçå¾
鿩任å¡ååå è½½ |
| | | if (props.taskId) { |
| | | loadParamDetails(props.taskId); |
| | | loadProcessByTask(props.taskId); |
| | | } else { |
| | | paramDetails.value = []; |
| | | billingType.value = undefined; |
| | | wageRate.value = 0; |
| | | } |
| | | } |
| | | }, |
| | |
| | | const selectedTask = ref<MesProTaskApi.Task>(); |
| | | const formData = ref({ |
| | | feedbackQuantity: undefined as number | undefined, |
| | | workHours: undefined as number | undefined, |
| | | feedbackTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), |
| | | remark: '', |
| | | }); |
| | | const submitting = ref(false); |
| | | /** å·¥åºè®¡è´¹ç±»åï¼1-计æ¶ï¼éå½å·¥æ¶ï¼ 2-计件 */ |
| | | const billingType = ref<number>(); |
| | | const wageRate = ref(0); |
| | | |
| | | /** æ ¹æ®ä»»å¡å¸¦åºå·¥åºç计费æ¹å¼ï¼è®¡æ¶å·¥åºéè¦ç¨æ·è¡¥å½å·¥æ¶ */ |
| | | async function loadProcessByTask(taskId?: number) { |
| | | billingType.value = undefined; |
| | | wageRate.value = 0; |
| | | if (!taskId) { |
| | | return; |
| | | } |
| | | try { |
| | | const task = await getTask(taskId); |
| | | if (!task?.processId) { |
| | | return; |
| | | } |
| | | const process = await getProcess(task.processId); |
| | | billingType.value = process?.billingType ?? 2; |
| | | wageRate.value = process?.wageRate ?? 0; |
| | | } catch { |
| | | // å·¥åºä¿¡æ¯å 载失败ä¸é»ææ¥å·¥ï¼ä¿åæ¶å端ä¼åæ¬¡æ ¡éªå¹¶ç»åºæç¡®æç¤º |
| | | } |
| | | } |
| | | |
| | | const hasPredefinedTask = computed(() => !!props.taskId); |
| | | |
| | |
| | | selectedTask.value = task; |
| | | selectedTaskId.value = task?.id; |
| | | loadParamDetails(task?.id); |
| | | loadProcessByTask(task?.id); |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | |
| | | message.warning('请è¾å
¥æ¥å·¥æ°é'); |
| | | return; |
| | | } |
| | | if (billingType.value === 1 && wageRate.value > 0 && !formData.value.workHours) { |
| | | message.warning('å½åå·¥åºæè®¡æ¶è®¡èªï¼è¯·è¾å
¥æ¬æ¬¡æ¥å·¥å·¥æ¶ï¼å°æ¶ï¼'); |
| | | return; |
| | | } |
| | | submitting.value = true; |
| | | try { |
| | | await quickCreateFeedback({ |
| | | taskId, |
| | | feedbackQuantity: formData.value.feedbackQuantity, |
| | | workHours: formData.value.workHours, |
| | | feedbackTime: formData.value.feedbackTime || undefined, |
| | | paramValues: paramDetails.value.map((detail) => ({ |
| | | detailId: detail.id, |
| | |
| | | class="!w-full" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item |
| | | v-if="billingType === 1" |
| | | label="æ¥å·¥å·¥æ¶(h)" |
| | | :required="wageRate > 0" |
| | | > |
| | | <a-input-number |
| | | v-model:value="formData.workHours" |
| | | :min="0.01" |
| | | :precision="2" |
| | | placeholder="请è¾å
¥æ¬æ¬¡æ¥å·¥å·¥æ¶ï¼å°æ¶ï¼" |
| | | class="!w-full" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item label="æ¥å·¥æ¶é´"> |
| | | <a-date-picker |
| | | v-model:value="formData.feedbackTime" |
| | |
| | | slots: { default: 'keyFlag' }, |
| | | }, |
| | | { |
| | | field: 'checkFlag', |
| | | title: 'è´¨æ£å·¥åº', |
| | | width: 80, |
| | | slots: { default: 'checkFlag' }, |
| | | }, |
| | | { |
| | | field: 'backflushFlag', |
| | | title: 'åå²', |
| | | width: 80, |
| | |
| | | defaultValue: false, |
| | | }, |
| | | { |
| | | fieldName: 'checkFlag', |
| | | label: 'æ¯å¦è´¨æ£å·¥åº', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | options: [ |
| | | { label: 'å¦', value: false }, |
| | | { label: 'æ¯', value: true }, |
| | | ], |
| | | }, |
| | | defaultValue: false, |
| | | }, |
| | | { |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¯å¦åå²', |
| | | component: 'RadioGroup', |
| | |
| | | <Tag v-if="row.keyFlag" color="red">å
³é®</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #checkFlag="{ row }"> |
| | | <Tag v-if="row.checkFlag" color="blue">è´¨æ£</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #backflushFlag="{ row }"> |
| | | <Tag v-if="row.backflushFlag" color="green">æ¯</Tag> |
| | | <Tag v-else color="default">å¦</Tag> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProWorktimeApi } from '#/api/mes/pro/worktime'; |
| | | |
| | | import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { getWorkstationPage } from '#/api/mes/md/workstation'; |
| | | |
| | | /** è®¡éæ¹å¼ */ |
| | | export const MEASURE_TYPE_OPTIONS = [ |
| | | { label: 'ææ¬¡', value: 1 }, |
| | | { label: 'æä¸ª', value: 2 }, |
| | | { label: 'æç', value: 3 }, |
| | | { label: 'æç±³æç®', value: 4 }, |
| | | ]; |
| | | |
| | | export function getMeasureTypeLabel(measureType?: number) { |
| | | return ( |
| | | MEASURE_TYPE_OPTIONS.find((item) => item.value === measureType)?.label ?? '-' |
| | | ); |
| | | } |
| | | |
| | | /** è®¡èªæ¹å¼ï¼0-ä¸è®¡èª 1-æå·¥åºè¾
å©å°æ¶å·¥èµ 2-ææ¬é¡¹åä»· */ |
| | | export const WAGE_MODE_OPTIONS = [ |
| | | { label: 'ä¸è®¡èª', value: 0 }, |
| | | { label: 'æå·¥åºè¾
å©å°æ¶å·¥èµ', value: 1 }, |
| | | { label: 'ææ¬é¡¹åä»·', value: 2 }, |
| | | ]; |
| | | |
| | | export function getWageModeLabel(wageMode?: number) { |
| | | return ( |
| | | WAGE_MODE_OPTIONS.find((item) => item.value === wageMode)?.label ?? 'ä¸è®¡èª' |
| | | ); |
| | | } |
| | | |
| | | const loadProcessOptions = async () => { |
| | | const list = await getProcessSimpleList(); |
| | | return (list ?? []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id ?? 0, |
| | | })); |
| | | }; |
| | | |
| | | const loadWorkstationOptions = async () => { |
| | | const result = await getWorkstationPage({ pageNo: 1, pageSize: 500 }); |
| | | return (result?.list ?? []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id ?? 0, |
| | | })); |
| | | }; |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¾
å©å·¥æ¶é¡¹ç® */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'processId', |
| | | label: 'å·¥åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: loadProcessOptions, |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: 'è¯·éæ©å·¥åº', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'workstationId', |
| | | label: 'éç¨æºå°', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: loadWorkstationOptions, |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: 'ä¸é表示ä¸éæºå°', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemCode', |
| | | label: '项ç®ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | maxLength: 64, |
| | | placeholder: 'å¦ AUX_REEL_CHANGE', |
| | | }, |
| | | rules: z.string().min(1, '项ç®ç¼ç ä¸è½ä¸ºç©º').max(64), |
| | | }, |
| | | { |
| | | fieldName: 'itemName', |
| | | label: '项ç®åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | maxLength: 100, |
| | | placeholder: 'å¦ ä¸ä¸æ¾çº¿ç', |
| | | }, |
| | | rules: z.string().min(1, '项ç®åç§°ä¸è½ä¸ºç©º').max(100), |
| | | }, |
| | | { |
| | | fieldName: 'measureType', |
| | | label: 'è®¡éæ¹å¼', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: MEASURE_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è®¡éæ¹å¼', |
| | | }, |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'wageMode', |
| | | label: 'è®¡èªæ¹å¼', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: WAGE_MODE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è¾
å©å·¥æ¶è®¡èªæ¹å¼', |
| | | }, |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'wageRate', |
| | | label: '计èªåä»·', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['wageMode'], |
| | | show: (values) => values.wageMode === 2, |
| | | }, |
| | | componentProps: { |
| | | addonAfter: 'å
/å°æ¶', |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 4, |
| | | placeholder: 'æ¬é¡¹è¾
å©å·¥æ¶çå°æ¶å·¥èµ', |
| | | }, |
| | | rules: z.number().min(0).optional(), |
| | | }, |
| | | { |
| | | fieldName: 'durationMinutes', |
| | | label: 'å项åéæ°', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['measureType'], |
| | | show: (values) => values.measureType !== 4, |
| | | }, |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '忬¡/å个/åççèæ¶åéæ°', |
| | | }, |
| | | rules: z.number().min(0).optional(), |
| | | }, |
| | | { |
| | | fieldName: 'speedMPerMin', |
| | | label: 'æç®é度', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['measureType'], |
| | | show: (values) => values.measureType === 4, |
| | | }, |
| | | componentProps: { |
| | | class: 'w-full', |
| | | addonAfter: 'ç±³/å', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: 'æç±³æç®æ¶ï¼æ¶é´ = ç±³æ° Ã· é度', |
| | | }, |
| | | rules: z.number().min(0).optional(), |
| | | }, |
| | | { |
| | | fieldName: 'materialFrom', |
| | | label: 'åè²/ææ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | maxLength: 100, |
| | | placeholder: 'æ¢è²åºæ¯å¡«åèµ·å§ææï¼å¦ PVC', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'materialTo', |
| | | label: 'ç®æ è²/ææ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | maxLength: 100, |
| | | placeholder: 'æ¢è²åºæ¯å¡«åç®æ ææï¼å¦ LSZH', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | }, |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'sort', |
| | | label: 'æåº', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 0, |
| | | }, |
| | | rules: z.number().default(0), |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | maxLength: 250, |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 2, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢ */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'processId', |
| | | label: 'å·¥åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: loadProcessOptions, |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: 'è¯·éæ©å·¥åº', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemName', |
| | | label: '项ç®åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥é¡¹ç®åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** åè¡¨åæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesProWorktimeApi.AuxItem>['columns'] { |
| | | return [ |
| | | { field: 'processName', title: 'å·¥åº', minWidth: 150 }, |
| | | { |
| | | field: 'workstationName', |
| | | title: 'éç¨æºå°', |
| | | minWidth: 150, |
| | | formatter: ({ cellValue }) => cellValue || 'ä¸éæºå°', |
| | | }, |
| | | { field: 'itemName', title: 'è¾
å©é¡¹ç®', minWidth: 160 }, |
| | | { field: 'itemCode', title: '项ç®ç¼ç ', minWidth: 150 }, |
| | | { |
| | | field: 'measureType', |
| | | title: 'è®¡éæ¹å¼', |
| | | width: 110, |
| | | formatter: ({ cellValue }) => getMeasureTypeLabel(cellValue), |
| | | }, |
| | | { |
| | | field: 'wageMode', |
| | | title: 'è®¡èªæ¹å¼', |
| | | width: 150, |
| | | formatter: ({ cellValue }) => getWageModeLabel(cellValue), |
| | | }, |
| | | { |
| | | field: 'wageRate', |
| | | title: '计èªåä»·', |
| | | width: 120, |
| | | formatter: ({ cellValue, row }) => |
| | | row.wageMode === 2 && cellValue != null ? `${cellValue} å
/å°æ¶` : '-', |
| | | }, |
| | | { field: 'durationMinutes', title: 'å项åéæ°', width: 110 }, |
| | | { |
| | | field: 'speedMPerMin', |
| | | title: 'æç®é度(m/å)', |
| | | width: 130, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'materialFrom', |
| | | title: 'æ¢è²ææ', |
| | | minWidth: 160, |
| | | formatter: ({ row }) => |
| | | row.materialFrom || row.materialTo |
| | | ? `${row.materialFrom || '-'} â ${row.materialTo || '-'}` |
| | | : '-', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.COMMON_STATUS }, |
| | | }, |
| | | }, |
| | | { field: 'sort', title: 'æåº', width: 80 }, |
| | | { |
| | | title: 'æä½', |
| | | width: 160, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProWorktimeApi } from '#/api/mes/pro/worktime'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteWorktimeAux, |
| | | getWorktimeAuxPage, |
| | | } from '#/api/mes/pro/worktime'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: MesProWorktimeApi.AuxItem) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: MesProWorktimeApi.AuxItem) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.itemName]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteWorktimeAux(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.itemName])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getWorktimeAuxPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | isHover: true, |
| | | keyField: 'id', |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesProWorktimeApi.AuxItem>, |
| | | }); |
| | | </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: ['mes:pro-worktime-aux:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:pro-worktime-aux:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pro-worktime-aux:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.itemName]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProWorktimeApi } from '#/api/mes/pro/worktime'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createWorktimeAux, |
| | | getWorktimeAux, |
| | | updateWorktimeAux, |
| | | } from '#/api/mes/pro/worktime'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<'create' | 'update'>('create'); |
| | | const formData = ref<MesProWorktimeApi.AuxItem>(); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 110, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProWorktimeApi.AuxItem; |
| | | await (formData.value?.id |
| | | ? updateWorktimeAux(data) |
| | | : createWorktimeAux(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; |
| | | } |
| | | formApi.setState({ schema: useFormSchema() }); |
| | | const data = modalApi.getData<{ formType: 'create' | 'update'; id?: number }>(); |
| | | formType.value = data.formType; |
| | | if (!data?.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getWorktimeAux(data.id); |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="formType === 'create' ? 'æ°å¢è¾
å©å·¥æ¶é¡¹ç®' : 'ç¼è¾è¾
å©å·¥æ¶é¡¹ç®'"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | import type { MesProWorktimeApi } from '#/api/mes/pro/worktime'; |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | message, |
| | | Select, |
| | | Statistic, |
| | | Table, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getWorktimeReport } from '#/api/mes/pro/worktime'; |
| | | |
| | | defineOptions({ name: 'MesProWorktimeReport' }); |
| | | |
| | | const { RangePicker } = DatePicker; |
| | | |
| | | const DIM_OPTIONS = [ |
| | | { label: 'æäºº', value: 1 }, |
| | | { label: 'æå·¥åº', value: 2 }, |
| | | { label: 'ææºå°', value: 3 }, |
| | | { label: 'ææ¥æ', value: 4 }, |
| | | ]; |
| | | |
| | | interface ReportRow extends MesProWorktimeApi.ReportRow { |
| | | actualHoursText?: string; |
| | | standardHoursText?: string; |
| | | diffText?: string; |
| | | diffRateText?: string; |
| | | } |
| | | |
| | | const range = ref<[Dayjs, Dayjs]>([ |
| | | dayjs().subtract(29, 'day').startOf('day'), |
| | | dayjs().endOf('day'), |
| | | ]); |
| | | const dimType = ref(1); |
| | | const loading = ref(false); |
| | | const rows = ref<ReportRow[]>([]); |
| | | const summary = ref<ReportRow>({}); |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | |
| | | const columns = [ |
| | | { dataIndex: 'dimName', key: 'dimName', title: 'åç»', width: 180 }, |
| | | { dataIndex: 'feedbackCount', key: 'feedbackCount', title: 'æ¥å·¥åæ°', width: 100 }, |
| | | { |
| | | dataIndex: 'productionLength', |
| | | key: 'productionLength', |
| | | title: 'ç产é¿åº¦(ç±³)', |
| | | width: 130, |
| | | }, |
| | | { |
| | | dataIndex: 'actualHoursText', |
| | | key: 'actualHoursText', |
| | | title: 'å®é
å·¥æ¶(h)', |
| | | width: 120, |
| | | }, |
| | | { |
| | | dataIndex: 'standardHoursText', |
| | | key: 'standardHoursText', |
| | | title: 'æ åå·¥æ¶(h)', |
| | | width: 120, |
| | | }, |
| | | { dataIndex: 'diffText', key: 'diffText', title: 'å·¥æ¶å·®å¼(h)', width: 120 }, |
| | | { dataIndex: 'diffRateText', key: 'diffRateText', title: 'å·®å¼ç', width: 100 }, |
| | | { dataIndex: 'wageAmount', key: 'wageAmount', title: 'æ¥å·¥å·¥èµ(å
)', width: 130 }, |
| | | ]; |
| | | |
| | | function formatRow(row: MesProWorktimeApi.ReportRow): ReportRow { |
| | | const diff = row.workHoursDiff ?? 0; |
| | | const rate = row.workHoursDiffRate; |
| | | return { |
| | | ...row, |
| | | actualHoursText: Number(row.actualHours ?? 0).toFixed(2), |
| | | standardHoursText: Number(row.standardHours ?? 0).toFixed(2), |
| | | diffText: `${diff > 0 ? '+' : ''}${diff.toFixed(2)}`, |
| | | diffRateText: rate == null ? '-' : `${rate > 0 ? '+' : ''}${rate}%`, |
| | | }; |
| | | } |
| | | |
| | | async function loadData() { |
| | | if (!range.value?.[0] || !range.value?.[1]) { |
| | | message.warning('è¯·éæ©ç»è®¡å¨æ'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const result = await getWorktimeReport({ |
| | | beginTime: range.value[0].format('YYYY-MM-DD HH:mm:ss'), |
| | | endTime: range.value[1].format('YYYY-MM-DD HH:mm:ss'), |
| | | dimType: dimType.value, |
| | | }); |
| | | rows.value = (result?.rows ?? []).map(formatRow); |
| | | summary.value = formatRow(result?.summary ?? {}); |
| | | await renderChart(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function renderChart() { |
| | | const names = rows.value.map((row) => row.dimName ?? ''); |
| | | await renderEcharts({ |
| | | grid: { bottom: 60, left: 50, right: 20, top: 40 }, |
| | | legend: { data: ['å®é
å·¥æ¶', 'æ åå·¥æ¶'] }, |
| | | series: [ |
| | | { |
| | | barMaxWidth: 28, |
| | | data: rows.value.map((row) => Number(row.actualHours ?? 0)), |
| | | name: 'å®é
å·¥æ¶', |
| | | type: 'bar', |
| | | itemStyle: { color: '#1677ff' }, |
| | | }, |
| | | { |
| | | barMaxWidth: 28, |
| | | data: rows.value.map((row) => Number(row.standardHours ?? 0)), |
| | | name: 'æ åå·¥æ¶', |
| | | type: 'bar', |
| | | itemStyle: { color: '#52c41a' }, |
| | | }, |
| | | ], |
| | | tooltip: { trigger: 'axis' }, |
| | | xAxis: { |
| | | axisLabel: { rotate: names.length > 6 ? 30 : 0 }, |
| | | data: names, |
| | | type: 'category', |
| | | }, |
| | | yAxis: { name: 'å°æ¶', type: 'value' }, |
| | | }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex h-full flex-col gap-3"> |
| | | <Card size="small"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="text-sm text-gray-500">ç»è®¡å¨æ</span> |
| | | <RangePicker v-model:value="range" show-time /> |
| | | <span class="text-sm text-gray-500">ç»è®¡ç»´åº¦</span> |
| | | <Select v-model:value="dimType" class="w-32" :options="DIM_OPTIONS" /> |
| | | <Button type="primary" :loading="loading" @click="loadData"> |
| | | æ¥è¯¢ |
| | | </Button> |
| | | </div> |
| | | </Card> |
| | | <div class="grid grid-cols-2 gap-3 md:grid-cols-5"> |
| | | <Card size="small"> |
| | | <Statistic title="æ¥å·¥åæ°" :value="summary.feedbackCount ?? 0" /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="å®é
å·¥æ¶(h)" |
| | | :value="Number(summary.actualHours ?? 0)" |
| | | :precision="2" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="æ åå·¥æ¶(h)" |
| | | :value="Number(summary.standardHours ?? 0)" |
| | | :precision="2" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="å·¥æ¶å·®å¼(h)" |
| | | :value="Number(summary.workHoursDiff ?? 0)" |
| | | :precision="2" |
| | | :value-style="{ |
| | | color: Number(summary.workHoursDiff ?? 0) > 0 ? '#cf1322' : '#3f8600', |
| | | }" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="æ¥å·¥å·¥èµ(å
)" |
| | | :value="Number(summary.wageAmount ?? 0)" |
| | | :precision="2" |
| | | /> |
| | | </Card> |
| | | </div> |
| | | <Alert |
| | | v-if="summary.workHoursDiffRate != null && summary.workHoursDiffRate > 20" |
| | | message="æ´ä½å·¥æ¶é«äºæ åå·¥æ¶è¶
è¿ 20%" |
| | | :description="`æ¬å¨æå®é
å·¥æ¶ ${summary.actualHoursText} å°æ¶ï¼æ åå·¥æ¶ ${summary.standardHoursText} å°æ¶ï¼å·®å¼ ${summary.diffText} å°æ¶ï¼${summary.diffRateText}ï¼ã建议æåç»ä¸é»æ ¸å¯¹åºæ¥å·¥å·¥æ¶ä¸è¾
å©é¡¹æ¬¡æ°ã`" |
| | | show-icon |
| | | type="warning" |
| | | /> |
| | | <Card class="flex-1" size="small" title="å®é
å·¥æ¶ vs æ åå·¥æ¶"> |
| | | <EchartsUI ref="chartRef" class="h-[260px] w-full" /> |
| | | </Card> |
| | | <Card class="flex-1" size="small" title="åç»æç»"> |
| | | <Table |
| | | :columns="columns" |
| | | :data-source="rows" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | row-key="dimKey" |
| | | size="small" |
| | | /> |
| | | </Card> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | /** 计费类åï¼1-è®¡æ¶ 2-计件 */ |
| | | export const PROCESS_BILLING_TYPE_OPTIONS = [ |
| | | { label: '计æ¶ï¼å
/å°æ¶ï¼', value: 1 }, |
| | | { label: '计件ï¼å
/ä»¶ï¼', value: 2 }, |
| | | ]; |
| | | |
| | | /** 计费类ååç§° */ |
| | | export function getBillingTypeLabel(billingType?: number) { |
| | | return ( |
| | | PROCESS_BILLING_TYPE_OPTIONS.find((item) => item.value === billingType) |
| | | ?.label ?? '-' |
| | | ); |
| | | } |
| | | |
| | | /** å·¥èµå®é¢åä½ */ |
| | | export function getWageRateUnit(billingType?: number) { |
| | | return billingType === 1 ? 'å
/å°æ¶' : 'å
/ä»¶'; |
| | | } |
| | | |
| | | /** æ åå·¥æ¶è®¡ç®æ¹å¼ */ |
| | | export const STD_TIME_MODE_OPTIONS = [ |
| | | { label: 'æªå¯ç¨', value: 0 }, |
| | | { label: '转é à èè·', value: 1 }, |
| | | { label: '线é度ç´å', value: 2 }, |
| | | { label: '满éç¾åæ¯ Ã èè·', value: 3 }, |
| | | ]; |
| | | |
| | | /** æ åå·¥æ¶è®¡ç®æ¹å¼åç§° */ |
| | | export function getStdTimeModeLabel(mode?: number) { |
| | | return STD_TIME_MODE_OPTIONS.find((item) => item.value === mode)?.label ?? '-'; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç产工åºç表å */ |
| | | export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { |
| | | return [ |
| | |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | }, |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'billingType', |
| | | label: '计费类å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: PROCESS_BILLING_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è®¡è´¹ç±»å', |
| | | }, |
| | | rules: z.number().default(2), |
| | | }, |
| | | { |
| | | fieldName: 'wageRate', |
| | | label: 'å·¥èµå®é¢', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['billingType'], |
| | | componentProps: (values: Record<string, unknown>) => ({ |
| | | addonAfter: getWageRateUnit(values.billingType as number), |
| | | placeholder: |
| | | values.billingType === 1 |
| | | ? '请è¾å
¥å°æ¶å·¥èµå®é¢' |
| | | : '请è¾å
¥è®¡ä»¶åä»·', |
| | | }), |
| | | }, |
| | | componentProps: { |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | rules: z.number().min(0, 'å·¥èµå®é¢ä¸è½å°äº 0'), |
| | | }, |
| | | { |
| | | fieldName: 'auxHourlyRate', |
| | | label: 'è¾
å©å°æ¶å·¥èµ', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | addonAfter: 'å
/å°æ¶', |
| | | class: 'w-full', |
| | | min: 0, |
| | | precision: 4, |
| | | placeholder: 'è¾
å©å·¥æ¶æå·¥åºè®¡èªæ¶çåä»·ï¼å¦æ¢çº¿', |
| | | }, |
| | | rules: z.number().min(0, 'è¾
å©å°æ¶å·¥èµä¸è½å°äº 0').default(0), |
| | | }, |
| | | { |
| | | fieldName: 'stdTimeMode', |
| | | label: 'å·¥æ¶è®¡ç®æ¹å¼', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: STD_TIME_MODE_OPTIONS, |
| | | placeholder: 'è¯·éæ©æ åå·¥æ¶è®¡ç®æ¹å¼', |
| | | }, |
| | | rules: z.number().default(0), |
| | | }, |
| | | { |
| | | fieldName: 'stdTimeFactor', |
| | | label: 'æç®ç³»æ°', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => |
| | | values.stdTimeMode === 1 || values.stdTimeMode === 3, |
| | | }, |
| | | componentProps: { |
| | | min: 0, |
| | | precision: 4, |
| | | placeholder: 'æä¸å¡« 1.8ï¼å
¶ä½å¡« 1', |
| | | }, |
| | | rules: z.number().min(0).default(1), |
| | | }, |
| | | { |
| | | fieldName: 'maxRpm', |
| | | label: 'æºå°æ»¡è½¬é', |
| | | component: 'InputNumber', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => values.stdTimeMode === 3, |
| | | }, |
| | | componentProps: { |
| | | addonAfter: 'r/min', |
| | | min: 0, |
| | | precision: 0, |
| | | placeholder: '请è¾å
¥æºå°æ»¡è½¬é', |
| | | }, |
| | | rules: z.number().min(0).optional().default(0), |
| | | }, |
| | | { |
| | | fieldName: 'stdRpmParamCode', |
| | | label: '转éåæ°ç¼ç ', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => |
| | | values.stdTimeMode === 1 || values.stdTimeMode === 3, |
| | | }, |
| | | componentProps: { |
| | | maxLength: 64, |
| | | placeholder: 'å¡«åâå·¥åºåæ°ç®¡çâéç»å®çåæ°ç¼ç ï¼å¦ RPM', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'stdPitchParamCode', |
| | | label: 'èè·åæ°ç¼ç ', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => |
| | | values.stdTimeMode === 1 || values.stdTimeMode === 3, |
| | | }, |
| | | componentProps: { |
| | | maxLength: 64, |
| | | placeholder: 'å¡«ååæ°ç¼ç ï¼å¦ PITCH', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'stdSpeedParamCode', |
| | | label: '线éåº¦åæ°ç¼ç ', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => values.stdTimeMode === 2, |
| | | }, |
| | | componentProps: { |
| | | maxLength: 64, |
| | | placeholder: 'å¡«ååæ°ç¼ç ï¼ç±³/åï¼ï¼å¦ SPEED', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'stdPercentParamCode', |
| | | label: '转éç¾åæ¯ç¼ç ', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: ['stdTimeMode'], |
| | | show: (values: Record<string, unknown>) => values.stdTimeMode === 3, |
| | | }, |
| | | componentProps: { |
| | | maxLength: 64, |
| | | placeholder: 'å¡«ååæ°ç¼ç ï¼%ï¼ï¼å¦ RPM_PERCENT', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'attention', |
| | |
| | | }, |
| | | { field: 'name', title: 'å·¥åºåç§°', minWidth: 180 }, |
| | | { |
| | | field: 'billingType', |
| | | title: '计费类å', |
| | | width: 120, |
| | | formatter: ({ cellValue }) => getBillingTypeLabel(cellValue), |
| | | }, |
| | | { |
| | | field: 'wageRate', |
| | | title: 'å·¥èµå®é¢', |
| | | width: 120, |
| | | formatter: ({ cellValue, row }) => |
| | | cellValue == null ? '-' : `${cellValue} ${getWageRateUnit(row.billingType)}`, |
| | | }, |
| | | { |
| | | field: 'auxHourlyRate', |
| | | title: 'è¾
å©å°æ¶å·¥èµ', |
| | | width: 130, |
| | | formatter: ({ cellValue }) => |
| | | cellValue == null ? '-' : `${cellValue} å
/å°æ¶`, |
| | | }, |
| | | { |
| | | field: 'stdTimeMode', |
| | | title: 'å·¥æ¶è®¡ç®æ¹å¼', |
| | | width: 150, |
| | | formatter: ({ cellValue }) => getStdTimeModeLabel(cellValue), |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ParamManage from './modules/param-manage.vue'; |
| | | import QualityRuleManage from './modules/quality-rule-manage.vue'; |
| | | import WageFieldManage from './modules/wage-field-manage.vue'; |
| | | import WageRuleManage from './modules/wage-rule-manage.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [WageFieldModal, wageFieldModalApi] = useVbenModal({ |
| | | connectedComponent: WageFieldManage, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [WageRuleModal, wageRuleModalApi] = useVbenModal({ |
| | | connectedComponent: WageRuleManage, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [QualityRuleModal, qualityRuleModalApi] = useVbenModal({ |
| | | connectedComponent: QualityRuleManage, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | |
| | | /** 管çå·¥åºç»å®çå·¥èºåæ° */ |
| | | function handleParamManage(row: MesProProcessApi.Process) { |
| | | paramManageModalApi |
| | | .setData({ processId: row.id, processName: row.name }) |
| | | .open(); |
| | | } |
| | | |
| | | /** 管çå·¥åºå·¥èµå段 */ |
| | | function handleWageFieldManage(row: MesProProcessApi.Process) { |
| | | wageFieldModalApi |
| | | .setData({ processId: row.id, processName: row.name }) |
| | | .open(); |
| | | } |
| | | |
| | | /** 管çå·¥åºå·¥èµåä»·è§å */ |
| | | function handleWageRuleManage(row: MesProProcessApi.Process) { |
| | | wageRuleModalApi |
| | | .setData({ |
| | | processId: row.id, |
| | | processName: row.name, |
| | | billingType: row.billingType, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** 管çè´¨éå·¥èµè§å */ |
| | | function handleQualityRuleManage(row: MesProProcessApi.Process) { |
| | | qualityRuleModalApi |
| | | .setData({ processId: row.id, processName: row.name }) |
| | | .open(); |
| | | } |
| | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ParamManageModal @success="handleRefresh" /> |
| | | <WageFieldModal @success="handleRefresh" /> |
| | | <WageRuleModal @success="handleRefresh" /> |
| | | <QualityRuleModal @success="handleRefresh" /> |
| | | <Grid table-title="ç产工åºå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | }, |
| | | }, |
| | | ]" |
| | | :drop-down-actions="[ |
| | | { |
| | | label: 'å·¥èµå段', |
| | | onClick: handleWageFieldManage.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åä»·è§å', |
| | | onClick: handleWageRuleManage.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'è´¨éè§å', |
| | | onClick: handleQualityRuleManage.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesProProcessApi } from '#/api/mes/pro/process'; |
| | | import type { MesProWageApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | |
| | | getProcess, |
| | | updateProcess, |
| | | } from '#/api/mes/pro/process'; |
| | | import { getWageFieldList, saveWageFieldList } from '#/api/mes/pro/wage'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import ContentList from './content-list.vue'; |
| | | import WageFieldEditor from './wage-field-editor.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); // è¡¨åæ¨¡å¼ |
| | | const formData = ref<MesProProcessApi.Process>(); |
| | | /** æ°å¢/ä¿®æ¹æ¶éå·¥åºä¸èµ·ç»´æ¤çèªå®ä¹å·¥èµå段 */ |
| | | const wageFields = ref<MesProWageApi.WageField[]>([]); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); // æ¯å¦æ¥çæ¨¡å¼ |
| | | const getTitle = computed(() => { |
| | |
| | | // æäº¤è¡¨å |
| | | const data = (await formApi.getValues()) as MesProProcessApi.Process; |
| | | try { |
| | | await (formData.value?.id ? updateProcess(data) : createProcess(data)); |
| | | // å·¥èµå段å
ååç«¯æ ¡éªï¼é¿å
å端æ´è¡¨ä¿åæ¶æ¥ä¸æ¡ç¬¼ç»é误 |
| | | for (const [index, item] of wageFields.value.entries()) { |
| | | if (!item.fieldCode?.trim()) { |
| | | message.warning(`å·¥èµå段第 ${index + 1} è¡ï¼å段ç¼ç ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | if (!item.fieldName?.trim()) { |
| | | message.warning(`å·¥èµå段第 ${index + 1} è¡ï¼å段åç§°ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | } |
| | | const isUpdate = Boolean(formData.value?.id); |
| | | const id = formData.value?.id ?? (await createProcess(data)); |
| | | if (isUpdate) { |
| | | await updateProcess(data); |
| | | } else { |
| | | // å·¥åºå·²å建æåï¼è®°ä¸ç¼å·ï¼å·¥èµå段ä¿å失败æ¶éè¯èµ°ä¿®æ¹ï¼é¿å
éå¤å»ºå·¥åº |
| | | formData.value = { ...data, id }; |
| | | } |
| | | // ä¿®æ¹æ¶å
¨éè¦çï¼æ°å¢æ¶åªæå¡«äºå段æä¿å |
| | | if (id && (isUpdate || wageFields.value.length > 0)) { |
| | | await saveWageFieldList( |
| | | id, |
| | | wageFields.value.map((item, index) => ({ |
| | | id: item.id, |
| | | processId: id, |
| | | fieldCode: item.fieldCode?.trim(), |
| | | fieldName: item.fieldName?.trim(), |
| | | fieldType: item.fieldType ?? 1, |
| | | unit: item.unit, |
| | | fieldValue: item.fieldValue, |
| | | calcRole: item.calcRole ?? 1, |
| | | unitPrice: item.calcRole === 3 ? item.unitPrice : undefined, |
| | | sort: item.sort ?? index + 1, |
| | | status: item.status ?? 0, |
| | | remark: item.remark, |
| | | })), |
| | | ); |
| | | } |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | wageFields.value = []; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | wageFields.value = []; |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | |
| | | formData.value = await getProcess(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | // 带åºå·²é
ç½®çå·¥åºå·¥èµå段 |
| | | wageFields.value = (await getWageFieldList(data.id)) ?? []; |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-4" /> |
| | | <Divider class="!my-3" orientation="left">å·¥èµå段</Divider> |
| | | <div class="mx-4"> |
| | | <WageFieldEditor :disabled="isDetail" :rows="wageFields" /> |
| | | </div> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºå·¥åºæä½æ¥éª¤åè¡¨ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <template v-if="processId"> |
| | | <Divider class="!my-3" orientation="left">æä½æ¥éª¤</Divider> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProWageApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, Input, InputNumber, message, Select } from 'ant-design-vue'; |
| | | |
| | | import { getRouteSimpleList } from '#/api/mes/pro/route'; |
| | | import { |
| | | createQualityRule, |
| | | deleteQualityRule, |
| | | getQualityRuleList, |
| | | updateQualityRule, |
| | | } from '#/api/mes/pro/wage'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | /** è§åç±»åï¼1-åæ ¼çç³»æ° 2-ä¸åæ ¼æ£æ¬¾ */ |
| | | const RULE_TYPE_OPTIONS = [ |
| | | { label: 'åæ ¼çç³»æ°', value: 1 }, |
| | | { label: 'ä¸åæ ¼æ£æ¬¾', value: 2 }, |
| | | ]; |
| | | |
| | | const processId = ref<number>(); |
| | | const processName = ref<string>(''); |
| | | const loading = ref(false); |
| | | const list = ref<MesProWageApi.QualityRule[]>([]); |
| | | const originalIds = ref<number[]>([]); |
| | | const routeOptions = ref<Array<{ label: string; value: number }>>([]); |
| | | |
| | | const getTitle = computed(() => `è´¨éå·¥èµè§å - ${processName.value || ''}`); |
| | | |
| | | /** å è½½è§åä¸è·¯çº¿ */ |
| | | async function loadData() { |
| | | if (!processId.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const [rules, routes] = await Promise.all([ |
| | | getQualityRuleList(processId.value), |
| | | getRouteSimpleList(), |
| | | ]); |
| | | list.value = rules; |
| | | originalIds.value = rules |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => id !== undefined); |
| | | routeOptions.value = (routes as MesProRouteApi.Route[]).map((item) => ({ |
| | | label: `${item.code ?? ''} ${item.name ?? ''}`.trim(), |
| | | value: item.id!, |
| | | })); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢ä¸æ¡è§å */ |
| | | function addRow() { |
| | | list.value.push({ |
| | | processId: processId.value, |
| | | ruleType: 1, |
| | | minRate: 0, |
| | | maxRate: 100, |
| | | coefficient: 1, |
| | | priority: 0, |
| | | status: 0, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¸æ¡è§å */ |
| | | function removeRow(index: number) { |
| | | list.value.splice(index, 1); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (processId.value === undefined) { |
| | | return; |
| | | } |
| | | for (const [index, item] of list.value.entries()) { |
| | | if (item.minRate === undefined || item.maxRate === undefined) { |
| | | message.warning(`第 ${index + 1} è¡ï¼åæ ¼çåºé´ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | if (item.minRate >= item.maxRate) { |
| | | message.warning(`第 ${index + 1} è¡ï¼åæ ¼çä¸éå¿
é¡»å°äºä¸é`); |
| | | return; |
| | | } |
| | | if (item.ruleType === 1 && (!item.coefficient || item.coefficient <= 0)) { |
| | | message.warning(`第 ${index + 1} è¡ï¼åæ ¼çç³»æ°å¿
é¡»å¤§äº 0`); |
| | | return; |
| | | } |
| | | if ( |
| | | item.ruleType === 2 && |
| | | (item.deductionPerUnit === undefined || item.deductionPerUnit === null) |
| | | ) { |
| | | message.warning(`第 ${index + 1} è¡ï¼æ¯ä»¶ä¸åæ ¼æ£æ¬¾ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const keepIds = list.value |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => id !== undefined); |
| | | const removedIds = originalIds.value.filter((id) => !keepIds.includes(id)); |
| | | for (const id of removedIds) { |
| | | await deleteQualityRule(id); |
| | | } |
| | | for (const item of list.value) { |
| | | const payload: MesProWageApi.QualityRule = { |
| | | id: item.id, |
| | | processId: processId.value, |
| | | routeId: item.routeId, |
| | | ruleType: item.ruleType ?? 1, |
| | | minRate: item.minRate, |
| | | maxRate: item.maxRate, |
| | | coefficient: item.ruleType === 1 ? item.coefficient : undefined, |
| | | deductionPerUnit: |
| | | item.ruleType === 2 ? item.deductionPerUnit : undefined, |
| | | priority: item.priority ?? 0, |
| | | status: item.status ?? 0, |
| | | remark: item.remark, |
| | | }; |
| | | if (item.id) { |
| | | await updateQualityRule(payload); |
| | | } else { |
| | | await createQualityRule(payload); |
| | | } |
| | | } |
| | | message.success('ä¿åæå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | processId.value = undefined; |
| | | list.value = []; |
| | | originalIds.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ processId: number; processName?: string }>(); |
| | | processId.value = data.processId; |
| | | processName.value = data.processName || ''; |
| | | await loadData(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <div class="mx-4"> |
| | | <div class="mb-3 rounded bg-blue-50 p-3 text-xs text-gray-600"> |
| | | è´¨æ£å·¥åºå®¡æ¹æ¶æè¿ç¨æ£éªåæ ¼çå¹é
è§åï¼åæ ¼ç ⥠ä¸éä¸ < ä¸éå³å½ä¸ã |
| | | ç³»æ°è§åæãå·¥èµ Ã ç³»æ°ã计ç®ï¼æ£æ¬¾è§åæãå·¥èµ â ä¸åæ ¼æ°é à æ¯ä»¶æ£æ¬¾ã计ç®ï¼ |
| | | åä¸åæ ¼çå½ä¸å¤æ¡æ¶åä¼å
级大çï¼è·¯çº¿ä¸ºç©ºè§ä¸ºéç¨è§åã |
| | | </div> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="text-sm font-medium">è´¨éå·¥èµè§å</span> |
| | | <Button type="primary" size="small" @click="addRow">æ°å¢è§å</Button> |
| | | </div> |
| | | <div v-if="list.length > 0" class="rounded border border-gray-200"> |
| | | <div |
| | | class="grid grid-cols-24 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium" |
| | | > |
| | | <div class="col-span-3">è§åç±»å</div> |
| | | <div class="col-span-3">å·¥èºè·¯çº¿</div> |
| | | <div class="col-span-2">ä¸é(%)</div> |
| | | <div class="col-span-2">ä¸é(%)</div> |
| | | <div class="col-span-3">ç³»æ° / æ¯ä»¶æ£æ¬¾</div> |
| | | <div class="col-span-1">ä¼å
级</div> |
| | | <div class="col-span-2">ç¶æ</div> |
| | | <div class="col-span-4">夿³¨</div> |
| | | <div class="col-span-2 text-center">æä½</div> |
| | | </div> |
| | | <div |
| | | v-for="(item, index) in list" |
| | | :key="item.id ?? `new-${index}`" |
| | | class="grid grid-cols-24 items-center gap-2 border-b border-gray-100 p-2" |
| | | > |
| | | <div class="col-span-3"> |
| | | <Select |
| | | v-model:value="item.ruleType" |
| | | :options="RULE_TYPE_OPTIONS" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <Select |
| | | v-model:value="item.routeId" |
| | | :allow-clear="true" |
| | | :options="routeOptions" |
| | | option-filter-prop="label" |
| | | placeholder="éç¨" |
| | | show-search |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber |
| | | v-model:value="item.minRate" |
| | | :max="100" |
| | | :min="0" |
| | | :precision="2" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber |
| | | v-model:value="item.maxRate" |
| | | :max="100" |
| | | :min="0" |
| | | :precision="2" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <InputNumber |
| | | v-if="item.ruleType === 1" |
| | | v-model:value="item.coefficient" |
| | | :min="0" |
| | | :precision="4" |
| | | placeholder="ç³»æ°ï¼å¦ 0.92" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | <InputNumber |
| | | v-else |
| | | v-model:value="item.deductionPerUnit" |
| | | :min="0" |
| | | :precision="4" |
| | | placeholder="å
/ä»¶" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-1"> |
| | | <InputNumber v-model:value="item.priority" :min="0" size="small" class="w-full" /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.status" |
| | | :options="[ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-4"> |
| | | <Input v-model:value="item.remark" placeholder="夿³¨" size="small" /> |
| | | </div> |
| | | <div class="col-span-2 flex items-center justify-center"> |
| | | <Button type="text" danger size="small" @click="removeRow(index)"> |
| | | å é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400" |
| | | > |
| | | {{ loading ? 'å è½½ä¸â¦' : 'æªé
ç½®è§åæ¶è´¨æ£å·¥åºä¸åè´¨éå·¥èµè°æ´' }} |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProWageApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { Button, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | /** |
| | | * å·¥åºå·¥èµå段ç¼è¾å¨ï¼åæ§ï¼ç´æ¥ä¿®æ¹ä¼ å
¥ç rows æ°ç»ï¼ |
| | | * æ°å¢/ä¿®æ¹å·¥åºè¡¨åä¸ç¬ç«çãå·¥èµå段ãå¼¹æ¡å
±ç¨åä¸å¥å |
| | | */ |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | rows: MesProWageApi.WageField[]; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | }, |
| | | ); |
| | | |
| | | /** åæ®µç±»åï¼1-æ°å¼ 2-ææ¬ 3-䏿 4-å¸å° */ |
| | | const FIELD_TYPE_OPTIONS = [ |
| | | { label: 'æ°å¼', value: 1 }, |
| | | { label: 'ææ¬', value: 2 }, |
| | | { label: '䏿', value: 3 }, |
| | | { label: 'å¸å°', value: 4 }, |
| | | ]; |
| | | |
| | | /** 计ç®è§è²ï¼1-ç³»æ° 2-å åéé¢ 3-计价æ°é 4-ä»
è®°å½ */ |
| | | const CALC_ROLE_OPTIONS = [ |
| | | { label: 'ç³»æ°ï¼è¿ä¹ï¼', value: 1 }, |
| | | { label: 'å åéé¢', value: 2 }, |
| | | { label: '计价æ°é', value: 3 }, |
| | | { label: 'ä»
è®°å½', value: 4 }, |
| | | ]; |
| | | |
| | | /** æ°å¢ä¸è¡å·¥èµå段 */ |
| | | function addRow() { |
| | | props.rows.push({ |
| | | fieldCode: '', |
| | | fieldName: '', |
| | | fieldType: 1, |
| | | calcRole: 1, |
| | | unitPrice: undefined, |
| | | sort: props.rows.length + 1, |
| | | status: 0, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¸è¡ */ |
| | | function removeRow(index: number) { |
| | | props.rows.splice(index, 1); |
| | | } |
| | | |
| | | /** 计ç®è§è²ä¸ºã计价æ°éãæ¶æéè¦åä»· */ |
| | | function needUnitPrice(calcRole?: number) { |
| | | return calcRole === 3; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="text-sm font-medium">å·¥åºå·¥èµå段ï¼ç¨æ·èªå®ä¹ï¼</span> |
| | | <Button |
| | | v-if="!disabled" |
| | | type="primary" |
| | | size="small" |
| | | @click="addRow" |
| | | > |
| | | æ°å¢å段 |
| | | </Button> |
| | | </div> |
| | | <div class="mb-2 text-xs text-gray-500"> |
| | | ç³»æ°ä¸ä¸»å·¥èµãè¾
å©å·¥èµè¿ä¹ï¼ç空æ 1ï¼ï¼å åéé¢ç´æ¥å å°å·¥èµï¼è®¡ä»·æ°é = åæ®µå¼ à åä»·ï¼éå¡«åä»·ï¼ã |
| | | åºå®å¼å¯ç空ï¼ç±è¿ç¨æ£éªæ¶è´¨æ£å¡«åï¼å¡«åå¼ä¼å
äºåºå®å¼ã |
| | | </div> |
| | | <div v-if="rows.length > 0" class="rounded border border-gray-200"> |
| | | <div |
| | | class="grid grid-cols-24 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium" |
| | | > |
| | | <div class="col-span-3">åæ®µç¼ç </div> |
| | | <div class="col-span-3">åæ®µåç§°</div> |
| | | <div class="col-span-2">ç±»å</div> |
| | | <div class="col-span-2">åä½</div> |
| | | <div class="col-span-3">计ç®è§è²</div> |
| | | <div class="col-span-2">åºå®å¼</div> |
| | | <div class="col-span-2">åä»·</div> |
| | | <div class="col-span-2">æåº</div> |
| | | <div class="col-span-2">ç¶æ</div> |
| | | <div class="col-span-2">夿³¨</div> |
| | | <div class="col-span-1 text-center">æä½</div> |
| | | </div> |
| | | <div |
| | | v-for="(item, index) in rows" |
| | | :key="item.id ?? `new-${index}`" |
| | | class="grid grid-cols-24 items-center gap-2 border-b border-gray-100 p-2" |
| | | > |
| | | <div class="col-span-3"> |
| | | <Input |
| | | v-model:value="item.fieldCode" |
| | | :disabled="disabled" |
| | | placeholder="å¦ TEMP_COEF" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <Input |
| | | v-model:value="item.fieldName" |
| | | :disabled="disabled" |
| | | placeholder="å¦ é«æ¸©ç³»æ°" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.fieldType" |
| | | :disabled="disabled" |
| | | :options="FIELD_TYPE_OPTIONS" |
| | | class="w-full" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input |
| | | v-model:value="item.unit" |
| | | :disabled="disabled" |
| | | placeholder="åä½" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <Select |
| | | v-model:value="item.calcRole" |
| | | :disabled="disabled" |
| | | :options="CALC_ROLE_OPTIONS" |
| | | class="w-full" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input |
| | | v-model:value="item.fieldValue" |
| | | :disabled="disabled" |
| | | placeholder="åºå®å¼" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber |
| | | v-model:value="item.unitPrice" |
| | | :disabled="disabled || !needUnitPrice(item.calcRole)" |
| | | :min="0" |
| | | :precision="4" |
| | | class="w-full" |
| | | placeholder="åä»·" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber |
| | | v-model:value="item.sort" |
| | | :disabled="disabled" |
| | | :min="0" |
| | | class="w-full" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.status" |
| | | :disabled="disabled" |
| | | :options="[ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]" |
| | | class="w-full" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input |
| | | v-model:value="item.remark" |
| | | :disabled="disabled" |
| | | placeholder="夿³¨" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="col-span-1 flex items-center justify-center"> |
| | | <Button |
| | | v-if="!disabled" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click="removeRow(index)" |
| | | > |
| | | å é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="rounded border border-dashed border-gray-300 p-3 text-center text-xs text-gray-400" |
| | | > |
| | | {{ |
| | | disabled |
| | | ? 'æªé
置工èµå段' |
| | | : 'ææªé
置工èµå段ï¼å¦éæå·¥åºåèªå®ä¹åæ°ï¼å¦é«æ¸©ç³»æ°ãè®¾å¤æçï¼ï¼ç¹å»ãæ°å¢åæ®µãæ·»å ' |
| | | }} |
| | | </div> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProWageApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | Button, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Select, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getWageFieldList, |
| | | saveWageFieldList, |
| | | } from '#/api/mes/pro/wage'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | /** åæ®µç±»åï¼1-æ°å¼ 2-ææ¬ 3-䏿 4-å¸å° */ |
| | | const FIELD_TYPE_OPTIONS = [ |
| | | { label: 'æ°å¼', value: 1 }, |
| | | { label: 'ææ¬', value: 2 }, |
| | | { label: '䏿', value: 3 }, |
| | | { label: 'å¸å°', value: 4 }, |
| | | ]; |
| | | |
| | | /** 计ç®è§è²ï¼1-ç³»æ° 2-å åéé¢ 3-计价æ°é 4-ä»
è®°å½ */ |
| | | const CALC_ROLE_OPTIONS = [ |
| | | { label: 'ç³»æ°ï¼è¿ä¹ï¼', value: 1 }, |
| | | { label: 'å åéé¢', value: 2 }, |
| | | { label: '计价æ°é', value: 3 }, |
| | | { label: 'ä»
è®°å½', value: 4 }, |
| | | ]; |
| | | |
| | | const processId = ref<number>(); |
| | | const processName = ref<string>(''); |
| | | const loading = ref(false); |
| | | const list = ref<MesProWageApi.WageField[]>([]); |
| | | |
| | | const getTitle = computed(() => `å·¥åºå·¥èµå段 - ${processName.value || ''}`); |
| | | |
| | | /** å 载工åºå·¥èµå段 */ |
| | | async function loadData() { |
| | | if (!processId.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | list.value = await getWageFieldList(processId.value); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢ä¸è¡å·¥èµå段 */ |
| | | function addRow() { |
| | | list.value.push({ |
| | | processId: processId.value, |
| | | fieldCode: '', |
| | | fieldName: '', |
| | | fieldType: 1, |
| | | calcRole: 1, |
| | | unitPrice: 0, |
| | | sort: list.value.length + 1, |
| | | status: 0, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¸è¡ */ |
| | | function removeRow(index: number) { |
| | | list.value.splice(index, 1); |
| | | } |
| | | |
| | | /** 计ç®è§è²ä¸ºã计价æ°éãæ¶æéè¦åä»· */ |
| | | function needUnitPrice(calcRole?: number) { |
| | | return calcRole === 3; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (processId.value === undefined) { |
| | | return; |
| | | } |
| | | // å端å
åå¿
å¡«æ ¡éªï¼é¿å
å端æ´è¡¨ä¿åæ¶æ¥ä¸æ¡ç¬¼ç»é误 |
| | | for (const [index, item] of list.value.entries()) { |
| | | if (!item.fieldCode?.trim()) { |
| | | message.warning(`第 ${index + 1} è¡ï¼å段ç¼ç ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | if (!item.fieldName?.trim()) { |
| | | message.warning(`第 ${index + 1} è¡ï¼å段åç§°ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await saveWageFieldList( |
| | | processId.value, |
| | | list.value.map((item, index) => ({ |
| | | id: item.id, |
| | | processId: processId.value, |
| | | fieldCode: item.fieldCode?.trim(), |
| | | fieldName: item.fieldName?.trim(), |
| | | fieldType: item.fieldType ?? 1, |
| | | unit: item.unit, |
| | | fieldValue: item.fieldValue, |
| | | calcRole: item.calcRole ?? 1, |
| | | unitPrice: needUnitPrice(item.calcRole) ? item.unitPrice : undefined, |
| | | sort: item.sort ?? index + 1, |
| | | status: item.status ?? 0, |
| | | remark: item.remark, |
| | | })), |
| | | ); |
| | | message.success('ä¿åæå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | processId.value = undefined; |
| | | list.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ processId: number; processName?: string }>(); |
| | | processId.value = data.processId; |
| | | processName.value = data.processName || ''; |
| | | await loadData(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <div class="mx-4"> |
| | | <div class="mb-3 rounded bg-blue-50 p-3 text-xs text-gray-600"> |
| | | ç³»æ°ï¼ä¸ä¸»å·¥èµãè¾
å©å·¥èµè¿ä¹ï¼æªå¡«æâ¤0 æ 1 å¤çï¼ï¼å åéé¢ï¼ç´æ¥å å°å·¥èµä¸ï¼ |
| | | 计价æ°éï¼åæ®µå¼ Ã åä»·åå è¿å·¥èµï¼ä»
è®°å½ï¼åªéæ¥å·¥å¿«ç
§ä¿åï¼ä¸åä¸è®¡ç®ã |
| | | </div> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="text-sm font-medium">å·¥åºå·¥èµå段</span> |
| | | <Button type="primary" size="small" @click="addRow">æ°å¢å段</Button> |
| | | </div> |
| | | <div v-if="list.length > 0" class="rounded border border-gray-200"> |
| | | <div |
| | | class="grid grid-cols-24 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium" |
| | | > |
| | | <div class="col-span-3">åæ®µç¼ç </div> |
| | | <div class="col-span-3">åæ®µåç§°</div> |
| | | <div class="col-span-2">ç±»å</div> |
| | | <div class="col-span-2">åä½</div> |
| | | <div class="col-span-3">计ç®è§è²</div> |
| | | <div class="col-span-2">åºå®å¼</div> |
| | | <div class="col-span-2">åä»·</div> |
| | | <div class="col-span-2">æåº</div> |
| | | <div class="col-span-2">ç¶æ</div> |
| | | <div class="col-span-2">夿³¨</div> |
| | | <div class="col-span-1 text-center">æä½</div> |
| | | </div> |
| | | <div |
| | | v-for="(item, index) in list" |
| | | :key="item.id ?? `new-${index}`" |
| | | class="grid grid-cols-24 items-center gap-2 border-b border-gray-100 p-2" |
| | | > |
| | | <div class="col-span-3"> |
| | | <Input v-model:value="item.fieldCode" placeholder="å¦ TEMP_COEF" size="small" /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <Input v-model:value="item.fieldName" placeholder="å¦ é«æ¸©ç³»æ°" size="small" /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.fieldType" |
| | | :options="FIELD_TYPE_OPTIONS" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input v-model:value="item.unit" placeholder="åä½" size="small" /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <Select |
| | | v-model:value="item.calcRole" |
| | | :options="CALC_ROLE_OPTIONS" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input v-model:value="item.fieldValue" placeholder="åºå®å¼" size="small" /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber |
| | | v-model:value="item.unitPrice" |
| | | :disabled="!needUnitPrice(item.calcRole)" |
| | | :min="0" |
| | | :precision="4" |
| | | placeholder="åä»·" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <InputNumber v-model:value="item.sort" :min="0" size="small" class="w-full" /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.status" |
| | | :options="[ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input v-model:value="item.remark" placeholder="夿³¨" size="small" /> |
| | | </div> |
| | | <div class="col-span-1 flex items-center justify-center"> |
| | | <Button type="text" danger size="small" @click="removeRow(index)"> |
| | | å é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400" |
| | | > |
| | | {{ loading ? 'å è½½ä¸â¦' : 'ææªé
置工èµå段ï¼ç¹å»ãæ°å¢åæ®µãæ·»å ' }} |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import type { MesDvMachineryApi } from '#/api/mes/dv/machinery'; |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | import type { MesProWageApi } from '#/api/mes/pro/wage'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | Button, |
| | | DatePicker, |
| | | InputNumber, |
| | | Input, |
| | | message, |
| | | Select, |
| | | } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { getMachinerySimpleList } from '#/api/mes/dv/machinery'; |
| | | import { getRouteSimpleList } from '#/api/mes/pro/route'; |
| | | import { |
| | | createWageRule, |
| | | deleteWageRule, |
| | | getWageRuleList, |
| | | updateWageRule, |
| | | } from '#/api/mes/pro/wage'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | interface RuleRow extends Omit<MesProWageApi.WageRule, 'effectiveFrom' | 'effectiveTo'> { |
| | | effectiveFrom?: Dayjs | string; |
| | | effectiveTo?: Dayjs | string; |
| | | } |
| | | |
| | | const BILLING_TYPE_OPTIONS = [ |
| | | { label: '计æ¶ï¼å
/å°æ¶ï¼', value: 1 }, |
| | | { label: '计件ï¼å
/ä»¶ï¼', value: 2 }, |
| | | ]; |
| | | |
| | | const processId = ref<number>(); |
| | | const processName = ref<string>(''); |
| | | const processBillingType = ref<number>(2); |
| | | const loading = ref(false); |
| | | const list = ref<RuleRow[]>([]); |
| | | const originalIds = ref<number[]>([]); |
| | | const routeOptions = ref<Array<{ label: string; value: number }>>([]); |
| | | const machineryOptions = ref<Array<{ label: string; value: number }>>([]); |
| | | |
| | | const getTitle = computed(() => `å·¥åºå·¥èµåä»·è§å - ${processName.value || ''}`); |
| | | |
| | | /** åä»·åä½ */ |
| | | function getRateUnit(billingType?: number) { |
| | | return billingType === 1 ? 'å
/å°æ¶' : 'å
/ä»¶'; |
| | | } |
| | | |
| | | /** å è½½è§åã路线ãè®¾å¤ */ |
| | | async function loadData() { |
| | | if (!processId.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const [rules, routes, machineries] = await Promise.all([ |
| | | getWageRuleList(processId.value), |
| | | getRouteSimpleList(), |
| | | getMachinerySimpleList(), |
| | | ]); |
| | | list.value = rules as RuleRow[]; |
| | | originalIds.value = rules |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => id !== undefined); |
| | | routeOptions.value = (routes as MesProRouteApi.Route[]).map((item) => ({ |
| | | label: `${item.code ?? ''} ${item.name ?? ''}`.trim(), |
| | | value: item.id!, |
| | | })); |
| | | machineryOptions.value = (machineries as MesDvMachineryApi.Machinery[]).map( |
| | | (item) => ({ |
| | | label: `${item.code ?? ''} ${item.name ?? ''}`.trim(), |
| | | value: item.id!, |
| | | }), |
| | | ); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢ä¸æ¡è§å */ |
| | | function addRow() { |
| | | list.value.push({ |
| | | processId: processId.value, |
| | | billingType: processBillingType.value, |
| | | wageRate: undefined, |
| | | priority: 0, |
| | | status: 0, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¸æ¡è§å */ |
| | | function removeRow(index: number) { |
| | | list.value.splice(index, 1); |
| | | } |
| | | |
| | | /** æ¥æè½¬å符串 */ |
| | | function formatDate(value?: Dayjs | string) { |
| | | if (!value) { |
| | | return undefined; |
| | | } |
| | | return dayjs.isDayjs(value) ? value.format('YYYY-MM-DD') : value; |
| | | } |
| | | |
| | | /** ç»è£
æäº¤æ°æ® */ |
| | | function buildPayload(item: RuleRow): MesProWageApi.WageRule { |
| | | return { |
| | | id: item.id, |
| | | processId: processId.value, |
| | | routeId: item.routeId, |
| | | machineryId: item.machineryId, |
| | | billingType: item.billingType ?? processBillingType.value, |
| | | wageRate: item.wageRate, |
| | | effectiveFrom: formatDate(item.effectiveFrom), |
| | | effectiveTo: formatDate(item.effectiveTo), |
| | | priority: item.priority ?? 0, |
| | | status: item.status ?? 0, |
| | | remark: item.remark, |
| | | }; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (processId.value === undefined) { |
| | | return; |
| | | } |
| | | for (const [index, item] of list.value.entries()) { |
| | | if (item.wageRate === undefined || item.wageRate === null) { |
| | | message.warning(`第 ${index + 1} è¡ï¼å·¥èµåä»·ä¸è½ä¸ºç©º`); |
| | | return; |
| | | } |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const keepIds = list.value |
| | | .map((item) => item.id) |
| | | .filter((id): id is number => id !== undefined); |
| | | // ç§»é¤çè¡ç´æ¥å é¤ |
| | | const removedIds = originalIds.value.filter((id) => !keepIds.includes(id)); |
| | | for (const id of removedIds) { |
| | | await deleteWageRule(id); |
| | | } |
| | | // æ°å¢ä¸ä¿®æ¹éè¡æäº¤ |
| | | for (const item of list.value) { |
| | | const payload = buildPayload(item); |
| | | if (item.id) { |
| | | await updateWageRule(payload); |
| | | } else { |
| | | await createWageRule(payload); |
| | | } |
| | | } |
| | | message.success('ä¿åæå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | processId.value = undefined; |
| | | list.value = []; |
| | | originalIds.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | processId: number; |
| | | processName?: string; |
| | | billingType?: number; |
| | | }>(); |
| | | processId.value = data.processId; |
| | | processName.value = data.processName || ''; |
| | | processBillingType.value = data.billingType ?? 2; |
| | | await loadData(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <div class="mx-4"> |
| | | <div class="mb-3 rounded bg-blue-50 p-3 text-xs text-gray-600"> |
| | | æãå·¥èºè·¯çº¿ + 设å¤ãè¦çå·¥åºé»è®¤å·¥èµå®é¢ãå¹é
顺åºï¼è·¯çº¿+è®¾å¤ â 路线 â è®¾å¤ â å·¥åºé»è®¤ï¼ |
| | | ååæ¶ä¼å
级大çä¼å
ï¼åæçæå¼å§æ¥ææ°çä¼å
ã路线/设å¤ç空表示ä¸éã |
| | | </div> |
| | | <div class="mb-2 flex items-center justify-between"> |
| | | <span class="text-sm font-medium">å·¥èµåä»·è§å</span> |
| | | <Button type="primary" size="small" @click="addRow">æ°å¢è§å</Button> |
| | | </div> |
| | | <div v-if="list.length > 0" class="rounded border border-gray-200"> |
| | | <div |
| | | class="grid grid-cols-24 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium" |
| | | > |
| | | <div class="col-span-4">å·¥èºè·¯çº¿</div> |
| | | <div class="col-span-4">设å¤</div> |
| | | <div class="col-span-2">计费类å</div> |
| | | <div class="col-span-3">å·¥èµåä»·</div> |
| | | <div class="col-span-2">çæå¼å§</div> |
| | | <div class="col-span-2">çæç»æ</div> |
| | | <div class="col-span-1">ä¼å
级</div> |
| | | <div class="col-span-2">ç¶æ</div> |
| | | <div class="col-span-2">夿³¨</div> |
| | | <div class="col-span-2 text-center">æä½</div> |
| | | </div> |
| | | <div |
| | | v-for="(item, index) in list" |
| | | :key="item.id ?? `new-${index}`" |
| | | class="grid grid-cols-24 items-center gap-2 border-b border-gray-100 p-2" |
| | | > |
| | | <div class="col-span-4"> |
| | | <Select |
| | | v-model:value="item.routeId" |
| | | :allow-clear="true" |
| | | :options="routeOptions" |
| | | placeholder="ä¸é路线" |
| | | show-search |
| | | option-filter-prop="label" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-4"> |
| | | <Select |
| | | v-model:value="item.machineryId" |
| | | :allow-clear="true" |
| | | :options="machineryOptions" |
| | | placeholder="ä¸é设å¤" |
| | | show-search |
| | | option-filter-prop="label" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.billingType" |
| | | :options="BILLING_TYPE_OPTIONS" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-3"> |
| | | <InputNumber |
| | | v-model:value="item.wageRate" |
| | | :addon-after="getRateUnit(item.billingType)" |
| | | :min="0" |
| | | :precision="4" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <DatePicker |
| | | v-model:value="item.effectiveFrom" |
| | | placeholder="ä¸é" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <DatePicker |
| | | v-model:value="item.effectiveTo" |
| | | placeholder="ä¸é" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-1"> |
| | | <InputNumber v-model:value="item.priority" :min="0" size="small" class="w-full" /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Select |
| | | v-model:value="item.status" |
| | | :options="[ |
| | | { label: 'å¯ç¨', value: 0 }, |
| | | { label: 'ç¦ç¨', value: 1 }, |
| | | ]" |
| | | size="small" |
| | | class="w-full" |
| | | /> |
| | | </div> |
| | | <div class="col-span-2"> |
| | | <Input v-model:value="item.remark" placeholder="夿³¨" size="small" /> |
| | | </div> |
| | | <div class="col-span-2 flex items-center justify-center"> |
| | | <Button type="text" danger size="small" @click="removeRow(index)"> |
| | | å é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400" |
| | | > |
| | | {{ loading ? 'å è½½ä¸â¦' : 'æªé
ç½®è§åæ¶ç´æ¥ä½¿ç¨å·¥åºå·¥èµå®é¢' }} |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: "checkFlag", |
| | | label: "æ¯å¦è´¨æ£ç¡®è®¤", |
| | | component: "Switch", |
| | | componentProps: { |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: "backflushFlag", |
| | | label: "æ¯å¦åå²", |
| | | component: "Switch", |
| | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; |
| | | import type { MesQcIpqcApi } from '#/api/mes/qc/ipqc'; |
| | | import type { MesQcIpqcLineApi } from '#/api/mes/qc/ipqc/line'; |
| | | |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesQcTypeEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplatePage } from '#/api/mes/qc/template'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { getWorkOrderProcessListByWorkOrderId } from '#/api/mes/pro/workorder/process'; |
| | | import { ProTaskSelect } from '#/views/mes/pro/task/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | |
| | | /** æå·¥å带åºå¯éå·¥åºï¼å·¥åå·¥èºè·¯çº¿ä¸çå·¥åºï¼ */ |
| | | async function getWorkOrderProcessOptions(workOrderId?: number) { |
| | | if (!workOrderId) { |
| | | return []; |
| | | } |
| | | const list = await getWorkOrderProcessListByWorkOrderId(workOrderId); |
| | | return (list ?? []) |
| | | .filter((item) => item.processId) |
| | | .map((item) => ({ |
| | | label: item.processName || item.processCode || `å·¥åº ${item.processId}`, |
| | | value: item.processId!, |
| | | })); |
| | | } |
| | | |
| | | /** æå·¥å带åºå¯éçåæåï¼åå·¥åºç产åºç©æï¼ */ |
| | | async function getWorkOrderOutputItemOptions(workOrderId?: number) { |
| | | if (!workOrderId) { |
| | | return []; |
| | | } |
| | | const list = await getWorkOrderProcessListByWorkOrderId(workOrderId); |
| | | const options = new Map<number, { label: string; value: number }>(); |
| | | (list ?? []).forEach((process) => { |
| | | const ids = process.outputItemIds?.length |
| | | ? process.outputItemIds |
| | | : process.outputItemId |
| | | ? [process.outputItemId] |
| | | : []; |
| | | const names = (process.outputItemNames || process.outputItemName || '') |
| | | .split(',') |
| | | .map((name) => name.trim()) |
| | | .filter(Boolean); |
| | | ids.forEach((id, index) => { |
| | | if (!options.has(id)) { |
| | | options.set(id, { |
| | | label: |
| | | names[index] || |
| | | (ids.length === 1 ? names[0] : undefined) || |
| | | `åæå ${id}`, |
| | | value: id, |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | return [...options.values()]; |
| | | } |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹è¡¨åç交äºé©å */ |
| | | export interface IpqcFormHooks { |
| | | /** å·¥ä½åæ´ï¼å¸¦åºå·¥ä½æå±å·¥åºï¼ç¨äºæå·¥åºè§£æé»è®¤è´¨æ£æ¹æ¡ */ |
| | | onWorkstationChange?: ( |
| | | item: MesMdWorkstationApi.Workstation | undefined, |
| | | ) => void; |
| | | /** æ£éªå·¥åºåæ´ï¼æå·¥åºéæ°è§£æé»è®¤è´¨æ£æ¹æ¡ä¸å¯å¡«åæ£éªé¡¹ */ |
| | | onProcessChange?: (processId?: number) => void; |
| | | } |
| | | |
| | | /** æä¸åæ ¼åæ°éåæ¥ä¸ºå·¥åº + æåº + å
¶ä»åºåä¹å */ |
| | | function syncUnqualified(formApi?: VbenFormApi) { |
| | |
| | | })(); |
| | | } |
| | | |
| | | /** è¿ç¨æ£éªå¯éçè´¨æ£æ¹æ¡ï¼ä»
å¯ç¨ä¸çè¿ç¨æ£éªæ¹æ¡ */ |
| | | function getIpqcTemplateQuery() { |
| | | return { |
| | | pageNo: 1, |
| | | pageSize: 100, |
| | | type: MesQcTypeEnum.IPQC, |
| | | status: 0, |
| | | }; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | formApi?: VbenFormApi, |
| | | hooks?: IpqcFormHooks, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | // 订ååæ´æ¶æ¸
空çäº§ä»»å¡ |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | // 订ååæ´æ¶æ¸
空ç产任å¡ãå·¥åºä¸åæå |
| | | onChange: () => { |
| | | void formApi?.setFieldValue('taskId', undefined); |
| | | void formApi?.setFieldValue('processId', undefined); |
| | | void formApi?.setFieldValue('itemId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©ç产订å', |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | onChange: () => { |
| | | void formApi?.setFieldValue('taskId', undefined); |
| | | void formApi?.setFieldValue('processId', undefined); |
| | | void formApi?.setFieldValue('itemId', undefined); |
| | | }, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'processId', |
| | | label: 'æ£éªå·¥åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: () => getWorkOrderProcessOptions(), |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: '请å
éæ©ç产订å', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | onChange: (processId?: number) => |
| | | hooks?.onProcessChange?.(processId), |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId', 'workOrderId'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | api: () => getWorkOrderProcessOptions(values.workOrderId), |
| | | disabled: !!values.sourceDocId || !values.workOrderId, |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: values.workOrderId |
| | | ? 'è¯·éæ©æ¬å·¥åçæ£éªå·¥åº' |
| | | : '请å
éæ©ç产订å', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | onChange: (processId?: number) => |
| | | hooks?.onProcessChange?.(processId), |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemId', |
| | | label: 'åæå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: () => getWorkOrderOutputItemOptions(), |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: '请å
éæ©ç产订å', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId', 'workOrderId'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | api: () => getWorkOrderOutputItemOptions(values.workOrderId), |
| | | disabled: !!values.sourceDocId || !values.workOrderId, |
| | | labelField: 'label', |
| | | valueField: 'value', |
| | | placeholder: values.workOrderId |
| | | ? 'è¯·éæ©è¦æ£éªçåæåï¼å·¥åå·¥åºäº§åºï¼' |
| | | : '请å
éæ©ç产订å', |
| | | showSearch: true, |
| | | filterOption: true, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'reelNo', |
| | | label: 'åæåçå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¬æ¬¡æ£éªçåæåçå·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | label: 'å·¥ä½', |
| | | component: markRaw(MdWorkstationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å·¥ä½', |
| | | placeholder: 'å¯éï¼ä¸å¡«åææ£éªå·¥åºè®°å½', |
| | | onChange: (item?: MesMdWorkstationApi.Workstation) => |
| | | hooks?.onWorkstationChange?.(item), |
| | | }, |
| | | rules: 'selectRequired', |
| | | help: 'å·¥ä½ä»
ç¨äºè¿½æº¯ï¼ç°åºæ²¡æåºå®å·¥ä½æ¶å¯ä¸å¡«ï¼ç´æ¥éæ©æ£éªå·¥åºã', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: 'è¯·éæ©å·¥ä½', |
| | | placeholder: 'å¯éï¼ä¸å¡«åææ£éªå·¥åºè®°å½', |
| | | onChange: (item?: MesMdWorkstationApi.Workstation) => |
| | | hooks?.onWorkstationChange?.(item), |
| | | }), |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'templateId', |
| | | label: 'è´¨æ£æ¹æ¡', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: () => getTemplatePage(getIpqcTemplateQuery()), |
| | | labelField: 'name', |
| | | placeholder: 'ç空åæå·¥åºé»è®¤ç»å®æ¹æ¡å¸¦åº', |
| | | resultField: 'list', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | valueField: 'id', |
| | | }, |
| | | help: 'ç空ä¿åæ¶æå·¥ä½æå±å·¥åºçé»è®¤ç»å®æ¹æ¡çææ£éªé¡¹ï¼æå·¥éæ©å以æéæ¹æ¡ä¸ºåã', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | api: () => getTemplatePage(getIpqcTemplateQuery()), |
| | | disabled: !!values.sourceDocId, |
| | | labelField: 'name', |
| | | placeholder: 'ç空åæå·¥åºé»è®¤ç»å®æ¹æ¡å¸¦åº', |
| | | resultField: 'list', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | valueField: 'id', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'checkQuantity', |
| | | label: 'æ£æµæ°é', |
| | | component: 'InputNumber', |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; |
| | | import type { MesQcIndicatorResultApi } from '#/api/mes/qc/indicatorresult'; |
| | | import type { MesQcIpqcApi } from '#/api/mes/qc/ipqc'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { confirm, useVbenModal } from '@vben/common-ui'; |
| | | import { MesQcStatusEnum } from '@vben/constants'; |
| | | import { |
| | | MesQcResultValueType, |
| | | MesQcStatusEnum, |
| | | MesQcTypeEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Button, message, Tabs } from 'ant-design-vue'; |
| | | import { |
| | | Alert, |
| | | Button, |
| | | Empty, |
| | | Input, |
| | | InputNumber, |
| | | message, |
| | | Popconfirm, |
| | | Select, |
| | | Table, |
| | | Tabs, |
| | | Upload, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | import { uploadFile, type SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createIpqc, finishIpqc, getIpqc, updateIpqc } from '#/api/mes/qc/ipqc'; |
| | | import { |
| | | createIndicatorResult, |
| | | getIndicatorResultDetail, |
| | | getIndicatorResultPage, |
| | | updateIndicatorResult, |
| | | } from '#/api/mes/qc/indicatorresult'; |
| | | import { |
| | | createIpqc, |
| | | finishIpqc, |
| | | getIpqc, |
| | | getIpqcWageFieldList, |
| | | getTemplateByProcess, |
| | | saveIpqcWageFieldValues, |
| | | updateIpqc, |
| | | } from '#/api/mes/qc/ipqc'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import QcAttachments from '../../components/qc-attachments.vue'; |
| | |
| | | const subTabsName = ref('attachments'); |
| | | const originalSnapshot = ref(''); // 表ååå§æ°æ®å¿«ç
§ï¼ç¨äº finish æ¶è·³è¿æªåæ´çä¿åè¯·æ± |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | /** 工使å±å·¥åºçé»è®¤è´¨æ£æ¹æ¡åæ£æµææ ï¼éå·¥ä½åèªå¨å¸¦åºï¼ */ |
| | | const resolvedScheme = ref<MesQcIpqcApi.TemplateResolve>(); |
| | | const resolvingScheme = ref(false); |
| | | const showSchemeCard = computed( |
| | | () => resolvingScheme.value || resolvedScheme.value?.processId !== undefined, |
| | | ); |
| | | const canFinish = computed( |
| | | () => |
| | | formType.value === 'update' && |
| | |
| | | ? $t('ui.actionTitle.edit', ['è¿ç¨æ£éªå']) |
| | | : $t('ui.actionTitle.create', ['è¿ç¨æ£éªå']); |
| | | }); |
| | | |
| | | /** æ¸
空已解æçæ¹æ¡ï¼å
³éæéå¼å¼¹çªæ¶é¿å
æ®çï¼ */ |
| | | function resetResolvedScheme() { |
| | | resolvedScheme.value = undefined; |
| | | } |
| | | |
| | | /** |
| | | * æå·¥åºè§£æé»è®¤è´¨æ£æ¹æ¡ï¼å¹¶ææ¹æ¡åå¡«å°è¡¨å |
| | | * |
| | | * @param processId å·¥åºç¼å· |
| | | * @param force true=以解æç»æè¦ç已鿹æ¡ï¼å·¥ä½åæ´æ¶ï¼ï¼false=ä»
卿ªéæ¹æ¡æ¶åå¡«ï¼æå¼å·²æåæ®æ¶ï¼ |
| | | */ |
| | | async function resolveScheme(processId?: number, force = false) { |
| | | if (!processId) { |
| | | resolvedScheme.value = undefined; |
| | | if (force) { |
| | | await formApi.setFieldValue('templateId', undefined); |
| | | } |
| | | return; |
| | | } |
| | | resolvingScheme.value = true; |
| | | try { |
| | | const resp = await getTemplateByProcess(processId, MesQcTypeEnum.IPQC); |
| | | resolvedScheme.value = resp; |
| | | const current = await formApi.getValues(); |
| | | if (force ? true : !current.templateId) { |
| | | await formApi.setFieldValue('templateId', resp?.templateId); |
| | | } |
| | | } catch { |
| | | resolvedScheme.value = undefined; |
| | | } finally { |
| | | resolvingScheme.value = false; |
| | | } |
| | | } |
| | | |
| | | /** å·¥ä½åæ´ï¼å¸¦åºå·¥ä½æå±å·¥åºï¼å¹¶è§£æé»è®¤è´¨æ£æ¹æ¡ä¸æ£æµææ */ |
| | | async function handleWorkstationChange( |
| | | item?: MesMdWorkstationApi.Workstation, |
| | | ) { |
| | | if (!formData.value?.id) { |
| | | // æ°å»ºåæ®æ¢å·¥åºï¼ä¸ä¸æ¬¡çæ£æµå¼ä¸åéç¨ |
| | | indicatorResultId.value = undefined; |
| | | } |
| | | if (item?.processId) { |
| | | await formApi.setFieldValue('processId', item.processId); |
| | | } |
| | | await handleProcessChange(item?.processId); |
| | | } |
| | | |
| | | /** æ£éªå·¥åºåæ´ï¼éæ°è§£æé»è®¤è´¨æ£æ¹æ¡ä¸æ£éªé¡¹ */ |
| | | async function handleProcessChange(processId?: number) { |
| | | if (!formData.value?.id) { |
| | | indicatorResultId.value = undefined; |
| | | } |
| | | await resolveScheme(processId, true); |
| | | buildRowsFromScheme(); |
| | | // å·¥åºåäºï¼ææ°å·¥åºéæ°å¸¦åºå·¥èµå段 |
| | | await loadWageFields(processId, formData.value?.id); |
| | | } |
| | | |
| | | /** ä»å·¥åºæ¨¡æ¿ä¸è¿½å ä¸ä¸ªå°æªä½¿ç¨çæ£éªé¡¹ */ |
| | | function handleAddIndicator() { |
| | | const used = new Set(indicatorRows.value.map((row) => row.indicatorId)); |
| | | const next = (resolvedScheme.value?.indicators ?? []).find( |
| | | (indicator) => !used.has(indicator.indicatorId), |
| | | ); |
| | | if (!next) { |
| | | message.warning('该工åºçæ£éªé¡¹å·²å
¨é¨æ·»å ï¼æ²¡æå¯æ°å¢çæ£éªé¡¹'); |
| | | return; |
| | | } |
| | | indicatorRows.value.push({ |
| | | indicatorId: next.indicatorId, |
| | | indicatorName: next.indicatorName, |
| | | valueType: next.resultType, |
| | | valueSpecification: next.resultSpecification, |
| | | standardValue: next.standardValue, |
| | | unitText: next.unitText, |
| | | thresholdMin: next.thresholdMin, |
| | | thresholdMax: next.thresholdMax, |
| | | blobIds: [], |
| | | files: [], |
| | | attachmentTouched: false, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¸è¡æ£éªé¡¹ï¼è³å°ä¿çä¸è¡ï¼ */ |
| | | function handleRemoveIndicator(index: number) { |
| | | if (indicatorRows.value.length <= 1) { |
| | | message.warning('è³å°ä¿çä¸è¡æ£éªé¡¹'); |
| | | return; |
| | | } |
| | | indicatorRows.value.splice(index, 1); |
| | | } |
| | | |
| | | /** è¯»åææ¡æ£éªè¡å·²ä¸ä¼ çéä»¶ */ |
| | | async function loadRowAttachments(row: IndicatorValueRow) { |
| | | if (!row.detailId) { |
| | | return; |
| | | } |
| | | const files = await requestClient.get<SystemStorageApi.StorageBlob[]>( |
| | | '/mes/qc/attachment/ipqc-detail/list', |
| | | { params: { detailId: row.detailId } }, |
| | | ); |
| | | row.files = files ?? []; |
| | | row.blobIds = row.files.map((file) => file.id); |
| | | row.attachmentTouched = false; |
| | | } |
| | | |
| | | /** ä¸ä¼ æè¡æ£éªéä»¶ï¼ä»
ä¼ éä»¶ï¼ä¸å¡«æ£æµå¼ï¼ */ |
| | | async function handleRowUpload(row: IndicatorValueRow, file: File) { |
| | | const uploaded = await uploadFile([file]); |
| | | const blob = uploaded?.[0]; |
| | | if (!blob?.id) { |
| | | throw new Error('ä¸ä¼ ç»æç¼ºå°æä»¶ç¼å·'); |
| | | } |
| | | row.files = [...(row.files ?? []), blob]; |
| | | row.blobIds = [...(row.blobIds ?? []), blob.id]; |
| | | row.attachmentTouched = true; |
| | | message.success('éä»¶å·²ä¸ä¼ ï¼ä¿åæ£éªååçæ'); |
| | | return Upload.LIST_IGNORE; |
| | | } |
| | | |
| | | /** ç§»é¤æè¡éä»¶ï¼å·²ä¿åçç«å³è§£ç»ï¼æªä¿åçåªä»æ¬å°ç§»é¤ */ |
| | | async function handleRowRemoveFile(row: IndicatorValueRow, blobId: number) { |
| | | if (row.detailId) { |
| | | await requestClient.delete('/mes/qc/attachment/ipqc-detail/remove', { |
| | | params: { detailId: row.detailId, blobId }, |
| | | }); |
| | | } |
| | | row.files = (row.files ?? []).filter((file) => file.id !== blobId); |
| | | row.blobIds = (row.blobIds ?? []).filter((id) => id !== blobId); |
| | | row.attachmentTouched = true; |
| | | message.success('é件已移é¤'); |
| | | } |
| | | |
| | | /** ä¿ååè¡éä»¶ç»å®ï¼é卿£éªç»ææç»çæåè°ç¨ï¼ */ |
| | | async function saveRowAttachments() { |
| | | for (const row of indicatorRows.value) { |
| | | if (!row.detailId || !row.attachmentTouched) { |
| | | continue; |
| | | } |
| | | await requestClient.post('/mes/qc/attachment/ipqc-detail/save', { |
| | | detailId: row.detailId, |
| | | blobIds: row.blobIds ?? [], |
| | | }); |
| | | row.attachmentTouched = false; |
| | | } |
| | | } |
| | | |
| | | /** å·¥èµå段ï¼å·¥åºèªå®ä¹å段ï¼ç±è´¨æ£å¡«åååä¸å·¥èµè®¡ç®ï¼ */ |
| | | interface WageFieldRow { |
| | | fieldId?: number; |
| | | fieldCode?: string; |
| | | fieldName?: string; |
| | | fieldType?: number; |
| | | unit?: string; |
| | | calcRole?: number; |
| | | unitPrice?: number; |
| | | defaultValue?: string; |
| | | value?: string; |
| | | valueNumber?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å·¥èµå段类åï¼1-æ°å¼ 2-ææ¬ 3-䏿 4-å¸å° */ |
| | | const WAGE_FIELD_TYPE_NUMBER = 1; |
| | | const WAGE_FIELD_TYPE_BOOLEAN = 4; |
| | | /** å·¥èµå段计ç®è§è²ææ¡ */ |
| | | const WAGE_CALC_ROLE_LABEL: Record<number, string> = { |
| | | 1: 'ç³»æ°', |
| | | 2: 'å åéé¢', |
| | | 3: '计价æ°é', |
| | | 4: 'ä»
è®°å½', |
| | | }; |
| | | |
| | | const wageFields = ref<WageFieldRow[]>([]); |
| | | |
| | | /** æå·¥åºå¸¦åºå·¥èµå段ï¼å¹¶å填该æ£éªå已填åçå¼ */ |
| | | async function loadWageFields(processId?: number, ipqcId?: number) { |
| | | if (!processId && !ipqcId) { |
| | | wageFields.value = []; |
| | | return; |
| | | } |
| | | const list = await getIpqcWageFieldList({ processId, ipqcId }); |
| | | wageFields.value = (list ?? []).map((field) => ({ |
| | | ...field, |
| | | valueNumber: |
| | | field.fieldType === WAGE_FIELD_TYPE_NUMBER && |
| | | field.value !== undefined && |
| | | field.value !== null && |
| | | String(field.value).trim() !== '' && |
| | | Number.isFinite(Number(field.value)) |
| | | ? Number(field.value) |
| | | : undefined, |
| | | })); |
| | | } |
| | | |
| | | /** ä¿åè´¨æ£å¡«åçå·¥èµåæ®µå¼ */ |
| | | async function saveWageFieldValues(ipqcId: number) { |
| | | if (wageFields.value.length === 0) { |
| | | return; |
| | | } |
| | | await saveIpqcWageFieldValues( |
| | | ipqcId, |
| | | wageFields.value |
| | | .filter((field) => field.fieldId) |
| | | .map((field) => ({ |
| | | fieldId: field.fieldId!, |
| | | value: |
| | | field.fieldType === WAGE_FIELD_TYPE_NUMBER |
| | | ? field.valueNumber === undefined || field.valueNumber === null |
| | | ? '' |
| | | : String(field.valueNumber) |
| | | : field.value || '', |
| | | remark: field.remark, |
| | | })), |
| | | ); |
| | | } |
| | | |
| | | /** 详æ
页工èµå段å¼å±ç¤º */ |
| | | function getWageFieldDisplay(row: WageFieldRow) { |
| | | if (row.fieldType === WAGE_FIELD_TYPE_BOOLEAN && row.value) { |
| | | return row.value === '1' ? 'æ¯' : 'å¦'; |
| | | } |
| | | return row.value || 'æªå¡«å'; |
| | | } |
| | | |
| | | /** æ£éªé¡¹å¡«åè¡ï¼ç±å·¥åºé»è®¤æ¹æ¡çæ£æµææ çæï¼ */ |
| | | interface IndicatorValueRow { |
| | | detailId?: number; |
| | | indicatorId?: number; |
| | | indicatorName?: string; |
| | | valueType?: number; |
| | | valueSpecification?: string; |
| | | standardValue?: number; |
| | | unitText?: string; |
| | | thresholdMin?: number; |
| | | thresholdMax?: number; |
| | | value?: string; |
| | | valueNumber?: number; |
| | | remark?: string; |
| | | /** æ¬æ¬¡æ°å¢ãå°æªç»å®å°æ£éªè¡æç»çéä»¶ blobId */ |
| | | blobIds?: number[]; |
| | | /** æ£éªè¡éä»¶ï¼å·²ä¿åææ¬æ¬¡ä¸ä¼ çå±ç¤ºæ°æ®ï¼ */ |
| | | files?: SystemStorageApi.StorageBlob[]; |
| | | /** éä»¶æ¯å¦æåæ´ï¼ä¿åæ¶ééæ°ç»å® */ |
| | | attachmentTouched?: boolean; |
| | | } |
| | | |
| | | /** æ£éªé¡¹å¡«åï¼æ£æµå¼ï¼ï¼éæ£éªåä¸èµ·ä¿åä¸ºãæ£éªç»æã */ |
| | | const indicatorRows = ref<IndicatorValueRow[]>([]); |
| | | /** å·²ä¿åçæ£éªç»æç¼å·ï¼ç©º=å°æªçæ */ |
| | | const indicatorResultId = ref<number>(); |
| | | const indicatorResultCode = ref('é»è®¤'); |
| | | |
| | | /** æ°å¼ç±»æ£æµé¡¹ï¼æµ®ç¹/æ´æ°ï¼èµ°æ°åè¾å
¥ */ |
| | | function isNumericType(valueType?: number) { |
| | | return ( |
| | | valueType === MesQcResultValueType.FLOAT || |
| | | valueType === MesQcResultValueType.INTEGER |
| | | ); |
| | | } |
| | | |
| | | /** åå
¸ç±»æ£æµé¡¹çé项ï¼valueSpecification 为系ç»åå
¸ç±»ååï¼ */ |
| | | function getDictValueOptions(dictType?: string) { |
| | | return dictType |
| | | ? getDictOptions(dictType, 'string').map(({ label, value }) => ({ |
| | | label, |
| | | value: String(value), |
| | | })) |
| | | : []; |
| | | } |
| | | |
| | | /** æå·¥åºé»è®¤æ¹æ¡çæéè¦å¡«åçæ£éªé¡¹å段ï¼ä¿çå·²å½å
¥çå¼ */ |
| | | function buildRowsFromScheme() { |
| | | const previous = new Map( |
| | | indicatorRows.value.map((row) => [row.indicatorId, row]), |
| | | ); |
| | | indicatorRows.value = (resolvedScheme.value?.indicators ?? []).map( |
| | | (indicator) => { |
| | | const old = previous.get(indicator.indicatorId); |
| | | return { |
| | | detailId: old?.detailId, |
| | | indicatorId: indicator.indicatorId, |
| | | indicatorName: indicator.indicatorName, |
| | | valueType: indicator.resultType, |
| | | valueSpecification: indicator.resultSpecification, |
| | | standardValue: indicator.standardValue, |
| | | unitText: indicator.unitText, |
| | | thresholdMin: indicator.thresholdMin, |
| | | thresholdMax: indicator.thresholdMax, |
| | | value: old?.value, |
| | | valueNumber: old?.valueNumber, |
| | | remark: old?.remark, |
| | | blobIds: old?.blobIds ?? [], |
| | | files: old?.files ?? [], |
| | | attachmentTouched: old?.attachmentTouched, |
| | | }; |
| | | }, |
| | | ); |
| | | } |
| | | |
| | | /** å è½½æ£éªåå·²ä¿åçæ£æµå¼ */ |
| | | async function loadIndicatorValues(ipqcId: number) { |
| | | indicatorRows.value = []; |
| | | indicatorResultId.value = undefined; |
| | | indicatorResultCode.value = 'é»è®¤'; |
| | | const page = await getIndicatorResultPage({ |
| | | pageNo: 1, |
| | | pageSize: 20, |
| | | qcId: ipqcId, |
| | | qcType: MesQcTypeEnum.IPQC, |
| | | }); |
| | | const target = |
| | | (page.list ?? []).find( |
| | | (item) => !item.code || item.code === indicatorResultCode.value, |
| | | ) ?? page.list?.[0]; |
| | | if (!target?.id) { |
| | | return; |
| | | } |
| | | const detail = await getIndicatorResultDetail( |
| | | ipqcId, |
| | | MesQcTypeEnum.IPQC, |
| | | target.id, |
| | | ); |
| | | indicatorResultId.value = detail.id; |
| | | indicatorResultCode.value = detail.code || 'é»è®¤'; |
| | | indicatorRows.value = (detail.items ?? []).map((item) => ({ |
| | | detailId: item.id, |
| | | indicatorId: item.indicatorId, |
| | | indicatorName: item.indicatorName, |
| | | valueType: item.valueType, |
| | | valueSpecification: item.valueSpecification, |
| | | value: item.value, |
| | | valueNumber: |
| | | isNumericType(item.valueType) && |
| | | item.value !== undefined && |
| | | item.value !== null && |
| | | String(item.value).trim() !== '' && |
| | | Number.isFinite(Number(item.value)) |
| | | ? Number(item.value) |
| | | : undefined, |
| | | blobIds: [], |
| | | files: [], |
| | | attachmentTouched: false, |
| | | })); |
| | | // éè¡å 载已ä¸ä¼ çæ£éªéä»¶ |
| | | await Promise.all(indicatorRows.value.map((row) => loadRowAttachments(row))); |
| | | } |
| | | |
| | | /** ç¨å·¥åºæ¹æ¡è¡¥é½æ£éªè¡çåç§°ãæ åå¼ä¸åä½ï¼ä¸æ¹åå·²ä¿åçè¡é¡ºåºåå¢å ç»æï¼ */ |
| | | function enrichRowsFromScheme() { |
| | | const indicatorMap = new Map( |
| | | (resolvedScheme.value?.indicators ?? []).map((indicator) => [ |
| | | indicator.indicatorId, |
| | | indicator, |
| | | ]), |
| | | ); |
| | | indicatorRows.value.forEach((row) => { |
| | | const indicator = row.indicatorId |
| | | ? indicatorMap.get(row.indicatorId) |
| | | : undefined; |
| | | if (!indicator) { |
| | | return; |
| | | } |
| | | row.indicatorName ??= indicator.indicatorName; |
| | | row.valueType ??= indicator.resultType; |
| | | row.valueSpecification ??= indicator.resultSpecification; |
| | | row.standardValue ??= indicator.standardValue; |
| | | row.unitText ??= indicator.unitText; |
| | | row.thresholdMin ??= indicator.thresholdMin; |
| | | row.thresholdMax ??= indicator.thresholdMax; |
| | | }); |
| | | } |
| | | |
| | | /** æ¶éå½åæ£éªè¡ãè´¨æ£åªä¼ éä»¶ï¼æ£æµå¼ç»ä¸ç空ï¼ç±éä»¶æ¿è½½æ£æµç»æã */ |
| | | function collectIndicatorItems() { |
| | | return indicatorRows.value |
| | | .filter((row) => row.indicatorId) |
| | | .map((row) => ({ |
| | | id: row.detailId, |
| | | indicatorId: row.indicatorId!, |
| | | value: '', |
| | | remark: row.remark, |
| | | })); |
| | | } |
| | | |
| | | /** ä¿åæ£éªè¡ï¼å«æå·¥å¢å çè¡ï¼ï¼å¹¶æçæçæç»ç¼å·åå¡«å°è¡ä¸ */ |
| | | async function saveIndicatorValues(ipqcId: number) { |
| | | const items = collectIndicatorItems(); |
| | | if (items.length === 0) { |
| | | return; |
| | | } |
| | | const payload: MesQcIndicatorResultApi.IndicatorResult = { |
| | | code: indicatorResultCode.value || 'é»è®¤', |
| | | qcId: ipqcId, |
| | | qcType: MesQcTypeEnum.IPQC, |
| | | items, |
| | | }; |
| | | if (indicatorResultId.value) { |
| | | payload.id = indicatorResultId.value; |
| | | await updateIndicatorResult(payload); |
| | | } else { |
| | | indicatorResultId.value = (await createIndicatorResult( |
| | | payload, |
| | | )) as number; |
| | | } |
| | | // ä¿ååæææ åå¡«æç»ç¼å·ï¼ä¾æ¯è¡éä»¶ç»å®ä½¿ç¨ |
| | | const detail = await getIndicatorResultDetail( |
| | | ipqcId, |
| | | MesQcTypeEnum.IPQC, |
| | | indicatorResultId.value, |
| | | ); |
| | | const detailMap = new Map( |
| | | (detail.items ?? []).map((item) => [item.indicatorId, item.id]), |
| | | ); |
| | | indicatorRows.value.forEach((row) => { |
| | | const detailId = row.indicatorId |
| | | ? detailMap.get(row.indicatorId) |
| | | : undefined; |
| | | if (detailId) { |
| | | row.detailId = detailId; |
| | | } |
| | | }); |
| | | await saveRowAttachments(); |
| | | } |
| | | |
| | | /** åå
¸ç±»æ£æµå¼çå±ç¤ºææ¡ */ |
| | | function getIndicatorDisplay(row: IndicatorValueRow) { |
| | | if (!row.value) { |
| | | return 'æªå¡«å'; |
| | | } |
| | | if (row.valueType === MesQcResultValueType.DICT) { |
| | | return ( |
| | | getDictValueOptions(row.valueSpecification).find( |
| | | (option) => option.value === String(row.value), |
| | | )?.label ?? row.value |
| | | ); |
| | | } |
| | | return row.value; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | |
| | | await formApi.setFieldValue('status', formData.value.status); |
| | | formType.value = 'update'; |
| | | } |
| | | // æ£æµå¼ä¸å·¥èµåæ®µéæ£éªåä¿å |
| | | await saveIndicatorValues(formData.value.id!); |
| | | await saveWageFieldValues(formData.value.id!); |
| | | // å·æ°å¿«ç
§ï¼å·²å¸¦ä¸ id / statusï¼ï¼ä¸æ¬¡ finish ç¨äºå¤ææ¯å¦éè¦åä¿å |
| | | originalSnapshot.value = JSON.stringify(await formApi.getValues()); |
| | | return true; |
| | |
| | | formData.value = { ...formData.value, ...data }; |
| | | originalSnapshot.value = current; |
| | | } |
| | | // æ£æµå¼/å·¥èµå段å¯è½åªæ¹äºæç»ï¼ä¸ä½ç°å¨ä¸»è¡¨åå¿«ç
§é |
| | | await saveIndicatorValues(id); |
| | | await saveWageFieldValues(id); |
| | | await finishIpqc(id); |
| | | message.success('宿æå'); |
| | | await modalApi.close(); |
| | |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | originalSnapshot.value = ''; |
| | | resetResolvedScheme(); |
| | | indicatorRows.value = []; |
| | | indicatorResultId.value = undefined; |
| | | wageFields.value = []; |
| | | return; |
| | | } |
| | | subTabsName.value = 'attachments'; |
| | | resetResolvedScheme(); |
| | | indicatorRows.value = []; |
| | | indicatorResultId.value = undefined; |
| | | wageFields.value = []; |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | |
| | | prefill?: MesQcIpqcApi.Ipqc; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, { |
| | | onWorkstationChange: handleWorkstationChange, |
| | | onProcessChange: handleProcessChange, |
| | | }), |
| | | }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | modalApi.setState({ showConfirmButton: formType.value !== 'detail' }); |
| | | if (data?.id) { |
| | |
| | | formData.value = await getIpqc(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | // ç¼è¾/详æ
ï¼æå·²ä¿åçå·¥åºå¸¦åºé»è®¤è´¨æ£æ¹æ¡ä¸æ£æµææ |
| | | await resolveScheme(formData.value.processId); |
| | | // å·²ä¿åçæ£æµå¼ä¼å
å±ç¤ºï¼åææ¹æ¡è¡¥é½ææ åç§°ãæ åå¼ä¸åä½ |
| | | await loadIndicatorValues(data.id); |
| | | if ((resolvedScheme.value?.indicators?.length ?? 0) > 0) { |
| | | enrichRowsFromScheme(); |
| | | } |
| | | // å·¥èµåæ®µï¼æå·¥åºå¸¦åºï¼å¹¶å填已填åçå¼ |
| | | await loadWageFields(formData.value.processId, data.id); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | formData.value = { ...data.prefill }; |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(data.prefill); |
| | | await resolveScheme(data.prefill.processId); |
| | | buildRowsFromScheme(); |
| | | await loadWageFields(data.prefill.processId); |
| | | } |
| | | originalSnapshot.value = JSON.stringify(await formApi.getValues()); |
| | | }, |
| | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-4" /> |
| | | <div v-if="showSchemeCard" class="mx-4 mt-2"> |
| | | <div class="rounded border border-gray-200 p-3"> |
| | | <div class="mb-2 flex flex-wrap items-center gap-2 text-sm font-medium"> |
| | | è´¨æ£æ¹æ¡ |
| | | <span v-if="resolvedScheme?.templateName" class="text-gray-500"> |
| | | {{ resolvedScheme?.templateCode }} {{ resolvedScheme?.templateName }} |
| | | </span> |
| | | <span v-else-if="resolvingScheme" class="text-xs text-gray-400"> |
| | | æ£å¨æå·¥ä½æå±å·¥åºè§£æâ¦ |
| | | </span> |
| | | </div> |
| | | <Alert |
| | | v-if="!resolvedScheme?.indicators?.length" |
| | | type="warning" |
| | | show-icon |
| | | :message=" |
| | | resolvedScheme?.templateId |
| | | ? '该工åºçé»è®¤è´¨æ£æ¹æ¡æªç»´æ¤æ£æµææ ï¼ä¿ååä¸ä¼çææ£éªé¡¹ã' |
| | | : 'è¯¥å·¥åºæªç»å®é»è®¤è´¨æ£æ¹æ¡ï¼ä¿ååä¸ä¼çææ£éªé¡¹ï¼è¯·å°ãè´¨é管ç-è´¨æ£æ¹æ¡ â éç¨å·¥åºãä¸ç»´æ¤ã' |
| | | " |
| | | /> |
| | | <div class="mt-2 text-xs text-gray-400"> |
| | | é»è®¤æ¹æ¡ç±å·¥ä½æå±å·¥åºèªå¨å¸¦åºï¼å¦éæ´æ¢ï¼å¯å¨ãè´¨æ£æ¹æ¡ã䏿工鿩ï¼ä¿ååææéæ¹æ¡çææ£éªé¡¹ã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="wageFields.length > 0" class="mx-4 mt-3"> |
| | | <div class="rounded border border-gray-200 p-3"> |
| | | <div class="mb-2 flex flex-wrap items-center gap-2 text-sm font-medium"> |
| | | å·¥èµå段 |
| | | <span class="text-xs font-normal text-gray-400"> |
| | | ï¼å·¥åºèªå®ä¹å段ï¼ç±è´¨æ£å¡«åï¼ä¿åååä¸è¯¥å·¥åºæ¥å·¥å·¥èµæ ¸ç®ï¼ |
| | | </span> |
| | | </div> |
| | | <div class="grid grid-cols-1 gap-x-4 gap-y-3 md:grid-cols-2"> |
| | | <div |
| | | v-for="row in wageFields" |
| | | :key="row.fieldId" |
| | | class="flex items-start gap-2" |
| | | > |
| | | <div class="w-40 shrink-0 pt-1 text-xs"> |
| | | <div class="text-gray-700">{{ row.fieldName }}</div> |
| | | <div class="text-gray-400"> |
| | | <span v-if="row.unit">åä½ {{ row.unit }}</span> |
| | | <span v-if="row.calcRole" class="ml-1"> |
| | | · {{ WAGE_CALC_ROLE_LABEL[row.calcRole] }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="flex-1"> |
| | | <div v-if="isDetail" class="pt-1 text-sm"> |
| | | {{ getWageFieldDisplay(row) }} |
| | | </div> |
| | | <InputNumber |
| | | v-else-if="row.fieldType === WAGE_FIELD_TYPE_NUMBER" |
| | | v-model:value="row.valueNumber" |
| | | :precision="4" |
| | | class="!w-full" |
| | | placeholder="请è¾å
¥" |
| | | /> |
| | | <Select |
| | | v-else-if="row.fieldType === WAGE_FIELD_TYPE_BOOLEAN" |
| | | v-model:value="row.value" |
| | | allow-clear |
| | | :options="[ |
| | | { label: 'æ¯', value: '1' }, |
| | | { label: 'å¦', value: '0' }, |
| | | ]" |
| | | placeholder="è¯·éæ©" |
| | | class="w-full" |
| | | /> |
| | | <Input v-else v-model:value="row.value" placeholder="请è¾å
¥" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mt-2 text-xs text-gray-400"> |
| | | æªå¡«åçåæ®µæå·¥åºä¸é
ç½®çåºå®å¼è®¡ç®ï¼å¡«åå鿣éªåä¿åï¼æ¥å·¥å®¡æ¹æ¶ç¨äºéç®å·¥èµã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mx-4 mt-3"> |
| | | <div class="rounded border border-gray-200 p-3"> |
| | | <div class="mb-2 flex flex-wrap items-center gap-2 text-sm font-medium"> |
| | | æ£éªä¿¡æ¯ |
| | | <span class="text-xs font-normal text-gray-400"> |
| | | ï¼å¯æå·¥å¢å æ£éªè¡ï¼è¿ç¨æ£éªåªä¸ä¼ éä»¶ï¼ä¸å¡«åæ£æµå¼ï¼ |
| | | </span> |
| | | <Button |
| | | v-if="!isDetail" |
| | | class="ml-auto" |
| | | size="small" |
| | | type="primary" |
| | | ghost |
| | | @click="handleAddIndicator" |
| | | > |
| | | æ·»å æ£éªé¡¹ |
| | | </Button> |
| | | </div> |
| | | <Table |
| | | v-if="indicatorRows.length > 0" |
| | | :data-source="indicatorRows" |
| | | :pagination="false" |
| | | bordered |
| | | row-key="indicatorId" |
| | | size="small" |
| | | > |
| | | <Table.Column title="æ£éªé¡¹" key="indicatorName" :width="180"> |
| | | <template #default="{ record }"> |
| | | <div class="text-gray-700">{{ record.indicatorName || '-' }}</div> |
| | | <div class="text-xs text-gray-400"> |
| | | <template |
| | | v-if=" |
| | | record.standardValue !== undefined && |
| | | record.standardValue !== null |
| | | " |
| | | > |
| | | æ å {{ record.standardValue }}{{ record.unitText || '' }} |
| | | </template> |
| | | <template v-else-if="record.unitText"> |
| | | åä½ {{ record.unitText }} |
| | | </template> |
| | | </div> |
| | | </template> |
| | | </Table.Column> |
| | | <Table.Column title="æ£éªéä»¶" key="attachments"> |
| | | <template #default="{ record }"> |
| | | <div class="flex flex-col gap-1"> |
| | | <div |
| | | v-for="file in record.files || []" |
| | | :key="file.id" |
| | | class="flex items-center gap-2 text-xs" |
| | | > |
| | | <a |
| | | :href="file.previewURL || file.downloadURL" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | > |
| | | {{ file.name }} |
| | | </a> |
| | | <Popconfirm |
| | | v-if="!isDetail" |
| | | title="确认移é¤è¯¥æ£éªè¡éä»¶ï¼" |
| | | ok-text="ç§»é¤" |
| | | cancel-text="åæ¶" |
| | | @confirm="handleRowRemoveFile(record, file.id)" |
| | | > |
| | | <a class="text-red-500">ç§»é¤</a> |
| | | </Popconfirm> |
| | | </div> |
| | | <Upload |
| | | v-if="!isDetail" |
| | | :show-upload-list="false" |
| | | :before-upload="(file: File) => handleRowUpload(record, file)" |
| | | > |
| | | <Button size="small">ä¸ä¼ éä»¶</Button> |
| | | </Upload> |
| | | <span |
| | | v-if="isDetail && (record.files || []).length === 0" |
| | | class="text-xs text-gray-400" |
| | | > |
| | | æªä¸ä¼ éä»¶ |
| | | </span> |
| | | </div> |
| | | </template> |
| | | </Table.Column> |
| | | <Table.Column |
| | | v-if="!isDetail" |
| | | title="æä½" |
| | | key="actions" |
| | | :width="80" |
| | | > |
| | | <template #default="{ index }"> |
| | | <a class="text-red-500" @click="handleRemoveIndicator(index)"> |
| | | å é¤ |
| | | </a> |
| | | </template> |
| | | </Table.Column> |
| | | <template #emptyText> |
| | | <Empty description="ææ æ£éªé¡¹ï¼è¯·ç¹å»ãæ·»å æ£éªé¡¹ã" /> |
| | | </template> |
| | | </Table> |
| | | <Empty |
| | | v-else |
| | | :description=" |
| | | resolvedScheme?.indicators?.length |
| | | ? 'ææ æ£éªé¡¹ï¼è¯·ç¹å»ãæ·»å æ£éªé¡¹ã' |
| | | : 'è¯¥å·¥åºæªé
ç½®æ£éªæ¹æ¡ï¼æ æ³æ·»å æ£éªé¡¹' |
| | | " |
| | | /> |
| | | </div> |
| | | </div> |
| | | <Tabs |
| | | v-if="formData?.id" |
| | | v-model:active-key="subTabsName" |
| | |
| | | import { DeptTreeSelect } from '#/views/system/dept/components'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | | /** NCR ç¶ææä¸¾ */ |
| | | /** NCR ç¶ææä¸¾ï¼ä¸å端 MesNcrStatusEnum ä¿æä¸è´ï¼ */ |
| | | export const NCR_STATUS = { |
| | | PENDING_REVIEW: 0, |
| | | REVIEWED: 1, |
| | | DISPOSED: 2, |
| | | CLOSED: 3, |
| | | APPROVING: 2, |
| | | REVIEWED: 6, |
| | | DISPOSED: 7, |
| | | CLOSED: 8, |
| | | } as const; |
| | | |
| | | export const NCR_STATUS_OPTIONS = [ |
| | | { label: 'å¾
è¯å®¡', value: 0 }, |
| | | { label: 'å·²è¯å®¡', value: 1 }, |
| | | { label: 'å·²å¤ç½®', value: 2 }, |
| | | { label: 'å·²å
³é', value: 3 }, |
| | | { label: 'å¾
è¯å®¡', value: NCR_STATUS.PENDING_REVIEW }, |
| | | { label: '审æ¹ä¸', value: NCR_STATUS.APPROVING }, |
| | | { label: 'å·²è¯å®¡', value: NCR_STATUS.REVIEWED }, |
| | | { label: 'å·²å¤ç½®', value: NCR_STATUS.DISPOSED }, |
| | | { label: 'å·²å
³é', value: NCR_STATUS.CLOSED }, |
| | | ]; |
| | | |
| | | /** NCR å¤ç½®æ¹å¼ï¼ä¸å端 MesNcrDispositionEnum ä¿æä¸è´ï¼ç¨äºå岿°æ®å±ç¤ºï¼ */ |
| | | export const NCR_DISPOSITION_OPTIONS = [ |
| | | { label: 'éè´§', value: 1 }, |
| | | { label: 'æ¥åº', value: 2 }, |
| | | { label: 'è¿å·¥', value: 3 }, |
| | | { label: 'è®©æ¥æ¾è¡', value: 4 }, |
| | | { label: 'é级使ç¨', value: 5 }, |
| | | { label: 'è¿ä¿®', value: 6 }, |
| | | ]; |
| | | |
| | | /** è´¨é鍿ç»å¤ç½®å¯é项ï¼å端ä»
æ¥åè¿åç§ï¼ */ |
| | | export const NCR_DISPOSAL_OPTIONS = [ |
| | | { label: 'æ¥åº', value: 2 }, |
| | | { label: 'è¿å·¥', value: 3 }, |
| | | { label: 'è®©æ¥æ¾è¡', value: 4 }, |
| | | { label: 'è¿ä¿®', value: 6 }, |
| | | ]; |
| | | |
| | | /** ç¶æå¼è½¬æ¾ç¤ºææ¡ */ |
| | | export function getNcrStatusLabel(status?: number): string { |
| | | return NCR_STATUS_OPTIONS.find((o) => o.value === status)?.label || ''; |
| | | } |
| | | |
| | | /** ç¶æå¼è½¬ Tag é¢è² */ |
| | | export function getNcrStatusColor(status?: number): string { |
| | | switch (status) { |
| | | case NCR_STATUS.APPROVING: { |
| | | return 'processing'; |
| | | } |
| | | case NCR_STATUS.REVIEWED: { |
| | | return 'blue'; |
| | | } |
| | | case NCR_STATUS.DISPOSED: { |
| | | return 'success'; |
| | | } |
| | | case NCR_STATUS.CLOSED: { |
| | | return 'default'; |
| | | } |
| | | default: { |
| | | return 'warning'; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** å¤ç½®æ¹å¼å¼è½¬æ¾ç¤ºææ¡ */ |
| | | export function getNcrDispositionLabel(disposition?: number): string { |
| | | return ( |
| | | NCR_DISPOSITION_OPTIONS.find((o) => o.value === disposition)?.label || '' |
| | | ); |
| | | } |
| | | |
| | | /** æ£éªç±»åæä¸¾ */ |
| | | export const QC_TYPE_OPTIONS = [ |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_NCR_DISPOSITION, 'number'), |
| | | options: NCR_DISPOSAL_OPTIONS, |
| | | placeholder: 'è¯·éæ©å¤ç½®æ¹å¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'disposition', |
| | | label: 'å¤ç½®æ¹å¼', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: NCR_DISPOSAL_OPTIONS, |
| | | placeholder: 'è¯·éæ©å¤ç½®æ¹å¼ï¼è®©æ¥æ¾è¡/è¿ä¿®/è¿å·¥/æ¥åºï¼', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'handlerUserId', |
| | |
| | | field: 'disposition', |
| | | title: 'å¤ç½®æ¹å¼', |
| | | width: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_NCR_DISPOSITION }, |
| | | }, |
| | | slots: { default: 'disposition' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_NCR_STATUS }, |
| | | }, |
| | | width: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 220, |
| | | width: 280, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesQcNcrApi } from '#/api/mes/qc/ncr'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | |
| | | disposeNcr, |
| | | exportNcr, |
| | | getNcrPage, |
| | | reviewNcr, |
| | | submitNcrApproval, |
| | | } from '#/api/mes/qc/ncr'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | getNcrDispositionLabel, |
| | | getNcrStatusColor, |
| | | getNcrStatusLabel, |
| | | NCR_STATUS, |
| | | QC_TYPE_OPTIONS, |
| | | useCloseFormSchema, |
| | | useDisposeFormSchema, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | useReviewFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'NcrIndex' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** è¯å®¡å¼¹çª */ |
| | | const [ReviewForm, reviewFormApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useReviewFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | const [ReviewModal, reviewModalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await reviewFormApi.validate(); |
| | | if (!valid) return; |
| | | reviewModalApi.lock(); |
| | | try { |
| | | await reviewNcr( |
| | | (await reviewFormApi.getValues()) as { |
| | | defectLevel?: number; |
| | | disposition: number; |
| | | id: number; |
| | | responsibleDeptId?: number; |
| | | reviewOpinion?: string; |
| | | }, |
| | | ); |
| | | await reviewModalApi.close(); |
| | | message.success('è¯å®¡æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | reviewModalApi.unlock(); |
| | | } |
| | | }, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | const data = reviewModalApi.getData<MesQcNcrApi.Ncr>(); |
| | | reviewFormApi.setValues({ id: data?.id }); |
| | | }, |
| | | }); |
| | | |
| | | /** å¤ç½®å¼¹çª */ |
| | |
| | | try { |
| | | await disposeNcr( |
| | | (await disposeFormApi.getValues()) as { |
| | | disposition: number; |
| | | handleResult?: string; |
| | | handlerUserId?: number; |
| | | id: number; |
| | |
| | | disposeModalApi.unlock(); |
| | | } |
| | | }, |
| | | onOpenChange(isOpen: boolean) { |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) return; |
| | | // æ¯æ¬¡æå¼éç½®ï¼ä¿è¯å¤ç½®æ¹å¼é»è®¤ä¸éä¸ï¼å¿
é¡»ç±è´¨éé¨éæ°éæ©ï¼ |
| | | await disposeFormApi.resetForm(); |
| | | const data = disposeModalApi.getData<MesQcNcrApi.Ncr>(); |
| | | disposeFormApi.setValues({ id: data?.id }); |
| | | }, |
| | |
| | | } |
| | | } |
| | | |
| | | /** è¯å®¡ */ |
| | | function handleReview(row: MesQcNcrApi.Ncr) { |
| | | reviewModalApi.setData(row).open(); |
| | | /** æäº¤å®¡æ¹ */ |
| | | async function handleSubmitApproval(row: MesQcNcrApi.Ncr) { |
| | | const hideLoading = message.loading({ |
| | | content: `æ£å¨æäº¤ ${row.code} 审æ¹...`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitNcrApproval(row.id!); |
| | | message.success( |
| | | `ä¸åæ ¼åå¤çå ${row.code} å·²æäº¤å®¡æ¹ï¼å¯å¨ãæ¥ç审æ¹ãä¸è·è¸ªè¿åº¦`, |
| | | ); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è®°å½ï¼å¤ç¨ BPM æµç¨å®ä¾è¯¦æ
é¡µçæ¶é´çº¿ï¼ */ |
| | | function handleViewApproval(row: MesQcNcrApi.Ncr) { |
| | | if (!row.processInstanceId) { |
| | | message.warning( |
| | | `ä¸åæ ¼åå¤çå ${row.code} æ²¡ææµç¨å®ä¾ï¼å¯è½å¼å¯äºå
¨å±å
审æ¹ï¼æå°æªæäº¤å®¡æ¹`, |
| | | ); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId, viewOnly: '1' }, |
| | | }); |
| | | } |
| | | |
| | | /** å¤ç½® */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ReviewModal title="è¯å®¡ä¸åæ ¼åå¤çå" class="w-2/5"> |
| | | <ReviewForm class="mx-4" /> |
| | | </ReviewModal> |
| | | <DisposeModal title="å¤ç½®ä¸åæ ¼åå¤çå" class="w-2/5"> |
| | | <DisposeForm class="mx-4" /> |
| | | </DisposeModal> |
| | |
| | | {{ getQcTypeLabel(row.qcType) }} |
| | | </Tag> |
| | | </template> |
| | | <template #disposition="{ row }"> |
| | | <span v-if="row.disposition !== undefined && row.disposition !== null"> |
| | | {{ getNcrDispositionLabel(row.disposition) }} |
| | | </span> |
| | | <span v-else class="text-gray-400">--</span> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag :color="getNcrStatusColor(row.status)"> |
| | | {{ getNcrStatusLabel(row.status) }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | v-if="row.status === NCR_STATUS.PENDING_REVIEW" |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:qc-ncr:update'], |
| | | ifShow: row.status === NCR_STATUS.PENDING_REVIEW, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: 'lucide:send', |
| | | auth: ['mes:qc-ncr:update'], |
| | | ifShow: row.status === NCR_STATUS.PENDING_REVIEW, |
| | | popConfirm: { |
| | | title: `确认æäº¤ä¸åæ ¼åå¤çå ${row.code} 审æ¹ï¼`, |
| | | confirm: handleSubmitApproval.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:qc-ncr:delete'], |
| | | ifShow: row.status === NCR_STATUS.PENDING_REVIEW, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'è¯å®¡', |
| | | type: 'link', |
| | | icon: 'lucide:clipboard-check', |
| | | auth: ['mes:qc-ncr:review'], |
| | | onClick: handleReview.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction |
| | | v-else-if="row.status === NCR_STATUS.REVIEWED" |
| | | :actions="[ |
| | | { |
| | | label: 'å¤ç½®', |
| | | type: 'link', |
| | | icon: 'lucide:play', |
| | | auth: ['mes:qc-ncr:dispose'], |
| | | ifShow: row.status === NCR_STATUS.REVIEWED, |
| | | onClick: handleDispose.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | <TableAction |
| | | v-else-if="row.status === NCR_STATUS.DISPOSED" |
| | | :actions="[ |
| | | { |
| | | label: 'å
³é', |
| | | type: 'link', |
| | | icon: 'lucide:check-circle', |
| | | auth: ['mes:qc-ncr:close'], |
| | | ifShow: row.status === NCR_STATUS.DISPOSED, |
| | | onClick: handleClose.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: 'lucide:history', |
| | | ifShow: !!row.processInstanceId, |
| | | onClick: handleViewApproval.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | <span v-else class="text-gray-400">--</span> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesQcPatrolApi } from '#/api/mes/qc/patrol'; |
| | | |
| | | import { h, markRaw } from 'vue'; |
| | | |
| | | import { MesAutoCodeRuleCode } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getDateRangePickerDefaultProps } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | formApi?: VbenFormApi, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'å·¡æ£åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¡æ£åç¼å·', |
| | | }, |
| | | rules: 'required', |
| | | suffix: () => |
| | | h( |
| | | Button, |
| | | { |
| | | disabled: formType === 'detail', |
| | | type: 'default', |
| | | onClick: async () => { |
| | | const code = await generateAutoCode( |
| | | MesAutoCodeRuleCode.QC_PATROL_CODE, |
| | | ); |
| | | await formApi?.setFieldValue('code', code); |
| | | }, |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å·¡æ£ååç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¡æ£ååç§°', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'itemId', |
| | | label: '产åç©æ', |
| | | component: markRaw(MdmItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©äº§åç©æ', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'reelNo', |
| | | label: 'çå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥çå·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | | label: 'æ¹æ¬¡å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¹æ¬¡å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'qualifiedQuantity', |
| | | label: 'åæ ¼æ°é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | placeholder: '请è¾å
¥', |
| | | precision: 2, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'unqualifiedQuantity', |
| | | label: 'ä¸åæ ¼æ°é', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | placeholder: '请è¾å
¥', |
| | | precision: 2, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | | label: 'å·¡æ£æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©å·¡æ£æ¥æ', |
| | | showTime: false, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | | label: 'å·¡æ£äººå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©å·¡æ£äººå', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: 'å·¡æ£åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥å·¡æ£åç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å·¡æ£ååç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥å·¡æ£ååç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemId', |
| | | label: '产åç©æ', |
| | | component: markRaw(MdmItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©äº§åç©æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'reelNo', |
| | | label: 'çå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥çå·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | | label: 'å·¡æ£äººå', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©å·¡æ£äººå', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | | label: 'å·¡æ£æ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getDateRangePickerDefaultProps(), |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesQcPatrolApi.Patrol>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'code', |
| | | title: 'å·¡æ£åç¼å·', |
| | | width: 170, |
| | | slots: { default: 'code' }, |
| | | }, |
| | | { |
| | | field: 'name', |
| | | title: 'å·¡æ£ååç§°', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'itemCode', |
| | | title: '产åç¼å·', |
| | | width: 130, |
| | | }, |
| | | { |
| | | field: 'itemName', |
| | | title: '产ååç§°', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'itemSpecification', |
| | | title: 'è§æ ¼åå·', |
| | | width: 130, |
| | | }, |
| | | { |
| | | field: 'unitName', |
| | | title: 'åä½', |
| | | width: 80, |
| | | }, |
| | | { |
| | | field: 'reelNo', |
| | | title: 'çå·', |
| | | width: 130, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | width: 130, |
| | | }, |
| | | { |
| | | field: 'qualifiedQuantity', |
| | | title: 'åæ ¼æ°é', |
| | | width: 110, |
| | | }, |
| | | { |
| | | field: 'unqualifiedQuantity', |
| | | title: 'ä¸åæ ¼æ°é', |
| | | width: 110, |
| | | }, |
| | | { |
| | | field: 'inspectDate', |
| | | title: 'å·¡æ£æ¥æ', |
| | | width: 160, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'inspectorNickname', |
| | | title: 'å·¡æ£äººå', |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 180, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ÎļþÃû´Ó src/views/mes/cal/team/index.vue ÐÞ¸Ä |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesCalTeamApi } from '#/api/mes/cal/team'; |
| | | import type { MesQcPatrolApi } from '#/api/mes/qc/patrol'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteTeam, exportTeam, getTeamPage } from '#/api/mes/cal/team'; |
| | | import { deletePatrol, getPatrolPage } from '#/api/mes/qc/patrol'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å建çç» */ |
| | | /** å建质éå·¡æ£å */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** æ¥ççç» */ |
| | | function handleDetail(row: MesCalTeamApi.Team) { |
| | | formModalApi.setData({ id: row.id, formType: 'detail' }).open(); |
| | | /** æ¥çè´¨éå·¡æ£å */ |
| | | function handleDetail(row: MesQcPatrolApi.Patrol) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾çç» */ |
| | | function handleEdit(row: MesCalTeamApi.Team) { |
| | | formModalApi.setData({ id: row.id, formType: 'update' }).open(); |
| | | /** ç¼è¾è´¨éå·¡æ£å */ |
| | | function handleEdit(row: MesQcPatrolApi.Patrol) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤çç» */ |
| | | async function handleDelete(row: MesCalTeamApi.Team) { |
| | | /** å é¤è´¨éå·¡æ£å */ |
| | | async function handleDelete(row: MesQcPatrolApi.Patrol) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.name]), |
| | | content: $t('ui.actionMessage.deleting', [row.code]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteTeam(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.name])); |
| | | await deletePatrol(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.code])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** 导åºçç» */ |
| | | async function handleExport() { |
| | | const data = await exportTeam(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'çç».xls', source: data }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getTeamPage({ |
| | | return await getPatrolPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesCalTeamApi.Team>, |
| | | } as VxeTableGridOptions<MesQcPatrolApi.Patrol>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <Grid table-title="çç»å表"> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="è´¨éå·¡æ£åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['çç»']), |
| | | label: $t('ui.actionTitle.create', ['è´¨éå·¡æ£å']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:cal-team:create'], |
| | | auth: ['mes:qc-patrol:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:cal-team:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #code="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.code }} |
| | | </Button> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:cal-team:update'], |
| | | auth: ['mes:qc-patrol:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:cal-team:delete'], |
| | | auth: ['mes:qc-patrol:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.name]), |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.code]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesQcPatrolApi } from '#/api/mes/qc/patrol'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createPatrol, |
| | | getPatrol, |
| | | updatePatrol, |
| | | } from '#/api/mes/qc/patrol'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import QcAttachments from '../../components/qc-attachments.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesQcPatrolApi.Patrol>(); |
| | | const subTabsName = ref('attachments'); |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'detail') { |
| | | return $t('ui.actionTitle.view', ['è´¨éå·¡æ£å']); |
| | | } |
| | | return formType.value === 'update' |
| | | ? $t('ui.actionTitle.edit', ['è´¨éå·¡æ£å']) |
| | | : $t('ui.actionTitle.create', ['è´¨éå·¡æ£å']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 110, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** æäº¤è¡¨å */ |
| | | async function handleSubmit(): Promise<boolean> { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | const data = (await formApi.getValues()) as MesQcPatrolApi.Patrol; |
| | | if (formData.value?.id) { |
| | | await updatePatrol({ ...data, id: formData.value.id }); |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | const id = await createPatrol(data); |
| | | formData.value = { ...data, id: id as unknown as number }; |
| | | await formApi.setFieldValue('id', formData.value.id); |
| | | formType.value = 'update'; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (isDetail.value) { |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const ok = await handleSubmit(); |
| | | if (!ok) { |
| | | return; |
| | | } |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | emit('success'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | subTabsName.value = 'attachments'; |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | modalApi.setState({ |
| | | confirmText: formType.value === 'detail' ? undefined : 'ä¿å', |
| | | showConfirmButton: formType.value !== 'detail', |
| | | }); |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getPatrol(data.id); |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-[820px]"> |
| | | <Form class="mx-4" /> |
| | | <Tabs |
| | | v-if="formData?.id" |
| | | v-model:active-key="subTabsName" |
| | | class="mx-4 mt-4" |
| | | > |
| | | <Tabs.TabPane key="attachments" tab="å·¡æ£éä»¶"> |
| | | <QcAttachments qc-type="patrol" :qc-id="formData!.id!" /> |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesQcStatisticsApi } from '#/api/mes/qc/statistics'; |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | import { |
| | | Button, |
| | | Card, |
| | | DatePicker, |
| | | Statistic, |
| | | Table, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getQcStatisticsSummary } from '#/api/mes/qc/statistics'; |
| | | |
| | | defineOptions({ name: 'MesQcStatistics' }); |
| | | |
| | | const { RangePicker } = DatePicker; |
| | | |
| | | /** æå·¥åºè¡¨æ ¼è¡ï¼è¡¥å
稳å®ç rowKeyï¼ */ |
| | | interface ProcessRow extends MesQcStatisticsApi.ByProcess { |
| | | _key: string; |
| | | } |
| | | |
| | | /** æå·¥åè¡¨æ ¼è¡ï¼è¡¥å
稳å®ç rowKeyï¼ */ |
| | | interface WorkOrderRow extends MesQcStatisticsApi.ByWorkOrder { |
| | | _key: string; |
| | | } |
| | | |
| | | const loading = ref(false); |
| | | const range = ref<[Dayjs, Dayjs]>([ |
| | | dayjs().subtract(29, 'day').startOf('day'), |
| | | dayjs().endOf('day'), |
| | | ]); |
| | | |
| | | /** 空ç»è®¡ç»æï¼é¿å
é¦å±æ¸²ææ¶åä¸å°å段 */ |
| | | function createEmptySummary(): MesQcStatisticsApi.Summary { |
| | | return { |
| | | overall: { |
| | | totalQcCount: 0, |
| | | checkQuantity: 0, |
| | | qualifiedQuantity: 0, |
| | | unqualifiedQuantity: 0, |
| | | qualifiedRate: 0, |
| | | unqualifiedRate: 0, |
| | | types: [], |
| | | }, |
| | | byProcess: [], |
| | | byWorkOrder: [], |
| | | }; |
| | | } |
| | | |
| | | const summary = ref<MesQcStatisticsApi.Summary>(createEmptySummary()); |
| | | const processRows = ref<ProcessRow[]>([]); |
| | | const workOrderRows = ref<WorkOrderRow[]>([]); |
| | | |
| | | const typeRows = computed(() => summary.value.overall.types ?? []); |
| | | |
| | | const typeColumns = [ |
| | | { dataIndex: 'qcTypeName', key: 'qcTypeName', title: 'æ£éªç±»å', width: 120 }, |
| | | { dataIndex: 'qcCount', key: 'qcCount', title: 'è´¨æ£åæ°é', width: 110 }, |
| | | { dataIndex: 'checkQuantity', key: 'checkQuantity', title: 'æ£æµæ»æ°', width: 120 }, |
| | | { dataIndex: 'qualifiedQuantity', key: 'qualifiedQuantity', title: 'åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'unqualifiedQuantity', key: 'unqualifiedQuantity', title: 'ä¸åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'qualifiedRate', key: 'qualifiedRate', title: 'åæ ¼ç', width: 100 }, |
| | | { dataIndex: 'unqualifiedRate', key: 'unqualifiedRate', title: 'ä¸åæ ¼ç', width: 100 }, |
| | | ]; |
| | | |
| | | const processColumns = [ |
| | | { dataIndex: 'processName', key: 'processName', title: 'å·¥åºåç§°', width: 180 }, |
| | | { dataIndex: 'checkQuantity', key: 'checkQuantity', title: 'æ£æµæ°', width: 120 }, |
| | | { dataIndex: 'qualifiedQuantity', key: 'qualifiedQuantity', title: 'åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'unqualifiedQuantity', key: 'unqualifiedQuantity', title: 'ä¸åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'qualifiedRate', key: 'qualifiedRate', title: 'åæ ¼ç', width: 100 }, |
| | | ]; |
| | | |
| | | const workOrderColumns = [ |
| | | { dataIndex: 'workOrderCode', key: 'workOrderCode', title: 'å·¥åå·', width: 180 }, |
| | | { dataIndex: 'productName', key: 'productName', title: '产ååç§°', width: 220 }, |
| | | { dataIndex: 'checkQuantity', key: 'checkQuantity', title: 'æ£æµæ°', width: 120 }, |
| | | { dataIndex: 'qualifiedQuantity', key: 'qualifiedQuantity', title: 'åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'unqualifiedQuantity', key: 'unqualifiedQuantity', title: 'ä¸åæ ¼æ°', width: 110 }, |
| | | { dataIndex: 'qualifiedRate', key: 'qualifiedRate', title: 'åæ ¼ç', width: 100 }, |
| | | ]; |
| | | |
| | | /** æ°éå±ç¤ºï¼ååä½ + æå¤ä¸¤ä½å°æ° */ |
| | | function formatQuantity(value?: number) { |
| | | return Number(value ?? 0).toLocaleString('zh-CN', { |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | } |
| | | |
| | | /** æ¯çå±ç¤ºï¼ç»ä¸ä¸¤ä½å°æ°å¹¶å¸¦ç¾åå· */ |
| | | function formatRate(value?: number) { |
| | | return `${Number(value ?? 0).toFixed(2)}%`; |
| | | } |
| | | |
| | | /** å 载质éç»è®¡æ°æ® */ |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | const params: { beginTime?: string; endTime?: string } = {}; |
| | | if (range.value?.[0]) { |
| | | params.beginTime = range.value[0].format('YYYY-MM-DD 00:00:00'); |
| | | } |
| | | if (range.value?.[1]) { |
| | | params.endTime = range.value[1].format('YYYY-MM-DD 23:59:59'); |
| | | } |
| | | const data = await getQcStatisticsSummary(params); |
| | | summary.value = data; |
| | | processRows.value = (data.byProcess ?? []).map((row, index) => ({ |
| | | ...row, |
| | | _key: |
| | | row.processId === null || row.processId === undefined |
| | | ? `process-none-${index}` |
| | | : `process-${row.processId}`, |
| | | })); |
| | | workOrderRows.value = (data.byWorkOrder ?? []).map((row, index) => ({ |
| | | ...row, |
| | | _key: |
| | | row.workOrderId === null || row.workOrderId === undefined |
| | | ? `work-order-none-${index}` |
| | | : `work-order-${row.workOrderId}`, |
| | | })); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="flex h-full flex-col gap-3"> |
| | | <!-- çé --> |
| | | <Card size="small"> |
| | | <div class="flex flex-wrap items-center gap-3"> |
| | | <span class="text-muted-foreground text-sm">æ£æµæ¥æ</span> |
| | | <RangePicker v-model:value="range" /> |
| | | <Button type="primary" :loading="loading" @click="loadData"> |
| | | æ¥è¯¢ |
| | | </Button> |
| | | <span class="text-muted-foreground text-xs"> |
| | | ä»
ç»è®¡å·²å®ææ£éªåï¼ä¸éæ¥æåç»è®¡å
¨é¨ |
| | | </span> |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- æ¦è§å¡ç --> |
| | | <div class="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6"> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="è´¨æ£åæ»æ°" |
| | | :value="summary.overall.totalQcCount" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="æ»æ£æµæ°" |
| | | :value="Number(summary.overall.checkQuantity ?? 0)" |
| | | :precision="2" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="åæ ¼æ°" |
| | | :value="Number(summary.overall.qualifiedQuantity ?? 0)" |
| | | :precision="2" |
| | | :value-style="{ color: '#3f8600' }" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="ä¸åæ ¼æ°" |
| | | :value="Number(summary.overall.unqualifiedQuantity ?? 0)" |
| | | :precision="2" |
| | | :value-style="{ color: '#cf1322' }" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="åæ ¼ç" |
| | | :value="Number(summary.overall.qualifiedRate ?? 0)" |
| | | :precision="2" |
| | | suffix="%" |
| | | :value-style="{ color: '#3f8600' }" |
| | | /> |
| | | </Card> |
| | | <Card size="small"> |
| | | <Statistic |
| | | title="ä¸åæ ¼ç" |
| | | :value="Number(summary.overall.unqualifiedRate ?? 0)" |
| | | :precision="2" |
| | | suffix="%" |
| | | :value-style="{ color: '#cf1322' }" |
| | | /> |
| | | </Card> |
| | | </div> |
| | | |
| | | <!-- 忣éªç±»å --> |
| | | <Card size="small" title="忣éªç±»åç»è®¡"> |
| | | <Table |
| | | :columns="typeColumns" |
| | | :data-source="typeRows" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | row-key="qcType" |
| | | size="small" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'checkQuantity'"> |
| | | {{ formatQuantity(record.checkQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedQuantity'"> |
| | | {{ formatQuantity(record.qualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'unqualifiedQuantity'"> |
| | | {{ formatQuantity(record.unqualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedRate'"> |
| | | {{ formatRate(record.qualifiedRate) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'unqualifiedRate'"> |
| | | {{ formatRate(record.unqualifiedRate) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <!-- æå·¥åº --> |
| | | <Card size="small" title="æå·¥åºç»è®¡ï¼è¿ç¨æ£éª IPQCï¼"> |
| | | <Table |
| | | :columns="processColumns" |
| | | :data-source="processRows" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | row-key="_key" |
| | | size="small" |
| | | :scroll="{ x: 720 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'checkQuantity'"> |
| | | {{ formatQuantity(record.checkQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedQuantity'"> |
| | | {{ formatQuantity(record.qualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'unqualifiedQuantity'"> |
| | | {{ formatQuantity(record.unqualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedRate'"> |
| | | {{ formatRate(record.qualifiedRate) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | |
| | | <!-- æè®¢å --> |
| | | <Card size="small" title="æç产工åç»è®¡ï¼è¿ç¨æ£éª IPQCï¼"> |
| | | <Table |
| | | :columns="workOrderColumns" |
| | | :data-source="workOrderRows" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | row-key="_key" |
| | | size="small" |
| | | :scroll="{ x: 860 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'checkQuantity'"> |
| | | {{ formatQuantity(record.checkQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedQuantity'"> |
| | | {{ formatQuantity(record.qualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'unqualifiedQuantity'"> |
| | | {{ formatQuantity(record.unqualifiedQuantity) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'qualifiedRate'"> |
| | | {{ formatRate(record.qualifiedRate) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessBind from './modules/process-bind.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessBindModal, processBindModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessBind, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** ç¼è¾è´¨æ£æ¹æ¡ */ |
| | | function handleEdit(row: MesQcTemplateApi.Template) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç»´æ¤æ¹æ¡éç¨çå·¥åº */ |
| | | function handleProcessBind(row: MesQcTemplateApi.Template) { |
| | | processBindModalApi |
| | | .setData({ |
| | | templateId: row.id, |
| | | templateName: row.name, |
| | | types: row.types, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å é¤è´¨æ£æ¹æ¡ */ |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ProcessBindModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="è´¨æ£æ¹æ¡å表"> |
| | | <template #toolbar-tools> |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'éç¨å·¥åº', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:qc-template:update'], |
| | | onClick: handleProcessBind.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesQcTemplateProcessApi } from '#/api/mes/qc/template-process'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { |
| | | Button, |
| | | InputNumber, |
| | | message, |
| | | Select, |
| | | Switch, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { |
| | | getTemplateProcessListByTemplate, |
| | | saveTemplateProcessByTemplate, |
| | | } from '#/api/mes/qc/template-process'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const QC_TYPE_OPTIONS: Array<{ label: string; value: number }> = getDictOptions( |
| | | DICT_TYPE.MES_QC_TYPE, |
| | | 'number', |
| | | ).map((item) => ({ label: item.label, value: Number(item.value) })); |
| | | |
| | | const templateId = ref<number>(); |
| | | const templateName = ref<string>(''); |
| | | const templateTypes = ref<number[]>([]); |
| | | const qcType = ref<number>(); |
| | | const loading = ref(false); |
| | | /** æ¹æ¡ä¸å
¨é¨ç»å®ï¼ææ£æµç§ç±»è¿æ»¤å±ç¤ºï¼ */ |
| | | const allList = ref<MesQcTemplateProcessApi.Binding[]>([]); |
| | | const list = computed(() => |
| | | allList.value.filter((item) => (item.qcType ?? qcType.value) === qcType.value), |
| | | ); |
| | | const processOptions = ref<Array<{ label: string; value: number }>>([]); |
| | | const selectedProcessId = ref<number>(); |
| | | |
| | | const getTitle = computed(() => `éç¨å·¥åº - ${templateName.value || ''}`); |
| | | /** æ¹æ¡èªèº«æ¯æçæ£æµç§ç±» */ |
| | | const qcTypeOptions = computed(() => |
| | | templateTypes.value.length > 0 |
| | | ? QC_TYPE_OPTIONS.filter((item) => templateTypes.value.includes(item.value)) |
| | | : QC_TYPE_OPTIONS, |
| | | ); |
| | | |
| | | /** å è½½ç»å®ä¸å·¥åºé项 */ |
| | | async function loadData() { |
| | | if (!templateId.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const [bindings, processes] = await Promise.all([ |
| | | getTemplateProcessListByTemplate(templateId.value), |
| | | getProcessSimpleList(), |
| | | ]); |
| | | allList.value = bindings; |
| | | processOptions.value = (processes ?? []).map((item) => ({ |
| | | label: `${item.code ?? ''} ${item.name ?? ''}`.trim(), |
| | | value: item.id!, |
| | | })); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** 追å ä¸ä¸ªå·¥åº */ |
| | | function addProcess() { |
| | | if (!selectedProcessId.value) { |
| | | message.warning('请å
éæ©è¦æ·»å çå·¥åº'); |
| | | return; |
| | | } |
| | | if (list.value.some((item) => item.processId === selectedProcessId.value)) { |
| | | message.warning('该工åºå·²å¨éç¨å表ä¸'); |
| | | return; |
| | | } |
| | | const process = processOptions.value.find( |
| | | (item) => item.value === selectedProcessId.value, |
| | | ); |
| | | allList.value.push({ |
| | | processId: selectedProcessId.value, |
| | | processName: process?.label, |
| | | qcType: qcType.value, |
| | | defaultFlag: list.value.length === 0, |
| | | sort: list.value.length + 1, |
| | | }); |
| | | selectedProcessId.value = undefined; |
| | | } |
| | | |
| | | /** ç§»é¤å·¥åº */ |
| | | function removeProcess(index: number) { |
| | | const target = list.value[index]; |
| | | if (!target) { |
| | | return; |
| | | } |
| | | const allIndex = allList.value.indexOf(target); |
| | | if (allIndex >= 0) { |
| | | allList.value.splice(allIndex, 1); |
| | | } |
| | | } |
| | | |
| | | /** åªè½è®¾ç½®ä¸ä¸ªé»è®¤æ¹æ¡ï¼åæ¢æ¶åæ¶å
¶ä» */ |
| | | function setDefault(index: number, checked: boolean) { |
| | | if (checked) { |
| | | list.value.forEach((item, itemIndex) => { |
| | | item.defaultFlag = itemIndex === index; |
| | | }); |
| | | } else { |
| | | list.value[index]!.defaultFlag = false; |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!templateId.value || !qcType.value) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await saveTemplateProcessByTemplate({ |
| | | templateId: templateId.value, |
| | | qcType: qcType.value, |
| | | list: list.value.map((item, index) => ({ |
| | | processId: item.processId!, |
| | | defaultFlag: item.defaultFlag, |
| | | sort: item.sort ?? index + 1, |
| | | })), |
| | | }); |
| | | message.success('ä¿åæå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | templateId.value = undefined; |
| | | templateTypes.value = []; |
| | | qcType.value = undefined; |
| | | allList.value = []; |
| | | selectedProcessId.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | templateId: number; |
| | | templateName?: string; |
| | | types?: number[]; |
| | | }>(); |
| | | templateId.value = data.templateId; |
| | | templateName.value = data.templateName || ''; |
| | | templateTypes.value = data.types ?? []; |
| | | qcType.value = templateTypes.value[0] ?? 2; |
| | | await loadData(); |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <div class="mx-4"> |
| | | <div class="mb-3 flex items-center gap-3"> |
| | | <span class="text-sm text-gray-600">æ£æµç§ç±»</span> |
| | | <Select |
| | | v-model:value="qcType" |
| | | :options="qcTypeOptions" |
| | | class="!w-48" |
| | | placeholder="è¯·éæ©æ£æµç§ç±»" |
| | | /> |
| | | <span class="text-xs text-gray-400"> |
| | | è¿ç¨æ£éªï¼IPQCï¼éå·¥åºæ¶ææ¤å¤ç»å®å¸¦åºæ¹æ¡ |
| | | </span> |
| | | </div> |
| | | <div class="mb-2 flex items-center gap-2"> |
| | | <Select |
| | | v-model:value="selectedProcessId" |
| | | :options="processOptions" |
| | | allow-clear |
| | | option-filter-prop="label" |
| | | placeholder="éæ©è¦æ·»å çå·¥åº" |
| | | show-search |
| | | class="!w-64" |
| | | /> |
| | | <Button type="primary" size="small" @click="addProcess">æ·»å å·¥åº</Button> |
| | | </div> |
| | | <div v-if="list.length > 0" class="rounded border border-gray-200"> |
| | | <div |
| | | class="grid grid-cols-12 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium" |
| | | > |
| | | <div class="col-span-5">å·¥åº</div> |
| | | <div class="col-span-2">æ£æµç§ç±»</div> |
| | | <div class="col-span-2 text-center">é»è®¤æ¹æ¡</div> |
| | | <div class="col-span-1">æåº</div> |
| | | <div class="col-span-2 text-center">æä½</div> |
| | | </div> |
| | | <div |
| | | v-for="(item, index) in list" |
| | | :key="item.processId" |
| | | class="grid grid-cols-12 items-center gap-2 border-b border-gray-100 p-2" |
| | | > |
| | | <div class="col-span-5 text-sm">{{ item.processName || item.processId }}</div> |
| | | <div class="col-span-2"> |
| | | <DictTag :type="DICT_TYPE.MES_QC_TYPE" :value="item.qcType ?? qcType" /> |
| | | </div> |
| | | <div class="col-span-2 flex items-center justify-center"> |
| | | <Switch |
| | | :checked="item.defaultFlag" |
| | | size="small" |
| | | @change="(checked) => setDefault(index, checked === true)" |
| | | /> |
| | | </div> |
| | | <div class="col-span-1"> |
| | | <InputNumber v-model:value="item.sort" :min="0" size="small" class="w-full" /> |
| | | </div> |
| | | <div class="col-span-2 flex items-center justify-center"> |
| | | <Button type="text" danger size="small" @click="removeProcess(index)"> |
| | | ç§»é¤ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-else |
| | | class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400" |
| | | > |
| | | {{ loading ? 'å è½½ä¸â¦' : 'è¯¥æ¹æ¡ææªç»å®éç¨å·¥åº' }} |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | <Descriptions.Item label="åæ ¼åæ°é">{{ task.qualifyQuantity ?? 0 }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä¸è¯åæ°é">{{ task.unqualifyQuantity ?? 0 }}</Descriptions.Item> |
| | | <Descriptions.Item label="客æ·åç§°">{{ task.clientName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ¯å¦è´¨æ£å·¥åº"> |
| | | <Tag v-if="task.checkFlag" color="orange">æ¯</Tag> |
| | | <Tag v-else>å¦</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="éæ±æ¥æ" :span="2"> |
| | | {{ task.requestDate ?? 'â' }} |
| | | </Descriptions.Item> |
| | |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | import { DatePicker, message, Modal, Tag } from 'ant-design-vue'; |
| | | import { h } from 'vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | 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(); |
| | | function handleConfirm(row: SrmInquiryApi.Inquiry) { |
| | | let paymentDate = ''; |
| | | Modal.confirm({ |
| | | title: `确认询价å ${row.no} ç䏿 ç»æ`, |
| | | content: h('div', { class: 'mt-3' }, [ |
| | | h('p', '确认å尿䏿 ä¾åºåçæéè´è®¢åè稿ï¼è¯¢ä»·åä¸å¯åä¿®æ¹ã'), |
| | | h('label', { class: 'mb-2 block', for: 'inquiry-payment-date' }, 'éè´è®¢å仿¬¾æ¥æ'), |
| | | h(DatePicker, { |
| | | id: 'inquiry-payment-date', |
| | | class: 'w-full', |
| | | format: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©ä»æ¬¾æ¥æ', |
| | | onChange: (_date: unknown, value: string | string[]) => { |
| | | paymentDate = typeof value === 'string' ? value : ''; |
| | | }, |
| | | }), |
| | | ]), |
| | | async onOk() { |
| | | if (!paymentDate) { |
| | | message.error('è¯·éæ©éè´è®¢å仿¬¾æ¥æ'); |
| | | return Promise.reject(new Error('仿¬¾æ¥æä¸è½ä¸ºç©º')); |
| | | } |
| | | const result = await confirmInquiry(row.id!, [], paymentDate); |
| | | const orders = result.orders ?? []; |
| | | message.success( |
| | | `询价å ${row.no} å·²ç¡®è®¤ä¸æ ï¼çæ ${orders.length} å¼ éè´è®¢åè稿ï¼${orders |
| | | .map((item) => item.purchaseOrderNo) |
| | | .join('ã')}ï¼éå¨éè´è®¢å页æäº¤å®¡æ¹ï¼`, |
| | | ); |
| | | handleRefresh(); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | async function handleCancel(row: SrmInquiryApi.Inquiry) { |
| | |
| | | type: 'link', |
| | | auth: ['srm:inquiry:confirm'], |
| | | ifShow: () => row.status === 0 || row.status === 10, |
| | | popConfirm: { |
| | | title: `确认询价å ${row.no} ç䏿 ç»æå¹¶çæéè´è®¢åè稿åï¼çæåä¸å¯åä¿®æ¹è¯¥è¯¢ä»·åã`, |
| | | confirm: handleConfirm.bind(null, row), |
| | | }, |
| | | onClick: handleConfirm.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åæ¶', |
| | |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'companyPhone', |
| | | label: 'å
¬å¸çµè¯', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'éå¡«ï¼è¯·è¾å
¥å
¬å¸çµè¯' }, |
| | | }, |
| | | { |
| | | fieldName: 'taxNo', |
| | | label: 'ç¨å·', |
| | | component: 'Input', |
| | |
| | | minWidth: 80, |
| | | cellRender: { name: 'CellTag', props: SUPPLIER_STATUS_CELLTAG }, |
| | | }, |
| | | { field: 'companyPhone', title: 'å
¬å¸çµè¯', minWidth: 120 }, |
| | | { field: 'contactName', title: 'è系人', minWidth: 100 }, |
| | | { field: 'contactMobile', title: 'èç³»çµè¯', minWidth: 120 }, |
| | | { field: 'contactEmail', title: 'é®ç®±', minWidth: 150 }, |
| | |
| | | cancelProductSales, |
| | | deleteProductSales, |
| | | exportProductSales, |
| | | exportProductSalesWord, |
| | | getProductSalesPage, |
| | | } from '#/api/mes/wm/productsales'; |
| | | import { $t } from '#/locales'; |
| | |
| | | await cancelProductSales(row.id!); |
| | | message.success('åæ¶æå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** ä¸è½½æååºåºå Word */ |
| | | async function handleDownloadWord(row: MesWmProductSalesApi.ProductSales) { |
| | | const source = await exportProductSalesWord(row.id!); |
| | | downloadFileFromBlobPart({ fileName: `æååºåºå-${row.code}.docx`, source }); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | |
| | | onClick: handleFinish.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä¸è½½åºåºå', |
| | | type: 'link', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:wm-product-sales:export'], |
| | | ifShow: |
| | | row.status === MesWmProductSalesStatusEnum.APPROVED || |
| | | row.status === MesWmProductSalesStatusEnum.FINISHED, |
| | | onClick: handleDownloadWord.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åæ¶', |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'logisticsCompany', |
| | | label: 'ç©æµå
¬å¸', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç©æµå
¬å¸ï¼å¦ï¼é¡ºä¸°éè¿', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'expressFee', |
| | | label: 'å¿«éè´¹', |
| | | component: 'InputNumber', |
| | | defaultValue: 0, |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥å¿«éè´¹', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'invoiceStatus', |
| | | label: 'å¼ç¥¨ç¶æ', |
| | | component: 'Select', |
| | | defaultValue: 0, |
| | | componentProps: { |
| | | class: '!w-full', |
| | | options: [ |
| | | { label: 'æªå¼ç¥¨', value: 0 }, |
| | | { label: 'é¨åå¼ç¥¨', value: 1 }, |
| | | { label: 'å·²å¼ç¥¨', value: 2 }, |
| | | ], |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'logisticsCompany', |
| | | title: 'ç©æµå
¬å¸', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'expressFee', |
| | | title: 'å¿«éè´¹', |
| | | width: 100, |
| | | align: 'right', |
| | | }, |
| | | { |
| | | field: 'invoiceStatus', |
| | | title: 'å¼ç¥¨ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'invoiceStatus' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'åæ®ç¶æ', |
| | | minWidth: 100, |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** åè´§å°è´¦ï¼æäº§åå¹³éºï¼çåæ®µ */ |
| | | export function useLedgerGridColumns(): VxeTableGridOptions<MesWmSalesNoticeApi.SalesNoticeLedger>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'noticeCode', |
| | | title: 'åè´§åå·', |
| | | minWidth: 160, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'saleOrderCode', |
| | | title: 'éå®è®¢åå·', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'clientName', |
| | | title: '客æ·åç§°', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'salesDate', |
| | | title: 'åè´§æ¥æ', |
| | | width: 120, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'itemCode', |
| | | title: '产åç¼å·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemName', |
| | | title: '产ååç§°', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'itemSpecification', |
| | | title: 'è§æ ¼åå·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'åè´§æ°é', |
| | | width: 100, |
| | | align: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'unitName', |
| | | title: 'åä½', |
| | | width: 80, |
| | | }, |
| | | { |
| | | field: 'outCount', |
| | | title: 'å·²åºåºæ°é', |
| | | width: 110, |
| | | align: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'logisticsCompany', |
| | | title: 'ç©æµå
¬å¸', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'expressFee', |
| | | title: 'å¿«éè´¹', |
| | | width: 100, |
| | | align: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'invoiceStatus', |
| | | title: 'å¼ç¥¨ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'invoiceStatus' }, |
| | | }, |
| | | { |
| | | field: 'outStatus', |
| | | title: 'åºåºç¶æ', |
| | | width: 100, |
| | | slots: { default: 'outStatus' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'åæ®ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_SALES_NOTICE_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'recipientName', |
| | | title: 'æ¶è´§äºº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'recipientAddress', |
| | | title: 'æ¶è´§å°å', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'lineRemark', |
| | | title: 'æç»å¤æ³¨', |
| | | minWidth: 120, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** éç¥åè¡å表çåæ®µ */ |
| | | export function useLineGridColumns( |
| | | editable: boolean, |
| | |
| | | import { MesWmSalesNoticeOutStatusEnum, MesWmSalesNoticeStatusEnum } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | import { Button, message, RadioButton, RadioGroup, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | |
| | | import { |
| | | deleteSalesNotice, |
| | | exportSalesNotice, |
| | | exportSalesNoticeWord, |
| | | generateProductSales, |
| | | getSalesNoticeLedgerPage, |
| | | getSalesNoticePage, |
| | | } from '#/api/mes/wm/salesnotice'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import { useGridColumns, useGridFormSchema, useLedgerGridColumns } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | |
| | | }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | /** å表è§å¾ï¼æåè´§å / æäº§åå¹³éº */ |
| | | const viewMode = ref<'notice' | 'product'>('notice'); |
| | | |
| | | const outStatusLabelMap: Record<number, string> = { |
| | | [MesWmSalesNoticeOutStatusEnum.NONE]: 'æªåºåº', |
| | |
| | | } catch (e) { |
| | | console.error(e); |
| | | } |
| | | } |
| | | |
| | | /** ä¸è½½åè´§å Word */ |
| | | async function handleDownloadWord(row: MesWmSalesNoticeApi.SalesNotice) { |
| | | const source = await exportSalesNoticeWord(row.id!); |
| | | downloadFileFromBlobPart({ fileName: `åè´§å-${row.code}.docx`, source }); |
| | | } |
| | | |
| | | /** å é¤åè´§éç¥å */ |
| | |
| | | }, |
| | | } as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>, |
| | | }); |
| | | |
| | | const [LedgerGrid] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useLedgerGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getSalesNoticeLedgerPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'lineId', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNoticeLedger>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="åè´§éç¥åå表"> |
| | | <div class="flex h-full flex-col gap-2"> |
| | | <div class="flex justify-end"> |
| | | <RadioGroup v-model:value="viewMode" button-style="solid" size="small"> |
| | | <RadioButton value="notice">æåè´§å</RadioButton> |
| | | <RadioButton value="product">æäº§åå¹³éº</RadioButton> |
| | | </RadioGroup> |
| | | </div> |
| | | |
| | | <div class="min-h-0 flex-1"> |
| | | <Grid v-if="viewMode === 'notice'" table-title="åè´§éç¥åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.code }} |
| | | </Button> |
| | | </template> |
| | | <template #invoiceStatus="{ row }"> |
| | | <Tag v-if="row.invoiceStatus === 2" color="success">å·²å¼ç¥¨</Tag> |
| | | <Tag v-else-if="row.invoiceStatus === 1" color="processing">é¨åå¼ç¥¨</Tag> |
| | | <Tag v-else color="default">æªå¼ç¥¨</Tag> |
| | | </template> |
| | | <template #outStatus="{ row }"> |
| | | <Tag :color="outStatusColorMap[row.outStatus ?? 0]">{{ outStatusLabelMap[row.outStatus ?? 0] }}</Tag> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: 'ä¸è½½åè´§å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['mes:wm-sales-notice:export'], |
| | | ifShow: row.status === MesWmSalesNoticeStatusEnum.APPROVED, |
| | | onClick: handleDownloadWord.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'çæåºåºå', |
| | | type: 'link', |
| | | icon: 'ant-design:file-protect-outlined', |
| | |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | |
| | | <LedgerGrid v-else table-title="åè´§å°è´¦ï¼æäº§åå¹³éºï¼"> |
| | | <template #invoiceStatus="{ row }"> |
| | | <Tag v-if="row.invoiceStatus === 2" color="success">å·²å¼ç¥¨</Tag> |
| | | <Tag v-else-if="row.invoiceStatus === 1" color="processing">é¨åå¼ç¥¨</Tag> |
| | | <Tag v-else color="default">æªå¼ç¥¨</Tag> |
| | | </template> |
| | | <template #outStatus="{ row }"> |
| | | <Tag :color="outStatusColorMap[row.outStatus ?? 0]">{{ outStatusLabelMap[row.outStatus ?? 0] }}</Tag> |
| | | </template> |
| | | </LedgerGrid> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { MesWmSalesNoticeStatusEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Divider, message, Popconfirm } from 'ant-design-vue'; |
| | | import { Button, Divider, message, Modal as AModal, Popconfirm, Select } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createSalesNotice, |
| | | getSalesNotice, |
| | | getSalesNoticeApproveProcessList, |
| | | submitSalesNotice, |
| | | updateSalesNotice, |
| | | } from '#/api/mes/wm/salesnotice'; |
| | | import { automaticApproval } from '#/store/audit-policy'; |
| | | import { createSalesNoticeLine } from '#/api/mes/wm/salesnotice/line'; |
| | | import { getSaleOrderItemListByOrderId } from '#/api/erp/sale/order'; |
| | | import { $t } from '#/locales'; |
| | |
| | | wrapperClass: 'grid-cols-3', |
| | | }); |
| | | |
| | | /** æäº¤éç¥åï¼è¡¨åæä¿®æ¹æ¶å
ä¿åï¼åè°ç¨æäº¤æ¥å£ */ |
| | | /** æäº¤å®¡æ¹å¼¹çª */ |
| | | const submitModalVisible = ref(false); |
| | | const processList = ref<Array<{ key: string; name: string }>>([]); |
| | | const selectedProcessKey = ref<string>(); |
| | | const submitting = ref(false); |
| | | |
| | | async function doSubmit(processDefinitionKey: string) { |
| | | submitting.value = true; |
| | | try { |
| | | await submitSalesNotice(formData.value!.id!, processDefinitionKey); |
| | | message.success(automaticApproval.value ? 'å·²æäº¤çæ' : 'æäº¤å®¡æ¹æå'); |
| | | submitModalVisible.value = false; |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirmSubmit() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | await doSubmit(selectedProcessKey.value); |
| | | } |
| | | |
| | | /** æäº¤éç¥åï¼è¡¨åæä¿®æ¹æ¶å
ä¿åï¼åæäº¤å®¡æ¹ */ |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid || !formData.value?.id) { |
| | |
| | | await updateSalesNotice({ ...formData.value, ...data }); |
| | | originalSnapshot.value = current; |
| | | } |
| | | await submitSalesNotice(formData.value.id); |
| | | message.success('æäº¤æå'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | if (automaticApproval.value) { |
| | | await doSubmit('sales_delivery_approve'); |
| | | return; |
| | | } |
| | | const list = await getSalesNoticeApproveProcessList(); |
| | | if (list.length === 1) { |
| | | await doSubmit(list[0]!.key); |
| | | return; |
| | | } |
| | | processList.value = list; |
| | | selectedProcessKey.value = undefined; |
| | | submitModalVisible.value = true; |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </Popconfirm> |
| | | </div> |
| | | </template> |
| | | <AModal |
| | | v-model:open="submitModalVisible" |
| | | title="éæ©å®¡æ¹æµç¨" |
| | | :confirm-loading="submitting" |
| | | width="400px" |
| | | @ok="confirmSubmit" |
| | | > |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | class="w-full" |
| | | placeholder="è¯·éæ©åè´§å®¡æ¹æµç¨" |
| | | > |
| | | <Select.Option v-for="item in processList" :key="item.key" :value="item.key"> |
| | | {{ item.name }} |
| | | </Select.Option> |
| | | </Select> |
| | | </AModal> |
| | | </Modal> |
| | | </template> |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'workOrderCode', |
| | | title: 'ç产订åå·', |
| | | minWidth: 150, |
| | | showOverflow: 'tooltip', |
| | | }, |
| | | { |
| | | field: 'itemCode', |
| | | title: 'ç©æç¼ç ', |
| | | width: 120, |