<script setup lang="ts">
|
import { computed, onMounted, ref } from 'vue';
|
|
import type { TablePaginationConfig } from 'ant-design-vue';
|
import { IconifyIcon } from '@vben/icons';
|
|
import {
|
getSeedQualityPage,
|
type SeedQualityRecord,
|
} from '#/api/mes/qc/seedQuality';
|
|
const records = ref<SeedQualityRecord[]>([]);
|
const loading = ref(false);
|
const page = ref(1);
|
const pageSize = ref(10);
|
|
const code = ref('');
|
const batchCode = ref('');
|
const result = ref<number>();
|
|
const total = ref(0);
|
const passTotal = ref(0);
|
const failTotal = ref(0);
|
|
const processingTotal = computed(() =>
|
Math.max(total.value - passTotal.value - failTotal.value, 0),
|
);
|
|
const resultLabel = (value: number) =>
|
value === 1 ? '合格' : value === 2 ? '不合格' : '处理中';
|
const resultColor = (value: number) =>
|
value === 1 ? '#16a34a' : value === 2 ? '#dc2626' : '#d97706';
|
const resultBg = (value: number) =>
|
value === 1 ? '#ecfdf3' : value === 2 ? '#fef2f2' : '#fffbeb';
|
const formatDateTime = (value?: string) =>
|
value ? new Date(value).toLocaleString() : '—';
|
|
async function loadRecords() {
|
loading.value = true;
|
try {
|
const data = await getSeedQualityPage({
|
pageNo: page.value,
|
pageSize: pageSize.value,
|
code: code.value.trim() || undefined,
|
batchCode: batchCode.value.trim() || undefined,
|
result: result.value,
|
});
|
records.value = data.list;
|
total.value = data.total;
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function loadStats() {
|
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 }),
|
]);
|
total.value = all.total;
|
passTotal.value = pass.total;
|
failTotal.value = fail.total;
|
} catch {
|
// 统计加载失败不阻塞列表
|
}
|
}
|
|
function searchRecords() {
|
page.value = 1;
|
void loadRecords();
|
}
|
|
function resetFilters() {
|
code.value = '';
|
batchCode.value = '';
|
result.value = undefined;
|
searchRecords();
|
}
|
|
function refreshAll() {
|
void Promise.all([loadRecords(), loadStats()]);
|
}
|
|
function handleTableChange(pagination: TablePaginationConfig) {
|
page.value = pagination.current || 1;
|
pageSize.value = pagination.pageSize || 10;
|
void loadRecords();
|
}
|
|
onMounted(refreshAll);
|
</script>
|
|
<template>
|
<div class="record-page">
|
<!-- 页头 -->
|
<header class="page-header">
|
<div class="header-copy">
|
<div class="eyebrow">MES / 质量管理</div>
|
<h1>质检记录</h1>
|
<p>按袋码沉淀每一袋种子的检验结论,追踪合格、不合格与待处理结果</p>
|
</div>
|
<div class="header-badge">
|
<span class="badge-pulse" />
|
实时汇总
|
</div>
|
</header>
|
|
<!-- 结果统计 -->
|
<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>
|
<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>
|
<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>
|
<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>
|
</section>
|
|
<!-- 记录列表 -->
|
<section class="record-card">
|
<div class="card-toolbar">
|
<div class="card-title">
|
<IconifyIcon icon="ep:list" />
|
明细列表
|
<span class="count-chip">{{ total.toLocaleString() }} 条</span>
|
</div>
|
<div class="filters">
|
<a-input v-model:value="code" allow-clear placeholder="袋码" @press-enter="searchRecords">
|
<template #prefix><IconifyIcon icon="ep:box" /></template>
|
</a-input>
|
<a-input v-model:value="batchCode" allow-clear placeholder="生产批次" @press-enter="searchRecords">
|
<template #prefix><IconifyIcon icon="ep:collection-tag" /></template>
|
</a-input>
|
<a-select
|
v-model:value="result"
|
allow-clear
|
placeholder="检验结果"
|
:options="[
|
{ label: '合格', value: 1 },
|
{ label: '不合格', value: 2 },
|
{ label: '处理中', value: 0 },
|
]"
|
/>
|
<a-button type="primary" @click="searchRecords"><IconifyIcon icon="ep:search" />查询</a-button>
|
<a-button @click="resetFilters"><IconifyIcon icon="ep:refresh-left" />重置</a-button>
|
<a-tooltip title="刷新数据">
|
<a-button :loading="loading" aria-label="刷新数据" @click="refreshAll"><IconifyIcon icon="ep:refresh" /></a-button>
|
</a-tooltip>
|
</div>
|
</div>
|
|
<a-table
|
:data-source="records"
|
:loading="loading"
|
:scroll="{ x: 1120 }"
|
:pagination="{
|
current: page,
|
pageSize,
|
total,
|
showSizeChanger: true,
|
showTotal: (t: number) => `共 ${t} 条`,
|
}"
|
row-key="id"
|
size="middle"
|
@change="handleTableChange"
|
>
|
<template #emptyText>
|
<div class="empty-state">
|
<IconifyIcon icon="ep:data-board" />
|
<p>暂无质检记录</p>
|
</div>
|
</template>
|
<a-table-column key="id" title="质检编号" data-index="id" width="100" align="right">
|
<template #default="{ record }"><span class="mono muted">{{ record.id }}</span></template>
|
</a-table-column>
|
<a-table-column key="code" title="袋码" data-index="code" min-width="220">
|
<template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template>
|
</a-table-column>
|
<a-table-column key="batchCode" title="生产批次" data-index="batchCode" min-width="190">
|
<template #default="{ record }">{{ record.batchCode || '—' }}</template>
|
</a-table-column>
|
<a-table-column key="result" title="检验结果" width="130">
|
<template #default="{ record }">
|
<span class="result-tag" :style="{ color: resultColor(record.result), background: resultBg(record.result) }">
|
<i class="result-dot" :style="{ background: resultColor(record.result) }" />
|
{{ resultLabel(record.result) }}
|
</span>
|
</template>
|
</a-table-column>
|
<a-table-column key="reason" title="不合格原因" data-index="reason" min-width="220" ellipsis>
|
<template #default="{ record }">
|
<span v-if="record.reason" class="reason-text">{{ record.reason }}</span>
|
<span v-else class="muted">—</span>
|
</template>
|
</a-table-column>
|
<a-table-column key="inspectedAt" title="检验时间" width="190">
|
<template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.inspectedAt) }}</span></template>
|
</a-table-column>
|
<a-table-column key="createTime" title="提交时间" width="190">
|
<template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template>
|
</a-table-column>
|
</a-table>
|
</section>
|
</div>
|
</template>
|
|
<style scoped>
|
.record-page { min-height: 100%; padding: 20px 24px 32px; background: #f4f7fb; color: #172033; }
|
|
/* ===== 页头 ===== */
|
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
|
.header-copy .eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
|
.header-copy h1 { margin: 6px 0; font-size: 26px; line-height: 1.2; }
|
.header-copy p { margin: 0; color: #718096; }
|
.header-badge { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 1px solid #cfe0fb; border-radius: 999px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; }
|
.badge-pulse { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 0 0 rgba(34, 197, 94, .45); animation: pulse 2s infinite; }
|
@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(34, 197, 94, 0); } 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } }
|
|
/* ===== 结果统计 ===== */
|
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 18px; }
|
.stat-card { display: flex; align-items: center; gap: 13px; padding: 15px 17px; border: 1px solid #e8edf5; border-radius: 12px; background: #fff; box-shadow: 0 3px 12px #243b5310; }
|
.stat-icon { flex: 0 0 40px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 10px; font-size: 21px; }
|
.stat-icon :deep(.iconify) { font-size: 21px; }
|
.stat-main span, .stat-main small { display: block; color: #7b8799; font-size: 12px; }
|
.stat-main small { margin-top: 2px; }
|
.stat-main strong { display: block; margin: 3px 0 0; font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; }
|
.stat-card.total .stat-icon { color: #1677ff; background: #eaf3ff; }
|
.stat-card.total .stat-main strong { color: #1677ff; }
|
.stat-card.pass .stat-icon { color: #16a34a; background: #ecfdf3; }
|
.stat-card.pass .stat-main strong { color: #16a34a; }
|
.stat-card.fail .stat-icon { color: #dc2626; background: #fef2f2; }
|
.stat-card.fail .stat-main strong { color: #dc2626; }
|
.stat-card.pending .stat-icon { color: #d97706; background: #fffbeb; }
|
.stat-card.pending .stat-main strong { color: #d97706; }
|
|
/* ===== 明细卡片 ===== */
|
.record-card { padding: 6px 16px 16px; border: 1px solid #e8edf5; border-radius: 12px; background: #fff; box-shadow: 0 3px 12px #243b5310; }
|
.card-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 13px 0 15px; flex-wrap: wrap; border-bottom: 1px dashed #eef1f6; }
|
.card-title { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; }
|
.card-title :deep(.iconify) { color: #1677ff; }
|
.count-chip { padding: 1px 9px; border-radius: 999px; background: #f0f6ff; color: #1677ff; font-size: 12px; font-weight: 600; }
|
.filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
.filters .ant-input { width: 150px; }
|
.filters .ant-select { width: 128px; }
|
.filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; }
|
.filters :deep(.ant-btn .iconify) { font-size: 14px; }
|
|
/* ===== 单元格 ===== */
|
.strong { font-weight: 600; color: #172033; }
|
.muted { color: #8793a5; }
|
.mono { font-variant-numeric: tabular-nums; }
|
.result-tag { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 20px; }
|
.result-dot { width: 6px; height: 6px; border-radius: 50%; }
|
.reason-text { color: #59677b; }
|
.empty-state { padding: 34px 0; text-align: center; color: #b6c0cf; }
|
.empty-state :deep(.iconify) { font-size: 42px; color: #d5dce6; }
|
.empty-state p { margin: 10px 0 0; }
|
|
/* 状态感知行:不合格行轻高亮 */
|
:deep(.ant-table-row) { transition: background-color .18s ease; }
|
:deep(.ant-table-row:hover) { background: #f7faff !important; }
|
|
/* ===== 响应式 ===== */
|
@media (max-width: 1100px) { .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media (max-width: 760px) {
|
.record-page { padding: 14px; }
|
.page-header { display: block; }
|
.header-badge { margin-top: 12px; }
|
.stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.card-toolbar { align-items: flex-start; flex-direction: column; }
|
.filters .ant-input { width: 100%; flex: 1; }
|
}
|
</style>
|