恢复实时页检测柜工步方案
实时数据页优先展示后端检测柜测试事实,保留数据列表工步映射与回归契约,避免空计划配置导致红框无数据。
| | |
| | | updatedAt: string |
| | | sources: LiveSource[] |
| | | exceptions: Array<Record<string, unknown>> |
| | | testPlan?: BtsTestSnapshot | null |
| | | } |
| | | |
| | | export interface BtsTestPlanRow { |
| | | rowType: 'STEP' | 'CYCLE' | 'END' | string |
| | | stepNo: number |
| | | stepName: string |
| | | durationSeconds: number |
| | | voltageV: number | null |
| | | executionStatus: string |
| | | cycleCount: number | null |
| | | recordCount: number | null |
| | | } |
| | | |
| | | export interface BtsTestSnapshot { |
| | | currentCycleNo: number |
| | | currentStepNo: number |
| | | plannedCycleCount: number |
| | | rows: BtsTestPlanRow[] |
| | | } |
| | | |
| | | export async function saveLogicStepMappings(testId: number, mappings: TestLogicStepMapping[]): Promise<TestConfigurationDetail> { |
| | |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import * as XLSX from 'xlsx' |
| | | import { Download, EditPen, Refresh, Search, UploadFilled, Clock, Check } from '@element-plus/icons-vue' |
| | | import { getBtsCycles, getBtsRecords, getBtsSteps, type BtsCycleRecord, type BtsRecord, type BtsStepRecord } from '@/api/tests' |
| | | import { getBtsCycles, getBtsRecords, getBtsSteps, type BtsCycleRecord, type BtsRecord, type BtsStepRecord, type TestLogicStepMapping } from '@/api/tests' |
| | | import { getConsoleDataOverrides, submitConsoleDataAction, type ConsoleDataType } from '@/api/consoleData' |
| | | |
| | | type DataTab = 'realtime' | 'step' | 'cycle' |
| | | interface ConsoleCell { no: string } |
| | | interface FilterState { batteryNo: string; dateRange: [string, string] | null; cycleNo: string; stepNo: string } |
| | | interface DataRow { id: string; recordNo: string; batteryNo: string; cycleNo: number; stepNo: number; stepName: string; sequenceId: number; recordedAt: string; absoluteTime: string; stepDuration: string; startVoltage: string; endVoltage: string; chargePeakVoltage: string; dischargeValleyVoltage: string; voltage: string; current: string; capacity: string; energy: string; chargeCapacity: string; dischargeCapacity: string; efficiency: string; chargeEnergy: string; dischargeEnergy: string } |
| | | const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; bindings?: Array<{ batteryNo: string; devid?: number | null; chlid?: number | null }> }>() |
| | | interface DataRow { id: string; recordNo: string; batteryNo: string; cycleNo: number; stepNo: number; stepName: string; actualStepLabel: string; sequenceId: number; recordedAt: string; absoluteTime: string; stepDuration: string; startVoltage: string; endVoltage: string; chargePeakVoltage: string; dischargeValleyVoltage: string; voltage: string; current: string; capacity: string; energy: string; chargeCapacity: string; dischargeCapacity: string; efficiency: string; chargeEnergy: string; dischargeEnergy: string } |
| | | const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; bindings?: Array<{ batteryNo: string; devid?: number | null; chlid?: number | null }>; logicStepMappings?: TestLogicStepMapping[] }>() |
| | | const route = useRoute(); const router = useRouter() |
| | | const requestedDataType = String(route.query.dataType || 'REALTIME') |
| | | const activeTab = ref<DataTab>(requestedDataType === 'STEP' ? 'step' : requestedDataType === 'CYCLE' ? 'cycle' : 'realtime'); const loading = ref(false); const loadError = ref(''); const rows = ref<DataRow[]>([]); const stepRows = ref<DataRow[]>([]); const cycleRows = ref<DataRow[]>([]) |
| | |
| | | ? [{ label: '工步时间', value: 'stepDuration' }, { label: '容量(Ah)', value: 'capacity' }, { label: '能量(Wh)', value: 'energy' }, { label: '起始电压(V)', value: 'startVoltage' }, { label: '终止电压(V)', value: 'endVoltage' }, { label: '充电峰值电压(V)', value: 'chargePeakVoltage' }, { label: '放电谷值电压(V)', value: 'dischargeValleyVoltage' }] |
| | | : [{ label: '充电容量(Ah)', value: 'chargeCapacity' }, { label: '放电容量(Ah)', value: 'dischargeCapacity' }, { label: '效率(%)', value: 'efficiency' }, { label: '充电能量(Wh)', value: 'chargeEnergy' }, { label: '放电能量(Wh)', value: 'dischargeEnergy' }]) |
| | | function formatTime(value: string) { return value ? value.replace('T', ' ').replace('Z', '').replace(/\.\d{3,6}$/, '') : '-' } |
| | | function emptyRow(base: Partial<DataRow>): DataRow { return { id: '', recordNo: '', batteryNo: '', cycleNo: 0, stepNo: 0, stepName: '', sequenceId: 0, recordedAt: '', absoluteTime: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargePeakVoltage: '-', dischargeValleyVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', ...base } } |
| | | function emptyRow(base: Partial<DataRow>): DataRow { return { id: '', recordNo: '', batteryNo: '', cycleNo: 0, stepNo: 0, stepName: '', actualStepLabel: '-', sequenceId: 0, recordedAt: '', absoluteTime: '', stepDuration: '-', startVoltage: '-', endVoltage: '-', chargePeakVoltage: '-', dischargeValleyVoltage: '-', voltage: '-', current: '-', capacity: '-', energy: '-', chargeCapacity: '-', dischargeCapacity: '-', efficiency: '-', chargeEnergy: '-', dischargeEnergy: '-', ...base } } |
| | | function mapRecord(record: BtsRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const timestamp = formatTime(record.deviceTime); return emptyRow({ id: `${deviceId}:${channelId}:${record.sequenceId}`, recordNo: `RD-${deviceId}-${channelId}-${String(record.sequenceId).padStart(6, '0')}`, batteryNo: batteryNo || '-', cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.sequenceId, recordedAt: timestamp, absoluteTime: timestamp, voltage: Number(record.voltageV).toFixed(4), current: Number(record.currentA).toFixed(4), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4) }) } |
| | | function mapStep(record: BtsStepRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return emptyRow({ id: `step:${deviceId}:${channelId}:${record.cycleNo}:${record.stepNo}`, recordNo: `STEP-${deviceId}-${channelId}-${record.cycleNo}-${record.stepNo}`, batteryNo, cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, stepDuration: String(record.durationSeconds), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4), startVoltage: Number(record.startVoltageV).toFixed(4), endVoltage: Number(record.endVoltageV).toFixed(4), chargePeakVoltage: record.chargePeakVoltageV == null ? '-' : Number(record.chargePeakVoltageV).toFixed(4), dischargeValleyVoltage: record.dischargeValleyVoltageV == null ? '-' : Number(record.dischargeValleyVoltageV).toFixed(4) }) } |
| | | function mapStep(record: BtsStepRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return emptyRow({ id: `step:${deviceId}:${channelId}:${record.cycleNo}:${record.stepNo}`, recordNo: `STEP-${deviceId}-${channelId}-${record.cycleNo}-${record.stepNo}`, batteryNo, cycleNo: record.cycleNo, stepNo: record.stepNo, stepName: record.stepName, actualStepLabel: `${record.stepNo} · ${record.stepName}`, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, stepDuration: String(record.durationSeconds), capacity: Number(record.capacityAh).toFixed(4), energy: Number(record.energyWh).toFixed(4), startVoltage: Number(record.startVoltageV).toFixed(4), endVoltage: Number(record.endVoltageV).toFixed(4), chargePeakVoltage: record.chargePeakVoltageV == null ? '-' : Number(record.chargePeakVoltageV).toFixed(4), dischargeValleyVoltage: record.dischargeValleyVoltageV == null ? '-' : Number(record.dischargeValleyVoltageV).toFixed(4) }) } |
| | | function mapCycle(record: BtsCycleRecord, batteryNo: string, deviceId: number, channelId: number): DataRow { const startedAt = formatTime(record.startedAt); return emptyRow({ id: `cycle:${deviceId}:${channelId}:${record.cycleNo}`, recordNo: record.recordNo, batteryNo, cycleNo: record.cycleNo, sequenceId: record.recordCount, recordedAt: startedAt, absoluteTime: startedAt, chargeCapacity: Number(record.chargeCapacityAh).toFixed(4), dischargeCapacity: Number(record.dischargeCapacityAh).toFixed(4), efficiency: Number(record.efficiency).toFixed(2), chargeEnergy: Number(record.chargeEnergyWh).toFixed(4), dischargeEnergy: Number(record.dischargeEnergyWh).toFixed(4) }) } |
| | | function applyOverrides(list: DataRow[], overrides: Awaited<ReturnType<typeof getConsoleDataOverrides>>) { const byIdentity = new Map(overrides.map(item => [item.stableIdentity, item])); const known = new Set(list.map(row => row.id)); const result = list.flatMap(row => { const override = byIdentity.get(row.id); if (!override) return [row]; if (override.deleted) return []; try { return [{ ...row, ...(override.fieldValues ? JSON.parse(override.fieldValues) : {}) }] } catch { return [row] } }); overrides.filter(item => !known.has(item.stableIdentity) && !item.deleted && item.fieldValues).forEach(item => { try { result.push(emptyRow({ ...(JSON.parse(item.fieldValues || '{}')), id: item.stableIdentity, recordNo: item.stableIdentity })) } catch { /* 忽略无法解析的历史覆盖 */ } }); return result } |
| | | async function loadRecords() { loading.value = true; loadError.value = ''; try { const bindings = (props.bindings || []).filter(item => Number.isFinite(item.devid) && Number.isFinite(item.chlid)); const batches = await Promise.all(bindings.map(async binding => { const deviceId = Number(binding.devid); const channelId = Number(binding.chlid); const [records, steps, cycles] = await Promise.all([getBtsRecords(deviceId, channelId), getBtsSteps(deviceId, channelId), getBtsCycles(deviceId, channelId)]); return { records: records.map(item => mapRecord(item, binding.batteryNo, deviceId, channelId)), steps: steps.map(item => mapStep(item, binding.batteryNo, deviceId, channelId)), cycles: cycles.map(item => mapCycle(item, binding.batteryNo, deviceId, channelId)) } })); const overrides = await Promise.all((['REALTIME', 'STEP', 'CYCLE'] as ConsoleDataType[]).map(type => getConsoleDataOverrides(props.testId, type))); rows.value = applyOverrides(batches.flatMap(item => item.records), overrides[0]).sort((a, b) => b.absoluteTime.localeCompare(a.absoluteTime) || b.sequenceId - a.sequenceId); stepRows.value = applyOverrides(batches.flatMap(item => item.steps), overrides[1]); cycleRows.value = applyOverrides(batches.flatMap(item => item.cycles), overrides[2]) } catch (error) { rows.value = []; stepRows.value = []; cycleRows.value = []; loadError.value = error instanceof Error ? error.message : '检测柜记录加载失败' } finally { loading.value = false } } |
| | | const sourceRows = computed(() => activeTab.value === 'step' ? stepRows.value : activeTab.value === 'cycle' ? cycleRows.value : rows.value) |
| | | function mappingFor(stepNo: number) { return (props.logicStepMappings || []).find(item => stepNo >= item.startStepNo && stepNo <= item.endStepNo) } |
| | | const mappedStepRows = computed(() => { |
| | | const groups = new Map<string, DataRow[]>() |
| | | stepRows.value.forEach(row => { |
| | | const mapping = mappingFor(row.stepNo) |
| | | const logicalNo = mapping?.logicalNo || row.stepNo |
| | | const logicalName = mapping?.logicalName || row.stepName |
| | | const key = `${row.batteryNo}:${row.cycleNo}:${logicalNo}` |
| | | const group = groups.get(key) || [] |
| | | group.push({ ...row, stepName: logicalName, id: `${row.id}|${logicalNo}`, recordNo: `STEP-${row.batteryNo}-${row.cycleNo}-${logicalNo}`, stepNo: logicalNo }) |
| | | groups.set(key, group) |
| | | }) |
| | | return [...groups.values()].map(group => { |
| | | group.sort((a, b) => a.sequenceId - b.sequenceId) |
| | | const first = group[0]; const last = group[group.length - 1] |
| | | const numberValues = (field: keyof DataRow) => group.map(item => Number(item[field])).filter(value => Number.isFinite(value)) |
| | | const peaks = numberValues('chargePeakVoltage'); const valleys = numberValues('dischargeValleyVoltage') |
| | | return { ...last, id: group.map(item => item.id).join('|'), stepName: first.stepName, |
| | | actualStepLabel: group.length === 1 ? first.actualStepLabel : `${group.map(item => item.actualStepLabel.split(' · ')[0]).join('-')}`, |
| | | stepDuration: String(group.reduce((sum, item) => sum + Number(item.stepDuration || 0), 0)), |
| | | startVoltage: first.startVoltage, endVoltage: last.endVoltage, |
| | | chargePeakVoltage: peaks.length ? Math.max(...peaks).toFixed(4) : '-', |
| | | dischargeValleyVoltage: valleys.length ? Math.min(...valleys).toFixed(4) : '-', } |
| | | }).sort((a, b) => a.batteryNo.localeCompare(b.batteryNo) || a.cycleNo - b.cycleNo || a.stepNo - b.stepNo) |
| | | }) |
| | | const sourceRows = computed(() => activeTab.value === 'step' ? mappedStepRows.value : activeTab.value === 'cycle' ? cycleRows.value : rows.value) |
| | | const singleRow = computed(() => Object.values(draftRows.value)[0] || null) |
| | | const batteryOptions = computed(() => [...new Set(sourceRows.value.map(row => row.batteryNo).filter(Boolean))]); const cycleOptions = computed(() => [...new Set(sourceRows.value.map(row => row.cycleNo))].sort((a, b) => a - b)); const stepOptions = computed(() => [...new Map(sourceRows.value.map(row => [row.stepNo, row.stepName])).entries()].filter(item => item[0] > 0).sort((a, b) => a[0] - b[0])) |
| | | const filteredRows = computed(() => sourceRows.value.filter(row => { const query = appliedFilters.value; if (query.batteryNo && row.batteryNo !== query.batteryNo) return false; if (query.cycleNo && row.cycleNo !== Number(query.cycleNo)) return false; if (activeTab.value !== 'cycle' && query.stepNo && row.stepNo !== Number(query.stepNo)) return false; if (query.dateRange) { const date = row.absoluteTime.slice(0, 10); if (date < query.dateRange[0] || date > query.dateRange[1]) return false } return true })) |
| | |
| | | <header class="section-heading"><div><i></i><strong>数据列表</strong><span>TEST DATA</span></div><nav class="data-tabs" aria-label="数据列表子页"><button v-for="tab in dataTabs" :key="tab.key" type="button" :class="{ active: activeTab === tab.key }" :aria-current="activeTab === tab.key ? 'page' : undefined" @click="changeTab(tab.key)">{{ tab.label }}</button></nav><small>{{ loading ? '加载中...' : `共 ${filteredRows.length} 条` }}</small></header> |
| | | <div class="filter-grid"><label><span>电池</span><el-select v-model="filters.batteryNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in batteryOptions" :key="item" :label="item" :value="item" /></el-select></label><label><span>设备时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></label><label><span>循环</span><el-select v-model="filters.cycleNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label><label><span>实际工步</span><el-select v-model="filters.stepNo" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in stepOptions" :key="item[0]" :label="`${item[0]}-${item[1]}`" :value="String(item[0])" /></el-select></label><div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><Refresh /></el-icon>重置</button></div></div> |
| | | <div class="data-toolbar"><div class="toolbar-group"><button class="primary-button" type="button" @click="goToImport"><el-icon><UploadFilled /></el-icon>导入</button><button class="primary-button" type="button" @click="downloadTemplate"><el-icon><Download /></el-icon>下载模板</button><button class="primary-button" type="button" @click="goToRevisions"><el-icon><Clock /></el-icon>修订记录</button><button v-if="!editing" class="primary-button" type="button" @click="openBatchEditor"><el-icon><EditPen /></el-icon>批量修改</button><template v-else><button class="primary-button" type="button" @click="saveBatchEdit"><el-icon><Check /></el-icon>保存修改</button><button class="secondary-button" type="button" @click="cancelBatchEdit">取消</button></template><button class="danger-button" type="button" :disabled="editing" @click="deleteSelected">删除</button></div><span v-if="loadError" class="load-error">{{ loadError }}</span><span v-else>{{ editing ? '编辑模式:仅允许修改当前页规定字段,保存后填写原因并一次提交' : `仅展示检测柜实际返回的 ${activeTab === 'realtime' ? 'record' : activeTab === 'step' ? 'step' : 'cycle'} 数据` }}</span><div class="toolbar-group"><button class="secondary-button" type="button" :disabled="loading || editing" @click="loadRecords"><el-icon><Refresh /></el-icon>刷新</button><button class="secondary-button" type="button" :disabled="editing" @click="downloadCsv"><el-icon><Download /></el-icon>下载</button></div></div> |
| | | <div class="table-wrap"><table><thead><tr v-if="activeTab === 'realtime'"><th class="selection-column"><el-checkbox :model-value="allPageSelected" :indeterminate="somePageSelected" aria-label="选择本页" @change="(checked: string | number | boolean) => togglePageSelection(Boolean(checked))" /></th><th>记录编号</th><th>电池</th><th>循环号</th><th>实际工步号</th><th>实际工步名称</th><th>记录序号</th><th>记录时间</th><th>绝对时间</th><th>电压(V)</th><th>电流(A)</th><th>容量(Ah)</th><th>能量(Wh)</th><th class="action-column">操作</th></tr><tr v-else-if="activeTab === 'step'"><th class="selection-column"></th><th>工步记录</th><th>电池</th><th>循环号</th><th>实际工步号</th><th>实际工步名称</th><th>记录数量</th><th>开始时间</th><th>结束时间</th><th>容量(Ah)</th><th>能量(Wh)</th><th class="action-column">操作</th></tr><tr v-else><th class="selection-column"></th><th>循环记录</th><th>电池</th><th>循环号</th><th>充电容量(Ah)</th><th>放电容量(Ah)</th><th>充放电效率(%)</th><th>充电能量(Wh)</th><th>放电能量(Wh)</th><th class="action-column">操作</th></tr></thead><tbody><template v-for="row in pagedRows" :key="row.id"><tr v-if="activeTab === 'realtime'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].voltage" size="small" /><span v-else>{{ row.voltage }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].current" size="small" /><span v-else>{{ row.current }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].capacity" size="small" /><span v-else>{{ row.capacity }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].energy" size="small" /><span v-else>{{ row.energy }}</span></td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr><tr v-else-if="activeTab === 'step'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].capacity" size="small" /><span v-else>{{ row.capacity }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].energy" size="small" /><span v-else>{{ row.energy }}</span></td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr><tr v-else><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" :aria-label="`选择 ${row.recordNo}`" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].chargeCapacity" size="small" /><span v-else>{{ row.chargeCapacity }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].dischargeCapacity" size="small" /><span v-else>{{ row.dischargeCapacity }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].efficiency" size="small" /><span v-else>{{ row.efficiency }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].chargeEnergy" size="small" /><span v-else>{{ row.chargeEnergy }}</span></td><td><el-input v-if="editing && draftRows[row.id]" v-model="draftRows[row.id].dischargeEnergy" size="small" /><span v-else>{{ row.dischargeEnergy }}</span></td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr></template><tr v-if="!loading && !pagedRows.length"><td :colspan="activeTab === 'realtime' ? 14 : activeTab === 'step' ? 12 : 10" class="empty-cell">{{ loadError || '暂无检测柜记录' }}</td></tr></tbody></table></div> |
| | | <div class="table-wrap"><table><thead><tr v-if="activeTab === 'realtime'"><th class="selection-column"><el-checkbox :model-value="allPageSelected" :indeterminate="somePageSelected" aria-label="选择本页" @change="(checked: string | number | boolean) => togglePageSelection(Boolean(checked))" /></th><th>记录编号</th><th>电池</th><th>循环号</th><th>实际工步号</th><th>实际工步名称</th><th>记录序号</th><th>记录时间</th><th>绝对时间</th><th>电压(V)</th><th>电流(A)</th><th>容量(Ah)</th><th>能量(Wh)</th><th class="action-column">操作</th></tr><tr v-else-if="activeTab === 'step'"><th class="selection-column"></th><th>工步记录</th><th>电池</th><th>循环号</th><th>逻辑工步</th><th>实际工步</th><th>工步时间</th><th>容量(Ah)</th><th>能量(Wh)</th><th>起始电压(V)</th><th>终止电压(V)</th><th>充电峰值(V)</th><th>放电谷值(V)</th><th class="action-column">操作</th></tr><tr v-else><th class="selection-column"></th><th>循环记录</th><th>电池</th><th>循环号</th><th>充电容量(Ah)</th><th>放电容量(Ah)</th><th>充放电效率(%)</th><th>充电能量(Wh)</th><th>放电能量(Wh)</th><th class="action-column">操作</th></tr></thead><tbody><template v-for="row in pagedRows" :key="row.id"><tr v-if="activeTab === 'realtime'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" aria-label="选择记录" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }}</td><td>{{ row.stepName }}</td><td>{{ row.sequenceId }}</td><td>{{ row.recordedAt }}</td><td>{{ row.absoluteTime }}</td><td>{{ row.voltage }}</td><td>{{ row.current }}</td><td>{{ row.capacity }}</td><td>{{ row.energy }}</td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr><tr v-else-if="activeTab === 'step'"><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" aria-label="选择记录" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.stepNo }} · {{ row.stepName }}</td><td>{{ row.actualStepLabel }}</td><td>{{ row.stepDuration }}</td><td>{{ row.capacity }}</td><td>{{ row.energy }}</td><td>{{ row.startVoltage }}</td><td>{{ row.endVoltage }}</td><td>{{ row.chargePeakVoltage }}</td><td>{{ row.dischargeValleyVoltage }}</td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr><tr v-else><td class="selection-column"><el-checkbox :model-value="selectedIds.has(row.id)" aria-label="选择记录" @change="(checked: string | number | boolean) => toggleRowSelection(row.id, Boolean(checked))" /></td><td class="record-code">{{ row.recordNo }}</td><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.chargeCapacity }}</td><td>{{ row.dischargeCapacity }}</td><td>{{ row.efficiency }}</td><td>{{ row.chargeEnergy }}</td><td>{{ row.dischargeEnergy }}</td><td class="action-column"><div class="row-actions"><button type="button" @click="openRowEditor(row)">编辑</button><button class="delete-action" type="button" @click="deleteSelected([row.id])">删除</button></div></td></tr></template><tr v-if="!loading && !pagedRows.length"><td :colspan="activeTab === 'realtime' ? 14 : activeTab === 'step' ? 14 : 10" class="empty-cell">{{ loadError || '暂无检测柜记录' }}</td></tr></tbody></table></div> |
| | | <footer class="pagination-bar"><span>显示 {{ pagedRows.length }} 条,共 {{ filteredRows.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]" :total="filteredRows.length" layout="sizes, prev, pager, next" background /></footer> |
| | | <el-dialog v-model="editorVisible" title="编辑数据" width="860px" align-center> |
| | | <template v-if="singleRow"> |
| | |
| | | import ConsoleProjectChartPanel from '@/components/ConsoleProjectChartPanel.vue' |
| | | import ConsoleDataListPanel from '@/components/ConsoleDataListPanel.vue' |
| | | import { useTests } from '@/stores/tests' |
| | | import { endTestReception, getBtsRecords, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, saveLogicStepMappings, startTestReception, type BtsRecord, type TestConfigurationDetail, type TestLiveState, type TestLogicStepMapping } from '@/api/tests' |
| | | import { endTestReception, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, saveLogicStepMappings, startTestReception, type TestConfigurationDetail, type TestLiveState, type TestLogicStepMapping } from '@/api/tests' |
| | | |
| | | type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束' |
| | | type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收' |
| | |
| | | const testStatusLabel = ref('待测试') |
| | | let closeLiveStream: (() => void) | null = null |
| | | let reconnectTimer: number | null = null |
| | | let stepRefreshTimer: number | null = null |
| | | const runtimeAttach = ref(false) |
| | | const receptionState = ref<ReceptionState>('未开始接收') |
| | | const testProfiles: Record<number, { name: string; owner: string; purpose: string; startedAt: string; batteryGroup: string; batteryCount: number; abnormal: boolean }> = { |
| | |
| | | const stepDialogVisible = ref(false) |
| | | const stepSettings = ref<TestLogicStepMapping[]>([]) |
| | | const stepDraft = ref<TestLogicStepMapping[]>([]) |
| | | interface ScopedBtsRecord extends BtsRecord { receptionIntervalId: number } |
| | | interface TestStepRow { batteryNo: string; deviceId: number; channelId: number; cycleNo: number; stepNo: number; stepName: string; durationSeconds: number; capacityAh: number; energyWh: number; startVoltageV: number; endVoltageV: number; chargePeakVoltageV: number | null; dischargeValleyVoltageV: number | null } |
| | | const stepRows = ref<TestStepRow[]>([]) |
| | | const stepLoadError = ref('') |
| | | const activeTab = ref<ConsoleTab>(route.query.tab === 'data-list' ? 'data-list' : 'realtime') |
| | | const consoleTabs: Array<{ key: ConsoleTab; label: string }> = [ |
| | | { key: 'realtime', label: '实时数据' }, |
| | |
| | | const detail = await getTestDetail(testId.value) |
| | | backendDetail.value = detail |
| | | stepSettings.value = detail.logicStepMappings || [] |
| | | await loadStepRows(detail) |
| | | stepRefreshTimer = window.setInterval(() => { if (backendDetail.value) void loadStepRows(backendDetail.value) }, 5000) |
| | | runtimeAttach.value = detail.runtimeAttach === true |
| | | if (detail.receptionStatus === 'NOT_STARTED') { |
| | | state.value = '未开始接收' |
| | |
| | | ElMessage.error(error instanceof Error ? error.message : '测试状态加载失败') |
| | | } |
| | | }) |
| | | onBeforeUnmount(() => { closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer); if (stepRefreshTimer !== null) window.clearInterval(stepRefreshTimer) }) |
| | | onBeforeUnmount(() => { closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer) }) |
| | | |
| | | const elapsedText = computed(() => { |
| | | const hours = Math.floor(elapsedSeconds.value / 3600) |
| | |
| | | const currentExceptions = computed(() => (liveState.value?.exceptions || []) |
| | | .filter(item => item.sourceType !== 'COLLECTOR' || !item.status || item.status === 'OPEN')) |
| | | |
| | | async function loadStepRows(detail: TestConfigurationDetail) { |
| | | try { |
| | | const batches = await Promise.all((detail.batteries || []).map(async battery => { |
| | | const binding = battery.binding |
| | | if (binding?.bindingId == null || !Number.isFinite(binding.devid) || !Number.isFinite(binding.chlid)) return [] |
| | | const deviceId = Number(binding.devid); const channelId = Number(binding.chlid) |
| | | const intervals = (detail.receptionIntervals || []).filter(item => item.bindingId === binding.bindingId && item.sourceType === 'CABINET_CHANNEL') |
| | | // 接收时段是测试归属边界;暂停空档和同通道其他运行记录不进入当前测试工步。 |
| | | const records = (await getBtsRecords(deviceId, channelId)).flatMap(record => { |
| | | const time = Date.parse(record.deviceTime) |
| | | const interval = intervals.find(item => time >= Date.parse(item.startBoundaryAtUtc) && (!item.endBoundaryAtUtc || time <= Date.parse(item.endBoundaryAtUtc))) |
| | | return interval ? [{ ...record, receptionIntervalId: interval.intervalId }] : [] |
| | | }) |
| | | return summarizeSteps(records, battery.batteryNo, deviceId, channelId) |
| | | })) |
| | | stepRows.value = batches.flat() |
| | | stepLoadError.value = '' |
| | | } catch (error) { |
| | | stepLoadError.value = error instanceof Error ? error.message : '工步数据加载失败' |
| | | } |
| | | } |
| | | |
| | | function summarizeSteps(records: ScopedBtsRecord[], batteryNo: string, deviceId: number, channelId: number): TestStepRow[] { |
| | | const groups = new Map<string, ScopedBtsRecord[]>() |
| | | records.forEach(record => { const key = `${record.cycleNo}:${record.stepNo}`; const group = groups.get(key) || []; group.push(record); groups.set(key, group) }) |
| | | return [...groups.values()].map(group => { |
| | | group.sort((a, b) => Date.parse(a.deviceTime) - Date.parse(b.deviceTime) || a.sequenceId - b.sequenceId) |
| | | const first = group[0]; const last = group[group.length - 1] |
| | | const charge = group.filter(item => item.currentA >= 0).map(item => item.voltageV) |
| | | const discharge = group.filter(item => item.currentA < 0).map(item => item.voltageV) |
| | | const durationSeconds = [...new Set(group.map(item => item.receptionIntervalId))].reduce((total, intervalId) => { |
| | | const scoped = group.filter(item => item.receptionIntervalId === intervalId) |
| | | return total + Math.max(0, (Date.parse(scoped[scoped.length - 1].deviceTime) - Date.parse(scoped[0].deviceTime)) / 1000) |
| | | }, 0) |
| | | return { batteryNo, deviceId, channelId, cycleNo: first.cycleNo, stepNo: first.stepNo, stepName: first.stepName, |
| | | durationSeconds, |
| | | capacityAh: last.capacityAh, energyWh: last.energyWh, startVoltageV: first.voltageV, endVoltageV: last.voltageV, |
| | | chargePeakVoltageV: charge.length ? Math.max(...charge) : null, |
| | | dischargeValleyVoltageV: discharge.length ? Math.min(...discharge) : null } |
| | | }) |
| | | } |
| | | |
| | | function formatDuration(seconds: number) { |
| | | const millis = Math.round(Number(seconds || 0) * 1000) |
| | | const h = Math.floor(millis / 3600000); const m = Math.floor((millis % 3600000) / 60000); const s = Math.floor((millis % 60000) / 1000); const ms = millis % 1000 |
| | | return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(3, '0')}` |
| | | } |
| | | function formatVoltage(value: number | null) { return value === null ? '-' : Number(value).toFixed(4) } |
| | | function mappingFor(stepNo: number) { return stepSettings.value.find(item => stepNo >= item.startStepNo && stepNo <= item.endStepNo) } |
| | | const mappedStepRows = computed(() => { |
| | | const groups = new Map<string, (typeof stepRows.value[number] & { displayNo: number; displayName: string })[]>() |
| | | stepRows.value.forEach(row => { const mapping = mappingFor(row.stepNo); const displayNo = mapping?.logicalNo || row.stepNo; const displayName = mapping?.logicalName || row.stepName; const key = `${row.batteryNo}:${row.cycleNo}:${displayNo}`; const group = groups.get(key) || []; group.push({ ...row, displayNo, displayName }); groups.set(key, group) }) |
| | | return [...groups.values()].map(group => { |
| | | group.sort((a, b) => a.stepNo - b.stepNo) |
| | | const first = group[0]; const last = group[group.length - 1] |
| | | const chargePeaks = group.map(item => item.chargePeakVoltageV).filter((value): value is number => value !== null) |
| | | const dischargeValleys = group.map(item => item.dischargeValleyVoltageV).filter((value): value is number => value !== null) |
| | | return { ...last, displayNo: first.displayNo, displayName: first.displayName, |
| | | actualStepLabel: first.stepNo === last.stepNo ? `${first.stepNo} · ${first.stepName}` : `${first.stepNo}-${last.stepNo}`, |
| | | durationSeconds: group.reduce((sum, item) => sum + Number(item.durationSeconds || 0), 0), |
| | | startVoltageV: first.startVoltageV, endVoltageV: last.endVoltageV, |
| | | chargePeakVoltageV: chargePeaks.length ? Math.max(...chargePeaks) : null, |
| | | dischargeValleyVoltageV: dischargeValleys.length ? Math.min(...dischargeValleys) : null } |
| | | }) |
| | | }) |
| | | const visibleStepRows = computed(() => [...mappedStepRows.value].sort((a, b) => a.batteryNo.localeCompare(b.batteryNo) || a.cycleNo - b.cycleNo || a.displayNo - b.displayNo)) |
| | | function openStepSettings() { stepDraft.value = stepSettings.value.map(item => ({ ...item })) ; stepDialogVisible.value = true } |
| | | function addStepMapping() { const next = stepDraft.value.length + 1; const start = stepDraft.value.length ? Math.max(...stepDraft.value.map(item => item.endStepNo)) + 1 : 1; stepDraft.value.push({ logicalNo: next, logicalName: `逻辑工步 ${next}`, startStepNo: start, endStepNo: start }) } |
| | | function removeStepMapping(index: number) { stepDraft.value.splice(index, 1); stepDraft.value.forEach((item, i) => { item.logicalNo = i + 1 }) } |
| | |
| | | try { const detail = await saveLogicStepMappings(testId.value, stepDraft.value); stepSettings.value = detail.logicStepMappings || []; backendDetail.value = detail; stepDialogVisible.value = false; ElMessage.success('工步合并规则已保存') } |
| | | catch (error) { ElMessage.error(error instanceof Error ? error.message : '工步设置保存失败') } |
| | | } |
| | | |
| | | interface PlanDisplayStep { no: number; name: string; time: string; voltage: string; state: '进行中' | '已完成' | '待执行' } |
| | | function formatDuration(seconds: number) { |
| | | const millis = Math.round(Number(seconds || 0) * 1000) |
| | | const h = Math.floor(millis / 3600000); const m = Math.floor((millis % 3600000) / 60000); const s = Math.floor((millis % 60000) / 1000); const ms = millis % 1000 |
| | | return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(3, '0')}` |
| | | } |
| | | function snapshotNumber(key: string) { |
| | | const snapshots = liveState.value?.sources.map(source => source.latestSnapshot as Record<string, unknown>) || [] |
| | | const values = snapshots.map(snapshot => snapshot[key]).filter(value => Number.isFinite(Number(value))).map(Number) |
| | | return values.length ? Math.max(...values) : undefined |
| | | } |
| | | const currentCycleNo = computed(() => liveState.value?.testPlan?.currentCycleNo |
| | | || snapshotNumber('cycleNo') || snapshotNumber('cycle') || 0) |
| | | const currentStepNo = computed(() => liveState.value?.testPlan?.currentStepNo |
| | | || snapshotNumber('stepNo') || snapshotNumber('step') || 0) |
| | | const plannedCycleCount = computed(() => liveState.value?.testPlan?.plannedCycleCount || 0) |
| | | const planRows = computed<PlanDisplayStep[]>(() => { |
| | | const detectorRows = liveState.value?.testPlan?.rows |
| | | if (detectorRows?.length) return detectorRows.map(row => ({ |
| | | no: row.stepNo, |
| | | name: row.stepName, |
| | | time: row.rowType === 'CYCLE' ? `起始工步 ${row.stepNo || '-'}` : row.rowType === 'END' ? '-' : formatDuration(row.durationSeconds), |
| | | voltage: row.rowType === 'CYCLE' ? `循环次数 ${row.cycleCount || '-'}` : row.voltageV == null ? '-' : `${Number(row.voltageV).toFixed(4)} V`, |
| | | state: (row.executionStatus || '待执行') as PlanDisplayStep['state'], |
| | | })) |
| | | return [] |
| | | }) |
| | | |
| | | const parameters = [ |
| | | ['测试名称', profile.value.name], ['创建者', '陈师傅'], ['负责人', profile.value.owner], ['测试项目', profile.value.purpose], |
| | |
| | | |
| | | <div class="console-layout"> |
| | | <main> |
| | | <section class="panel step-panel"><header class="panel-heading"><div><i></i><strong>工步数据</strong><span>STEP SUMMARY</span></div><button class="primary-button compact-button" type="button" @click="openStepSettings"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>按当前测试规则映射,覆盖全部循环</span></div><div class="step-table-wrap"><table><thead><tr><th>电池号</th><th>循环</th><th>逻辑工步</th><th>实际工步</th><th>工步时间</th><th>容量(Ah)</th><th>能量(Wh)</th><th>起始电压(V)</th><th>终止电压(V)</th><th>充电峰值(V)</th><th>放电谷值(V)</th></tr></thead><tbody><tr v-for="row in visibleStepRows" :key="`${row.deviceId}:${row.channelId}:${row.cycleNo}:${row.stepNo}`"><td><strong>{{ row.batteryNo }}</strong></td><td>{{ row.cycleNo }}</td><td>{{ row.displayNo }} · {{ row.displayName }}</td><td>{{ row.actualStepLabel }}</td><td>{{ formatDuration(row.durationSeconds) }}</td><td>{{ Number(row.capacityAh).toFixed(4) }}</td><td>{{ Number(row.energyWh).toFixed(4) }}</td><td>{{ Number(row.startVoltageV).toFixed(4) }}</td><td>{{ Number(row.endVoltageV).toFixed(4) }}</td><td>{{ formatVoltage(row.chargePeakVoltageV) }}</td><td>{{ formatVoltage(row.dischargeValleyVoltageV) }}</td></tr><tr v-if="!visibleStepRows.length"><td colspan="11" class="empty-cell">{{ stepLoadError || '暂无当前测试接收时段内的检测柜实际工步' }}</td></tr></tbody></table></div></section> |
| | | <section class="panel step-panel"><header class="panel-heading"><div><i></i><strong>项目参数</strong><span>STEP PARAMETERS</span></div><button class="primary-button compact-button" type="button" @click="openStepSettings"><el-icon><Setting /></el-icon>工步设置</button></header><div class="project-meta"><span>测试项目 <strong>{{ profile.purpose }}</strong></span><span>当前循环 <strong>{{ currentCycleNo || '-' }} / 计划 {{ plannedCycleCount || '-' }}</strong></span><span>执行状态 <strong>{{ testStatusLabel }}</strong></span></div><div class="step-table-wrap"><table><thead><tr><th>工步号</th><th>工步名称</th><th>时间(h:min:s.ms)</th><th>电压(V)</th><th>执行状态</th></tr></thead><tbody><tr v-for="(step, index) in planRows" :key="`${index}-${step.no}-${step.name}`" :class="{ active: step.state === '进行中' }"><td>{{ step.no }}</td><td><strong>{{ step.name }}</strong></td><td>{{ step.time }}</td><td>{{ step.voltage }}</td><td><span class="step-state" :class="step.state">{{ step.state }}</span></td></tr><tr v-if="!planRows.length"><td colspan="5" class="empty-cell">暂无后端检测柜工步方案</td></tr></tbody></table></div></section> |
| | | <section class="panel alarm-panel"><header class="panel-heading"><div><i></i><strong>异常警告</strong><span>ALARM</span></div></header><div v-if="currentExceptions.length" class="alarm-list"><div v-for="(item, index) in currentExceptions" :key="`${item.sourceId || item.sourceType}-${item.channelCode || index}-${index}`"><el-icon><WarningFilled /></el-icon><span><strong>{{ item.sourceType === 'COLLECTOR' ? `采集器${item.collectorName || item.collectorCode || `#${item.sourceId}`}${item.status === 'OPEN' ? '离线' : '断线已恢复'} · ${item.channelCode || '来源'}` : (item.message || '数据来源当前不可观测') }}</strong><small>{{ item.measurementName ? `${item.measurementName}受影响 · ` : '' }}{{ item.status === 'OPEN' ? '缺失区间开放' : (item.message || '当前异常') }} · {{ item.missingFrom || item.openedAt || '-' }}<template v-if="item.missingTo"> 至 {{ item.missingTo }}</template></small></span></div></div><div v-else class="alarm-empty"><el-icon><CircleCheckFilled /></el-icon>暂无异常</div></section> |
| | | <section class="panel channel-panel"><header class="panel-heading"><div><i></i><strong>通道</strong><span>CHANNEL STATUS</span></div></header><div class="channel-table-wrap"><table><thead><tr><th>电池号</th><th>测试柜</th><th>柜状态</th><th>检测通道</th><th>采集器</th><th>采集器状态</th><th>温度</th><th>正电位</th><th>负电位</th></tr></thead><tbody><tr v-for="item in channels" :key="item.no"><td><strong>{{ item.no }}</strong></td><td>{{ item.cabinet }}</td><td><span class="device-state" :class="item.cabinetState">{{ item.cabinetState }}</span></td><td>{{ item.channel }}</td><td>{{ item.collector }}</td><td><span class="device-state" :class="{ 在线: item.collectorState === '在线', 异常: item.collectorState !== '在线' }">{{ item.collectorState }}</span></td><td>{{ item.temperature }}</td><td>{{ item.positive }}</td><td>{{ item.negative }}</td></tr></tbody></table></div></section> |
| | | </main> |
| | |
| | | <ConsoleChartPanel v-if="activeTab === 'step-chart'" :cells="cells" :test-name="profile.name" :battery-group="profile.batteryGroup" :duration-minutes="5" /> |
| | | <ConsoleProjectChartPanel v-if="activeTab === 'project-chart'" :cells="cells" /> |
| | | |
| | | <ConsoleDataListPanel v-if="activeTab === 'data-list' && backendDetail" :test-id="testId" :test-name="profile.name" :cells="cells" :can-import="state === '已结束'" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, devid: item.binding?.devid, chlid: item.binding?.chlid }))" /> |
| | | <ConsoleDataListPanel v-if="activeTab === 'data-list' && backendDetail" :test-id="testId" :test-name="profile.name" :cells="cells" :can-import="state === '已结束'" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, devid: item.binding?.devid, chlid: item.binding?.chlid }))" :logic-step-mappings="backendDetail.logicStepMappings" /> |
| | | |
| | | <el-dialog v-model="stepDialogVisible" title="工步设置" width="760px" align-center><div class="step-dialog-content"><p>将连续实际工步合并为逻辑工步;循环和结束统计行不参与,原始检测柜数据保持不变。</p><div class="mapping-list"><div v-for="(item, index) in stepDraft" :key="index" class="mapping-row"><strong>逻辑工步 {{ item.logicalNo }}</strong><el-input v-model="item.logicalName" placeholder="逻辑工步名称" /><el-input-number v-model="item.startStepNo" :min="1" controls-position="right" /><span>至</span><el-input-number v-model="item.endStepNo" :min="1" controls-position="right" /><button class="secondary-button" type="button" aria-label="取消合并" @click="removeStepMapping(index)"><el-icon><Delete /></el-icon></button></div></div><button class="secondary-button" type="button" @click="addStepMapping"><el-icon><Plus /></el-icon>新增合并组</button></div><template #footer><button class="secondary-button" type="button" @click="stepDialogVisible=false">取消</button><button class="primary-button" type="button" @click="saveSteps"><el-icon><Check /></el-icon>保存规则</button></template></el-dialog> |
| | | </div> |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { readFile } from 'node:fs/promises' |
| | | |
| | | const view = await readFile(new URL('../src/views/TestConsole.vue', import.meta.url), 'utf8') |
| | | const dataList = await readFile(new URL('../src/components/ConsoleDataListPanel.vue', import.meta.url), 'utf8') |
| | | |
| | | assert.match(view, /<strong>项目参数<\/strong><span>STEP PARAMETERS<\/span>/) |
| | | assert.match(view, /testPlan\?\.rows/) |
| | | assert.match(view, /row\.rowType === 'CYCLE'/) |
| | | assert.match(view, /当前循环/) |
| | | assert.doesNotMatch(view, /<strong>工步数据<\/strong><span>STEP SUMMARY<\/span>/) |
| | | assert.doesNotMatch(view, /visibleStepRows/) |
| | | assert.match(view, /:logic-step-mappings="backendDetail\.logicStepMappings"/) |
| | | assert.match(dataList, /mappingFor\(stepNo: number\)/) |
| | | assert.match(dataList, /逻辑工步/) |
| | | assert.match(dataList, /起始电压\(V\)/) |
| | | assert.match(dataList, /放电谷值\(V\)/) |
| | | |
| | | console.log('Ticket23 实时工步方案与数据列表工步映射回归契约通过') |