feat(crm,mes): 扩展客户字段与重构批次号生成逻辑
- 客户实体新增社会信用代码及上级经销商等扩展字段
- 客户查询与保存接口支持新增字段的传入与返回
- 增加客户所属上级经销商不能选择自身的校验逻辑
- 重构生产批次号生成规则为四位年份加三位流水号
- 生产批次支持手动输入批次号或系统自动生成
- 简化工作站仓库层级处理并移除库区库位强校验
- 批次列表接口新增返回关联的正式托盘码列表
- 新增袋码与托盘码的批量扫码内容反查方法
- 更新扫码入库与库存袋码查询相关的接口文档
已添加13个文件
已删除12个文件
已修改79个文件
| | |
| | | export interface Customer { |
| | | id: number; // ç¼å· |
| | | name: string; // 客æ·åç§° |
| | | socialCreditCode?: string; // 社ä¼ä¿¡ç¨ä»£ç |
| | | idCardNo?: string; // 身份è¯å· |
| | | customerName?: string; // 客æ·å§å |
| | | filingSerialNo?: string; // 夿¡æµæ°´å· |
| | | bankAccountName?: string; // é¶è¡è´¦æ·åç§° |
| | | region?: string; // æå±åºå |
| | | parentDealerId?: number; // æå±ä¸çº§ç»éåç¼å· |
| | | parentDealerName?: string; // æå±ä¸çº§ç»éååç§° |
| | | followUpStatus: boolean; // è·è¿ç¶æ |
| | | contactLastTime: Date; // æåè·è¿æ¶é´ |
| | | contactLastContent: string; // æåè·è¿å
容 |
| | |
| | | itemName?: string; |
| | | bindingMode?: 'pallet' | 'tonBag'; // æ¹æ¬¡å·²æç»å®æ¹å¼ |
| | | businessCode?: string; // å
é¨åºå
¥åºä¸å¡ç |
| | | code?: string; // æ¹æ¬¡å·ï¼è¡¥å½æ£å¼ä¿¡æ¯åèªå¨çæï¼ |
| | | code?: string; // æ¹æ¬¡å·ï¼æ°å¢/è¡¥å½å¯äººå·¥è¾å
¥ï¼ç空èªå¨çæï¼çæåä¸å¯ä¿®æ¹ï¼ |
| | | tempCode?: string; // ä¸´æ¶æ¹æ¬¡ç ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼æ«ç 追溯å
¼å®¹ï¼ |
| | | lineId?: number; // 产线ç¼å·ï¼é¡µé¢å±ç¤ºä¸ºãå å·¥åä½ãï¼ |
| | | lineCode?: string; // 产线ç¼ç ï¼å端åå¡«ï¼é¡µé¢å·²éèï¼ |
| | |
| | | ownerUserName?: string; // 责任人æµç§° |
| | | bagQuantity?: number; // å·²çæè¢ç æ°é |
| | | palletQuantity?: number; // å·²çææçç æ°é |
| | | palletCodes?: string[]; // æ¹æ¬¡ä¸å
¨é¨æ£å¼æçç |
| | | status?: number; // æ¹æ¬¡ç¶æï¼åå
¸ mes_pro_batch_statusï¼ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | |
| | | return requestClient.put('/mes/pro/batch/fill-info', data); |
| | | } |
| | | |
| | | /** æ°å¢çäº§æ¹æ¬¡ï¼èªå¨çææ¹æ¬¡å·ï¼ */ |
| | | /** æ°å¢çäº§æ¹æ¬¡ï¼æ¹æ¬¡å·å¯äººå·¥è¾å
¥ï¼ç空èªå¨çæï¼ */ |
| | | export function createBatch(data: MesProBatchApi.Batch) { |
| | | return requestClient.post<number>('/mes/pro/batch/create', data); |
| | | } |
| | |
| | | bagCode?: string; |
| | | scanType?: ScanType; |
| | | batchCode?: string; |
| | | /** æå±æçç¼å·ï¼æçç äºä»¶ä¸ºæ«ç æçï¼è¢ç äºä»¶ä¸ºæå±æçï¼æ¹æ¬¡äºä»¶ä¸ºç©ºï¼ */ |
| | | palletId?: number; |
| | | /** æçç */ |
| | | palletCode?: string; |
| | | quantity?: number; |
| | | deviceCode?: string; |
| | | lineCode?: string; |
| | |
| | | export interface Dashboard { |
| | | latest?: ScanEvent; |
| | | total?: number; |
| | | success?: number; |
| | | failed?: number; |
| | | } |
| | | |
| | | export interface InboundParams { |
| | | eventId: string; |
| | | bagCode: string; |
| | | scanType: ScanType; |
| | | /** ä¸ä¼ é»è®¤æè¢ç BAG å¤ç */ |
| | | scanType?: ScanType; |
| | | quantity: number; |
| | | deviceCode?: string; |
| | | lineCode?: string; |
| | | locationId: number; |
| | | areaId: number; |
| | | /** åºåºãåºä½å·²ä¸åè¦æ±åç«¯éæ©ï¼å¯ä¸ä¼ */ |
| | | locationId?: number; |
| | | areaId?: number; |
| | | remark?: string; |
| | | } |
| | | } |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export interface SeedIndicatorRange { |
| | | id: number; |
| | | itemId: number; |
| | | indicatorCode: string; |
| | | indicatorName: string; |
| | | unit?: string; |
| | | minValue: number; |
| | | maxValue: number; |
| | | sort?: number; |
| | | status?: number; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | export function getSeedIndicatorRanges() { |
| | | return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges'); |
| | | export interface SeedIndicatorRangeSaveParams { |
| | | itemId: number; |
| | | indicatorCode: string; |
| | | indicatorName: string; |
| | | unit?: string; |
| | | minValue: number; |
| | | maxValue: number; |
| | | sort?: number; |
| | | status?: number; |
| | | } |
| | | |
| | | export function saveSeedIndicatorRanges(ranges: SeedIndicatorRange[]) { |
| | | return requestClient.put('/mes/qc/seed-quality/ranges', { ranges }); |
| | | /** 产åè´¨æ£é¡¹é
ç½®å表ï¼itemId å¿
ä¼ ï¼keyword/status å¯éï¼ */ |
| | | export function getSeedIndicatorRanges(params: { itemId: number; keyword?: string; status?: number }) { |
| | | return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges', { params }); |
| | | } |
| | | |
| | | /** 产åçæçè´¨æ£é¡¹ï¼äº§åé
ç½®ä¼å
ï¼æªé
ç½®æ¶åééç¨é»è®¤é¡¹ */ |
| | | export function getEffectiveSeedIndicatorRanges(itemId: number) { |
| | | return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges/effective', { params: { itemId } }); |
| | | } |
| | | |
| | | export function createSeedIndicatorRange(data: SeedIndicatorRangeSaveParams) { |
| | | return requestClient.post<number>('/mes/qc/seed-quality/ranges', data); |
| | | } |
| | | |
| | | export function updateSeedIndicatorRange(id: number, data: SeedIndicatorRangeSaveParams) { |
| | | return requestClient.put(`/mes/qc/seed-quality/ranges/${id}`, data); |
| | | } |
| | | |
| | | export function deleteSeedIndicatorRange(id: number) { |
| | | return requestClient.delete(`/mes/qc/seed-quality/ranges/${id}`); |
| | | } |
| | | |
| | | /** å°éç¨é»è®¤é¡¹å¤å¶å°æå®äº§åï¼è¿åå¤å¶æ¡æ° */ |
| | | export function copyDefaultSeedIndicatorRanges(itemId: number) { |
| | | return requestClient.post<number>('/mes/qc/seed-quality/ranges/copy-default', undefined, { params: { itemId } }); |
| | | } |
| | | |
| | | export interface SeedQualityPageParams { |
| | |
| | | pageSize?: number; |
| | | code?: string; |
| | | batchCode?: string; |
| | | /** çäº§æ¹æ¬¡å·ç²¾ç¡®å¹é
ï¼ææ¹æ¬¡å±å¼æç»æ¶ä½¿ç¨ï¼ */ |
| | | exactBatchCode?: string; |
| | | /** ä»
æ¥è¯¢æªç»å®çäº§æ¹æ¬¡çè®°å½ */ |
| | | unbound?: boolean; |
| | | result?: number; |
| | | workflowStatus?: number; |
| | | /** è´¨æ£çº¿ï¼1 å å·¥é¨ 2 éå®é¨ 0 æªåºåï¼å岿°æ®ï¼ */ |
| | | qcLine?: number; |
| | | /** ä¸å¡ç±»åï¼1 ç产å
¥åºè´¨æ£ 2 ç»éåéè´§è´¨æ£ */ |
| | | bizType?: number; |
| | | } |
| | | |
| | | /** æçäº§æ¹æ¬¡åç»çè´¨æ£è®°å½æ±æ» */ |
| | | export interface SeedQualityBatchGroup { |
| | | batchId?: number; |
| | | batchCode: string; |
| | | batchLabel: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | lineName?: string; |
| | | produceDate?: string; |
| | | totalCount: number; |
| | | passCount: number; |
| | | failCount: number; |
| | | processingCount: number; |
| | | pendingRetestCount: number; |
| | | pendingProcessReviewCount: number; |
| | | pendingCompanyReviewCount: number; |
| | | releasedCount: number; |
| | | scrappedCount: number; |
| | | lastInspectedAt?: string; |
| | | } |
| | | |
| | | export function getSeedQualityBatchPage(params: SeedQualityPageParams) { |
| | | return requestClient.get<{ list: SeedQualityBatchGroup[]; total: number }>('/mes/qc/seed-quality/batch-page', { params }); |
| | | } |
| | | |
| | | export interface SeedQualityRecord { |
| | | id: number; |
| | | code: string; |
| | | /** è´¨æ£çº¿ï¼1 å å·¥é¨ 2 éå®é¨ 0 æªåºåï¼å岿°æ®ï¼ */ |
| | | qcLine?: number; |
| | | /** ä¸å¡ç±»åï¼1 ç产å
¥åºè´¨æ£ 2 ç»éåéè´§è´¨æ£ */ |
| | | bizType?: number; |
| | | palletCode?: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | batchCode?: string; |
| | | result: number; |
| | | workflowStatus: number; |
| | | processInstanceId?: string; |
| | | companyProcessInstanceId?: string; |
| | | returnSalesId?: number; |
| | | returnSalesCode?: string; |
| | | retestResult?: number; |
| | | retestedAt?: string; |
| | | processApprovedAt?: string; |
| | | lineFinishedAt?: string; |
| | | companySubmittedAt?: string; |
| | | companyApprovedAt?: string; |
| | | reason?: string; |
| | | inspectorId?: number; |
| | | inspectedAt?: string; |
| | |
| | | } |
| | | export interface SeedQualityIndicator { |
| | | indicatorCode: string; |
| | | indicatorName?: string; |
| | | unit?: string; |
| | | value?: number; |
| | | minValue: number; |
| | | maxValue: number; |
| | | } |
| | | |
| | | /** 产åçæçè´¨æ£é¡¹ï¼è¯å«æ«ç 对象æ¶è¿åï¼ */ |
| | | export interface EffectiveIndicator { |
| | | code: string; |
| | | name: string; |
| | | unit?: string; |
| | | minValue: number; |
| | | maxValue: number; |
| | | } |
| | |
| | | code: string; |
| | | scanType: 'PALLET' | 'BATCH'; |
| | | batchCode?: string; |
| | | itemId?: number; |
| | | indicators?: EffectiveIndicator[]; |
| | | totalCount: number; |
| | | retestIds: number[]; |
| | | initialRecheckIds: number[]; |
| | |
| | | pendingCompanyIds?: number[]; |
| | | pendingCompanyFailIds?: number[]; |
| | | } |
| | | export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH') { |
| | | return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType }, suppressErrorMessage: true }); |
| | | export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH', qcLine: 1 | 2 = 1) { |
| | | return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType, qcLine }, suppressErrorMessage: true }); |
| | | } |
| | | export function submitSeedRetestScope(data: { |
| | | code: string; scanType: 'PALLET' | 'BATCH'; workflowStatus: 10 | 50; |
| | | code: string; scanType: 'PALLET' | 'BATCH'; qcLine: 1 | 2; workflowStatus: 10 | 50; |
| | | qualityIds: number[]; indicators: MesQcSeedQualityApi.Indicator[]; reason?: string; |
| | | }) { |
| | | return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/retest-scope', data, { suppressErrorMessage: true }); |
| | |
| | | } |
| | | export function approveSeedQualityBatch(stage: 'process' | 'company', data: { ids: number[]; approved: boolean; opinion: string }) { |
| | | return requestClient.post(`/mes/qc/seed-quality/${stage}-approve-batch`, data, { suppressErrorMessage: true }); |
| | | } |
| | | export function retrySeedQualityBpm(id: number) { |
| | | return requestClient.post('/mes/qc/seed-quality/retry-bpm', undefined, { params: { id } }); |
| | | } |
| | | |
| | | export namespace MesQcSeedQualityApi { |
| | |
| | | |
| | | export interface SubmitParams { |
| | | code: string; |
| | | /** å
¼å®¹å段ï¼ä¸æ¬¡æäº¤ä¼åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½ */ |
| | | qcLine?: 1 | 2; |
| | | batchCode?: string; |
| | | indicators: Indicator[]; |
| | | reason?: string; |
| | |
| | | return requestClient.post<number>('/mes/qc/seed-quality/submit', data); |
| | | } |
| | | |
| | | /** å线质æ£ï¼åä¸è¢ç å¨å å·¥é¨ãéå®é¨åæä¸æ¡è´¨æ£è®°å½ */ |
| | | export interface SeedQualityLineStatus { |
| | | id: number; |
| | | qcLine: number; |
| | | workflowStatus: number; |
| | | } |
| | | export interface SeedQualityScan { |
| | | code: string; businessCode?: string; batchCode?: string; |
| | | itemId?: number; itemCode?: string; itemName?: string; |
| | | indicators?: EffectiveIndicator[]; |
| | | inbound: boolean; canceled: boolean; qualityId?: number; result?: number; |
| | | qualityIds?: number[]; qualityLines?: SeedQualityLineStatus[]; |
| | | scanType?: 'BAG' | 'PALLET' | 'BATCH'; totalCount?: number; |
| | | pendingBagIds?: number[]; completedBagIds?: number[]; |
| | | } |
| | | export function resolveSeedQuality(code: string, scanType: 'BAG' | 'PALLET' | 'BATCH' = 'BAG') { |
| | | return requestClient.get<SeedQualityScan>('/mes/qc/seed-quality/resolve', { params: { code, scanType }, suppressErrorMessage: true }); |
| | | export function resolveSeedQuality(code: string, scanType: 'BAG' | 'PALLET' | 'BATCH' = 'BAG', qcLine: 1 | 2 = 1) { |
| | | return requestClient.get<SeedQualityScan>('/mes/qc/seed-quality/resolve', { params: { code, scanType, qcLine }, suppressErrorMessage: true }); |
| | | } |
| | | export function submitSeedQualityGroup(data: MesQcSeedQualityApi.SubmitParams & { scanType: 'BAG' | 'PALLET' | 'BATCH'; bagIds: number[] }) { |
| | | return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/submit-group', data, { suppressErrorMessage: true }); |
| | | } |
| | | |
| | | /** ç»éåéè´§è´¨æ£ï¼è§£æéè´§åå¾
è´¨æ£ä¿¡æ¯ */ |
| | | export interface SeedReturnQualityInfo { |
| | | returnSalesId: number; |
| | | returnSalesCode: string; |
| | | returnSalesStatus: number; |
| | | itemId?: number; |
| | | batchId?: number; |
| | | batchCode?: string; |
| | | indicators?: EffectiveIndicator[]; |
| | | qualityId?: number; |
| | | result?: number; |
| | | workflowStatus?: number; |
| | | } |
| | | export function resolveSeedReturnQuality(returnSalesId: number) { |
| | | return requestClient.get<SeedReturnQualityInfo>('/mes/qc/seed-quality/return/resolve', { params: { returnSalesId }, suppressErrorMessage: true }); |
| | | } |
| | | export function submitSeedReturnQuality(data: { returnSalesId: number; indicators: MesQcSeedQualityApi.Indicator[]; reason?: string }) { |
| | | return requestClient.post<number>('/mes/qc/seed-quality/return/submit', data, { suppressErrorMessage: true }); |
| | | } |
| | | export function approveSeedReturnQuality(data: { id: number; approved: boolean; opinion: string }) { |
| | | return requestClient.post('/mes/qc/seed-quality/return/approve', data, { suppressErrorMessage: true }); |
| | | } |
| | |
| | | |
| | | export interface SalesTarget { |
| | | salesCode?: string; |
| | | shipmentId?: number; |
| | | salesDate?: string; |
| | | salesOrderCode?: string; |
| | | clientName?: string; |
| | |
| | | return requestClient.get<MesTraceBatchApi.Detail>('/mes/trace/batch/detail', { |
| | | params: { code }, |
| | | }); |
| | | } |
| | | } |
| | |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | | specification?: string; // è§æ ¼åå· |
| | | itemTypeName?: string; // ç©æç±»å«åç§° |
| | | unitMeasureName?: string; // 计éåä½åç§° |
| | | batchId?: number; // æ¹æ¬¡ç¼å· |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | |
| | | ); |
| | | } |
| | | |
| | | export function getPickingStocks(lineId: number) { |
| | | return requestClient.get<MesWmMaterialStockApi.MaterialStock[]>('/mes/wm/material-stock/picking-list', { params: { lineId } }); |
| | | } |
| | | |
| | | /** æ¥è¯¢åºåå°è´¦è¯¦æ
*/ |
| | | export interface MaterialStockBagDetail { |
| | | id: number; |
| | |
| | | frozen?: boolean; |
| | | inboundTime?: string; |
| | | outboundTime?: string; |
| | | outboundBizType?: number; |
| | | pickingStatus?: 'PICKED' | 'RESERVED'; |
| | | } |
| | | |
| | | export function getMaterialStockBagDetails(params: { |
| | | bagCode?: string; |
| | | includeHistory?: boolean; |
| | | materialStockId: number; |
| | | pageNo?: number; |
| | | pageSize?: number; |
| | |
| | | id?: number; // æç»ç¼å· |
| | | lineId?: number; // åºåºåè¡ç¼å· |
| | | salesId?: number; // åºåºåç¼å· |
| | | shipmentId?: number; // åè´§è®°å½ç¼å· |
| | | itemId?: number; // ç©æç¼å· |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | |
| | | ); |
| | | } |
| | | |
| | | export function createMobileManualDetail(data: MesWmProductSalesDetailApi.ProductSalesDetail) { |
| | | return requestClient.post<number>('/mes/wm/product-sales-detail/create-mobile-manual', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹éå®åºåºæç» */ |
| | | export function updateProductSalesDetail( |
| | | data: MesWmProductSalesDetailApi.ProductSalesDetail, |
| | |
| | | shippingNumber?: string; // è¿è¾åå· |
| | | status?: number; // åæ®ç¶æ |
| | | remark?: string; // 夿³¨ |
| | | closeReason?: string; |
| | | partiallyShipped?: boolean; |
| | | stockState?: number; // åºåç¶æ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | |
| | | quantity?: number; |
| | | scanType?: 'BAG' | 'PALLET' | 'BATCH'; |
| | | bagIds?: number[]; |
| | | lineId?: number; |
| | | materialStockId?: number; |
| | | manual?: boolean; |
| | | } |
| | | |
| | | export interface Shipment { |
| | | id: number; |
| | | salesId: number; |
| | | status: number; |
| | | carrier?: string; |
| | | shippingNumber?: string; |
| | | outboundTime?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | export interface PickingBag { |
| | |
| | | code: string; |
| | | businessCode?: string; |
| | | palletId?: number; |
| | | materialStockId?: number; |
| | | selectable: boolean; |
| | | unavailableReason?: string; |
| | | } |
| | |
| | | return requestClient.get<MesWmProductSalesApi.ProductSales>( |
| | | `/mes/wm/product-sales/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | export function getProductSalesShipments(id: number) { |
| | | return requestClient.get<MesWmProductSalesApi.Shipment[]>('/mes/wm/product-sales/shipments', { params: { id } }); |
| | | } |
| | | |
| | | /** æ°å¢éå®åºåºå */ |
| | |
| | | return requestClient.put(`/mes/wm/product-sales/cancel?id=${id}`); |
| | | } |
| | | |
| | | export function closeProductSalesRemaining(id: number, reason: string) { |
| | | return requestClient.put('/mes/wm/product-sales/close-remaining', undefined, { params: { id, reason } }); |
| | | } |
| | | |
| | | export function cancelProductSalesShipment(id: number) { |
| | | return requestClient.put(`/mes/wm/product-sales/cancel-shipment?id=${id}`); |
| | | } |
| | | |
| | | /** æ ¡éªéå®åºåºåæ£è´§æ°éæ¯å¦ä¸åºåºæ°éä¸è´ */ |
| | | export function checkProductSalesQuantity(id: number) { |
| | | return requestClient.get<boolean>( |
| | |
| | | specification?: string; // è§æ ¼åå· |
| | | unitMeasureName?: string; // åä½ |
| | | quantity?: number; // åºåºæ°é |
| | | shippedQuantity?: number; |
| | | remainingQuantity?: number; |
| | | pickedQuantity?: number; // å·²æ£è´§æ°é |
| | | batchId?: number; // æ¹æ¬¡ç¼å· |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | |
| | | itemId?: number; // ç©æID |
| | | itemCode?: string; // ç©æç¼ç |
| | | itemName?: string; // ç©æåç§° |
| | | specification?: string; // è§æ ¼åå· |
| | | itemTypeName?: string; // ç©æç±»å«åç§° |
| | | quantity?: number; // å卿°é |
| | | batchId?: number; // æ¹æ¬¡ID |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | palletCode?: string; // åºåºæç»ä¸çæçç |
| | | bagCode?: string; // è¢ç |
| | | shipmentId?: number; |
| | | warehouseId?: number; // ä»åºID |
| | | warehouseName?: string; // ä»åºåç§° |
| | | locationId?: number; // åºåºID |
| | |
| | | /** å页æ¥è¯¢åæ° */ |
| | | export interface PageParams extends PageParam { |
| | | itemId?: number; |
| | | batchCodeExact?: string; |
| | | bizCodeExact?: string; |
| | | shipmentId?: number; |
| | | batchCode?: string; |
| | | warehouseId?: number; |
| | | locationId?: number; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace SystemAppearanceApi { |
| | | /** ç³»ç»å¤è§ä¿¡æ¯ */ |
| | | export interface Appearance { |
| | | logoBlobId?: number; |
| | | logoUrl?: string; |
| | | loginBackgroundBlobId?: number; |
| | | loginBackgroundUrl?: string; |
| | | } |
| | | |
| | | /** ä¿åå¤è§è®¾ç½®è¯·æ±ï¼ä¼ null 表示æ¸
é¤ */ |
| | | export interface SaveReq { |
| | | logoBlobId?: null | number; |
| | | loginBackgroundBlobId?: null | number; |
| | | } |
| | | |
| | | /** å
Œ
±ä¸ä¼ è¿åçæä»¶ä¿¡æ¯ */ |
| | | export interface PublicBlob { |
| | | id: number; |
| | | previewURL?: string; |
| | | downloadURL?: string; |
| | | originalFilename?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾ç³»ç»å¤è§ */ |
| | | export function getSystemAppearance() { |
| | | return requestClient.get<SystemAppearanceApi.Appearance>( |
| | | '/system/appearance/get', |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åç³»ç»å¤è§ */ |
| | | export function saveSystemAppearance(data: SystemAppearanceApi.SaveReq) { |
| | | return requestClient.post<boolean>('/system/appearance/save', data); |
| | | } |
| | | |
| | | /** ä¸ä¼ å¤è§å¾çï¼æ°¸ä¹
ææï¼æªç»å½ä¹å¯è®¿é®ï¼ */ |
| | | export function uploadAppearanceImage(file: File) { |
| | | const formData = new FormData(); |
| | | formData.append('files', file); |
| | | return requestClient.post<SystemAppearanceApi.PublicBlob[]>( |
| | | '/system/storage-blob/public-upload', |
| | | formData, |
| | | { headers: { 'Content-Type': 'multipart/form-data' } }, |
| | | ); |
| | | } |
| | |
| | | |
| | | import { $t } from '#/locales'; |
| | | import { terminalType } from '#/mobile/terminal'; |
| | | import { appearanceLogoUrl } from '#/utils/system-appearance'; |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | |
| | |
| | | <AuthPageLayout |
| | | v-else |
| | | :app-name="appName" |
| | | :brand-logo="appearanceLogoUrl" |
| | | :logo="logo" |
| | | :logo-dark="logoDark" |
| | | page-description="æ°åèµè½åä¸ Â· ç§æå¼é¢æªæ¥" |
| | |
| | | import { unmountGlobalLoading } from './packages/utils/src'; |
| | | |
| | | import { overridesPreferences, preferencesExtension } from './preferences'; |
| | | import { loadSystemAppearance } from './utils/system-appearance'; |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | declare const __APP_FAVICON__: string; |
| | |
| | | } |
| | | } |
| | | |
| | | // å è½½æå¡ç«¯é
ç½®çå¤è§ï¼Logoãç»å½èæ¯å¾ï¼ï¼å¤±è´¥æ¶ä¿çé»è®¤æ ·å¼ |
| | | await loadSystemAppearance(); |
| | | |
| | | // å¯å¨åºç¨å¹¶æè½½ |
| | | // vueåºç¨ä¸»è¦é»è¾åè§å¾ |
| | | const { bootstrap } = await import('./bootstrap'); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import duplicateScanAudio from './duplicate-scan.wav'; |
| | | import duplicateBindAudio from './duplicate-bind.wav'; |
| | | import duplicateOutboundAudio from './duplicate-outbound.wav'; |
| | | import duplicateQualityAudio from './duplicate-quality.wav'; |
| | | |
| | | let audio: HTMLAudioElement | undefined; |
| | | export type ScanVoiceOperation = 'inbound' | 'outbound' | 'quality' | 'bind'; |
| | | const sources: Record<ScanVoiceOperation, string> = { |
| | | inbound: duplicateScanAudio, |
| | | outbound: duplicateOutboundAudio, |
| | | quality: duplicateQualityAudio, |
| | | bind: duplicateBindAudio, |
| | | }; |
| | | |
| | | export async function speakDuplicateScan(operation: ScanVoiceOperation = 'inbound'): Promise<boolean> { |
| | | try { |
| | | if (!audio) { |
| | | audio = new Audio(); |
| | | audio.preload = 'auto'; |
| | | } |
| | | audio.pause(); |
| | | if (audio.getAttribute('src') !== sources[operation]) audio.src = sources[operation]; |
| | | audio.currentTime = 0; |
| | | await audio.play(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | |
| | | |
| | | interface Props { |
| | | appName?: string; |
| | | brandLogo?: string; |
| | | logo?: string; |
| | | logoDark?: string; |
| | | pageTitle?: string; |
| | |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | appName: '', |
| | | brandLogo: '', |
| | | copyright: true, |
| | | logo: '', |
| | | logoDark: '', |
| | |
| | | <div class="agri-brand"> |
| | | <div class="flex items-center gap-4"> |
| | | <img |
| | | src="/images/tahe-logo.png" |
| | | :src="brandLogo || '/images/tahe-logo.png'" |
| | | alt="塿²³ç§ä¸" |
| | | class="h-16 w-auto shrink-0 rounded-xl bg-white/95 p-1.5 shadow-lg" |
| | | /> |
| | |
| | | min-height: 100dvh; |
| | | overflow-y: auto; |
| | | background-color: #124b39; |
| | | background-image: url('/agriculture-login-bg.png'); |
| | | background-image: var(--login-bg-image, url('/agriculture-login-bg.png')); |
| | | background-position: center; |
| | | background-size: cover; |
| | | isolation: isolate; |
| | |
| | | .dark .login-background { |
| | | background-image: |
| | | linear-gradient(90deg, rgba(2, 26, 20, 0.9), rgba(2, 26, 20, 0.62)), |
| | | url('/agriculture-login-bg.png'); |
| | | var(--login-bg-image, url('/agriculture-login-bg.png')); |
| | | } |
| | | |
| | | .dark .agri-form-panel { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { preferencesManager } from '@vben/preferences'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | /** ç»å½é¡µåç Logoï¼æªé
ç½®æ¶ä¸ºç©º */ |
| | | export const appearanceLogoUrl = ref(''); |
| | | /** ç»å½é¡µèæ¯å¾ï¼æªé
ç½®æ¶ä¸ºç©º */ |
| | | export const appearanceLoginBackgroundUrl = ref(''); |
| | | |
| | | /** 馿¬¡åºç¨æ¶è®°å½çé»è®¤ Logoï¼ç¨äºæ¸
é¤é
ç½®ååé */ |
| | | let defaultLogoSource: string | undefined; |
| | | |
| | | interface AppearancePayload { |
| | | logoUrl?: string; |
| | | loginBackgroundUrl?: string; |
| | | } |
| | | |
| | | /** |
| | | * åºç¨å¤è§è®¾ç½®ï¼æ´æ°é¦é¡µä¾§è¾¹æ Logo ä¸ç»å½é¡µæ ·å¼ |
| | | */ |
| | | export function applyAppearance(appearance: AppearancePayload) { |
| | | appearanceLogoUrl.value = appearance.logoUrl ?? ''; |
| | | appearanceLoginBackgroundUrl.value = appearance.loginBackgroundUrl ?? ''; |
| | | defaultLogoSource ??= preferencesManager.getPreferences().logo.source; |
| | | preferencesManager.updatePreferences({ |
| | | logo: { source: appearanceLogoUrl.value || defaultLogoSource }, |
| | | }); |
| | | if (appearanceLoginBackgroundUrl.value) { |
| | | document.documentElement.style.setProperty( |
| | | '--login-bg-image', |
| | | `url("${appearanceLoginBackgroundUrl.value}")`, |
| | | ); |
| | | } else { |
| | | document.documentElement.style.removeProperty('--login-bg-image'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * å¯å¨æ¶å 载系ç»å¤è§ï¼å¤±è´¥æ¶éé»ä¿çé»è®¤æ ·å¼ |
| | | */ |
| | | export async function loadSystemAppearance() { |
| | | try { |
| | | const controller = new AbortController(); |
| | | const timer = setTimeout(() => controller.abort(), 2000); |
| | | const baseURL = ((import.meta.env.VITE_GLOB_API_URL as string) || '').replace( |
| | | /\/+$/, |
| | | '', |
| | | ); |
| | | const response = await fetch(`${baseURL}/system/appearance/get`, { |
| | | headers: { 'tenant-id': '1' }, |
| | | signal: controller.signal, |
| | | }); |
| | | clearTimeout(timer); |
| | | if (!response.ok) { |
| | | return; |
| | | } |
| | | const result = await response.json(); |
| | | if (result?.code !== 0 || !result.data) { |
| | | return; |
| | | } |
| | | applyAppearance(result.data); |
| | | } catch { |
| | | // å¤è§å 载失败ä¸å½±ååºç¨å¯å¨ |
| | | } |
| | | } |
| | |
| | | import { useAuthStore } from '#/store'; |
| | | import { selectTerminal, terminalType } from '#/mobile/terminal'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import { appearanceLogoUrl } from '#/utils/system-appearance'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | defineOptions({ name: 'Login' }); |
| | |
| | | const verifyRef = ref(); |
| | | |
| | | const captchaType = 'blockPuzzle'; // éªè¯ç ç±»åï¼'blockPuzzle' | 'clickWord' |
| | | |
| | | /** åç Logoï¼ä¼å
ä½¿ç¨æå¡ç«¯é
ç½®ï¼å¦å使ç¨é»è®¤å¾ç */ |
| | | const brandLogoSrc = computed( |
| | | () => appearanceLogoUrl.value || '/images/tahe-logo.png', |
| | | ); |
| | | |
| | | /** æ»åéªè¯ç 尺寸ï¼ç§»å¨ç«¯æè§å£å®½åº¦æ¶ç¼©ï¼é¿å
è¶
åºå±å¹æ æ³æå¨ */ |
| | | const captchaImgSize = computed(() => { |
| | |
| | | <div> |
| | | <div class="login-brand"> |
| | | <img |
| | | src="/images/tahe-logo.png" |
| | | :src="brandLogoSrc" |
| | | alt="塿²³ç§ä¸" |
| | | class="login-brand-logo" |
| | | /> |
| | |
| | | processDefinition?.formType === BpmModelFormType.CUSTOM |
| | | " |
| | | > |
| | | <BusinessFormComponent :id="processInstance?.businessKey" /> |
| | | <BusinessFormComponent |
| | | :id="processInstance?.businessKey" |
| | | :process-definition-key="processDefinition?.key" |
| | | /> |
| | | </div> |
| | | </Col> |
| | | <Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8"> |
| | |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { AreaCascader } from '#/components/area'; |
| | | import CrmCustomerSelect from '#/components/crm-customer-select.vue'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å®¢æ·åç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerName', |
| | | label: '客æ·å§å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å®¢æ·å§å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'socialCreditCode', |
| | | label: '社ä¼ä¿¡ç¨ä»£ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'idCardNo', |
| | | label: '身份è¯å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èº«ä»½è¯å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'filingSerialNo', |
| | | label: '夿¡æµæ°´å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ¡æµæ°´å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'bankAccountName', |
| | | label: 'é¶è¡è´¦æ·åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¶è¡è´¦æ·åç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'region', |
| | | label: 'æå±åºå', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æå±åºå', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'parentDealerId', |
| | | label: 'æå±ä¸çº§ç»éå', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æå±ä¸çº§ç»éå', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'customerName', |
| | | label: '客æ·å§å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å®¢æ·å§å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'socialCreditCode', |
| | | label: '社ä¼ä¿¡ç¨ä»£ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'idCardNo', |
| | | label: '身份è¯å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èº«ä»½è¯å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'filingSerialNo', |
| | | label: '夿¡æµæ°´å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ¡æµæ°´å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'region', |
| | | label: 'æå±åºå', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æå±åºå', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'parentDealerId', |
| | | label: 'æå±ä¸çº§ç»éå', |
| | | component: markRaw(CrmCustomerSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æå±ä¸çº§ç»éå', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'mobile', |
| | | label: 'ææºå·', |
| | | component: 'Input', |
| | |
| | | slots: { default: 'name' }, |
| | | }, |
| | | { |
| | | field: 'customerName', |
| | | title: '客æ·å§å', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'socialCreditCode', |
| | | title: '社ä¼ä¿¡ç¨ä»£ç ', |
| | | minWidth: 180, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'idCardNo', |
| | | title: '身份è¯å·', |
| | | minWidth: 180, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'filingSerialNo', |
| | | title: '夿¡æµæ°´å·', |
| | | minWidth: 160, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'bankAccountName', |
| | | title: 'é¶è¡è´¦æ·åç§°', |
| | | minWidth: 180, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'region', |
| | | title: 'æå±åºå', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'parentDealerName', |
| | | title: 'æå±ä¸çº§ç»éå', |
| | | minWidth: 160, |
| | | formatter: ({ cellValue }) => cellValue || '-', |
| | | }, |
| | | { |
| | | field: 'source', |
| | | title: 'å®¢æ·æ¥æº', |
| | | minWidth: 100, |
| | |
| | | label: '客æ·åç§°', |
| | | }, |
| | | { |
| | | field: 'customerName', |
| | | label: '客æ·å§å', |
| | | }, |
| | | { |
| | | field: 'socialCreditCode', |
| | | label: '社ä¼ä¿¡ç¨ä»£ç ', |
| | | }, |
| | | { |
| | | field: 'idCardNo', |
| | | label: '身份è¯å·', |
| | | }, |
| | | { |
| | | field: 'filingSerialNo', |
| | | label: '夿¡æµæ°´å·', |
| | | }, |
| | | { |
| | | field: 'bankAccountName', |
| | | label: 'é¶è¡è´¦æ·åç§°', |
| | | }, |
| | | { |
| | | field: 'region', |
| | | label: 'æå±åºå', |
| | | }, |
| | | { |
| | | field: 'parentDealerName', |
| | | label: 'æå±ä¸çº§ç»éå', |
| | | }, |
| | | { |
| | | field: 'source', |
| | | label: 'å®¢æ·æ¥æº', |
| | | render: (val) => |
| | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { ProProcessSelect } from '#/views/mes/process-design/process/components'; |
| | | |
| | | import { MdWorkshopSelect } from './components'; |
| | |
| | | allowClear: true, |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('locationId', undefined); |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const list = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options: list, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const list = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options: list, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessCode', label: 'ä¸å¡ç ', component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥åºå
¥åºä¸å¡ç ' }, |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | | label: 'æ¹æ¬¡å·', |
| | | component: 'Input', |
| | |
| | | fixed: 'left', |
| | | minWidth: 220, |
| | | }, |
| | | { field: 'businessCode', title: 'ä¸å¡ç ï¼åºå
¥åºï¼', minWidth: 210 }, |
| | | { |
| | | field: 'bagNo', |
| | | title: 'è¢åºå·', |
| | |
| | | deleteBagCodesByUuid, |
| | | exportBagCodeExcel, |
| | | exportBagQr, |
| | | exportBagBusinessQr, |
| | | generateBagBusinessCodes, |
| | | getBagCodePage, |
| | | bindBagCodeBatch, |
| | | unbindBagCodePallet, |
| | |
| | | import RawGenerateModal from "./modules/raw-generate-form.vue"; |
| | | import ReplaceModal from "./modules/replace-form.vue"; |
| | | import QrTraceModal from "../components/qr-trace-modal.vue"; |
| | | import BusinessQrModal from "../components/business-qr.vue"; |
| | | import QrExportTaskModal from '../components/qr-export-task-modal.vue'; |
| | | |
| | | defineOptions({ name: "MesProBagCode" }); |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [BusinessQrViewModal, businessQrApi] = useVbenModal({ |
| | | connectedComponent: BusinessQrModal, destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleBusinessQr(row: MesProBagCodeApi.BagCode) { |
| | | if (!row.businessCode) { message.warning('请å
è¡¥çæä¸å¡ç '); return; } |
| | | businessQrApi.setData({ code: row.businessCode }).open(); |
| | | } |
| | | |
| | | async function handleGenerateBusinessCodes() { |
| | | const records = getSelectedRecords(); |
| | | if (!records) return; |
| | | if (records.length > 200) { message.warning('忬¡æå¤è¡¥çæ 200 æ¡'); return; } |
| | | if (records.some(row => row.status === MesProBagCodeStatusEnum.CANCELED)) { |
| | | message.warning('å·²ä½åºè¢ç ä¸å¯è¡¥çæä¸å¡ç '); return; |
| | | } |
| | | const ids = records.map(row => row.id).filter((id): id is number => id != null); |
| | | if (ids.length !== records.length) { message.warning('æéè¢ç 缺å°ç¼å·'); return; } |
| | | const count = await generateBagBusinessCodes(ids); |
| | | message.success(`å·²è¡¥çæ ${count} 份ä¸å¡ç ï¼å·²æç å¼ä¿æä¸å`); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | |
| | | } |
| | | |
| | | /** 导åºäºç»´ç ZIPï¼æå½åæ¥è¯¢æ¡ä»¶ççæç»/æ¹æ¬¡ï¼ */ |
| | | async function handleExportQr(kind: 'trace' | 'business' = 'trace') { |
| | | async function handleExportQr() { |
| | | const values = await gridApi.formApi.getValues(); |
| | | const params: { uuid?: string; batchId?: number } = {}; |
| | | if (values.uuid) { |
| | |
| | | message.warning("请å
æçæç»ææ¹æ¬¡çéåå导åºäºç»´ç "); |
| | | return; |
| | | } |
| | | const bizType = kind === 'business' ? 'BAG_BUSINESS' : 'BAG_LABEL'; |
| | | const bizType = 'BAG_LABEL'; |
| | | const count = await countQrExport({ bizType, ...params }); |
| | | if (count.mode === 'ASYNC') { |
| | | const task = await submitQrExport({ bizType, ...params }); |
| | |
| | | message.success(`å·²æäº¤ ${count.totalCount} å¼ äºç»´ç çåå°å¯¼åºä»»å¡`); |
| | | return; |
| | | } |
| | | const data = await (kind === 'business' ? exportBagBusinessQr : exportBagQr)(params); |
| | | downloadFileFromBlobPart({ fileName: kind === 'business' ? 'è¢ç ä¸å¡äºç»´ç .zip' : 'è¢ç 追溯äºç»´ç .zip', source: data }); |
| | | const data = await exportBagQr(params); |
| | | downloadFileFromBlobPart({ fileName: 'è¢ç 追溯äºç»´ç .zip', source: data }); |
| | | } |
| | | |
| | | /** 导åºè¢ç Excelï¼äº¤ä»å°å·ï¼ï¼å¯¼åºåè¢ç ç¶æå为已å°å·ï¼éå·æ°å表 */ |
| | |
| | | <BindPalletFormModal @success="handleRefresh" /> |
| | | <ReplaceFormModal @success="handleReplaceSuccess" /> |
| | | <QrTraceQueryModal /> |
| | | <BusinessQrViewModal /> |
| | | <QrExportTaskModal ref="qrExportTaskModal" /> |
| | | <Grid table-title="è¢ç å表ï¼ä¸è¢ä¸ç ï¼"> |
| | | <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">è¢ç å表ï¼ä¸è¢ä¸ç ï¼</span></template> |
| | |
| | | { label: 'å½å
¥æ¹æ¬¡', auth: ['mes:pro-bag-code:update'], onClick: handleBindBatch }, |
| | | { label: 'ç»å®æç', auth: ['mes:pro-bag-code:update'], onClick: handleBindPallet }, |
| | | { label: 'æ«ç 追溯', icon: ACTION_ICON.SEARCH, auth: ['mes:pro-bag-code:query'], onClick: handleTrace }]" /> |
| | | <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '追溯äºç»´ç ', auth: ['mes:pro-bag-code:export'], onClick: () => handleExportQr('trace') }, |
| | | { label: 'ä¸å¡äºç»´ç ', auth: ['mes:pro-bag-code:export'], onClick: () => handleExportQr('business') }, |
| | | <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '追溯äºç»´ç ', auth: ['mes:pro-bag-code:export'], onClick: () => handleExportQr() }, |
| | | { label: 'Excel æ°æ®å
', auth: ['mes:pro-bag-code:export'], onClick: handleExport }]"> |
| | | <template #more><Button>å¯¼åº <IconifyIcon icon="lucide:chevron-down" /></Button></template> |
| | | </TableAction> |
| | | <Button @click="qrExportTaskModal?.showLast()">åå°å¯¼åºä»»å¡</Button> |
| | | <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: 'çæåå§è¢ç ', auth: ['mes:pro-bag-code:create'], onClick: handleRawGenerate }, |
| | | { label: 'è¡¥çæä¸å¡ç ', auth: ['mes:pro-bag-code:create'], onClick: handleGenerateBusinessCodes }, |
| | | { label: 'ç ´ææ¿æ¢', auth: ['mes:pro-bag-code:replace'], onClick: handleReplace }, |
| | | { label: 'è§£ç»æç', auth: ['mes:pro-bag-code:update'], onClick: handleUnbindPallet }]"> |
| | | <template #more><Button>æ´å¤æä½ <IconifyIcon icon="lucide:chevron-down" /></Button></template> |
| | |
| | | type: 'link', |
| | | auth: ['mes:pro-bag-code:query'], |
| | | onClick: handleViewQr.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä¸å¡äºç»´ç ', type: 'link', auth: ['mes:pro-bag-code:query'], |
| | | ifShow: !!row.businessCode, onClick: handleBusinessQr.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | existingBinding?: BindingMode, |
| | | ): VbenFormSchema[] { |
| | | const disabled = formType === 'detail'; |
| | | // æ¹æ¬¡å·ä»
卿°å¢ãè¡¥å½æ¶å
许人工è¾å
¥ï¼ç空ç±å端æã年份+3使µæ°´ãèªå¨çæ |
| | | const codeEditable = formType === 'create' || formType === 'fill'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | label: 'æ¹æ¬¡å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'è¡¥å½åèªå¨çæ', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'create', |
| | | disabled: !codeEditable, |
| | | maxlength: 7, |
| | | placeholder: codeEditable ? 'ç空èªå¨çæï¼å¦ 2026001' : 'æ¹æ¬¡å·èªå¨çæ', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { fieldName: 'businessCode', label: 'ä¸å¡ç ', component: 'Input', componentProps: { allowClear: true, placeholder: '请è¾å
¥ä½ä¸æ«ç ä¸å¡ç ' } }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'æ¹æ¬¡å·', |
| | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesProBatchApi.Batch>['columns'] { |
| | | return [ |
| | | { field: 'businessCode', title: 'ä¸å¡ç ï¼ä½ä¸æ«ç ï¼', minWidth: 210 }, |
| | | { |
| | | field: 'code', |
| | | title: 'æ¹æ¬¡å·', |
| | |
| | | minWidth: 200, |
| | | slots: { default: 'code' }, |
| | | }, |
| | | { |
| | | field: 'palletCodes', |
| | | title: 'æçç ', |
| | | minWidth: 200, |
| | | showOverflow: 'tooltip', |
| | | slots: { default: 'palletCodes' }, |
| | | }, |
| | | { field: 'itemName', title: 'åç§åç§°', minWidth: 150 }, |
| | | { field: 'itemCode', title: 'åç§ç¼ç ', minWidth: 150 }, |
| | | { |
| | |
| | | deleteBatch, |
| | | exportBatchExcel, |
| | | exportBatchQr, |
| | | exportBatchBusinessQr, |
| | | generateBatchBusinessCodes, |
| | | getBatchPage, |
| | | updateBatchStatus, |
| | | } from "#/api/mes/pro/batch"; |
| | |
| | | import Form from "./modules/form.vue"; |
| | | import PreGenerateModal from "./modules/pre-generate.vue"; |
| | | import QrTraceModal from "../components/qr-trace-modal.vue"; |
| | | import BusinessQrModal from "../components/business-qr.vue"; |
| | | import QrExportTaskModal from '../components/qr-export-task-modal.vue'; |
| | | |
| | | const qrExportTaskModal = ref<InstanceType<typeof QrExportTaskModal>>(); |
| | |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | const [BusinessQrViewModal, businessQrApi] = useVbenModal({ connectedComponent: BusinessQrModal, destroyOnClose: true }); |
| | | function handleBusinessQr(row: MesProBatchApi.Batch) { |
| | | businessQrApi.setData({ code: row.businessCode }).open(); |
| | | } |
| | | async function handleGenerateBusiness(row: MesProBatchApi.Batch) { |
| | | if (row.id == null) return; |
| | | const count = await generateBatchBusinessCodes([row.id]); |
| | | message.success(`å·²è¡¥çæ ${count} 份ä¸å¡ç ï¼å·²æç å¼ä¿æä¸å`); handleRefresh(); |
| | | } |
| | | async function handleExportBusiness(row: MesProBatchApi.Batch) { |
| | | if (row.id == null) return; |
| | | const params = { bizType: 'BATCH_BUSINESS' as const, batchId: row.id }; |
| | | const count = await countQrExport(params); |
| | | if (count.mode === 'ASYNC') { |
| | | qrExportTaskModal.value?.show(await submitQrExport(params)); |
| | | message.success(`å·²æäº¤ ${count.totalCount} å¼ äºç»´ç çåå°å¯¼åºä»»å¡`); |
| | | return; |
| | | } |
| | | const data = await exportBatchBusinessQr(row.id); |
| | | downloadFileFromBlobPart({ fileName: 'æ¹æ¬¡ä¸æçä¸å¡äºç»´ç .zip', source: data }); |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | |
| | | <FormModal @success="handleRefresh" /> |
| | | <PreGenerateModalApi @success="handlePreGenerateSuccess" /> |
| | | <QrTraceQueryModal /> |
| | | <BusinessQrViewModal /> |
| | | <QrExportTaskModal ref="qrExportTaskModal" /> |
| | | <Grid table-title="çäº§æ¹æ¬¡å表"> |
| | | <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">çäº§æ¹æ¬¡å表</span></template> |
| | |
| | | {{ getDisplayCode(row) }} |
| | | </Button> |
| | | <Tag v-if="!row.code" color="warning">å¾
è¡¥å½</Tag> |
| | | </template> |
| | | <template #palletCodes="{ row }"> |
| | | <span v-if="row.palletCodes?.length">{{ row.palletCodes.join(', ') }}</span> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction :actions="[ |
| | |
| | | }, |
| | | ]" |
| | | :drop-down-actions="[ |
| | | { label: 'ä¸å¡äºç»´ç ', auth: ['mes:pro-batch:query'], ifShow: !!row.businessCode, onClick: () => handleBusinessQr(row) }, |
| | | { label: 'è¡¥çæä¸å¡ç ', auth: ['mes:pro-batch:create'], ifShow: !row.businessCode && row.status !== MesProBatchStatusEnum.CANCELED, onClick: () => handleGenerateBusiness(row) }, |
| | | { label: 'å¯¼åºæ´å¥ä¸å¡äºç»´ç ', auth: ['mes:pro-batch:export'], ifShow: row.status !== MesProBatchStatusEnum.CANCELED, onClick: () => handleExportBusiness(row) }, |
| | | { |
| | | label: 'å¼å§ç产', |
| | | auth: ['mes:pro-batch:update'], |
| | |
| | | // æ¸
ç©ºä¸æ¬¡æå¼æ¶å¾éçæç/å¨è¢ï¼é¿å
æ®çå¼è¯¯è§¦åãæ¹æ¬¡è¿å
¥ç产ä¸ã |
| | | await formApi.setFieldValue("palletIds", undefined); |
| | | await formApi.setFieldValue("tonBagIds", undefined); |
| | | // æ¸
ç©ºä¸æ¬¡æå¼æ¶äººå·¥è¾å
¥çæ¹æ¬¡å·ï¼ç¼è¾/è¡¥å½åºæ¯éåç±æ¥å£æ°æ®åå¡« |
| | | await formApi.setFieldValue("code", undefined); |
| | | void loadPalletPage(true); |
| | | void loadTonBagPage(true); |
| | | } |
| | |
| | | label: "追溯ç ", |
| | | span: 2, |
| | | }, |
| | | { field: 'businessCode', label: 'ä¸å¡ç ï¼åºå
¥åºï¼', span: 2 }, |
| | | { |
| | | field: "bagNo", |
| | | label: "è¢åºå·", |
| | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { fieldName: 'businessCode', label: 'ä¸å¡ç ', component: 'Input', componentProps: { allowClear: true, placeholder: '请è¾å
¥åºå
¥åºä¸å¡ç ' } }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'æçç ', |
| | |
| | | export function useGridColumns(): VxeTableGridOptions<MesProPalletApi.Pallet>['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 50 }, |
| | | { field: 'businessCode', title: 'ä¸å¡ç ï¼åºå
¥åºï¼', minWidth: 210 }, |
| | | { |
| | | field: 'code', |
| | | title: 'æçç ', |
| | |
| | | exportPalletExcel, |
| | | exportPalletQr, |
| | | exportPalletQrByIds, |
| | | exportPalletBusinessQr, |
| | | generatePalletBusinessCodes, |
| | | getPalletPage, |
| | | } from "#/api/mes/pro/pallet"; |
| | | import { IconifyIcon } from "@vben/icons"; |
| | |
| | | import BindBatchModal from "./modules/bind-batch.vue"; |
| | | import GenerateModal from "./modules/generate-form.vue"; |
| | | import QrTraceModal from "../components/qr-trace-modal.vue"; |
| | | import BusinessQrModal from "../components/business-qr.vue"; |
| | | import QrExportTaskModal from '../components/qr-export-task-modal.vue'; |
| | | |
| | | const qrExportTaskModal = ref<InstanceType<typeof QrExportTaskModal>>(); |
| | |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | const [BusinessQrViewModal, businessQrApi] = useVbenModal({ connectedComponent: BusinessQrModal, destroyOnClose: true }); |
| | | function handleBusinessQr(row: MesProPalletApi.Pallet) { |
| | | businessQrApi.setData({ code: row.businessCode }).open(); |
| | | } |
| | | function getBusinessIds() { |
| | | const records = [...(gridApi.grid.getCheckboxReserveRecords?.() || []), ...(gridApi.grid.getCheckboxRecords?.() || [])] as MesProPalletApi.Pallet[]; |
| | | const ids = [...new Set(records.map(row => row.id).filter((id): id is number => id != null))]; |
| | | if (!ids.length || ids.length > 200) { message.warning('请å¾é 1 è³ 200 æ¡æçè®°å½'); return; } |
| | | if (records.some(row => row.status === 20)) { message.warning('å·²ä½åºæçä¸å¯æä½ä¸å¡ç '); return; } |
| | | return ids; |
| | | } |
| | | async function handleGenerateBusiness() { |
| | | const ids = getBusinessIds(); if (!ids) return; |
| | | const count = await generatePalletBusinessCodes(ids); |
| | | message.success(`å·²è¡¥çæ ${count} 份ä¸å¡ç ï¼å·²æç å¼ä¿æä¸å`); handleRefresh(); |
| | | } |
| | | async function handleExportBusiness() { |
| | | const ids = getBusinessIds(); if (!ids) return; |
| | | const data = await exportPalletBusinessQr(ids); |
| | | downloadFileFromBlobPart({ fileName: 'æçä¸å¡äºç»´ç .zip', source: data }); |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | |
| | | <GenerateFormModal @success="handlePreGenerateSuccess" /> |
| | | <BindBatchFormModal @success="handleRefresh" /> |
| | | <QrTraceQueryModal /> |
| | | <BusinessQrViewModal /> |
| | | <QrExportTaskModal ref="qrExportTaskModal" /> |
| | | <Grid table-title="æçç å表"> |
| | | <template #table-title><span class="text-[1rem] font-bold" style="white-space:nowrap">æçç å表</span></template> |
| | |
| | | { label: 'ç»å®æ¹æ¬¡', auth: ['mes:pro-pallet:update'], onClick: handleBindBatch }, |
| | | { label: 'æ«ç 追溯', icon: ACTION_ICON.SEARCH, auth: ['mes:pro-pallet:query'], onClick: handleTrace }]" /> |
| | | <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: '追溯äºç»´ç ï¼ææ¹æ¬¡ï¼', auth: ['mes:pro-pallet:export'], onClick: handleExportQr }, |
| | | { label: 'ä¸å¡äºç»´ç ï¼éä¸è®°å½ï¼', auth: ['mes:pro-pallet:export'], onClick: handleExportBusiness }, |
| | | { label: 'Excel æ°æ®', auth: ['mes:pro-pallet:export'], onClick: handleExport }]"> |
| | | <template #more><Button>å¯¼åº <IconifyIcon icon="lucide:chevron-down" /></Button></template> |
| | | </TableAction> |
| | | <Button @click="qrExportTaskModal?.showLast()">åå°å¯¼åºä»»å¡</Button> |
| | | <TableAction :dropdown-trigger="['click']" :drop-down-actions="[{ label: 'è¡¥çæä¸å¡ç ï¼éä¸è®°å½ï¼', auth: ['mes:pro-pallet:create'], onClick: handleGenerateBusiness }]"> |
| | | <template #more><Button>æ´å¤æä½ <IconifyIcon icon="lucide:chevron-down" /></Button></template> |
| | | </TableAction> |
| | | </div> |
| | | </template> |
| | | <template #code="{ row }"> |
| | |
| | | auth: ['mes:pro-pallet:query'], |
| | | onClick: handleViewQr.bind(null, row), |
| | | }, |
| | | { label: 'ä¸å¡äºç»´ç ', type: 'link', auth: ['mes:pro-pallet:query'], ifShow: !!row.businessCode, onClick: handleBusinessQr.bind(null, row) }, |
| | | { |
| | | label: '导åºäºç»´ç ', |
| | | type: 'link', |
| | |
| | | type MesProScanEventApi, |
| | | } from '#/api/mes/pro/scanEvent'; |
| | | import { getLineSimpleList, type MesProLineApi } from '#/api/mes/pro/line'; |
| | | import { |
| | | getWarehouseLocationSimpleList, |
| | | type MesWmWarehouseLocationApi, |
| | | } from '#/api/mes/wm/warehouse/location'; |
| | | import { |
| | | getWarehouseAreaSimpleList, |
| | | type MesWmWarehouseAreaApi, |
| | | } from '#/api/mes/wm/warehouse/area'; |
| | | |
| | | interface ScanRecord extends MesProScanEventApi.ScanEvent { |
| | | key: number; |
| | |
| | | const code = ref(''); |
| | | const scanType = ref<MesProScanEventApi.ScanType>('BAG'); |
| | | const lineCode = ref(''); |
| | | const locationId = ref<number>(); |
| | | const areaId = ref<number>(); |
| | | const lines = ref<MesProLineApi.Line[]>([]); |
| | | const locations = ref<MesWmWarehouseLocationApi.WarehouseLocation[]>([]); |
| | | const areas = ref<MesWmWarehouseAreaApi.WarehouseArea[]>([]); |
| | | const loading = ref(false); |
| | | const refreshing = ref(false); |
| | | const records = ref<ScanRecord[]>([]); |
| | |
| | | const latest = computed(() => dashboard.value.latest); |
| | | const successCount = computed(() => dashboard.value.success ?? records.value.filter((item) => item.status === 'SUCCESS').length); |
| | | const failedCount = computed(() => dashboard.value.failed ?? records.value.filter((item) => item.status === 'FAILED').length); |
| | | const isReady = computed(() => Boolean(locationId.value && areaId.value)); |
| | | const lineOptions = computed(() => |
| | | lines.value.map((item) => ({ |
| | | label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''), |
| | | value: item.code ?? '', |
| | | })), |
| | | ); |
| | | const locationOptions = computed(() => |
| | | locations.value.map((item) => ({ |
| | | label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''), |
| | | value: item.id ?? 0, |
| | | })), |
| | | ); |
| | | const areaOptions = computed(() => |
| | | areas.value.map((item) => ({ |
| | | label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''), |
| | | value: item.id ?? 0, |
| | | })), |
| | | ); |
| | | const locationName = computed(() => locations.value.find((item) => item.id === locationId.value)?.name ?? ''); |
| | | const areaName = computed(() => areas.value.find((item) => item.id === areaId.value)?.name ?? ''); |
| | | const pageRoot = ref<HTMLElement>(); |
| | | let animationContext: gsap.Context | undefined; |
| | | let feedbackTimeline: gsap.core.Timeline | undefined; |
| | |
| | | |
| | | async function loadOptions() { |
| | | try { |
| | | const [lineList, locationList] = await Promise.all([ |
| | | getLineSimpleList(), |
| | | getWarehouseLocationSimpleList(), |
| | | ]); |
| | | lines.value = lineList ?? []; |
| | | locations.value = locationList ?? []; |
| | | lines.value = (await getLineSimpleList()) ?? []; |
| | | } catch { |
| | | lines.value = []; |
| | | locations.value = []; |
| | | } |
| | | } |
| | | |
| | | async function loadAreas() { |
| | | areas.value = []; |
| | | areaId.value = undefined; |
| | | if (!locationId.value) return; |
| | | try { |
| | | areas.value = (await getWarehouseAreaSimpleList(locationId.value)) ?? []; |
| | | } catch { |
| | | areas.value = []; |
| | | } |
| | | } |
| | | |
| | |
| | | message.warning('请å
æ«æè¿½æº¯ç '); |
| | | return; |
| | | } |
| | | if (!locationId.value || !areaId.value) { |
| | | message.warning('请å
éæ© WIP æååºåºååºä½'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | await inboundScanEvent({ |
| | | const result = await inboundScanEvent({ |
| | | eventId: `${scanType.value}-${Date.now()}`, |
| | | bagCode, |
| | | scanType: scanType.value, |
| | | quantity: 1, |
| | | lineCode: lineCode.value || undefined, |
| | | locationId: locationId.value, |
| | | areaId: areaId.value, |
| | | }); |
| | | if (result.status === 'FAILED') { |
| | | message.error(result.errorMessage || `${SCAN_TYPE_LABEL[scanType.value]} ${bagCode} å
¥åºå¤±è´¥`); |
| | | await refresh(); |
| | | return; |
| | | } |
| | | message.success(`${SCAN_TYPE_LABEL[scanType.value]} ${bagCode} 已宿å
¥åº`); |
| | | if (animationContext && pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { |
| | | feedbackTimeline?.kill(); |
| | |
| | | if (errorMessage && /å
¥åºäºå¡æ°éå¿
é¡»ä¸ºæ£æ°|æ°é.*为 0|quantity.*positive/i.test(errorMessage)) { |
| | | message.error('å
¥åºæ°éä¸è½ä¸º 0ï¼è¯·æ£æ¥è¯¥æ¹æ¬¡çè¢æ°é
ç½®'); |
| | | } else if (!errorMessage || /UnexpectedRollbackException|ç³»ç»å¼å¸¸|Internal Server Error/i.test(errorMessage)) { |
| | | message.error('æ«ç å
¥åºå¤±è´¥ï¼è¯·æ£æ¥æ¹æ¬¡æ°éååºä½é
ç½®'); |
| | | message.error('æ«ç å
¥åºå¤±è´¥ï¼è¯·æ£æ¥æ¹æ¬¡æ°éåä»åºé
ç½®'); |
| | | } else { |
| | | message.error(errorMessage); |
| | | } |
| | |
| | | <a-divider /> |
| | | <a-form layout="vertical" class="config-form"> |
| | | <a-form-item label="ç产产线"><a-select v-model:value="lineCode" :options="lineOptions" allow-clear show-search option-filter-prop="label" placeholder="è¯·éæ©äº§çº¿" /></a-form-item> |
| | | <a-form-item label="WIP æååºåº" required><a-select v-model:value="locationId" :options="locationOptions" show-search option-filter-prop="label" placeholder="è¯·éæ©åºåº" @change="loadAreas" /></a-form-item> |
| | | <a-form-item label="WIP æååºä½" required><a-select v-model:value="areaId" :options="areaOptions" show-search option-filter-prop="label" :disabled="!locationId" placeholder="è¯·éæ©åºä½" /></a-form-item> |
| | | </a-form> |
| | | </a-card> |
| | | |
| | | <a-card class="status-card" :bordered="false"> |
| | | <template #title><div class="card-title"><ApiOutlined />éé设å¤ç¶æ</div></template> |
| | | <div class="device-state"><span class="online-badge"><span class="status-dot" aria-hidden="true" /><span>{{ isReady ? 'ä½ç½®å·²å°±ç»ª' : 'çå¾
é
ç½®' }}</span></span><span class="muted">15 ç§èªå¨å·æ°</span></div> |
| | | <div class="status-list"><div><span>ç产产线</span><strong>{{ lineCode || 'æªé
ç½®' }}</strong></div><div><span>æåä½ç½®</span><strong>{{ locationId && areaId ? `${locationName || locationId} / ${areaName || areaId}` : 'æªé
ç½®' }}</strong></div><div><span>æè¿å
¥åºæ¶é´</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>æè¿çäº§æ¹æ¬¡</span><strong>{{ latest?.batchCode || '-' }}</strong></div><div><span>æè¿æ«ç ç±»å</span><strong>{{ latest?.scanType ? SCAN_TYPE_LABEL[latest.scanType] : '-' }}</strong></div></div> |
| | | <div class="device-state"><span class="online-badge"><span class="status-dot" aria-hidden="true" /><span>设å¤å·²å°±ç»ª</span></span><span class="muted">15 ç§èªå¨å·æ°</span></div> |
| | | <div class="status-list"><div><span>ç产产线</span><strong>{{ lineCode || 'æªé
ç½®' }}</strong></div><div><span>æè¿å
¥åºæ¶é´</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>æè¿çäº§æ¹æ¬¡</span><strong>{{ latest?.batchCode || '-' }}</strong></div><div><span>æè¿æ«ç ç±»å</span><strong>{{ latest?.scanType ? SCAN_TYPE_LABEL[latest.scanType] : '-' }}</strong></div></div> |
| | | <div class="sync-box"><IconifyIcon icon="ant-design:check-circle-outlined" /><div><strong>ä»å¨ç³»ç»åæ¥æ£å¸¸</strong><span>æ«ç äºä»¶ä¸åºåäºå¡å®æ¶åå
¥</span></div></div> |
| | | </a-card> |
| | | </section> |
| | | |
| | | <a-card class="records-card" :bordered="false"> |
| | | <template #title><div class="card-title"><IconifyIcon icon="ant-design:clock-circle-outlined" />æè¿æ«ç äºä»¶ <span class="record-count">æè¿ 20 æ¡</span></div></template> |
| | | <a-table :data-source="records" :pagination="false" :scroll="{ x: 1050 }" row-key="key" size="middle"> |
| | | <a-table-column key="bagCode" title="追溯ç " data-index="bagCode" /><a-table-column key="scanType" title="ç±»å"><template #default="{ record }">{{ record.scanType ? SCAN_TYPE_LABEL[record.scanType as MesProScanEventApi.ScanType] : 'è¢ç ' }}</template></a-table-column><a-table-column key="batchCode" title="çäº§æ¹æ¬¡" data-index="batchCode" /><a-table-column key="lineCode" title="产线"><template #default="{ record }">{{ record.lineCode || '-' }}</template></a-table-column> |
| | | <a-table :data-source="records" :pagination="false" :scroll="{ x: 1250 }" row-key="key" size="middle"> |
| | | <a-table-column key="bagCode" title="追溯ç " data-index="bagCode" /><a-table-column key="scanType" title="ç±»å"><template #default="{ record }">{{ record.scanType ? SCAN_TYPE_LABEL[record.scanType as MesProScanEventApi.ScanType] : 'è¢ç ' }}</template></a-table-column><a-table-column key="palletCode" title="æçç "><template #default="{ record }">{{ record.palletCode || '-' }}</template></a-table-column><a-table-column key="batchCode" title="æ¹æ¬¡ç "><template #default="{ record }">{{ record.batchCode || '-' }}</template></a-table-column><a-table-column key="lineCode" title="产线"><template #default="{ record }">{{ record.lineCode || '-' }}</template></a-table-column> |
| | | <a-table-column key="status" title="ç¶æ"><template #default="{ record }"><a-tag :color="record.status === 'SUCCESS' ? 'success' : 'error'">{{ record.status === 'SUCCESS' ? 'æåå
¥åº' : '失败' }}</a-tag></template></a-table-column><a-table-column key="errorCode" title="é误ç " data-index="errorCode" /><a-table-column key="errorMessage" title="失败åå " data-index="errorMessage" /><a-table-column key="eventTime" title="å¤çæ¶é´" data-index="eventTime" /><a-table-column key="transactionId" title="åºåäºå¡" data-index="transactionId" /> |
| | | </a-table> |
| | | </a-card> |
| | |
| | | const code = ref(''); |
| | | const deviceCode = ref(''); |
| | | const lineCode = ref(''); |
| | | const locationId = ref<number>(); |
| | | const areaId = ref<number>(); |
| | | const loading = ref(false); |
| | | const refreshing = ref(false); |
| | | const records = ref<ScanRecord[]>([]); |
| | |
| | | const latest = computed(() => dashboard.value.latest); |
| | | const successCount = computed(() => dashboard.value.success ?? records.value.filter((item) => item.status === 'SUCCESS').length); |
| | | const failedCount = computed(() => dashboard.value.failed ?? records.value.filter((item) => item.status === 'FAILED').length); |
| | | const isReady = computed(() => Boolean(deviceCode.value.trim() && locationId.value && areaId.value)); |
| | | const isReady = computed(() => Boolean(deviceCode.value.trim())); |
| | | const pageRoot = ref<HTMLElement>(); |
| | | let animationContext: gsap.Context | undefined; |
| | | let feedbackTimeline: gsap.core.Timeline | undefined; |
| | |
| | | message.warning('请å
å¡«å设å¤ç¼ç ï¼åæ«æè¢ç '); |
| | | return; |
| | | } |
| | | if (!locationId.value || !areaId.value) { |
| | | message.warning('请å
é
ç½® WIP æååºåºååºä½'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | await inboundScanEvent({ |
| | | const result = await inboundScanEvent({ |
| | | eventId: `${deviceCode.value.trim()}-${Date.now()}`, |
| | | bagCode, |
| | | quantity: 1, |
| | | deviceCode: deviceCode.value.trim(), |
| | | lineCode: lineCode.value.trim() || undefined, |
| | | locationId: locationId.value, |
| | | areaId: areaId.value, |
| | | }); |
| | | if (result.status === 'FAILED') { |
| | | message.error(result.errorMessage || `è¢ç ${bagCode} å
¥åºå¤±è´¥`); |
| | | await refresh(); |
| | | return; |
| | | } |
| | | message.success(`è¢ç ${bagCode} 已宿å
¥åº`); |
| | | if (animationContext && pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { |
| | | feedbackTimeline?.kill(); |
| | |
| | | <a-form layout="vertical" class="config-form"> |
| | | <a-form-item label="设å¤ç¼ç " required><a-input v-model:value="deviceCode" prefix="设å¤" placeholder="ä¾å¦ï¼CAM-LINE-01" /></a-form-item> |
| | | <a-form-item label="产线ç¼ç "><a-input v-model:value="lineCode" placeholder="ä¾å¦ï¼LINE-A" /></a-form-item> |
| | | <a-form-item label="WIP æååºåº" required><a-input-number v-model:value="locationId" class="full-width" :min="1" placeholder="åºåºç¼å·" /></a-form-item> |
| | | <a-form-item label="WIP æååºä½" required><a-input-number v-model:value="areaId" class="full-width" :min="1" placeholder="åºä½ç¼å·" /></a-form-item> |
| | | </a-form> |
| | | </a-card> |
| | | |
| | | <a-card class="status-card" :bordered="false"> |
| | | <template #title><div class="card-title"><ApiOutlined />éé设å¤ç¶æ</div></template> |
| | | <div class="device-state"><span class="online-badge"><span class="status-dot" aria-hidden="true" /><span>{{ isReady ? '设å¤å·²å°±ç»ª' : 'çå¾
é
ç½®' }}</span></span><span class="muted">15 ç§èªå¨å·æ°</span></div> |
| | | <div class="status-list"><div><span>å½å设å¤</span><strong>{{ deviceCode || 'æªé
ç½®' }}</strong></div><div><span>ç产产线</span><strong>{{ lineCode || 'æªé
ç½®' }}</strong></div><div><span>æåä½ç½®</span><strong>{{ locationId && areaId ? `${locationId} åº / ${areaId} ä½` : 'æªé
ç½®' }}</strong></div><div><span>æè¿å
¥åºæ¶é´</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>æè¿çäº§æ¹æ¬¡</span><strong>{{ latest?.batchCode || '-' }}</strong></div></div> |
| | | <div class="status-list"><div><span>å½å设å¤</span><strong>{{ deviceCode || 'æªé
ç½®' }}</strong></div><div><span>ç产产线</span><strong>{{ lineCode || 'æªé
ç½®' }}</strong></div><div><span>æè¿å
¥åºæ¶é´</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>æè¿çäº§æ¹æ¬¡</span><strong>{{ latest?.batchCode || '-' }}</strong></div></div> |
| | | <div class="sync-box"><IconifyIcon icon="ant-design:check-circle-outlined" /><div><strong>ä»å¨ç³»ç»åæ¥æ£å¸¸</strong><span>æ«ç äºä»¶ä¸åºåäºå¡å®æ¶åå
¥</span></div></div> |
| | | </a-card> |
| | | </section> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue'; |
| | | import type { MesTraceBatchApi } from '#/api/mes/trace/batch'; |
| | | import { getTransactionPage, type MesWmTransactionApi } from '#/api/mes/wm/transaction'; |
| | | |
| | | const props = defineProps<{ sales: MesTraceBatchApi.SalesTarget; batchCode?: string }>(); |
| | | const rows = ref<MesWmTransactionApi.Transaction[]>([]); |
| | | const page = ref(0), total = ref(0), loading = ref(false), error = ref(''); |
| | | async function load(reset = false) { |
| | | if (loading.value || !props.sales.salesCode || !props.batchCode) return; |
| | | loading.value = true; error.value = ''; |
| | | try { |
| | | const next = reset ? 1 : page.value + 1; |
| | | const result = await getTransactionPage({ pageNo: next, pageSize: 20, bizType: 119, type: 2, |
| | | bizCodeExact: props.sales.salesCode, batchCodeExact: props.batchCode, shipmentId: props.sales.shipmentId }); |
| | | rows.value = reset ? result.list : [...rows.value, ...result.list]; |
| | | total.value = result.total; page.value = next; |
| | | } catch { error.value = 'åºåºæµæ°´å 载失败ï¼è¯·æ£æ¥æµæ°´æ¥è¯¢æéåéè¯ã'; } |
| | | finally { loading.value = false; } |
| | | } |
| | | function toggle(event: Event) { |
| | | if ((event.target as HTMLDetailsElement).open && !page.value) void load(true); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="mt-1 text-sm">åºåºåï¼{{ sales.salesCode || 'æªè®°å½' }} · 客æ·ï¼{{ sales.clientName || 'æªè®°å½' }} · æ¹æ¬¡ï¼{{ batchCode || 'æªè®°å½' }}</div> |
| | | <details class="mt-2 text-sm" @toggle="toggle"> |
| | | <summary class="cursor-pointer">æ¥çåºåºæçä¸è¢ç </summary> |
| | | <p v-if="!sales.salesCode || !batchCode">缺å°åºåºåå·ææ¹æ¬¡å·ï¼æ æ³æ¥è¯¢æç»ã</p> |
| | | <p v-if="loading" role="status">æ£å¨å è½½åºåºæµæ°´â¦</p> |
| | | <p v-if="error" role="alert">{{ error }} <button type="button" @click="load(true)">éè¯</button></p> |
| | | <p v-if="page && !rows.length">ææ å¯¹åºåºåºæµæ°´ã</p> |
| | | <div v-for="row in rows" :key="row.id" class="mt-2 border-b pb-2"> |
| | | <div>æçï¼{{ row.palletCode || 'æªè®°å½' }} · è¢ç ï¼{{ row.bagCode || 'æªè®°å½' }}</div> |
| | | <div>æ°éï¼{{ row.quantity ?? 'â' }} · åºåºæ¶é´ï¼{{ row.transactionTime || 'æªè®°å½' }}</div> |
| | | </div> |
| | | <button v-if="rows.length < total" type="button" :disabled="loading" @click="load()">å è½½æ´å¤ï¼{{ rows.length }} / {{ total }}ï¼</button> |
| | | </details> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | import type { MesTraceBatchApi } from '#/api/mes/trace/batch'; |
| | | import SalesTraceDetails from './SalesTraceDetails.vue'; |
| | | |
| | | const props = defineProps<{ detail: MesTraceBatchApi.Detail }>(); |
| | | |
| | | const formatTime = (value?: string) => value ? new Date(value).toLocaleString() : 'æªè®°å½'; |
| | | |
| | | const timeline = computed(() => { |
| | | if (props.detail.timeline?.length) return props.detail.timeline; |
| | | const timeline = computed<(MesTraceBatchApi.Timeline & { sales?: MesTraceBatchApi.SalesTarget })[]>(() => { |
| | | if (props.detail.timeline?.length) { |
| | | const events: (MesTraceBatchApi.Timeline & { sales?: MesTraceBatchApi.SalesTarget })[] = props.detail.timeline.filter(item => item.eventType !== 'OUTBOUND' || item.stage !== 'éå®'); |
| | | if (props.detail.salesTargets?.length) { |
| | | events.push(...props.detail.salesTargets.map(sales => ({ stage: 'éå®', eventType: 'OUTBOUND', eventName: 'éå®åºåº', eventTime: sales.salesDate, quantity: sales.quantity, sales }))); |
| | | } else { |
| | | events.push(...props.detail.timeline.filter(item => item.eventType === 'OUTBOUND' && item.stage === 'éå®')); |
| | | } |
| | | return events.sort((a, b) => (a.eventTime || '\uffff').localeCompare(b.eventTime || '\uffff')); |
| | | } |
| | | return [ |
| | | ...(props.detail.purchaseSources?.length ? [{ stage: 'æ¥æº', eventName: 'éè´æ¥æº', status: `${props.detail.purchaseSources.length} æ¡è®°å½` }] : []), |
| | | { stage: 'ç产', eventType: 'BATCH_CREATED', eventName: 'çäº§æ¹æ¬¡å»ºç«', eventTime: props.detail.produceDate, status: props.detail.batchStatusName }, |
| | | { stage: 'å
è£
', eventName: 'å
è£
èµç ', quantity: props.detail.bagTotal }, |
| | | ...(props.detail.inbound ? [{ stage: 'ä»å¨', eventType: 'INBOUND', eventName: 'ä»å¨å
¥åº', eventTime: props.detail.inbound.eventTime, quantity: props.detail.inbound.quantity, status: props.detail.inbound.status }] : []), |
| | | ...(props.detail.salesTargets ?? []).map((item) => ({ stage: 'éå®', eventType: 'OUTBOUND', eventName: `éå®å»å · ${item.clientName || 'æªè®°å½å®¢æ·'}`, eventTime: item.salesDate, quantity: item.quantity, status: item.qualityStatus == null ? undefined : String(item.qualityStatus) })), |
| | | ...(props.detail.salesTargets ?? []).map((item) => ({ stage: 'éå®', eventType: 'OUTBOUND', eventName: 'éå®åºåº', eventTime: item.salesDate, quantity: item.quantity, sales: item })), |
| | | ...(props.detail.antiFake?.scanCount ? [{ stage: 'æ¶è´¹', eventType: 'CONSUMER_SCANNED', eventName: 'æ¶è´¹è
éªè¯', eventTime: props.detail.antiFake.lastScanTime, quantity: props.detail.antiFake.scanCount, status: `累计æ«ç ${props.detail.antiFake.scanCount} 次` }] : []), |
| | | ]; |
| | | }); |
| | |
| | | {{ item.stage || 'æªåç±»' }} · æ°éï¼{{ item.quantity ?? 'â' }} · {{ formatTime(item.eventTime) }} |
| | | <span v-if="item.status"> · {{ item.status }}</span> |
| | | </div> |
| | | <SalesTraceDetails v-if="item.sales" :sales="item.sales" :batch-code="detail.batchCode" /> |
| | | </a-timeline-item> |
| | | <a-empty v-if="!timeline.length" description="ææ è¿½æº¯äºä»¶" /> |
| | | </a-timeline> |
| | |
| | | import { |
| | | resolveSeedQuality, |
| | | submitSeedQualityGroup, |
| | | getSeedIndicatorRanges, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | |
| | |
| | | const qualifiedWarehouses = ref<MesWmWarehouseApi.Warehouse[]>([]); |
| | | const qualifiedWarehouseId = ref<number>(); |
| | | |
| | | const indicators = ref<IndicatorForm[]>([ |
| | | { code: 'purity', label: '纯度', unit: '%' }, |
| | | { code: 'cleanliness', label: 'å度', unit: '%' }, |
| | | { code: 'germination_rate', label: 'åè½ç', unit: '%' }, |
| | | { code: 'moisture', label: 'æ°´å', unit: '%' }, |
| | | ]); |
| | | const rangesReady = computed(() => indicators.value.every((item) => |
| | | const indicators = ref<IndicatorForm[]>([]); |
| | | const rangesReady = computed(() => indicators.value.length > 0 && indicators.value.every((item) => |
| | | item.minValue !== undefined && item.maxValue !== undefined)); |
| | | |
| | | async function loadRanges() { |
| | | const ranges = await getSeedIndicatorRanges(); |
| | | for (const item of indicators.value) { |
| | | const range = ranges.find((row) => row.indicatorCode === item.code); |
| | | item.minValue = range?.minValue === undefined ? undefined : Number(range.minValue); |
| | | item.maxValue = range?.maxValue === undefined ? undefined : Number(range.maxValue); |
| | | } |
| | | /** è¯å«å¯¹è±¡åï¼æäº§åçæçè´¨æ£é¡¹é建å½å
¥é¡¹ */ |
| | | function applyIndicators(scan: SeedQualityScan) { |
| | | indicators.value = (scan.indicators ?? []).map((item) => ({ |
| | | code: item.code, |
| | | label: item.name || item.code, |
| | | unit: item.unit || '', |
| | | value: undefined, |
| | | minValue: item.minValue === null || item.minValue === undefined ? undefined : Number(item.minValue), |
| | | maxValue: item.maxValue === null || item.maxValue === undefined ? undefined : Number(item.maxValue), |
| | | })); |
| | | } |
| | | |
| | | const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0); |
| | | const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0); |
| | | const enteredCount = computed(() => indicators.value.filter((item) => item.value !== undefined && item.value !== null).length); |
| | | const passedCount = computed(() => indicators.value.filter(isPassed).length); |
| | | const allEntered = computed(() => indicators.value.every((item) => |
| | | const allEntered = computed(() => indicators.value.length > 0 && indicators.value.every((item) => |
| | | item.value !== undefined && item.value !== null |
| | | && item.minValue !== undefined && item.minValue !== null |
| | | && item.maxValue !== undefined && item.maxValue !== null)); |
| | |
| | | resolving.value = true; |
| | | submittedCount.value = undefined; |
| | | try { |
| | | await loadRanges().catch(() => undefined); |
| | | const data = await resolveSeedQuality(value, scanType.value); |
| | | const data = await resolveSeedQuality(value, scanType.value, 1); |
| | | if (data.canceled) { |
| | | target.value = undefined; |
| | | message.error('该质æ£å¯¹è±¡å·²ä½åºï¼ä¸è½è´¨æ£'); |
| | |
| | | message.error('该对象没æå¾
è´¨æ£è¢ç ï¼è¯·å¨è´¨æ£è®°å½ä¸æ¥çå·²æç»æ'); |
| | | return; |
| | | } |
| | | if (!data.indicators?.length) { |
| | | target.value = undefined; |
| | | message.error('该产åå°æªé
置质æ£é¡¹ï¼è¯·å
å¨ãè´¨æ£é¡¹åæ ¼èå´é
ç½®ãä¸ç»´æ¤'); |
| | | return; |
| | | } |
| | | target.value = data; |
| | | applyIndicators(data); |
| | | resetIndicators(); |
| | | message.success(`${SCAN_TYPE_LABEL[scanType.value]} ${data.code} å·²è¯å«ï¼æ¬æ¬¡å¾
æ£ ${data.pendingBagIds.length} è¢`); |
| | | } catch (error) { |
| | |
| | | return; |
| | | } |
| | | if (!allEntered.value) { |
| | | message.warning('请填ååé¡¹ææ ç宿µå¼åæ¬æ¬¡åæ ¼ä¸ä¸é'); |
| | | message.warning('请填åå
¨é¨è´¨æ£é¡¹ç宿µå¼åæ¬æ¬¡åæ ¼ä¸ä¸é'); |
| | | return; |
| | | } |
| | | if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) { |
| | |
| | | Modal.confirm({ |
| | | title: overallPassed.value ? '确认æäº¤åæ ¼ç»æï¼' : '确认æäº¤ä¸åæ ¼ç»æï¼', |
| | | content: overallPassed.value |
| | | ? `æ¬æ¬¡ ${count} è¢å°ç»ä¸è®°å½ä¸ºåæ£åæ ¼ï¼å¾
åæ£å®¡æ ¸éè¿å夿£ï¼å¤æ£å®¡æ ¸éè¿åæè½¬å
¥åæ ¼åºã${scopeNote}` |
| | | : `æ¬æ¬¡ ${count} è¢å°è®°å½ä¸ºåæ£ä¸åæ ¼ï¼ä¿çæååºåï¼åæ£å®¡æ ¸åå³å®æ¯å¦è¿å
¥å¤æ£ã${scopeNote}`, |
| | | ? `æ¬æ¬¡ ${count} è¢å°ç»ä¸è®°å½ä¸ºåæ£åæ ¼ï¼è¿å
¥é¨é¨å®¡æ¹ï¼å线审æ¹åå
¬å¸è´¨æ£å®¡æ¹å®æåæè½¬å
¥åæ ¼åºã${scopeNote}` |
| | | : `æ¬æ¬¡ ${count} è¢å°è®°å½ä¸ºåæ£ä¸åæ ¼ï¼ä¿çæååºåå¹¶è¿å
¥å¤æ£ã${scopeNote}`, |
| | | okText: '确认æäº¤', |
| | | cancelText: 'è¿åä¿®æ¹', |
| | | okButtonProps: { danger: !overallPassed.value }, |
| | |
| | | })), |
| | | }); |
| | | submittedCount.value = result.processedCount; |
| | | message.success(`æ¬æ¬¡ ${result.processedCount} è¢å·²æäº¤åæ£ï¼å¾
åæ£å®¡æ ¸`); |
| | | message.success(`æ¬æ¬¡ ${result.processedCount} è¢è´¨æ£å·²æäº¤ï¼å·²åæ¥çæå å·¥é¨ãéå®é¨ä¸¤æ¡è´¨æ£è®°å½`); |
| | | if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { |
| | | gsap.fromTo('.result-card', { scale: 0.97, autoAlpha: 0.5 }, { scale: 1, autoAlpha: 1, duration: 0.35, ease: 'power2.out' }); |
| | | } |
| | |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | await loadRanges(); |
| | | } catch (error) { |
| | | message.error(errorText(error)); |
| | | } |
| | | try { |
| | | const warehouses = await getWarehouseSimpleList(); |
| | | qualifiedWarehouses.value = (warehouses ?? []).filter( |
| | |
| | | <div> |
| | | <div class="eyebrow">MES / è´¨é管ç</div> |
| | | <h1>ç§åè´¨æ£</h1> |
| | | <p>è¯å«å¾
æ£èå´å¹¶å½å
¥åé¡¹ææ ï¼åæ£å®¡æ ¸å夿£ï¼å¤æ£å®¡æ ¸åå
¥åºæå¤ç½®</p> |
| | | <p>䏿¬¡æäº¤åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è´¨æ£è®°å½ï¼åæ£ä¸åæ ¼å
夿£ï¼å线é¨é¨å®¡æ¹åè¿å
¥å
¬å¸è´¨æ£å®¡æ¹</p> |
| | | </div> |
| | | <div class="header-badge"><IconifyIcon icon="ep:checked" /> è¢ç / æçç / æ¹æ¬¡ç </div> |
| | | </header> |
| | | |
| | | <section class="summary-grid" aria-label="è´¨æ£æè¦"> |
| | | <div class="summary-card blue"><IconifyIcon icon="ep:document-checked" /><div><span>æ£éªé¡¹ç®</span><strong>4</strong><small>纯度ãå度ãåè½çãæ°´å</small></div></div> |
| | | <div class="summary-card blue"><IconifyIcon icon="ep:document-checked" /><div><span>æ£éªé¡¹ç®</span><strong>{{ indicators.length }}</strong><small>{{ indicators.length ? indicators.map((item) => item.label).join('ã') : 'è¯å«åæäº§å带åº' }}</small></div></div> |
| | | <div class="summary-card" :class="target ? 'green' : 'blue'"><IconifyIcon icon="ep:box" /><div><span>æ¬æ¬¡å¾
æ£</span><strong>{{ target ? `${pendingCount} è¢` : 'å¾
è¯å«' }}</strong><small>{{ target ? `${SCAN_TYPE_LABEL[scanType]} ${target.code}` : `è¯·éæ©ç±»ååè¯å«${SCAN_TYPE_LABEL[scanType]}` }}</small></div></div> |
| | | <div class="summary-card" :class="overallPassed ? 'green' : allEntered ? 'red' : 'blue'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><div><span>æ»ä½ç»è®º</span><strong>{{ overallPassed ? 'åæ ¼' : allEntered ? 'ä¸åæ ¼' : 'å¾
å½å
¥' }}</strong><small>{{ overallPassed ? 'å¯æäº¤åæ ¼ç»æ' : allEntered ? '请填åä¸åæ ¼åå ' : `å·²å½å
¥ ${enteredCount} / 4 项` }}</small></div></div> |
| | | <div class="summary-card" :class="overallPassed ? 'green' : allEntered ? 'red' : 'blue'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><div><span>æ»ä½ç»è®º</span><strong>{{ overallPassed ? 'åæ ¼' : allEntered ? 'ä¸åæ ¼' : 'å¾
å½å
¥' }}</strong><small>{{ overallPassed ? 'å¯æäº¤åæ ¼ç»æ' : allEntered ? '请填åä¸åæ ¼åå ' : `å·²å½å
¥ ${enteredCount} / ${indicators.length} 项` }}</small></div></div> |
| | | </section> |
| | | |
| | | <section class="content-grid"> |
| | |
| | | </a-form-item> |
| | | </div> |
| | | <p v-if="!qualifiedWarehouses.length" class="target-warning">ææ å¯ç¨åæ ¼åºï¼è¯·è系管çåé
ç½®ååæäº¤è´¨æ£ã</p> |
| | | <a-alert v-if="!rangesReady" class="target-empty" type="info" show-icon message="æ¬æ¬¡è¯·æå¨å¡«ååæ ¼èå´" description="管çåå°æªé
ç½®å®æ´é»è®¤èå´ï¼ä»å¯æå¨å¡«ååé¡¹ææ çåæ ¼ä¸ä¸éã" /> |
| | | <a-alert v-if="target && !rangesReady" class="target-empty" type="info" show-icon message="æ¬æ¬¡è¯·æå¨å¡«ååæ ¼èå´" description="该产åé¨åè´¨æ£é¡¹æªé
ç½®é»è®¤èå´ï¼ä»å¯æå¨å¡«åæ¬æ¬¡åæ ¼ä¸ä¸éã" /> |
| | | |
| | | <a-divider orientation="left">ææ ç»æ</a-divider> |
| | | <div class="indicator-list"> |
| | |
| | | <div class="indicator-name"><span class="indicator-dot" :class="isPassed(item) ? 'pass' : 'fail'" /><strong>{{ item.label }}</strong><small>{{ item.unit }}</small></div> |
| | | <a-input-number v-model:value="item.value" class="indicator-input" :disabled="loading" :min="0" :precision="3" placeholder="宿µå¼" /> |
| | | <span class="range-separator">èå´</span> |
| | | <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <span>â</span> |
| | | <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? 'å¾
å½å
¥' : isPassed(item) ? 'åæ ¼' : 'è¶
åºèå´' }}</a-tag> |
| | | </div> |
| | | </div> |
| | |
| | | |
| | | <a-card class="result-card" :bordered="false"> |
| | | <template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />å¤å®è¯´æ</div></template> |
| | | <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? 'å½åç»æåæ ¼' : allEntered ? 'å½ååå¨ä¸åæ ¼é¡¹' : 'çå¾
å½å
¥ææ ' }}</strong><span>{{ allEntered ? '宿µå¼éå¤äºæå°å¼ä¸æå¤§å¼ä¹é´' : `å·²å½å
¥ ${enteredCount} / 4 项宿µå¼` }}</span></div> |
| | | <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>å
è¯å«ç å¼åå½å
¥ææ ï¼åé¡¹ææ å¿
é¡»å
¨é¨æäº¤</span></div><div><IconifyIcon icon="ep:check" /><span>è¾¹çå¼æåæ ¼å¤ç</span></div><div><IconifyIcon icon="ep:check" /><span>æçç ãæ¹æ¬¡ç çç»è®ºåºç¨äºæ¬æ¬¡å
¨é¨å¾
æ£è¢</span></div><div><IconifyIcon icon="ep:check" /><span>ä¸åæ ¼ç»æéä¿çåå </span></div><div><IconifyIcon icon="ep:check" /><span>è´¨æ£å䏿£éªæ¶é´ç±æå¡ç«¯è®°å½</span></div></div> |
| | | <a-alert message="夿£å®¡æ ¸å®æåæµè½¬åºå" description="忣å夿£æé´è¢ç ç卿ååºãåæ£å®¡æ ¸éè¿åè¿å
¥å¤æ£ï¼å¤æ£åæ ¼å¹¶å®¡æ ¸éè¿æè½¬å
¥åæ ¼åºï¼å¤æ£ä¸åæ ¼ç»ç¡®è®¤åææ¥åºã" type="info" show-icon /> |
| | | <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? 'å½åç»æåæ ¼' : allEntered ? 'å½ååå¨ä¸åæ ¼é¡¹' : 'çå¾
å½å
¥ææ ' }}</strong><span>{{ allEntered ? '宿µå¼éå¤äºæå°å¼ä¸æå¤§å¼ä¹é´' : `å·²å½å
¥ ${enteredCount} / ${indicators.length} 项宿µå¼` }}</span></div> |
| | | <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>å
è¯å«ç å¼åå½å
¥ææ ï¼äº§åé
ç½®çè´¨æ£é¡¹å¿
é¡»å
¨é¨æäº¤</span></div><div><IconifyIcon icon="ep:check" /><span>å å·¥é¨ãéå®é¨åèªæäº¤å®æµå¼ä¸æ£éªç»è®º</span></div><div><IconifyIcon icon="ep:check" /><span>è¾¹çå¼æåæ ¼å¤ç</span></div><div><IconifyIcon icon="ep:check" /><span>æçç ãæ¹æ¬¡ç çç»è®ºåºç¨äºæ¬æ¬¡å
¨é¨å¾
æ£è¢</span></div><div><IconifyIcon icon="ep:check" /><span>ä¸åæ ¼ç»æéä¿çåå </span></div><div><IconifyIcon icon="ep:check" /><span>è´¨æ£å䏿£éªæ¶é´ç±æå¡ç«¯è®°å½</span></div></div> |
| | | <a-alert message="å线审æ¹å®æåè¿å
¥å
¬å¸è´¨æ£å®¡æ¹" description="忣ä¸åæ ¼å
夿£ï¼æé´è¢ç ç卿ååºã两æ¡è´¨æ£çº¿åèªå®æé¨é¨å®¡æ¹ååèµ·å
¬å¸è´¨æ£å®¡æ¹ï¼å
¬å¸å®¡æ¹éè¿æè½¬å
¥åæ ¼åºï¼ä»»ä¸æ¡çº¿ä¸åæ ¼åæ´è¢æ¥åºã" type="info" show-icon /> |
| | | <div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>æè¿æäº¤è´¨æ£ï¼<strong>{{ submittedCount }} è¢</strong></span></div> |
| | | </a-card> |
| | | </section> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | getSeedQualityActions, |
| | | getSeedQualityIndicators, |
| | | getSeedQualityPage, |
| | | resolveSeedReturnQuality, |
| | | type SeedQualityAction, |
| | | type SeedQualityIndicator, |
| | | type SeedQualityRecord, |
| | | type SeedReturnQualityInfo, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | defineOptions({ name: 'MesQcSeedQualityProcessDetail' }); |
| | | |
| | | const props = defineProps<{ |
| | | /** æµç¨ä¸å¡æ è¯ï¼é¨é¨å®¡æ¹ qcLine#batchCodeï¼å
¬å¸å®¡æ¹ä¸ºçäº§æ¹æ¬¡å·ï¼é货审æ¹ä¸º returnSalesId */ |
| | | id?: string; |
| | | /** æµç¨å®ä¹æ è¯ï¼ç¨äºåºåé¨é¨å®¡æ¹ / å
¬å¸å®¡æ¹ / éè´§å®¡æ¹ */ |
| | | processDefinitionKey?: string; |
| | | }>(); |
| | | |
| | | const LINE_PROCESS_KEY = 'mes_seed_quality_line_approve'; |
| | | const COMPANY_PROCESS_KEY = 'mes_seed_quality_company_approve'; |
| | | const RETURN_PROCESS_KEY = 'mes_seed_return_approve'; |
| | | |
| | | type DetailMode = 'line' | 'company' | 'return' | 'unknown'; |
| | | |
| | | const loading = ref(false); |
| | | const errorMessage = ref(''); |
| | | const mode = ref<DetailMode>('unknown'); |
| | | const records = ref<SeedQualityRecord[]>([]); |
| | | const returnInfo = ref<SeedReturnQualityInfo>(); |
| | | |
| | | const parsedLine = ref<number>(); |
| | | const parsedBatchCode = ref(''); |
| | | const parsedBagCode = ref(''); |
| | | const companyByBatch = ref(true); |
| | | const pageNo = ref(1); |
| | | const pageSize = ref(20); |
| | | const pageTotal = ref(0); |
| | | |
| | | const indicatorCache = ref<Record<number, SeedQualityIndicator[]>>({}); |
| | | const actionCache = ref<Record<number, SeedQualityAction[]>>({}); |
| | | const expandedKeys = ref<number[]>([]); |
| | | |
| | | const workflowLabels: Record<number, string> = { |
| | | 10: 'å¾
夿£', |
| | | 20: 'å¾
é¨é¨å®¡æ¹', |
| | | 25: 'å¾
é¨é¨ä¸åæ ¼å®¡æ¹', |
| | | 30: 'å¾
å
¬å¸è´¨æ£å®¡æ¹', |
| | | 40: 'å·²æ¾è¡', |
| | | 50: '忣驳åå¾
鿣', |
| | | 60: 'å¾
å
¬å¸è´¨æ£ä¸åæ ¼å¤ç', |
| | | 70: 'å·²æ¥åº', |
| | | }; |
| | | const qcLineLabels: Record<number, string> = { 0: 'æªåºå', 1: 'å å·¥é¨', 2: 'éå®é¨' }; |
| | | const resultLabels: Record<number, string> = { 0: 'å¤çä¸', 1: 'åæ ¼', 2: 'ä¸åæ ¼' }; |
| | | const actionLabels: Record<string, string> = { |
| | | INITIAL: '忣', |
| | | INITIAL_RECHECK: '鿰忣', |
| | | RETEST: '夿£', |
| | | PROCESS_APPROVE: 'åæ£å®¡æ ¸éè¿', |
| | | PROCESS_REJECT: 'åæ£å®¡æ ¸é©³å', |
| | | COMPANY_APPROVE: '夿£å®¡æ ¸éè¿', |
| | | COMPANY_REJECT: '夿£å®¡æ ¸é©³å', |
| | | }; |
| | | const indicatorLabels: Record<string, string> = { |
| | | purity: '纯度', |
| | | cleanliness: 'å度', |
| | | germination_rate: 'åè½ç', |
| | | moisture: 'æ°´å', |
| | | }; |
| | | |
| | | const businessKey = computed(() => (props.id ?? '').trim()); |
| | | |
| | | const displayRecords = computed<SeedQualityRecord[]>(() => { |
| | | if (mode.value !== 'return') { |
| | | return records.value; |
| | | } |
| | | const info = returnInfo.value; |
| | | if (!info?.qualityId) { |
| | | return []; |
| | | } |
| | | return [ |
| | | { |
| | | id: info.qualityId, |
| | | code: info.returnSalesCode || `éè´§å ${info.returnSalesId}`, |
| | | bizType: 2, |
| | | batchCode: info.batchCode, |
| | | result: info.result ?? 0, |
| | | workflowStatus: info.workflowStatus ?? 0, |
| | | returnSalesId: info.returnSalesId, |
| | | returnSalesCode: info.returnSalesCode, |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | const modeLabel = computed(() => { |
| | | if (mode.value === 'line') return 'ç§åè´¨æ£ Â· é¨é¨å®¡æ¹'; |
| | | if (mode.value === 'company') return 'ç§åè´¨æ£ Â· å
¬å¸è´¨æ£å®¡æ¹'; |
| | | if (mode.value === 'return') return 'ç»éåéè´§ · è´¨æ£å®¡æ¹'; |
| | | return 'ç§åè´¨æ£'; |
| | | }); |
| | | |
| | | const summaryTitle = computed(() => { |
| | | if (mode.value === 'line') { |
| | | return `çäº§æ¹æ¬¡ ${parsedBatchCode.value || 'â'} · ${qcLineLabels[parsedLine.value ?? 0] || 'æªç¥è´¨æ£çº¿'}`; |
| | | } |
| | | if (mode.value === 'company') { |
| | | return companyByBatch.value |
| | | ? `çäº§æ¹æ¬¡ ${parsedBatchCode.value || 'â'}` |
| | | : `è¢ç ${parsedBagCode.value || 'â'}`; |
| | | } |
| | | if (mode.value === 'return') { |
| | | const info = returnInfo.value; |
| | | return info?.returnSalesCode ? `éè´§å ${info.returnSalesCode}` : 'ç»éåéè´§è´¨æ£'; |
| | | } |
| | | return 'è´¨æ£è¯¦æ
'; |
| | | }); |
| | | |
| | | const summarySubtitle = computed(() => { |
| | | if (mode.value === 'line') { |
| | | return `å
± ${pageTotal.value} æ¡è´¨æ£è®°å½ï¼ææ 䏿µç¨ççå¯å±å¼æ¥ç`; |
| | | } |
| | | if (mode.value === 'company') { |
| | | return `æ¬æ¹æ¬¡å
± ${pageTotal.value} æ¡è´¨æ£è®°å½ï¼æ¯ä¸ªè¢çå å·¥é¨ãéå®é¨ä¸¤æ¡çº¿ååæ ¼ææ¾è¡å
¥åº`; |
| | | } |
| | | if (mode.value === 'return') { |
| | | return returnInfo.value?.batchCode ? `çäº§æ¹æ¬¡ ${returnInfo.value.batchCode}` : 'éè´§è´¨æ£è®°å½'; |
| | | } |
| | | return ''; |
| | | }); |
| | | |
| | | const passCount = computed(() => records.value.filter((item) => item.result === 1).length); |
| | | const failCount = computed(() => records.value.filter((item) => item.result === 2).length); |
| | | const processingCount = computed(() => records.value.filter((item) => !item.result).length); |
| | | |
| | | const tablePagination = computed(() => { |
| | | if ( |
| | | (mode.value !== 'line' && mode.value !== 'company') || |
| | | pageTotal.value <= pageSize.value |
| | | ) { |
| | | return false as const; |
| | | } |
| | | return { |
| | | current: pageNo.value, |
| | | pageSize: pageSize.value, |
| | | pageSizeOptions: ['20', '50', '100', '200'], |
| | | showSizeChanger: true, |
| | | showTotal: (count: number) => `å
± ${count} æ¡`, |
| | | total: pageTotal.value, |
| | | onChange: (page: number, size: number) => { |
| | | pageSize.value = size; |
| | | if (mode.value === 'line') { |
| | | void loadLine(businessKey.value, page); |
| | | } else if (mode.value === 'company') { |
| | | void loadCompany(businessKey.value, page); |
| | | } |
| | | }, |
| | | }; |
| | | }); |
| | | |
| | | function detectMode(): DetailMode { |
| | | const key = props.processDefinitionKey; |
| | | if (key === LINE_PROCESS_KEY) return 'line'; |
| | | if (key === COMPANY_PROCESS_KEY) return 'company'; |
| | | if (key === RETURN_PROCESS_KEY) return 'return'; |
| | | if (businessKey.value.includes('#')) return 'line'; |
| | | return 'unknown'; |
| | | } |
| | | |
| | | async function loadDetails() { |
| | | const key = businessKey.value; |
| | | if (!key) { |
| | | errorMessage.value = 'æµç¨ç¼ºå°ä¸å¡æ è¯ï¼æ æ³å±ç¤ºè´¨æ£è¯¦æ
'; |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | errorMessage.value = ''; |
| | | records.value = []; |
| | | returnInfo.value = undefined; |
| | | indicatorCache.value = {}; |
| | | actionCache.value = {}; |
| | | expandedKeys.value = []; |
| | | pageNo.value = 1; |
| | | pageTotal.value = 0; |
| | | try { |
| | | const detected = detectMode(); |
| | | if (detected === 'line') { |
| | | mode.value = 'line'; |
| | | await loadLine(key); |
| | | } else if (detected === 'return') { |
| | | mode.value = 'return'; |
| | | await loadReturn(key); |
| | | } else if (detected === 'company') { |
| | | mode.value = 'company'; |
| | | await loadCompany(key); |
| | | } else { |
| | | await loadCompany(key); |
| | | if (!records.value.length && /^\d+$/.test(key)) { |
| | | mode.value = 'return'; |
| | | await loadReturn(key); |
| | | } else { |
| | | mode.value = 'company'; |
| | | } |
| | | } |
| | | } catch (error) { |
| | | errorMessage.value = readError(error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadLine(key: string, page = pageNo.value) { |
| | | const index = key.indexOf('#'); |
| | | const line = Number(key.slice(0, index)); |
| | | const batchCode = key.slice(index + 1); |
| | | if (!Number.isFinite(line) || !batchCode) { |
| | | throw new Error('审æ¹ä¸å¡æ è¯æ ¼å¼å¼å¸¸ï¼åºä¸º è´¨æ£çº¿#æ¹æ¬¡å·'); |
| | | } |
| | | parsedLine.value = line; |
| | | parsedBatchCode.value = batchCode; |
| | | const resp = await getSeedQualityPage({ |
| | | pageNo: page, |
| | | pageSize: pageSize.value, |
| | | exactBatchCode: batchCode, |
| | | qcLine: line, |
| | | }); |
| | | records.value = resp.list ?? []; |
| | | pageTotal.value = resp.total ?? 0; |
| | | pageNo.value = page; |
| | | } |
| | | |
| | | async function loadCompany(key: string, page = pageNo.value) { |
| | | // æ°æµç¨æçäº§æ¹æ¬¡åèµ·å
¬å¸å®¡æ¹ï¼å岿°æ®æè¢ç åèµ·ï¼å䏿¬¡å
¼å®¹åé |
| | | companyByBatch.value = true; |
| | | parsedBatchCode.value = key; |
| | | parsedBagCode.value = ''; |
| | | let resp = await getSeedQualityPage({ |
| | | pageNo: page, |
| | | pageSize: pageSize.value, |
| | | exactBatchCode: key, |
| | | bizType: 1, |
| | | }); |
| | | let list = resp.list ?? []; |
| | | let total = resp.total ?? 0; |
| | | if (!list.length && page === 1) { |
| | | companyByBatch.value = false; |
| | | parsedBatchCode.value = ''; |
| | | parsedBagCode.value = key; |
| | | resp = await getSeedQualityPage({ |
| | | pageNo: 1, |
| | | pageSize: pageSize.value, |
| | | code: key, |
| | | bizType: 1, |
| | | }); |
| | | list = (resp.list ?? []).filter((item) => item.code === key); |
| | | total = list.length; |
| | | } |
| | | records.value = list; |
| | | pageTotal.value = total; |
| | | pageNo.value = page; |
| | | if (list.length) { |
| | | const first = list[0]; |
| | | if (first) { |
| | | expandedKeys.value = [first.id]; |
| | | } |
| | | await Promise.all(list.slice(0, 5).map((item) => loadIndicators(item.id))); |
| | | } |
| | | } |
| | | |
| | | async function loadReturn(key: string) { |
| | | const returnSalesId = Number(key); |
| | | if (!Number.isFinite(returnSalesId)) { |
| | | throw new Error('é货审æ¹ä¸å¡æ è¯ä¸æ¯ææçéå®éè´§åç¼å·'); |
| | | } |
| | | returnInfo.value = await resolveSeedReturnQuality(returnSalesId); |
| | | const qualityId = returnInfo.value?.qualityId; |
| | | if (qualityId) { |
| | | expandedKeys.value = [qualityId]; |
| | | await Promise.all([loadIndicators(qualityId), loadActions(qualityId)]); |
| | | } |
| | | } |
| | | |
| | | async function loadIndicators(id: number) { |
| | | if (indicatorCache.value[id]) { |
| | | return; |
| | | } |
| | | try { |
| | | const list = await getSeedQualityIndicators(id); |
| | | indicatorCache.value = { ...indicatorCache.value, [id]: list ?? [] }; |
| | | } catch { |
| | | indicatorCache.value = { ...indicatorCache.value, [id]: [] }; |
| | | } |
| | | } |
| | | |
| | | async function loadActions(id: number) { |
| | | if (actionCache.value[id]) { |
| | | return; |
| | | } |
| | | try { |
| | | const list = await getSeedQualityActions(id); |
| | | actionCache.value = { ...actionCache.value, [id]: list ?? [] }; |
| | | } catch { |
| | | actionCache.value = { ...actionCache.value, [id]: [] }; |
| | | } |
| | | } |
| | | |
| | | async function handleExpand(expanded: boolean, record: SeedQualityRecord) { |
| | | if (!expanded) { |
| | | expandedKeys.value = []; |
| | | return; |
| | | } |
| | | expandedKeys.value = [record.id]; |
| | | await Promise.all([loadIndicators(record.id), loadActions(record.id)]); |
| | | } |
| | | |
| | | function readError(error: unknown) { |
| | | const response = error as { |
| | | data?: { msg?: string }; |
| | | message?: string; |
| | | msg?: string; |
| | | response?: { data?: { msg?: string } }; |
| | | }; |
| | | return ( |
| | | response?.response?.data?.msg || |
| | | response?.data?.msg || |
| | | response?.msg || |
| | | response?.message || |
| | | '读åè´¨æ£è¯¦æ
失败' |
| | | ); |
| | | } |
| | | |
| | | function statusLabel(record: SeedQualityRecord) { |
| | | if (record.bizType === 2) { |
| | | if (record.workflowStatus === 10) return 'å¾
夿£'; |
| | | if (record.workflowStatus === 20) return 'å¾
éå®é¨é¢å¯¼å®¡æ¹'; |
| | | if (record.workflowStatus === 40) return 'éè´§è´¨æ£éè¿'; |
| | | if (record.workflowStatus === 70) return 'éè´§è´¨æ£ä¸éè¿'; |
| | | } |
| | | return workflowLabels[record.workflowStatus] || 'æªç¥ç¶æ'; |
| | | } |
| | | |
| | | function statusColor(record: SeedQualityRecord) { |
| | | if (record.workflowStatus === 40) return 'green'; |
| | | if (record.workflowStatus === 70) return 'red'; |
| | | if (record.workflowStatus === 60) return 'orange'; |
| | | return 'blue'; |
| | | } |
| | | |
| | | function resultColor(result?: number) { |
| | | if (result === 1) return 'success'; |
| | | if (result === 2) return 'error'; |
| | | return 'warning'; |
| | | } |
| | | |
| | | function qcLineColor(line?: number) { |
| | | if (line === 1) return 'blue'; |
| | | if (line === 2) return 'purple'; |
| | | return 'default'; |
| | | } |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? new Date(value).toLocaleString() : 'â'; |
| | | } |
| | | |
| | | function indicatorLabel(item: SeedQualityIndicator) { |
| | | return item.indicatorName || indicatorLabels[item.indicatorCode] || item.indicatorCode; |
| | | } |
| | | |
| | | function indicatorPassed(item: SeedQualityIndicator) { |
| | | const value = Number(item.value); |
| | | return ( |
| | | Number.isFinite(value) && |
| | | value >= Number(item.minValue) && |
| | | value <= Number(item.maxValue) |
| | | ); |
| | | } |
| | | |
| | | function actionColor(action: string) { |
| | | if (action.includes('REJECT')) return 'red'; |
| | | if (action.includes('APPROVE')) return 'green'; |
| | | return 'blue'; |
| | | } |
| | | |
| | | function actionIndicators(action: SeedQualityAction) { |
| | | if (!action.indicatorsJson) { |
| | | return [] as Array<{ code: string; maxValue: number; minValue: number; value: number }>; |
| | | } |
| | | try { |
| | | const parsed: unknown = JSON.parse(action.indicatorsJson); |
| | | return Array.isArray(parsed) |
| | | ? (parsed as Array<{ code: string; maxValue: number; minValue: number; value: number }>) |
| | | : []; |
| | | } catch { |
| | | return [] as Array<{ code: string; maxValue: number; minValue: number; value: number }>; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => [props.id, props.processDefinitionKey], |
| | | () => { |
| | | void loadDetails(); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <a-spin :spinning="loading"> |
| | | <div class="seed-process-detail"> |
| | | <a-alert |
| | | v-if="errorMessage" |
| | | type="error" |
| | | show-icon |
| | | :message="errorMessage" |
| | | /> |
| | | <template v-else-if="displayRecords.length || returnInfo"> |
| | | <div class="summary-bar"> |
| | | <div class="summary-main"> |
| | | <span class="eyebrow"> |
| | | <IconifyIcon icon="ant-design:experiment-outlined" /> |
| | | {{ modeLabel }} |
| | | </span> |
| | | <strong class="summary-title">{{ summaryTitle }}</strong> |
| | | <span class="summary-sub">{{ summarySubtitle }}</span> |
| | | </div> |
| | | <div v-if="mode !== 'return'" class="summary-counts"> |
| | | <span class="count pass">æ¬é¡µåæ ¼ {{ passCount }}</span> |
| | | <span class="count fail">æ¬é¡µä¸åæ ¼ {{ failCount }}</span> |
| | | <span class="count pending">æ¬é¡µæµç¨ä¸ {{ processingCount }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-table |
| | | :data-source="displayRecords" |
| | | :expanded-row-keys="expandedKeys" |
| | | :pagination="tablePagination" |
| | | :scroll="{ x: 720 }" |
| | | row-key="id" |
| | | size="small" |
| | | @expand="handleExpand" |
| | | > |
| | | <template #expandedRowRender="{ record }"> |
| | | <div class="expand-body"> |
| | | <div class="expand-section"> |
| | | <div class="expand-title">è´¨æ£ææ </div> |
| | | <div |
| | | v-if="indicatorCache[record.id]?.length" |
| | | class="indicator-grid" |
| | | > |
| | | <div |
| | | v-for="item in indicatorCache[record.id]" |
| | | :key="item.indicatorCode" |
| | | class="indicator-item" |
| | | > |
| | | <span class="indicator-name">{{ indicatorLabel(item) }}</span> |
| | | <strong |
| | | class="indicator-value" |
| | | :class="{ failed: !indicatorPassed(item) }" |
| | | > |
| | | {{ item.value ?? 'â' }} |
| | | <small v-if="item.unit">{{ item.unit }}</small> |
| | | </strong> |
| | | <span class="indicator-range"> |
| | | åæ ¼ {{ item.minValue }} ~ {{ item.maxValue }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div v-else class="expand-empty">ææ ææ è®°å½</div> |
| | | </div> |
| | | |
| | | <div class="expand-section"> |
| | | <div class="expand-title">æµç¨çç</div> |
| | | <div |
| | | v-if="actionCache[record.id]?.length" |
| | | class="action-list" |
| | | > |
| | | <div |
| | | v-for="action in actionCache[record.id]" |
| | | :key="action.id" |
| | | class="action-item" |
| | | > |
| | | <div class="action-head"> |
| | | <a-tag :color="actionColor(action.action)"> |
| | | {{ actionLabels[action.action] || action.action }} |
| | | </a-tag> |
| | | <span class="action-meta"> |
| | | {{ formatDateTime(action.createTime) }} · æä½äºº |
| | | {{ action.operatorName || action.operatorId || 'â' }} |
| | | </span> |
| | | </div> |
| | | <p v-if="action.opinion" class="action-opinion"> |
| | | {{ action.opinion }} |
| | | </p> |
| | | <div |
| | | v-if="actionIndicators(action).length" |
| | | class="action-indicators" |
| | | > |
| | | <span |
| | | v-for="indicator in actionIndicators(action)" |
| | | :key="indicator.code" |
| | | class="action-indicator" |
| | | > |
| | | {{ indicatorLabels[indicator.code] || indicator.code }} |
| | | <strong>{{ indicator.value }}</strong> |
| | | <em>{{ indicator.minValue }}~{{ indicator.maxValue }}</em> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-else class="expand-empty">ææ æµç¨çç</div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <a-table-column key="code" title="è¢ç / åå·" data-index="code"> |
| | | <template #default="{ record }"> |
| | | <span class="mono strong">{{ record.code }}</span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column |
| | | v-if="mode !== 'return'" |
| | | key="qcLine" |
| | | title="è´¨æ£çº¿" |
| | | width="90" |
| | | > |
| | | <template #default="{ record }"> |
| | | <a-tag :color="qcLineColor(record.qcLine)"> |
| | | {{ qcLineLabels[record.qcLine ?? 0] || 'æªåºå' }} |
| | | </a-tag> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="result" title="æ£éªç»æ" width="90"> |
| | | <template #default="{ record }"> |
| | | <a-tag :color="resultColor(record.result)"> |
| | | {{ resultLabels[record.result] || 'å¤çä¸' }} |
| | | </a-tag> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="workflowStatus" title="æµç¨ç¶æ" width="150"> |
| | | <template #default="{ record }"> |
| | | <a-tag :color="statusColor(record)"> |
| | | {{ statusLabel(record) }} |
| | | </a-tag> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column |
| | | v-if="mode !== 'return'" |
| | | key="reason" |
| | | title="ä¸åæ ¼åå " |
| | | data-index="reason" |
| | | ellipsis |
| | | > |
| | | <template #default="{ record }"> |
| | | <span v-if="record.reason">{{ record.reason }}</span> |
| | | <span v-else class="muted">â</span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="inspectedAt" title="æ£éªæ¶é´" width="170"> |
| | | <template #default="{ record }"> |
| | | <span class="mono muted"> |
| | | {{ formatDateTime(record.inspectedAt) }} |
| | | </span> |
| | | </template> |
| | | </a-table-column> |
| | | </a-table> |
| | | |
| | | <div v-if="mode === 'return'" class="return-actions"> |
| | | <span class="muted">审æ¹ç»è®ºç±éå®é¨é¢å¯¼å¨æµç¨ä¸ç¡®è®¤ã</span> |
| | | </div> |
| | | </template> |
| | | <a-empty v-else description="æªæ¾å°å
³èçè´¨æ£è®°å½" /> |
| | | </div> |
| | | </a-spin> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .seed-process-detail { |
| | | padding: 2px 0; |
| | | } |
| | | |
| | | .summary-bar { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 16px; |
| | | margin-bottom: 14px; |
| | | padding: 14px 16px; |
| | | border: 1px solid #e3e9f2; |
| | | border-radius: 10px; |
| | | background: linear-gradient(135deg, #f8fbff 0%, #ffffff 70%); |
| | | } |
| | | |
| | | .summary-main { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .eyebrow { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | color: #1677ff; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.04em; |
| | | } |
| | | |
| | | .summary-title { |
| | | color: #172033; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .summary-sub { |
| | | color: #718096; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .summary-counts { |
| | | display: inline-flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .count { |
| | | padding: 2px 10px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | line-height: 20px; |
| | | } |
| | | |
| | | .count.pass { |
| | | color: #16a34a; |
| | | background: #ecfdf3; |
| | | } |
| | | |
| | | .count.fail { |
| | | color: #dc2626; |
| | | background: #fef2f2; |
| | | } |
| | | |
| | | .count.pending { |
| | | color: #d97706; |
| | | background: #fffbeb; |
| | | } |
| | | |
| | | .expand-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | padding: 8px 4px 12px; |
| | | } |
| | | |
| | | .expand-section { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .expand-title { |
| | | color: #344054; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .expand-empty { |
| | | color: #9aa5b5; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .indicator-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | .indicator-item { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 8px; |
| | | padding: 8px 12px; |
| | | border: 1px solid #e3e9f2; |
| | | border-radius: 6px; |
| | | background: #fff; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .indicator-name { |
| | | color: #5b6b80; |
| | | } |
| | | |
| | | .indicator-value { |
| | | color: #172033; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .indicator-value.failed { |
| | | color: #dc2626; |
| | | } |
| | | |
| | | .indicator-value small { |
| | | margin-left: 2px; |
| | | color: #8793a5; |
| | | font-size: 11px; |
| | | font-weight: 400; |
| | | } |
| | | |
| | | .indicator-range { |
| | | margin-left: auto; |
| | | color: #667085; |
| | | font-size: 11px; |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .action-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .action-item { |
| | | padding: 8px 12px; |
| | | border-left: 3px solid #1677ff; |
| | | border-radius: 4px; |
| | | background: #f7faff; |
| | | } |
| | | |
| | | .action-head { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .action-meta { |
| | | color: #5b6b80; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .action-opinion { |
| | | margin: 6px 0 0; |
| | | color: #3d4a5c; |
| | | font-size: 13px; |
| | | line-height: 1.6; |
| | | white-space: pre-wrap; |
| | | overflow-wrap: anywhere; |
| | | } |
| | | |
| | | .action-indicators { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | margin-top: 6px; |
| | | } |
| | | |
| | | .action-indicator { |
| | | display: inline-flex; |
| | | align-items: baseline; |
| | | gap: 4px; |
| | | padding: 2px 8px; |
| | | border: 1px solid #e3e9f2; |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | color: #5b6b80; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .action-indicator strong { |
| | | color: #172033; |
| | | } |
| | | |
| | | .action-indicator em { |
| | | color: #8793a5; |
| | | font-size: 11px; |
| | | font-style: normal; |
| | | } |
| | | |
| | | .return-actions { |
| | | margin-top: 12px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .strong { |
| | | color: #172033; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .muted { |
| | | color: #8793a5; |
| | | } |
| | | |
| | | .mono { |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import RecordPage from './record.vue'; |
| | | |
| | | defineOptions({ name: 'MesQcSeedQualityRecordProcessing' }); |
| | | </script> |
| | | |
| | | <template> |
| | | <RecordPage :initial-qc-line="1" /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import RecordPage from './record.vue'; |
| | | |
| | | defineOptions({ name: 'MesQcSeedQualityRecordSales' }); |
| | | </script> |
| | | |
| | | <template> |
| | | <RecordPage :initial-qc-line="2" /> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import type { TablePaginationConfig } from 'ant-design-vue'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | |
| | | import { |
| | | getSeedQualityBatchPage, |
| | | getSeedQualityPage, |
| | | getSeedQualityIndicators, |
| | | getSeedQualityActions, |
| | | getSeedIndicatorRanges, |
| | | retestSeedQuality, |
| | | retestSeedQualityBatch, |
| | | resolveSeedRetestScope, |
| | | submitSeedRetestScope, |
| | | approveSeedQuality, |
| | | approveSeedQualityBatch, |
| | | retrySeedQualityBpm, |
| | | resolveSeedQuality, |
| | | submitSeedQualityGroup, |
| | | type SeedQualityBatchGroup, |
| | | type SeedQualityRecord, |
| | | type SeedQualityScan, |
| | | type SeedQualityIndicator, |
| | | type SeedQualityAction, |
| | | type SeedRetestScope, |
| | | type EffectiveIndicator, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | const records = ref<SeedQualityRecord[]>([]); |
| | | const loading = ref(false); |
| | | const page = ref(1); |
| | | const pageSize = ref(10); |
| | | interface BatchBucket { |
| | | list: SeedQualityRecord[]; |
| | | total: number; |
| | | page: number; |
| | | pageSize: number; |
| | | loading: boolean; |
| | | } |
| | | |
| | | const groups = ref<SeedQualityBatchGroup[]>([]); |
| | | const groupLoading = ref(false); |
| | | const groupTotal = ref(0); |
| | | const batchPage = ref(1); |
| | | const batchPageSize = ref(10); |
| | | const expandedKeys = ref<string[]>([]); |
| | | const buckets = ref<Record<string, BatchBucket>>({}); |
| | | const batchSelections = ref<Record<string, number[]>>({}); |
| | | const loading = computed(() => |
| | | groupLoading.value || Object.values(buckets.value).some((bucket) => bucket.loading), |
| | | ); |
| | | |
| | | /** åºå®è´¨æ£çº¿ï¼1-å å·¥é¨ï¼2-éå®é¨ï¼ä¸ä¼ 表示å
¨é¨ */ |
| | | const props = withDefaults(defineProps<{ initialQcLine?: number }>(), { |
| | | initialQcLine: undefined, |
| | | }); |
| | | |
| | | const code = ref(''); |
| | | const batchCode = ref(''); |
| | | const result = ref<number>(); |
| | | const workflowStatus = ref<number>(); |
| | | const qcLine = ref<number | undefined>(props.initialQcLine); |
| | | |
| | | const total = ref(0); |
| | | const pageTitle = computed(() => { |
| | | if (props.initialQcLine === 1) return 'å å·¥é¨è´¨æ£'; |
| | | if (props.initialQcLine === 2) return 'éå®é¨è´¨æ£'; |
| | | return 'è´¨æ£è®°å½'; |
| | | }); |
| | | |
| | | const statsTotal = ref(0); |
| | | const passTotal = ref(0); |
| | | const failTotal = ref(0); |
| | |
| | | Math.max(statsTotal.value - passTotal.value - failTotal.value, 0), |
| | | ); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const router = useRouter(); |
| | | function openProcess(processInstanceId?: string) { |
| | | if (!processInstanceId) { |
| | | message.warning('å®¡æ¹æµç¨å°æªçæï¼è¯·è系管çåæ£æ¥ BPM æµç¨é
ç½®'); |
| | | return; |
| | | } |
| | | void router.push({ name: 'BpmProcessInstanceDetail', query: { id: processInstanceId } }); |
| | | } |
| | | async function retryProcess(id: number) { |
| | | try { |
| | | await retrySeedQualityBpm(id); |
| | | message.success('已鿰åèµ· BPM 审æ¹'); |
| | | refreshAll(); |
| | | } catch (error) { |
| | | message.error(scopeErrorText(error)); |
| | | } |
| | | } |
| | | const canScopeAction = computed(() => [ |
| | | 'mes:qc-seed-quality:submit', 'mes:qc-seed-quality:retest', |
| | | 'mes:qc-seed-quality:process-approve', 'mes:qc-seed-quality:company-approve', |
| | | ].some((code) => hasAccessByCodes([code]))); |
| | | const workflowLabels: Record<number, string> = { |
| | | 10: 'å¾
夿£', 20: 'å¾
åæ£å®¡æ ¸', 25: 'å¾
忣ä¸åæ ¼å¤ç', 30: 'å¾
夿£å®¡æ ¸', |
| | | 40: 'å·²æ¾è¡', 50: '忣驳åå¾
鿣', 60: 'å¾
夿£ä¸åæ ¼å¤ç', 70: 'å·²æ¥åº', |
| | | 10: 'å¾
夿£', 20: 'å¾
é¨é¨å®¡æ¹', 25: 'å¾
é¨é¨ä¸åæ ¼å®¡æ¹', 30: 'å¾
å
¬å¸è´¨æ£å®¡æ¹', |
| | | 40: 'å·²æ¾è¡', 50: '忣驳åå¾
鿣', 60: 'å¾
å
¬å¸è´¨æ£ä¸åæ ¼å¤ç', 70: 'å·²æ¥åº', |
| | | }; |
| | | /** å线质æ£ï¼åä¸è¢ç ç±å å·¥é¨ãéå®é¨åçæä¸æ¡è´¨æ£è®°å½ */ |
| | | const qcLineLabels: Record<number, string> = { 0: 'æªåºå', 1: 'å å·¥é¨', 2: 'éå®é¨' }; |
| | | const qcLineColors: Record<number, string> = { 0: 'default', 1: 'blue', 2: 'purple' }; |
| | | function qcLineLabel(value?: number) { |
| | | return qcLineLabels[value ?? 0] || 'æªåºå'; |
| | | } |
| | | /** ç»éåéè´§è´¨æ£å¤ç¨å䏿µç¨ç¶æï¼æä¸å¡ç±»åå±ç¤ºæ´åç¡®çèç¹åç§° */ |
| | | function statusLabel(record: SeedQualityRecord) { |
| | | if (record.bizType === 2) { |
| | | if (record.workflowStatus === 10) return 'å¾
夿£'; |
| | | if (record.workflowStatus === 20) return 'å¾
éå®é¨é¢å¯¼å®¡æ¹'; |
| | | if (record.workflowStatus === 40) return 'éè´§è´¨æ£éè¿'; |
| | | if (record.workflowStatus === 70) return 'éè´§è´¨æ£ä¸éè¿'; |
| | | } |
| | | return workflowLabels[record.workflowStatus] || 'æªç¥ç¶æ'; |
| | | } |
| | | function statusColor(record: SeedQualityRecord) { |
| | | if (record.workflowStatus === 40) return 'green'; |
| | | if (record.workflowStatus === 70) return 'red'; |
| | | if (record.workflowStatus === 60) return 'orange'; |
| | | return 'blue'; |
| | | } |
| | | const actionLabels: Record<string, string> = { |
| | | INITIAL: '忣', INITIAL_RECHECK: '鿰忣', RETEST: '夿£', PROCESS_APPROVE: 'åæ£å®¡æ ¸éè¿', |
| | | PROCESS_REJECT: 'åæ£å®¡æ ¸é©³å', COMPANY_APPROVE: '夿£å®¡æ ¸éè¿', |
| | |
| | | const indicatorLabels: Record<string, string> = { |
| | | purity: '纯度', cleanliness: 'å度', germination_rate: 'åè½ç', moisture: 'æ°´å', |
| | | }; |
| | | function indicatorLabel(code: string, name?: string) { |
| | | return name || indicatorLabels[code] || code; |
| | | } |
| | | function buildIndicators(list?: EffectiveIndicator[]): SeedQualityIndicator[] { |
| | | return (list || []).map((item) => ({ |
| | | indicatorCode: item.code, |
| | | indicatorName: item.name || indicatorLabels[item.code] || item.code, |
| | | unit: item.unit, |
| | | value: undefined, |
| | | minValue: Number(item.minValue), |
| | | maxValue: Number(item.maxValue), |
| | | })); |
| | | } |
| | | function actionColor(action: string) { |
| | | if (action.includes('REJECT')) return 'red'; |
| | | if (action.includes('APPROVE')) return 'green'; |
| | |
| | | label: warehouse.code ? `${warehouse.name}ï¼${warehouse.code}ï¼` : warehouse.name || '', |
| | | value: warehouse.id ?? 0, |
| | | })).filter((item) => item.value)); |
| | | const scopeReviewGroups = computed(() => { |
| | | const scope = retestScope.value; |
| | | if (!scope) return []; |
| | | return [ |
| | | { status: 20, stage: 'process' as const, ids: scope.pendingProcessIds || [] }, |
| | | { status: 25, stage: 'process' as const, ids: scope.pendingInitialFailIds || [] }, |
| | | { status: 30, stage: 'company' as const, ids: scope.pendingCompanyIds || [] }, |
| | | { status: 60, stage: 'company' as const, ids: scope.pendingCompanyFailIds || [] }, |
| | | ].filter((group) => group.ids.length && hasAccessByCodes([`mes:qc-seed-quality:${group.stage === 'process' ? 'process-approve' : 'company-approve'}`])); |
| | | }); |
| | | const scopeHasReviewPermission = computed(() => { |
| | | const scope = retestScope.value; |
| | | if (!scope) return false; |
| | | return (scope.pendingProcessCount > 0 && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])) |
| | | || (scope.pendingCompanyCount > 0 && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])); |
| | | const scopeReviewGroups = computed(() => [] as Array<{ status: number; stage: 'process' | 'company'; ids: number[] }>); |
| | | const scopeHasReviewPermission = computed(() => false); |
| | | const retestTargetLabel = computed(() => { |
| | | const record = activeRecord.value; |
| | | if (!record) return ''; |
| | | if (record.bizType === 2) return `ç»éåéè´§å ${record.returnSalesCode || record.code}`; |
| | | return `è¢ç ï¼${record.code} · ${qcLineLabel(record.qcLine)}è´¨æ£çº¿`; |
| | | }); |
| | | const reviewOpen = ref(false); |
| | | const reviewBusy = ref(false); |
| | |
| | | const selectedIds = ref<number[]>([]); |
| | | const selectedRecordMap = ref(new Map<number, SeedQualityRecord>()); |
| | | const selectedRows = computed(() => selectedIds.value.map((id) => selectedRecordMap.value.get(id)).filter((row): row is SeedQualityRecord => !!row)); |
| | | const reviewTitle = computed(() => { |
| | | const record = activeRecord.value; |
| | | if (record?.bizType === 2) return record.workflowStatus === 20 ? 'éå®é¨é¢å¯¼å®¡æ¹' : 'éè´§å¤æ£ä¸åæ ¼å¤ç'; |
| | | if ([25, 60].includes(record?.workflowStatus ?? 0)) return 'ä¸åæ ¼å¤ç'; |
| | | return reviewStage.value === 'process' ? 'åæ£å®¡æ ¸' : 'å
¬å¸è´¨æ£å®¡æ¹'; |
| | | }); |
| | | const reviewGroups = computed(() => [20, 25, 30, 60].map((status) => ({ |
| | | status, |
| | | rows: selectedRows.value.filter((row) => row.workflowStatus === status), |
| | | stage: status === 20 || status === 25 ? 'process' as const : 'company' as const, |
| | | })).filter((group) => group.rows.length && hasAccessByCodes([`mes:qc-seed-quality:${group.stage === 'process' ? 'process-approve' : 'company-approve'}`]))); |
| | | })).filter((group) => group.rows.length && group.rows.every((row) => row.qcLine === 0) |
| | | && hasAccessByCodes([`mes:qc-seed-quality:${group.stage === 'process' ? 'process-approve' : 'company-approve'}`]))); |
| | | const retestGroups = computed(() => { |
| | | if (!hasAccessByCodes(['mes:qc-seed-quality:retest'])) return []; |
| | | const groups = new Map<string, { status: 10 | 50; batchCode: string; rows: SeedQualityRecord[] }>(); |
| | | for (const row of selectedRows.value) { |
| | | if (row.workflowStatus !== 10 && row.workflowStatus !== 50) continue; |
| | | if (!row.batchCode) continue; |
| | | const key = `${row.workflowStatus}:${row.batchCode}`; |
| | | const key = `${row.workflowStatus}:${row.batchCode}:${row.qcLine ?? 0}`; |
| | | if (!groups.has(key)) groups.set(key, { status: row.workflowStatus, batchCode: row.batchCode, rows: [] }); |
| | | groups.get(key)!.rows.push(row); |
| | | } |
| | | return [...groups.values()]; |
| | | }); |
| | | function changeSelection(keys: (string | number)[]) { |
| | | const nextIds = keys.map(Number); |
| | | const nextMap = new Map(selectedRecordMap.value); |
| | | for (const row of records.value) if (nextIds.includes(row.id)) nextMap.set(row.id, row); |
| | | for (const id of nextMap.keys()) if (!nextIds.includes(id)) nextMap.delete(id); |
| | | function groupKey(group: SeedQualityBatchGroup) { |
| | | return group.batchCode || '__unbound__'; |
| | | } |
| | | function isExpanded(group: SeedQualityBatchGroup) { |
| | | return expandedKeys.value.includes(groupKey(group)); |
| | | } |
| | | function bucketOf(group: SeedQualityBatchGroup) { |
| | | const key = groupKey(group); |
| | | buckets.value[key] ??= { list: [], total: 0, page: 1, pageSize: 10, loading: false }; |
| | | return buckets.value[key]!; |
| | | } |
| | | function syncSelection() { |
| | | const ids = new Set(Object.values(batchSelections.value).flat()); |
| | | const nextMap = new Map<number, SeedQualityRecord>(); |
| | | for (const bucket of Object.values(buckets.value)) { |
| | | for (const row of bucket.list) if (ids.has(row.id)) nextMap.set(row.id, row); |
| | | } |
| | | for (const [id, row] of selectedRecordMap.value) { |
| | | if (ids.has(id) && !nextMap.has(id)) nextMap.set(id, row); |
| | | } |
| | | selectedIds.value = [...ids]; |
| | | selectedRecordMap.value = nextMap; |
| | | selectedIds.value = nextIds; |
| | | } |
| | | function changeGroupSelection(group: SeedQualityBatchGroup, keys: (number | string)[]) { |
| | | batchSelections.value = { ...batchSelections.value, [groupKey(group)]: keys.map(Number) }; |
| | | syncSelection(); |
| | | } |
| | | function selectionConfig(group: SeedQualityBatchGroup) { |
| | | return { |
| | | selectedRowKeys: batchSelections.value[groupKey(group)] || [], |
| | | preserveSelectedRowKeys: true, |
| | | onChange: (keys: (number | string)[]) => changeGroupSelection(group, keys), |
| | | }; |
| | | } |
| | | function clearSelection() { |
| | | selectedIds.value = []; |
| | | selectedRecordMap.value = new Map(); |
| | | batchSelections.value = {}; |
| | | } |
| | | function removeSelection(ids: number[]) { |
| | | const processed = new Set(ids); |
| | | const next: Record<string, number[]> = {}; |
| | | for (const [key, value] of Object.entries(batchSelections.value)) { |
| | | const remain = value.filter((id) => !processed.has(id)); |
| | | if (remain.length) next[key] = remain; |
| | | } |
| | | batchSelections.value = next; |
| | | selectedIds.value = selectedIds.value.filter((id) => !processed.has(id)); |
| | | selectedRecordMap.value = new Map([...selectedRecordMap.value].filter(([id]) => !processed.has(id))); |
| | | } |
| | |
| | | activeRecord.value = record; |
| | | try { |
| | | const previous = await getSeedQualityIndicators(record.id); |
| | | const ranges = await getSeedIndicatorRanges().catch(() => []); |
| | | retestIndicators.value = previous.map((item) => { |
| | | const range = ranges.find((row) => row.indicatorCode === item.indicatorCode); |
| | | return { ...item, minValue: range ? Number(range.minValue) : Number(item.minValue), maxValue: range ? Number(range.maxValue) : Number(item.maxValue) }; |
| | | }); |
| | | if (retestIndicators.value.length !== 4) throw new Error('è´¨æ£ææ ä¸å®æ´'); |
| | | retestIndicators.value = retestIndicators.value.map((item) => ({ ...item, value: undefined })); |
| | | retestIndicators.value = previous.map((item) => ({ |
| | | ...item, |
| | | indicatorName: indicatorLabel(item.indicatorCode, item.indicatorName), |
| | | value: undefined, |
| | | })); |
| | | if (!retestIndicators.value.length) throw new Error('è´¨æ£ææ è®°å½ç¼ºå¤±'); |
| | | retestReason.value = ''; |
| | | retestOpen.value = true; |
| | | } catch (error) { |
| | |
| | | const scannedCode = scopeCode.value.trim(); |
| | | let data: SeedRetestScope; |
| | | try { |
| | | data = await resolveSeedRetestScope(scannedCode, scopeType.value); |
| | | data = await resolveSeedRetestScope(scannedCode, scopeType.value, qcLine.value === 2 ? 2 : 1); |
| | | } catch (error) { |
| | | if (!scopeErrorText(error).startsWith('æªæ¾å°')) throw error; |
| | | const otherType = scopeType.value === 'PALLET' ? 'BATCH' : 'PALLET'; |
| | | try { |
| | | data = await resolveSeedRetestScope(scannedCode, otherType); |
| | | data = await resolveSeedRetestScope(scannedCode, otherType, qcLine.value === 2 ? 2 : 1); |
| | | scopeType.value = otherType; |
| | | } catch (otherError) { |
| | | if (scopeErrorText(otherError).startsWith('æªæ¾å°')) { |
| | |
| | | } |
| | | await hydrateScopeReviewIds(data, scannedCode); |
| | | retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50; |
| | | initialScope.value = await resolveSeedQuality(scannedCode, data.scanType); |
| | | const ranges = await getSeedIndicatorRanges().catch(() => []); |
| | | const emptyIndicators = () => Object.entries(indicatorLabels).map(([indicatorCode]) => { |
| | | const range = ranges.find((row) => row.indicatorCode === indicatorCode); |
| | | return { indicatorCode, value: undefined, minValue: range ? Number(range.minValue) : undefined as unknown as number, |
| | | maxValue: range ? Number(range.maxValue) : undefined as unknown as number }; |
| | | }); |
| | | retestIndicators.value = emptyIndicators(); |
| | | initialIndicators.value = emptyIndicators(); |
| | | initialScope.value = await resolveSeedQuality(scannedCode, data.scanType, qcLine.value === 2 ? 2 : 1); |
| | | retestIndicators.value = buildIndicators(data.indicators); |
| | | initialIndicators.value = buildIndicators(initialScope.value?.indicators); |
| | | retestReason.value = ''; |
| | | initialReason.value = ''; |
| | | } catch (error) { scopeError.value = scopeErrorText(error); } |
| | |
| | | const target = initialScope.value; |
| | | if (!target || !target.inbound || !initialSubmitIds.value.length) return; |
| | | if (!qualifiedWarehouseId.value) { scopeError.value = 'è¯·éæ©æ£éªåæ ¼åå
¥åºçåæ ¼åº'; return; } |
| | | if (initialIndicators.value.some((item) => item.value === undefined || item.minValue === undefined || item.maxValue === undefined |
| | | || Number(item.value) < 0 || Number(item.value) > 100 || Number(item.minValue) < 0 || Number(item.maxValue) > 100 |
| | | if (initialIndicators.value.length === 0 || initialIndicators.value.some((item) => item.value === undefined || item.minValue === undefined || item.maxValue === undefined |
| | | || Number(item.value) < 0 || Number(item.minValue) < 0 || Number(item.maxValue) < 0 |
| | | || Number(item.minValue) > Number(item.maxValue))) { |
| | | scopeError.value = '请填ååé¡¹ææ ç宿µå¼å 0â100 èå´å
çåæ ¼ä¸ä¸é'; return; |
| | | scopeError.value = '请填åå
¨é¨è´¨æ£é¡¹ç宿µå¼åéè´çåæ ¼ä¸ä¸é'; return; |
| | | } |
| | | if (!initialPassed.value && !initialReason.value.trim()) { scopeError.value = '忣ä¸åæ ¼æ¶è¯·å¡«ååå '; return; } |
| | | scopeError.value = ''; |
| | |
| | | scopeBusy.value = true; |
| | | try { |
| | | const submitted = await submitSeedQualityGroup({ |
| | | code: target.code, batchCode: target.batchCode, scanType: target.scanType || scopeType.value, |
| | | code: target.code, qcLine: qcLine.value === 2 ? 2 : 1, |
| | | batchCode: target.batchCode, scanType: target.scanType || scopeType.value, |
| | | bagIds: initialSubmitIds.value, qualifiedWarehouseId: qualifiedWarehouseId.value, |
| | | reason: initialReason.value.trim() || undefined, |
| | | indicators: initialIndicators.value.map((item) => ({ code: item.indicatorCode, value: Number(item.value), |
| | |
| | | } |
| | | async function saveScopeRetest() { |
| | | if (!retestScope.value || !scopeSubmitIds.value.length) return; |
| | | if (retestIndicators.value.some(item => item.value === undefined || item.value === null || item.minValue === undefined || item.maxValue === undefined |
| | | || Number(item.minValue) < 0 || Number(item.maxValue) > 100 || Number(item.minValue) > Number(item.maxValue))) { |
| | | message.warning('请填ååé¡¹ææ ç宿µå¼ä¸æ¬æ¬¡åæ ¼èå´'); return; |
| | | if (retestIndicators.value.length === 0 || retestIndicators.value.some(item => item.value === undefined || item.value === null || item.minValue === undefined || item.maxValue === undefined |
| | | || Number(item.minValue) < 0 || Number(item.maxValue) < 0 || Number(item.minValue) > Number(item.maxValue))) { |
| | | message.warning('请填åå
¨é¨è´¨æ£é¡¹ç宿µå¼ä¸æ¬æ¬¡åæ ¼èå´'); return; |
| | | } |
| | | const passed = retestIndicators.value.every(item => Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue)); |
| | | if (!passed && !retestReason.value.trim()) { message.warning('ä¸åæ ¼è¯·å¡«ååå '); return; } |
| | | scopeBusy.value = true; |
| | | try { |
| | | const result = await submitSeedRetestScope({ code: retestScope.value.code, scanType: retestScope.value.scanType, workflowStatus: scopeStatus.value, |
| | | const result = await submitSeedRetestScope({ code: retestScope.value.code, scanType: retestScope.value.scanType, |
| | | qcLine: qcLine.value === 2 ? 2 : 1, workflowStatus: scopeStatus.value, |
| | | qualityIds: scopeSubmitIds.value, reason: retestReason.value.trim(), indicators: retestIndicators.value.map(item => ({ |
| | | code: item.indicatorCode, value: Number(item.value), minValue: Number(item.minValue), maxValue: Number(item.maxValue), |
| | | })) }); |
| | |
| | | value === 1 ? '#ecfdf3' : value === 2 ? '#fef2f2' : '#fffbeb'; |
| | | const formatDateTime = (value?: string) => |
| | | value ? new Date(value).toLocaleString() : 'â'; |
| | | const formatDate = (value?: string) => (value ? value.slice(0, 10) : 'â'); |
| | | const showGroupTotal = (count: number) => `å
± ${count} ä¸ªæ¹æ¬¡`; |
| | | |
| | | async function loadRecords() { |
| | | loading.value = true; |
| | | function batchQuery() { |
| | | return { |
| | | code: code.value.trim() || undefined, |
| | | batchCode: batchCode.value.trim() || undefined, |
| | | result: result.value, |
| | | workflowStatus: workflowStatus.value, |
| | | qcLine: qcLine.value, |
| | | }; |
| | | } |
| | | |
| | | async function loadGroups() { |
| | | groupLoading.value = true; |
| | | try { |
| | | const data = await getSeedQualityBatchPage({ |
| | | pageNo: batchPage.value, |
| | | pageSize: batchPageSize.value, |
| | | ...batchQuery(), |
| | | }); |
| | | groups.value = data.list; |
| | | groupTotal.value = data.total; |
| | | const keys = groups.value.map((group) => groupKey(group)); |
| | | expandedKeys.value = expandedKeys.value.filter((key) => keys.includes(key)); |
| | | if (!expandedKeys.value.length && groups.value[0] && Object.keys(buckets.value).length === 0) { |
| | | expandedKeys.value = [groupKey(groups.value[0])]; |
| | | } |
| | | await Promise.all(groups.value.filter((group) => isExpanded(group)).map((group) => loadBucket(group))); |
| | | } finally { |
| | | groupLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadBucket(group: SeedQualityBatchGroup, page?: number, pageSize?: number) { |
| | | const bucket = bucketOf(group); |
| | | if (page) bucket.page = page; |
| | | if (pageSize) bucket.pageSize = pageSize; |
| | | bucket.loading = true; |
| | | try { |
| | | const data = await getSeedQualityPage({ |
| | | pageNo: page.value, |
| | | pageSize: pageSize.value, |
| | | code: code.value.trim() || undefined, |
| | | batchCode: batchCode.value.trim() || undefined, |
| | | result: result.value, |
| | | workflowStatus: workflowStatus.value, |
| | | pageNo: bucket.page, |
| | | pageSize: bucket.pageSize, |
| | | ...batchQuery(), |
| | | batchCode: undefined, |
| | | exactBatchCode: group.batchCode || undefined, |
| | | unbound: !group.batchCode, |
| | | }); |
| | | records.value = data.list; |
| | | total.value = data.total; |
| | | bucket.list = data.list; |
| | | bucket.total = data.total; |
| | | } finally { |
| | | loading.value = false; |
| | | bucket.loading = false; |
| | | } |
| | | } |
| | | |
| | | async function toggleGroup(group: SeedQualityBatchGroup) { |
| | | const key = groupKey(group); |
| | | if (isExpanded(group)) { |
| | | expandedKeys.value = expandedKeys.value.filter((item) => item !== key); |
| | | return; |
| | | } |
| | | expandedKeys.value = [...expandedKeys.value, key]; |
| | | await loadBucket(group); |
| | | } |
| | | |
| | | function tablePagination(group: SeedQualityBatchGroup): TablePaginationConfig { |
| | | const bucket = buckets.value[groupKey(group)]; |
| | | return { |
| | | current: bucket?.page ?? 1, |
| | | pageSize: bucket?.pageSize ?? 10, |
| | | total: bucket?.total ?? 0, |
| | | showSizeChanger: true, |
| | | showTotal: (count: number) => `å
± ${count} æ¡`, |
| | | onChange: (nextPage: number, nextPageSize: number) => |
| | | void loadBucket(group, nextPage, nextPageSize), |
| | | }; |
| | | } |
| | | |
| | | async function loadStats() { |
| | | try { |
| | | const [all, pass, fail] = await Promise.all([ |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1 }), |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 40 }), |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 70 }), |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1, qcLine: qcLine.value }), |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1, qcLine: qcLine.value, workflowStatus: 40 }), |
| | | getSeedQualityPage({ pageNo: 1, pageSize: 1, qcLine: qcLine.value, workflowStatus: 70 }), |
| | | ]); |
| | | statsTotal.value = all.total; |
| | | passTotal.value = pass.total; |
| | |
| | | } |
| | | |
| | | function searchRecords() { |
| | | page.value = 1; |
| | | batchPage.value = 1; |
| | | clearSelection(); |
| | | void loadRecords(); |
| | | buckets.value = {}; |
| | | expandedKeys.value = []; |
| | | void loadGroups(); |
| | | } |
| | | |
| | | function resetFilters() { |
| | |
| | | batchCode.value = ''; |
| | | result.value = undefined; |
| | | workflowStatus.value = undefined; |
| | | // åºå®è´¨æ£çº¿çèå页éç½®æ¶ä¿çå½åè´¨æ£çº¿ |
| | | if (props.initialQcLine === undefined) { |
| | | qcLine.value = undefined; |
| | | } |
| | | searchRecords(); |
| | | void loadStats(); |
| | | } |
| | | |
| | | function refreshAll() { |
| | | void Promise.all([loadRecords(), loadStats()]); |
| | | void Promise.all([loadGroups(), loadStats()]); |
| | | } |
| | | function refreshAndClearSelection() { |
| | | clearSelection(); |
| | | refreshAll(); |
| | | } |
| | | |
| | | function handleTableChange(pagination: TablePaginationConfig) { |
| | | page.value = pagination.current || 1; |
| | | pageSize.value = pagination.pageSize || 10; |
| | | void loadRecords(); |
| | | function handleGroupPageChange(nextPage: number, nextSize: number) { |
| | | batchPage.value = nextPage; |
| | | batchPageSize.value = nextSize; |
| | | clearSelection(); |
| | | buckets.value = {}; |
| | | void loadGroups(); |
| | | } |
| | | |
| | | onMounted(refreshAll); |
| | |
| | | <header class="page-header"> |
| | | <div class="header-copy"> |
| | | <div class="eyebrow">MES / è´¨é管ç</div> |
| | | <h1>è´¨æ£è®°å½</h1> |
| | | <p>åæ£å®¡æ ¸éè¿åè¿è¡å¤æ£ï¼å¤æ£å®¡æ ¸éè¿åææ¾è¡å
¥åºæå¤ç½®ä¸åæ ¼å</p> |
| | | <h1>{{ pageTitle }}</h1> |
| | | <p>æ¯è¢ä¿çå å·¥é¨ãéå®é¨ä¸¤æ¡è´¨æ£çº¿ï¼åæ£ä¸åæ ¼å
夿£ï¼å线é¨é¨å®¡æ¹å®æåè¿å
¥å
¬å¸è´¨æ£å®¡æ¹</p> |
| | | </div> |
| | | <div class="header-badge"> |
| | | <span class="badge-pulse" /> |
| | |
| | | <section class="stat-grid" aria-label="è´¨æ£ç»ææ±æ»"> |
| | | <div class="stat-card total"> |
| | | <div class="stat-icon"><IconifyIcon icon="ep:document" /></div> |
| | | <div class="stat-main"><span>è®°å½æ»æ°</span><strong>{{ statsTotal.toLocaleString() }}</strong><small>æè¢ç 计</small></div> |
| | | <div class="stat-main"><span>è®°å½æ»æ°</span><strong>{{ statsTotal.toLocaleString() }}</strong><small>æè´¨æ£è®°å½è®¡</small></div> |
| | | </div> |
| | | <div class="stat-card pass"> |
| | | <div class="stat-icon"><IconifyIcon icon="ep:success-filled" /></div> |
| | |
| | | <div class="card-toolbar"> |
| | | <div class="card-title"> |
| | | <IconifyIcon icon="ep:list" /> |
| | | æç»å表 |
| | | <span class="count-chip">{{ total.toLocaleString() }} æ¡</span> |
| | | æçäº§æ¹æ¬¡æ¥ç |
| | | <span class="count-chip">{{ groupTotal.toLocaleString() }} ä¸ªæ¹æ¬¡</span> |
| | | </div> |
| | | <div class="filters"> |
| | | <a-button v-if="canScopeAction" class="desktop-scope-action" type="primary" @click="openScopeRetest">ææç / æ¹æ¬¡æ¹éå¤ç</a-button> |
| | |
| | | v-for="group in retestGroups" |
| | | :key="`retest-${group.status}-${group.batchCode}`" |
| | | @click="openBatchRetest(group)" |
| | | >{{ group.status === 50 ? '鿰忣' : '夿£' }} · {{ group.batchCode }} · {{ group.rows.length }} è¢</a-button> |
| | | >{{ group.status === 50 ? '鿰忣' : '夿£' }} · {{ group.batchCode }} · {{ group.rows.length }} æ¡è®°å½</a-button> |
| | | <span v-if="!reviewGroups.length && !retestGroups.length" class="muted">æéè®°å½å½å没æå¯æ¹éå¤ççæä½ï¼è¯·æ¥çæµç¨ç¶ææåæ¡æä½ã</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-table |
| | | :data-source="records" |
| | | :row-selection="{ selectedRowKeys: selectedIds, preserveSelectedRowKeys: true, onChange: changeSelection }" |
| | | :loading="loading" |
| | | :scroll="{ x: 1900 }" |
| | | :pagination="{ |
| | | current: page, |
| | | pageSize, |
| | | total, |
| | | showSizeChanger: true, |
| | | showTotal: (t: number) => `å
± ${t} æ¡`, |
| | | }" |
| | | row-key="id" |
| | | size="middle" |
| | | @change="handleTableChange" |
| | | > |
| | | <template #emptyText> |
| | | <div class="empty-state"> |
| | | <IconifyIcon icon="ep:data-board" /> |
| | | <p>ææ è´¨æ£è®°å½</p> |
| | | </div> |
| | | </template> |
| | | <a-table-column key="id" title="è´¨æ£ç¼å·" data-index="id" width="100" align="right"> |
| | | <template #default="{ record }"><span class="mono muted">{{ record.id }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="code" title="è¢ç " data-index="code" min-width="220"> |
| | | <template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="palletCode" title="æçç " data-index="palletCode" min-width="190"> |
| | | <template #default="{ record }"><span class="mono">{{ record.palletCode || 'â' }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="batchCode" title="çäº§æ¹æ¬¡" data-index="batchCode" min-width="190"> |
| | | <template #default="{ record }">{{ record.batchCode || 'â' }}</template> |
| | | </a-table-column> |
| | | <a-table-column key="result" title="æ£éªç»æ" width="130"> |
| | | <template #default="{ record }"> |
| | | <span class="result-tag" :style="{ color: resultColor(record.result), background: resultBg(record.result) }"> |
| | | <i class="result-dot" :style="{ background: resultColor(record.result) }" /> |
| | | {{ resultLabel(record.result) }} |
| | | <a-spin :spinning="groupLoading"> |
| | | <div v-if="!groups.length && !groupLoading" class="empty-state"> |
| | | <IconifyIcon icon="ep:data-board" /> |
| | | <p>ææ è´¨æ£è®°å½</p> |
| | | </div> |
| | | <div |
| | | v-for="group in groups" |
| | | :key="groupKey(group)" |
| | | class="batch-group" |
| | | :class="{ expanded: isExpanded(group) }" |
| | | > |
| | | <div |
| | | class="batch-head" |
| | | role="button" |
| | | tabindex="0" |
| | | @click="toggleGroup(group)" |
| | | @keydown.enter="toggleGroup(group)" |
| | | @keydown.space.prevent="toggleGroup(group)" |
| | | > |
| | | <span class="batch-chevron"> |
| | | <IconifyIcon :icon="isExpanded(group) ? 'ep:arrow-down' : 'ep:arrow-right'" /> |
| | | </span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="workflowStatus" title="æµç¨ç¶æ" width="190"> |
| | | <template #default="{ record }"><a-tag color="blue">{{ workflowLabels[record.workflowStatus] || 'æªç¥ç¶æ' }}</a-tag></template> |
| | | </a-table-column> |
| | | <a-table-column key="reason" title="ä¸åæ ¼åå " data-index="reason" min-width="220" ellipsis> |
| | | <template #default="{ record }"> |
| | | <span v-if="record.reason" class="reason-text">{{ record.reason }}</span> |
| | | <span v-else class="muted">â</span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="inspectedAt" title="æ£éªæ¶é´" width="190"> |
| | | <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.inspectedAt) }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="createTime" title="æäº¤æ¶é´" width="190"> |
| | | <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="actions" title="æä½" width="260" fixed="right"> |
| | | <template #default="{ record }"> |
| | | <a-space> |
| | | <a-button v-if="[10, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">{{ record.workflowStatus === 50 ? '鿰忣' : '夿£' }}</a-button> |
| | | <a-button v-if="[20, 25].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">{{ record.workflowStatus === 25 ? 'ä¸åæ ¼å¤ç' : 'åæ£å®¡æ ¸' }}</a-button> |
| | | <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">{{ record.workflowStatus === 60 ? 'ä¸åæ ¼å¤ç' : '夿£å®¡æ ¸' }}</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:query'])" type="link" @click="openHistory(record)">æµç¨è®°å½</a-button> |
| | | </a-space> |
| | | </template> |
| | | </a-table-column> |
| | | </a-table> |
| | | <span class="batch-code">{{ group.batchLabel }}</span> |
| | | <span class="batch-meta"> |
| | | <span v-if="group.itemName">åç§ {{ group.itemName }}</span> |
| | | <span v-if="group.lineName">{{ group.lineName }}</span> |
| | | <span v-if="group.produceDate">ç产 {{ formatDate(group.produceDate) }}</span> |
| | | <span v-if="group.lastInspectedAt">æè¿æ£éª {{ formatDateTime(group.lastInspectedAt) }}</span> |
| | | </span> |
| | | <span class="batch-counts"> |
| | | <span class="count-pill total">å
± {{ group.totalCount }} è¢</span> |
| | | <span class="count-pill pass">åæ ¼ {{ group.passCount }}</span> |
| | | <span class="count-pill fail">ä¸åæ ¼ {{ group.failCount }}</span> |
| | | <span class="count-pill pending">å¤çä¸ {{ group.processingCount }}</span> |
| | | </span> |
| | | <span class="batch-flags"> |
| | | <a-tag v-if="group.pendingProcessReviewCount" color="orange">å¾
åæ£å®¡æ ¸ {{ group.pendingProcessReviewCount }}</a-tag> |
| | | <a-tag v-if="group.pendingCompanyReviewCount" color="gold">å¾
å
¬å¸è´¨æ£å®¡æ¹ {{ group.pendingCompanyReviewCount }}</a-tag> |
| | | <a-tag v-if="group.pendingRetestCount" color="blue">å¾
æ£éª {{ group.pendingRetestCount }}</a-tag> |
| | | <a-tag v-if="group.releasedCount" color="green">å·²æ¾è¡ {{ group.releasedCount }}</a-tag> |
| | | <a-tag v-if="group.scrappedCount" color="red">å·²æ¥åº {{ group.scrappedCount }}</a-tag> |
| | | </span> |
| | | </div> |
| | | <div v-if="isExpanded(group)" class="batch-body"> |
| | | <a-table |
| | | :data-source="buckets[groupKey(group)]?.list || []" |
| | | :loading="buckets[groupKey(group)]?.loading" |
| | | :pagination="tablePagination(group)" |
| | | :row-selection="selectionConfig(group)" |
| | | :scroll="{ x: 1500 }" |
| | | row-key="id" |
| | | size="middle" |
| | | > |
| | | <template #emptyText> |
| | | <div class="empty-state"> |
| | | <IconifyIcon icon="ep:data-board" /> |
| | | <p>è¯¥æ¹æ¬¡ä¸ææ å¹é
çè´¨æ£è®°å½</p> |
| | | </div> |
| | | </template> |
| | | <a-table-column key="id" title="è´¨æ£ç¼å·" data-index="id" width="100" align="right"> |
| | | <template #default="{ record }"><span class="mono muted">{{ record.id }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="code" title="è¢ç " data-index="code" min-width="220"> |
| | | <template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="palletCode" title="æçç " data-index="palletCode" min-width="190"> |
| | | <template #default="{ record }"><span class="mono">{{ record.palletCode || 'â' }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="qcLine" title="è´¨æ£çº¿" width="150"> |
| | | <template #default="{ record }"> |
| | | <a-space> |
| | | <a-tag :color="qcLineColors[record.qcLine ?? 0]">{{ qcLineLabel(record.qcLine) }}</a-tag> |
| | | <a-tooltip v-if="record.bizType === 2" :title="`ç»éåéè´§å ${record.returnSalesCode || record.code}`"> |
| | | <a-tag color="magenta">éè´§</a-tag> |
| | | </a-tooltip> |
| | | </a-space> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="result" title="æ£éªç»æ" width="130"> |
| | | <template #default="{ record }"> |
| | | <span class="result-tag" :style="{ color: resultColor(record.result), background: resultBg(record.result) }"> |
| | | <i class="result-dot" :style="{ background: resultColor(record.result) }" /> |
| | | {{ resultLabel(record.result) }} |
| | | </span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="workflowStatus" title="æµç¨ç¶æ" width="190"> |
| | | <template #default="{ record }"><a-tag :color="statusColor(record)">{{ statusLabel(record) }}</a-tag></template> |
| | | </a-table-column> |
| | | <a-table-column key="reason" title="ä¸åæ ¼åå " data-index="reason" min-width="220" ellipsis> |
| | | <template #default="{ record }"> |
| | | <span v-if="record.reason" class="reason-text">{{ record.reason }}</span> |
| | | <span v-else class="muted">â</span> |
| | | </template> |
| | | </a-table-column> |
| | | <a-table-column key="inspectedAt" title="æ£éªæ¶é´" width="190"> |
| | | <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.inspectedAt) }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="createTime" title="æäº¤æ¶é´" width="190"> |
| | | <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template> |
| | | </a-table-column> |
| | | <a-table-column key="actions" title="æä½" width="260" fixed="right"> |
| | | <template #default="{ record }"> |
| | | <a-space> |
| | | <a-button v-if="[10, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">{{ record.workflowStatus === 50 ? '鿰忣' : '夿£' }}</a-button> |
| | | <a-button v-if="[20, 25].includes(record.workflowStatus)" type="link" @click="openProcess(record.processInstanceId)">æ¥çé¨é¨å®¡æ¹</a-button> |
| | | <a-button v-if="[30, 60].includes(record.workflowStatus)" type="link" @click="openProcess(record.companyProcessInstanceId)">æ¥çå
¬å¸å®¡æ¹</a-button> |
| | | <a-button v-if="[20, 25, 30, 60].includes(record.workflowStatus) && !(record.workflowStatus === 20 || record.workflowStatus === 25 ? record.processInstanceId : record.companyProcessInstanceId) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="retryProcess(record.id)">éæ°å起审æ¹</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:query'])" type="link" @click="openHistory(record)">æµç¨è®°å½</a-button> |
| | | </a-space> |
| | | </template> |
| | | </a-table-column> |
| | | </a-table> |
| | | </div> |
| | | </div> |
| | | </a-spin> |
| | | <div v-if="groupTotal > 0" class="group-pagination"> |
| | | <a-pagination |
| | | :current="batchPage" |
| | | :page-size="batchPageSize" |
| | | :total="groupTotal" |
| | | show-size-changer |
| | | :page-size-options="['10', '20', '50']" |
| | | :show-total="showGroupTotal" |
| | | @change="handleGroupPageChange" |
| | | /> |
| | | </div> |
| | | </section> |
| | | <a-modal v-model:open="retestOpen" wrap-class-name="seed-quality-modal" :title="activeRecord?.workflowStatus === 50 ? '鿰忣' : 'ç§åè´¨æ£å¤æ£'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? 'æäº¤åæ£' : 'æäº¤å¤æ£'" @ok="saveRetest"> |
| | | <p class="muted">{{ retestBatchIds.length ? `æ¬æ¬¡å¤ç ${retestBatchIds.length} è¢ï¼åä¸ç»ææ ä¸ç»è®ºä¼åºç¨äºå
¨é¨æéè¢ç ã` : `è¢ç ï¼${activeRecord?.code}ã` }}请å½å
¥æ¬æ¬¡å®æµå¼å¹¶æ ¸å¯¹åæ ¼èå´ã</p> |
| | | <a-modal v-model:open="retestOpen" wrap-class-name="seed-quality-modal" :title="activeRecord?.workflowStatus === 50 ? '鿰忣' : activeRecord?.bizType === 2 ? 'ç»éåéè´§å¤æ£' : 'ç§åè´¨æ£å¤æ£'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? 'æäº¤åæ£' : 'æäº¤å¤æ£'" @ok="saveRetest"> |
| | | <p class="muted">{{ retestBatchIds.length ? `æ¬æ¬¡å¤ç ${retestBatchIds.length} æ¡è®°å½ï¼åä¸ç»ææ ä¸ç»è®ºä¼åºç¨äºå
¨é¨æéè®°å½ã` : `${retestTargetLabel}ã` }}请å½å
¥æ¬æ¬¡å®æµå¼å¹¶æ ¸å¯¹åæ ¼èå´ã</p> |
| | | <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] || item.indicatorCode }}</strong> |
| | | <strong>{{ item.indicatorName || indicatorLabel(item.indicatorCode) }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="æ¬æ¬¡å®æµå¼" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="æ¬æ¬¡æ£éªä¸åæ ¼æ¶å¡«ååå " /> |
| | | </a-modal> |
| | |
| | | <template v-if="initialIds.length"> |
| | | <a-alert v-if="!initialScope?.inbound" type="warning" show-icon message="该èå´çææè¢ç å°æªå
¨é¨å
¥åºï¼æä¸è½æäº¤åæ£" /> |
| | | <template v-else-if="hasAccessByCodes(['mes:qc-seed-quality:submit'])"> |
| | | <p class="muted">åé¡¹ææ åç»è®ºä¼åºç¨äºæ¬æ¬¡ {{ initialSubmitIds.length }} è¢ï¼è¶
è¿ 500 è¢æ¶ç³»ç»ä¼åæ¹å¤çå¹¶èªå¨å è½½å©ä½è®°å½ã</p> |
| | | <p class="muted">å
¨é¨è´¨æ£é¡¹åç»è®ºä¼åºç¨äºæ¬æ¬¡ {{ initialSubmitIds.length }} è¢ï¼è¶
è¿ 500 è¢æ¶ç³»ç»ä¼åæ¹å¤çå¹¶èªå¨å è½½å©ä½è®°å½ã</p> |
| | | <a-form-item label="æ£éªåæ ¼åå
¥åºçåæ ¼åº" required> |
| | | <a-select v-model:value="qualifiedWarehouseId" :options="qualifiedWarehouseOptions" placeholder="è¯·éæ©åæ ¼åº" style="width: 100%" /> |
| | | </a-form-item> |
| | | <a-alert v-if="!qualifiedWarehouseOptions.length" type="warning" show-icon message="ææ å¯ç¨åæ ¼åºï¼è¯·è系管çåé
ç½®" /> |
| | | <div v-for="item in initialIndicators" :key="`initial-${item.indicatorCode}`" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :max="100" :precision="3" placeholder="宿µå¼" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <strong>{{ item.indicatorName || indicatorLabels[item.indicatorCode] }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="宿µå¼" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | </div> |
| | | <a-textarea v-model:value="initialReason" :rows="2" :maxlength="500" :placeholder="initialPassed ? 'åæ ¼ç»ææ éå¡«ååå ' : '忣ä¸åæ ¼æ¶å¡«ååå '" /> |
| | | <div class="scope-actions"><a-button type="primary" :loading="scopeBusy" @click="saveScopeInitial">æäº¤æ¬æ¬¡ {{ initialSubmitIds.length }} è¢åæ£</a-button></div> |
| | |
| | | </a-radio-group> |
| | | <p class="muted">åä¸ç»ææ å°åºç¨äºæ¬æ¬¡ {{ scopeSubmitIds.length }} è¢ï¼è¶
è¿ 500 è¢æ¶å®æåç»§ç»å¤çå©ä½è®°å½ã模æ¿èå´å¯ä»¥ä¿®æ¹ã</p> |
| | | <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :max="100" :precision="3" placeholder="宿µå¼" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <strong>{{ item.indicatorName || indicatorLabels[item.indicatorCode] }}</strong> |
| | | <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="宿µå¼" /> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="2" :maxlength="500" placeholder="ä¸åæ ¼æ¶å¡«ååå " /> |
| | | <div class="scope-actions"><a-button type="primary" :loading="scopeBusy" @click="saveScopeRetest">æäº¤æ¬æ¬¡ {{ scopeSubmitIds.length }} è¢æ£éª</a-button></div> |
| | |
| | | <a-alert v-if="!initialIds.length && !scopePendingCount && !retestScope.retestIds.length && !retestScope.initialRecheckIds.length" type="success" show-icon message="å½åèå´æ²¡æå¾
å¤ççè´¨æ£è®°å½" /> |
| | | </template> |
| | | </a-modal> |
| | | <a-modal v-model:open="reviewOpen" wrap-class-name="seed-quality-modal" :title="[25, 60].includes(activeRecord?.workflowStatus ?? 0) ? 'ä¸åæ ¼å¤ç' : reviewStage === 'process' ? 'åæ£å®¡æ ¸' : '夿£å®¡æ ¸'" :confirm-loading="reviewBusy" :ok-text="reviewApproved ? '确认å¤ç' : '确认驳å'" @ok="saveReview"> |
| | | <p>{{ reviewFromScope || reviewIds.length > 1 ? `å
± ${reviewIds.length} æ¡è®°å½` : activeRecord?.code }} · {{ workflowLabels[activeRecord?.workflowStatus ?? 0] }}</p> |
| | | <a-modal v-model:open="reviewOpen" wrap-class-name="seed-quality-modal" :title="reviewTitle" :confirm-loading="reviewBusy" :ok-text="reviewApproved ? '确认å¤ç' : '确认驳å'" @ok="saveReview"> |
| | | <p>{{ reviewFromScope || reviewIds.length > 1 ? `å
± ${reviewIds.length} æ¡è®°å½` : activeRecord?.code }} · {{ activeRecord ? statusLabel(activeRecord) : '' }}</p> |
| | | <a-radio-group v-model:value="reviewApproved" class="review-choice"> |
| | | <a-radio :value="true">{{ reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '确认ä¸åæ ¼ï¼å®æå¤æ£' : 'éè¿ï¼è¿å
¥å¤æ£' : activeRecord?.workflowStatus === 30 ? 'éè¿ï¼æ¾è¡å
¥åº' : '确认ä¸åæ ¼ï¼æ¥åºå¤ç½®' }}</a-radio> |
| | | <a-radio :value="false">{{ reviewStage === 'process' ? '驳åï¼éæ°åæ£' : '驳åï¼éæ°å¤æ£' }}</a-radio> |
| | | <a-radio :value="true">{{ activeRecord?.bizType === 2 ? 'ç¡®è®¤åæ ¼ï¼è¿å
¥å¤æ£' : reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '确认ä¸åæ ¼ï¼å®æå¤æ£' : 'éè¿ï¼è¿å
¥å¤æ£' : activeRecord?.workflowStatus === 30 ? 'éè¿ï¼æ¾è¡å
¥åº' : '确认ä¸åæ ¼ï¼æ¥åºå¤ç½®' }}</a-radio> |
| | | <a-radio :value="false">{{ activeRecord?.bizType === 2 ? '确认ä¸åæ ¼ï¼ä¸äºéè´§å¹¶ä½åº' : reviewStage === 'process' ? '驳åï¼éæ°åæ£' : '驳åï¼ä¸¤æ¡çº¿ä¸èµ·éåå¾
夿£' }}</a-radio> |
| | | </a-radio-group> |
| | | <a-alert v-if="reviewIds.length > 1" type="info" show-icon :message="reviewApproved ? reviewStage === 'company' ? activeRecord?.workflowStatus === 60 ? `确认å ${reviewIds.length} è¢å°æ¥åº` : `确认å ${reviewIds.length} è¢å°æ¾è¡å
¥åº` : `确认å ${reviewIds.length} è¢å°è¿å
¥å¤æ£` : `驳åå ${reviewIds.length} è¢å°è¿åæ£éª`" /> |
| | | <a-alert |
| | | v-if="reviewStage === 'company' && activeRecord?.bizType !== 2" |
| | | type="info" show-icon message="å
¬å¸è´¨æ£å®¡æ¹æè¢å¤ç" |
| | | description="确认å该è¢å å·¥é¨ãéå®é¨ä¸¤æ¡è´¨æ£çº¿åæ¶æ´æ°ï¼ä¸¤æ¡çº¿å¤æ£é½åæ ¼ææ¾è¡å
¥åºï¼ä»»ä¸æ¡ä¸åæ ¼åæ´è¢æ¥åºï¼é©³åæ¶ä¸¤æ¡çº¿ä¸èµ·éåå¾
夿£ã" |
| | | /> |
| | | <a-alert v-else-if="activeRecord?.bizType === 2" type="info" show-icon message="ç»éåéè´§è´¨æ£" |
| | | description="审æ¹éè¿åéè´§åè¿å
¥å¾
æ§è¡ï¼å®¡æ¹ä¸éè¿åéè´§åèªå¨åæ¶å¹¶çåè´§æ£è®°å½ã" /> |
| | | <a-alert v-if="reviewIds.length > 1" type="info" show-icon :message="reviewApproved ? reviewStage === 'company' ? activeRecord?.workflowStatus === 60 ? `确认å ${reviewIds.length} æ¡è®°å½ï¼æè¢åå¹¶ï¼å°æ¥åº` : `确认å ${reviewIds.length} æ¡è®°å½ï¼æè¢åå¹¶ï¼å°æ¾è¡å
¥åº` : `确认å ${reviewIds.length} æ¡è®°å½å°è¿å
¥å¤æ£` : `驳åå ${reviewIds.length} æ¡è®°å½å°è¿åæ£éª`" /> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" :placeholder="reviewApproved ? 'å®¡æ¹æè§ï¼éå¡«ï¼' : '请填å驳åæè§'" /> |
| | | </a-modal> |
| | | <a-modal v-model:open="historyOpen" wrap-class-name="seed-quality-modal seed-quality-history-modal" |
| | |
| | | .filters .ant-select { width: 128px; } |
| | | .filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; } |
| | | .filters :deep(.ant-btn .iconify) { font-size: 14px; } |
| | | /* ===== æçäº§æ¹æ¬¡æå ===== */ |
| | | .batch-group { margin-bottom: 12px; border: 1px solid #e8edf5; border-radius: 10px; overflow: hidden; background: #fff; transition: box-shadow .2s ease; } |
| | | .batch-group.expanded { box-shadow: 0 4px 16px #243b5314; } |
| | | .batch-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; padding: 13px 16px; border-left: 3px solid #1677ff; background: linear-gradient(90deg, #f8fbff 0%, #fff 70%); cursor: pointer; } |
| | | .batch-head:hover { background: #f5f9ff; } |
| | | .batch-chevron { display: inline-flex; color: #1677ff; font-size: 16px; } |
| | | .batch-code { color: #172033; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; } |
| | | .batch-meta { display: inline-flex; flex-wrap: wrap; gap: 4px 12px; color: #718096; font-size: 12px; } |
| | | .batch-counts { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: auto; } |
| | | .count-pill { padding: 1px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 20px; font-variant-numeric: tabular-nums; } |
| | | .count-pill.total { color: #334155; background: #eef2f7; } |
| | | .count-pill.pass { color: #16a34a; background: #ecfdf3; } |
| | | .count-pill.fail { color: #dc2626; background: #fef2f2; } |
| | | .count-pill.pending { color: #d97706; background: #fffbeb; } |
| | | .batch-flags { display: inline-flex; flex-wrap: wrap; gap: 4px; } |
| | | .batch-flags :deep(.ant-tag) { margin: 0; } |
| | | .batch-body { padding: 0 12px 12px; border-top: 1px dashed #eef1f6; background: #fff; } |
| | | .group-pagination { display: flex; justify-content: flex-end; padding: 14px 0 2px; } |
| | | .selection-bar { padding: 12px 0; border-bottom: 1px solid #eef1f6; } |
| | | .selection-summary, .selection-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } |
| | | .selection-summary { margin-bottom: 8px; color: #59677b; } |
| | |
| | | .mobile-batch-copy p { margin: 0; color: #718096; font-size: 13px; line-height: 1.55; } |
| | | .mobile-batch-icon { display: flex; flex: 0 0 38px; width: 38px; height: 38px; align-items: center; justify-content: center; border-radius: 10px; color: #1677ff; background: #eaf3ff; font-size: 20px; } |
| | | .desktop-scope-action { display: none !important; } |
| | | .batch-head { align-items: flex-start; padding: 12px; } |
| | | .batch-counts { width: 100%; margin-left: 0; } |
| | | .batch-body { padding: 0 4px 8px; } |
| | | .group-pagination { justify-content: center; } |
| | | .record-card { padding: 4px 12px 12px; } |
| | | .card-toolbar { align-items: flex-start; flex-direction: column; } |
| | | .filters { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { message } from 'ant-design-vue'; |
| | | import { computed, onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { getSeedIndicatorRanges, saveSeedIndicatorRanges } from '#/api/mes/qc/seedQuality'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | const definitions = [ |
| | | { indicatorCode: 'purity', name: '纯度' }, |
| | | { indicatorCode: 'cleanliness', name: 'å度' }, |
| | | { indicatorCode: 'germination_rate', name: 'åè½ç' }, |
| | | { indicatorCode: 'moisture', name: 'æ°´å' }, |
| | | ]; |
| | | const ranges = ref(definitions.map((item) => ({ ...item, minValue: undefined as number | undefined, maxValue: undefined as number | undefined }))); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | import type { SeedIndicatorRange } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | import { |
| | | copyDefaultSeedIndicatorRanges, |
| | | createSeedIndicatorRange, |
| | | deleteSeedIndicatorRange, |
| | | getSeedIndicatorRanges, |
| | | updateSeedIndicatorRange, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | import { getItemPage } from '#/api/mdm/item'; |
| | | |
| | | interface ProductOption { |
| | | id: number; |
| | | code: string; |
| | | name: string; |
| | | } |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const complete = computed(() => ranges.value.every((item) => |
| | | item.minValue !== undefined && item.maxValue !== undefined && item.minValue <= item.maxValue)); |
| | | const canEdit = computed(() => hasAccessByCodes(['mes:qc-seed-range:update'])); |
| | | |
| | | async function load() { |
| | | const products = ref<ProductOption[]>([]); |
| | | const productLoading = ref(false); |
| | | const selectedItemId = ref<number>(); |
| | | const selectedProduct = computed(() => products.value.find((item) => item.id === selectedItemId.value)); |
| | | |
| | | const items = ref<SeedIndicatorRange[]>([]); |
| | | const loading = ref(false); |
| | | const copying = ref(false); |
| | | const togglingId = ref<number>(); |
| | | |
| | | const enabledCount = computed(() => items.value.filter((item) => item.status === 0).length); |
| | | const disabledCount = computed(() => items.value.length - enabledCount.value); |
| | | |
| | | const columns = [ |
| | | { title: 'è´¨æ£é¡¹', dataIndex: 'indicatorName', key: 'indicatorName', width: 200 }, |
| | | { title: 'ç¼ç ', dataIndex: 'indicatorCode', key: 'indicatorCode', width: 180 }, |
| | | { title: 'åæ ¼èå´', key: 'range', width: 180 }, |
| | | { title: 'æåº', dataIndex: 'sort', key: 'sort', width: 90, align: 'center' as const }, |
| | | { title: 'ç¶æ', key: 'status', width: 120, align: 'center' as const }, |
| | | { title: 'æä½', key: 'action', width: 150, align: 'center' as const, fixed: 'right' as const }, |
| | | ]; |
| | | |
| | | const modalOpen = ref(false); |
| | | const saving = ref(false); |
| | | const editingId = ref<number>(); |
| | | const form = reactive({ |
| | | indicatorCode: '', |
| | | indicatorName: '', |
| | | unit: '%', |
| | | minValue: undefined as number | undefined, |
| | | maxValue: undefined as number | undefined, |
| | | sort: 0, |
| | | status: 0, |
| | | }); |
| | | |
| | | function formatNumber(value?: number | string) { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? String(num) : 'â'; |
| | | } |
| | | |
| | | function rangeText(row: SeedIndicatorRange) { |
| | | return `${formatNumber(row.minValue)} ~ ${formatNumber(row.maxValue)}${row.unit ? ` ${row.unit}` : ''}`; |
| | | } |
| | | |
| | | async function loadProducts(search?: string) { |
| | | productLoading.value = true; |
| | | try { |
| | | const page = await getItemPage({ |
| | | pageNo: 1, |
| | | pageSize: 100, |
| | | status: 0, |
| | | name: search?.trim() || undefined, |
| | | }); |
| | | const list = (page.list || []).map((item) => ({ |
| | | id: Number(item.id), |
| | | code: item.code, |
| | | name: item.name, |
| | | })); |
| | | if (selectedProduct.value && !list.some((item) => item.id === selectedProduct.value?.id)) { |
| | | list.unshift(selectedProduct.value); |
| | | } |
| | | products.value = list; |
| | | } catch (error) { |
| | | message.error((error as Error).message || '读å产åå表失败'); |
| | | } finally { |
| | | productLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadItems() { |
| | | if (!selectedItemId.value) { |
| | | items.value = []; |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const data = await getSeedIndicatorRanges(); |
| | | ranges.value = definitions.map((item) => { |
| | | const saved = data.find((row) => row.indicatorCode === item.indicatorCode); |
| | | return { ...item, minValue: saved ? Number(saved.minValue) : undefined, |
| | | maxValue: saved ? Number(saved.maxValue) : undefined }; |
| | | }); |
| | | items.value = await getSeedIndicatorRanges({ itemId: selectedItemId.value }); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '读ååæ ¼èå´å¤±è´¥'); |
| | | items.value = []; |
| | | message.error((error as Error).message || '读åè´¨æ£é¡¹å¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function save() { |
| | | if (!complete.value || ranges.value.some((item) => item.minValue! < 0 || item.maxValue! > 100)) { |
| | | message.warning('请为åé¡¹ææ é
ç½® 0â100% çææä¸ä¸é'); |
| | | async function onProductChange(value: number) { |
| | | selectedItemId.value = value; |
| | | await loadItems(); |
| | | } |
| | | |
| | | function resetForm() { |
| | | form.indicatorCode = ''; |
| | | form.indicatorName = ''; |
| | | form.unit = '%'; |
| | | form.minValue = undefined; |
| | | form.maxValue = undefined; |
| | | form.sort = 0; |
| | | form.status = 0; |
| | | } |
| | | |
| | | function openCreate() { |
| | | if (!selectedItemId.value) { |
| | | message.warning('请å
éæ©äº§å'); |
| | | return; |
| | | } |
| | | editingId.value = undefined; |
| | | resetForm(); |
| | | form.sort = items.value.length * 10 + 10; |
| | | modalOpen.value = true; |
| | | } |
| | | |
| | | function openEdit(row: SeedIndicatorRange) { |
| | | editingId.value = row.id; |
| | | form.indicatorCode = row.indicatorCode; |
| | | form.indicatorName = row.indicatorName; |
| | | form.unit = row.unit || '%'; |
| | | form.minValue = Number(row.minValue); |
| | | form.maxValue = Number(row.maxValue); |
| | | form.sort = row.sort ?? 0; |
| | | form.status = row.status ?? 0; |
| | | modalOpen.value = true; |
| | | } |
| | | |
| | | function validateForm() { |
| | | if (!form.indicatorName.trim()) { |
| | | message.warning('请填åè´¨æ£é¡¹åç§°'); |
| | | return false; |
| | | } |
| | | if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(form.indicatorCode.trim())) { |
| | | message.warning('ç¼ç å¿
须以忝å¼å¤´ï¼ä»
å¯å
å«åæ¯ãæ°ååä¸å线'); |
| | | return false; |
| | | } |
| | | if (form.minValue === undefined || form.maxValue === undefined) { |
| | | message.warning('请填ååæ ¼ä¸ä¸é'); |
| | | return false; |
| | | } |
| | | if (Number(form.minValue) < 0 || Number(form.maxValue) < 0) { |
| | | message.warning('åæ ¼ä¸ä¸éä¸è½ä¸ºè´æ°'); |
| | | return false; |
| | | } |
| | | if (Number(form.minValue) > Number(form.maxValue)) { |
| | | message.warning('åæ ¼ä¸éä¸è½å¤§äºä¸é'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | async function submitForm() { |
| | | if (!selectedItemId.value || !validateForm()) return; |
| | | saving.value = true; |
| | | const payload = { |
| | | itemId: selectedItemId.value, |
| | | indicatorCode: form.indicatorCode.trim(), |
| | | indicatorName: form.indicatorName.trim(), |
| | | unit: form.unit?.trim() || '%', |
| | | minValue: Number(form.minValue), |
| | | maxValue: Number(form.maxValue), |
| | | sort: form.sort ?? 0, |
| | | status: form.status ?? 0, |
| | | }; |
| | | try { |
| | | await saveSeedIndicatorRanges(ranges.value.map((item) => ({ |
| | | indicatorCode: item.indicatorCode, |
| | | minValue: item.minValue!, maxValue: item.maxValue!, |
| | | }))); |
| | | message.success('é»è®¤åæ ¼èå´å·²ä¿åï¼æ£éªæ¶ä»å¯æå®é
æ
åµè°æ´'); |
| | | await load(); |
| | | if (editingId.value) { |
| | | await updateSeedIndicatorRange(editingId.value, payload); |
| | | message.success('è´¨æ£é¡¹å·²æ´æ°'); |
| | | } else { |
| | | await createSeedIndicatorRange(payload); |
| | | message.success('è´¨æ£é¡¹å·²æ°å¢'); |
| | | } |
| | | modalOpen.value = false; |
| | | await loadItems(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'ä¿ååæ ¼èå´å¤±è´¥'); |
| | | message.error((error as Error).message || 'ä¿åè´¨æ£é¡¹å¤±è´¥'); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(load); |
| | | async function removeItem(row: SeedIndicatorRange) { |
| | | try { |
| | | await deleteSeedIndicatorRange(row.id); |
| | | message.success('è´¨æ£é¡¹å·²å é¤'); |
| | | await loadItems(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'å é¤è´¨æ£é¡¹å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | async function toggleStatus(row: SeedIndicatorRange) { |
| | | if (!canEdit.value || !selectedItemId.value) return; |
| | | const next = row.status === 0 ? 1 : 0; |
| | | togglingId.value = row.id; |
| | | try { |
| | | await updateSeedIndicatorRange(row.id, { |
| | | itemId: selectedItemId.value, |
| | | indicatorCode: row.indicatorCode, |
| | | indicatorName: row.indicatorName, |
| | | unit: row.unit || '%', |
| | | minValue: Number(row.minValue), |
| | | maxValue: Number(row.maxValue), |
| | | sort: row.sort ?? 0, |
| | | status: next, |
| | | }); |
| | | row.status = next; |
| | | message.success(next === 0 ? 'å·²å¯ç¨' : 'å·²åç¨'); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'æ´æ°ç¶æå¤±è´¥'); |
| | | } finally { |
| | | togglingId.value = undefined; |
| | | } |
| | | } |
| | | |
| | | async function copyDefaults() { |
| | | if (!selectedItemId.value) return; |
| | | copying.value = true; |
| | | try { |
| | | const count = await copyDefaultSeedIndicatorRanges(selectedItemId.value); |
| | | message.success(count ? `å·²å¤å¶ ${count} 个éç¨é»è®¤é¡¹` : '没æå¯å¤å¶çéç¨é»è®¤é¡¹'); |
| | | await loadItems(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'å¤å¶é»è®¤é¡¹å¤±è´¥'); |
| | | } finally { |
| | | copying.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadProducts(); |
| | | if (products.value.length) { |
| | | selectedItemId.value = products.value[0]!.id; |
| | | await loadItems(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="range-page"> |
| | | <header class="page-header"> |
| | | <div><div class="eyebrow">MES / è´¨é管ç</div><h1>è´¨æ£é¡¹åæ ¼èå´é
ç½®</h1> |
| | | <p>ä¸ºåæ£å夿£æä¾åé¡¹ææ çé»è®¤èå´ã</p></div> |
| | | </header> |
| | | <a-alert type="info" show-icon message="è¿éé
ç½®çæ¯å¡«åæ¨¡æ¿" description="æ£éªåå¯ä»¥ä¿®æ¹æ¬æ¬¡åæ ¼ä¸ä¸éï¼å®é
æäº¤çä¸ä¸éå宿µå¼ä¸åä¿åï¼åå²è®°å½ä¸å模æ¿ä¿®æ¹å½±åã" /> |
| | | <a-card class="range-card" :loading="loading" :bordered="false"> |
| | | <div class="grid-head"><span>è´¨æ£é¡¹</span><span>åæ ¼ä¸é</span><span>åæ ¼ä¸é</span><span>åä½</span></div> |
| | | <div v-for="item in ranges" :key="item.indicatorCode" class="range-row"> |
| | | <strong>{{ item.name }}</strong> |
| | | <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="ä¸é" /> |
| | | <span>%</span> |
| | | <div class="header-main"> |
| | | <div class="eyebrow">MES / è´¨é管ç</div> |
| | | <h1>è´¨æ£é¡¹åæ ¼èå´é
ç½®</h1> |
| | | <p>æäº§åç»´æ¤è´¨æ£é¡¹ä¸é»è®¤åæ ¼èå´ï¼åæ£ä¸å¤æ£æè¯¥äº§åçè´¨æ£é¡¹å½å
¥ã</p> |
| | | </div> |
| | | <div class="actions"> |
| | | <a-button @click="load">å·æ°</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-range:update'])" type="primary" :loading="saving" :disabled="!complete" @click="save">ä¿ååæ ¼èå´</a-button> |
| | | <div class="header-badge"><IconifyIcon icon="ep:box" /> 产å维度é
ç½®</div> |
| | | </header> |
| | | |
| | | <a-alert |
| | | type="info" |
| | | show-icon |
| | | message="é
ç½®è§å" |
| | | description="æ¯ä¸ªè´¨æ£é¡¹å¿
é¡»ç»å®äº§åãæ£éªåä»å¯ä¿®æ¹æ¬æ¬¡åæ ¼ä¸ä¸éï¼å®é
æäº¤çèå´å宿µå¼ä¼éè®°å½ä¿åï¼åå²è®°å½ä¸å模æ¿ä¿®æ¹å½±åã" |
| | | /> |
| | | |
| | | <a-card class="range-card" :bordered="false"> |
| | | <div class="toolbar"> |
| | | <div class="toolbar-select"> |
| | | <span class="toolbar-label">éæ©äº§å</span> |
| | | <a-select |
| | | :value="selectedItemId" |
| | | class="product-select" |
| | | show-search |
| | | :filter-option="false" |
| | | :loading="productLoading" |
| | | placeholder="æç´¢äº§åç¼ç æåç§°" |
| | | :options="products.map((item) => ({ label: `${item.name}ï¼${item.code}ï¼`, value: item.id }))" |
| | | @change="onProductChange" |
| | | @search="loadProducts" |
| | | /> |
| | | </div> |
| | | <div class="toolbar-actions"> |
| | | <a-button :loading="loading" @click="loadItems">å·æ°</a-button> |
| | | <a-button v-if="canEdit" :loading="copying" :disabled="!selectedItemId" @click="copyDefaults"> |
| | | å¤å¶éç¨é»è®¤é¡¹ |
| | | </a-button> |
| | | <a-button v-if="canEdit" type="primary" :disabled="!selectedItemId" @click="openCreate"> |
| | | <IconifyIcon icon="ep:plus" /> æ°å¢è´¨æ£é¡¹ |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="selectedProduct" class="product-summary"> |
| | | <div class="product-identity"> |
| | | <span class="product-name">{{ selectedProduct.name }}</span> |
| | | <span class="product-code">{{ selectedProduct.code }}</span> |
| | | </div> |
| | | <div class="product-stats"> |
| | | <span class="stat"><b>{{ items.length }}</b> 个质æ£é¡¹</span> |
| | | <span class="stat stat-success"><b>{{ enabledCount }}</b> å¯ç¨</span> |
| | | <span class="stat stat-muted"><b>{{ disabledCount }}</b> åç¨</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="!selectedItemId" class="placeholder-box"> |
| | | <IconifyIcon icon="ep:search" class="placeholder-icon" /> |
| | | <p>è¯·éæ©äº§ååç»´æ¤è´¨æ£é¡¹</p> |
| | | </div> |
| | | |
| | | <a-table |
| | | v-else |
| | | :columns="columns" |
| | | :data-source="items" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | :scroll="{ x: 920 }" |
| | | row-key="id" |
| | | size="middle" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'range'"> |
| | | <span class="range-text">{{ rangeText(record) }}</span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | <a-switch |
| | | v-if="canEdit" |
| | | :checked="record.status === 0" |
| | | :loading="togglingId === record.id" |
| | | size="small" |
| | | @change="toggleStatus(record)" |
| | | /> |
| | | <a-tag v-else :color="record.status === 0 ? 'success' : 'default'"> |
| | | {{ record.status === 0 ? 'å¯ç¨' : 'åç¨' }} |
| | | </a-tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <div v-if="canEdit" class="row-actions"> |
| | | <a-button type="link" size="small" @click="openEdit(record)">ç¼è¾</a-button> |
| | | <a-popconfirm |
| | | title="确认å é¤è¯¥è´¨æ£é¡¹ï¼" |
| | | description="åå²è´¨æ£è®°å½ä¸åå½±åã" |
| | | ok-text="å é¤" |
| | | cancel-text="åæ¶" |
| | | @confirm="removeItem(record)" |
| | | > |
| | | <a-button type="link" size="small" danger>å é¤</a-button> |
| | | </a-popconfirm> |
| | | </div> |
| | | <span v-else class="muted">â</span> |
| | | </template> |
| | | </template> |
| | | </a-table> |
| | | |
| | | <div v-if="selectedItemId && !loading && !items.length" class="empty-state"> |
| | | <IconifyIcon icon="ep:document-add" class="empty-icon" /> |
| | | <p class="empty-title">该产åå°æªé
置质æ£é¡¹</p> |
| | | <p class="empty-desc">æªé
ç½®æ¶è´¨æ£å°ä½¿ç¨éç¨é»è®¤é¡¹ï¼å¯å¤å¶åæéè°æ´ã</p> |
| | | <div v-if="canEdit" class="empty-actions"> |
| | | <a-button type="primary" @click="openCreate">æ°å¢è´¨æ£é¡¹</a-button> |
| | | <a-button :loading="copying" @click="copyDefaults">å¤å¶éç¨é»è®¤é¡¹</a-button> |
| | | </div> |
| | | </div> |
| | | </a-card> |
| | | |
| | | <a-modal |
| | | v-model:open="modalOpen" |
| | | :title="editingId ? 'ç¼è¾è´¨æ£é¡¹' : 'æ°å¢è´¨æ£é¡¹'" |
| | | :confirm-loading="saving" |
| | | ok-text="ä¿å" |
| | | cancel-text="åæ¶" |
| | | width="560px" |
| | | @ok="submitForm" |
| | | > |
| | | <div class="modal-product"> |
| | | <span class="modal-product-label">ç»å®äº§å</span> |
| | | <span class="modal-product-value">{{ selectedProduct ? `${selectedProduct.name}ï¼${selectedProduct.code}ï¼` : 'â' }}</span> |
| | | </div> |
| | | |
| | | <div class="form-section"> |
| | | <div class="form-section-title">åºæ¬ä¿¡æ¯</div> |
| | | <div class="form-grid"> |
| | | <a-form-item label="è´¨æ£é¡¹åç§°" required> |
| | | <a-input v-model:value="form.indicatorName" :maxlength="64" placeholder="å¦ï¼çº¯åº¦" /> |
| | | </a-form-item> |
| | | <a-form-item label="è´¨æ£é¡¹ç¼ç " required> |
| | | <a-input |
| | | v-model:value="form.indicatorCode" |
| | | :disabled="!!editingId" |
| | | :maxlength="64" |
| | | placeholder="å¦ï¼purity" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item label="åä½"> |
| | | <a-input v-model:value="form.unit" :maxlength="16" placeholder="å¦ï¼%ãç²/å
¬æ¤" /> |
| | | </a-form-item> |
| | | <a-form-item label="ç¶æ"> |
| | | <a-select v-model:value="form.status"> |
| | | <a-select-option :value="0">å¯ç¨</a-select-option> |
| | | <a-select-option :value="1">åç¨</a-select-option> |
| | | </a-select> |
| | | </a-form-item> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="form-section"> |
| | | <div class="form-section-title">åæ ¼èå´</div> |
| | | <div class="range-row"> |
| | | <a-form-item label="åæ ¼ä¸é" required> |
| | | <a-input-number v-model:value="form.minValue" class="full-width" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | </a-form-item> |
| | | <span class="range-separator">~</span> |
| | | <a-form-item label="åæ ¼ä¸é" required> |
| | | <a-input-number v-model:value="form.maxValue" class="full-width" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | </a-form-item> |
| | | <a-form-item label="æåº"> |
| | | <a-input-number v-model:value="form.sort" class="full-width" :min="0" :precision="0" /> |
| | | </a-form-item> |
| | | </div> |
| | | </div> |
| | | </a-modal> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .range-page { padding: 24px; min-height: 100%; background: #f4f7fb; } |
| | | .page-header { margin-bottom: 20px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; } |
| | | h1 { margin: 6px 0; color: #172033; font-size: 28px; }.page-header p { margin: 0; color: #718096; } |
| | | .range-card { margin-top: 16px; border-radius: 10px; box-shadow: 0 3px 12px #243b5310; } |
| | | .grid-head, .range-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 1fr) 70px; align-items: center; gap: 16px; padding: 12px 16px; } |
| | | .grid-head { color: #718096; background: #f7f9fc; font-weight: 600; }.range-row { border-bottom: 1px solid #e8edf5; }.range-row :deep(.ant-input-number) { width: 100%; } |
| | | .actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } |
| | | @media (max-width: 640px) { .range-page { padding: 14px; }.grid-head { display: none; }.range-row { grid-template-columns: 1fr 1fr; }.range-row strong { grid-column: 1 / -1; } } |
| | | .page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 20px; } |
| | | .header-main { min-width: 0; } |
| | | .eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; } |
| | | h1 { margin: 6px 0; color: #172033; font-size: 28px; } |
| | | .page-header p { margin: 0; color: #718096; } |
| | | .header-badge { display: inline-flex; flex: none; align-items: center; gap: 8px; padding: 9px 13px; border: 1px solid #cfe0fb; border-radius: 7px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; } |
| | | .range-card { margin-top: 16px; border-radius: 12px; box-shadow: 0 3px 12px #243b5310; } |
| | | .toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding-bottom: 14px; } |
| | | .toolbar-select { display: flex; align-items: center; gap: 10px; } |
| | | .toolbar-label { color: #59677b; font-size: 13px; font-weight: 600; white-space: nowrap; } |
| | | .product-select { width: min(360px, 100%); } |
| | | .toolbar-actions { display: flex; align-items: center; gap: 10px; } |
| | | .product-summary { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; padding: 12px 16px; border: 1px solid #d6e8ff; border-radius: 10px; background: linear-gradient(90deg, #f7fbff, #fbfdff); } |
| | | .product-identity { display: flex; align-items: baseline; gap: 10px; } |
| | | .product-name { font-size: 16px; font-weight: 700; color: #172033; } |
| | | .product-code { color: #718096; font-size: 13px; } |
| | | .product-stats { display: flex; align-items: center; gap: 16px; } |
| | | .stat { color: #59677b; font-size: 13px; } |
| | | .stat b { color: #172033; font-size: 15px; } |
| | | .stat-success b { color: #1a9c5b; } |
| | | .stat-muted b { color: #98a2b3; } |
| | | .range-text { font-variant-numeric: tabular-nums; color: #2b3a55; } |
| | | .row-actions { display: flex; align-items: center; justify-content: center; gap: 4px; } |
| | | .muted { color: #98a2b3; } |
| | | .placeholder-box { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 48px 0; color: #98a2b3; } |
| | | .placeholder-icon { font-size: 30px; } |
| | | .empty-state { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 44px 0 36px; text-align: center; } |
| | | .empty-icon { font-size: 40px; color: #c2cddb; } |
| | | .empty-title { margin: 4px 0 0; color: #3b4a60; font-size: 15px; font-weight: 600; } |
| | | .empty-desc { margin: 0; color: #98a2b3; font-size: 13px; } |
| | | .empty-actions { display: flex; gap: 10px; margin-top: 12px; } |
| | | .modal-product { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; padding: 10px 14px; border-radius: 8px; background: #f5f8fd; } |
| | | .modal-product-label { color: #718096; font-size: 13px; } |
| | | .modal-product-value { color: #172033; font-size: 14px; font-weight: 600; } |
| | | .form-section + .form-section { margin-top: 6px; } |
| | | .form-section-title { margin-bottom: 10px; padding-left: 8px; border-left: 3px solid #1677ff; color: #2b3a55; font-size: 14px; font-weight: 600; } |
| | | .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; } |
| | | .range-row { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(0, 1fr); align-items: start; gap: 0 10px; } |
| | | .range-separator { padding-top: 32px; color: #98a2b3; } |
| | | .full-width { width: 100%; } |
| | | @media (max-width: 720px) { |
| | | .range-page { padding: 14px; } |
| | | .page-header { display: block; } |
| | | .header-badge { margin-top: 14px; } |
| | | .toolbar, .product-summary { align-items: flex-start; } |
| | | .form-grid, .range-row { grid-template-columns: 1fr; } |
| | | .range-separator { display: none; } |
| | | } |
| | | </style> |
| | |
| | | <h2 class="m-title">ä»ä¸æ¬¡æ«ç å¼å§</h2> |
| | | <RouterLink v-if="items.some(f=>f.key==='inbound')" class="m-primary" to="/mobile/inbound"><Icon name="scan"/>æ«ç å
¥åº</RouterLink> |
| | | <RouterLink v-else :to="'/mobile/'+items[0]!.key" class="m-primary">{{items[0]!.title}}</RouterLink> |
| | | <p class="m-muted" style="margin:12px 0 0">å
¥åºåæ ¸å¯¹å¯¹è±¡ãæ°éååºä½ï¼æäº¤åæ¥çå¤çç»æã</p> |
| | | <p class="m-muted" style="margin:12px 0 0">å
¥åºåæ ¸å¯¹å¯¹è±¡åæ°éï¼æäº¤åæ¥çå¤çç»æã</p> |
| | | </div> |
| | | </template> |
| | | <h2 class="m-section-title">{{route.path.endsWith('/work')?'å
¨é¨ä½ä¸':'常ç¨åè½'}}</h2> |
| | |
| | | import { onBeforeRouteLeave } from 'vue-router'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseLocationSimpleList, type MesWmWarehouseLocationApi } from '#/api/mes/wm/warehouse/location'; |
| | | import { getWarehouseAreaSimpleList, type MesWmWarehouseAreaApi } from '#/api/mes/wm/warehouse/area'; |
| | | import { inboundScanEvent, type MesProScanEventApi } from '#/api/mes/pro/scanEvent'; |
| | | import { lookup, myEvent, errorText, writeBlockReason, type TraceResult } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | import Picker from './picker.vue'; |
| | | import { speakDuplicateScan } from '#/mobile/scanVoice'; |
| | | const user=useUserStore(),storageKey='mom-mobile-inbound-'+user.userInfo?.id; |
| | | const positionKey='mom-mobile-position-'+user.userInfo?.id; |
| | | const locations=ref<MesWmWarehouseLocationApi.WarehouseLocation[]>([]),areas=ref<MesWmWarehouseAreaApi.WarehouseArea[]>([]); |
| | | const locationId=ref<number>(),areaId=ref<number>(),busy=ref(false),error=ref(''),trace=ref<TraceResult>(); |
| | | const targetName=ref(''); |
| | | const busy=ref(false),error=ref(''),trace=ref<TraceResult>(); |
| | | const quantity=ref(1),result=ref<MesProScanEventApi.ScanEvent>(),pending=ref<MesProScanEventApi.InboundParams>(),unknown=ref(false),ready=ref(false); |
| | | const remembered=ref(''); |
| | | const areaLoading=ref(false),positionLoading=ref(false); |
| | | let areaRequest=0; |
| | | const place=computed(()=>[locations.value.find(l=>l.id===locationId.value)?.name,areas.value.find(a=>a.id===areaId.value)?.name].filter(Boolean).join(' / ')||remembered.value); |
| | | async function loadAreas(){ |
| | | const ticket=++areaRequest, id=locationId.value; |
| | | areas.value=[];areaId.value=undefined;areaLoading.value=!!id; |
| | | try{if(id){const list=await getWarehouseAreaSimpleList(id);if(ticket===areaRequest)areas.value=list.filter(a=>!a.frozen&&(a.status==null||a.status===0));}} |
| | | finally{if(ticket===areaRequest)areaLoading.value=false;} |
| | | } |
| | | async function changeLocation(){error.value='';trace.value=undefined;try{await loadAreas();}catch(e){error.value=errorText(e);}} |
| | | async function loadPositions(){ |
| | | const positionLoading=ref(false); |
| | | const place=computed(()=>targetName.value||'ç产æåä»'); |
| | | async function loadTarget(){ |
| | | if(positionLoading.value)return;positionLoading.value=true; |
| | | error.value=''; |
| | | try{ |
| | | const warehouses=await getWarehouseSimpleList(); |
| | | const wip=warehouses.find(w=>w.code==='WIP_VIRTUAL_WAREHOUSE'); |
| | | if(!wip?.id)throw new Error('æªé
ç½®ç产æåä»ï¼è¯·è系管çå'); |
| | | locations.value=(await getWarehouseLocationSimpleList(wip.id)).filter(l=>!l.frozen); |
| | | const saved=JSON.parse(sessionStorage.getItem(positionKey)||'null'); |
| | | if(saved&&locations.value.some(l=>l.id===saved.locationId)){locationId.value=saved.locationId;await loadAreas();if(areas.value.some(a=>a.id===saved.areaId))areaId.value=saved.areaId;} |
| | | targetName.value=wip.name||'ç产æåä»'; |
| | | ready.value=true; |
| | | }catch(e){error.value=errorText(e);}finally{positionLoading.value=false;} |
| | | } |
| | | onMounted(async()=>{ |
| | | try { |
| | | const savedPosition=JSON.parse(sessionStorage.getItem(positionKey)||'null'); |
| | | remembered.value=savedPosition?.name||''; |
| | | const savedPending=JSON.parse(sessionStorage.getItem(storageKey)||'null'); |
| | | if(savedPending){pending.value=savedPending;unknown.value=true;return;} |
| | | } catch { error.value='䏿¬¡æä½ä¿¡æ¯æ æ³è¯»åï¼è¯·éè¿ä½ä¸è®°å½æ ¸å®ååå
¥åºã'; return; } |
| | | await loadPositions(); |
| | | await loadTarget(); |
| | | }); |
| | | function eventId(){ |
| | | const bytes=crypto.getRandomValues(new Uint8Array(16)); |
| | |
| | | try{ |
| | | const found=await lookup(code),reason=writeBlockReason(found); |
| | | if(reason)throw new Error(reason); |
| | | if (found.type === 'bag' && found.data.inboundTime || found.type !== 'bag' && found.data.pendingBagQuantity === 0) throw new Error('该对象没æå¾
å
¥åºè¢ç ï¼è¯·å¿éå¤å
¥åº'); |
| | | if (found.type === 'bag' && found.data.inboundTime || found.type !== 'bag' && found.data.pendingBagQuantity === 0) { speakDuplicateScan(); throw new Error('该对象没æå¾
å
¥åºè¢ç ï¼è¯·å¿éå¤å
¥åº'); } |
| | | trace.value=found;quantity.value=1; |
| | | }catch(e){error.value=errorText(e);}finally{busy.value=false;} |
| | | } |
| | | function complete(r:MesProScanEventApi.ScanEvent){ |
| | | result.value=r;unknown.value=false;pending.value=undefined;sessionStorage.removeItem(storageKey); |
| | | error.value=r.status==='SUCCESS'?'':(r.errorMessage||'å
¥åºå¤±è´¥'); |
| | | if(r.status!=='SUCCESS' && /å·²å
¥åº|éå¤å
¥åº/.test(r.errorMessage||'')) speakDuplicateScan(); |
| | | } |
| | | async function submit(){ |
| | | if(busy.value)return; |
| | | if(!pending.value){ |
| | | if(!trace.value||!locationId.value||!areaId.value){error.value='请å
éæ©åºåºãåºä½å¹¶è¯å«ç å¼';return;} |
| | | if(!trace.value){error.value='请å
è¯å«ç å¼';return;} |
| | | if(!Number.isFinite(quantity.value)||quantity.value<=0){error.value='å
¥åºæ°éå¿
须大äºé¶';return;} |
| | | pending.value={eventId:eventId(),bagCode:trace.value.code,scanType:trace.value.type.toUpperCase() as MesProScanEventApi.ScanType,quantity:quantity.value,locationId:locationId.value,areaId:areaId.value}; |
| | | sessionStorage.setItem(positionKey,JSON.stringify({locationId:locationId.value,areaId:areaId.value,name:place.value})); |
| | | pending.value={eventId:eventId(),bagCode:trace.value.code,scanType:trace.value.type.toUpperCase() as MesProScanEventApi.ScanType,quantity:quantity.value}; |
| | | } |
| | | sessionStorage.setItem(storageKey,JSON.stringify(pending.value)); |
| | | busy.value=true;error.value=''; |
| | | try{complete(await inboundScanEvent(pending.value));} |
| | | try{complete(await inboundScanEvent(pending.value!));} |
| | | catch(e){unknown.value=true;error.value='ç»æå¾
确认ï¼'+errorText(e)+'ã请å
æ ¸å®åæä½ï¼å¿éæ°æ«ç å
¥åºã';} |
| | | finally{busy.value=false;} |
| | | } |
| | |
| | | try{const r=await myEvent(pending.value.eventId);if(r)complete(r);else error.value='å°æªæ¥å°è®°å½ã请ç¨åæ ¸å®ï¼æä½¿ç¨åæä½å·éè¯ã';} |
| | | catch(e){error.value=errorText(e);}finally{busy.value=false;} |
| | | } |
| | | function next(){result.value=undefined;trace.value=undefined;error.value='';if(!ready.value)void loadPositions();} |
| | | function next(){result.value=undefined;trace.value=undefined;error.value='';if(!ready.value)void loadTarget();} |
| | | onBeforeRouteLeave(()=>!busy.value&&(!trace.value||!!result.value||unknown.value||window.confirm('离å¼å°æ¾å¼å°æªæäº¤çå¯¹è±¡ï¼æ¯å¦ç»§ç»ï¼'))); |
| | | </script> |
| | | <template> |
| | | <p class="m-muted">ç产æåå
¥åº · è´¨æ£åæå</p> |
| | | <div v-if="pending && unknown" class="m-panel"> |
| | | <h2 class="m-title">æ ¸å®ä¸æ¬¡å
¥åºç»æ</h2><p>{{pending.bagCode}}</p><p>ç®æ ä½ç½®ï¼{{place||('åºåº '+pending.locationId+' / åºä½ '+pending.areaId)}}</p><p class="m-muted">æä½å·ï¼{{pending.eventId}}</p> |
| | | <h2 class="m-title">æ ¸å®ä¸æ¬¡å
¥åºç»æ</h2><p>{{pending.bagCode}}</p><p>ç®æ ä»åºï¼{{place}}</p><p class="m-muted">æä½å·ï¼{{pending.eventId}}</p> |
| | | <p class="m-warning">å䏿ä½éè¯å°æ²¿ç¨åæä½å·ï¼ä¸å建æ°çå
¥åºè¯·æ±ã</p> |
| | | <div class="m-stack"><button class="m-primary" :disabled="busy" @click="verify">æ ¸å®ç»æ</button><button class="m-secondary" :disabled="busy" @click="submit">使ç¨åæä½å·éè¯</button></div> |
| | | </div> |
| | |
| | | </template> |
| | | <template v-else> |
| | | <div v-if="!trace" class="m-panel"> |
| | | <h2 class="m-title">ç®æ ä½ç½®</h2> |
| | | <Picker v-model="locationId" label="åºåº" :options="locations" :disabled="busy||!!trace" :loading="positionLoading" placeholder="è¯·éæ©åºåº" @update:model-value="changeLocation"/> |
| | | <Picker v-model="areaId" label="åºä½" :options="areas" :disabled="busy||!!trace||!locationId" :loading="areaLoading" placeholder="è¯·éæ©åºä½"/> |
| | | <h2 class="m-title">ç®æ ä»åº</h2> |
| | | <p>{{place}}</p> |
| | | <p v-if="!ready" class="m-muted">æ£å¨è·åä½ä¸ä½ç½®</p> |
| | | <button v-if="!ready&&error" class="m-secondary" @click="loadPositions">éæ°è·åä½ç½®</button> |
| | | <button v-if="!ready&&error" class="m-secondary" @click="loadTarget">éæ°è·åä½ç½®</button> |
| | | </div> |
| | | <Scanner v-if="!trace" :disabled="busy||!areaId||!ready" @code="identify"/> |
| | | <Scanner v-if="!trace" duplicate-voice :disabled="busy||!ready" @code="identify"/> |
| | | <div v-else class="m-panel"> |
| | | <h2 class="m-title">æ ¸å¯¹å
¥åºå¯¹è±¡</h2> |
| | | <dl><div class="m-row"><dt>åç§</dt><dd>{{trace.data.itemName||'æªå½å
¥'}}</dd></div><div class="m-row"><dt>ç å¼</dt><dd>{{trace.code}}</dd></div><div class="m-row"><dt>ç±»å</dt><dd>{{{bag:'è¢ç ',pallet:'æçç ',batch:'æ¹æ¬¡ç '}[trace.type]}}</dd></div><div class="m-row"><dt>ç®æ ä½ç½®</dt><dd>{{place}}</dd></div><div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{trace.data.batchCode||'æªç»å®'}}</dd></div></dl> |
| | | <dl><div class="m-row"><dt>åç§</dt><dd>{{trace.data.itemName||'æªå½å
¥'}}</dd></div><div class="m-row"><dt>ç å¼</dt><dd>{{trace.code}}</dd></div><div class="m-row"><dt>ç±»å</dt><dd>{{{bag:'è¢ç ',pallet:'æçç ',batch:'æ¹æ¬¡ç '}[trace.type]}}</dd></div><div class="m-row"><dt>ç®æ ä»åº</dt><dd>{{place}}</dd></div><div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{trace.data.batchCode||'æªç»å®'}}</dd></div></dl> |
| | | <label v-if="trace.type==='bag'" class="m-field"><span>å
¥åºæ°éï¼æåºå计éåä½ï¼</span><input v-model.number="quantity" type="number" inputmode="decimal" min="0.000001" step="any" :disabled="busy"/></label> |
| | | <p v-else class="m-warning">æ´æ / æ´æ¹å
¥åºï¼æ¬æ¬¡å¾
å
¥åº {{trace.data.pendingBagQuantity ?? 'â'}} è¢ï¼æç»ç±æå¡ç«¯ææªå
¥åºææè¢æ°è®¡ç®ã</p> |
| | | <div class="m-action-bar"><button class="m-primary" :disabled="busy" @click="submit">{{busy?'æ£å¨å
¥åºâ¦':'确认å
¥åº'}}</button><button class="m-secondary" :disabled="busy" @click="next">è¿åéæ°æ«æ</button></div> |
| | |
| | | batchCode: matched.batchCode, |
| | | batchId: matched.batchId, |
| | | warehouseId: matched.warehouseId, |
| | | locationId: matched.locationId, |
| | | areaId: matched.areaId, |
| | | remark: `æ«ç æ¥æº ${trace.value?.code ?? ''}`, |
| | | }); |
| | | notice.value = '已添å åºåºæç»ã'; |
| | |
| | | |
| | | import { |
| | | approveSeedQualityBatch, |
| | | getSeedIndicatorRanges, |
| | | getSeedQualityPage, |
| | | resolveSeedQuality, |
| | | resolveSeedRetestScope, |
| | |
| | | |
| | | const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref(''), notice = ref(''); |
| | | const scanType = ref<'BATCH' | 'PALLET'>('PALLET'), scannedCode = ref(''); |
| | | const qcLine = ref<1 | 2>(1); |
| | | const scope = ref<SeedRetestScope>(), initialScope = ref<SeedQualityScan>(); |
| | | const workflowStatus = ref<10 | 50>(10), reason = ref(''); |
| | | const warehouseId = ref<number>(); |
| | | const warehouseOptions = ref<Array<{ label: string; value: number }>>([]); |
| | | const reviewGroup = ref<ReviewGroup>(), reviewApproved = ref(true), reviewOpinion = ref(''); |
| | | const indicators = ref([ |
| | | { code: 'purity', name: '纯度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'cleanliness', name: 'å度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'germination_rate', name: 'åè½ç', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'moisture', name: 'æ°´å', value: '', minValue: '', maxValue: '' }, |
| | | ]); |
| | | const indicators = ref<Array<{ code: string; name: string; unit: string; value: string; minValue: string; maxValue: string }>>([]); |
| | | |
| | | const initialIds = computed(() => initialScope.value?.pendingBagIds || []); |
| | | const targetIds = computed(() => workflowStatus.value === 10 ? scope.value?.retestIds || [] : scope.value?.initialRecheckIds || []); |
| | | const complete = computed(() => indicators.value.every((item) => |
| | | const complete = computed(() => indicators.value.length > 0 && indicators.value.every((item) => |
| | | [item.value, item.minValue, item.maxValue].every((value) => String(value).trim() !== '' && Number.isFinite(Number(value))) |
| | | && Number(item.value) >= 0 && Number(item.value) <= 100 && Number(item.minValue) >= 0 |
| | | && Number(item.maxValue) <= 100 && Number(item.minValue) <= Number(item.maxValue))); |
| | | && Number(item.value) >= 0 && Number(item.minValue) >= 0 |
| | | && Number(item.maxValue) >= 0 && Number(item.minValue) <= Number(item.maxValue))); |
| | | const passed = computed(() => complete.value && indicators.value.every((item) => |
| | | Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue))); |
| | | const reviewGroups = computed<ReviewGroup[]>(() => { |
| | | if (!scope.value) return []; |
| | | return [ |
| | | { status: 20, stage: 'process' as const, ids: scope.value.pendingProcessIds || [] }, |
| | | { status: 25, stage: 'process' as const, ids: scope.value.pendingInitialFailIds || [] }, |
| | | { status: 30, stage: 'company' as const, ids: scope.value.pendingCompanyIds || [] }, |
| | | { status: 60, stage: 'company' as const, ids: scope.value.pendingCompanyFailIds || [] }, |
| | | ].filter((group) => group.ids.length && (group.stage === 'process' ? canProcessReview.value : canCompanyReview.value)); |
| | | }); |
| | | const reviewGroups = computed<ReviewGroup[]>(() => []); |
| | | const statusNames: Record<number, string> = { |
| | | 10: 'å¾
夿£', 20: 'å¾
åæ£å®¡æ ¸', 25: 'å¾
忣ä¸åæ ¼å¤ç', 30: 'å¾
夿£å®¡æ ¸', |
| | | 40: 'å·²æ¾è¡', 50: 'å¾
鿰忣', 60: 'å¾
夿£ä¸åæ ¼å¤ç', 70: 'å·²æ¥åº', |
| | | 10: 'å¾
夿£', 20: 'å¾
é¨é¨å®¡æ¹', 25: 'å¾
é¨é¨ä¸åæ ¼å®¡æ¹', 30: 'å¾
å
¬å¸è´¨æ£å®¡æ¹', |
| | | 40: 'å·²æ¾è¡', 50: 'å¾
鿰忣', 60: 'å¾
å
¬å¸è´¨æ£ä¸åæ ¼å®¡æ¹', 70: 'å·²æ¥åº', |
| | | }; |
| | | |
| | | function resetIndicators() { |
| | | indicators.value.forEach((item) => { item.value = ''; }); |
| | | reason.value = ''; |
| | | } |
| | | |
| | | async function hydrateReviewIds(current: SeedRetestScope, rawCode: string) { |
| | | const loaded = (current.pendingProcessIds?.length || 0) + (current.pendingInitialFailIds?.length || 0) |
| | |
| | | try { |
| | | const codeValue = extractCode(raw); |
| | | scannedCode.value = codeValue; |
| | | const current = await resolveSeedRetestScope(codeValue, scanType.value); |
| | | const current = await resolveSeedRetestScope(codeValue, scanType.value, qcLine.value); |
| | | await hydrateReviewIds(current, codeValue); |
| | | scope.value = current; |
| | | scanType.value = current.scanType; |
| | | workflowStatus.value = current.retestIds.length ? 10 : 50; |
| | | initialScope.value = await resolveSeedQuality(codeValue, current.scanType); |
| | | resetIndicators(); |
| | | initialScope.value = await resolveSeedQuality(codeValue, current.scanType, qcLine.value); |
| | | const source = current.indicators?.length ? current.indicators : initialScope.value?.indicators; |
| | | indicators.value = (source || []).map((item) => ({ |
| | | code: item.code, name: item.name || item.code, unit: item.unit || '%', |
| | | value: '', minValue: String(item.minValue), maxValue: String(item.maxValue), |
| | | })); |
| | | reason.value = ''; |
| | | if (!initialIds.value.length && !targetIds.value.length && !reviewGroups.value.length) { |
| | | notice.value = 'å½åæçæçäº§æ¹æ¬¡æ²¡æå¯å¤ççè´¨æ£è®°å½ã'; |
| | | } |
| | |
| | | if (!window.confirm(`确认æ¹éæäº¤ ${ids.length} è¢åæ£ï¼`)) return; |
| | | busy.value = true; error.value = ''; |
| | | try { |
| | | const result = await submitSeedQualityGroup({ code: initialScope.value.code, batchCode: initialScope.value.batchCode, |
| | | const result = await submitSeedQualityGroup({ code: initialScope.value.code, |
| | | batchCode: initialScope.value.batchCode, |
| | | scanType: initialScope.value.scanType || scanType.value, bagIds: ids, qualifiedWarehouseId: warehouseId.value, |
| | | reason: reason.value.trim() || undefined, indicators: indicatorPayload() }); |
| | | busy.value = false; |
| | | await refreshScopeAndList(`已宿 ${result.processedCount} è¢åæ£ã`); |
| | | await refreshScopeAndList(`已宿 ${result.processedCount} è¢åæ£ï¼å·²åæ¥çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½ã`); |
| | | } catch (exception) { error.value = errorText(exception) + 'ãè¯·éæ°è¯å«åæ ¸å¯¹å¾
æ£æ¸
åã'; } |
| | | finally { busy.value = false; } |
| | | } |
| | |
| | | if (!window.confirm(`ç¡®è®¤å°æ¬æ¬¡ææ ç¨äº ${ids.length} è¢${workflowStatus.value === 10 ? '夿£' : '鿰忣'}ï¼`)) return; |
| | | busy.value = true; error.value = ''; |
| | | try { |
| | | const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, |
| | | const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, qcLine: qcLine.value, |
| | | workflowStatus: workflowStatus.value, qualityIds: ids, reason: reason.value.trim() || undefined, indicators: indicatorPayload() }); |
| | | busy.value = false; |
| | | await refreshScopeAndList(`已宿 ${result.processedCount} è¢æ£éªã`); |
| | |
| | | if (warehouseOptions.value.length === 1) warehouseId.value = warehouseOptions.value[0]?.value; |
| | | } catch { warehouseOptions.value = []; } |
| | | } |
| | | if (canInitial.value || canRetest.value) { |
| | | try { |
| | | const ranges = await getSeedIndicatorRanges(); |
| | | indicators.value.forEach((item) => { const range = ranges.find((row) => row.indicatorCode === item.code); |
| | | if (range) { item.minValue = String(range.minValue); item.maxValue = String(range.maxValue); } }); |
| | | } catch { /* åæ ¼èå´ä»å¯æå·¥å¡«å */ } |
| | | } |
| | | await load(); |
| | | }); |
| | | </script> |
| | |
| | | <h2 class="m-title">æ¹éæ£éªä¸å®¡æ ¸</h2> |
| | | <p class="m-muted">æ«ææçç æçäº§æ¹æ¬¡ç ï¼ç³»ç»ä¼æå½åè´¦å·æéååºæ¹é忣ãå®¡æ ¸ã夿£æä½ã</p> |
| | | <label class="m-field"><span>æ«ç ç±»å</span><select v-model="scanType" :disabled="busy" @change="scope = undefined; initialScope = undefined"><option value="PALLET">æçç </option><option value="BATCH">çäº§æ¹æ¬¡ç </option></select></label> |
| | | <label class="m-field"><span>è´¨æ£çº¿ï¼å¤æ£/å®¡æ ¸ï¼</span><select v-model.number="qcLine" :disabled="busy" @change="scope = undefined; initialScope = undefined"><option :value="1">å å·¥é¨</option><option :value="2">éå®é¨</option></select></label> |
| | | <p class="m-muted">忣æäº¤ä¼åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½ï¼è´¨æ£çº¿ä»
ç¨äºçé夿£åå®¡æ ¸èå´ã</p> |
| | | <Scanner :disabled="busy" @code="identifyScope" /> |
| | | |
| | | <template v-if="scope"> |
| | | <div class="m-panel"><h3 class="m-title">å·²è¯å«èå´</h3><dl><div class="m-row"><dt>ç å¼</dt><dd>{{ scope.code }}</dd></div><div class="m-row"><dt>çäº§æ¹æ¬¡</dt><dd>{{ scope.batchCode || 'â' }}</dd></div><div class="m-row"><dt>ææè¢æ°</dt><dd>{{ scope.totalCount }}</dd></div></dl></div> |
| | | |
| | | <form v-if="canInitial && initialIds.length" class="m-panel" @submit.prevent="submitInitial"> |
| | | <h3 class="m-title">æ¹é忣ï¼{{ Math.min(initialIds.length, 500) }} è¢ï¼</h3> |
| | | <h3 class="m-title">æ¹é忣ï¼{{ Math.min(initialIds.length, 500) }} è¢ï¼åæ¶çæå线记å½ï¼</h3> |
| | | <p v-if="!initialScope?.inbound" class="m-warning">该èå´çææè¢ç å°æªå
¨é¨å
¥åºï¼æä¸è½æäº¤åæ£ã</p> |
| | | <template v-else> |
| | | <label class="m-field"><span>ç®æ åæ ¼åº</span><select v-model="warehouseId" required :disabled="busy"><option :value="undefined" disabled>è¯·éæ©åæ ¼åº</option><option v-for="item in warehouseOptions" :key="item.value" :value="item.value">{{ item.label }}</option></select></label> |
| | | <fieldset v-for="item in indicators" :key="`initial-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}ï¼%ï¼</legend><label class="m-field"><span>宿µå¼</span><input v-model="item.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset> |
| | | <fieldset v-for="item in indicators" :key="`initial-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}ï¼{{ item.unit || '%' }}ï¼</legend><label class="m-field"><span>宿µå¼</span><input v-model="item.value" type="number" inputmode="decimal" min="0" step="any" required /></label><div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" step="any" required /></label></div></fieldset> |
| | | <label class="m-field"><span>{{ complete && !passed ? 'ä¸åæ ¼åå ï¼å¿
å¡«ï¼' : 'æ£éªå¤æ³¨' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label> |
| | | <button class="m-primary" :disabled="busy || !complete || !warehouseId">{{ busy ? 'æ£å¨æäº¤â¦' : `æäº¤ ${Math.min(initialIds.length, 500)} è¢åæ£` }}</button> |
| | | </template> |
| | |
| | | <h3 class="m-title">æ¹é夿£ / 鿰忣</h3> |
| | | <label class="m-field"><span>è´¨æ£é¶æ®µ</span><select v-model="workflowStatus" :disabled="busy"><option v-if="scope.retestIds.length" :value="10">夿£ï¼{{ scope.retestIds.length }} è¢ï¼</option><option v-if="scope.initialRecheckIds.length" :value="50">鿰忣ï¼{{ scope.initialRecheckIds.length }} è¢ï¼</option></select></label> |
| | | <p class="m-warning">åä¸ç»ææ å°ç¨äºæ¬æ¬¡ {{ Math.min(targetIds.length, 500) }} è¢ï¼è¯·æ ¸å¯¹æ½æ ·ç»æã</p> |
| | | <fieldset v-for="item in indicators" :key="`retest-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}ï¼%ï¼</legend><label class="m-field"><span>宿µå¼</span><input v-model="item.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div></fieldset> |
| | | <fieldset v-for="item in indicators" :key="`retest-${item.code}`" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{ item.name }}ï¼{{ item.unit || '%' }}ï¼</legend><label class="m-field"><span>宿µå¼</span><input v-model="item.value" type="number" inputmode="decimal" min="0" step="any" required /></label><div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.minValue" type="number" inputmode="decimal" min="0" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="item.maxValue" type="number" inputmode="decimal" min="0" step="any" required /></label></div></fieldset> |
| | | <label class="m-field"><span>{{ complete && !passed ? 'ä¸åæ ¼åå ï¼å¿
å¡«ï¼' : 'æ£éªå¤æ³¨' }}</span><textarea v-model="reason" rows="3" maxlength="500" /></label> |
| | | <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{ busy ? 'æ£å¨æäº¤â¦' : `æäº¤ ${Math.min(targetIds.length, 500)} è¢æ£éª` }}</button> |
| | | </form> |
| | |
| | | <p class="m-muted">æ¥çè´¨æ£ç»æãæ¹æ¬¡ä¸ä¸åæ ¼åå ã</p> |
| | | <Scanner :disabled="busy" @code="value => { code = value; load(); }" /> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>ä¸å¡ç / 追溯ç </span><input v-model="code" :disabled="busy" placeholder="è¾å
¥è¢ç " /></label> |
| | | <label class="m-field"><span>追溯ç </span><input v-model="code" :disabled="busy" placeholder="è¾å
¥è¢ç " /></label> |
| | | <label class="m-field"><span>çäº§æ¹æ¬¡</span><input v-model="batchCode" :disabled="busy" placeholder="è¾å
¥æ¹æ¬¡å·" /></label> |
| | | <label class="m-field"><span>è´¨æ£ç»æ</span><select v-model="result" :disabled="busy"><option value="">å
¨é¨ç»æ</option><option value="1">åæ ¼</option><option value="2">ä¸åæ ¼</option></select></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢è®°å½' }}</button> |
| | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { onBeforeRouteLeave } from 'vue-router'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { getSeedIndicatorRanges, resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality'; |
| | | import { resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality'; |
| | | import { getWarehouseSimpleList, type MesWmWarehouseApi } from '#/api/mes/wm/warehouse'; |
| | | import { allowedMobilePath, extractCode } from '#/mobile/access'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | import { speakDuplicateScan } from '#/mobile/scanVoice'; |
| | | import Picker from './picker.vue'; |
| | | |
| | | const busy = ref(false), error = ref(''), notice = ref(''), bag = ref<SeedQualityScan>(); |
| | |
| | | const typeNames = { BAG: 'è¢ç ', PALLET: 'æçç ', BATCH: 'çäº§æ¹æ¬¡ç ' }; |
| | | const pendingType = ref<ScanType>('BAG'), pendingIds = ref<number[]>([]); |
| | | const storageKey = 'mom-mobile-quality-' + useUserStore().userInfo?.id; |
| | | const indicators = ref([ |
| | | { code: 'purity', name: '纯度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'cleanliness', name: 'å度', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'germination_rate', name: 'åè½ç', value: '', minValue: '', maxValue: '' }, |
| | | { code: 'moisture', name: 'æ°´å', value: '', minValue: '', maxValue: '' }, |
| | | ]); |
| | | const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => String(v).trim() !== '' && Number.isFinite(Number(v))) && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) <= Number(i.maxValue))); |
| | | const indicators = ref<Array<{ code: string; name: string; unit: string; value: string; minValue: string; maxValue: string }>>([]); |
| | | const complete = computed(() => indicators.value.length > 0 && indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => String(v).trim() !== '' && Number.isFinite(Number(v))) && Number(i.minValue) >= 0 && Number(i.maxValue) >= 0 && Number(i.value) >= 0 && Number(i.minValue) <= Number(i.maxValue))); |
| | | const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue))); |
| | | function clearPending() { pendingCode.value = ''; pendingIds.value = []; sessionStorage.removeItem(storageKey); } |
| | | async function identify(raw: string) { |
| | |
| | | try { |
| | | const data = await resolveSeedQuality(extractCode(raw), scanType.value); |
| | | if (data.canceled || !data.inbound) throw new Error('该对象已ä½åºæææè¢ç å°æªå
¨é¨å
¥åºï¼ä¸è½è´¨æ£'); |
| | | if (!data.pendingBagIds?.length) throw new Error('该对象没æå¾
è´¨æ£è¢ç ï¼è¯·å¨è´¨æ£è®°å½ä¸æ¥çå·²æç»æ'); |
| | | bag.value = data; indicators.value.forEach(i => { i.value = ''; i.minValue = ''; i.maxValue = ''; }); reason.value = ''; |
| | | try { |
| | | const ranges = await getSeedIndicatorRanges(); |
| | | indicators.value.forEach(i => { const range = ranges.find(r => r.indicatorCode === i.code); if (range) { i.minValue = String(range.minValue); i.maxValue = String(range.maxValue); } }); |
| | | } catch { /* 模æ¿ä¸å¯ç¨æ¶ä»å
许æå¨å¡«åæ¬æ¬¡èå´ */ } |
| | | if (!data.pendingBagIds?.length) { if (data.completedBagIds?.length) speakDuplicateScan('quality'); throw new Error('该对象没æå¾
è´¨æ£è¢ç ï¼è¯·å¨è´¨æ£è®°å½ä¸æ¥çå·²æç»æ'); } |
| | | if (!data.indicators?.length) throw new Error('该产åå°æªé
置质æ£é¡¹ï¼è¯·è系管çåç»´æ¤'); |
| | | bag.value = data; |
| | | indicators.value = data.indicators.map(i => ({ |
| | | code: i.code, name: i.name || i.code, unit: i.unit || '%', |
| | | value: '', minValue: String(i.minValue), maxValue: String(i.maxValue), |
| | | })); |
| | | reason.value = ''; |
| | | } catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function submit() { |
| | | if (busy.value || pendingCode.value || !bag.value || !complete.value || !warehouseId.value) return; |
| | | if (!passed.value && !reason.value.trim()) { error.value = '请填åä¸åæ ¼åå '; return; } |
| | | const count = bag.value.pendingBagIds!.length; |
| | | if (!window.confirm(passed.value ? `ç¡®è®¤å¯¹æ¬æ¬¡ ${count} è¢ç»ä¸æäº¤åæ£åæ ¼ï¼å®¡æ ¸éè¿åè¿å
¥å¤æ£ã` : `ç¡®è®¤å¯¹æ¬æ¬¡ ${count} è¢ç»ä¸æäº¤åæ£ä¸åæ ¼ï¼å°è¿å
¥ä¸åæ ¼å¤çã`)) return; |
| | | if (!window.confirm(passed.value ? `ç¡®è®¤å¯¹æ¬æ¬¡ ${count} è¢æäº¤åæ£åæ ¼ï¼å°åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½å¹¶è¿å
¥é¨é¨å®¡æ¹ã` : `ç¡®è®¤å¯¹æ¬æ¬¡ ${count} è¢æäº¤åæ£ä¸åæ ¼ï¼å°åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½å¹¶è¿å
¥å¤æ£ã`)) return; |
| | | error.value = ''; notice.value = ''; |
| | | pendingCode.value = bag.value.code; pendingType.value = scanType.value; pendingIds.value = [...bag.value.pendingBagIds!]; |
| | | sessionStorage.setItem(storageKey, JSON.stringify({ code: pendingCode.value, scanType: pendingType.value, bagIds: pendingIds.value })); busy.value = true; |
| | | try { |
| | | const result = await submitSeedQualityGroup({ code: bag.value.code, scanType: scanType.value, bagIds: pendingIds.value, qualifiedWarehouseId: warehouseId.value, reason: reason.value.trim() || undefined, indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) }); |
| | | clearPending(); notice.value = `æ¬æ¬¡ ${result.processedCount} è¢` + (passed.value ? 'åæ£åæ ¼ï¼çå¾
å®¡æ ¸ã' : '忣ä¸åæ ¼ï¼çå¾
ä¸åæ ¼å¤çã'); |
| | | clearPending(); notice.value = `æ¬æ¬¡ ${result.processedCount} è¢` + (passed.value ? 'åæ£åæ ¼ï¼å·²åæ¥çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½å¹¶è¿å
¥é¨é¨å®¡æ¹ã' : '忣ä¸åæ ¼ï¼å·²åæ¥çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½ï¼çå¾
夿£ã'); |
| | | bag.value = undefined; reason.value = ''; indicators.value.forEach(i => i.value = ''); |
| | | } catch (e) { error.value = 'æäº¤ç»æå¾
确认ï¼' + errorText(e) + 'ãè¯·æ ¸å®ååæä½ä¸ä¸è¢ã'; } finally { busy.value = false; } |
| | | } catch (e) { if (/已质æ£|å·²å
¨é¨è´¨æ£/.test(errorText(e))) speakDuplicateScan('quality'); error.value = 'æäº¤ç»æå¾
确认ï¼' + errorText(e) + 'ãè¯·æ ¸å®ååæä½ä¸ä¸è¢ã'; } finally { busy.value = false; } |
| | | } |
| | | async function verify() { |
| | | if (busy.value || !pendingCode.value) return; |
| | |
| | | <div v-if="pendingCode" class="m-panel"><h2 class="m-title">è´¨æ£ç»æå¾
确认</h2><p>{{pendingCode}}</p><button class="m-primary" :disabled="busy" @click="verify">æ ¸å®è´¨æ£ç»æ</button></div> |
| | | <template v-else> |
| | | <label class="m-field"><span>è´¨æ£æ«ç ç±»å</span><select v-model="scanType" :disabled="busy" @change="bag = undefined; notice = ''; error = ''"><option value="BAG">è¢ç </option><option value="PALLET">æçç </option><option value="BATCH">çäº§æ¹æ¬¡ç </option></select></label> |
| | | <p class="m-muted">æ«æ{{typeNames[scanType]}}ï¼æ ¸å¯¹å¾
æ£è¢æ°ï¼ç»ä¸å½å
¥åé¡¹ææ ãå·²æ£ãä½åºè¢ç ä¸ä¼éå¤å¤çã</p> |
| | | <Scanner :disabled="busy" @code="identify" /> |
| | | <p class="m-muted">æ«æ{{typeNames[scanType]}}ï¼æ ¸å¯¹å¾
æ£è¢æ°ï¼ç»ä¸å½å
¥è¯¥äº§åçå
¨é¨è´¨æ£é¡¹ã䏿¬¡æäº¤ä¼åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è´¨æ£è®°å½ï¼å·²æ£ãä½åºè¢ç ä¸ä¼éå¤å¤çã</p> |
| | | <Scanner voice-operation="quality" duplicate-voice :disabled="busy" @code="identify" /> |
| | | <form v-if="bag" @submit.prevent="submit"> |
| | | <div class="m-panel"><h2 class="m-title">æ¬æ¬¡æ£éªå¯¹è±¡</h2><dl><div class="m-row"><dt>对象类å</dt><dd>{{typeNames[scanType]}}</dd></div><div class="m-row"><dt>对象ç å¼</dt><dd>{{bag.code}}</dd></div><div class="m-row"><dt>ä¸å¡ç </dt><dd>{{bag.businessCode || 'â'}}</dd></div><div class="m-row"><dt>çäº§æ¹æ¬¡</dt><dd>{{bag.batchCode || 'â'}}</dd></div><div class="m-row"><dt>ææè¢æ°</dt><dd>{{bag.totalCount}}</dd></div><div class="m-row"><dt>å·²æ£è¢æ°</dt><dd>{{bag.completedBagIds?.length || 0}}</dd></div><div class="m-row"><dt>æ¬æ¬¡å¾
æ£</dt><dd>{{bag.pendingBagIds?.length || 0}} è¢</dd></div></dl><p v-if="scanType !== 'BAG'" class="m-warning">以䏿æ 忣éªç»è®ºå°åºç¨äºæ¬æ¬¡å
¨é¨å¾
æ£è¢ç ï¼è¯·ç¡®è®¤æ½æ ·ç»æéç¨äºè¿{{bag.pendingBagIds?.length}}è¢ã</p></div> |
| | | <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}ï¼%ï¼</legend> |
| | | <label class="m-field"><span>宿µå¼</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required placeholder="è¾å
¥å®æµç¾åæ¯" /></label> |
| | | <div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div> |
| | | <div class="m-panel"><h2 class="m-title">æ¬æ¬¡æ£éªå¯¹è±¡</h2><dl><div class="m-row"><dt>对象类å</dt><dd>{{typeNames[scanType]}}</dd></div><div class="m-row"><dt>对象ç å¼</dt><dd>{{bag.code}}</dd></div><div class="m-row"><dt>çäº§æ¹æ¬¡</dt><dd>{{bag.batchCode || 'â'}}</dd></div><div class="m-row"><dt>ææè¢æ°</dt><dd>{{bag.totalCount}}</dd></div><div class="m-row"><dt>å·²æ£è¢æ°</dt><dd>{{bag.completedBagIds?.length || 0}}</dd></div><div class="m-row"><dt>æ¬æ¬¡å¾
æ£</dt><dd>{{bag.pendingBagIds?.length || 0}} è¢</dd></div></dl><p v-if="scanType !== 'BAG'" class="m-warning">以䏿æ 忣éªç»è®ºå°åºç¨äºæ¬æ¬¡å
¨é¨å¾
æ£è¢ç ï¼è¯·ç¡®è®¤æ½æ ·ç»æéç¨äºè¿{{bag.pendingBagIds?.length}}è¢ã</p></div> |
| | | <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}ï¼{{i.unit || '%'}}ï¼</legend> |
| | | <label class="m-field"><span>宿µå¼</span><input v-model="i.value" type="number" inputmode="decimal" min="0" step="any" required placeholder="è¾å
¥å®æµå¼" /></label> |
| | | <div class="m-grid"><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" step="any" required /></label><label class="m-field"><span>åæ ¼ä¸é</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" step="any" required /></label></div> |
| | | </fieldset> |
| | | <div class="m-panel"><p class="m-muted">åæ ¼èå´ä»æ¨¡æ¿å¸¦å
¥ï¼å¯å¨æ¬æ¬¡è´¨æ£ä¸ä¿®æ¹ï¼è¯·æè¯¥åç§æ åæ ¸å¯¹ã</p><Picker v-model="warehouseId" label="ç®æ åæ ¼åº" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">ææ å¯ç¨åæ ¼åºï¼è¯·è系管çåé
ç½®ã</p><label class="m-field"><span>{{complete && !passed ? 'ä¸åæ ¼åå ï¼å¿
å¡«ï¼' : 'æ£éªå¤æ³¨'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div> |
| | | <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">é¢å¤ï¼{{passed ? 'åæ£åæ ¼ï¼çå¾
åæ£å®¡æ ¸' : '忣ä¸åæ ¼ï¼è¿å
¥ä¸åæ ¼å¤ç'}}ãæç»ä»¥æå¡ç«¯ç»æä¸ºåã</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? 'æ£å¨æäº¤â¦' : '确认æäº¤è´¨æ£'}}</button></div> |
| | | <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">é¢å¤ï¼{{passed ? 'åæ£åæ ¼ï¼çå¾
é¨é¨å®¡æ¹' : '忣ä¸åæ ¼ï¼è¿å
¥å¤æ£'}}ï¼åæ¶çæå å·¥é¨ãéå®é¨ä¸¤æ¡è®°å½ï¼ãæç»ä»¥æå¡ç«¯ç»æä¸ºåã</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? 'æ£å¨æäº¤â¦' : '确认æäº¤è´¨æ£'}}</button></div> |
| | | </form> |
| | | </template> |
| | | <RouterLink v-if="allowedMobilePath('/mobile/quality-records')" to="/mobile/quality-records" class="m-link">æ¥çè´¨æ£è®°å½</RouterLink> |
| | |
| | | import { can } from '#/mobile/access'; |
| | | import { lookup, errorText, writeBlockReason, type TraceResult, type TraceObject } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | import { speakDuplicateScan } from '#/mobile/scanVoice'; |
| | | defineOptions({name:'MobileQuery'}); |
| | | const route=useRoute(),key=route.path.split('/')[2]||'bags'; |
| | | const expected={bags:'bag',pallets:'pallet',batches:'batch'}[key]; |
| | |
| | | if((binding.value||autoPacking.value)&&writeBlockReason(r))throw new Error(writeBlockReason(r)); |
| | | if(autoPacking.value){ |
| | | if(r.type!=='bag')throw new Error('è¯·æ«æè¢ç '); |
| | | if(r.data.palletId)throw new Error('该è¢ç å·²ç»å®æçï¼è¯·æ ¸å®åæä½'); |
| | | if(r.data.palletId){speakDuplicateScan('bind');throw new Error('该è¢ç å·²ç»å®æçï¼è¯·æ ¸å®åæä½');} |
| | | if(!r.data.itemId)throw new Error('该è¢ç 缺å°åç§ï¼æ æ³ç»å®æç'); |
| | | if(bags.value.some(b=>b.data.id===r.data.id))throw new Error('æ¬æ¬¡å·²ç»éé该è¢ç '); |
| | | if(bags.value.some(b=>b.data.id===r.data.id)){speakDuplicateScan('bind');throw new Error('æ¬æ¬¡å·²ç»éé该è¢ç ');} |
| | | const first=bags.value[0]; |
| | | if(first&&first.data.itemId!==r.data.itemId)throw new Error('è¢ç åç§ä¸æ¬æ¬¡æçä¸ä¸è´'); |
| | | if(first&&first.data.batchId!==r.data.batchId)throw new Error('è¢ç æå±æ¹æ¬¡ä¸æ¬æ¬¡æçä¸ä¸è´'); |
| | |
| | | }else{ |
| | | if(r.type!==expected)throw new Error('è¯·æ«æ'+(key==='bags'?'è¢ç ':'æçç ')); |
| | | if(key==='bags'){ |
| | | if(r.data.palletId)throw new Error('该è¢ç å·²ç»å®æçï¼è¯·æ ¸å®åæä½'); |
| | | if(r.data.palletId){speakDuplicateScan('bind');throw new Error('该è¢ç å·²ç»å®æçï¼è¯·æ ¸å®åæä½');} |
| | | if(r.data.batchId&&target.value.data.batchId&&r.data.batchId!==target.value.data.batchId)throw new Error('è¢ç ä¸ç®æ æçæ¹æ¬¡ä¸ä¸è´'); |
| | | if(bags.value.some(b=>b.data.id===r.data.id))throw new Error('æ¬æ¬¡å·²ç»éé该è¢ç '); |
| | | if(bags.value.some(b=>b.data.id===r.data.id)){speakDuplicateScan('bind');throw new Error('æ¬æ¬¡å·²ç»éé该è¢ç ');} |
| | | bags.value.push(r); |
| | | }else{ |
| | | if(r.data.batchId)throw new Error('该æçå·²ç»å®æ¹æ¬¡'); |
| | |
| | | <div v-if="target" class="m-panel"><p>ç®æ ï¼{{target.code}}</p><p class="m-muted">{{target.data.itemName||'æªå½å
¥åç§'}} · {{target.data.batchCode||'æªç»å®æ¹æ¬¡'}}</p></div> |
| | | </template> |
| | | <template v-if="autoPacking"><h2 class="m-title">æ«æè¢ç çææç</h2><p class="m-muted">{{palletSize}} è¢ä¸ºæéèç¹ Â· å·²éé {{bags.length}} è¢ãè¾¾å°èç¹ä¼æéï¼å¯éæ¶çæå¹¶ç»å®ã</p></template> |
| | | <Scanner :disabled="busy||thresholdPrompt" :continuous="autoPacking" @code="identify"/> |
| | | <Scanner voice-operation="bind" :duplicate-voice="binding||autoPacking" :disabled="busy||thresholdPrompt" :continuous="autoPacking" @code="identify"/> |
| | | <section v-if="autoPacking&&thresholdPrompt" class="m-panel" role="alert" aria-labelledby="pallet-threshold-title" aria-describedby="pallet-threshold-description"> |
| | | <h2 id="pallet-threshold-title" class="m-title">å·²è¾¾å° {{palletSize}} è¢æéèç¹</h2> |
| | | <p id="pallet-threshold-description">å¯ç«å³çæå¹¶ç»å®æçç ï¼ä¹å¯ç»§ç»æ«ææ´å¤è¢ç å å
¥æ¬æçã</p> |
| | |
| | | <p v-else class="m-status">{{detail.data.inboundTime ? 'å·²å
¥åº' : 'æªå
¥åº'}}<span v-if="detail.type === 'pallet'"> · å¾
å
¥åº {{detail.data.pendingBagQuantity ?? 'â'}} è¢</span></p> |
| | | <p v-if="writeBlockReason(detail)" class="m-warning">{{writeBlockReason(detail)}}</p> |
| | | <dl><div class="m-row"><dt>ç å¼</dt><dd>{{detail.code}}</dd></div><div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{detail.data.batchCode||detail.data.tempCode||'æªç»å®'}}</dd></div><div class="m-row"><dt>æç</dt><dd>{{detail.data.palletCode||'â'}}</dd></div><div class="m-row"><dt>çäº§æ¥æ</dt><dd>{{detail.data.produceDate||'æªå½å
¥'}}</dd></div><div class="m-row"><dt>产线</dt><dd>{{detail.data.lineName||'æªå½å
¥'}}</dd></div></dl> |
| | | <dl v-if="detail.type==='bag'"><div class="m-row"><dt>追溯ç </dt><dd>{{detail.data.code}}</dd></div><div class="m-row"><dt>ä¸å¡ç </dt><dd>{{detail.data.businessCode||'æªçæ'}}</dd></div></dl> |
| | | <dl v-else><div class="m-row"><dt>ä¸å¡ç </dt><dd>{{detail.data.businessCode||'æªçæ'}}</dd></div></dl> |
| | | <dl v-if="detail.type==='bag'"><div class="m-row"><dt>追溯ç </dt><dd>{{detail.data.code}}</dd></div></dl> |
| | | </div> |
| | | <template v-if="binding||autoPacking"> |
| | | <p v-if="key==='bags'" class="m-muted">å·²éé {{bags.length}} è¢ï¼å°æªæäº¤ç»å®</p> |
| | |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ stock?.itemName || trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ stock?.batchCode || trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>åºä½</dt><dd>{{ [stock?.warehouseName, stock?.locationName, stock?.areaName].filter(Boolean).join(' / ') || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ä»åº</dt><dd>{{ stock?.warehouseName || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>åºåæ°é</dt><dd>{{ stock?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { onBeforeRouteLeave } from 'vue-router'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { getProductSalesPage, getProductSales, previewProductSalesScan, scanProductSales, checkProductSalesQuantity, stockProductSales, shippingProductSales, finishProductSales, type MesWmProductSalesApi } from '#/api/mes/wm/productsales'; |
| | | import { getProductSalesPage, getProductSales, getProductSalesShipments, closeProductSalesRemaining, cancelProductSalesShipment, previewProductSalesScan, scanProductSales, checkProductSalesQuantity, stockProductSales, shippingProductSales, finishProductSales, type MesWmProductSalesApi } from '#/api/mes/wm/productsales'; |
| | | import { getProductSalesLinePage, type MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line'; |
| | | import { getProductSalesDetailListByLineId, type MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail'; |
| | | import { getProductSalesDetailListByLineId, createMobileManualDetail, deleteProductSalesDetail, type MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail'; |
| | | import { can, extractCode } from '#/mobile/access'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | import { speakDuplicateScan } from '#/mobile/scanVoice'; |
| | | import PickingBags from '../wls/productsales/modules/picking-bags.vue'; |
| | | import { formatPickingRemark } from '../wls/productsales/data'; |
| | | import { getPickingStocks, type MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | |
| | | type Sales = MesWmProductSalesApi.ProductSales; |
| | | type Preview = MesWmProductSalesApi.ProductSalesScanResponse; |
| | | type Line = MesWmProductSalesLineApi.ProductSalesLine; |
| | | type Detail = MesWmProductSalesDetailApi.ProductSalesDetail; |
| | | type Action = 'scan' | 'stock' | 'shipping' | 'finish'; |
| | | type Pending = { salesId: number; action: Action; before: number; preview?: Preview; carrier?: string; shippingNumber?: string }; |
| | | const statusNames: Record<number, string> = { 0: 'è稿', 1: 'å¾
æ£æµ', 2: 'å¾
æ£è´§', 10: 'å¾
å¡«åè¿å', 3: 'å¾
æ§è¡åºåº', 4: '已宿', 5: '已忶' }; |
| | | type Action = 'scan' | 'manual' | 'stock' | 'shipping' | 'finish' | 'close' | 'cancelShipment' | 'delete'; |
| | | type Pending = { salesId: number; action: Action; before: number; preview?: Preview; carrier?: string; shippingNumber?: string; lineId?: number; materialStockId?: number; manual?: boolean; detail?: Detail; detailId?: number }; |
| | | const statusNames: Record<number, string> = { 0: 'è稿', 1: 'å¾
æ£æµ', 2: 'å¾
æ£è´§', 10: 'å¾
å¡«åè¿å', 3: 'å¾
æ§è¡åºåº', 4: '已宿', 5: '已忶', 8: 'å·²å
³é' }; |
| | | function statusLabel(order: Sales) { return `${order.partiallyShipped ? 'é¨ååè´§ / ' : ''}${statusNames[order.status!] || 'æªç¥ç¶æ'}`; } |
| | | const busy = ref(false), error = ref(''), notice = ref(''), code = ref(''), status = ref(''); |
| | | const rows = ref<Sales[]>([]), page = ref(0), total = ref(0), sales = ref<Sales>(); |
| | | const lines = ref<Line[]>([]), details = ref<Detail[]>([]), preview = ref<Preview>(), pending = ref<Pending>(); |
| | | const shipments = ref<MesWmProductSalesApi.Shipment[]>([]); |
| | | const shipmentNames: Record<number, string> = { 0: 'æ£è´§ä¸', 10: 'å¾
å¡«åè¿å', 20: 'å¾
æ§è¡åºåº', 30: 'å·²åºåº', 40: '已忶' }; |
| | | const carrier = ref(''), shippingNumber = ref(''); |
| | | const scanType = ref<'BAG' | 'PALLET' | 'BATCH'>('BAG'), selectedBagIds = ref<number[]>([]); |
| | | const manualLine = ref<Line>(), manualStock = ref<MesWmMaterialStockApi.MaterialStock>(); |
| | | const manualStocks = ref<MesWmMaterialStockApi.MaterialStock[]>([]), stocksLoaded = ref(false); |
| | | const manualQuantity = ref<number>(); |
| | | const storageKey = 'mom-mobile-sales-' + useUserStore().userInfo?.id; |
| | | let filters: { code?: string; status?: number } = {}; |
| | | const picking = computed(() => sales.value?.status === 2 && can('mes:wm-product-sales:stock')); |
| | | function clearPending() { pending.value = undefined; sessionStorage.removeItem(storageKey); } |
| | | function requestId() { |
| | | if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); |
| | | if (!globalThis.crypto?.getRandomValues) throw new Error('å½åç¯å¢æ æ³çæå®å
¨çè¯·æ±æ è¯'); |
| | | const bytes = crypto.getRandomValues(new Uint8Array(16)); |
| | | bytes[6] = (bytes[6]! & 15) | 64; bytes[8] = (bytes[8]! & 63) | 128; |
| | | const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); |
| | | return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`; |
| | | } |
| | | async function load(reset = true) { |
| | | if (busy.value || pending.value) return; |
| | | busy.value = true; error.value = ''; |
| | |
| | | if (all.length >= data.total || !data.list.length) break; |
| | | } |
| | | const picked = (await Promise.all(all.filter(l => l.id != null).map(l => getProductSalesDetailListByLineId(l.id!)))).flat(); |
| | | shipments.value = await getProductSalesShipments(id); |
| | | sales.value = order; lines.value = all; details.value = picked; |
| | | carrier.value = order.carrier || ''; shippingNumber.value = order.shippingNumber || ''; |
| | | const current = shipments.value.find(s => s.status === 10); |
| | | carrier.value = current ? current.carrier || '' : order.carrier || ''; |
| | | shippingNumber.value = current ? current.shippingNumber || '' : order.shippingNumber || ''; |
| | | } |
| | | async function open(id?: number) { |
| | | if (!id || busy.value || pending.value) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; preview.value = undefined; |
| | | busy.value = true; error.value = ''; notice.value = ''; preview.value = undefined; manualLine.value = undefined; manualStock.value = undefined; manualStocks.value = []; stocksLoaded.value = false; |
| | | try { await readOrder(id); } catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | function pickedQuantity(lineId?: number) { return details.value.filter(d => d.lineId === lineId).reduce((sum, d) => sum + Number(d.quantity || 0), 0); } |
| | | function shippedQuantity(lineId?: number) { const line = lines.value.find(l => l.id === lineId); if (line?.shippedQuantity != null) return Number(line.shippedQuantity); const done = new Set(shipments.value.filter(s => s.status === 30).map(s => s.id)); return details.value.filter(d => d.lineId === lineId && d.shipmentId && done.has(d.shipmentId)).reduce((sum, d) => sum + Number(d.quantity || 0), 0); } |
| | | async function identify(raw: string) { |
| | | if (busy.value || !picking.value || pending.value) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; preview.value = undefined; |
| | | busy.value = true; error.value = ''; notice.value = ''; preview.value = undefined; manualLine.value = undefined; manualStock.value = undefined; manualStocks.value = []; stocksLoaded.value = false; |
| | | try { |
| | | selectedBagIds.value = []; |
| | | const data = await previewProductSalesScan(extractCode(raw), scanType.value, sales.value?.id); |
| | | if (data.scanType === 'BAG' && data.codeId && details.value.some(d => d.bagCodeId === data.codeId)) { speakDuplicateScan('outbound'); throw new Error('该è¢ç 已卿£è´§æ¸
åä¸ï¼è¯·å¿é夿«æ'); } |
| | | if (!data.codeId || !data.scanQuantity || data.scanQuantity <= 0) throw new Error('æ æ³ç¡®è®¤æ«ç æ°éï¼è¯·æ ¸å¯¹ç å¼'); |
| | | const line = lines.value.find(l => l.itemId === data.itemId); |
| | | if (!line) throw new Error('æ«ç åç§ä¸å¨å½ååºåºåä¸'); |
| | | data.itemName = line.itemName; preview.value = data; |
| | | if (data.scanType === 'BAG') selectedBagIds.value = data.bags?.filter(b => b.selectable).map(b => b.id) || []; |
| | | } catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } catch (e) { error.value = errorText(e); if (/å·²æ«æåºåº/.test(error.value)) speakDuplicateScan('outbound'); } finally { busy.value = false; } |
| | | } |
| | | async function chooseManualLine(line: Line) { |
| | | if (!line.id || !picking.value || busy.value || pending.value) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; preview.value = undefined; |
| | | selectedBagIds.value = []; manualLine.value = line; manualStock.value = undefined; stocksLoaded.value = false; |
| | | try { manualStocks.value = await getPickingStocks(line.id); stocksLoaded.value = true; } |
| | | catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function chooseManualStock(stock: MesWmMaterialStockApi.MaterialStock) { |
| | | if (!stock.id || !manualLine.value || !sales.value?.id || busy.value) return; |
| | | if (!stock.bagCount) { manualStock.value = stock; manualQuantity.value = undefined; preview.value = undefined; return; } |
| | | if (!stock.batchCode) { error.value = '该åºåç¼ºå°æ¹æ¬¡å·ï¼æ æ³éæ©è¢ç '; return; } |
| | | busy.value = true; error.value = ''; preview.value = undefined; selectedBagIds.value = []; |
| | | try { |
| | | const data = await previewProductSalesScan(stock.batchCode, 'BATCH', sales.value.id); |
| | | if (data.itemId !== manualLine.value.itemId) throw new Error('åºåæ¹æ¬¡çåç§ä¸åºåºåè¡ä¸ä¸è´'); |
| | | const bags = (data.bags || []).filter(b => b.materialStockId === stock.id); |
| | | if (!bags.length) throw new Error('该åºå没æå¯è¯å«çè¢ç ï¼è¯·å¨ PC 端å¤çæ è¢ç åå²åºå'); |
| | | manualStock.value = stock; data.bags = bags; data.itemName = manualLine.value.itemName; |
| | | data.scanQuantity = bags.filter(b => b.selectable).length; preview.value = data; |
| | | } catch (e) { manualStock.value = undefined; error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function removePendingDetail(detail: Detail) { |
| | | if (!detail.id || detail.shipmentId || !sales.value?.id || !picking.value || busy.value || pending.value || !can('mes:wm-product-sales:delete')) return; |
| | | if (!window.confirm('确认移é¤è¿æ¡å°æªåè´§çæ£è´§æç»ï¼')) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; |
| | | const operation: Pending = { salesId: sales.value.id, before: sales.value.status!, action: 'delete', detailId: detail.id }; |
| | | pending.value = operation; sessionStorage.setItem(storageKey, JSON.stringify(operation)); |
| | | try { await deleteProductSalesDetail(detail.id); clearPending(); await readOrder(operation.salesId); notice.value = 'æ£è´§æç»å·²ç§»é¤ã'; } |
| | | catch (e) { error.value = 'æä½ç»æå¾
确认ï¼è¯·å
æ ¸å®ï¼' + errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function execute(action: Action) { |
| | | const order = sales.value; |
| | | if (!order?.id || busy.value || pending.value) return; |
| | | const permission = action === 'scan' ? 'stock' : action; |
| | | const permission = action === 'scan' || action === 'manual' ? 'stock' : action === 'close' || action === 'cancelShipment' ? 'cancel' : action; |
| | | if (!can('mes:wm-product-sales:' + permission)) return; |
| | | if (action === 'scan' && (!picking.value || !preview.value)) return; |
| | | if (action === 'scan' && !selectedBagIds.value.length) { error.value = 'è¯·éæ©æ¬æ¬¡æ£è´§å¯¹åºçè¢ç '; return; } |
| | | if (action === 'manual' && (!picking.value || !manualLine.value?.id || !manualStock.value?.id || !manualQuantity.value || manualQuantity.value <= 0 || manualQuantity.value > Number(manualStock.value.availableQuantity ?? manualStock.value.quantity ?? 0))) { error.value = '请è¾å
¥ä¸è¶
è¿å¯ç¨åºåçæææ°é'; return; } |
| | | if (action === 'stock' && order.status !== 2 || action === 'shipping' && order.status !== 10 || action === 'finish' && order.status !== 3) return; |
| | | if (action === 'shipping' && (!carrier.value.trim() || !shippingNumber.value.trim())) { error.value = 'è¯·å¡«åæ¿è¿ååè¿è¾åå·'; return; } |
| | | const message = action === 'scan' ? `ç¡®è®¤å°æé ${selectedBagIds.value.length} è¢å å
¥æ¬åæ£è´§å¹¶å
³è追溯è¢ç ï¼` : action === 'stock' ? 'ç¡®è®¤å®ææ£è´§ï¼ç³»ç»å°æ ¸å¯¹ååç§æ°éã' : action === 'shipping' ? '确认ä¿åè¿åå¹¶è¿å
¥å¾
æ§è¡åºåºï¼' : '确认æ§è¡åºåºï¼æ¬åå°æ£ååºåå¹¶æ è®°å·²å®æã'; |
| | | const closeReason = action === 'close' ? window.prompt('请è¾å
¥å
³éå©ä½æ°éçåå ï¼æå¤500åï¼') : undefined; |
| | | if (action === 'close' && closeReason === null) return; |
| | | if (action === 'close' && (!closeReason?.trim() || closeReason.trim().length > 500)) { error.value = '请填åä¸è¶
è¿500åçå
³éåå '; return; } |
| | | const message = action === 'cancelShipment' ? 'ç¡®è®¤åæ¶æ¬æ¬¡åè´§å¹¶éæ¾åºåå ç¨ï¼æ£è´§æç»ä¼ä¿çä¾è°æ´ã' : action === 'close' ? '确认å
³éå©ä½æªåæ°éï¼å·²åºåºè®°å½å°ä¿çã' : action === 'manual' ? `ç¡®è®¤ææ°éæ£è´§ ${manualQuantity.value}ï¼è¯¥åå²åºå没æè¢ç æç»ï¼æ æ³éè¢è¿½æº¯ã` : action === 'scan' ? `ç¡®è®¤å°æé ${selectedBagIds.value.length} è¢å å
¥æ¬åæ£è´§å¹¶å
³è追溯è¢ç ï¼` : action === 'stock' ? 'ç¡®è®¤å®ææ¬æ¬¡åè´§æ£è´§ï¼ç³»ç»å°æ ¸å¯¹ç´¯è®¡æ°éã' : action === 'shipping' ? '确认ä¿åæ¬æ¬¡è¿åå¹¶è¿å
¥å¾
æ§è¡åºåºï¼' : '确认æ§è¡æ¬æ¬¡åºåºï¼ç³»ç»å°æ£åæ¬æ¬¡åºåã'; |
| | | if (!window.confirm(message)) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; |
| | | try { |
| | | if (action === 'stock' && !await checkProductSalesQuantity(order.id)) throw new Error('æ£è´§æ°éä¸åºåºæ°éä¸ä¸è´ï¼è¯·æ ¸å¯¹æ¯ä¸ªåç§'); |
| | | const operation: Pending = { salesId: order.id, action, before: order.status!, preview: action === 'scan' ? { ...preview.value, selectedBagIds: [...selectedBagIds.value], scanQuantity: selectedBagIds.value.length } : undefined, carrier: carrier.value.trim(), shippingNumber: shippingNumber.value.trim() }; |
| | | if (action === 'stock' && !await checkProductSalesQuantity(order.id)) throw new Error('请å
æ£éæ¬æ¬¡æç»ï¼ä¸ç´¯è®¡æ°éä¸è½è¶
è¿è®¡åæ°é'); |
| | | const operation: Pending = { salesId: order.id, action, before: order.status!, preview: action === 'scan' ? { ...preview.value, selectedBagIds: [...selectedBagIds.value], scanQuantity: selectedBagIds.value.length } : undefined, carrier: carrier.value.trim(), shippingNumber: shippingNumber.value.trim(), lineId: manualStock.value ? manualLine.value?.id : undefined, materialStockId: manualStock.value?.id, manual: !!manualStock.value }; |
| | | if (action === 'manual') { |
| | | operation.detail = { salesId: order.id, lineId: manualLine.value!.id, itemId: manualLine.value!.itemId, |
| | | materialStockId: manualStock.value!.id, batchId: manualStock.value!.batchId, batchCode: manualStock.value!.batchCode, |
| | | warehouseId: manualStock.value!.warehouseId, |
| | | quantity: manualQuantity.value, remark: `ç§»å¨ç«¯æ è¢ç æ£è´§ #${requestId()}` }; |
| | | } |
| | | pending.value = operation; sessionStorage.setItem(storageKey, JSON.stringify(operation)); |
| | | if (action === 'scan') { |
| | | const result = await scanProductSales({ salesId: order.id, scanContent: operation.preview!.scanContent!, scanType: operation.preview!.scanType as 'BAG' | 'PALLET' | 'BATCH', bagIds: operation.preview!.selectedBagIds, quantity: operation.preview!.scanQuantity }); |
| | | const result = await scanProductSales({ salesId: order.id, scanContent: operation.preview!.scanContent!, scanType: operation.preview!.scanType as 'BAG' | 'PALLET' | 'BATCH', bagIds: operation.preview!.selectedBagIds, quantity: operation.preview!.scanQuantity, lineId: operation.lineId, materialStockId: operation.materialStockId, manual: operation.manual }); |
| | | if (result.alreadyScanned) speakDuplicateScan('outbound'); |
| | | notice.value = result.alreadyScanned ? '该ç 已卿£è´§æ¸
åä¸ï¼æ¬æ¬¡æªé夿·»å ã' : `å·²å å
¥æ£è´§ï¼${result.outboundQuantity || operation.preview!.scanQuantity} è¢ã`; |
| | | } else if (action === 'stock') { await stockProductSales(order.id); notice.value = 'æ£è´§å®æï¼è¯·å¡«åè¿åã'; } |
| | | } else if (action === 'manual') { await createMobileManualDetail(operation.detail!); notice.value = 'æ°éæ£è´§å·²å å
¥ï¼è¯¥æç»æ è¢ç 追溯ã'; } |
| | | else if (action === 'stock') { await stockProductSales(order.id); notice.value = 'æ£è´§å®æï¼è¯·å¡«åè¿åã'; } |
| | | else if (action === 'shipping') { await shippingProductSales({ id: order.id, carrier: operation.carrier, shippingNumber: operation.shippingNumber }); notice.value = 'è¿åå·²ä¿åï¼è¯·æ ¸å¯¹åæ§è¡åºåºã'; } |
| | | else { await finishProductSales(order.id); notice.value = 'åºåºå®æï¼åºåå·²æ´æ°ã'; } |
| | | clearPending(); preview.value = undefined; |
| | | else if (action === 'close') { await closeProductSalesRemaining(order.id, closeReason!.trim()); notice.value = 'å©ä½æ°éå·²å
³éã'; } |
| | | else if (action === 'cancelShipment') { await cancelProductSalesShipment(order.id); notice.value = 'æ¬æ¬¡åè´§å·²åæ¶ï¼åºåå ç¨å·²éæ¾ã'; } |
| | | else { await finishProductSales(order.id); notice.value = 'æ¬æ¬¡åºåºå®æï¼åºåå·²æ´æ°ã'; } |
| | | clearPending(); preview.value = undefined; manualStock.value = undefined; manualLine.value = undefined; manualStocks.value = []; stocksLoaded.value = false; |
| | | await readOrder(order.id); |
| | | } catch (e) { error.value = (pending.value ? 'æä½ç»æå¾
确认ï¼è¯·å
æ ¸å®ï¼' : '') + errorText(e); } finally { busy.value = false; } |
| | | } |
| | |
| | | const operation = pending.value; |
| | | await readOrder(operation.salesId); |
| | | const requestedIds = operation.preview?.selectedBagIds; |
| | | const found = operation.action === 'scan' ? requestedIds?.length ? requestedIds.every(id => details.value.some(d => d.bagCodeId === id)) : details.value.some(d => operation.preview?.scanType === 'PALLET' ? d.palletCodeId === operation.preview.codeId : d.bagCodeId === operation.preview?.codeId) : sales.value?.status !== operation.before; |
| | | const found = operation.action === 'delete' ? !details.value.some(d => d.id === operation.detailId) : operation.action === 'manual' ? details.value.some(d => d.remark === operation.detail?.remark) : operation.action === 'scan' ? requestedIds?.length ? requestedIds.every(id => details.value.some(d => d.bagCodeId === id)) : details.value.some(d => operation.preview?.scanType === 'PALLET' ? d.palletCodeId === operation.preview.codeId : d.bagCodeId === operation.preview?.codeId) : sales.value?.status !== operation.before; |
| | | if (operation.action === 'scan' && !found && requestedIds?.some(id => details.value.some(d => d.bagCodeId === id))) { error.value = 'æéè¢ç ä»
é¨ååå¨äºæ£è´§æç»ï¼è¯·èç³»è´è´£äººæ ¸å¯¹ï¼ä¸è½èªå¨éè¯ã'; return; } |
| | | if (found) { |
| | | notice.value = operation.action === 'scan' ? 'å·²æ ¸å®ï¼è¯¥ç 已卿£è´§æ¸
åä¸ã' : 'å½ååæ®ç¶æï¼' + statusNames[sales.value!.status!] + 'ãè¯·æ ¸å¯¹æç»åè¿ååç»§ç»ã'; |
| | |
| | | <p v-if="error" class="m-error" role="alert">{{error}}</p> |
| | | <section v-if="pending" class="m-panel"><h2 class="m-title">åºåºæä½ç»æå¾
确认</h2><p>åæ®ç¼å· {{pending.salesId}}ï¼è¯·å
æ ¸å®åç»§ç»æä½ã</p><button class="m-primary" :disabled="busy" @click="verify">æ ¸å®æä½ç»æ</button></section> |
| | | <template v-if="sales"> |
| | | <button v-if="!pending" class="m-secondary" :disabled="busy" @click="sales = undefined; preview = undefined; load()">è¿ååºåºåå表</button> |
| | | <section class="m-panel" style="margin-top:16px"><h2 class="m-title">{{sales.code}}</h2><strong class="m-status">{{statusNames[sales.status!] || 'æªç¥ç¶æ'}}</strong><dl><div class="m-row"><dt>客æ·</dt><dd>{{sales.clientName || 'â'}}</dd></div><div class="m-row"><dt>æ¶è´§äºº</dt><dd>{{sales.contactName || 'â'}} {{sales.contactTelephone}}</dd></div><div class="m-row"><dt>æ¶è´§å°å</dt><dd>{{sales.contactAddress || 'â'}}</dd></div><div v-if="sales.shippingNumber" class="m-row"><dt>è¿å</dt><dd>{{sales.carrier}} · {{sales.shippingNumber}}</dd></div></dl><button v-if="!pending" class="m-secondary" :disabled="busy" @click="open(sales.id)">å·æ°åæ®</button></section> |
| | | <button v-if="!pending" class="m-secondary" :disabled="busy" @click="sales = undefined; preview = undefined; manualLine = undefined; manualStock = undefined; manualStocks = []; stocksLoaded = false; load()">è¿ååºåºåå表</button> |
| | | <section class="m-panel" style="margin-top:16px"><h2 class="m-title">{{sales.code}}</h2><strong class="m-status">{{statusLabel(sales)}}</strong><dl><div class="m-row"><dt>客æ·</dt><dd>{{sales.clientName || 'â'}}</dd></div><div class="m-row"><dt>æ¶è´§äºº</dt><dd>{{sales.contactName || 'â'}} {{sales.contactTelephone}}</dd></div><div class="m-row"><dt>æ¶è´§å°å</dt><dd>{{sales.contactAddress || 'â'}}</dd></div><div v-if="sales.shippingNumber" class="m-row"><dt>æè¿è¿å</dt><dd>{{sales.carrier}} · {{sales.shippingNumber}}</dd></div></dl><button v-if="!pending" class="m-secondary" :disabled="busy" @click="open(sales.id)">å·æ°åæ®</button></section> |
| | | <h2 class="m-section-title">åºåºåç§ä¸æ£è´§è¿åº¦</h2> |
| | | <section v-if="shipments.length" class="m-panel"><h2 class="m-title">åè´§è®°å½</h2><div v-for="shipment in shipments" :key="shipment.id" class="m-record"><strong>第 {{shipments.indexOf(shipment) + 1}} 次 · {{shipmentNames[shipment.status] || 'æªç¥ç¶æ'}}</strong><p v-if="shipment.shippingNumber">{{shipment.carrier || 'æªå½å
¥æ¿è¿å'}} · {{shipment.shippingNumber}}</p><p v-if="shipment.outboundTime">åºåºæ¶é´ï¼{{shipment.outboundTime}}</p></div></section> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ åç§æç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"><h3 class="m-title">{{line.itemName || line.itemCode}}</h3><p>计å {{line.quantity || 0}} {{line.unitMeasureName || 'è¢'}} · å·²æ£ {{pickedQuantity(line.id)}} {{line.unitMeasureName || 'è¢'}}</p><details><summary>æ¥çæ£è´§æç»</summary><div v-for="detail in details.filter(d => d.lineId === line.id)" :key="detail.id" class="m-record"><p>{{detail.batchCode || 'æªå½å
¥æ¹æ¬¡'}} · {{detail.quantity}} {{line.unitMeasureName || 'è¢'}}</p><p class="m-muted">{{detail.warehouseName || 'â'}} / {{detail.locationName || 'â'}} / {{detail.areaName || 'â'}}</p><p class="m-muted">{{detail.bagCodeId ? 'è¢ç ç¼å· ' + detail.bagCodeId : 'æçç¼å· ' + detail.palletCodeId}}</p><p v-if="detail.remark" class="m-muted" style="overflow-wrap:anywhere">{{detail.remark}}</p></div></details></article> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"><h3 class="m-title">{{line.itemName || line.itemCode}}</h3><p>计å {{line.quantity || 0}} · å·²å {{shippedQuantity(line.id)}} · å·²æ£å计 {{pickedQuantity(line.id)}} {{line.unitMeasureName || 'è¢'}}</p><details><summary>æ¥çæ£è´§æç»</summary><div v-for="detail in details.filter(d => d.lineId === line.id)" :key="detail.id" class="m-record"><p>{{detail.batchCode || 'æªå½å
¥æ¹æ¬¡'}} · {{detail.quantity}} {{line.unitMeasureName || 'è¢'}}</p><p class="m-muted">{{detail.warehouseName || 'â'}}</p><p class="m-muted">{{detail.bagCodeId ? 'è¢ç ç¼å· ' + detail.bagCodeId : detail.palletCodeId ? 'æçç¼å· ' + detail.palletCodeId : 'æ è¢ç æç»'}}</p><p v-if="detail.remark" class="m-muted" style="overflow-wrap:anywhere">{{formatPickingRemark(detail.remark)}}</p><button v-if="picking && !detail.shipmentId && !pending && can('mes:wm-product-sales:delete')" class="m-secondary" :disabled="busy || !!preview" @click="removePendingDetail(detail)">ç§»é¤æªåæç»</button></div></details><button v-if="picking && !pending" class="m-secondary" :disabled="busy || !!preview" @click="chooseManualLine(line)">æå·¥éæ©åºåè¢ç </button></article> |
| | | <section v-if="manualLine && !pending" class="m-panel"> |
| | | <h2 class="m-title">éæ© {{manualLine.itemName || manualLine.itemCode}} çåºå</h2> |
| | | <p v-if="stocksLoaded && !manualStocks.length" class="m-empty">没æå¯éçåæ ¼åºå</p> |
| | | <button v-for="stock in manualStocks" :key="stock.id" class="m-order-button" :disabled="busy || !!preview" @click="chooseManualStock(stock)"> |
| | | <strong>æ¹æ¬¡ {{stock.batchCode || 'æªå½å
¥'}}</strong><span>{{stock.warehouseName}}</span><small>å¯ç¨ {{stock.availableQuantity ?? stock.quantity ?? 0}} · å¨åºè¢ç {{stock.bagCount ?? 0}}</small> |
| | | </button> |
| | | <div v-if="manualStock && !manualStock.bagCount && !preview" class="m-record"> |
| | | <p>该åºåææ å¨åºè¢ç æç»ï¼ææ°éæ£è´§æ æ³éè¢è¿½æº¯ã</p> |
| | | <label class="m-field"><span>æ¬æ¬¡æ£è´§æ°é</span><input v-model.number="manualQuantity" type="number" min="0.000001" step="any" :max="manualStock.availableQuantity ?? manualStock.quantity ?? 0" :disabled="busy" /></label> |
| | | <button class="m-primary" :disabled="busy || !manualQuantity" @click="execute('manual')">确认æ°éæ£è´§</button> |
| | | </div> |
| | | <button class="m-detail-toggle" :disabled="busy || !!preview" @click="manualLine = undefined; manualStocks = []; stocksLoaded = false">å
³éåºåéæ©</button> |
| | | </section> |
| | | <template v-if="!pending"> |
| | | <template v-if="picking"><h2 class="m-section-title">æ«ç æ£è´§</h2><label class="m-field"><span>æ£è´§æ«ç ç±»å</span><select v-model="scanType" :disabled="busy" @change="preview = undefined; selectedBagIds = []"><option value="BAG">è¢ç </option><option value="PALLET">æçç </option><option value="BATCH">çäº§æ¹æ¬¡ç </option></select></label><p class="m-muted">æ¯æä¸å¡ç ä¸è¿½æº¯ç ãæçãæ¹æ¬¡è¯å«åéæ©å®é
æ£è´§è¢ç ï¼æ°éææéè¢æ°è®¡å
¥ã</p><Scanner :disabled="busy" @code="identify" /><section v-if="preview" class="m-panel"><h3 class="m-title">æ ¸å¯¹æ£è´§å¯¹è±¡</h3><p>{{preview.itemName}}</p><p style="overflow-wrap:anywhere">{{preview.scanContent}}</p><p>æ¹æ¬¡ {{preview.batchCode}} · 坿£ {{preview.scanQuantity}} è¢</p><PickingBags v-if="preview.scanType !== 'BAG'" v-model="selectedBagIds" :bags="preview.bags || []" :disabled="busy" /><p v-else>è¢ç {{preview.bags?.[0]?.code}}</p><p role="status">æ¬æ¬¡æ£è´§ {{selectedBagIds.length}} è¢</p><button class="m-primary" :disabled="busy || !selectedBagIds.length" @click="execute('scan')">确认å å
¥æ£è´§</button><button class="m-detail-toggle" :disabled="busy" @click="preview = undefined">åæ¶æ¬æ¬¡æ«ç </button></section></template> |
| | | <template v-if="picking"><h2 class="m-section-title">æ«ç æ£è´§</h2><label class="m-field"><span>æ£è´§æ«ç ç±»å</span><select v-model="scanType" :disabled="busy" @change="preview = undefined; selectedBagIds = []"><option value="BAG">è¢ç </option><option value="PALLET">æçç </option><option value="BATCH">çäº§æ¹æ¬¡ç </option></select></label><p class="m-muted">æ¯æè¿½æº¯ç ãæçãæ¹æ¬¡è¯å«åéæ©å®é
æ£è´§è¢ç ï¼æ°éææéè¢æ°è®¡å
¥ã</p><Scanner voice-operation="outbound" duplicate-voice :disabled="busy" @code="identify" /><section v-if="preview" class="m-panel"><h3 class="m-title">æ ¸å¯¹æ£è´§å¯¹è±¡</h3><p>{{preview.itemName}}</p><p style="overflow-wrap:anywhere">{{preview.scanContent}}</p><p>æ¹æ¬¡ {{preview.batchCode}} · 坿£ {{preview.scanQuantity}} è¢</p><PickingBags v-if="preview.scanType !== 'BAG'" v-model="selectedBagIds" :bags="preview.bags || []" :disabled="busy" /><p v-else>è¢ç {{preview.bags?.[0]?.code}}</p><p role="status">æ¬æ¬¡æ£è´§ {{selectedBagIds.length}} è¢</p><button class="m-primary" :disabled="busy || !selectedBagIds.length" @click="execute('scan')">确认å å
¥æ£è´§</button><button class="m-detail-toggle" :disabled="busy" @click="preview = undefined">åæ¶æ¬æ¬¡æ«ç </button></section></template> |
| | | <form v-if="sales.status === 10 && can('mes:wm-product-sales:shipping')" class="m-panel" @submit.prevent="execute('shipping')"><h2 class="m-title">å¡«åè¿å</h2><label class="m-field"><span>æ¿è¿å</span><input v-model="carrier" :disabled="busy" maxlength="100" required /></label><label class="m-field"><span>è¿è¾åå·</span><input v-model="shippingNumber" :disabled="busy" maxlength="100" required /></label><button class="m-primary" :disabled="busy">ä¿åè¿å</button></form> |
| | | <div v-if="!preview" class="m-action-bar"><button v-if="picking" class="m-primary" :disabled="busy || !lines.length" @click="execute('stock')">宿æ£è´§å¹¶æ ¸å¯¹æ°é</button><button v-if="sales.status === 3 && can('mes:wm-product-sales:finish')" class="m-primary" :disabled="busy" @click="execute('finish')">确认æ§è¡åºåº</button></div> |
| | | <button v-if="picking && shipments.some(s => s.status === 30) && can('mes:wm-product-sales:cancel')" class="m-secondary" :disabled="busy || !!preview" @click="execute('close')">å
³éå©ä½æ°é</button> |
| | | <button v-if="(sales.status === 10 || sales.status === 3) && can('mes:wm-product-sales:cancel')" class="m-secondary" :disabled="busy" @click="execute('cancelShipment')">åæ¶æ¬æ¬¡åè´§</button> |
| | | <p v-if="sales.status === 8" class="m-warning">å©ä½æ°éå·²å
³éï¼{{sales.closeReason || 'æªè®°å½åå '}}</p> |
| | | <p v-if="sales.status === 0 || sales.status === 1" class="m-warning">{{sales.status === 0 ? '该å为è稿ï¼è¯·å¨ PC 端维æ¤åç§å¹¶æäº¤åè¿è¡ç§»å¨æ£è´§ã' : '该åå¾
æ£æµï¼æ£æµå®æåæè½æ£è´§ã'}}</p> |
| | | <p v-if="sales.status === 4" class="m-success">该å已宿åºåºã</p> |
| | | </template> |
| | |
| | | <template v-else-if="!pending"> |
| | | <form class="m-panel" @submit.prevent="load()"><label class="m-field"><span>åºåºåå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥åºåºåå·" /></label><label class="m-field"><span>åæ®ç¶æ</span><select v-model="status" :disabled="busy"><option value="">å
¨é¨ç¶æ</option><option v-for="(name, value) in statusNames" :key="value" :value="String(value)">{{name}}</option></select></label><button class="m-secondary" :disabled="busy">{{busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢åºåºå'}}</button></form> |
| | | <p class="m-muted" role="status">å
± {{total}} å¼ åºåºå</p><p v-if="!rows.length && !busy && !error" class="m-empty">ææ å¹é
çåºåºå</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"><strong>{{row.code}}</strong><span>{{statusNames[row.status!] || 'æªç¥ç¶æ'}}</span><small>{{row.clientName || 'æªå½å
¥å®¢æ·'}}</small></button> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"><strong>{{row.code}}</strong><span>{{statusLabel(row)}}</span><small>{{row.clientName || 'æªå½å
¥å®¢æ·'}}</small></button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤'}}</button> |
| | | </template> |
| | | </template> |
| | |
| | | <script setup lang="ts"> |
| | | import { nextTick, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; |
| | | import Icon from '#/mobile/icon.vue'; |
| | | const props=defineProps<{disabled?:boolean;continuous?:boolean}>(); |
| | | import { speakDuplicateScan, type ScanVoiceOperation } from '#/mobile/scanVoice'; |
| | | const props=defineProps<{disabled?:boolean;continuous?:boolean;duplicateVoice?:boolean;voiceOperation?:ScanVoiceOperation}>(); |
| | | const emit=defineEmits<{code:[value:string]}>(); |
| | | const input=ref(''),error=ref(''),active=ref(false),starting=ref(false),video=ref<HTMLVideoElement>(),codeInput=ref<HTMLInputElement>(); |
| | | let stream:MediaStream|undefined,frame=0,generation=0,last=0; |
| | | async function testVoice(){error.value='';if(!await speakDuplicateScan(props.voiceOperation))error.value='è¯é³ææ¾å¤±è´¥ï¼è¯·æ£æ¥åªä½é³éãæµè§å¨å£°é³æéåç½ç»ã';} |
| | | function stop(){generation++;active.value=false;starting.value=false;cancelAnimationFrame(frame);stream?.getTracks().forEach(t=>t.stop());stream=undefined;if(video.value)video.value.srcObject=null;} |
| | | function accept(value:string){if(props.disabled)return;stop();input.value=props.continuous?'':value;emit('code',value);} |
| | | watch([()=>props.disabled,()=>props.continuous],async ([disabled,continuous])=>{if(!disabled&&continuous){await nextTick();codeInput.value?.focus();}}); |
| | |
| | | <div class="m-panel"> |
| | | <button v-if="!active" type="button" class="m-secondary" :disabled="disabled||starting" @click="start"><Icon name="scan"/>{{starting?'æ£å¨è¯·æ±ç¸æºæéâ¦':'æå¼æå头æ«ç '}}</button> |
| | | <template v-else><video ref="video" class="m-camera" muted playsinline aria-label="äºç»´ç 忝"/><button type="button" class="m-secondary" @click="stop">å
³éæå头</button></template> |
| | | <button v-if="duplicateVoice" type="button" class="m-secondary" @click="testVoice">æµè¯é夿«ç è¯é³</button> |
| | | <p v-if="duplicateVoice" class="m-muted">馿¬¡ä½¿ç¨è¯·ç¹å»æµè¯å¹¶è°é«åªä½é³éã</p> |
| | | <p v-if="error" class="m-error" role="alert">{{error}}</p> |
| | | <form @submit.prevent="accept(input)"> |
| | | <label class="m-field" style="margin-top:16px"><span>ç å¼ / 溯æºé¾æ¥</span><input ref="codeInput" v-model="input" autocomplete="off" :disabled="disabled" placeholder="æå¨è¾å
¥æä½¿ç¨æ«ç 设å¤" maxlength="2048" /></label> |
| | |
| | | import { onMounted,ref } from 'vue'; |
| | | import { requestClient } from '#/api/request'; |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import { getMaterialStockBagDetails, type MaterialStockBagDetail } from '#/api/mes/wm/materialstock'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import { getWarehouseSimpleList, type MesWmWarehouseApi } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseLocationSimpleList, type MesWmWarehouseLocationApi } from '#/api/mes/wm/warehouse/location'; |
| | | import { getWarehouseAreaSimpleList, type MesWmWarehouseAreaApi } from '#/api/mes/wm/warehouse/area'; |
| | | import Picker from './picker.vue'; |
| | | defineOptions({name:'MobileStock'}); |
| | | const positionLoading=ref(false); |
| | | let positionRequest=0; |
| | | const warehouses=ref<MesWmWarehouseApi.Warehouse[]>([]),locations=ref<MesWmWarehouseLocationApi.WarehouseLocation[]>([]),areas=ref<MesWmWarehouseAreaApi.WarehouseArea[]>([]); |
| | | const warehouseId=ref<number>(),locationId=ref<number>(),areaId=ref<number>(); |
| | | async function changeWarehouse(){ |
| | | const ticket=++positionRequest,id=warehouseId.value;locationId.value=undefined;areaId.value=undefined;locations.value=[];areas.value=[];positionLoading.value=!!id; |
| | | try{if(id){const list=await getWarehouseLocationSimpleList(id);if(ticket===positionRequest)locations.value=list;}} |
| | | catch(e){if(ticket===positionRequest)error.value=errorText(e);}finally{if(ticket===positionRequest)positionLoading.value=false;} |
| | | } |
| | | async function changeLocation(){ |
| | | const ticket=++positionRequest,id=locationId.value;areaId.value=undefined;areas.value=[];positionLoading.value=!!id; |
| | | try{if(id){const list=await getWarehouseAreaSimpleList(id);if(ticket===positionRequest)areas.value=list;}} |
| | | catch(e){if(ticket===positionRequest)error.value=errorText(e);}finally{if(ticket===positionRequest)positionLoading.value=false;} |
| | | } |
| | | const keyword=ref(''),batch=ref(''),page=ref(1),busy=ref(false),error=ref(''),total=ref(0),rows=ref<MesWmMaterialStockApi.MaterialStock[]>([]),expanded=ref<number>(); |
| | | type Filters={itemName?:string;batchCode?:string;warehouseId?:number;locationId?:number;areaId?:number}; |
| | | const warehouses=ref<MesWmWarehouseApi.Warehouse[]>([]); |
| | | const warehouseId=ref<number>(); |
| | | const keyword=ref(''),batch=ref(''),includeZero=ref(false),page=ref(1),busy=ref(false),error=ref(''),total=ref(0),rows=ref<MesWmMaterialStockApi.MaterialStock[]>([]),expanded=ref<number>(); |
| | | type Filters={itemName?:string;batchCode?:string;warehouseId?:number;includeZero?:boolean}; |
| | | let applied:Filters={}; |
| | | const bags=ref<MaterialStockBagDetail[]>([]), bagTotal=ref(0), bagPage=ref(0), bagBusy=ref(false), bagStockId=ref<number>(); |
| | | async function loadBags(id:number, reset=false){ |
| | | if(bagBusy.value)return; |
| | | bagBusy.value=true; error.value=''; |
| | | try{ |
| | | const next=reset?1:bagPage.value+1; |
| | | const result=await getMaterialStockBagDetails({materialStockId:id,includeHistory:true,pageNo:next,pageSize:20}); |
| | | bags.value=reset?result.list:[...bags.value,...result.list]; bagTotal.value=result.total; bagPage.value=next; bagStockId.value=id; |
| | | }catch(e){error.value=errorText(e);}finally{bagBusy.value=false;} |
| | | } |
| | | async function load(reset=false){ |
| | | if(busy.value)return;busy.value=true;error.value='';const nextPage=reset?1:page.value+1; |
| | | const filters=reset?{itemName:keyword.value.trim()||undefined,batchCode:batch.value.trim()||undefined,warehouseId:warehouseId.value,locationId:locationId.value,areaId:areaId.value}:applied; |
| | | const filters=reset?{itemName:keyword.value.trim()||undefined,batchCode:batch.value.trim()||undefined,warehouseId:warehouseId.value,includeZero:includeZero.value}:applied; |
| | | try{ |
| | | const r=await requestClient.get<{list:MesWmMaterialStockApi.MaterialStock[];total:number}>('/mes/wm/material-stock/page',{params:{pageNo:nextPage,pageSize:20,...filters}}); |
| | | rows.value=reset?r.list:[...rows.value,...r.list];total.value=r.total;page.value=nextPage; |
| | |
| | | <form class="m-panel" @submit.prevent="load(true)"> |
| | | <label class="m-field"><span>åç§åç§°</span><input v-model="keyword" placeholder="æç´¢åç§"/></label> |
| | | <label class="m-field"><span>æ¹æ¬¡å·</span><input v-model="batch" placeholder="å¯éï¼è¾å
¥æ¹æ¬¡å·"/></label> |
| | | <details><summary class="m-link">æä»åº / åºä½çé</summary> |
| | | <Picker v-model="warehouseId" label="ä»åº" :options="warehouses" :disabled="busy" allow-empty placeholder="å
¨é¨ä»åº" @update:model-value="changeWarehouse"/> |
| | | <Picker v-model="locationId" label="åºåº" :options="locations" :disabled="busy||!warehouseId" :loading="positionLoading" allow-empty placeholder="å
¨é¨åºåº" @update:model-value="changeLocation"/> |
| | | <Picker v-model="areaId" label="åºä½" :options="areas" :disabled="busy||!locationId" :loading="positionLoading" allow-empty placeholder="å
¨é¨åºä½"/> |
| | | <label><input v-model="includeZero" type="checkbox" :disabled="busy" /> å
å«é¶åºåè®°å½</label> |
| | | <details><summary class="m-link">æä»åºçé</summary> |
| | | <Picker v-model="warehouseId" label="ä»åº" :options="warehouses" :disabled="busy" allow-empty placeholder="å
¨é¨ä»åº"/> |
| | | </details> |
| | | <button class="m-primary" :disabled="busy">{{busy?'æ£å¨æ¥è¯¢â¦':'æ¥è¯¢åºå'}}</button> |
| | | </form> |
| | |
| | | <p v-if="!rows.length&&!busy" class="m-empty">ææ å¹é
åºå</p> |
| | | <article v-for="row in rows" :key="row.id" class="m-panel"> |
| | | <h2 class="m-title">{{row.itemName}}</h2><p><strong>{{row.availableQuantity??row.quantity??0}}</strong> {{row.unitMeasureName}} <span class="m-muted">{{row.availableQuantity!=null?'å¯ç¨':'å¨åº'}}</span></p> |
| | | <p class="m-muted">{{row.warehouseName}} / {{row.locationName}} / {{row.areaName}}</p><p class="m-muted">æ¹æ¬¡ï¼{{row.batchCode||'æªå½å
¥'}}</p> |
| | | <p class="m-muted">{{row.warehouseName}}</p><p class="m-muted">æ¹æ¬¡ï¼{{row.batchCode||'æªå½å
¥'}}</p> |
| | | <button class="m-detail-toggle" :aria-expanded="expanded===row.id" @click="expanded=expanded===row.id?undefined:row.id"> {{expanded===row.id?'æ¶èµ·æç»':'æ¥çæ°éæç»'}}</button> |
| | | <dl v-if="expanded===row.id"><div class="m-row"><dt>å¨åºæ°é</dt><dd>{{row.quantity}} {{row.unitMeasureName}}</dd></div><div class="m-row"><dt>å»ç»æ°é</dt><dd>{{row.frozenQuantity??0}}</dd></div><div class="m-row"><dt>å ç¨æ°é</dt><dd>{{row.reservedQuantity??0}}</dd></div><div class="m-row"><dt>å
¥åºæ¶é´</dt><dd>{{row.receiptTime||'æªå½å
¥'}}</dd></div></dl> |
| | | <template v-if="expanded===row.id && row.bagDetailSupported"> |
| | | <button class="m-secondary" :disabled="bagBusy" @click="loadBags(row.id!,true)">æ¥çè¢ç ç¶æ</button> |
| | | <template v-if="bagStockId===row.id"> |
| | | <p v-if="!bags.length" class="m-muted">ææ è¢ç æç»</p> |
| | | <div v-for="bag in bags" :key="bag.id" class="m-record"><p style="overflow-wrap:anywhere">{{bag.bagCode}}</p><p>{{bag.status===1?(bag.pickingStatus==='RESERVED'?'å·²å ç¨':bag.pickingStatus==='PICKED'?'å·²æ£é':'å¨åº'):bag.status===2?(bag.outboundBizType===119?'éå®å·²åºåº':'å·²åºåº'):bag.status===3?'å·²æ¥åº':'æªç¥ç¶æ'}}<span v-if="bag.outboundTime"> · {{bag.outboundTime}}</span></p></div> |
| | | <button v-if="bags.length<bagTotal" class="m-secondary" :disabled="bagBusy" @click="loadBags(row.id!)">å è½½æ´å¤è¢ç </button> |
| | | </template></template> |
| | | </article> |
| | | <button v-if="rows.length<total" class="m-secondary" :disabled="busy" @click="load()">å è½½æ´å¤</button> |
| | | </template> |
| | |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>è´¦é¢æ°é {{ line.quantity ?? 0 }} <span class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</span></p> |
| | | <p class="m-muted">{{ [line.warehouseName, line.locationName, line.areaName].filter(Boolean).join(' / ') || 'â' }}</p> |
| | | <p class="m-muted">{{ line.warehouseName || 'â' }}</p> |
| | | <p v-if="line.takingQuantity != null" class="m-muted">å·²ç {{ line.takingQuantity }}ï¼å·®å¼ {{ line.differenceQuantity ?? 0 }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç çç¹</h2> |
| | |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || matchedLine?.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ trace.data.batchCode || matchedLine?.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>åºä½</dt><dd>{{ [matchedLine?.warehouseName, matchedLine?.locationName, matchedLine?.areaName].filter(Boolean).join(' / ') || 'ä¸å¨çç¹èå´' }}</dd></div> |
| | | <div class="m-row"><dt>ä»åº</dt><dd>{{ matchedLine?.warehouseName || 'ä¸å¨çç¹èå´' }}</dd></div> |
| | | <div class="m-row"><dt>è´¦é¢æ°é</dt><dd>{{ matchedLine?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | |
| | | quantity: quantity.value, |
| | | batchId: matched.batchId, |
| | | fromWarehouseId: matched.warehouseId, |
| | | fromLocationId: matched.locationId, |
| | | fromAreaId: matched.areaId, |
| | | remark: `æ«ç æ¥æº ${trace.value?.code ?? ''}`, |
| | | }); |
| | | notice.value = '已添å ç§»åºæç»ã'; |
| | |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ stock?.batchCode || trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>ç§»åºä½ç½®</dt><dd>{{ [stock?.warehouseName, stock?.locationName, stock?.areaName].filter(Boolean).join(' / ') || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç§»åºä»åº</dt><dd>{{ stock?.warehouseName || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>åºåæ°é</dt><dd>{{ stock?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getSystemAppearance, |
| | | saveSystemAppearance, |
| | | uploadAppearanceImage, |
| | | } from '#/api/system/appearance'; |
| | | import { applyAppearance } from '#/utils/system-appearance'; |
| | | |
| | | defineOptions({ name: 'SystemAppearance' }); |
| | | |
| | | interface ImageItem { |
| | | blobId: null | number; |
| | | url: string; |
| | | } |
| | | |
| | | const logo = reactive<ImageItem>({ blobId: null, url: '' }); |
| | | const loginBackground = reactive<ImageItem>({ blobId: null, url: '' }); |
| | | const saving = ref(false); |
| | | |
| | | async function loadAppearance() { |
| | | const data = await getSystemAppearance(); |
| | | logo.blobId = data?.logoBlobId ?? null; |
| | | logo.url = data?.logoUrl ?? ''; |
| | | loginBackground.blobId = data?.loginBackgroundBlobId ?? null; |
| | | loginBackground.url = data?.loginBackgroundUrl ?? ''; |
| | | } |
| | | |
| | | async function handleUpload(file: File, target: ImageItem) { |
| | | if (!file.type.startsWith('image/')) { |
| | | message.error('请ä¸ä¼ å¾çæä»¶'); |
| | | return false; |
| | | } |
| | | const hideLoading = message.loading({ content: 'ä¸ä¼ ä¸...', duration: 0 }); |
| | | try { |
| | | const blobs = await uploadAppearanceImage(file); |
| | | const blob = blobs?.[0]; |
| | | if (!blob) { |
| | | message.error('ä¸ä¼ 失败'); |
| | | return false; |
| | | } |
| | | target.blobId = blob.id; |
| | | target.url = blob.previewURL || blob.downloadURL || ''; |
| | | message.success('ä¸ä¼ æåï¼ç¹å»ä¿ååçæ'); |
| | | } catch { |
| | | message.error('ä¸ä¼ 失败'); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | function beforeLogoUpload(file: File) { |
| | | return handleUpload(file, logo); |
| | | } |
| | | |
| | | function beforeBackgroundUpload(file: File) { |
| | | return handleUpload(file, loginBackground); |
| | | } |
| | | |
| | | function handleClear(target: ImageItem) { |
| | | target.blobId = null; |
| | | target.url = ''; |
| | | } |
| | | |
| | | async function handleSave() { |
| | | saving.value = true; |
| | | try { |
| | | await saveSystemAppearance({ |
| | | logoBlobId: logo.blobId, |
| | | loginBackgroundBlobId: loginBackground.blobId, |
| | | }); |
| | | applyAppearance({ |
| | | logoUrl: logo.url || undefined, |
| | | loginBackgroundUrl: loginBackground.url || undefined, |
| | | }); |
| | | message.success('ä¿åæåï¼å¤è§å·²çæ'); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(loadAppearance); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="appearance-page"> |
| | | <a-card title="ç»å½é¡µèæ¯å¾" class="appearance-card"> |
| | | <div class="appearance-preview appearance-preview--background"> |
| | | <img v-if="loginBackground.url" :src="loginBackground.url" alt="ç»å½é¡µèæ¯å¾" /> |
| | | <div v-else class="appearance-empty">æªè®¾ç½®ï¼ä½¿ç¨ç³»ç»é»è®¤èæ¯</div> |
| | | </div> |
| | | <div class="appearance-actions"> |
| | | <a-upload |
| | | accept="image/*" |
| | | :before-upload="beforeBackgroundUpload" |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>ä¸ä¼ èæ¯å¾</a-button> |
| | | </a-upload> |
| | | <a-button |
| | | v-if="loginBackground.url" |
| | | danger |
| | | @click="handleClear(loginBackground)" |
| | | > |
| | | æ¸
é¤ |
| | | </a-button> |
| | | <span class="appearance-tip">æ¯æ jpgãpngãwebp ç常è§å¾çæ ¼å¼</span> |
| | | </div> |
| | | </a-card> |
| | | |
| | | <a-card title="ä¼ä¸ Logo" class="appearance-card"> |
| | | <div class="appearance-preview appearance-preview--logo"> |
| | | <img v-if="logo.url" :src="logo.url" alt="ä¼ä¸ Logo" /> |
| | | <div v-else class="appearance-empty">æªè®¾ç½®ï¼ä½¿ç¨ç³»ç»é»è®¤ Logo</div> |
| | | </div> |
| | | <div class="appearance-actions"> |
| | | <a-upload |
| | | accept="image/*" |
| | | :before-upload="beforeLogoUpload" |
| | | :show-upload-list="false" |
| | | > |
| | | <a-button>ä¸ä¼ Logo</a-button> |
| | | </a-upload> |
| | | <a-button v-if="logo.url" danger @click="handleClear(logo)"> |
| | | æ¸
é¤ |
| | | </a-button> |
| | | <span class="appearance-tip">Logo åæ¶ç¨äºç»å½é¡µä¸é¦é¡µä¾§è¾¹æ </span> |
| | | </div> |
| | | </a-card> |
| | | |
| | | <div class="appearance-footer"> |
| | | <a-button type="primary" :loading="saving" @click="handleSave"> |
| | | ä¿å |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .appearance-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | padding: 16px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .appearance-card { |
| | | max-width: 720px; |
| | | } |
| | | |
| | | .appearance-preview { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | overflow: hidden; |
| | | border: 1px dashed rgb(203 213 225); |
| | | border-radius: 8px; |
| | | background: rgb(248 250 252); |
| | | } |
| | | |
| | | .appearance-preview--background { |
| | | height: 220px; |
| | | } |
| | | |
| | | .appearance-preview--logo { |
| | | height: 140px; |
| | | } |
| | | |
| | | .appearance-preview img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .appearance-preview--logo img { |
| | | object-fit: contain; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .appearance-empty { |
| | | color: rgb(148 163 184); |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .appearance-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .appearance-tip { |
| | | color: rgb(148 163 184); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .appearance-footer { |
| | | max-width: 720px; |
| | | } |
| | | </style> |
| | |
| | | |
| | | import { WmMaterialStockSelect } from '../materialstock/components'; |
| | | import { WmPackageSelect } from '#/views/wls/packages/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '../warehouse/components'; |
| | | import { WmWarehouseSelect } from '../warehouse/components'; |
| | | |
| | | /** ä¸å¡å¯¹è±¡éä¸ååå¡«ä¸å¡ç¼ç ãä¸å¡åç§°ãæ¡ç å
容 */ |
| | | async function syncBizDetail(formApi: undefined | VbenFormApi, item: any) { |
| | |
| | | bizCode: undefined, |
| | | bizName: undefined, |
| | | content: undefined, |
| | | locationWarehouseId: undefined, |
| | | areaWarehouseId: undefined, |
| | | areaLocationId: undefined, |
| | | }), |
| | | options: getDictOptions(DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE, 'number'), |
| | | // ä¸åæ¯ææåºåº/åºä½çææ¡ç |
| | | options: getDictOptions( |
| | | DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE, |
| | | 'number', |
| | | ).filter( |
| | | (item) => |
| | | item.value !== BarcodeBizTypeEnum.LOCATION && |
| | | item.value !== BarcodeBizTypeEnum.AREA, |
| | | ), |
| | | placeholder: 'è¯·éæ©ä¸å¡ç±»å', |
| | | }, |
| | | rules: 'required', |
| | |
| | | dependencies: { |
| | | triggerFields: ['bizType'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.WAREHOUSE, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'locationWarehouseId', |
| | | label: 'åºåºÂ·ä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | onChange: () => |
| | | formApi?.setValues({ |
| | | bizId: undefined, |
| | | bizCode: undefined, |
| | | bizName: undefined, |
| | | content: undefined, |
| | | }), |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['bizType'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.LOCATION, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'bizId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['bizType', 'locationWarehouseId'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.LOCATION, |
| | | componentProps: (values) => ({ |
| | | onChange: (item: any) => syncBizDetail(formApi, item), |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.locationWarehouseId, |
| | | }), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'areaWarehouseId', |
| | | label: 'åºä½Â·ä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | onChange: () => |
| | | formApi?.setValues({ |
| | | areaLocationId: undefined, |
| | | bizId: undefined, |
| | | bizCode: undefined, |
| | | bizName: undefined, |
| | | content: undefined, |
| | | }), |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['bizType'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.AREA, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaLocationId', |
| | | label: 'åºä½Â·åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['bizType', 'areaWarehouseId'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.AREA, |
| | | componentProps: (values) => ({ |
| | | onChange: () => |
| | | formApi?.setValues({ |
| | | bizId: undefined, |
| | | bizCode: undefined, |
| | | bizName: undefined, |
| | | content: undefined, |
| | | }), |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.areaWarehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'bizId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | dependencies: { |
| | | triggerFields: ['bizType', 'areaLocationId'], |
| | | show: (values) => values.bizType === BarcodeBizTypeEnum.AREA, |
| | | componentProps: (values) => ({ |
| | | locationId: values.areaLocationId, |
| | | onChange: (item: any) => syncBizDetail(formApi, item), |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum, CommonStatusEnum } from '@vben/constants'; |
| | | import { CommonStatusEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createBarcode, getBarcode, updateBarcode } from '#/api/mes/wm/barcode'; |
| | | import { getWarehouseArea } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** å 载级èéæ©å¨çæ°æ®ï¼ç¼è¾æ¶åå¡«ä¸çº§ä»åº/åºåºï¼ */ |
| | | async function loadCascadeData(data: MesWmBarcodeApi.Barcode) { |
| | | if (!data.bizType || !data.bizId) { |
| | | return; |
| | | } |
| | | if (data.bizType === BarcodeBizTypeEnum.LOCATION) { |
| | | const location = await getWarehouseLocation(data.bizId); |
| | | if (location?.warehouseId) { |
| | | await formApi.setFieldValue('locationWarehouseId', location.warehouseId); |
| | | } |
| | | } else if (data.bizType === BarcodeBizTypeEnum.AREA) { |
| | | const area = await getWarehouseArea(data.bizId); |
| | | await formApi.setValues({ |
| | | areaWarehouseId: area?.warehouseId, |
| | | areaLocationId: area?.locationId, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | |
| | | formData.value = await getBarcode(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await loadCascadeData(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | WmArrivalNoticeLineSelect, |
| | | WmArrivalNoticeSelect, |
| | | } from '#/views/wls/arrivalnotice/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'å
¥åºä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // 忢ä»åºåæ¸
空åºåºååºä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | // 忢åºåºåæ¸
空åºä½ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | |
| | | import { z } from '#/adapter/form'; |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { moveMaterialStock } from '#/api/mes/wm/materialstock'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | defineOptions({ name: 'WmStockMoveModal' }); |
| | | |
| | |
| | | componentProps: { disabled: true }, |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | label: 'å½ååºåº', |
| | | component: 'Input', |
| | | componentProps: { disabled: true }, |
| | | }, |
| | | { |
| | | fieldName: 'areaName', |
| | | label: 'å½ååºä½', |
| | | component: 'Input', |
| | | componentProps: { disabled: true }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | | label: 'å½åæ°é', |
| | | component: 'InputNumber', |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç®æ ä»åº', |
| | | onChange: async () => { |
| | | await formApi.setFieldValue('toLocationId', undefined); |
| | | await formApi.setFieldValue('toAreaId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'toLocationId', |
| | | label: 'ç®æ åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { placeholder: 'è¯·éæ©ç®æ åºåº' }, |
| | | dependencies: { |
| | | triggerFields: ['toWarehouseId'], |
| | | componentProps: (values) => ({ |
| | | warehouseId: values.toWarehouseId, |
| | | placeholder: 'è¯·éæ©ç®æ åºåº', |
| | | onChange: async () => { |
| | | await formApi.setFieldValue('toAreaId', undefined); |
| | | }, |
| | | }), |
| | | trigger: (values, api) => { |
| | | if (values.toLocationId !== undefined) { |
| | | void api.setFieldValue('toLocationId', undefined); |
| | | void api.setFieldValue('toAreaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'toAreaId', |
| | | label: 'ç®æ åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { placeholder: 'è¯·éæ©ç®æ åºä½' }, |
| | | dependencies: { |
| | | triggerFields: ['toLocationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.toLocationId, |
| | | placeholder: 'è¯·éæ©ç®æ åºä½', |
| | | }), |
| | | trigger: (values, api) => { |
| | | if (values.toAreaId !== undefined) { |
| | | void api.setFieldValue('toAreaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | return; |
| | | } |
| | | |
| | | if (row.warehouseId === values.toWarehouseId && row.locationId === values.toLocationId && row.areaId === values.toAreaId) { |
| | | message.error('ç®æ åºä½ä¸å½ååºä½ç¸åï¼ä¸å
许移åº'); |
| | | if (row.warehouseId === values.toWarehouseId) { |
| | | message.error('ç®æ ä»åºä¸å½åä»åºç¸åï¼ä¸å
许移åº'); |
| | | return; |
| | | } |
| | | |
| | |
| | | materialStockId: row.id, |
| | | quantity: moveQuantity, |
| | | remark: values.remark, |
| | | toAreaId: values.toAreaId, |
| | | toLocationId: values.toLocationId, |
| | | toWarehouseId: values.toWarehouseId, |
| | | }); |
| | | message.success('ç§»åºæå'); |
| | |
| | | } |
| | | |
| | | await formApi.setValues({ |
| | | areaName: data.areaName, |
| | | batchCode: data.batchCode, |
| | | itemCode: data.itemCode, |
| | | itemName: data.itemName, |
| | | locationName: data.locationName, |
| | | quantity: data.quantity, |
| | | warehouseName: data.warehouseName, |
| | | }); |
| | |
| | | <div>æ¹æ¬¡ï¼{{ selectedItem.batchCode || '-' }}</div> |
| | | <div>æ°éï¼{{ selectedItem.quantity ?? '-' }}</div> |
| | | <div>ä»åºï¼{{ selectedItem.warehouseName || '-' }}</div> |
| | | <div>åºåºï¼{{ selectedItem.locationName || '-' }}</div> |
| | | <div>åºä½ï¼{{ selectedItem.areaName || '-' }}</div> |
| | | </div> |
| | | </template> |
| | | <Input |
| | |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import { ErpSupplierSelect } from '#/views/erp/purchase/supplier/components'; |
| | | import BatchConfigDisplay from './batch-config-display.vue'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | const emit = defineEmits<{ |
| | | success: []; |
| | |
| | | // æä½ç±»åï¼1=å
¥åºï¼2=åºåº |
| | | const operateType = ref<1 | 2>(1); |
| | | const selectedItem = ref<MdmItemApi.Item | null>(null); |
| | | const selectedWarehouse = ref<any>(null); |
| | | const selectedLocation = ref<any>(null); |
| | | |
| | | // æ¯å¦å¯ç¨æ¹æ¬¡ç®¡ç |
| | | const batchEnabled = ref(false); |
| | |
| | | await formApi.setFieldValue('manualBatchEntry', false); |
| | | } |
| | | |
| | | /** ä»åºéæ©åå */ |
| | | async function handleWarehouseChange(item: any) { |
| | | selectedWarehouse.value = item; |
| | | await formApi.setFieldValue('locationId', undefined); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | |
| | | /** åºåºéæ©åå */ |
| | | async function handleLocationChange(item: any) { |
| | | selectedLocation.value = item; |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | |
| | | /** æå¨å¡«åæ¹æ¬¡å·åå */ |
| | | async function handleManualBatchEntryChange(value: boolean) { |
| | | manualBatchEntry.value = value; |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | onChange: handleWarehouseChange, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: selectedWarehouse.value?.id, |
| | | onChange: handleLocationChange, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values: any) => ({ |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | onChange: handleLocationChange, |
| | | }), |
| | | trigger: (values: any, formApi: any) => { |
| | | if (values.locationId !== undefined) { |
| | | void formApi.setFieldValue('locationId', undefined); |
| | | void formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | locationId: selectedLocation.value?.id, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values: any) => ({ |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | locationId: values.locationId, |
| | | }), |
| | | trigger: (values: any, formApi: any) => { |
| | | if (values.areaId !== undefined) { |
| | | void formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | onOpenChange: async (isOpen: boolean) => { |
| | | if (!isOpen) { |
| | | selectedItem.value = null; |
| | | selectedWarehouse.value = null; |
| | | selectedLocation.value = null; |
| | | batchEnabled.value = false; |
| | | operateType.value = 1; |
| | | modalTitle.value = 'åºåè°æ´'; |
| | |
| | | await formApi.setValues({ |
| | | itemId: data.itemId, |
| | | warehouseId: data.warehouseId, |
| | | locationId: data.locationId, |
| | | areaId: data.areaId, |
| | | }); |
| | | if (data.itemId) { |
| | | const itemDetail = await getItem(data.itemId); |
| | |
| | | const params: MaterialStockAdjustInParams = { |
| | | itemId: values.itemId, |
| | | warehouseId: values.warehouseId, |
| | | locationId: values.locationId, |
| | | areaId: values.areaId, |
| | | batchCode: values.batchCode, |
| | | produceDate: values.produceDate, |
| | | expireDate: values.expireDate, |
| | |
| | | const params: MaterialStockAdjustOutParams = { |
| | | itemId: values.itemId, |
| | | warehouseId: values.warehouseId, |
| | | locationId: values.locationId, |
| | | areaId: values.areaId, |
| | | batchCode: values.batchCode, |
| | | vendorId: values.vendorId, |
| | | quantity: values.quantity, |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | { field: 'status', title: 'è¢ç ç¶æ', width: 110, |
| | | formatter: ({ row }) => row.status === 1 ? (row.pickingStatus === 'RESERVED' ? 'å·²å ç¨' : row.pickingStatus === 'PICKED' ? 'å·²æ£é' : 'å¨åº') : row.status === 2 ? (row.outboundBizType === 119 ? 'éå®å·²åºåº' : 'å·²åºåº') : row.status === 3 ? 'å·²æ¥åº' : 'æªç¥ç¶æ' }, |
| | | { field: 'quantity', title: 'æ°é', width: 80 }, |
| | | { field: 'outboundTime', title: 'åºåºæ¶é´', width: 180 }, |
| | | { field: 'inboundTime', title: 'å
¥åºæ¶é´', width: 180 }, |
| | | ], |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }) => getMaterialStockBagDetails({ |
| | | materialStockId: stockId.value, |
| | | includeHistory: true, |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | }), |
| | |
| | | |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '../warehouse/components'; |
| | | import { WmWarehouseSelect } from '../warehouse/components'; |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | warehouseId: values.warehouseId, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }), |
| | | // ä»åºåæ¢æ¶æ¸
空åºåºï¼é¿å
æ§åºåºæ¡ä»¶æ®ç |
| | | trigger: (values, formApi) => { |
| | | if (values.locationId !== undefined) { |
| | | void formApi.setFieldValue('locationId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'stockState', |
| | | label: 'åºåç¶æ', |
| | | component: 'Select', |
| | |
| | | options: [ |
| | | { label: 'æååºï¼å¾
è´¨æ£ï¼', value: 10 }, |
| | | { label: 'åæ ¼åºï¼æ£å¼åºåï¼', value: 20 }, |
| | | { label: 'å·²åºåºï¼é宿£åï¼', value: 30 }, |
| | | { label: 'å·²åºåº', value: 30 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©åºåç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'includeZero', |
| | | label: 'é¶åºåè®°å½', |
| | | component: 'Select', |
| | | componentProps: { allowClear: true, placeholder: 'é»è®¤ä¸æ¾ç¤º', options: [{ label: 'å
å«é¶åºå', value: true }] }, |
| | | }, |
| | | { |
| | | fieldName: 'frozen', |
| | |
| | | }, |
| | | { |
| | | field: 'specification', |
| | | title: 'è§æ ¼åå·', |
| | | title: 'è§æ ¼', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemTypeName', |
| | | title: 'ç±»å«', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'unitMeasureName', |
| | | title: 'åä½', |
| | | width: 80, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | minWidth: 140, |
| | |
| | | field: 'warehouseName', |
| | | title: 'ä»åº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 100, |
| | | slots: { default: 'areaName' }, |
| | | }, |
| | | { |
| | | field: 'receiptTime', |
| | |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | warehouseId: values.warehouseId, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }), |
| | | // ä»åºåæ¢æ¶æ¸
空åºåº |
| | | trigger: (values, formApi) => { |
| | | if (values.locationId !== undefined) { |
| | | void formApi.setFieldValue('locationId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId', 'locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | // ä»åºæåºåºåæ¢æ¶æ¸
空åºä½ |
| | | trigger: (values, formApi) => { |
| | | if (values.areaId !== undefined) { |
| | | void formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | }, |
| | | { |
| | | field: 'specification', |
| | | title: 'è§æ ¼åå·', |
| | | title: 'è§æ ¼', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'unitMeasureName', |
| | | title: 'åä½', |
| | | width: 80, |
| | | field: 'itemTypeName', |
| | | title: 'ç±»å«', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'ä»åº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | |
| | | import { $t } from '#/locales'; |
| | | import { MdItemTypeTree } from '#/views/mes/md/item/type/components'; |
| | | import QrTraceModal from '#/views/mes/pro/components/qr-trace-modal.vue'; |
| | | import AreaForm from '#/views/wls/warehouse/area/modules/form.vue'; |
| | | import { |
| | | WmStockAdjustModal, |
| | | WmStockBagModal, |
| | |
| | | } from './components'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | |
| | | const [AreaModal, areaModalApi] = useVbenModal({ |
| | | connectedComponent: AreaForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [StockAdjustModal, stockAdjustModalApi] = useVbenModal({ |
| | | connectedComponent: WmStockAdjustModal, |
| | |
| | | function handleTypeNodeClick(row: undefined | { id?: number }) { |
| | | searchItemTypeId.value = row?.id; |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æå¼åºä½è¯¦æ
å¼¹çª */ |
| | | function handleOpenAreaDetail(row: MesWmMaterialStockApi.MaterialStock) { |
| | | if (!row.areaId) { |
| | | return; |
| | | } |
| | | areaModalApi.setData({ formType: 'detail', id: row.areaId }).open(); |
| | | } |
| | | |
| | | /** æå¼åºåè°æ´å¼¹çª */ |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <AreaModal /> |
| | | <QrTraceQueryModal /> |
| | | <StockBagModal /> |
| | | <StockAdjustModal @success="handleStockAdjustSuccess" /> |
| | |
| | | @click="handleOpenBatchDetail(row)" |
| | | > |
| | | {{ row.batchCode }} |
| | | </Button> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #areaName="{ row }"> |
| | | <Button |
| | | v-if="row.areaId" |
| | | :title="row.areaName" |
| | | size="small" |
| | | type="link" |
| | | @click="handleOpenAreaDetail(row)" |
| | | > |
| | | {{ row.areaName }} |
| | | </Button> |
| | | <span v-else>-</span> |
| | | </template> |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'update'; |
| | |
| | | title: 'ä»åº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 120, |
| | | }, |
| | | ...(editable |
| | | ? [ |
| | | { |
| | |
| | | label: 'åºåç©èµ', |
| | | component: markRaw(WmMaterialStockSelect), |
| | | componentProps: { |
| | | // éæ©åºåç©èµåï¼èªå¨åå¡«ç©æãæ¹æ¬¡å·ãä»åºä½ç½®åæå¤§å¯åºåºæ°é |
| | | // éæ©åºåç©èµåï¼èªå¨åå¡«ç©æãæ¹æ¬¡å·ãä»åºåæå¤§å¯åºåºæ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | areaId: stock?.areaId, |
| | | batchCode: stock?.batchCode, |
| | | itemId: stock?.itemId, |
| | | locationId: stock?.locationId, |
| | | quantityMax: stock?.quantity, |
| | | warehouseId: stock?.warehouseId, |
| | | }); |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | areaId: stock.areaId, |
| | | batchCode: stock.batchCode, |
| | | itemId: stock.itemId, |
| | | locationId: stock.locationId, |
| | | materialStockId: stock.id, |
| | | quantity: |
| | | quantity && stock.quantity |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'update'; |
| | |
| | | title: 'ä»åº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 100, |
| | | }, |
| | | ...(editable |
| | | ? [ |
| | | { |
| | |
| | | label: 'ä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // 忢ä»åºåæ¸
空åºåºååºä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | // 忢åºåºåæ¸
空åºä½ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | import { MdVendorSelect } from '#/views/mes/md/vendor/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'åºåè®°å½', |
| | | component: markRaw(WmMaterialStockSelect), |
| | | componentProps: { |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/åºåº/åºä½/æ¹æ¬¡/æ°é |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/æ¹æ¬¡/æ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | areaId: stock?.areaId, |
| | | batchCode: stock?.batchCode, |
| | | batchId: stock?.batchId, |
| | | locationId: stock?.locationId, |
| | | quantity: stock?.quantity, |
| | | quantityMax: stock?.quantity, |
| | | warehouseId: stock?.warehouseId, |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdVendorSelect } from '#/views/mes/md/vendor/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'å
¥åºä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // 忢ä»åºåæ¸
空åºåºååºä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | // 忢åºåºåæ¸
空åºä½ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'åºåè®°å½', |
| | | component: markRaw(WmMaterialStockSelect), |
| | | componentProps: { |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/åºåº/åºä½/æ¹æ¬¡/æ°é |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/æ¹æ¬¡/æ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | areaId: stock?.areaId, |
| | | batchCode: stock?.batchCode, |
| | | batchId: stock?.batchId, |
| | | locationId: stock?.locationId, |
| | | quantity: stock?.quantity, |
| | | quantityMax: stock?.quantity, |
| | | warehouseId: stock?.warehouseId, |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'å
¥åºä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // 忢ä»åºåæ¸
空åºåºååºä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | // 忢åºåºåæ¸
空åºä½ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | |
| | | WmSalesNoticeLineSelect, |
| | | WmSalesNoticeSelect, |
| | | } from '#/views/wls/salesnotice/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions( |
| | | DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS, |
| | | 'number', |
| | | ), |
| | | options: [...getDictOptions(DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS, 'number'), { label: 'å·²å
³é', value: 8 }], |
| | | placeholder: 'è¯·éæ©åæ®ç¶æ', |
| | | }, |
| | | }, |
| | |
| | | field: 'status', |
| | | title: 'åæ®ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS }, |
| | | }, |
| | | formatter: ({ cellValue, row }) => `${row.partiallyShipped ? 'é¨ååè´§ / ' : ''}${({ 0: 'è稿', 1: 'å¾
æ£æµ', 2: 'å¾
æ£è´§', 3: 'å¾
æ§è¡åºåº', 4: '已宿', 5: '已忶', 8: 'å·²å
³é', 10: 'å¾
å¡«åè¿å' } as Record<number, string>)[cellValue] || 'æªç¥ç¶æ'}`, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | title: 'åºåºæ°é', |
| | | width: 100, |
| | | }, |
| | | { field: 'shippedQuantity', title: 'å·²åºåº', width: 100 }, |
| | | { field: 'remainingQuantity', title: 'å©ä½æ°é', width: 100 }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'shipmentId', |
| | | title: 'åè´§è®°å½', |
| | | minWidth: 120, |
| | | formatter: ({ row }) => row.shipmentId ? `åè´§ #${row.shipmentId}` : 'å¾
åé
/ å岿ç»', |
| | | }, |
| | | { |
| | | field: 'bagCodeId', |
| | | title: 'æ«ç æ¥æº', |
| | | minWidth: 110, |
| | | formatter: ({ row }) => |
| | | row.bagCodeId ? `è¢ç #${row.bagCodeId}${row.palletCodeId ? ` / æç #${row.palletCodeId}` : ''}` : row.palletCodeId ? `æçç #${row.palletCodeId}` : 'æå·¥æ£è´§', |
| | | slots: { default: 'scanSource' }, |
| | | }, |
| | | { |
| | | field: 'warehouseName', |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | | }, |
| | | { field: 'remark', title: 'è¢ç 䏿«ç æ¥æº', minWidth: 240 }, |
| | | { field: 'remark', title: 'è¢ç 䏿«ç æ¥æº', minWidth: 240, slots: { default: 'pickingRemark' } }, |
| | | ...(stockable |
| | | ? [ |
| | | { |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** æ«ç æ¥æºå±ç¤ºææ¡ */ |
| | | export function formatScanSource( |
| | | row: MesWmProductSalesDetailApi.ProductSalesDetail, |
| | | ) { |
| | | if (row.bagCodeId) { |
| | | return `è¢ç #${row.bagCodeId}${row.palletCodeId ? ` / æç #${row.palletCodeId}` : ''}`; |
| | | } |
| | | return row.palletCodeId ? `æçç #${row.palletCodeId}` : 'æå·¥æ£è´§'; |
| | | } |
| | | |
| | | export function formatPickingRemark(value?: string) { |
| | | const names: Record<string, string> = { BAG: 'è¢ç ', PALLET: 'æçç ', BATCH: 'æ¹æ¬¡ç ' }; |
| | | return (value || '') |
| | | .replace(/(æ«ç ç±»å\s+)(BAG|PALLET|BATCH)(\s*[ï¼;]\s*)æ¥æºç (?=\s)/g, |
| | | (_match, prefix: string, type: string, separator: string) => `${prefix}${type}${separator}æ¥æº${names[type]}`) |
| | | .replace(/æ«ç ç±»å\s+(BAG|PALLET|BATCH)(?=\s*[ï¼;]|$)/g, |
| | | (_match, type: string) => `æ«ç ç±»å ${names[type]}`); |
| | | } |
| | | |
| | | /** æ£è´§æç»æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { |
| | | export function useDetailFormSchema( |
| | | formApi?: VbenFormApi, |
| | | onStockChange?: (stock?: MesWmMaterialStockApi.MaterialStock) => void, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'quantityMax', |
| | |
| | | componentProps: { |
| | | // åªä»åæ ¼ä»åºï¼type=20ï¼éæ©å¯æ£è´§åºå |
| | | warehouseType: MesWmWarehouseTypeEnum.QUALIFIED, |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/åºåº/åºä½/æ¹æ¬¡/æ°é |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/æ¹æ¬¡/æ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | areaId: stock?.areaId, |
| | | batchCode: stock?.batchCode, |
| | | batchId: stock?.batchId, |
| | | locationId: stock?.locationId, |
| | | quantity: stock?.quantity, |
| | | quantityMax: stock?.quantity, |
| | | warehouseId: stock?.warehouseId, |
| | | }); |
| | | onStockChange?.(stock); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | cancelProductSales, |
| | | cancelProductSalesShipment, |
| | | closeProductSalesRemaining, |
| | | deleteProductSales, |
| | | exportProductSales, |
| | | getProductSalesPage, |
| | |
| | | await cancelProductSales(row.id!); |
| | | message.success('åæ¶æå'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleCloseRemaining(row: MesWmProductSalesApi.ProductSales) { |
| | | const reason = window.prompt('请è¾å
¥å
³éå©ä½æ°éçåå ï¼æå¤500åï¼'); |
| | | if (reason === null) return; |
| | | if (!reason.trim() || reason.trim().length > 500) { message.warning('请填åä¸è¶
è¿500åçå
³éåå '); return; } |
| | | await closeProductSalesRemaining(row.id!, reason.trim()); |
| | | message.success('å©ä½æ°éå·²å
³é'); handleRefresh(); |
| | | } |
| | | |
| | | async function handleCancelShipment(row: MesWmProductSalesApi.ProductSales) { |
| | | await cancelProductSalesShipment(row.id!); |
| | | message.success('æ¬æ¬¡åè´§å·²åæ¶ï¼åºåå ç¨å·²éæ¾'); handleRefresh(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | |
| | | onClick: handleFinish.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åæ¶æ¬æ¬¡åè´§', |
| | | type: 'link', |
| | | auth: ['mes:wm-product-sales:cancel'], |
| | | ifShow: row.status === MesWmProductSalesStatusEnum.SHIPPING || row.status === MesWmProductSalesStatusEnum.APPROVED, |
| | | popConfirm: { title: 'åæ¶æ¬æ¬¡åè´§å¹¶éæ¾åºåå ç¨ï¼æ£è´§æç»å°ä¿çä¾è°æ´ã', confirm: handleCancelShipment.bind(null, row) }, |
| | | }, |
| | | { |
| | | label: 'å
³éå©ä½', |
| | | type: 'link', |
| | | auth: ['mes:wm-product-sales:cancel'], |
| | | ifShow: row.status === MesWmProductSalesStatusEnum.APPROVING, |
| | | onClick: handleCloseRemaining.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åæ¶', |
| | | type: 'link', |
| | | danger: true, |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useDetailFormSchema } from '../data'; |
| | | import { previewProductSalesScan, scanProductSales, type MesWmProductSalesApi } from '#/api/mes/wm/productsales'; |
| | | import PickingBags from './picking-bags.vue'; |
| | | |
| | | const emit = defineEmits<{ success: [lineId: number] }>(); |
| | | const formData = ref<MesWmProductSalesDetailApi.ProductSalesDetail>(); |
| | | const salesId = ref<number>(); // æå±åºåºåç¼å· |
| | | const lineId = ref<number>(); // æå±åºåºåè¡ç¼å· |
| | | const bagPreview = ref<MesWmProductSalesApi.ProductSalesScanResponse>(); |
| | | const selectedBagIds = ref<number[]>([]); |
| | | const previewStockId = ref<number>(); |
| | | const previewLoading = ref(false); |
| | | |
| | | async function loadBags(showWarning = true) { |
| | | if (previewLoading.value || !salesId.value) return; |
| | | const values = await formApi.getValues(); |
| | | if (!values.materialStockId || !values.batchCode) { |
| | | if (showWarning) message.warning('请å
éæ©å¸¦æ¹æ¬¡å·çåºå'); |
| | | return; |
| | | } |
| | | previewLoading.value = true; bagPreview.value = undefined; selectedBagIds.value = []; previewStockId.value = undefined; |
| | | try { |
| | | const preview = await previewProductSalesScan(values.batchCode, 'BATCH', salesId.value); |
| | | previewStockId.value = values.materialStockId; |
| | | bagPreview.value = { ...preview, bags: (preview.bags || []).filter(b => b.materialStockId === values.materialStockId) }; |
| | | if (showWarning && !bagPreview.value.bags?.length) message.warning('该åºå没æå¯è¯å«çè¢ç ï¼å岿 è¢ç åºå坿æ°éæ£è´§'); |
| | | } finally { previewLoading.value = false; } |
| | | } |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | wrapperClass: 'grid-cols-3', |
| | | }); |
| | | |
| | | // è¢ç å¾éå忥顶鍿°éï¼ç´è§åæ å®é
æ£åºè¢æ° |
| | | watch(selectedBagIds, (ids) => { |
| | | if (bagPreview.value?.bags?.length && ids.length) { |
| | | void formApi.setFieldValue('quantity', ids.length); |
| | | } |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | |
| | | data.salesId = salesId.value; |
| | | data.lineId = lineId.value; |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateProductSalesDetail({ ...data, id: formData.value.id }) |
| | | : createProductSalesDetail(data)); |
| | | if (!formData.value?.id) { |
| | | // æªå è½½è¢ç æåºå/æ¹æ¬¡å·²åæ´æ¶ï¼å
å è½½ä¸æ¬¡ï¼æè¢ç çåºåä¸å
è®¸ææ°éæ£è´§ |
| | | if (!bagPreview.value || previewStockId.value !== data.materialStockId |
| | | || bagPreview.value.batchCode !== data.batchCode) { |
| | | try { await loadBags(false); } catch { /* é¢è§å¤±è´¥æ¶ææ°éæ£è´§ï¼ç±å端æç»æ ¡éª */ } |
| | | } |
| | | const preview = bagPreview.value; |
| | | const bags = preview?.bags ?? []; |
| | | if (preview && bags.length) { |
| | | if (previewStockId.value !== data.materialStockId || preview.batchCode !== data.batchCode) { |
| | | message.warning('åºåææ¹æ¬¡å·²åæ´ï¼è¯·éæ°å è½½è¢ç '); return; |
| | | } |
| | | if (!selectedBagIds.value.length) { message.warning('è¯·éæ©å®é
æ£åºçè¢ç '); return; } |
| | | await scanProductSales({ salesId: salesId.value!, lineId: lineId.value!, materialStockId: data.materialStockId, |
| | | manual: true, scanType: 'BATCH', scanContent: preview.scanContent!, |
| | | bagIds: selectedBagIds.value, quantity: selectedBagIds.value.length }); |
| | | } else { |
| | | await createProductSalesDetail(data); |
| | | } |
| | | } else { |
| | | await updateProductSalesDetail({ ...data, id: formData.value.id }); |
| | | } |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success', lineId.value!); |
| | |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | bagPreview.value = undefined; selectedBagIds.value = []; previewStockId.value = undefined; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useDetailFormSchema(formApi) }); |
| | | formApi.setState({ |
| | | schema: useDetailFormSchema(formApi, (stock) => { |
| | | // éæ©åºååèªå¨å è½½è¢ç ï¼é¿å
æè¢ç çåºå误走æ°éæ£è´§ |
| | | if (stock && !formData.value?.id) { void loadBags(false); } |
| | | }), |
| | | }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | batchId?: number; |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-4" /> |
| | | <div v-if="!formData?.id" class="mx-4 mt-2"> |
| | | <a-alert |
| | | type="info" |
| | | show-icon |
| | | class="mb-3" |
| | | message="请å
éæ©å¸¦æ¹æ¬¡å·çåºåï¼åå 载并å¾éå®é
æ£åºçè¢ç ï¼æ è¢ç çåå²åºå坿æ°éæ£è´§ã" |
| | | /> |
| | | <div class="mb-2 flex flex-wrap items-center gap-2"> |
| | | <a-button type="primary" :loading="previewLoading" @click="loadBags()"> |
| | | {{ previewLoading ? 'æ£å¨å è½½è¢ç â¦' : 'å è½½åºåè¢ç ' }} |
| | | </a-button> |
| | | <a-tag v-if="bagPreview" color="blue">å¯è¯å« {{ bagPreview.bags?.length ?? 0 }} è¢</a-tag> |
| | | <a-tag v-if="selectedBagIds.length" color="green">å·²é {{ selectedBagIds.length }} è¢</a-tag> |
| | | <span v-if="bagPreview && !bagPreview.bags?.length" class="text-sm text-gray-500"> |
| | | 该åºå没æå¯è¯å«çè¢ç ï¼å¯ææ°éæ£è´§ã |
| | | </span> |
| | | </div> |
| | | <PickingBags v-if="bagPreview?.bags?.length" v-model="selectedBagIds" :bags="bagPreview.bags" :disabled="previewLoading" /> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { deleteProductSalesDetail } from '#/api/mes/wm/productsales/detail'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useDetailGridColumns } from '../data'; |
| | | import { |
| | | formatPickingRemark, |
| | | formatScanSource, |
| | | useDetailGridColumns, |
| | | } from '../data'; |
| | | |
| | | const props = defineProps<{ |
| | | details: MesWmProductSalesDetailApi.ProductSalesDetail[]; |
| | |
| | | <template> |
| | | <div class="px-4 py-2"> |
| | | <Grid> |
| | | <template #scanSource="{ row }"> |
| | | <a-tooltip :title="formatScanSource(row)" placement="topLeft"> |
| | | <span class="block truncate">{{ formatScanSource(row) }}</span> |
| | | </a-tooltip> |
| | | </template> |
| | | <template #pickingRemark="{ row }"> |
| | | <a-tooltip |
| | | :title="formatPickingRemark(row.remark) || undefined" |
| | | placement="topLeft" |
| | | > |
| | | <span class="block truncate">{{ formatPickingRemark(row.remark) }}</span> |
| | | </a-tooltip> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | ifShow: !row.bagCodeId, |
| | | ifShow: !row.bagCodeId && !row.shipmentId, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | ifShow: !row.shipmentId, |
| | | icon: ACTION_ICON.DELETE, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [ |
| | |
| | | createProductSales, |
| | | finishProductSales, |
| | | getProductSales, |
| | | getProductSalesShipments, |
| | | previewProductSalesScan, |
| | | scanProductSales, |
| | | shippingProductSales, |
| | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesWmProductSalesApi.ProductSales>(); |
| | | const shipments = ref<MesWmProductSalesApi.Shipment[]>([]); |
| | | const shipmentStatus: Record<number, string> = { 0: 'æ£è´§ä¸', 10: 'å¾
å¡«åè¿å', 20: 'å¾
æ§è¡åºåº', 30: 'å·²åºåº', 40: '已忶' }; |
| | | const scanContent = ref(''); |
| | | const scanType = ref<'' | 'BAG' | 'BATCH' | 'PALLET'>(''); |
| | | const scanPreview = ref<MesWmProductSalesApi.ProductSalesScanResponse>(); |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ§è¡æ£è´§ï¼åºåºæ°é䏿£è´§æ°éä¸ä¸è´æ¶äºæ¬¡ç¡®è®¤ */ |
| | | /** æ§è¡æ£è´§ï¼æ ¸å¯¹åºåºæ°é䏿£è´§æ°é */ |
| | | async function handleStock() { |
| | | if (!formData.value?.id) { |
| | | return; |
| | | } |
| | | const quantityMatch = await checkProductSalesQuantity(formData.value.id); |
| | | if (!quantityMatch) { |
| | | try { |
| | | await confirm('åºåºæ°é䏿£è´§æ°éä¸ä¸è´ï¼ç¡®è®¤æ§è¡æ£è´§ï¼'); |
| | | } catch { |
| | | return; |
| | | } |
| | | message.warning( |
| | | 'æ¬è½®è¿æ²¡æå¾
åè´§çæ£è´§æç»ï¼è¯·å
å¨ç©æä¿¡æ¯ä¸ãæ£è´§ãææ«ç å å
¥è¢ç ï¼ä¸ç´¯è®¡æ°éä¸è½è¶
è¿è®¡åæ°é', |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | |
| | | scanContent.value = ''; |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | shipments.value = []; |
| | | originalSnapshot.value = ''; |
| | | return; |
| | | } |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getProductSales(data.id); |
| | | shipments.value = await getProductSalesShipments(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | if (formType.value === 'shipping') { |
| | | const current = shipments.value.find(s => s.status === 10); |
| | | if (current) await formApi.setValues({ carrier: current.carrier || '', shippingNumber: current.shippingNumber || '' }); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | <Form class="mx-4" /> |
| | | <!-- éæ°å»ºæ¨¡å¼å±ç¤ºç©æä¿¡æ¯ --> |
| | | <template v-if="formData?.id"> |
| | | <div v-if="shipments.length" class="mx-4 mb-3"> |
| | | <h3 class="font-semibold">åè´§è®°å½</h3> |
| | | <div v-for="(shipment, index) in shipments" :key="shipment.id" class="mt-2 border-b pb-2"> |
| | | 第 {{index + 1}} 次 · {{shipmentStatus[shipment.status] || 'æªç¥ç¶æ'}} |
| | | <span v-if="shipment.shippingNumber"> · {{shipment.carrier || 'æªå½å
¥æ¿è¿å'}} / {{shipment.shippingNumber}}</span> |
| | | <span v-if="shipment.outboundTime"> · åºåºæ¶é´ {{shipment.outboundTime}}</span> |
| | | </div> |
| | | </div> |
| | | <Divider>ç©æä¿¡æ¯</Divider> |
| | | <div class="mx-4"> |
| | | <div v-if="isStock" class="mb-3 flex items-center gap-2 px-4"> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from 'vue'; |
| | | import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales'; |
| | | |
| | | const props = defineProps<{ bags: MesWmProductSalesApi.PickingBag[]; modelValue: number[]; disabled?: boolean }>(); |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | defineOptions({ name: 'PickingBags' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | bags: MesWmProductSalesApi.PickingBag[]; |
| | | modelValue: number[]; |
| | | disabled?: boolean; |
| | | }>(), |
| | | { disabled: false }, |
| | | ); |
| | | const emit = defineEmits<{ 'update:modelValue': [ids: number[]] }>(); |
| | | const search = ref(''), desired = ref<number>(), limit = ref(50), selectionError = ref(''); |
| | | const eligible = computed(() => props.bags.filter(b => b.selectable)); |
| | | const filtered = computed(() => props.bags.filter(b => !search.value || b.code.includes(search.value.trim()) || b.businessCode?.includes(search.value.trim()))); |
| | | watch(search, () => { limit.value = 50; }); |
| | | watch(() => props.modelValue, ids => { desired.value = ids.length || undefined; }); |
| | | function choose(bag: MesWmProductSalesApi.PickingBag, checked: boolean) { |
| | | selectionError.value = ''; |
| | | emit('update:modelValue', checked ? [...props.modelValue, bag.id] : props.modelValue.filter(id => id !== bag.id)); |
| | | } |
| | | |
| | | const search = ref(''); |
| | | const desired = ref<number>(); |
| | | const selectionError = ref(''); |
| | | const VISIBLE_SELECTED = 8; |
| | | |
| | | const eligible = computed(() => props.bags.filter((bag) => bag.selectable)); |
| | | const unavailableCount = computed(() => props.bags.length - eligible.value.length); |
| | | const filtered = computed(() => { |
| | | const keyword = search.value.trim(); |
| | | if (!keyword) { |
| | | return props.bags; |
| | | } |
| | | return props.bags.filter( |
| | | (bag) => |
| | | bag.code.includes(keyword) || |
| | | (bag.businessCode ?? '').includes(keyword) || |
| | | (bag.unavailableReason ?? '').includes(keyword), |
| | | ); |
| | | }); |
| | | const selectedBags = computed(() => { |
| | | const selected = new Set(props.modelValue); |
| | | return props.bags.filter((bag) => selected.has(bag.id)); |
| | | }); |
| | | const visibleSelected = computed(() => selectedBags.value.slice(0, VISIBLE_SELECTED)); |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (ids) => { |
| | | desired.value = ids.length || undefined; |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | const rowSelection = computed(() => ({ |
| | | selectedRowKeys: props.modelValue, |
| | | onChange: (keys: (number | string)[]) => { |
| | | selectionError.value = ''; |
| | | emit('update:modelValue', keys.map(Number)); |
| | | }, |
| | | getCheckboxProps: (record: MesWmProductSalesApi.PickingBag) => ({ |
| | | disabled: props.disabled || !record.selectable, |
| | | }), |
| | | })); |
| | | |
| | | const columns = [ |
| | | { title: 'è¢ç ', dataIndex: 'code', key: 'code', ellipsis: true }, |
| | | { title: 'ä¸å¡ç ', dataIndex: 'businessCode', key: 'businessCode', width: 160, ellipsis: true }, |
| | | { title: 'æç', dataIndex: 'palletId', key: 'palletId', width: 90 }, |
| | | { title: 'ç¶æ', key: 'status', width: 100 }, |
| | | { title: '说æ', dataIndex: 'unavailableReason', key: 'reason', width: 180, ellipsis: true }, |
| | | ]; |
| | | |
| | | function chooseQuantity() { |
| | | const count = Number(desired.value); |
| | | if (!Number.isInteger(count) || count < 1 || count > eligible.value.length) { |
| | | selectionError.value = `请è¾å
¥ 1 è³ ${eligible.value.length} çæ´æ°è¢æ°`; return; |
| | | selectionError.value = `请è¾å
¥ 1 è³ ${eligible.value.length} çæ´æ°è¢æ°`; |
| | | return; |
| | | } |
| | | selectionError.value = ''; |
| | | emit('update:modelValue', eligible.value.slice(0, count).map(b => b.id)); |
| | | emit('update:modelValue', eligible.value.slice(0, count).map((bag) => bag.id)); |
| | | } |
| | | |
| | | function clearAll() { |
| | | selectionError.value = ''; |
| | | emit('update:modelValue', []); |
| | | } |
| | | |
| | | function removeBag(id: number) { |
| | | emit('update:modelValue', props.modelValue.filter((bagId) => bagId !== id)); |
| | | } |
| | | |
| | | function rowClassName(record: MesWmProductSalesApi.PickingBag) { |
| | | return record.selectable ? '' : 'picking-row-disabled'; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <fieldset class="pick-bags" :disabled="disabled"> |
| | | <legend>éæ©è¿½æº¯è¢ç </legend> |
| | | <p class="pick-summary" role="status">å¯é {{eligible.length}} è¢ Â· æ¬æ¬¡å·²é <strong>{{modelValue.length}}</strong> è¢</p> |
| | | <div class="pick-controls"> |
| | | <label>æ¬æ¬¡æ£è´§æ°é<input v-model="desired" type="number" min="1" :max="eligible.length" step="1" placeholder="è¾å
¥è¢æ°" /></label> |
| | | <button type="button" :disabled="!eligible.length" @click="chooseQuantity">ææ°ééæ©è¢ç </button> |
| | | <button type="button" :disabled="!modelValue.length" @click="emit('update:modelValue', [])">æ¸
ç©ºéæ©</button> |
| | | <div class="rounded-lg border border-gray-200 p-3"> |
| | | <div class="mb-3 flex flex-wrap items-center justify-between gap-2"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="font-semibold">æ£è´§è¢ç </span> |
| | | <a-tag color="blue">å¯é {{ eligible.length }} è¢</a-tag> |
| | | <a-tag color="green">å·²é {{ modelValue.length }} è¢</a-tag> |
| | | <a-tag v-if="unavailableCount > 0" color="default">ä¸å¯é {{ unavailableCount }} è¢</a-tag> |
| | | </div> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <a-input |
| | | v-model:value="search" |
| | | allow-clear |
| | | placeholder="æç´¢è¢ç / ä¸å¡ç " |
| | | style="width: 200px" |
| | | /> |
| | | <a-input-number |
| | | v-model:value="desired" |
| | | :min="1" |
| | | :max="eligible.length" |
| | | :precision="0" |
| | | placeholder="è¢æ°" |
| | | style="width: 90px" |
| | | /> |
| | | <a-button :disabled="disabled || !eligible.length" @click="chooseQuantity"> |
| | | ææ°ééæ© |
| | | </a-button> |
| | | <a-button :disabled="disabled || !modelValue.length" @click="clearAll"> |
| | | æ¸
ç©ºéæ© |
| | | </a-button> |
| | | </div> |
| | | </div> |
| | | <p class="pick-help">ææ°é鿩尿¿æ¢å½åéæ©ï¼æè¢ç 顺åºéåå¯éè¢ãè¯·æ ¸å¯¹å®ç©ï¼æéè¢å¾éã</p> |
| | | <p v-if="selectionError" class="pick-error" role="alert">{{selectionError}}</p> |
| | | <label class="pick-search">æ¥æ¾è¢ç <input v-model="search" type="search" placeholder="è¾å
¥ä¸å¡ç æè¿½æº¯ç " /></label> |
| | | <div class="pick-list"> |
| | | <label v-for="bag in filtered.slice(0, limit)" :key="bag.id" class="pick-option" :class="{ 'pick-unavailable': !bag.selectable, 'pick-selected': modelValue.includes(bag.id) }"> |
| | | <input type="checkbox" :checked="modelValue.includes(bag.id)" :disabled="!bag.selectable" :aria-label="'éæ©è¢ç ' + bag.code" @change="choose(bag, ($event.target as HTMLInputElement).checked)" /> |
| | | <span><strong>{{bag.code}}</strong><small v-if="bag.businessCode">ä¸å¡ç {{bag.businessCode}}</small><small v-if="bag.unavailableReason">{{bag.unavailableReason}}</small></span> |
| | | </label> |
| | | <p v-if="!filtered.length" class="pick-help">没æå¹é
çè¢ç </p> |
| | | |
| | | <a-alert |
| | | v-if="selectionError" |
| | | type="error" |
| | | show-icon |
| | | :message="selectionError" |
| | | class="mb-2" |
| | | /> |
| | | |
| | | <a-table |
| | | row-key="id" |
| | | :row-selection="rowSelection" |
| | | :columns="columns" |
| | | :data-source="filtered" |
| | | :pagination="{ pageSize: 10, showSizeChanger: false, size: 'small' }" |
| | | :scroll="{ y: 280 }" |
| | | :row-class-name="rowClassName" |
| | | bordered |
| | | size="small" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'palletId'"> |
| | | {{ record.palletId ? `#${record.palletId}` : '-' }} |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | <a-tag :color="record.selectable ? 'green' : 'default'"> |
| | | {{ record.selectable ? 'å¯é' : 'ä¸å¯é' }} |
| | | </a-tag> |
| | | </template> |
| | | <template v-else-if="column.key === 'reason'"> |
| | | {{ record.unavailableReason || '-' }} |
| | | </template> |
| | | </template> |
| | | </a-table> |
| | | |
| | | <div v-if="selectedBags.length" class="mt-3 flex flex-wrap items-center gap-1"> |
| | | <span class="text-sm text-gray-500">å·²éè¢ç ï¼</span> |
| | | <a-tag |
| | | v-for="bag in visibleSelected" |
| | | :key="bag.id" |
| | | :closable="!disabled" |
| | | @close="removeBag(bag.id)" |
| | | > |
| | | {{ bag.code }} |
| | | </a-tag> |
| | | <a-tag v-if="selectedBags.length > VISIBLE_SELECTED"> |
| | | +{{ selectedBags.length - VISIBLE_SELECTED }} |
| | | </a-tag> |
| | | </div> |
| | | <button v-if="filtered.length > limit" type="button" @click="limit += 50">æ¾ç¤ºæ´å¤è¢ç ï¼{{limit}} / {{filtered.length}}ï¼</button> |
| | | <details v-if="modelValue.length"><summary>æ ¸å¯¹å
¨é¨å·²éè¢ç ï¼{{modelValue.length}} è¢ï¼</summary><p v-for="bag in bags.filter(b => modelValue.includes(b.id))" :key="bag.id" class="pick-code">{{bag.code}}<small v-if="bag.businessCode"> · {{bag.businessCode}}</small></p></details> |
| | | </fieldset> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .pick-bags{border:0;padding:0;margin:16px 0;min-width:0;color:inherit}.pick-bags legend{font-weight:600}.pick-summary{margin:8px 0}.pick-controls{display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap}.pick-controls label{flex:1;min-width:130px}.pick-bags label{font-weight:500}.pick-bags input[type=number],.pick-bags input[type=search]{display:block;width:100%;min-width:0;min-height:44px;border:1px solid #64748b;border-radius:6px;background:var(--background, white);color:inherit;padding:8px 10px;font:inherit}.pick-bags button{min-height:44px;padding:8px 12px;border:1px solid #64748b;border-radius:6px;color:#0369a1;background:var(--background, white);cursor:pointer}.pick-bags button:disabled{opacity:.5;cursor:default}.pick-help{font-size:14px;color:#475569;line-height:1.5;margin:10px 0}.pick-search{display:block;margin:12px 0}.pick-list{max-height:360px;overflow:auto;border-top:1px solid #e2e8f0}.pick-option{display:flex;align-items:center;gap:12px;min-height:64px;padding:10px 4px;border-bottom:1px solid #e2e8f0;cursor:pointer}.pick-option input{width:20px;height:20px;flex-shrink:0;accent-color:#0369a1}.pick-option span{min-width:0;overflow-wrap:anywhere}.pick-option strong{display:block;font-weight:500}.pick-option small{display:block;color:#475569;font-size:14px}.pick-selected{background:#f0f9ff;color:#0f172a}.pick-unavailable{cursor:default;background:#f8fafc;color:#475569}.pick-error{color:#b91c1c}.pick-code{overflow-wrap:anywhere;font-size:14px;padding:6px 0}.pick-bags summary{cursor:pointer;min-height:44px;padding:10px 0;color:#0369a1}.pick-bags :focus-visible{outline:2px solid #0369a1;outline-offset:2px} |
| | | :deep(.picking-row-disabled) { |
| | | color: #94a3b8; |
| | | background: #f8fafc; |
| | | } |
| | | </style> |
| | |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | // componentProps: { |
| | | // excludeCodes: ['WIP_VIRTUAL_WAREHOUSE'], |
| | | // }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: '䏿¶ç®æ åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !values.warehouseId, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: '䏿¶ç®æ åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: !values.locationId, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import { WmBatchSelect } from '#/views/wls/batch/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | import { |
| | | WmReturnSalesProductSalesLineSelect, |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'å
¥åºä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // 忢ä»åºåæ¸
空åºåºååºä½ |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }); |
| | | }, |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | // 忢åºåºåæ¸
空åºä½ |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('areaId', undefined); |
| | | }, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ReturnQualityForm from './modules/return-quality-form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [QualityModal, qualityModalApi] = useVbenModal({ |
| | | connectedComponent: ReturnQualityForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** æ§è¡éè´§ */ |
| | | function handleFinish(row: MesWmReturnSalesApi.ReturnSales) { |
| | | formModalApi.setData({ formType: 'finish', id: row.id }).open(); |
| | | } |
| | | |
| | | /** éè´§è´¨æ£ */ |
| | | function handleQuality(row: MesWmReturnSalesApi.ReturnSales) { |
| | | qualityModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤éå®éè´§å */ |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <QualityModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="éå®éè´§åå表"> |
| | | <template #toolbar-tools> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: 'éè´§è´¨æ£', |
| | | type: 'link', |
| | | auth: ['mes:qc-seed-quality:submit'], |
| | | ifShow: row.status === MesWmReturnSalesStatusEnum.CONFIRMED, |
| | | onClick: handleQuality.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ§è¡éè´§', |
| | | type: 'link', |
| | | auth: ['mes:wm-return-sales:finish'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { SeedReturnQualityInfo } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | resolveSeedReturnQuality, |
| | | submitSeedReturnQuality, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | interface IndicatorForm { |
| | | code: string; |
| | | label: string; |
| | | unit: string; |
| | | value?: number; |
| | | minValue?: number; |
| | | maxValue?: number; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const info = ref<SeedReturnQualityInfo>(); |
| | | const indicators = ref<IndicatorForm[]>([]); |
| | | const reason = ref(''); |
| | | const returnSalesId = ref<number>(); |
| | | |
| | | const existing = computed(() => !!info.value?.qualityId); |
| | | const allEntered = computed( |
| | | () => |
| | | indicators.value.length > 0 && |
| | | indicators.value.every( |
| | | (item) => |
| | | item.value !== undefined && |
| | | item.value !== null && |
| | | item.minValue !== undefined && |
| | | item.maxValue !== undefined, |
| | | ), |
| | | ); |
| | | const passed = computed( |
| | | () => |
| | | allEntered.value && |
| | | indicators.value.every( |
| | | (item) => |
| | | Number(item.value) >= Number(item.minValue) && |
| | | Number(item.value) <= Number(item.maxValue), |
| | | ), |
| | | ); |
| | | |
| | | function buildIndicators(data: SeedReturnQualityInfo) { |
| | | indicators.value = (data.indicators ?? []).map((item) => ({ |
| | | code: item.code, |
| | | label: item.name || item.code, |
| | | unit: item.unit || '', |
| | | value: undefined, |
| | | minValue: |
| | | item.minValue === null || item.minValue === undefined |
| | | ? undefined |
| | | : Number(item.minValue), |
| | | maxValue: |
| | | item.maxValue === null || item.maxValue === undefined |
| | | ? undefined |
| | | : Number(item.maxValue), |
| | | })); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!returnSalesId.value) { |
| | | return; |
| | | } |
| | | if (existing.value) { |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | if (!indicators.value.length) { |
| | | message.warning('该产åå°æªé
置质æ£é¡¹ï¼è¯·å
ç»´æ¤è´¨æ£é¡¹åæ ¼èå´'); |
| | | return; |
| | | } |
| | | if (!allEntered.value) { |
| | | message.warning('请填åå
¨é¨è´¨æ£é¡¹ç宿µå¼ååæ ¼ä¸ä¸é'); |
| | | return; |
| | | } |
| | | if ( |
| | | indicators.value.some( |
| | | (item) => |
| | | Number(item.minValue) < 0 || |
| | | Number(item.maxValue) < 0 || |
| | | Number(item.minValue) > Number(item.maxValue), |
| | | ) |
| | | ) { |
| | | message.warning('è´¨æ£é¡¹ä¸éä¸è½å¤§äºä¸éï¼ä¸ä¸è½ä¸ºè´æ°'); |
| | | return; |
| | | } |
| | | if (!passed.value && !reason.value.trim()) { |
| | | message.warning('è´¨æ£ä¸åæ ¼æ¶è¯·å¡«ååå '); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitSeedReturnQuality({ |
| | | returnSalesId: returnSalesId.value, |
| | | reason: reason.value.trim() || undefined, |
| | | indicators: indicators.value.map((item) => ({ |
| | | code: item.code, |
| | | value: Number(item.value), |
| | | minValue: Number(item.minValue), |
| | | maxValue: Number(item.maxValue), |
| | | })), |
| | | }); |
| | | message.success('éè´§è´¨æ£å·²æäº¤ï¼å¾
éå®é¨é¢å¯¼å®¡æ¹'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'éè´§è´¨æ£æäº¤å¤±è´¥'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | info.value = undefined; |
| | | indicators.value = []; |
| | | reason.value = ''; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number }>(); |
| | | returnSalesId.value = data.id; |
| | | modalApi.lock(); |
| | | try { |
| | | info.value = await resolveSeedReturnQuality(data.id); |
| | | buildIndicators(info.value); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '读åéè´§åè´¨æ£ä¿¡æ¯å¤±è´¥'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="existing ? 'éè´§è´¨æ£ç»æ' : 'ç»éåéè´§è´¨æ£'" class="w-3/5"> |
| | | <div v-if="info" class="quality-body"> |
| | | <p class="quality-meta"> |
| | | éè´§å <strong>{{ info.returnSalesCode }}</strong> |
| | | <span v-if="info.batchCode"> · æ¹æ¬¡ {{ info.batchCode }}</span> |
| | | </p> |
| | | <a-alert |
| | | v-if="existing" |
| | | type="success" |
| | | show-icon |
| | | message="该éè´§åå·²å®æè´¨æ£" |
| | | description="å¦ééæ°æ£éªï¼è¯·å°ãç§åè´¨æ£ - è´¨æ£è®°å½ã䏿æµç¨åèµ·å¤æ£ã" |
| | | /> |
| | | <template v-else> |
| | | <a-alert |
| | | type="info" |
| | | show-icon |
| | | message="è´¨æ£ä¸åæ ¼çéè´§åå°èªå¨åæ¶ï¼ä¸äºéè´§å¹¶ä¿çæ£éªè®°å½ã" |
| | | /> |
| | | <a-alert |
| | | v-if="!indicators.length" |
| | | type="warning" |
| | | show-icon |
| | | message="该产åå°æªé
置质æ£é¡¹ï¼è¯·å
å¨ãè´¨æ£é¡¹åæ ¼èå´é
ç½®ãä¸ç»´æ¤" |
| | | /> |
| | | <div |
| | | v-for="item in indicators" |
| | | :key="item.code" |
| | | class="quality-row" |
| | | > |
| | | <strong>{{ item.label }}<small v-if="item.unit">ï¼{{ item.unit }}ï¼</small></strong> |
| | | <a-input-number |
| | | v-model:value="item.value" |
| | | :min="0" |
| | | :precision="3" |
| | | placeholder="宿µå¼" |
| | | /> |
| | | <a-input-number |
| | | v-model:value="item.minValue" |
| | | :min="0" |
| | | :precision="3" |
| | | placeholder="ä¸é" |
| | | /> |
| | | <a-input-number |
| | | v-model:value="item.maxValue" |
| | | :min="0" |
| | | :precision="3" |
| | | placeholder="ä¸é" |
| | | /> |
| | | </div> |
| | | <a-textarea |
| | | v-model:value="reason" |
| | | :rows="3" |
| | | :maxlength="500" |
| | | :placeholder="passed ? 'åæ ¼ç»ææ éå¡«ååå ' : 'è´¨æ£ä¸åæ ¼æ¶è¯·å¡«ååå '" |
| | | /> |
| | | </template> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .quality-body { |
| | | padding: 4px 4px 0; |
| | | } |
| | | .quality-meta { |
| | | margin-bottom: 14px; |
| | | color: #59677b; |
| | | } |
| | | .quality-row { |
| | | display: grid; |
| | | grid-template-columns: 130px repeat(3, 1fr); |
| | | gap: 8px; |
| | | align-items: center; |
| | | margin: 10px 0; |
| | | } |
| | | .quality-row :deep(.ant-input-number) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| | |
| | | import { ErpPurchaseOrderSelect } from '#/views/erp/purchase/order/components'; |
| | | import { WmBatchSelect } from '#/views/wls/batch/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update'; |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåºåç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½åç§°', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | width: 100, |
| | |
| | | label: 'åºåè®°å½', |
| | | component: markRaw(WmMaterialStockSelect), |
| | | componentProps: { |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/åºåº/åºä½/æ¹æ¬¡/æ°é |
| | | // éæ©åºåè®°å½åï¼èªå¨åå¡«ä»åº/æ¹æ¬¡/æ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | areaId: stock?.areaId, |
| | | batchCode: stock?.batchCode, |
| | | batchId: stock?.batchId, |
| | | locationId: stock?.locationId, |
| | | quantity: stock?.quantity, |
| | | quantityMax: stock?.quantity, |
| | | warehouseId: stock?.warehouseId, |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.locationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | |
| | | import { Select } from 'ant-design-vue'; |
| | | |
| | | import { getWarehouseArea } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocation } from '#/api/mes/wm/warehouse/location'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { WmBatchSelect } from '#/views/wls/batch/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | defineOptions({ name: 'StockTakingPlanConditionValueInput' }); |
| | | |
| | |
| | | |
| | | /** å
¼å®¹ VbenForm é»è®¤ v-model prop ('value') å Vue 3 æ å ('modelValue') */ |
| | | const effectiveValue = computed(() => props.value ?? props.modelValue); |
| | | |
| | | const locationWarehouseId = ref<number>(); // åºåºéæ©å¨ä¸´æ¶æ°æ®ï¼éä»åºåä¼ ç»åºåºéæ©å¨ |
| | | const areaWarehouseId = ref<number>(); // åºä½éæ©å¨ä¸´æ¶æ°æ®ï¼éä»åºåä¼ ç»åºåºéæ©å¨ |
| | | const areaLocationId = ref<number>(); // åºä½éæ©å¨ä¸´æ¶æ°æ®ï¼éåºåºåä¼ ç»åºä½éæ©å¨ |
| | | |
| | | const qualityStatusValue = ref<number>(); |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | /** åºåºä»åºéæ©åè°ï¼æ¸
空åºåº */ |
| | | function handleLocationWarehouseChange() { |
| | | emit('update:modelValue', undefined); |
| | | emit('update:value', undefined); |
| | | emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' }); |
| | | } |
| | | |
| | | /** åºä½ä»åºéæ©åè°ï¼æ¸
空åºåºååºä½ */ |
| | | function handleAreaWarehouseChange() { |
| | | areaLocationId.value = undefined; |
| | | emit('update:modelValue', undefined); |
| | | emit('update:value', undefined); |
| | | emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' }); |
| | | } |
| | | |
| | | /** åºä½åºåºéæ©åè°ï¼æ¸
空åºä½ */ |
| | | function handleAreaLocationChange() { |
| | | emit('update:modelValue', undefined); |
| | | emit('update:value', undefined); |
| | | emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' }); |
| | | } |
| | | |
| | | /** ç¼è¾æ¶å填级èéæ©å¨çä¸çº§æ°æ®ï¼åºåºæå±ä»åºãåºä½æå±ä»åº/åºåºï¼ */ |
| | | async function loadCascadeData() { |
| | | /** ç¼è¾æ¶åå¡«è´¨éç¶æ */ |
| | | function loadCascadeData() { |
| | | if (props.type === WlsStockTakingParamTypeEnum.QUALITY_STATUS) { |
| | | qualityStatusValue.value = props.valueCode |
| | | ? Number(props.valueCode) |
| | | : undefined; |
| | | return; |
| | | } |
| | | if (!effectiveValue.value) { |
| | | return; |
| | | } |
| | | if (props.type === WlsStockTakingParamTypeEnum.LOCATION) { |
| | | const location = await getWarehouseLocation(effectiveValue.value); |
| | | locationWarehouseId.value = location?.warehouseId; |
| | | } else if (props.type === WlsStockTakingParamTypeEnum.AREA) { |
| | | const area = await getWarehouseArea(effectiveValue.value); |
| | | areaWarehouseId.value = area?.warehouseId; |
| | | areaLocationId.value = area?.locationId; |
| | | } |
| | | } |
| | | |
| | | // æ¡ä»¶ç±»åååæ¶é置级èä¸´æ¶æ°æ®ï¼ç¼è¾åæ¾æ¶æç±»åå è½½ä¸çº§æ°æ® |
| | | // æ¡ä»¶ç±»åååæ¶éç½®ä¸´æ¶æ°æ®ï¼ç¼è¾åæ¾æ¶æç±»åå è½½ |
| | | watch( |
| | | () => props.type, |
| | | () => { |
| | | locationWarehouseId.value = undefined; |
| | | areaWarehouseId.value = undefined; |
| | | areaLocationId.value = undefined; |
| | | qualityStatusValue.value = undefined; |
| | | void loadCascadeData(); |
| | | loadCascadeData(); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | |
| | | :model-value="effectiveValue" |
| | | @change="handleSelectorChange" |
| | | /> |
| | | <div |
| | | v-else-if="type === WlsStockTakingParamTypeEnum.LOCATION" |
| | | class="space-y-2" |
| | | > |
| | | <WmWarehouseSelect |
| | | v-model="locationWarehouseId" |
| | | placeholder="è¯·éæ©ä»åº" |
| | | @change="handleLocationWarehouseChange" |
| | | /> |
| | | <WmWarehouseLocationSelect |
| | | v-if="locationWarehouseId" |
| | | :model-value="effectiveValue" |
| | | :warehouse-id="locationWarehouseId" |
| | | placeholder="è¯·éæ©åºåº" |
| | | @change="handleSelectorChange" |
| | | /> |
| | | </div> |
| | | <div |
| | | v-else-if="type === WlsStockTakingParamTypeEnum.AREA" |
| | | class="space-y-2" |
| | | > |
| | | <WmWarehouseSelect |
| | | v-model="areaWarehouseId" |
| | | placeholder="è¯·éæ©ä»åº" |
| | | @change="handleAreaWarehouseChange" |
| | | /> |
| | | <WmWarehouseLocationSelect |
| | | v-if="areaWarehouseId" |
| | | v-model="areaLocationId" |
| | | placeholder="è¯·éæ©åºåº" |
| | | :warehouse-id="areaWarehouseId" |
| | | @change="handleAreaLocationChange" |
| | | /> |
| | | <WmWarehouseAreaSelect |
| | | v-if="areaLocationId" |
| | | :model-value="effectiveValue" |
| | | :location-id="areaLocationId" |
| | | placeholder="è¯·éæ©åºä½" |
| | | @change="handleSelectorChange" |
| | | /> |
| | | </div> |
| | | <MdItemSelect |
| | | v-else-if="type === WlsStockTakingParamTypeEnum.ITEM" |
| | | :model-value="effectiveValue" |
| | |
| | | CommonStatusEnum, |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | WlsStockTakingParamTypeEnum, |
| | | WlsStockTakingTypeEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | label: 'æ¡ä»¶ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions( |
| | | DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE, |
| | | 'number', |
| | | ) as NumberDictDataType[], |
| | | // ä¸åæ¯ææåºåº/åºä½çç¹ |
| | | options: ( |
| | | getDictOptions( |
| | | DICT_TYPE.MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE, |
| | | 'number', |
| | | ) as NumberDictDataType[] |
| | | ).filter( |
| | | (item) => |
| | | item.value !== WlsStockTakingParamTypeEnum.LOCATION && |
| | | item.value !== WlsStockTakingParamTypeEnum.AREA, |
| | | ), |
| | | placeholder: 'è¯·éæ©æ¡ä»¶ç±»å', |
| | | // æ¡ä»¶ç±»åååï¼æ¸
ç©ºå·²éæ¡ä»¶å¼ï¼é¿å
æ®çæ§ç±»åçæ¡ä»¶å¼ |
| | | onChange: async () => { |
| | |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { StockTakingPlanSelect } from '#/views/wls/stocktaking/plan/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'execute' | 'submit' | 'update'; |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 100, |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | | title: 'æ°é', |
| | | minWidth: 120, |
| | |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | // éæ©çç¹æ¸
ååï¼èªå¨å¸¦åºç©æãæ¹æ¬¡åä»å¨ä½ç½®ä¿¡æ¯ |
| | | // éæ©çç¹æ¸
ååï¼èªå¨å¸¦åºç©æãæ¹æ¬¡åä»åºä¿¡æ¯ |
| | | onChange: async (lineId?: number) => { |
| | | const line = taskLines.find((item) => item.id === lineId); |
| | | await formApi?.setValues({ |
| | | areaId: line?.areaId, |
| | | batchCode: line?.batchCode, |
| | | batchId: line?.batchId, |
| | | itemId: line?.itemId, |
| | | locationId: line?.locationId, |
| | | materialStockId: line?.materialStockId, |
| | | warehouseId: line?.warehouseId, |
| | | }); |
| | | }, |
| | | options: taskLines.map((line) => ({ |
| | | label: `${line.itemCode} - ${line.itemName} (${line.warehouseName}${ |
| | | line.locationName ? ` / ${line.locationName}` : '' |
| | | }${line.areaName ? ` / ${line.areaName}` : ''})`, |
| | | label: `${line.itemCode} - ${line.itemName} (${line.warehouseName ?? ''})`, |
| | | value: line.id, |
| | | })), |
| | | placeholder: 'è¯·éæ©çç¹æ¸
åï¼å¯éï¼', |
| | |
| | | label: 'ä»åº', |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | // ä»åºååæ¶æ¸
空åºåºååºä½ |
| | | onChange: () => |
| | | formApi?.setValues({ |
| | | areaId: undefined, |
| | | locationId: undefined, |
| | | }), |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | dependencies: { |
| | | triggerFields: ['lineId'], |
| | | disabled: (values) => values.lineId !== null, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId', 'lineId'], |
| | | show: (values) => !!values.warehouseId, |
| | | // éä¸çç¹æ¸
ååï¼åºåºç±æ¸
å带åºä¸ç¦æ¢æ¹å¨ |
| | | disabled: (values) => values.lineId !== null, |
| | | componentProps: (values) => ({ |
| | | onChange: () => formApi?.setFieldValue('areaId', undefined), |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | warehouseId: values.warehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['locationId', 'lineId'], |
| | | show: (values) => !!values.locationId, |
| | | // éä¸çç¹æ¸
ååï¼åºä½ç±æ¸
å带åºä¸ç¦æ¢æ¹å¨ |
| | | disabled: (values) => values.lineId !== null, |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | |
| | | } |
| | | for (const stock of rows) { |
| | | await createStockTakingTaskLine({ |
| | | areaId: stock.areaId, |
| | | batchId: stock.batchId, |
| | | itemId: stock.itemId, |
| | | locationId: stock.locationId, |
| | | materialStockId: stock.id, |
| | | quantity: stock.quantity, |
| | | taskId: props.taskId, |
| | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** ä¸å¡ç±»åé项 */ |
| | | const bizTypeOptions = [ |
| | |
| | | component: markRaw(WmWarehouseSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä»åº', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | componentProps: (values) => ({ |
| | | warehouseId: values.warehouseId, |
| | | placeholder: 'è¯·éæ©åºåº', |
| | | }), |
| | | trigger: (values, formApi) => { |
| | | if (values.locationId !== undefined) { |
| | | void formApi.setFieldValue('locationId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | dependencies: { |
| | | triggerFields: ['warehouseId', 'locationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.locationId, |
| | | placeholder: 'è¯·éæ©åºä½', |
| | | }), |
| | | trigger: (values, formApi) => { |
| | | if (values.areaId !== undefined) { |
| | | void formApi.setFieldValue('areaId', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'bizCode', |
| | | title: 'ä¸å¡åå·', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'itemCode', |
| | | title: 'ç©æç¼ç ', |
| | | width: 120, |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'specification', |
| | | title: 'è§æ ¼', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'itemTypeName', |
| | | title: 'ç±»å«', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'bizCode', |
| | | title: 'ä¸å¡åå·', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | width: 120, |
| | | slots: { default: 'batchCode' }, |
| | | }, |
| | | { |
| | | field: 'palletCode', |
| | | title: 'æçç ', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'bagCode', |
| | | title: 'è¢ç ', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'quantity', |
| | |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'ä»åº', |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'locationName', |
| | | title: 'åºåº', |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'areaName', |
| | | title: 'åºä½', |
| | | width: 100, |
| | | }, |
| | | { |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { WmMaterialStockSelect } from '#/views/wls/materialstock/components'; |
| | | import { |
| | | WmWarehouseAreaSelect, |
| | | WmWarehouseLocationSelect, |
| | | WmWarehouseSelect, |
| | | } from '#/views/wls/warehouse/components'; |
| | | import { WmWarehouseSelect } from '#/views/wls/warehouse/components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = |
| | |
| | | title: 'ç§»åºä»åº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'fromLocationName', |
| | | title: 'ç§»åºåºåº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'fromAreaName', |
| | | title: 'ç§»åºåºä½', |
| | | minWidth: 120, |
| | | }, |
| | | ...(editable || stockable |
| | | ? [ |
| | | { |
| | |
| | | label: 'éæ©åºå', |
| | | component: markRaw(WmMaterialStockSelect), |
| | | componentProps: { |
| | | // éæ©åºååï¼èªå¨åå¡«ç©æ/ä»åºä½ç½®/æ¹æ¬¡/æ°é |
| | | // éæ©åºååï¼èªå¨åå¡«ç©æ/ä»åº/æ¹æ¬¡/æ°é |
| | | onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => { |
| | | await formApi?.setValues({ |
| | | batchCode: stock?.batchCode, |
| | | batchId: stock?.batchId, |
| | | fromAreaId: stock?.areaId, |
| | | fromLocationId: stock?.locationId, |
| | | fromWarehouseId: stock?.warehouseId, |
| | | itemId: stock?.itemId, |
| | | quantity: stock?.quantity, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'fromLocationId', |
| | | label: 'ç§»åºåºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['fromWarehouseId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | warehouseId: values.fromWarehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'fromAreaId', |
| | | label: 'ç§»åºåºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['fromLocationId'], |
| | | componentProps: (values) => ({ |
| | | disabled: true, |
| | | locationId: values.fromLocationId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | { |
| | | field: 'toWarehouseName', |
| | | title: 'ç§»å
¥ä»åº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'toLocationName', |
| | | title: 'ç§»å
¥åºåº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'toAreaName', |
| | | title: 'ç§»å
¥åºä½', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | |
| | | placeholder: 'è¯·éæ©ç§»å
¥ä»åº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'toLocationId', |
| | | label: 'ç§»å
¥åºåº', |
| | | component: markRaw(WmWarehouseLocationSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç§»å
¥åºåº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['toWarehouseId'], |
| | | componentProps: (values) => ({ |
| | | placeholder: 'è¯·éæ©ç§»å
¥åºåº', |
| | | warehouseId: values.toWarehouseId, |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'toAreaId', |
| | | label: 'ç§»å
¥åºä½', |
| | | component: markRaw(WmWarehouseAreaSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç§»å
¥åºä½', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['toLocationId'], |
| | | componentProps: (values) => ({ |
| | | locationId: values.toLocationId, |
| | | placeholder: 'è¯·éæ©ç§»å
¥åºä½', |
| | | }), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'quantity', |
| | |
| | | defaults: { |
| | | batchCode: stock.batchCode, |
| | | batchId: stock.batchId, |
| | | fromAreaId: stock.areaId, |
| | | fromLocationId: stock.locationId, |
| | | fromWarehouseId: stock.warehouseId, |
| | | itemId: stock.itemId, |
| | | materialStockId: stock.id, |
| | |
| | | export { default as WmWarehouseAreaSelect } from './area-select.vue'; |
| | | export { default as WmWarehouseLocationSelect } from './location-select.vue'; |
| | | export { default as WmWarehouseSelect } from './select.vue'; |
| | |
| | | import type { MesWmWarehouseApi } from '#/api/mes/wm/warehouse'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'WlsWarehouse' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** 跳转åºåºå表 */ |
| | | function handleOpenLocation(row: MesWmWarehouseApi.Warehouse) { |
| | | router.push({ |
| | | path: '/wls/location', |
| | | query: { warehouseId: String(row.id) }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥çä»åºæ¡ç */ |
| | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åºåº', |
| | | type: 'link', |
| | | auth: ['mes:wm-warehouse:query'], |
| | | onClick: handleOpenLocation.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |