| src/api/mes/qc/oqc/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/mes/qc/seedQuality/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mes/qc/oqc/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mes/qc/oqc/modules/form.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mes/qc/salesQuality/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mes/qc/seedQuality/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/mes/qc/seedQuality/record.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/mes/qc/oqc/index.ts
@@ -39,6 +39,7 @@ inspectorUserId?: number; // æ£æµäººåç¨æ· ID inspectorNickname?: string; // æ£æµäººåæµç§°ï¼å ³èæ¥è¯¢ï¼ status?: number; // ç¶æ workflowStatus?: number; // éå®è´¨æ£å¤æ£å审æ¹ç¶æ remark?: string; // 夿³¨ } } @@ -69,6 +70,16 @@ 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 } }); } /** å é¤åºè´§æ£éªå */ export function deleteOqc(id: number) { src/api/mes/qc/seedQuality/index.ts
@@ -6,6 +6,7 @@ code?: string; batchCode?: string; result?: number; workflowStatus?: number; } export interface SeedQualityRecord { @@ -13,11 +14,43 @@ code: string; batchCode?: string; result: number; workflowStatus: number; retestResult?: number; reason?: string; inspectorId?: number; inspectedAt?: string; createTime?: string; } export interface SeedQualityIndicator { indicatorCode: string; value: number; minValue: number; maxValue: number; } export function getSeedQualityIndicators(id: number) { return requestClient.get<SeedQualityIndicator[]>('/mes/qc/seed-quality/indicators', { params: { id } }); } export interface SeedQualityAction { id: number; action: string; result?: number; opinion?: string; indicatorsJson?: string; operatorId?: number; createTime?: string; } export function getSeedQualityActions(id: number) { return requestClient.get<SeedQualityAction[]>('/mes/qc/seed-quality/actions', { params: { id } }); } export function retestSeedQuality(id: number, data: Omit<MesQcSeedQualityApi.SubmitParams, 'qualifiedWarehouseId'>) { return requestClient.post('/mes/qc/seed-quality/retest', data, { params: { id } }); } export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) { return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data); } export function approveSeedQualityBatch(stage: 'process' | 'company', data: { ids: number[]; approved: boolean; opinion: string }) { return requestClient.post(`/mes/qc/seed-quality/${stage}-approve-batch`, data); } export namespace MesQcSeedQualityApi { export interface Indicator { src/views/mes/qc/oqc/index.vue
@@ -1,19 +1,95 @@ <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 { MesQcStatusEnum } from '@vben/constants'; import { MesBizTypeEnum, 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 { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc'; import { approveOqc, deleteOqc, exportOqc, getOqcActions, getOqcPage, retestOqc } 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, @@ -58,15 +134,23 @@ /** 导åºè¡¨æ ¼ */ async function handleExport() { const data = await exportOqc(await gridApi.formApi.getValues()); downloadFileFromBlobPart({ fileName: 'åºè´§æ£éªå.xls', source: data }); downloadFileFromBlobPart({ fileName: props.salesMode ? 'éå®è´¨æ£å.xls' : 'åºè´§æ£éªå.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: useGridColumns(), columns, height: 'auto', keepSource: true, proxyConfig: { @@ -76,6 +160,7 @@ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues, sourceDocType: props.salesMode ? MesBizTypeEnum.WM_PRODUCT_SALES : undefined, }); }, }, @@ -95,7 +180,7 @@ <template> <Page auto-content-height><FormModal @success="handleRefresh" /> <Grid table-title="åºè´§æ£éªåå表"> <Grid :table-title="props.salesMode ? 'éå®è´¨æ£åå表' : 'åºè´§æ£éªåå表'"> <template #toolbar-tools> <TableAction :actions="[ @@ -120,6 +205,9 @@ <Button type="link" @click="handleDetail(row)"> {{ row.code }} </Button> </template> <template #workflow="{ row }"> <span>{{ workflowLabels[row.workflowStatus ?? 0] || 'æªç¥ç¶æ' }}</span> </template> <template #actions="{ row }"> <TableAction @@ -153,7 +241,38 @@ }, ]" /> <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> src/views/mes/qc/oqc/modules/form.vue
@@ -105,7 +105,7 @@ return; } try { await confirm('æ¯å¦å®æåºè´§æ£éªåç¼å¶ï¼ã宿åå°ä¸è½æ´æ¹ã'); await confirm('æ¯å¦æäº¤åºè´§æ£éªç»è®ºï¼æäº¤åè¿å ¥å®¡æ¹æµç¨ï¼åæ£ä¸åæ ¼éå 夿£ã'); } catch { return; } @@ -126,7 +126,7 @@ originalSnapshot.value = current; } await finishOqc(id); message.success('宿æå'); message.success('æ£éªå·²æäº¤ï¼è¯·å¨éå®è´¨æ£èåç»§ç»å¤æ£æå®¡æ¹'); await modalApi.close(); emit('success'); } finally { src/views/mes/qc/salesQuality/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,20 @@ <script setup lang="ts"> import OqcIndex from '../oqc/index.vue'; </script> <template> <div class="sales-quality-page"> <div class="flow-notice"> <strong>éå®è´¨æ£</strong> <span>éå®åºåºå忣ãä¸åæ ¼å¤æ£ãéå®é¨ä¸å ¬å¸è´¨æ£å®¡æ¹ï¼å®¡æ¹å®æååååºåºè´¨éç¶æã</span> </div> <OqcIndex sales-mode /> </div> </template> <style scoped> .sales-quality-page { height: 100%; } .flow-notice { display: flex; align-items: center; gap: 12px; padding: 12px 20px; margin: 0 0 8px; border: 1px solid #d6e6fa; border-radius: 8px; background: #f2f7fe; color: #385071; } .flow-notice strong { flex: none; color: #1557a0; } @media (max-width: 720px) { .flow-notice { align-items: flex-start; flex-direction: column; } } </style> src/views/mes/qc/seedQuality/index.vue
@@ -54,17 +54,20 @@ const qualifiedWarehouseId = ref<number>(); const indicators = ref<IndicatorForm[]>([ { code: 'purity', label: '纯度', unit: '%', minValue: 0, maxValue: 100 }, { code: 'cleanliness', label: 'å度', unit: '%', minValue: 0, maxValue: 100 }, { code: 'germination_rate', label: 'åè½ç', unit: '%', minValue: 0, maxValue: 100 }, { code: 'moisture', label: 'æ°´å', unit: '%', minValue: 0, maxValue: 100 }, { code: 'purity', label: '纯度', unit: '%' }, { code: 'cleanliness', label: 'å度', unit: '%' }, { code: 'germination_rate', label: 'åè½ç', unit: '%' }, { code: 'moisture', label: 'æ°´å', unit: '%' }, ]); const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0); const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0); const enteredCount = computed(() => indicators.value.filter((item) => item.value !== undefined && item.value !== null).length); const passedCount = computed(() => indicators.value.filter(isPassed).length); const allEntered = computed(() => enteredCount.value === indicators.value.length); const allEntered = computed(() => indicators.value.every((item) => item.value !== undefined && item.value !== null && item.minValue !== undefined && item.minValue !== null && item.maxValue !== undefined && item.maxValue !== null)); const overallPassed = computed(() => allEntered.value && passedCount.value === indicators.value.length); const isGroupScan = computed(() => scanType.value !== 'BAG'); const qualifiedWarehouseOptions = computed(() => @@ -99,8 +102,8 @@ reason.value = ''; indicators.value.forEach((item) => { item.value = undefined; item.minValue = 0; item.maxValue = 100; item.minValue = undefined; item.maxValue = undefined; }); } @@ -163,7 +166,7 @@ return; } if (!allEntered.value) { message.warning('请å å¡«ååé¡¹ææ ç宿µå¼'); message.warning('请填ååé¡¹ææ ç宿µå¼ååæ ¼ä¸ä¸é'); return; } if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) { @@ -187,8 +190,8 @@ Modal.confirm({ title: overallPassed.value ? '确认æäº¤åæ ¼ç»æï¼' : '确认æäº¤ä¸åæ ¼ç»æï¼', content: overallPassed.value ? `æ¬æ¬¡ ${count} è¢å°ç»ä¸å¤å®åæ ¼ï¼å¹¶ä»æååºåè½¬å ¥æéåæ ¼åºã${scopeNote}` : `æ¬æ¬¡ ${count} è¢å°ä»æååºå䏿¸ é¤å¹¶æ è®°ä¸åæ ¼ï¼åæ¶ä¿çä¸åæ ¼åå ç¨äºè´¨éåæã${scopeNote}`, ? `æ¬æ¬¡ ${count} è¢å°ç»ä¸è®°å½ä¸ºåæ£åæ ¼ï¼å¾ å å·¥é¨åå ¬å¸è´¨æ£å®¡æ¹åæè½¬å ¥åæ ¼åºã${scopeNote}` : `æ¬æ¬¡ ${count} è¢å°è®°å½ä¸ºåæ£ä¸åæ ¼ï¼ä¿çæååºåå¹¶çå¾ å¤æ£ã${scopeNote}`, okText: '确认æäº¤', cancelText: 'è¿åä¿®æ¹', okButtonProps: { danger: !overallPassed.value }, @@ -216,7 +219,7 @@ })), }); submittedCount.value = result.processedCount; message.success(`æ¬æ¬¡ ${result.processedCount} è¢å·²æäº¤ï¼${passed ? 'è´¨æ£åæ ¼ï¼å·²è½¬å ¥æéåæ ¼åº' : 'è´¨æ£ä¸åæ ¼ï¼å·²è®°å½åå å¹¶ç§»é¤æååºå'}`); message.success(`æ¬æ¬¡ ${result.processedCount} è¢å·²æäº¤ï¼${passed ? 'åæ£åæ ¼ï¼å¾ 两级审æ¹' : '忣ä¸åæ ¼ï¼å¾ 夿£'}`); 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' }); } @@ -263,7 +266,7 @@ <div> <div class="eyebrow">MES / è´¨é管ç</div> <h1>ç§åè´¨æ£</h1> <p>éæ©è¢ç ãæçç ææ¹æ¬¡ç è¯å«å¾ æ£èå´ï¼å½å ¥çº¯åº¦ãå度ãåè½çåæ°´åï¼ç³»ç»èªå¨å¤å®æ£éªç»è®º</p> <p>è¯å«å¾ æ£èå´å¹¶å½å ¥åé¡¹ææ ï¼åæ ¼ç»å å·¥é¨ä¸å ¬å¸è´¨æ£å®¡æ¹åå ¥åºï¼ä¸åæ ¼å 夿£</p> </div> <div class="header-badge"><IconifyIcon icon="ep:checked" /> è¢ç / æçç / æ¹æ¬¡ç </div> </header> @@ -318,14 +321,14 @@ <a-divider orientation="left">ææ ç»æ</a-divider> <div class="indicator-list"> <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: !isPassed(item) }"> <div v-for="item in indicators" :key="item.code" class="indicator-row" :class="{ failed: allEntered && !isPassed(item) }"> <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="ä¸é" /> <span>â</span> <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="ä¸é" /> <a-tag :color="isPassed(item) ? 'success' : 'error'">{{ isPassed(item) ? 'åæ ¼' : 'è¶ åºèå´' }}</a-tag> <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? 'å¾ å½å ¥' : isPassed(item) ? 'åæ ¼' : 'è¶ åºèå´' }}</a-tag> </div> </div> @@ -338,7 +341,7 @@ <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> src/views/mes/qc/seedQuality/record.vue
@@ -3,10 +3,19 @@ import type { TablePaginationConfig } from 'ant-design-vue'; import { IconifyIcon } from '@vben/icons'; import { useAccess } from '@vben/access'; import { message } from 'ant-design-vue'; import { getSeedQualityPage, getSeedQualityIndicators, getSeedQualityActions, retestSeedQuality, approveSeedQuality, approveSeedQualityBatch, type SeedQualityRecord, type SeedQualityIndicator, type SeedQualityAction, } from '#/api/mes/qc/seedQuality'; const records = ref<SeedQualityRecord[]>([]); @@ -19,12 +28,157 @@ const result = ref<number>(); const total = ref(0); const statsTotal = ref(0); const passTotal = ref(0); const failTotal = ref(0); const processingTotal = computed(() => Math.max(total.value - passTotal.value - failTotal.value, 0), Math.max(statsTotal.value - passTotal.value - failTotal.value, 0), ); const { hasAccessByCodes } = useAccess(); const workflowLabels: Record<number, string> = { 10: 'å¾ å¤æ£', 20: 'å¾ å å·¥é¨å®¡æ¹', 30: 'å¾ å ¬å¸è´¨æ£å®¡æ¹', 40: 'å·²æ¾è¡', 50: 'å¾ å å·¥é¨ä¸åæ ¼è¯å®¡', 60: 'å¾ å ¬å¸ä¸åæ ¼å®¡æ¹', 70: 'å·²æ¥åº', }; const actionLabels: Record<string, string> = { INITIAL: '忣', RETEST: '夿£', PROCESS_APPROVE: 'å å·¥é¨éè¿', PROCESS_REJECT: 'å å·¥é¨é©³å', COMPANY_APPROVE: 'å ¬å¸è´¨æ£éè¿', COMPANY_REJECT: 'å ¬å¸è´¨æ£é©³å', }; const indicatorLabels: Record<string, string> = { purity: '纯度', cleanliness: 'å度', germination_rate: 'åè½ç', moisture: 'æ°´å', }; function actionIndicators(action: SeedQualityAction) { if (!action.indicatorsJson) return []; try { const parsed: unknown = JSON.parse(action.indicatorsJson); return Array.isArray(parsed) ? parsed as Array<{ code: string; value: number; minValue: number; maxValue: number }> : []; } catch { return []; } } const historyOpen = ref(false); const historyActions = ref<SeedQualityAction[]>([]); async function openHistory(record: SeedQualityRecord) { activeRecord.value = record; try { historyActions.value = await getSeedQualityActions(record.id); historyOpen.value = true; } catch (error) { message.error((error as Error).message || 'è¯»åæµç¨è®°å½å¤±è´¥'); } } const activeRecord = ref<SeedQualityRecord>(); const retestOpen = ref(false); const retestBusy = ref(false); const retestIndicators = ref<SeedQualityIndicator[]>([]); const retestReason = ref(''); const reviewOpen = ref(false); const reviewBusy = ref(false); const reviewStage = ref<'process' | 'company'>('process'); const reviewApproved = ref(true); const reviewOpinion = ref(''); const reviewIds = ref<number[]>([]); const selectedIds = ref<number[]>([]); const selectedRows = computed(() => records.value.filter((record) => selectedIds.value.includes(record.id))); 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) => [30, 60].includes(record.workflowStatus))) return 'company'; return undefined; }); async function openRetest(record: SeedQualityRecord) { activeRecord.value = record; try { retestIndicators.value = await getSeedQualityIndicators(record.id); if (retestIndicators.value.length !== 4) throw new Error('è´¨æ£ææ ä¸å®æ´'); retestReason.value = ''; retestOpen.value = true; } catch (error) { message.error((error as Error).message || '读åè´¨æ£ææ 失败'); } } async function saveRetest() { const record = activeRecord.value; if (!record) return; 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('请填å宿´ç宿µå¼åææä¸ä¸é'); 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; } retestBusy.value = true; try { await retestSeedQuality(record.id, { code: record.code, 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 ? '夿£åæ ¼ï¼å¾ å å·¥é¨å®¡æ¹' : '夿£ä¸åæ ¼ï¼å¾ å å·¥é¨è¯å®¡'); retestOpen.value = false; refreshAll(); } catch (error) { message.error((error as Error).message || '夿£æäº¤å¤±è´¥'); } finally { retestBusy.value = false; } } function openReview(record: SeedQualityRecord, stage: 'process' | 'company') { activeRecord.value = record; reviewIds.value = [record.id]; reviewStage.value = stage; reviewApproved.value = true; reviewOpinion.value = ''; reviewOpen.value = true; } function openBatchReview() { const stage = selectableStage.value; if (!stage) return; activeRecord.value = selectedRows.value[0]; reviewIds.value = selectedRows.value.map((row) => row.id); reviewStage.value = stage; reviewApproved.value = true; reviewOpinion.value = ''; reviewOpen.value = true; } async function saveReview() { if (!reviewIds.value.length) return; if (!reviewOpinion.value.trim()) { message.warning('请填åå®¡æ¹æè§'); return; } reviewBusy.value = true; try { if (reviewIds.value.length === 1) { await approveSeedQuality(reviewStage.value, { id: reviewIds.value[0]!, approved: reviewApproved.value, opinion: reviewOpinion.value.trim(), }); } else { await approveSeedQualityBatch(reviewStage.value, { ids: reviewIds.value, approved: reviewApproved.value, opinion: reviewOpinion.value.trim(), }); } message.success(`已宿 ${reviewIds.value.length} æ¡å®¡æ¹`); reviewOpen.value = false; selectedIds.value = []; refreshAll(); } catch (error) { message.error((error as Error).message || '审æ¹å¤±è´¥'); } finally { reviewBusy.value = false; } } const resultLabel = (value: number) => value === 1 ? 'åæ ¼' : value === 2 ? 'ä¸åæ ¼' : 'å¤çä¸'; @@ -46,6 +200,7 @@ result: result.value, }); records.value = data.list; selectedIds.value = []; total.value = data.total; } finally { loading.value = false; @@ -56,10 +211,10 @@ try { const [all, pass, fail] = await Promise.all([ getSeedQualityPage({ pageNo: 1, pageSize: 1 }), getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 1 }), getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 2 }), getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 40 }), getSeedQualityPage({ pageNo: 1, pageSize: 1, workflowStatus: 70 }), ]); total.value = all.total; statsTotal.value = all.total; passTotal.value = pass.total; failTotal.value = fail.total; } catch { @@ -99,7 +254,7 @@ <div class="header-copy"> <div class="eyebrow">MES / è´¨é管ç</div> <h1>è´¨æ£è®°å½</h1> <p>æè¢ç æ²æ·æ¯ä¸è¢ç§åçæ£éªç»è®ºï¼è¿½è¸ªåæ ¼ãä¸åæ ¼ä¸å¾ å¤çç»æ</p> <p>è·è¸ªåæ£ã夿£ãå å·¥é¨å®¡æ¹åå ¬å¸è´¨æ£å®¡æ¹ï¼å®æå®¡æ¹åææµè½¬åºå</p> </div> <div class="header-badge"> <span class="badge-pulse" /> @@ -111,19 +266,19 @@ <section class="stat-grid" aria-label="è´¨æ£ç»ææ±æ»"> <div class="stat-card total"> <div class="stat-icon"><IconifyIcon icon="ep:document" /></div> <div class="stat-main"><span>è®°å½æ»æ°</span><strong>{{ total.toLocaleString() }}</strong><small>æè¢ç 计</small></div> <div class="stat-main"><span>è®°å½æ»æ°</span><strong>{{ statsTotal.toLocaleString() }}</strong><small>æè¢ç 计</small></div> </div> <div class="stat-card pass"> <div class="stat-icon"><IconifyIcon icon="ep:success-filled" /></div> <div class="stat-main"><span>è´¨æ£åæ ¼</span><strong>{{ passTotal.toLocaleString() }}</strong><small>坿£å¸¸å ¥åæ ¼åº</small></div> <div class="stat-main"><span>å·²æ¾è¡</span><strong>{{ passTotal.toLocaleString() }}</strong><small>å·²è½¬å ¥åæ ¼åº</small></div> </div> <div class="stat-card fail"> <div class="stat-icon"><IconifyIcon icon="ep:warning-filled" /></div> <div class="stat-main"><span>è´¨æ£ä¸åæ ¼</span><strong>{{ failTotal.toLocaleString() }}</strong><small>å·²æ è®°æ¥åº</small></div> <div class="stat-main"><span>å·²æ¥åº</span><strong>{{ failTotal.toLocaleString() }}</strong><small>审æ¹å®æåå¤ç½®</small></div> </div> <div class="stat-card pending"> <div class="stat-icon"><IconifyIcon icon="ep:clock" /></div> <div class="stat-main"><span>å¤çä¸</span><strong>{{ processingTotal.toLocaleString() }}</strong><small>å¾ å®ç»è®º</small></div> <div class="stat-main"><span>æµç¨ä¸</span><strong>{{ processingTotal.toLocaleString() }}</strong><small>夿£æå®¡æ¹ä¸</small></div> </div> </section> @@ -136,6 +291,7 @@ <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-input v-model:value="code" allow-clear placeholder="è¢ç " @press-enter="searchRecords"> <template #prefix><IconifyIcon icon="ep:box" /></template> </a-input> @@ -162,6 +318,7 @@ <a-table :data-source="records" :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }" :loading="loading" :scroll="{ x: 1120 }" :pagination="{ @@ -198,6 +355,9 @@ </span> </template> </a-table-column> <a-table-column key="workflowStatus" title="æµç¨ç¶æ" width="190"> <template #default="{ record }"><a-tag color="blue">{{ workflowLabels[record.workflowStatus] || 'æªç¥ç¶æ' }}</a-tag></template> </a-table-column> <a-table-column key="reason" title="ä¸åæ ¼åå " data-index="reason" min-width="220" ellipsis> <template #default="{ record }"> <span v-if="record.reason" class="reason-text">{{ record.reason }}</span> @@ -210,8 +370,49 @@ <a-table-column key="createTime" title="æäº¤æ¶é´" width="190"> <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template> </a-table-column> <a-table-column key="actions" title="æä½" width="260" fixed="right"> <template #default="{ record }"> <a-space> <a-button v-if="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="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> <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="ä¸é" /> </div> <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"> <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-group> <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填åå®¡æ¹æè§" /> </a-modal> <a-modal v-model:open="historyOpen" :title="`æµç¨è®°å½ · ${activeRecord?.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 class="muted"> · {{ formatDateTime(action.createTime) }} · æä½äºº {{ action.operatorId || 'â' }}</span> <div v-if="action.opinion">{{ action.opinion }}</div> <div v-for="indicator in actionIndicators(action)" :key="indicator.code" class="muted"> {{ indicatorLabels[indicator.code] || indicator.code }}ï¼{{ indicator.value }}ï¼{{ indicator.minValue }}â{{ indicator.maxValue }}ï¼ </div> </a-timeline-item> </a-timeline> <a-empty v-else description="ææ æµç¨è®°å½" /> </a-modal> </div> </template> @@ -255,6 +456,9 @@ .filters .ant-select { width: 128px; } .filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; } .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%; } .review-choice { display: flex; gap: 16px; margin: 10px 0 18px; } /* ===== åå æ ¼ ===== */ .strong { font-weight: 600; color: #172033; }