From 069df25f4cf294a3fd9509bee6d631d1e699cd82 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 25 八月 2026 10:43:42 +0800
Subject: [PATCH] feat(mes): 添加种子质检功能模块
---
src/views/mes/qc/seedQuality/index.vue | 189 +++++++++++++++++++++++++++++++++++++++++++++++
src/api/mes/qc/seedQuality/index.ts | 22 +++++
2 files changed, 211 insertions(+), 0 deletions(-)
diff --git a/src/api/mes/qc/seedQuality/index.ts b/src/api/mes/qc/seedQuality/index.ts
new file mode 100644
index 0000000..fa5e1f9
--- /dev/null
+++ b/src/api/mes/qc/seedQuality/index.ts
@@ -0,0 +1,22 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesQcSeedQualityApi {
+ export interface Indicator {
+ code: string;
+ value: number;
+ minValue: number;
+ maxValue: number;
+ }
+
+ export interface SubmitParams {
+ code: string;
+ itemId: number;
+ batchCode?: string;
+ indicators: Indicator[];
+ reason?: string;
+ }
+}
+
+export function submitSeedQuality(data: MesQcSeedQualityApi.SubmitParams) {
+ return requestClient.post<number>('/mes/qc/seed-quality/submit', data);
+}
diff --git a/src/views/mes/qc/seedQuality/index.vue b/src/views/mes/qc/seedQuality/index.vue
new file mode 100644
index 0000000..18e8f24
--- /dev/null
+++ b/src/views/mes/qc/seedQuality/index.vue
@@ -0,0 +1,189 @@
+<script setup lang="ts">
+import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
+
+import { message } from 'ant-design-vue';
+import { IconifyIcon } from '@vben/icons';
+import { gsap } from 'gsap';
+
+import {
+ submitSeedQuality,
+ type MesQcSeedQualityApi,
+} from '#/api/mes/qc/seedQuality';
+
+interface IndicatorForm {
+ code: string;
+ label: string;
+ unit: string;
+ value?: number;
+ minValue?: number;
+ maxValue?: number;
+}
+
+const pageRoot = ref<HTMLElement>();
+const code = ref('');
+const itemId = ref<number>();
+const batchCode = ref('');
+const reason = ref('');
+const loading = ref(false);
+const submittedId = ref<number>();
+let animationContext: gsap.Context | undefined;
+
+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 },
+]);
+
+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 overallPassed = computed(() => allEntered.value && passedCount.value === indicators.value.length);
+
+function isPassed(item: IndicatorForm) {
+ return item.value !== undefined && item.value !== null
+ && item.minValue !== undefined && item.maxValue !== undefined
+ && item.value >= item.minValue && item.value <= item.maxValue;
+}
+
+function resetForm(clearResult = true) {
+ code.value = '';
+ itemId.value = undefined;
+ batchCode.value = '';
+ reason.value = '';
+ if (clearResult) submittedId.value = undefined;
+ indicators.value.forEach((item) => {
+ item.value = undefined;
+ item.minValue = 0;
+ item.maxValue = 100;
+ });
+}
+
+async function submit() {
+ if (!code.value.trim() || !itemId.value) {
+ message.warning('璇峰~鍐欒鐮佸拰鐗╂枡 ID');
+ return;
+ }
+ if (!allEntered.value) {
+ message.warning('璇峰厛濉啓鍥涢」鎸囨爣鐨勫疄娴嬪��');
+ return;
+ }
+ if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
+ message.warning('鎸囨爣涓嬮檺涓嶈兘澶т簬涓婇檺');
+ return;
+ }
+ if (!overallPassed.value && !reason.value.trim()) {
+ message.warning('妫�楠屼笉鍚堟牸鏃跺繀椤诲~鍐欎笉鍚堟牸鍘熷洜');
+ return;
+ }
+
+ loading.value = true;
+ try {
+ submittedId.value = await submitSeedQuality({
+ code: code.value.trim(),
+ itemId: itemId.value,
+ batchCode: batchCode.value.trim() || undefined,
+ indicators: indicators.value.map(({ code: indicatorCode, value, minValue, maxValue }) => ({
+ code: indicatorCode,
+ value: value as number,
+ minValue: minValue as number,
+ maxValue: maxValue as number,
+ })),
+ reason: reason.value.trim() || undefined,
+ });
+ message.success(`璐ㄦ鎻愪氦鎴愬姛锛岃川妫�缂栧彿锛�${submittedId.value}`);
+ 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' });
+ }
+ resetForm(false);
+ } catch (error) {
+ const response = error as { message?: string; msg?: string };
+ message.error(response.message ?? response.msg ?? '璐ㄦ鎻愪氦澶辫触锛岃妫�鏌ヨ〃鍗曞悗閲嶈瘯');
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(async () => {
+ await nextTick();
+ if (!pageRoot.value) return;
+ animationContext = gsap.context(() => {
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ gsap.from('.page-header, .summary-card, .form-card, .result-card', {
+ autoAlpha: reduceMotion ? 1 : 0,
+ y: reduceMotion ? 0 : 14,
+ duration: reduceMotion ? 0 : 0.4,
+ stagger: reduceMotion ? 0 : 0.06,
+ ease: 'power2.out',
+ });
+ }, pageRoot.value);
+});
+
+onUnmounted(() => animationContext?.revert());
+</script>
+
+<template>
+ <div ref="pageRoot" class="quality-page">
+ <header class="page-header">
+ <div>
+ <div class="eyebrow">MES / 璐ㄩ噺绠$悊</div>
+ <h1>绉嶅瓙璐ㄦ</h1>
+ <p>褰曞叆绾害銆佸噣搴︺�佸彂鑺界巼鍜屾按鍒嗭紝绯荤粺鏍规嵁鏍囧噯鑼冨洿鑷姩鍒ゅ畾妫�楠岀粨璁�</p>
+ </div>
+ <div class="header-badge"><IconifyIcon icon="ep:checked" /> 鍥涢」鎸囨爣妫�楠�</div>
+ </header>
+
+ <section class="summary-grid" aria-label="璐ㄦ鎽樿">
+ <div class="summary-card blue"><IconifyIcon icon="ep:document-checked" /><div><span>妫�楠岄」鐩�</span><strong>4</strong><small>绾害銆佸噣搴︺�佸彂鑺界巼銆佹按鍒�</small></div></div>
+ <div class="summary-card green"><IconifyIcon icon="ep:success-filled" /><div><span>褰撳墠鍚堟牸椤�</span><strong>{{ passedCount }} / 4</strong><small>瀹炴椂渚濇嵁涓婁笅闄愬垽鏂�</small></div></div>
+ <div class="summary-card" :class="overallPassed ? 'green' : allEntered ? 'red' : 'blue'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><div><span>鎬讳綋缁撹</span><strong>{{ overallPassed ? '鍚堟牸' : allEntered ? '涓嶅悎鏍�' : '寰呭綍鍏�' }}</strong><small>{{ overallPassed ? '鍙彁浜ゅ悎鏍肩粨鏋�' : allEntered ? '璇峰~鍐欎笉鍚堟牸鍘熷洜' : `宸插綍鍏� ${enteredCount} / 4 椤筦 }}</small></div></div>
+ </section>
+
+ <section class="content-grid">
+ <a-card class="form-card" :bordered="false">
+ <template #title><div class="card-title"><IconifyIcon icon="ep:edit-pen" />璐ㄦ淇℃伅</div></template>
+ <a-form layout="vertical">
+ <div class="base-grid">
+ <a-form-item label="琚嬬爜" required><a-input v-model:value="code" size="large" placeholder="璇疯緭鍏ユ垨鎵弿琚嬬爜" /></a-form-item>
+ <a-form-item label="鐗╂枡 ID" required><a-input-number v-model:value="itemId" class="full-width" size="large" :min="1" placeholder="璇疯緭鍏ョ墿鏂欑紪鍙�" /></a-form-item>
+ <a-form-item label="鐢熶骇鎵规"><a-input v-model:value="batchCode" size="large" placeholder="璇疯緭鍏ユ壒娆″彿锛堥�夊~锛�" /></a-form-item>
+ </div>
+
+ <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 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" :min="0" :precision="3" placeholder="瀹炴祴鍊�" />
+ <span class="range-separator">鑼冨洿</span>
+ <a-input-number v-model:value="item.minValue" class="range-input" :min="0" :precision="3" placeholder="涓嬮檺" />
+ <span>鈥�</span>
+ <a-input-number v-model:value="item.maxValue" class="range-input" :min="0" :precision="3" placeholder="涓婇檺" />
+ <a-tag :color="isPassed(item) ? 'success' : 'error'">{{ isPassed(item) ? '鍚堟牸' : '瓒呭嚭鑼冨洿' }}</a-tag>
+ </div>
+ </div>
+
+ <a-form-item label="涓嶅悎鏍煎師鍥�" :required="!overallPassed" class="reason-item"><a-textarea v-model:value="reason" :rows="3" :maxlength="500" show-count :placeholder="overallPassed ? '鍚堟牸缁撴灉鏃犻渶濉啓' : '璇疯鏄庝笉鍚堟牸鍘熷洜'" /></a-form-item>
+ <div class="form-actions"><a-button @click="resetForm">閲嶇疆</a-button><a-button type="primary" size="large" :loading="loading" @click="submit"><IconifyIcon icon="ep:upload-filled" />鎻愪氦璐ㄦ</a-button></div>
+ </a-form>
+ </a-card>
+
+ <a-card class="result-card" :bordered="false">
+ <template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />鍒ゅ畾璇存槑</div></template>
+ <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '褰撳墠缁撴灉鍚堟牸' : allEntered ? '褰撳墠瀛樺湪涓嶅悎鏍奸」' : '绛夊緟褰曞叆鎸囨爣' }}</strong><span>{{ allEntered ? '瀹炴祴鍊奸渶澶勪簬鏈�灏忓�间笌鏈�澶у�间箣闂�' : `宸插綍鍏� ${enteredCount} / 4 椤瑰疄娴嬪�糮 }}</span></div>
+ <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>鍥涢」鎸囨爣蹇呴』鍏ㄩ儴鎻愪氦</span></div><div><IconifyIcon icon="ep:check" /><span>杈圭晫鍊兼寜鍚堟牸澶勭悊</span></div><div><IconifyIcon icon="ep:check" /><span>涓嶅悎鏍肩粨鏋滈渶淇濈暀鍘熷洜</span></div><div><IconifyIcon icon="ep:check" /><span>璐ㄦ鍛樹笌妫�楠屾椂闂寸敱鏈嶅姟绔褰�</span></div></div>
+ <a-alert message="搴撳瓨浣嶇疆鐢卞悗鍙伴厤缃В鏋�" description="鏈〉闈㈠彧鎻愪氦琚嬬爜銆佺墿鏂欍�佹壒娆″拰妫�楠屾寚鏍囷紝涓嶅厑璁稿墠绔�夋嫨浠撳簱銆佸簱鍖烘垨搴撲綅銆�" type="info" show-icon />
+ <div v-if="submittedId" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>鏈�杩戞彁浜よ川妫�缂栧彿锛�<strong>{{ submittedId }}</strong></span></div>
+ </a-card>
+ </section>
+ </div>
+</template>
+
+<style scoped>
+.quality-page { min-height: 100%; padding: 24px; background: #f4f7fb; color: #172033; }
+.page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 22px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }.page-header h1 { margin: 6px 0; font-size: 28px; }.page-header p { margin: 0; color: #718096; }.header-badge { display: inline-flex; align-items: center; gap: 8px; padding: 9px 13px; border: 1px solid #cfe0fb; border-radius: 7px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; }
+.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }.summary-card { display: flex; align-items: flex-start; gap: 14px; padding: 17px 19px; border: 1px solid #e8edf5; border-radius: 10px; background: #fff; box-shadow: 0 3px 12px #243b5310; }.summary-card > .iconify { flex: 0 0 38px; width: 38px; height: 38px; padding: 9px; border-radius: 8px; font-size: 20px; }.summary-card.blue > .iconify { color: #1677ff; background: #eaf3ff; }.summary-card.green > .iconify { color: #17a673; background: #e6f8f0; }.summary-card.red > .iconify { color: #e05252; background: #fff0f0; }.summary-card span, .summary-card small { display: block; color: #7b8799; font-size: 12px; }.summary-card strong { display: block; margin: 4px 0; font-size: 23px; }.content-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(310px, .75fr); gap: 18px; }.form-card, .result-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 9px; font-weight: 700; }.card-title :deep(.iconify) { color: #1677ff; }.base-grid { display: grid; grid-template-columns: 1.3fr 1fr 1.2fr; gap: 16px; }.full-width { width: 100%; }
+.indicator-list { display: flex; flex-direction: column; gap: 10px; }.indicator-row { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid #e8edf5; border-radius: 7px; background: #fbfcfe; }.indicator-row.failed { border-color: #ffd6d6; background: #fffafa; }.indicator-name { display: flex; align-items: center; gap: 8px; min-width: 82px; }.indicator-name small { color: #8793a5; font-size: 12px; }.indicator-dot { width: 8px; height: 8px; border-radius: 50%; }.indicator-dot.pass { background: #20b878; }.indicator-dot.fail { background: #e05252; }.indicator-input { width: 105px; }.range-input { width: 88px; }.range-separator { margin-left: auto; color: #8793a5; font-size: 12px; }.reason-item { margin-top: 18px; }.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; }.form-actions :deep(.iconify) { margin-right: 5px; }
+.result-hero { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 8px 0 22px; padding: 25px 15px; border-radius: 8px; text-align: center; }.result-hero :deep(.iconify) { font-size: 35px; }.result-hero strong { font-size: 18px; }.result-hero span { color: #718096; font-size: 12px; }.result-hero.passed { color: #16835b; background: #effaf5; }.result-hero.failed-result { color: #c53e3e; background: #fff3f3; }.result-hero.pending-result { color: #1677ff; background: #f0f6ff; }.rule-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 22px; }.rule-list div { display: flex; align-items: center; gap: 9px; color: #59677b; font-size: 13px; }.rule-list :deep(.iconify) { color: #20b878; }.success-note { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 11px 12px; border-radius: 7px; color: #16835b; background: #effaf5; font-size: 13px; }
+@media (max-width: 1050px) { .content-grid { grid-template-columns: 1fr; }.base-grid { grid-template-columns: repeat(3, 1fr); } }
+@media (max-width: 760px) { .quality-page { padding: 14px; }.page-header { display: block; }.header-badge { margin-top: 14px; }.summary-grid, .base-grid { grid-template-columns: 1fr; }.indicator-row { flex-wrap: wrap; }.range-separator { margin-left: 0; }.indicator-input { flex: 1; min-width: 105px; }.range-input { flex: 1; min-width: 75px; } }
+</style>
--
Gitblit v1.9.3