yeg
2026-09-03 2f6f04dc89ab0aea2e9f58e695aaad8a7d29c809
接入测试预警异常页面

异常管理页面改为加载后端事实,展示来源并通过后端处理异常,移除本地删除和预置记录。
3个文件已修改
127 ■■■■■ 已修改文件
src/api/tests.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/exceptions.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Exception.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -407,6 +407,22 @@
  return getPersistedTestData<PersistedTestCycle>(testId, 'cycles')
}
export interface WarningExceptionRecord {
  exceptionId: number; testId: number; testName?: string; sourceType: string; sourceIdentity: string;
  metricCode: string; abnormalValue: number; unit: string; ruleSnapshot: string; severity: string;
  exceptionStatus: 'OPEN' | 'CLOSED'; firstOccurredAtUtc: string; lastOccurredAtUtc: string;
  occurrenceCount: number; handledByName?: string; handledAtUtc?: string; handlingNote?: string;
}
export async function getWarningExceptions(params: { testId?: number; status?: string } = {}): Promise<WarningExceptionRecord[]> {
  const query = new URLSearchParams(); if (params.testId) query.set('testId', String(params.testId)); if (params.status) query.set('status', params.status);
  const result = await request<ApiResponse & { rows?: WarningExceptionRecord[] }>(`/system/test-warning-exceptions/list?${query}`)
  return result.rows || []
}
export async function closeWarningException(exceptionId: number, handlingNote: string): Promise<WarningExceptionRecord> {
  const result = await request<ApiResponse & { data: WarningExceptionRecord }>(`/system/test-warning-exceptions/${exceptionId}/close`, { method: 'PUT', body: JSON.stringify({ handlingNote }) })
  return result.data
}
export type TestLiveEvent = 'snapshot' | 'device-status' | 'reception-status' | 'test-status' | 'exception' | 'heartbeat'
/** 使用 fetch 流读取鉴权 SSE;调用返回函数关闭连接,断线由页面决定是否重连。 */
src/stores/exceptions.ts
@@ -1,4 +1,5 @@
import { ref } from 'vue'
import { closeWarningException, getWarningExceptions, type WarningExceptionRecord } from '@/api/tests'
export type ExceptionStatus = '待处理' | '已处理'
@@ -7,6 +8,7 @@
  testId: number
  testName: string
  testTime: string
  source: string
  exceptionItem: string
  abnormalValue: string
  normalValue: string
