接入检测结果查询页面
新增历史结果筛选分页、状态提示和真实曲线展示,全部数据通过后端接口恢复。
| | |
| | | interface TestOptionsResponse extends ApiResponse { data: TestConfigurationOptions } |
| | | export interface TestListResult { total: number; rows: TestRecord[] } |
| | | |
| | | export interface HistoricalResultRecord { |
| | | resultId: number |
| | | sourceType: 'CABINET_CHANNEL' | 'COLLECTOR_CHANNEL' | string |
| | | testId: number |
| | | testName: string |
| | | testStatus: TestStatusCode |
| | | receptionStatus: ReceptionStatusCode |
| | | relationId: number |
| | | batteryNo: string |
| | | sourceId: number |
| | | sourceIdentity: string |
| | | intervalId: number |
| | | intervalStartAtUtc: string |
| | | intervalEndAtUtc: string | null |
| | | cycleNo: number | null |
| | | stepNo: number | null |
| | | stepType: string | null |
| | | workStatus: string | null |
| | | voltageV: number | null |
| | | currentA: number | null |
| | | capacityAh: number | null |
| | | energyWh: number | null |
| | | metricCode: string | null |
| | | metricValue: number | null |
| | | unit: string |
| | | unitMetadata: string |
| | | integrityConclusion: string |
| | | deviceTimeUtc: string |
| | | deviceTimeRaw: string |
| | | serverReceivedAtUtc: string |
| | | } |
| | | interface HistoricalResultResponse extends ApiResponse { total: number; rows: HistoricalResultRecord[] } |
| | | |
| | | export interface HistoricalResultQuery { |
| | | pageNum: number |
| | | pageSize: number |
| | | testId?: number |
| | | testName?: string |
| | | batteryNo?: string |
| | | sourceType?: string |
| | | sourceId?: number |
| | | sourceChannelCode?: string |
| | | beginDeviceTime?: string |
| | | endDeviceTime?: string |
| | | cycleNo?: number |
| | | stepNo?: number |
| | | } |
| | | |
| | | export async function getHistoricalResultRecords(query: HistoricalResultQuery): Promise<{ total: number; rows: HistoricalResultRecord[] }> { |
| | | const params = new URLSearchParams() |
| | | Object.entries(query).forEach(([key, value]) => { |
| | | if (value !== undefined && value !== '' && value !== null) params.set(key, String(value)) |
| | | }) |
| | | const result = await request<HistoricalResultResponse>(`/system/test/results?${params.toString()}`) |
| | | return { total: Number(result.total) || 0, rows: result.rows || [] } |
| | | } |
| | | |
| | | const statusMap: Record<TestStatusCode, TestStatus> = { |
| | | WAITING: '待测试', RUNNING: '检测中', PAUSED: '暂停', RUNNING_ABNORMAL: '检测中(异常)', |
| | | COMPLETION_PENDING: '完成确认中', COMPLETED: '检测完成', TERMINATED: '已终止', |
| | |
| | | const menuGroups = [ |
| | | { title: '首页', icon: 'HomeFilled', path: '/dashboard' }, |
| | | { title: '测试管理', icon: 'Document', children: [ |
| | | { path: '/test-list', title: '测试列表' }, { path: '/battery-group', title: '电池组管理' }, |
| | | { path: '/test-list', title: '测试列表' }, { path: '/result-query', title: '结果查询' }, { path: '/battery-group', title: '电池组管理' }, |
| | | { path: '/test-type', title: '测试类型管理' }, { path: '/data-revision', title: '数据修订记录' }, |
| | | { path: '/exception', title: '异常管理' }, |
| | | ]}, |
| | |
| | | meta: { title: '测试列表' }, |
| | | }, |
| | | { |
| | | path: 'result-query', |
| | | name: 'result-query', |
| | | component: () => import('@/views/ResultQuery.vue'), |
| | | meta: { title: '结果查询' }, |
| | | }, |
| | | { |
| | | path: 'test-list/new', |
| | | name: 'test-create', |
| | | component: () => import('@/views/TestCreate.vue'), |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { RefreshLeft, Search } from '@element-plus/icons-vue' |
| | | import { getHistoricalResultRecords, getTestList, type HistoricalResultRecord } from '@/api/tests' |
| | | |
| | | const filters = reactive({ testName: '', batteryNo: '', sourceType: '', sourceChannelCode: '', beginDeviceTime: '', endDeviceTime: '', cycleNo: null as number | null, stepNo: null as number | null }) |
| | | const applied = ref({ ...filters }) |
| | | const rows = ref<HistoricalResultRecord[]>([]) |
| | | const total = ref(0) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const loading = ref(false) |
| | | const statusTests = ref<Awaited<ReturnType<typeof getTestList>>['rows']>([]) |
| | | |
| | | async function load() { |
| | | loading.value = true |
| | | try { |
| | | const result = await getHistoricalResultRecords({ pageNum: pageNum.value, pageSize: pageSize.value, ...applied.value, cycleNo: applied.value.cycleNo || undefined, stepNo: applied.value.stepNo || undefined, beginDeviceTime: applied.value.beginDeviceTime || undefined, endDeviceTime: applied.value.endDeviceTime || undefined }) |
| | | rows.value = result.rows |
| | | total.value = result.total |
| | | } catch (error) { |
| | | rows.value = []; total.value = 0 |
| | | ElMessage.error(error instanceof Error ? error.message : '结果加载失败') |
| | | } finally { loading.value = false } |
| | | } |
| | | function search() { applied.value = { ...filters }; pageNum.value = 1; load() } |
| | | function reset() { Object.assign(filters, { testName: '', batteryNo: '', sourceType: '', sourceChannelCode: '', beginDeviceTime: '', endDeviceTime: '', cycleNo: null, stepNo: null }); search() } |
| | | function changePage(value: number) { pageNum.value = value; load() } |
| | | function changeSize(value: number) { pageSize.value = value; pageNum.value = 1; load() } |
| | | function sourceLabel(value: string) { return value === 'CABINET_CHANNEL' ? '检测柜' : value === 'COLLECTOR_CHANNEL' ? '采集器' : value } |
| | | function coreValue(row: HistoricalResultRecord) { return row.sourceType === 'CABINET_CHANNEL' ? `电压 ${row.voltageV ?? '-'} V · 电流 ${row.currentA ?? '-'} A · 容量 ${row.capacityAh ?? '-'} Ah · 能量 ${row.energyWh ?? '-'} Wh` : `${row.metricCode || '-'} ${row.metricValue ?? '-'} ${row.unit || ''}` } |
| | | async function loadStatuses() { try { const result = await getTestList({ pageNum: 1, pageSize: 1000, status: 'COMPLETION_PENDING' }); statusTests.value = result.rows } catch { statusTests.value = [] } } |
| | | function chartY(row: HistoricalResultRecord, index: number) { const value = Number(row.voltageV ?? row.metricValue ?? 0); return 105 - Math.min(90, Math.max(0, value * 12)) + index % 2 } |
| | | onMounted(() => { load(); loadStatuses() }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="result-query-page"> |
| | | <section class="filter-panel"> |
| | | <header class="section-heading"><div><i></i><strong>结果查询</strong><span>HISTORICAL RESULT QUERY</span></div><small>仅显示检测完成测试的历史原始结果</small></header> |
| | | <div class="filter-grid"> |
| | | <label><span>测试名称</span><el-input v-model="filters.testName" clearable @keyup.enter="search" /></label> |
| | | <label><span>电池号</span><el-input v-model="filters.batteryNo" clearable @keyup.enter="search" /></label> |
| | | <label><span>来源</span><el-select v-model="filters.sourceType" clearable><el-option label="检测柜" value="CABINET_CHANNEL" /><el-option label="采集器" value="COLLECTOR_CHANNEL" /></el-select></label> |
| | | <label><span>来源通道</span><el-input v-model="filters.sourceChannelCode" clearable /></label> |
| | | <label><span>开始时间</span><el-date-picker v-model="filters.beginDeviceTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></label> |
| | | <label><span>结束时间</span><el-date-picker v-model="filters.endDeviceTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></label> |
| | | <label><span>循环</span><el-input-number v-model="filters.cycleNo" :min="1" controls-position="right" /></label> |
| | | <label><span>实际工步</span><el-input-number v-model="filters.stepNo" :min="1" controls-position="right" /></label> |
| | | <div class="filter-actions"><button class="primary-button" type="button" @click="search"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="reset"><el-icon><RefreshLeft /></el-icon>重置</button></div> |
| | | </div> |
| | | </section> |
| | | <section class="result-panel"> |
| | | <div v-if="statusTests.length" class="pending-strip"><strong>处理中测试</strong><span v-for="item in statusTests" :key="item.id">{{ item.name }} · {{ item.status }}</span></div> |
| | | <div class="result-toolbar"><div><i></i><strong>历史结果</strong><span>{{ total }} RESULTS · 按设备时间排序</span></div></div> |
| | | <div v-if="rows.length" class="curve-panel"><strong>真实结果曲线</strong><svg viewBox="0 0 600 120" role="img" aria-label="历史结果曲线"><polyline :points="rows.slice(0, 24).map((row,index) => `${index * (560 / Math.max(1, Math.min(24, rows.length) - 1)) + 20},${chartY(row,index)}`).join(' ')" fill="none" stroke="#2b78a3" stroke-width="2" /></svg><small>按设备时间关联,检测柜与采集器采样频率可不同</small></div> |
| | | <div v-loading="loading" class="table-wrap"><table><thead><tr><th>测试 / 电池号</th><th>来源身份</th><th>区间边界</th><th>循环 / 实际工步</th><th>设备时间</th><th>结果值</th><th>单位</th><th>完整性</th></tr></thead><tbody> |
| | | <tr v-for="row in rows" :key="`${row.sourceType}-${row.resultId}`"><td><strong>{{ row.testName }}</strong><small>#{{ row.testId }} · {{ row.batteryNo }}</small></td><td><span class="source-tag">{{ sourceLabel(row.sourceType) }}</span><small>{{ row.sourceIdentity }}</small></td><td><small>#{{ row.intervalId }} {{ row.intervalStartAtUtc }} 至 {{ row.intervalEndAtUtc || '开放' }}</small></td><td>{{ row.cycleNo ?? '-' }} / {{ row.stepNo ?? '-' }}<small>{{ row.stepType || row.workStatus || '-' }}</small></td><td>{{ row.deviceTimeUtc }}<small>接收 {{ row.serverReceivedAtUtc }}</small></td><td>{{ coreValue(row) }}</td><td>{{ row.unit || '-' }}<small>{{ row.unitMetadata }}</small></td><td>{{ row.integrityConclusion }}</td></tr> |
| | | <tr v-if="!loading && !rows.length"><td colspan="8" class="empty-cell">暂无检测完成测试的历史结果</td></tr> |
| | | </tbody></table></div> |
| | | <footer class="pagination-bar"><span>显示 {{ rows.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="pageNum" :page-size="pageSize" :page-sizes="[10,20,50]" :total="total" layout="sizes, prev, pager, next" background @current-change="changePage" @size-change="changeSize" /></footer> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .result-query-page{width:100%}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;background:#f4f8fb;border-bottom:1px solid #d8e1e9}.section-heading{height:44px;padding:0 16px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading i,.result-toolbar i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:#26333f;font-size:15px}.section-heading span,.result-toolbar span{color:#81909d;font-size:10px}.section-heading small{color:#647380;font-size:11px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor),.filter-grid :deep(.el-input-number){width:100%}.filter-actions{display:flex;justify-content:flex-end;gap:8px}.primary-button,.secondary-button{height:34px;padding:0 13px;border:1px solid;border-radius:3px;cursor:pointer}.primary-button{color:#fff;background:#2b78a3;border-color:#2b78a3}.secondary-button{color:#52606d;background:#fff;border-color:#b8c8d6}.result-toolbar{height:50px;padding:0 16px}.table-wrap{overflow-x:auto}table{width:100%;min-width:1100px;border-collapse:collapse;color:#52606d;font-size:13px}th,td{height:50px;padding:7px 12px;border-bottom:1px solid #dde7f0;text-align:left;vertical-align:middle}th{height:40px;background:#eaf4ff;font-size:12px}td strong,td small{display:block}td small{margin-top:4px;color:#8795a3;font-size:11px}.source-tag{display:inline-block;padding:3px 6px;border:1px solid #b8d9fb;color:#176fcb;background:#edf6ff;font-size:11px}.empty-cell{height:160px;text-align:center;color:#8795a3}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;padding:8px 14px;border-top:1px solid #d8e1e9}.pagination-bar>span{color:#7b8794;font-size:11px}@media(max-width:1000px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.filter-grid{grid-template-columns:1fr}.filter-actions{justify-content:stretch}.filter-actions button{flex:1}.pagination-bar{align-items:flex-start;flex-direction:column;gap:10px}} |
| | | .pending-strip{display:flex;align-items:center;gap:14px;padding:10px 16px;color:#8a5a12;background:#fff8e9;border-bottom:1px solid #efd59e;font-size:12px}.pending-strip span{color:#a96b16}.curve-panel{padding:12px 16px;border-bottom:1px solid #d8e1e9}.curve-panel strong,.curve-panel small{display:block}.curve-panel small{margin-top:4px;color:#8795a3;font-size:11px}.curve-panel svg{width:100%;height:120px;margin-top:8px;background:#f8fbfd;border:1px solid #e1ebf2} |
| | | </style> |