yeg
2026-08-31 ab894b1d2351c079e080faa49b09960f1c875ce2
接入检测结果查询页面

新增历史结果筛选分页、状态提示和真实曲线展示,全部数据通过后端接口恢复。
1个文件已添加
3个文件已修改
135 ■■■■■ 已修改文件
src/api/tests.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/Sidebar.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/ResultQuery.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -126,6 +126,63 @@
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: '已终止',
src/layout/Sidebar.vue
@@ -14,7 +14,7 @@
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: '异常管理' },
  ]},
src/router/index.ts
@@ -29,6 +29,12 @@
          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'),
src/views/ResultQuery.vue
New file
@@ -0,0 +1,70 @@
<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>