@@ -17,55 +19,24 @@
  handleRemark: string
}
const STORAGE_KEY = 'battery_detection_exceptions_v1'
const records = ref<ExceptionRecord[]>([])
const seedRecords: ExceptionRecord[] = [
  { id: 5012, testId: 1024, testName: '高温循环性能测试', testTime: '2026-08-05 09:42', exceptionItem: '壳体温度', abnormalValue: '62.8 ℃', normalValue: '≤ 55 ℃', generatedAt: '2026-08-05 10:18', processor: '', handledAt: '-', status: '待处理', handleRemark: '' },
  { id: 5011, testId: 1023, testName: '电芯容量一致性测试', testTime: '2026-08-05 09:10', exceptionItem: '容量偏差', abnormalValue: '8.6%', normalValue: '≤ 5%', generatedAt: '2026-08-05 09:58', processor: '李工', handledAt: '2026-08-05 11:06', status: '已处理', handleRemark: '复核采样记录后确认单体容量异常,已转入复测。' },
  { id: 5010, testId: 1022, testName: '低温充放电测试', testTime: '2026-08-04 16:22', exceptionItem: '放电电压', abnormalValue: '2.41 V', normalValue: '≥ 2.50 V', generatedAt: '2026-08-04 17:04', processor: '', handledAt: '-', status: '待处理', handleRemark: '' },
  { id: 5009, testId: 1021, testName: '电芯老化寿命测试', testTime: '2026-08-04 14:30', exceptionItem: '容量保持率', abnormalValue: '77.2%', normalValue: '≥ 80%', generatedAt: '2026-08-04 15:36', processor: '王工', handledAt: '2026-08-04 16:12', status: '已处理', handleRemark: '确认达到寿命终止条件,测试已正常结束。' },
  { id: 5008, testId: 1020, testName: '3C倍率放电验证', testTime: '2026-08-04 13:14', exceptionItem: '放电温升', abnormalValue: '31.7 ℃', normalValue: '≤ 25 ℃', generatedAt: '2026-08-04 13:32', processor: '', handledAt: '-', status: '待处理', handleRemark: '' },
  { id: 5007, testId: 1019, testName: '常温容量复测', testTime: '2026-08-03 18:48', exceptionItem: '通道电流', abnormalValue: '12.8 mA', normalValue: '0 ± 5 mA', generatedAt: '2026-08-03 18:50', processor: '李工', handledAt: '2026-08-04 08:42', status: '已处理', handleRemark: '已完成电流零点补偿并复核后续数据。' },
  { id: 5006, testId: 1018, testName: '过充安全边界测试', testTime: '2026-08-03 15:05', exceptionItem: '过充电压', abnormalValue: '4.43 V', normalValue: '≤ 4.40 V', generatedAt: '2026-08-03 15:06', processor: '陈工', handledAt: '2026-08-03 15:28', status: '已处理', handleRemark: '保护装置已触发,复核为预期边界响应。' },
  { id: 5005, testId: 1017, testName: '低温启动能力验证', testTime: '2026-08-03 11:27', exceptionItem: '启动电压', abnormalValue: '8.8 V', normalValue: '≥ 9.0 V', generatedAt: '2026-08-03 11:28', processor: '', handledAt: '-', status: '待处理', handleRemark: '' },
  { id: 5004, testId: 1016, testName: '1C标准循环测试', testTime: '2026-08-02 17:20', exceptionItem: '负极电位', abnormalValue: '0.018 V', normalValue: '≥ 0.020 V', generatedAt: '2026-08-02 17:21', processor: '张工', handledAt: '2026-08-02 18:02', status: '已处理', handleRemark: '降低充电倍率后电位恢复正常,继续测试。' },
  { id: 5003, testId: 1015, testName: '高倍率温升测试', testTime: '2026-08-02 14:48', exceptionItem: '采集器温度', abnormalValue: '48.6 ℃', normalValue: '≤ 45 ℃', generatedAt: '2026-08-02 14:49', processor: '', handledAt: '-', status: '待处理', handleRemark: '' },
  { id: 5002, testId: 1014, testName: '批次容量抽检', testTime: '2026-08-01 16:35', exceptionItem: '容量下限', abnormalValue: '2864 mAh', normalValue: '≥ 2900 mAh', generatedAt: '2026-08-01 16:36', processor: '李工', handledAt: '2026-08-01 17:15', status: '已处理', handleRemark: '异常电芯已移出抽检批次并登记复测。' },
  { id: 5001, testId: 1013, testName: '短路保护响应测试', testTime: '2026-08-01 10:08', exceptionItem: '保护响应时间', abnormalValue: '126 ms', normalValue: '≤ 100 ms', generatedAt: '2026-08-01 10:09', processor: '陈工', handledAt: '2026-08-01 10:46', status: '已处理', handleRemark: '复核高速记录仪数据,确认保护响应超限。' },
]
function loadRecords() {
  try {
    const stored = localStorage.getItem(STORAGE_KEY)
    if (stored) return JSON.parse(stored) as ExceptionRecord[]
  } catch {
    localStorage.removeItem(STORAGE_KEY)
  }
  return seedRecords
}
const records = ref<ExceptionRecord[]>(loadRecords())
function persist() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(records.value))
function mapRecord(item: WarningExceptionRecord): ExceptionRecord {
  let normalValue = '-'
  try { const rule = JSON.parse(item.ruleSnapshot || '{}'); normalValue = rule.maxValue != null ? `≤ ${rule.maxValue} ${item.unit}` : rule.minValue != null ? `≥ ${rule.minValue} ${item.unit}` : '-' } catch { /* 快照不可解析时仍展示异常事实 */ }
  return { id: item.exceptionId, testId: item.testId, testName: item.testName || `测试 #${item.testId}`, testTime: item.firstOccurredAtUtc, source: `${item.sourceType}${item.sourceIdentity ? ` · ${item.sourceIdentity}` : ''}`, exceptionItem: item.metricCode, abnormalValue: `${item.abnormalValue} ${item.unit}`, normalValue, generatedAt: item.lastOccurredAtUtc, processor: item.handledByName || '', handledAt: item.handledAtUtc || '-', status: item.exceptionStatus === 'OPEN' ? '待处理' : '已处理', handleRemark: item.handlingNote || '' }
}
export function useExceptions() {
  function handleRecord(id: number, processor: string, handleRemark: string, handledAt: string) {
    const target = records.value.find(item => item.id === id)
    if (!target) return false
    target.processor = processor
    target.handleRemark = handleRemark
    target.handledAt = handledAt
    target.status = '已处理'
    persist()
  async function loadFromServer() {
    try { records.value = (await getWarningExceptions()).map(mapRecord); return true } catch { records.value = []; return false }
  }
  async function handleServerRecord(id: number, handleRemark: string) {
    const saved = await closeWarningException(id, handleRemark)
    const index = records.value.findIndex(item => item.id === id)
    if (index >= 0) records.value[index] = mapRecord(saved)
    return true
  }
  function removeRecord(id: number) {
    records.value = records.value.filter(item => item.id !== id)
    persist()
  }
  return { records, handleRecord, removeRecord }
  return { records, handleServerRecord, loadFromServer }
}
src/views/Exception.vue
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { computed, reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCurrentUser } from '@/utils/auth'
import { useExceptions, type ExceptionRecord, type ExceptionStatus } from '@/stores/exceptions'
interface FilterState {
@@ -12,7 +11,8 @@
  processor: string
}
const { records, handleRecord: saveHandledRecord, removeRecord } = useExceptions()
const { records, handleServerRecord, loadFromServer } = useExceptions()
onMounted(async () => { if (!await loadFromServer()) ElMessage.error('异常记录加载失败,请检查后端服务') })
const statuses: ExceptionStatus[] = ['待处理', '已处理']
const processors = ['张工', '李工', '周工', '王工', '赵工', '陈工', 'Admin']
const filters = reactive<FilterState>({ id: '', testName: '', dateRange: null, status: '', processor: '' })
@@ -52,11 +52,6 @@
  currentPage.value = 1
}
function formatDateTime(date = new Date()) {
  const pad = (value: number) => String(value).padStart(2, '0')
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
async function handleException(item: ExceptionRecord) {
  if (item.status === '已处理') return
@@ -69,27 +64,14 @@
      inputValidator: value => value.trim() ? true : '请输入处理说明',
      inputErrorMessage: '请输入处理说明',
    })
    const processor = getCurrentUser()?.displayName || 'Admin'
    saveHandledRecord(item.id, processor, result.value.trim(), formatDateTime())
    ElMessage.success('异常记录已处理')
  } catch {
    // 用户取消处理。
  }
}
async function deleteException(item: ExceptionRecord) {
  try {
    await ElMessageBox.confirm(`删除异常记录“#${item.id}”后无法恢复,是否继续?`, '确认删除异常记录', {
      confirmButtonText: '删除',
      cancelButtonText: '取消',
      type: 'warning',
      confirmButtonClass: 'danger-confirm',
    })
    removeRecord(item.id)
    currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value)))
    ElMessage.success('异常记录已删除')
  } catch {
    // 用户取消删除。
    try {
      await handleServerRecord(item.id, result.value.trim())
      ElMessage.success('异常记录已处理')
    } catch {
      ElMessage.error('异常处理失败,请刷新后重试')
    }
  } catch (reason) {
    if (reason !== 'cancel' && reason !== 'close') ElMessage.error('异常处理窗口打开失败')
  }
}
@@ -103,9 +85,9 @@
    return
  }
  const headers = ['异常记录ID', '测试ID', '测试名称', '测试时间', '异常项', '异常值', '正常值', '异常生成时间', '处理人', '处理时间', '状态', '处理说明']
  const headers = ['异常记录ID', '测试ID', '测试名称', '测试时间', '来源', '异常项', '异常值', '正常值', '异常生成时间', '处理人', '处理时间', '状态', '处理说明']
  const rows = filteredRecords.value.map(item => [
    item.id, item.testId, item.testName, item.testTime, item.exceptionItem, item.abnormalValue,
    item.id, item.testId, item.testName, item.testTime, item.source, item.exceptionItem, item.abnormalValue,
    item.normalValue, item.generatedAt, item.processor || '-', item.handledAt, item.status, item.handleRemark || '-',
  ])
  const csv = `\uFEFF${[headers, ...rows].map(row => row.map(escapeCsv).join(',')).join('\n')}`
