refactor(mdm): 移除物料主数据中废弃的字段配置
- 移除了辅单位相关字段(辅单位1、辅单位2及其换算比率)
- 移除了条码、价格、库存等业务字段
- 移除了批次管理和有效期等配置选项
- 移除了备注字段和批次属性配置分组
- 优化了表单数据处理逻辑,保留历史字段避免清空
feat(batch): 支持生产批次绑定吨袋功能
- 新增吨袋绑定功能,与托盘绑定互斥
- 添加了吨袋选择器组件和相关依赖逻辑
- 实现了吨袋绑定时的数据联动和校验
- 更新了表单架构以支持托盘和吨袋两种模式
- 添加了吨袋数据获取API接口
fix(batch): 完善批次绑定验证和状态处理
- 添加了托盘和吨袋互斥绑定的前端验证
- 修复了表单初始化时未清空残留值的问题
- 优化了绑定模式切换时的状态管理
- 增强了批次数据提交前的校验机制
refactor(oqc): 简化出货检验流程处理逻辑
- 移除了销售质检相关的审批流程代码
- 删除了复检、审批和流程记录功能模块
- 移除了工作流状态相关的字段和显示
- 简化了页面组件结构和交互逻辑
feat(seedQuality): 增强种子质检指标管理功能
- 新增种子指标范围配置的获取和保存功能
- 添加了指标合格范围的动态加载机制
- 优化了指标录入界面的验证和提示信息
- 更新了质检流程描述和操作反馈文案
refactor(qc): 调整质检流程和界面布局
- 移除了销售质检独立页面模块
- 重构了种子质检的界面布局和交互流程
- 优化了质量检验结果的提交和状态更新
- 改进了页面响应式设计和用户体验
| | |
| | | ); |
| | | } |
| | | |
| | | /** å¯ç»å®æ¹æ¬¡çå¨è¢ï¼æ æç䏿ªç»å®æ¹æ¬¡ï¼ */ |
| | | export function getUnboundTonBags() { |
| | | return requestClient.get<MesProBagCodeApi.BagCode[]>('/mes/pro/bag-code/unbound-ton-bags'); |
| | | } |
| | | |
| | | /** æ¹é导åºè¢ç Excelï¼äº¤ä»å°å·ï¼ */ |
| | | export function exportBagCodeExcel(params: MesProBagCodeApi.PageParams) { |
| | | return requestClient.download('/mes/pro/bag-code/export-excel', { params }); |
| | |
| | | /** çäº§æ¹æ¬¡ */ |
| | | export interface Batch { |
| | | id?: number; // ç¼å· |
| | | itemId?: number; // æç带å
¥çåç§ç¼å· |
| | | itemId?: number; // æçæå¨è¢å¸¦å
¥çåç§ç¼å· |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | bindingMode?: 'pallet' | 'tonBag'; // æ¹æ¬¡å·²æç»å®æ¹å¼ |
| | | businessCode?: string; // å
é¨åºå
¥åºä¸å¡ç |
| | | code?: string; // æ¹æ¬¡å·ï¼è¡¥å½æ£å¼ä¿¡æ¯åèªå¨çæï¼ |
| | | tempCode?: string; // ä¸´æ¶æ¹æ¬¡ç ï¼é¢çæäºç»´ç é¶æ®µä½¿ç¨ï¼æ«ç 追溯å
¼å®¹ï¼ |
| | |
| | | createTime?: string; // å建æ¶é´ |
| | | workers?: BatchWorker[]; // ç产人åé
ç½®å表 |
| | | palletIds?: number[]; // ç»å®æçç¼å·å表ï¼ä»
æ°å¢/è¡¥å½æäº¤æ¶ä¼ å
¥ï¼ç»å®æ¶æ¹æ¬¡ç´æ¥è¿å
¥ç产ä¸ï¼ |
| | | tonBagIds?: number[]; // ç»å®å¨è¢ç¼å·å表ï¼ä¸æçäºæ¥ |
| | | } |
| | | |
| | | /** çäº§æ¹æ¬¡æ«ç 追溯 */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesQcApproverConfigApi { |
| | | /** MES è´¨æ£å®¡æ ¸äººé
ç½® */ |
| | | export interface ApproverConfig { |
| | | id?: number; // ç¼å· |
| | | qcType?: number; // è´¨æ£ç±»åï¼1ç§åè´¨æ£ |
| | | approveNode?: number; // å®¡æ ¸èç¹ï¼1å å·¥é¨å®¡æ¹ 3å
¬å¸è´¨æ£å®¡æ¹ |
| | | userId?: number; // å®¡æ ¸äººç¨æ· ID |
| | | userNickname?: string; // å®¡æ ¸äººæµç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | status?: number; // ç¶æï¼0å¯ç¨ 1åç¨ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨æ£å®¡æ ¸äººé
ç½®å页 */ |
| | | export function getApproverConfigPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesQcApproverConfigApi.ApproverConfig>>( |
| | | '/mes/qc/approver-config/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¨æ£å®¡æ ¸äººé
置详æ
*/ |
| | | export function getApproverConfig(id: number) { |
| | | return requestClient.get<MesQcApproverConfigApi.ApproverConfig>( |
| | | `/mes/qc/approver-config/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è´¨æ£å®¡æ ¸äººé
ç½® */ |
| | | export function createApproverConfig( |
| | | data: MesQcApproverConfigApi.ApproverConfig, |
| | | ) { |
| | | return requestClient.post('/mes/qc/approver-config/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¨æ£å®¡æ ¸äººé
ç½® */ |
| | | export function updateApproverConfig( |
| | | data: MesQcApproverConfigApi.ApproverConfig, |
| | | ) { |
| | | return requestClient.put('/mes/qc/approver-config/update', data); |
| | | } |
| | | |
| | | /** å é¤è´¨æ£å®¡æ ¸äººé
ç½® */ |
| | | export function deleteApproverConfig(id: number) { |
| | | return requestClient.delete( |
| | | `/mes/qc/approver-config/delete?id=${id}`, |
| | | ); |
| | | } |
| | |
| | | inspectorUserId?: number; // æ£æµäººåç¨æ· ID |
| | | inspectorNickname?: string; // æ£æµäººåæµç§°ï¼å
³èæ¥è¯¢ï¼ |
| | | status?: number; // ç¶æ |
| | | workflowStatus?: number; // éå®è´¨æ£å¤æ£å审æ¹ç¶æ |
| | | remark?: string; // 夿³¨ |
| | | } |
| | | } |
| | |
| | | /** 宿åºè´§æ£éªå */ |
| | | export function finishOqc(id: number) { |
| | | return requestClient.put(`/mes/qc/oqc/finish?id=${id}`); |
| | | } |
| | | export function retestOqc(data: { id: number; checkQuantity: number; qualifiedQuantity: number; unqualifiedQuantity: number; checkResult: number; opinion?: string }) { |
| | | return requestClient.post('/mes/qc/oqc/retest', data); |
| | | } |
| | | export function approveOqc(stage: 'sales' | 'company', data: { id: number; approved: boolean; opinion: string }) { |
| | | return requestClient.post(`/mes/qc/oqc/${stage}-approve`, data); |
| | | } |
| | | export interface OqcAction { id: number; action: string; result?: number; opinion?: string; quantitiesJson?: string; operatorId?: number; createTime?: string } |
| | | export function getOqcActions(id: number) { |
| | | return requestClient.get<OqcAction[]>('/mes/qc/oqc/actions', { params: { id } }); |
| | | } |
| | | |
| | | /** å é¤åºè´§æ£éªå */ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export interface SeedIndicatorRange { |
| | | indicatorCode: string; |
| | | minValue: number; |
| | | maxValue: number; |
| | | updateTime?: string; |
| | | } |
| | | |
| | | export function getSeedIndicatorRanges() { |
| | | return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges'); |
| | | } |
| | | |
| | | export function saveSeedIndicatorRanges(ranges: SeedIndicatorRange[]) { |
| | | return requestClient.put('/mes/qc/seed-quality/ranges', { ranges }); |
| | | } |
| | | |
| | | export interface SeedQualityPageParams { |
| | | pageNo?: number; |
| | | pageSize?: number; |
| | |
| | | export interface SeedQualityRecord { |
| | | id: number; |
| | | code: string; |
| | | palletCode?: string; |
| | | batchCode?: string; |
| | | result: number; |
| | | workflowStatus: number; |
| | |
| | | } |
| | | export interface SeedQualityIndicator { |
| | | indicatorCode: string; |
| | | value: number; |
| | | value?: number; |
| | | minValue: number; |
| | | maxValue: number; |
| | | } |
| | |
| | | export function retestSeedQuality(id: number, data: Omit<MesQcSeedQualityApi.SubmitParams, 'qualifiedWarehouseId'>) { |
| | | return requestClient.post('/mes/qc/seed-quality/retest', data, { params: { id } }); |
| | | } |
| | | |
| | | export function retestSeedQualityBatch(data: { |
| | | ids: number[]; |
| | | reason?: string; |
| | | indicators: MesQcSeedQualityApi.Indicator[]; |
| | | }) { |
| | | return requestClient.post('/mes/qc/seed-quality/retest-batch', data); |
| | | } |
| | | export interface SeedRetestScope { |
| | | code: string; |
| | | scanType: 'PALLET' | 'BATCH'; |
| | | batchCode?: string; |
| | | totalCount: number; |
| | | retestIds: number[]; |
| | | initialRecheckIds: number[]; |
| | | pendingProcessCount: number; |
| | | pendingCompanyCount: number; |
| | | } |
| | | export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH') { |
| | | return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType } }); |
| | | } |
| | | export function submitSeedRetestScope(data: { |
| | | code: string; scanType: 'PALLET' | 'BATCH'; workflowStatus: 10 | 50; |
| | | qualityIds: number[]; indicators: MesQcSeedQualityApi.Indicator[]; reason?: string; |
| | | }) { |
| | | return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/retest-scope', data); |
| | | } |
| | | export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) { |
| | | return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data); |
| | | } |
| | |
| | | }, |
| | | }, |
| | | { |
| | | component: "ApiSelect", |
| | | fieldName: "unitMeasureId2", |
| | | label: "è¾
åä½1", |
| | | formItemClass: "col-span-1", |
| | | componentProps: { |
| | | placeholder: "è¯·éæ©è¾
åä½1", |
| | | allowClear: true, |
| | | api: getUnitListCached, |
| | | labelField: "name", |
| | | valueField: "id", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "unitMeasureRate1", |
| | | label: "è¾
åä½1æ¢ç®æ¯ç", |
| | | formItemClass: "col-span-1", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 6, |
| | | placeholder: "请è¾å
¥æ¢ç®æ¯ç", |
| | | }, |
| | | }, |
| | | { |
| | | component: "ApiSelect", |
| | | fieldName: "unitMeasureId3", |
| | | label: "è¾
åä½2", |
| | | formItemClass: "col-span-1", |
| | | componentProps: { |
| | | placeholder: "è¯·éæ©è¾
åä½2", |
| | | allowClear: true, |
| | | api: getUnitListCached, |
| | | labelField: "name", |
| | | valueField: "id", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "unitMeasureRate2", |
| | | label: "è¾
åä½2æ¢ç®æ¯ç", |
| | | formItemClass: "col-span-1", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 6, |
| | | placeholder: "请è¾å
¥æ¢ç®æ¯ç", |
| | | }, |
| | | }, |
| | | { |
| | | component: 'ApiSelect', |
| | | fieldName: 'warehouseId', |
| | | label: 'é»è®¤ä»åº', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | component: "Input", |
| | | fieldName: "barCode", |
| | | label: "æ¡ç ", |
| | | componentProps: { |
| | | placeholder: "请è¾å
¥æ¡ç ", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "salesPrice", |
| | | label: "éå®ä»·", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: "请è¾å
¥éå®ä»·", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "costPrice", |
| | | label: "ææ¬ä»·", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: "请è¾å
¥ææ¬ä»·", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "safetyStock", |
| | | label: "å®å
¨åºå", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: "请è¾å
¥å®å
¨åºå", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "minStock", |
| | | label: "æä½åºå", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: "请è¾å
¥æä½åºå", |
| | | }, |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "maxStock", |
| | | label: "æé«åºå", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: "请è¾å
¥æé«åºå", |
| | | }, |
| | | }, |
| | | { |
| | | component: 'Switch', |
| | | fieldName: 'isBatchManaged', |
| | | label: 'æ¹æ¬¡ç®¡ç', |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | component: "InputNumber", |
| | | fieldName: "expiryDay", |
| | | label: "æææ(天)", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | placeholder: "请è¾å
¥æææ", |
| | | }, |
| | | }, |
| | | { |
| | | component: 'Switch', |
| | | fieldName: 'syncMes', |
| | | label: '忥å°MES', |
| | | defaultValue: true, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(true), |
| | | }, |
| | | { |
| | | component: "RadioGroup", |
| | | fieldName: "status", |
| | | label: "ç¶æ", |
| | |
| | | optionType: "button", |
| | | }, |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | component: "Textarea", |
| | | fieldName: "remark", |
| | | label: "夿³¨", |
| | | formItemClass: "col-span-2", |
| | | componentProps: { |
| | | placeholder: "请è¾å
¥å¤æ³¨", |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | // ===== æ¹æ¬¡å±æ§é
ç½®ï¼isBatchManaged = true æ¶æ¾ç¤ºï¼ ===== |
| | | { |
| | | component: "Divider", |
| | | fieldName: "batchConfigDivider", |
| | | label: "æ¹æ¬¡å±æ§é
ç½®", |
| | | formItemClass: "col-span-2", |
| | | componentProps: { |
| | | orientation: "left", |
| | | plain: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "produceDateFlag", |
| | | label: "çäº§æ¥æå¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "expireDateFlag", |
| | | label: "æææå¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "receiptDateFlag", |
| | | label: "å
¥åºæ¥æå¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "vendorFlag", |
| | | label: "ä¾åºåå¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "purchaseOrderCodeFlag", |
| | | label: "éè´è®¢åå·å¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "lotNumberFlag", |
| | | label: "ç产æ¹å·å¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | { |
| | | component: "Switch", |
| | | fieldName: "qualityStatusFlag", |
| | | label: "è´¨éç¶æå¿
å¡«", |
| | | defaultValue: false, |
| | | componentProps: { |
| | | class: "w-auto", |
| | | checkedChildren: "æ¯", |
| | | unCheckedChildren: "å¦", |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ["isBatchManaged"], |
| | | show: (values) => values.isBatchManaged === true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as MdmItemApi.Item; |
| | | // ç¼è¾æ¶ä¿çå表æªå±ç¤ºçåå²å段ï¼é¿å
ä¿åç²¾ç®è¡¨åæ¶æ¸
空å®ä»¬ã |
| | | const data = { |
| | | ...formData.value, |
| | | ...(await formApi.getValues()), |
| | | syncMes: formData.value?.syncedMes ?? true, |
| | | } as MdmItemApi.Item; |
| | | try { |
| | | await (formData.value?.id ? updateItem(data) : createItem(data)); |
| | | await modalApi.close(); |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getItem(data.id); |
| | | // åæ¥ç¶ææ å°ï¼è¯¦æ
æ¥å£è¿å syncedMesï¼è¡¨ååæ®µä¸º syncMes |
| | | formData.value.syncMes = formData.value.syncedMes ?? true; |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | itemCode?: string; |
| | | } |
| | | |
| | | export type TonBagOption = PalletOption; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | palletOptions: PalletOption[] = [], |
| | | onPalletChange?: (ids: number[]) => void, |
| | | tonBagOptions: TonBagOption[] = [], |
| | | onTonBagChange?: (ids: number[]) => void, |
| | | existingBinding?: 'pallet' | 'tonBag', |
| | | ): VbenFormSchema[] { |
| | | const disabled = formType === 'detail'; |
| | | return [ |
| | |
| | | }, |
| | | { |
| | | fieldName: 'itemName', label: 'åç§åç§°', component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'éæ©æçåèªå¨å¸¦å
¥' }, |
| | | componentProps: { disabled: true, placeholder: 'éæ©æçæå¨è¢åèªå¨å¸¦å
¥' }, |
| | | }, |
| | | { |
| | | fieldName: 'itemCode', label: 'åç§ç¼ç ', component: 'Input', |
| | | componentProps: { disabled: true, placeholder: 'éæ©æçåèªå¨å¸¦å
¥' }, |
| | | componentProps: { disabled: true, placeholder: 'éæ©æçæå¨è¢åèªå¨å¸¦å
¥' }, |
| | | }, |
| | | { |
| | | fieldName: 'palletIds', |
| | |
| | | showSearch: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['itemId'], |
| | | triggerFields: ['itemId', 'tonBagIds'], |
| | | show: () => formType !== 'detail', |
| | | componentProps: (values) => ({ |
| | | options: palletOptions.map(option => ({ ...option, |
| | | disabled: !option.itemId || (!!values.itemId && option.itemId !== values.itemId), |
| | | disabled: existingBinding === 'tonBag' || !!values.tonBagIds?.length || !option.itemId || (!!values.itemId && option.itemId !== values.itemId), |
| | | })), |
| | | disabled: disabled || existingBinding === 'tonBag' || !!values.tonBagIds?.length, |
| | | }), |
| | | }, |
| | | help: 'åªè½ç»å®ç¸ååç§çæçï¼åç§ä¿¡æ¯èªå¨å¸¦å
¥ï¼ç¼ºå°åç§çæç请å
è£
è¢ãç»å®åæ¹æ¬¡ç´æ¥è¿å
¥ç产ä¸ï¼æççææ£å¼æçç ï¼æç䏿ªå½æ¹æ¬¡çè¢ç èªå¨çº§èå½å
¥æ¬æ¹æ¬¡ï¼è¢ç åç§é¡»ä¸è´ï¼ï¼ç¼è¾æ¶ä»
éå¾éæ°å¢æçï¼å·²ç»å®çä¸å¯è§£ç»', |
| | | }, |
| | | { |
| | | fieldName: 'tonBagIds', |
| | | label: 'ç»å®å¨è¢', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | disabled, |
| | | maxTagCount: 3, |
| | | dropdownMatchSelectWidth: false, |
| | | dropdownStyle: { minWidth: '380px', maxWidth: 'calc(100vw - 32px)' }, |
| | | mode: 'multiple', |
| | | optionFilterProp: 'label', |
| | | options: tonBagOptions, |
| | | onChange: onTonBagChange, |
| | | placeholder: 'è¯·éæ©ç¸ååç§çå¨è¢', |
| | | showSearch: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['itemId', 'palletIds'], |
| | | show: () => formType !== 'detail', |
| | | componentProps: (values) => ({ |
| | | options: tonBagOptions.map(option => ({ ...option, |
| | | disabled: existingBinding === 'pallet' || !!values.palletIds?.length || (!!values.itemId && option.itemId !== values.itemId), |
| | | })), |
| | | disabled: disabled || existingBinding === 'pallet' || !!values.palletIds?.length, |
| | | }), |
| | | }, |
| | | help: 'ä»
å¯ç»å®è®¡éåä½ä¸ºå¨ãæ æç䏿ªå½å
¥æ¹æ¬¡çè¢ç ï¼ä¸ç»å®æçäºæ¥ãç¼è¾æ¶ä»
éæ©æ°å¢å¨è¢ï¼å·²ç»å®çä¸å¯è§£ç»', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType, PalletOption } from "../data"; |
| | | import type { FormType, PalletOption, TonBagOption } from "../data"; |
| | | |
| | | import type { VxeTableGridOptions } from "#/adapter/vxe-table"; |
| | | import type { MesProBatchApi } from "#/api/mes/pro/batch"; |
| | |
| | | updateBatch, |
| | | } from "#/api/mes/pro/batch"; |
| | | import { getUnboundPalletList } from "#/api/mes/pro/pallet"; |
| | | import { getUnboundTonBags } from "#/api/mes/pro/bagCode"; |
| | | import { $t } from "#/locales"; |
| | | import { UserSelect } from "#/views/system/user/components"; |
| | | |
| | |
| | | const formType = ref<FormType>("create"); |
| | | const formData = ref<MesProBatchApi.Batch>(); |
| | | let palletOptions: PalletOption[] = []; |
| | | let tonBagOptions: TonBagOption[] = []; |
| | | async function handlePalletChange(ids: number[] = []) { |
| | | const values = await formApi.getValues(); |
| | | if (ids.length && (values.tonBagIds?.length || formData.value?.bindingMode === 'tonBag')) { |
| | | message.warning('ç»å®æçåç»å®å¨è¢åªè½éæ©ä¸ç§'); |
| | | await formApi.setFieldValue('palletIds', []); |
| | | return; |
| | | } |
| | | const selected = palletOptions.filter(option => ids.includes(option.value)); |
| | | const source = formData.value?.itemId ? formData.value : selected[0]; |
| | | if (selected.some(option => !option.itemId || (source?.itemId && option.itemId !== source.itemId))) { |
| | | message.warning('çäº§æ¹æ¬¡åªè½ç»å®ç¸ååç§çæç'); |
| | | await formApi.setFieldValue('palletIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value)); |
| | | } |
| | | await formApi.setFieldValue('itemId', source?.itemId); |
| | | await formApi.setFieldValue('itemName', source?.itemName); |
| | | await formApi.setFieldValue('itemCode', source?.itemCode); |
| | | } |
| | | |
| | | async function handleTonBagChange(ids: number[] = []) { |
| | | const values = await formApi.getValues(); |
| | | if (ids.length && (values.palletIds?.length || formData.value?.bindingMode === 'pallet')) { |
| | | message.warning('ç»å®æçåç»å®å¨è¢åªè½éæ©ä¸ç§'); |
| | | await formApi.setFieldValue('tonBagIds', []); |
| | | return; |
| | | } |
| | | const selected = tonBagOptions.filter(option => ids.includes(option.value)); |
| | | const source = formData.value?.itemId ? formData.value : selected[0]; |
| | | if (selected.some(option => option.itemId !== source?.itemId)) { |
| | | message.warning('çäº§æ¹æ¬¡åªè½ç»å®ç¸ååç§çå¨è¢'); |
| | | await formApi.setFieldValue('tonBagIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value)); |
| | | } |
| | | await formApi.setFieldValue('itemId', source?.itemId); |
| | | await formApi.setFieldValue('itemName', source?.itemName); |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProBatchApi.Batch; |
| | | if (data.palletIds?.length && data.tonBagIds?.length) { |
| | | message.warning('ç»å®æçåç»å®å¨è¢åªè½éæ©ä¸ç§'); |
| | | return; |
| | | } |
| | | if (formType.value === "fill") { |
| | | await fillBatchInfo({ ...data, id: formData.value!.id, workers }); |
| | | } else if (formData.value?.id) { |
| | |
| | | formType.value = data.formType; |
| | | // æ°å¢/è¡¥å½/ç¼è¾æå è½½æªç»å®æ¹æ¬¡çæçï¼ä¾ãç»å®æçãå¤éä¸æä½¿ç¨ |
| | | palletOptions = []; |
| | | tonBagOptions = []; |
| | | if (formType.value !== "detail") { |
| | | const pallets = await getUnboundPalletList(); |
| | | const [pallets, tonBags] = await Promise.all([getUnboundPalletList(), getUnboundTonBags()]); |
| | | palletOptions = pallets.map((pallet) => ({ |
| | | label: `${pallet.tempCode ?? `æç${pallet.id}`} · ${pallet.itemName || "å¾
å®ååç§"}ï¼å·²è£
${pallet.bagCount ?? 0} è¢ï¼`, |
| | | value: pallet.id as number, |
| | | itemId: pallet.itemId, itemName: pallet.itemName, itemCode: pallet.itemCode, |
| | | })); |
| | | tonBagOptions = tonBags.map((bag) => ({ |
| | | label: `${bag.code ?? `å¨è¢${bag.id}`} · ${bag.itemName || 'å¾
å®ååç§'}`, |
| | | value: bag.id as number, |
| | | itemId: bag.itemId, itemName: bag.itemName, itemCode: bag.itemCode, |
| | | })); |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange) }); |
| | | formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange) }); |
| | | if (formType.value !== "detail") { |
| | | // æ¸
ç©ºä¸æ¬¡æå¼æ¶å¾éçæçï¼é¿å
æ®çå¼è¯¯è§¦åãæ¹æ¬¡è¿å
¥ç产ä¸ã |
| | | await formApi.setFieldValue("palletIds", undefined); |
| | | await formApi.setFieldValue("tonBagIds", undefined); |
| | | } |
| | | // æ ¹æ®è¡¨åæåæ¢äººåå表å |
| | | workerGridApi.setGridOptions({ |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getBatch(data.id); |
| | | const existingBinding = formData.value.bindingMode; |
| | | formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange, existingBinding) }); |
| | | await formApi.setValues(formData.value); |
| | | const workers = (formData.value.workers ?? []).map( |
| | | (worker, index) => ({ ...worker, _rowKey: worker.id ?? index }) |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | |
| | | /** è´¨æ£ç±»å */ |
| | | export const QC_TYPE_OPTIONS = [{ label: 'ç§åè´¨æ£', value: 1 }]; |
| | | |
| | | /** å®¡æ ¸èç¹ */ |
| | | export const APPROVE_NODE_OPTIONS = [ |
| | | { label: 'åæ£å®¡æ ¸', value: 1 }, |
| | | { label: '夿£å®¡æ ¸', value: 3 }, |
| | | ]; |
| | | |
| | | /** æè´¨æ£ç±»åè¿åå¯éçå®¡æ ¸èç¹ */ |
| | | export function getNodeOptions(_qcType?: number) { |
| | | return APPROVE_NODE_OPTIONS; |
| | | } |
| | | |
| | | export function getQcTypeLabel(qcType?: number): string { |
| | | return QC_TYPE_OPTIONS.find((item) => item.value === qcType)?.label || 'â'; |
| | | } |
| | | |
| | | export function getApproveNodeLabel(approveNode?: number): string { |
| | | return ( |
| | | APPROVE_NODE_OPTIONS.find((item) => item.value === approveNode)?.label || |
| | | 'â' |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'qcType', |
| | | label: 'è´¨æ£ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: QC_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è´¨æ£ç±»å', |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('approveNode', undefined); |
| | | }, |
| | | }, |
| | | defaultValue: 1, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'approveNode', |
| | | label: 'å®¡æ ¸èç¹', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: APPROVE_NODE_OPTIONS, |
| | | placeholder: 'è¯·éæ©å®¡æ ¸èç¹', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['qcType'], |
| | | componentProps: (values) => ({ |
| | | allowClear: true, |
| | | options: getNodeOptions(values.qcType), |
| | | placeholder: 'è¯·éæ©å®¡æ ¸èç¹', |
| | | }), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'å®¡æ ¸äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | placeholder: 'è¯·éæ©å®¡æ ¸äºº', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'qcType', |
| | | label: 'è´¨æ£ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: QC_TYPE_OPTIONS, |
| | | placeholder: 'è¯·éæ©è´¨æ£ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'approveNode', |
| | | label: 'å®¡æ ¸èç¹', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: APPROVE_NODE_OPTIONS, |
| | | placeholder: 'è¯·éæ©å®¡æ ¸èç¹', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'qcType', |
| | | title: 'è´¨æ£ç±»å', |
| | | width: 140, |
| | | slots: { default: 'qcType' }, |
| | | }, |
| | | { |
| | | field: 'approveNode', |
| | | title: 'å®¡æ ¸èç¹', |
| | | width: 160, |
| | | slots: { default: 'approveNode' }, |
| | | }, |
| | | { |
| | | field: 'userNickname', |
| | | title: 'å®¡æ ¸äºº', |
| | | width: 140, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.COMMON_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 160, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteApproverConfig, |
| | | getApproverConfigPage, |
| | | } from '#/api/mes/qc/approverConfig'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | getApproveNodeLabel, |
| | | getQcTypeLabel, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** åå»ºå®¡æ ¸äººé
ç½® */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾å®¡æ ¸äººé
ç½® */ |
| | | function handleEdit(row: MesQcApproverConfigApi.ApproverConfig) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** å é¤å®¡æ ¸äººé
ç½® */ |
| | | async function handleDelete(row: MesQcApproverConfigApi.ApproverConfig) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.userNickname || '']), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteApproverConfig(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.userNickname || ''])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getApproverConfigPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="è´¨æ£å®¡æ ¸äººé
ç½®"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å®¡æ ¸äººé
ç½®']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-approver:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #qcType="{ row }"> |
| | | <span>{{ getQcTypeLabel(row.qcType) }}</span> |
| | | </template> |
| | | <template #approveNode="{ row }"> |
| | | <span>{{ getApproveNodeLabel(row.approveNode) }}</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:qc-approver:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:qc-approver:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [ |
| | | row.userNickname || '', |
| | | ]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createApproverConfig, |
| | | getApproverConfig, |
| | | updateApproverConfig, |
| | | } from '#/api/mes/qc/approverConfig'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesQcApproverConfigApi.ApproverConfig>(); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['å®¡æ ¸äººé
ç½®']) |
| | | : $t('ui.actionTitle.create', ['å®¡æ ¸äººé
ç½®']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = |
| | | (await formApi.getValues()) as MesQcApproverConfigApi.ApproverConfig; |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateApproverConfig(data) |
| | | : createApproverConfig(data)); |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<MesQcApproverConfigApi.ApproverConfig>(); |
| | | if (!data || !data.id) { |
| | | await formApi.resetForm(); |
| | | await formApi.setValues({ qcType: 1, status: 0 }); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getApproverConfig(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesQcOqcApi } from '#/api/mes/qc/oqc'; |
| | | import type { OqcAction } from '#/api/mes/qc/oqc'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { MesBizTypeEnum, MesQcStatusEnum } from '@vben/constants'; |
| | | import { MesQcStatusEnum } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { approveOqc, deleteOqc, exportOqc, getOqcActions, getOqcPage, retestOqc } from '#/api/mes/qc/oqc'; |
| | | import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const props = withDefaults(defineProps<{ salesMode?: boolean }>(), { salesMode: false }); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const workflowLabels: Record<number, string> = { |
| | | 0: 'è稿', 10: 'å¾
夿£', 20: 'å¾
éå®é¨å®¡æ¹', 30: 'å¾
å
¬å¸è´¨æ£å®¡æ¹', |
| | | 40: 'å·²æ¾è¡', 50: 'å¾
éå®é¨ä¸åæ ¼è¯å®¡', 60: 'å¾
å
¬å¸ä¸åæ ¼å®¡æ¹', 70: 'å·²æç»åºè´§', |
| | | }; |
| | | const active = ref<MesQcOqcApi.Oqc>(); |
| | | const retestOpen = ref(false); |
| | | const retestBusy = ref(false); |
| | | const retestForm = ref({ checkQuantity: 0, qualifiedQuantity: 0, unqualifiedQuantity: 0, opinion: '' }); |
| | | function openRetest(row: MesQcOqcApi.Oqc) { |
| | | active.value = row; |
| | | retestForm.value = { |
| | | checkQuantity: Number(row.checkQuantity ?? 0), qualifiedQuantity: Number(row.qualifiedQuantity ?? 0), |
| | | unqualifiedQuantity: Number(row.unqualifiedQuantity ?? 0), opinion: '', |
| | | }; |
| | | retestOpen.value = true; |
| | | } |
| | | async function saveRetest() { |
| | | if (!active.value?.id) return; |
| | | const form = retestForm.value; |
| | | if (form.checkQuantity < 0 || form.qualifiedQuantity < 0 || form.unqualifiedQuantity < 0 |
| | | || form.checkQuantity !== form.qualifiedQuantity + form.unqualifiedQuantity) { |
| | | message.warning('æ£æµæ°éå¿
é¡»çäºåæ ¼ä¸ä¸åæ ¼æ°éä¹å'); |
| | | return; |
| | | } |
| | | if (form.unqualifiedQuantity > 0 && !form.opinion.trim()) { |
| | | message.warning('夿£ä¸åæ ¼è¯·å¡«ååå '); |
| | | return; |
| | | } |
| | | retestBusy.value = true; |
| | | try { |
| | | await retestOqc({ id: active.value.id, ...form, checkResult: form.unqualifiedQuantity === 0 ? 1 : 2 }); |
| | | message.success('夿£ç»æå·²æäº¤ï¼å¾
éå®é¨è¯å®¡'); |
| | | retestOpen.value = false; |
| | | handleRefresh(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '夿£å¤±è´¥'); |
| | | } finally { retestBusy.value = false; } |
| | | } |
| | | const reviewOpen = ref(false); |
| | | const reviewBusy = ref(false); |
| | | const reviewStage = ref<'sales' | 'company'>('sales'); |
| | | const reviewApproved = ref(true); |
| | | const reviewOpinion = ref(''); |
| | | function openReview(row: MesQcOqcApi.Oqc, stage: 'sales' | 'company') { |
| | | active.value = row; reviewStage.value = stage; reviewApproved.value = true; |
| | | reviewOpinion.value = ''; reviewOpen.value = true; |
| | | } |
| | | async function saveReview() { |
| | | if (!active.value?.id) return; |
| | | if (!reviewOpinion.value.trim()) { message.warning('请填åå®¡æ¹æè§'); return; } |
| | | reviewBusy.value = true; |
| | | try { |
| | | await approveOqc(reviewStage.value, { id: active.value.id, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() }); |
| | | message.success('审æ¹å®æ'); reviewOpen.value = false; handleRefresh(); |
| | | } catch (error) { message.error((error as Error).message || '审æ¹å¤±è´¥'); } |
| | | finally { reviewBusy.value = false; } |
| | | } |
| | | const historyOpen = ref(false); |
| | | const historyActions = ref<OqcAction[]>([]); |
| | | const actionLabels: Record<string, string> = { |
| | | INITIAL: '忣', RETEST: '夿£', SALES_APPROVE: 'éå®é¨éè¿', SALES_REJECT: 'éå®é¨é©³å', |
| | | COMPANY_APPROVE: 'å
¬å¸è´¨æ£éè¿', COMPANY_REJECT: 'å
¬å¸è´¨æ£é©³å', |
| | | }; |
| | | async function openHistory(row: MesQcOqcApi.Oqc) { |
| | | if (!row.id) return; |
| | | active.value = row; |
| | | try { historyActions.value = await getOqcActions(row.id); historyOpen.value = true; } |
| | | catch (error) { message.error((error as Error).message || 'è¯»åæµç¨è®°å½å¤±è´¥'); } |
| | | } |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportOqc(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: props.salesMode ? 'éå®è´¨æ£å.xls' : 'åºè´§æ£éªå.xls', source: data }); |
| | | downloadFileFromBlobPart({ fileName: 'åºè´§æ£éªå.xls', source: data }); |
| | | } |
| | | |
| | | const columns = useGridColumns(); |
| | | if (props.salesMode && columns) { |
| | | columns.splice(columns.length - 1, 0, { |
| | | field: 'workflowStatus', title: 'éå®è´¨æ£æµç¨', width: 170, slots: { default: 'workflow' }, |
| | | }); |
| | | const actions = columns.at(-1); |
| | | if (actions) actions.width = 320; |
| | | } |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns, |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | sourceDocType: props.salesMode ? MesBizTypeEnum.WM_PRODUCT_SALES : undefined, |
| | | }); |
| | | }, |
| | | }, |
| | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid :table-title="props.salesMode ? 'éå®è´¨æ£åå表' : 'åºè´§æ£éªåå表'"> |
| | | <Grid table-title="åºè´§æ£éªåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.code }} |
| | | </Button> |
| | | </template> |
| | | <template #workflow="{ row }"> |
| | | <span>{{ workflowLabels[row.workflowStatus ?? 0] || 'æªç¥ç¶æ' }}</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | |
| | | }, |
| | | ]" |
| | | /> |
| | | <Button v-if="props.salesMode && row.workflowStatus === 10 && hasAccessByCodes(['mes:qc-oqc:retest'])" type="link" @click="openRetest(row)">夿£</Button> |
| | | <Button v-if="props.salesMode && [20, 50].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:sales-approve'])" type="link" @click="openReview(row, 'sales')">éå®é¨å®¡æ¹</Button> |
| | | <Button v-if="props.salesMode && [30, 60].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:company-approve'])" type="link" @click="openReview(row, 'company')">å
¬å¸è´¨æ£å®¡æ¹</Button> |
| | | <Button v-if="props.salesMode && hasAccessByCodes(['mes:qc-oqc:query'])" type="link" @click="openHistory(row)">æµç¨è®°å½</Button> |
| | | </template> |
| | | </Grid> |
| | | <a-modal v-if="props.salesMode" v-model:open="retestOpen" title="éå®è´¨æ£å¤æ£" :confirm-loading="retestBusy" ok-text="æäº¤å¤æ£" @ok="saveRetest"> |
| | | <p>æ£éªåï¼{{ active?.code }}</p> |
| | | <a-form layout="vertical"> |
| | | <a-form-item label="æ¬æ¬¡æ£æµæ°é"><a-input-number v-model:value="retestForm.checkQuantity" :min="0" style="width: 100%" /></a-form-item> |
| | | <a-form-item label="åæ ¼æ°é"><a-input-number v-model:value="retestForm.qualifiedQuantity" :min="0" style="width: 100%" /></a-form-item> |
| | | <a-form-item label="ä¸åæ ¼æ°é"><a-input-number v-model:value="retestForm.unqualifiedQuantity" :min="0" style="width: 100%" /></a-form-item> |
| | | <a-form-item label="夿£æè§"><a-textarea v-model:value="retestForm.opinion" :rows="3" :maxlength="500" /></a-form-item> |
| | | </a-form> |
| | | </a-modal> |
| | | <a-modal v-if="props.salesMode" v-model:open="reviewOpen" :title="reviewStage === 'sales' ? 'éå®é¨å®¡æ¹' : 'å
¬å¸è´¨æ£å®¡æ¹'" :confirm-loading="reviewBusy" ok-text="æäº¤å®¡æ¹" @ok="saveReview"> |
| | | <p>{{ active?.code }} · {{ workflowLabels[active?.workflowStatus ?? 0] }}</p> |
| | | <a-radio-group v-model:value="reviewApproved" style="display: flex; gap: 16px; margin: 12px 0 18px"> |
| | | <a-radio :value="true">åæ{{ reviewStage === 'company' ? active?.workflowStatus === 30 ? 'åºè´§æ¾è¡' : 'ä¸åæ ¼å¤ç½®' : '䏿¥å
¬å¸è´¨æ£' }}</a-radio> |
| | | <a-radio :value="false">驳å鿣</a-radio> |
| | | </a-radio-group> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填åå®¡æ¹æè§" /> |
| | | </a-modal> |
| | | <a-modal v-if="props.salesMode" v-model:open="historyOpen" :title="`æµç¨è®°å½ · ${active?.code || ''}`" :footer="null"> |
| | | <a-timeline v-if="historyActions.length"> |
| | | <a-timeline-item v-for="action in historyActions" :key="action.id"> |
| | | <strong>{{ actionLabels[action.action] || action.action }}</strong> |
| | | <span> · {{ action.createTime || 'â' }} · æä½äºº {{ action.operatorId || 'â' }}</span> |
| | | <div v-if="action.opinion">{{ action.opinion }}</div> |
| | | </a-timeline-item> |
| | | </a-timeline> |
| | | <a-empty v-else description="ææ æµç¨è®°å½" /> |
| | | </a-modal> |
| | | </Page> |
| | | </template> |
| | |
| | | return; |
| | | } |
| | | try { |
| | | await confirm('æ¯å¦æäº¤åºè´§æ£éªç»è®ºï¼æäº¤åè¿å
¥å®¡æ¹æµç¨ï¼åæ£ä¸åæ ¼éå
夿£ã'); |
| | | await confirm('æ¯å¦æäº¤åºè´§æ£éªç»è®ºï¼'); |
| | | } catch { |
| | | return; |
| | | } |
| | |
| | | originalSnapshot.value = current; |
| | | } |
| | | await finishOqc(id); |
| | | message.success('æ£éªå·²æäº¤ï¼è¯·å¨éå®è´¨æ£èåç»§ç»å¤æ£æå®¡æ¹'); |
| | | message.success('æ£éªå·²æäº¤'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | |
| | | import { |
| | | resolveSeedQuality, |
| | | submitSeedQualityGroup, |
| | | getSeedIndicatorRanges, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | |
| | |
| | | { code: 'germination_rate', label: 'åè½ç', unit: '%' }, |
| | | { code: 'moisture', label: 'æ°´å', unit: '%' }, |
| | | ]); |
| | | const rangesReady = computed(() => 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); |
| | | } |
| | | } |
| | | |
| | | const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0); |
| | | const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0); |
| | |
| | | reason.value = ''; |
| | | indicators.value.forEach((item) => { |
| | | item.value = undefined; |
| | | item.minValue = undefined; |
| | | item.maxValue = undefined; |
| | | }); |
| | | } |
| | | |
| | |
| | | resolving.value = true; |
| | | submittedCount.value = undefined; |
| | | try { |
| | | await loadRanges().catch(() => undefined); |
| | | const data = await resolveSeedQuality(value, scanType.value); |
| | | if (data.canceled) { |
| | | target.value = undefined; |
| | |
| | | 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} è¢å·²æäº¤ï¼${passed ? 'åæ£åæ ¼ï¼å¾
两级审æ¹' : '忣ä¸åæ ¼ï¼å¾
夿£'}`); |
| | | 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> |
| | |
| | | </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-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" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <span>â</span> |
| | | <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="ä¸é" /> |
| | | <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="ä¸é" /> |
| | | <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? 'å¾
å½å
¥' : isPassed(item) ? 'åæ ¼' : 'è¶
åºèå´' }}</a-tag> |
| | | </div> |
| | | </div> |
| | |
| | | <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 /> |
| | | <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> |
| | |
| | | getSeedQualityPage, |
| | | getSeedQualityIndicators, |
| | | getSeedQualityActions, |
| | | getSeedIndicatorRanges, |
| | | retestSeedQuality, |
| | | resolveSeedRetestScope, |
| | | submitSeedRetestScope, |
| | | approveSeedQuality, |
| | | approveSeedQualityBatch, |
| | | type SeedQualityRecord, |
| | | type SeedQualityIndicator, |
| | | type SeedQualityAction, |
| | | type SeedRetestScope, |
| | | } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | const records = ref<SeedQualityRecord[]>([]); |
| | |
| | | ); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const workflowLabels: Record<number, string> = { |
| | | 10: 'å¾
夿£', 20: 'å¾
å å·¥é¨å®¡æ¹', 30: 'å¾
å
¬å¸è´¨æ£å®¡æ¹', |
| | | 40: 'å·²æ¾è¡', 50: 'å¾
å å·¥é¨ä¸åæ ¼è¯å®¡', 60: 'å¾
å
¬å¸ä¸åæ ¼å®¡æ¹', 70: 'å·²æ¥åº', |
| | | 10: 'å¾
夿£', 20: 'å¾
åæ£å®¡æ ¸', 25: 'å¾
忣ä¸åæ ¼å¤ç', 30: 'å¾
夿£å®¡æ ¸', |
| | | 40: 'å·²æ¾è¡', 50: '忣驳åå¾
鿣', 60: 'å¾
夿£ä¸åæ ¼å¤ç', 70: 'å·²æ¥åº', |
| | | }; |
| | | const actionLabels: Record<string, string> = { |
| | | INITIAL: '忣', RETEST: '夿£', PROCESS_APPROVE: 'å å·¥é¨éè¿', |
| | | PROCESS_REJECT: 'å å·¥é¨é©³å', COMPANY_APPROVE: 'å
¬å¸è´¨æ£éè¿', |
| | | COMPANY_REJECT: 'å
¬å¸è´¨æ£é©³å', |
| | | INITIAL: '忣', INITIAL_RECHECK: '鿰忣', RETEST: '夿£', PROCESS_APPROVE: 'åæ£å®¡æ ¸éè¿', |
| | | PROCESS_REJECT: 'åæ£å®¡æ ¸é©³å', COMPANY_APPROVE: '夿£å®¡æ ¸éè¿', |
| | | COMPANY_REJECT: '夿£å®¡æ ¸é©³å', |
| | | }; |
| | | const indicatorLabels: Record<string, string> = { |
| | | purity: '纯度', cleanliness: 'å度', germination_rate: 'åè½ç', moisture: 'æ°´å', |
| | |
| | | const retestBusy = ref(false); |
| | | const retestIndicators = ref<SeedQualityIndicator[]>([]); |
| | | const retestReason = ref(''); |
| | | const scopeOpen = ref(false); |
| | | const scopeType = ref<'PALLET' | 'BATCH'>('PALLET'); |
| | | const scopeCode = ref(''); |
| | | const retestScope = ref<SeedRetestScope>(); |
| | | const scopeStatus = ref<10 | 50>(10); |
| | | const scopeBusy = ref(false); |
| | | const scopeIds = computed(() => scopeStatus.value === 10 ? retestScope.value?.retestIds || [] : retestScope.value?.initialRecheckIds || []); |
| | | const reviewOpen = ref(false); |
| | | const reviewBusy = ref(false); |
| | | const reviewStage = ref<'process' | 'company'>('process'); |
| | |
| | | const selectableStage = computed(() => { |
| | | if (!selectedRows.value.length) return undefined; |
| | | if (!selectedRows.value.every((record) => record.workflowStatus === selectedRows.value[0]?.workflowStatus)) return undefined; |
| | | if (selectedRows.value.every((record) => [20, 50].includes(record.workflowStatus))) return 'process'; |
| | | if (selectedRows.value.every((record) => [20, 25].includes(record.workflowStatus))) return 'process'; |
| | | if (selectedRows.value.every((record) => [30, 60].includes(record.workflowStatus))) return 'company'; |
| | | return undefined; |
| | | }); |
| | |
| | | async function openRetest(record: SeedQualityRecord) { |
| | | activeRecord.value = record; |
| | | try { |
| | | retestIndicators.value = await getSeedQualityIndicators(record.id); |
| | | 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 })); |
| | | retestReason.value = ''; |
| | | retestOpen.value = true; |
| | | } catch (error) { |
| | | message.error((error as Error).message || '读åè´¨æ£ææ 失败'); |
| | | } |
| | | } |
| | | function openScopeRetest() { |
| | | scopeOpen.value = true; retestScope.value = undefined; scopeCode.value = ''; retestReason.value = ''; |
| | | } |
| | | async function identifyScope() { |
| | | if (!scopeCode.value.trim()) { message.warning('è¯·æ«ææè¾å
¥æçç / çäº§æ¹æ¬¡ç '); return; } |
| | | scopeBusy.value = true; retestScope.value = undefined; |
| | | try { |
| | | const data = await resolveSeedRetestScope(scopeCode.value.trim(), scopeType.value); |
| | | if (!data.retestIds.length && !data.initialRecheckIds.length) { |
| | | const pending = [ |
| | | data.pendingProcessCount ? `${data.pendingProcessCount} æ¡å¾
åæ£å®¡æ ¸æä¸åæ ¼å¤ç` : '', |
| | | data.pendingCompanyCount ? `${data.pendingCompanyCount} æ¡å¾
夿£å®¡æ ¸æä¸åæ ¼å¤ç` : '', |
| | | ].filter(Boolean).join('ã'); |
| | | throw new Error(pending |
| | | ? `å½åèå´æ${pending}ï¼è¯·å
宿坹åºå®¡æ ¸æå¤ç` |
| | | : 'å½åèå´æ²¡æå¾
夿£æå¾
鿰忣çè¢ç '); |
| | | } |
| | | retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50; |
| | | const ranges = await getSeedIndicatorRanges().catch(() => []); |
| | | retestIndicators.value = 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 }; |
| | | }); |
| | | retestReason.value = ''; |
| | | } catch (error) { message.error((error as Error).message || 'è¯å«å¤±è´¥'); } |
| | | finally { scopeBusy.value = false; } |
| | | } |
| | | async function saveScopeRetest() { |
| | | if (!retestScope.value || !scopeIds.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; |
| | | } |
| | | 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, |
| | | qualityIds: scopeIds.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), |
| | | })) }); |
| | | message.success(`已宿 ${result.processedCount} è¢è´¨æ£`); scopeOpen.value = false; refreshAll(); |
| | | } catch (error) { message.error((error as Error).message || 'æ¹éè´¨æ£å¤±è´¥'); retestScope.value = undefined; } |
| | | finally { scopeBusy.value = false; } |
| | | } |
| | | async function saveRetest() { |
| | | const record = activeRecord.value; |
| | |
| | | if (retestIndicators.value.some((item) => item.value === null || item.value === undefined |
| | | || item.minValue === null || item.minValue === undefined || item.maxValue === null || item.maxValue === undefined |
| | | || Number(item.minValue) > Number(item.maxValue))) { |
| | | message.warning('请填å宿´ç宿µå¼åææä¸ä¸é'); |
| | | 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('夿£ä¸åæ ¼è¯·å¡«ååå '); |
| | | message.warning('æ£éªä¸åæ ¼è¯·å¡«ååå '); |
| | | return; |
| | | } |
| | | retestBusy.value = true; |
| | | try { |
| | | await retestSeedQuality(record.id, { |
| | | code: record.code, |
| | | const payload = { |
| | | reason: retestReason.value.trim(), |
| | | indicators: retestIndicators.value.map((item) => ({ |
| | | code: item.indicatorCode, value: Number(item.value), |
| | | minValue: Number(item.minValue), maxValue: Number(item.maxValue), |
| | | })), |
| | | }); |
| | | message.success(passed ? '夿£åæ ¼ï¼å¾
å å·¥é¨å®¡æ¹' : '夿£ä¸åæ ¼ï¼å¾
å å·¥é¨è¯å®¡'); |
| | | }; |
| | | await retestSeedQuality(record.id, { ...payload, code: record.code }); |
| | | message.success(record.workflowStatus === 50 |
| | | ? passed ? 'éæ°åæ£åæ ¼ï¼å¾
åæ£å®¡æ ¸' : '鿰忣ä¸åæ ¼ï¼å¾
ä¸åæ ¼å¤ç' |
| | | : passed ? '夿£åæ ¼ï¼å¾
夿£å®¡æ ¸' : '夿£ä¸åæ ¼ï¼å¾
ä¸åæ ¼å¤ç'); |
| | | retestOpen.value = false; |
| | | selectedIds.value = []; |
| | | refreshAll(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || '夿£æäº¤å¤±è´¥'); |
| | |
| | | } |
| | | async function saveReview() { |
| | | if (!reviewIds.value.length) return; |
| | | if (!reviewOpinion.value.trim()) { |
| | | message.warning('请填åå®¡æ¹æè§'); |
| | | if (!reviewApproved.value && !reviewOpinion.value.trim()) { |
| | | message.warning('é©³åæ¶è¯·å¡«åå®¡æ¹æè§'); |
| | | return; |
| | | } |
| | | reviewBusy.value = true; |
| | |
| | | <div class="header-copy"> |
| | | <div class="eyebrow">MES / è´¨é管ç</div> |
| | | <h1>è´¨æ£è®°å½</h1> |
| | | <p>è·è¸ªåæ£ã夿£ãå å·¥é¨å®¡æ¹åå
¬å¸è´¨æ£å®¡æ¹ï¼å®æå®¡æ¹åææµè½¬åºå</p> |
| | | <p>åæ£å®¡æ ¸éè¿åè¿è¡å¤æ£ï¼å¤æ£å®¡æ ¸éè¿åææ¾è¡å
¥åºæå¤ç½®ä¸åæ ¼å</p> |
| | | </div> |
| | | <div class="header-badge"> |
| | | <span class="badge-pulse" /> |
| | |
| | | <span class="count-chip">{{ total.toLocaleString() }} æ¡</span> |
| | | </div> |
| | | <div class="filters"> |
| | | <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">æ¹é{{ selectableStage === 'process' ? 'å å·¥é¨å®¡æ¹' : 'å
¬å¸è´¨æ£å®¡æ¹' }}ï¼{{ selectedRows.length }}ï¼</a-button> |
| | | <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="primary" @click="openScopeRetest">ææç / æ¹æ¬¡æ¹é夿£</a-button> |
| | | <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">æ¹é{{ selectableStage === 'process' ? selectedRows[0]?.workflowStatus === 25 ? '忣ä¸åæ ¼å¤ç' : 'åæ£å®¡æ ¸' : selectedRows[0]?.workflowStatus === 60 ? '夿£ä¸åæ ¼å¤ç' : '夿£å®¡æ ¸' }}ï¼{{ selectedRows.length }}ï¼</a-button> |
| | | <a-input v-model:value="code" allow-clear placeholder="è¢ç " @press-enter="searchRecords"> |
| | | <template #prefix><IconifyIcon icon="ep:box" /></template> |
| | | </a-input> |
| | |
| | | :data-source="records" |
| | | :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }" |
| | | :loading="loading" |
| | | :scroll="{ x: 1120 }" |
| | | :scroll="{ x: 1900 }" |
| | | :pagination="{ |
| | | current: page, |
| | | pageSize, |
| | |
| | | </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 key="actions" title="æä½" width="260" fixed="right"> |
| | | <template #default="{ record }"> |
| | | <a-space> |
| | | <a-button v-if="record.workflowStatus === 10 && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">夿£</a-button> |
| | | <a-button v-if="[20, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">å å·¥é¨å®¡æ¹</a-button> |
| | | <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">å
¬å¸è´¨æ£å®¡æ¹</a-button> |
| | | <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> |
| | | </section> |
| | | <a-modal v-model:open="retestOpen" title="ç§åè´¨æ£å¤æ£" :confirm-loading="retestBusy" ok-text="æäº¤å¤æ£" @ok="saveRetest"> |
| | | <p class="muted">è¢ç ï¼{{ activeRecord?.code }}ãåææ èå´å·²å¸¦å
¥ï¼è¯·å½å
¥æ¬æ¬¡å®æµå¼å¹¶æ ¸å¯¹ä¸ä¸éã</p> |
| | | <a-modal v-model:open="retestOpen" :title="activeRecord?.workflowStatus === 50 ? '鿰忣' : 'ç§åè´¨æ£å¤æ£'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? 'æäº¤åæ£' : 'æäº¤å¤æ£'" @ok="saveRetest"> |
| | | <p class="muted">è¢ç ï¼{{ activeRecord?.code }}ãåé¡¹ææ ä¸åæ£ä¸è´ï¼è¯·å½å
¥æ¬æ¬¡å®æµå¼å¹¶æ ¸å¯¹åæ ¼èå´ã</p> |
| | | <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row"> |
| | | <strong>{{ indicatorLabels[item.indicatorCode] || 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="ä¸é" /> |
| | | <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="ä¸é" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="夿£ä¸åæ ¼æ¶å¡«ååå " /> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="æ¬æ¬¡æ£éªä¸åæ ¼æ¶å¡«ååå " /> |
| | | </a-modal> |
| | | <a-modal v-model:open="reviewOpen" :title="reviewStage === 'process' ? 'å å·¥é¨å®¡æ¹' : 'å
¬å¸è´¨æ£å®¡æ¹'" :confirm-loading="reviewBusy" ok-text="æäº¤å®¡æ¹" @ok="saveReview"> |
| | | <a-modal v-model:open="scopeOpen" title="ææç / çäº§æ¹æ¬¡æ¹é夿£æéæ°åæ£" :confirm-loading="scopeBusy" ok-text="æäº¤æ¬é¶æ®µå
¨é¨è¢ç " :ok-button-props="{ disabled: !scopeIds.length }" @ok="saveScopeRetest"> |
| | | <a-space class="scope-scan"> |
| | | <a-select v-model:value="scopeType" style="width: 130px" :options="[{ label: 'æçç ', value: 'PALLET' }, { label: 'çäº§æ¹æ¬¡ç ', value: 'BATCH' }]" @change="retestScope = undefined" /> |
| | | <a-input-search v-model:value="scopeCode" placeholder="æ«ææè¾å
¥ç å¼" enter-button="è¯å«" :loading="scopeBusy" @search="identifyScope" /> |
| | | </a-space> |
| | | <template v-if="retestScope"> |
| | | <p>å·²è¯å« {{ retestScope.code }} · ææè¢ {{ retestScope.totalCount }} · çäº§æ¹æ¬¡ {{ retestScope.batchCode || 'â' }}</p> |
| | | <a-radio-group v-model:value="scopeStatus"> |
| | | <a-radio-button v-if="retestScope.retestIds.length" :value="10">夿£ {{ retestScope.retestIds.length }} è¢</a-radio-button> |
| | | <a-radio-button v-if="retestScope.initialRecheckIds.length" :value="50">鿰忣 {{ retestScope.initialRecheckIds.length }} è¢</a-radio-button> |
| | | </a-radio-group> |
| | | <p class="muted">åä¸ç»ææ å°åºç¨äºæ¬é¶æ®µå
¨é¨ {{ scopeIds.length }} è¢ã模æ¿èå´å¯ä»¥ä¿®æ¹ã</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="ä¸é" /> |
| | | </div> |
| | | <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="ä¸åæ ¼æ¶å¡«ååå " /> |
| | | </template> |
| | | </a-modal> |
| | | <a-modal v-model:open="reviewOpen" :title="[25, 60].includes(activeRecord?.workflowStatus ?? 0) ? 'ä¸åæ ¼å¤ç' : reviewStage === 'process' ? 'åæ£å®¡æ ¸' : '夿£å®¡æ ¸'" :confirm-loading="reviewBusy" ok-text="æäº¤å®¡æ ¸" @ok="saveReview"> |
| | | <p>{{ reviewIds.length > 1 ? `å
± ${reviewIds.length} æ¡è®°å½` : activeRecord?.code }} · {{ workflowLabels[activeRecord?.workflowStatus ?? 0] }}</p> |
| | | <a-radio-group v-model:value="reviewApproved" class="review-choice"> |
| | | <a-radio :value="true">åæ{{ reviewStage === 'company' ? activeRecord?.workflowStatus === 30 ? 'æ¾è¡å
¥åº' : 'æ¥åºå¤ç½®' : '䏿¥å
¬å¸è´¨æ£' }}</a-radio> |
| | | <a-radio :value="false">驳å鿣</a-radio> |
| | | <a-radio :value="true">{{ reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '确认ä¸åæ ¼ï¼å®æå¤æ£' : 'éè¿ï¼è¿å
¥å¤æ£' : activeRecord?.workflowStatus === 30 ? 'éè¿ï¼æ¾è¡å
¥åº' : '确认ä¸åæ ¼ï¼æ¥åºå¤ç½®' }}</a-radio> |
| | | <a-radio :value="false">{{ reviewStage === 'process' ? '驳åï¼éæ°åæ£' : '驳åï¼éæ°å¤æ£' }}</a-radio> |
| | | </a-radio-group> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填åå®¡æ¹æè§" /> |
| | | <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" :placeholder="reviewApproved ? 'å®¡æ¹æè§ï¼éå¡«ï¼' : '请填å驳åæè§'" /> |
| | | </a-modal> |
| | | <a-modal v-model:open="historyOpen" :title="`æµç¨è®°å½ · ${activeRecord?.code || ''}`" :footer="null"> |
| | | <a-timeline v-if="historyActions.length"> |
| | |
| | | .filters :deep(.ant-btn .iconify) { font-size: 14px; } |
| | | .retest-row { display: grid; grid-template-columns: 60px repeat(3, 1fr); gap: 8px; align-items: center; margin-bottom: 10px; } |
| | | .retest-row :deep(.ant-input-number) { width: 100%; } |
| | | .scope-scan { display: flex; width: 100%; margin-bottom: 16px; } |
| | | .scope-scan :deep(.ant-space-item:last-child) { flex: 1; } |
| | | .review-choice { display: flex; gap: 16px; margin: 10px 0 18px; } |
| | | |
| | | /* ===== åå
æ ¼ ===== */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { message } from 'ant-design-vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { getSeedIndicatorRanges, saveSeedIndicatorRanges } from '#/api/mes/qc/seedQuality'; |
| | | |
| | | 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); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const complete = computed(() => ranges.value.every((item) => |
| | | item.minValue !== undefined && item.maxValue !== undefined && item.minValue <= item.maxValue)); |
| | | |
| | | async function load() { |
| | | 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 }; |
| | | }); |
| | | } catch (error) { |
| | | 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% çææä¸ä¸é'); |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await saveSeedIndicatorRanges(ranges.value.map((item) => ({ |
| | | indicatorCode: item.indicatorCode, |
| | | minValue: item.minValue!, maxValue: item.maxValue!, |
| | | }))); |
| | | message.success('é»è®¤åæ ¼èå´å·²ä¿åï¼æ£éªæ¶ä»å¯æå®é
æ
åµè°æ´'); |
| | | await load(); |
| | | } catch (error) { |
| | | message.error((error as Error).message || 'ä¿ååæ ¼èå´å¤±è´¥'); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(load); |
| | | </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> |
| | | <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> |
| | | </a-card> |
| | | </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; } } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { getSeedQualityPage, type SeedQualityRecord } from '#/api/mes/qc/seedQuality'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { getSeedIndicatorRanges, getSeedQualityPage, resolveSeedRetestScope, submitSeedRetestScope, type SeedQualityRecord, type SeedRetestScope } from '#/api/mes/qc/seedQuality'; |
| | | import { errorText } from '#/mobile/api'; |
| | | import { extractCode } from '#/mobile/access'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref(''); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const canRetest = computed(() => hasAccessByCodes(['mes:qc-seed-quality:retest'])); |
| | | const scanType = ref<'PALLET' | 'BATCH'>('PALLET'), scope = ref<SeedRetestScope>(); |
| | | const workflowStatus = ref<10 | 50>(10), reason = ref(''), notice = 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 targetIds = computed(() => workflowStatus.value === 10 ? scope.value?.retestIds || [] : scope.value?.initialRecheckIds || []); |
| | | const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => v.trim() !== '' && Number.isFinite(Number(v))) |
| | | && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && 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))); |
| | | async function identifyRetest(raw: string) { |
| | | if (busy.value) return; |
| | | busy.value = true; error.value = ''; notice.value = ''; scope.value = undefined; |
| | | try { |
| | | const data = await resolveSeedRetestScope(extractCode(raw), scanType.value); |
| | | if (!data.retestIds.length && !data.initialRecheckIds.length) throw new Error('å½åæçææ¹æ¬¡æ²¡æå¾
夿£æå¾
鿰忣çè®°å½'); |
| | | scope.value = data; |
| | | workflowStatus.value = data.retestIds.length ? 10 : 50; |
| | | 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 { /* 坿å¨å¡«åæ¬æ¬¡èå´ */ } |
| | | } catch (e) { error.value = errorText(e); } finally { busy.value = false; } |
| | | } |
| | | async function submitRetest() { |
| | | if (busy.value || !scope.value || !targetIds.value.length || !complete.value) return; |
| | | if (!passed.value && !reason.value.trim()) { error.value = 'æ£éªä¸åæ ¼è¯·å¡«ååå '; return; } |
| | | const count = targetIds.value.length; |
| | | if (!window.confirm(`ç¡®è®¤å°æ¬æ¬¡ææ ç¨äº ${count} è¢${workflowStatus.value === 10 ? '夿£' : '鿰忣'}ï¼`)) return; |
| | | busy.value = true; error.value = ''; |
| | | try { |
| | | const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, workflowStatus: workflowStatus.value, |
| | | qualityIds: targetIds.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) })) }); |
| | | notice.value = `已宿 ${result.processedCount} è¢æ£éªï¼çå¾
åç»å®¡æ ¸æä¸åæ ¼å¤çã`; |
| | | scope.value = undefined; |
| | | } catch (e) { error.value = errorText(e) + 'ãè¯·éæ°æ«ç æ ¸å¯¹æ¸
åååæäº¤ã'; scope.value = undefined; } |
| | | finally { busy.value = false; if (notice.value) void load(); } |
| | | } |
| | | const code = ref(''), batchCode = ref(''), result = ref(''), page = ref(0), total = ref(0); |
| | | const statusNames: Record<number, string> = { 10: 'å¾
夿£', 20: 'å¾
åæ£å®¡æ ¸', 25: 'å¾
忣ä¸åæ ¼å¤ç', 30: 'å¾
夿£å®¡æ ¸', 40: 'å·²æ¾è¡', 50: 'å¾
鿰忣', 60: 'å¾
夿£ä¸åæ ¼å¤ç', 70: 'å·²æ¥åº' }; |
| | | let filters: { code?: string; batchCode?: string; result?: number } = {}; |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | |
| | | onMounted(() => load()); |
| | | </script> |
| | | <template> |
| | | <section v-if="canRetest" class="m-panel"> |
| | | <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"><option value="PALLET">æçç </option><option value="BATCH">çäº§æ¹æ¬¡ç </option></select></label> |
| | | <Scanner :disabled="busy" @code="identifyRetest" /> |
| | | <form v-if="scope" @submit.prevent="submitRetest"> |
| | | <p class="m-muted">{{scope.code}} · {{scope.batchCode || 'æªæ æ³¨æ¹æ¬¡'}} · ææè¢ {{scope.totalCount}}</p> |
| | | <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">åä¸ç»ææ å°ç¨äºæ¬é¶æ®µå
¨é¨ {{targetIds.length}} è¢ãè¯·æ ¸å¯¹æ½æ ·ç»ææ¯å¦éç¨ã</p> |
| | | <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 /></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> |
| | | </fieldset> |
| | | <p class="m-muted">ä¸ä¸é仿¨¡æ¿å¸¦å
¥ï¼å¯ææ¬æ¬¡æ£éªæ åè°æ´ã</p> |
| | | <label class="m-field"><span>{{complete && !passed ? 'ä¸åæ ¼åå ï¼å¿
å¡«ï¼' : 'æ£éªå¤æ³¨'}}</span><textarea v-model="reason" :required="complete && !passed" maxlength="500" rows="3" /></label> |
| | | <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{busy ? 'æ£å¨æäº¤â¦' : `æäº¤ ${targetIds.length} è¢è´¨æ£`}}</button> |
| | | </form> |
| | | </section> |
| | | <p v-if="notice" class="m-success" role="status">{{notice}}</p> |
| | | <p class="m-muted">æ¥çè´¨æ£ç»æãæ¹æ¬¡ä¸ä¸åæ ¼åå ã</p> |
| | | <Scanner :disabled="busy" @code="value => { code = value; load(); }" /> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ å¹é
çè´¨æ£è®°å½</p> |
| | | <article v-for="row in rows" :key="row.id" class="m-panel"> |
| | | <strong :class="['m-status', row.result === 1 ? 'success' : row.result === 2 ? 'failed' : '']">{{row.result === 1 ? 'åæ ¼' : row.result === 2 ? 'ä¸åæ ¼' : 'å¤çä¸'}}</strong> |
| | | <dl><div class="m-row"><dt>追溯ç </dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>çäº§æ¹æ¬¡</dt><dd>{{row.batchCode || 'â'}}</dd></div><div class="m-row"><dt>æ£éªæ¶é´</dt><dd>{{row.inspectedAt || row.createTime || 'â'}}</dd></div><div v-if="row.reason" class="m-row"><dt>åå / 夿³¨</dt><dd>{{row.reason}}</dd></div></dl> |
| | | <dl><div class="m-row"><dt>追溯ç </dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>çäº§æ¹æ¬¡</dt><dd>{{row.batchCode || 'â'}}</dd></div><div class="m-row"><dt>æµç¨ç¶æ</dt><dd>{{statusNames[row.workflowStatus] || 'æªç¥ç¶æ'}}</dd></div><div class="m-row"><dt>æ£éªæ¶é´</dt><dd>{{row.inspectedAt || row.createTime || 'â'}}</dd></div><div v-if="row.reason" class="m-row"><dt>åå / 夿³¨</dt><dd>{{row.reason}}</dd></div></dl> |
| | | </article> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤'}}</button> |
| | | </template> |
| | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { onBeforeRouteLeave } from 'vue-router'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality'; |
| | | import { getSeedIndicatorRanges, 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'; |
| | |
| | | const pendingType = ref<ScanType>('BAG'), pendingIds = ref<number[]>([]); |
| | | const storageKey = 'mom-mobile-quality-' + useUserStore().userInfo?.id; |
| | | const indicators = ref([ |
| | | { code: 'purity', name: '纯度', value: '', minValue: '0', maxValue: '100' }, |
| | | { code: 'cleanliness', name: 'å度', value: '', minValue: '0', maxValue: '100' }, |
| | | { code: 'germination_rate', name: 'åè½ç', value: '', minValue: '0', maxValue: '100' }, |
| | | { code: 'moisture', name: 'æ°´å', value: '', minValue: '0', maxValue: '100' }, |
| | | { 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 passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue))); |
| | |
| | | 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 = ''); reason.value = ''; |
| | | 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 { /* 模æ¿ä¸å¯ç¨æ¶ä»å
许æå¨å¡«åæ¬æ¬¡èå´ */ } |
| | | } 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; } |
| | | } |
| | |
| | | <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> |
| | | </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-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> |
| | | </form> |
| | | </template> |
| | | <RouterLink v-if="allowedMobilePath('/mobile/quality-records')" to="/mobile/quality-records" class="m-link">æ¥çè´¨æ£è®°å½</RouterLink> |