接入测试预警异常页面
异常管理页面改为加载后端事实,展示来源并通过后端处理异常,移除本地删除和预置记录。
| | |
| | | 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;调用返回函数关闭连接,断线由页面决定是否重连。 */ |
| | |
| | | import { ref } from 'vue' |
| | | import { closeWarningException, getWarningExceptions, type WarningExceptionRecord } from '@/api/tests' |
| | | |
| | | export type ExceptionStatus = '待处理' | '已处理' |
| | | |
| | |
| | | testId: number |
| | | testName: string |
| | | testTime: string |
| | | source: string |
| | | exceptionItem: string |
| | | abnormalValue: string |
| | | normalValue: string |
| | |
| | | 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 } |
| | | } |
| | |
| | | <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 { |
| | |
| | | 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: '' }) |
| | |
| | | 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 |
| | | |
| | |
| | | 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('异常处理窗口打开失败') |
| | | } |
| | | } |
| | | |
| | |
| | | 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')}` |
| | |
| | | </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> |
| | |
| | | <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> |