@@ -146,13 +128,13 @@
      </div>
      <div class="table-wrap">
        <table>
          <thead><tr><th>异常记录ID</th><th>测试ID</th><th>测试名称</th><th>测试时间</th><th>异常项</th><th>异常值</th><th>正常值</th><th>异常生成时间</th><th>处理人</th><th>处理时间</th><th>状态</th><th class="action-column">操作</th></tr></thead>
          <thead><tr><th>异常记录ID</th><th>测试ID</th><th>测试名称</th><th>测试时间</th><th>来源</th><th>异常项</th><th>异常值</th><th>正常值</th><th>异常生成时间</th><th>处理人</th><th>处理时间</th><th>状态</th><th class="action-column">操作</th></tr></thead>
          <tbody>
            <tr v-for="item in pagedRecords" :key="item.id">
              <td class="id-cell">#{{ item.id }}</td>
              <td class="code-cell">#{{ item.testId }}</td>
              <td><strong>{{ item.testName }}</strong></td>
              <td>{{ item.testTime }}</td>
              <td>{{ item.testTime }}</td><td>{{ item.source }}</td>
              <td>{{ item.exceptionItem }}</td>
              <td class="abnormal-value">{{ item.abnormalValue }}</td>
              <td>{{ item.normalValue }}</td>
@@ -160,9 +142,9 @@
              <td>{{ item.processor || '-' }}</td>
              <td>{{ item.handledAt }}</td>
              <td><span class="status-tag" :class="item.status === '待处理' ? 'pending' : 'handled'"><i></i>{{ item.status }}</span></td>
              <td class="action-column"><div class="row-actions"><button v-if="item.status === '待处理'" type="button" @click="handleException(item)">处理</button><button class="delete-action" type="button" @click="deleteException(item)">删除</button></div></td>
              <td class="action-column"><div class="row-actions"><button v-if="item.status === '待处理'" type="button" @click="handleException(item)">处理</button></div></td>
            </tr>
            <tr v-if="!pagedRecords.length"><td colspan="12" class="empty-cell">暂无符合条件的异常记录</td></tr>
            <tr v-if="!pagedRecords.length"><td colspan="13" class="empty-cell">暂无符合条件的异常记录</td></tr>
          </tbody>
        </table>
      </div>