| AGENTS.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/deviceAdapters.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/deviceCollectors.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/tests.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/ConsoleDataListPanel.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/stores/deviceCollectors.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/DeviceCollectorForm.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/TestConsole.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
AGENTS.md
@@ -35,5 +35,5 @@ - 修改 API 类型、状态映射或实时流时,检查加载、空数据、断线、重连、无权限和后端异常状态。 - 完成前运行 `npm run build`。若新增测试脚本,再同时运行受影响测试。 - 设备登记浏览器主接缝运行 `npm run test:e2e:ticket04`;该命令必须使用隔离端口和临时 MySQL/Redis,结束后不得遗留进程或容器。 - 设备登记浏览器主接缝运行 `npm run test:e2e:ticket04`;该命令必须使用隔离端口和临时 MySQL/Redis,结束后不得遗留进程或容器。**注意:该编排原本还会启动开发模拟器(后端 `dev-simulator` profile 与 `/dev/bts-simulator` 页面),模拟器已按 Ticket04(2026-10-08)整体拆除**,`tests/e2e/` 下依赖模拟器页面与端点的用例(含设备登记与采集器登记的旧流程)在按真实来源重写之前跑不通,见 `docs/KNOWN-GAPS.md`。 - 需要浏览器交互验证时启动 `npm run dev`,检查控制台和网络请求;完成后说明仍依赖模拟数据的页面。 src/api/deviceAdapters.ts
New file @@ -0,0 +1,34 @@ import { request, type ApiResponse } from './request' interface DeviceAdapterDto { adapterId: number adapterCode: string adapterName: string | null adapterType: string | null host: string | null connectionStatus: string | null } interface DeviceAdapterListResponse extends ApiResponse { rows: DeviceAdapterDto[] } export interface DeviceAdapterOption { id: number code: string name: string type: string connectionStatus: string } /** 供采集器登记选择「数据来源」:采集器的辅助测量由所链接来源实例提供(ADR-0064)。 */ export async function getDeviceAdapterOptions(): Promise<DeviceAdapterOption[]> { const result = await request<DeviceAdapterListResponse>('/system/deviceAdapter/list?pageNum=1&pageSize=200') return (result.rows || []).map(item => ({ id: item.adapterId, code: item.adapterCode, name: item.adapterName || item.adapterCode, type: item.adapterType || '-', connectionStatus: item.connectionStatus || '', })) } src/api/deviceCollectors.ts
@@ -1,7 +1,6 @@ import type { CollectorOnlineStatus, CollectorStatus, CollectorPayload, DeviceCollectorRecord, } from '@/stores/deviceCollectors' import { request, type ApiResponse } from './request' @@ -20,6 +19,7 @@ interface CollectorDto { collectorId: number adapterId: number | null collectorCode: string | null collectorName: string | null ipAddress: string | null @@ -43,23 +43,8 @@ data: CollectorDto } interface DiscoveredCollectorDto { deviceCode: string ip: string mac: string name: string manufacturer: string model: string channelCount: number onlineStatus: CollectorOnlineStatus } interface DiscoveredCollectorListResponse extends ApiResponse { total: number rows: DiscoveredCollectorDto[] } export interface DeviceCollectorInput { adapterId: number | null deviceCode: string ip: string mac: string @@ -73,6 +58,7 @@ interface CollectorSaveDto { collectorId?: number adapterId: number | null collectorCode: string ipAddress: string macAddress: string @@ -89,17 +75,13 @@ rows: DeviceCollectorRecord[] } export interface DiscoveredCollectorListResult { total: number rows: CollectorPayload[] } const onlineStatusMap: Record<string, CollectorOnlineStatus> = { '0': '离线', '1': '在线' } const statusMap: Record<string, CollectorStatus> = { '0': '启用', '1': '禁用' } function mapCollector(item: CollectorDto): DeviceCollectorRecord { return { id: item.collectorId, adapterId: item.adapterId ?? null, deviceCode: item.collectorCode || '-', ip: item.ipAddress || '-', mac: item.macAddress || '-', @@ -118,6 +100,7 @@ function toSaveDto(input: DeviceCollectorInput, collectorId?: number): CollectorSaveDto { return { collectorId, adapterId: input.adapterId, collectorCode: input.deviceCode.trim(), ipAddress: input.ip.trim(), macAddress: input.mac.trim(), @@ -146,28 +129,6 @@ export async function getDeviceCollector(collectorId: number): Promise<DeviceCollectorRecord> { const result = await request<CollectorDetailResponse>(`/system/collector/${collectorId}`) return mapCollector(result.data) } export async function getDiscoveredCollectorList(pageNum: number, pageSize: number): Promise<DiscoveredCollectorListResult> { const result = await request<DiscoveredCollectorListResponse>( `/system/collector/discovered/list?pageNum=${pageNum}&pageSize=${pageSize}`, ) return { total: Number(result.total) || 0, rows: (result.rows || []).map(item => ({ deviceCode: item.deviceCode, ip: item.ip, mac: item.mac, name: item.name || '', manufacturer: item.manufacturer || '', model: item.model || '', channelCount: item.channelCount || 1, onlineStatus: item.onlineStatus || '离线', relatedTests: [], status: '启用', remark: '', })), } } export function createDeviceCollector(input: DeviceCollectorInput): Promise<ApiResponse> { src/api/tests.ts
@@ -270,70 +270,6 @@ return (await request<TestLiveStateResponse>(`/system/test/${testId}/live-state`)).data } export interface BtsRecord { sequenceId: number deviceTime: string currentA: number voltageV: number capacityAh: number energyWh: number cycleNo: number stepNo: number stepName: string stepType: string } export interface BtsStepRecord { cycleNo: number stepNo: number cyclePosition: number stepName: string stepType: string recordCount: number startedAt: string endedAt: string capacityAh: number energyWh: number durationSeconds: number startVoltageV: number endVoltageV: number chargePeakVoltageV: number dischargeValleyVoltageV: number } export interface BtsCycleRecord { recordNo: string cycleNo: number recordCount: number startedAt: string endedAt: string chargeCapacityAh: number dischargeCapacityAh: number chargeEnergyWh: number dischargeEnergyWh: number efficiency: number } async function getSimulatorChannelData<T>(deviceId: number, channelId: number, kind: 'records' | 'steps' | 'cycles'): Promise<T[]> { const token = getAuthToken() const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || '/dev-api'}/dev/bts-simulator/devices/${deviceId}/channels/${channelId}/${kind}`, { headers: token ? { Authorization: `Bearer ${token}` } : undefined }) if (!response.ok) throw new Error(`检测柜${kind}数据加载失败(${response.status})`) const result = await response.json() as T[] return Array.isArray(result) ? result : [] } export function getBtsRecords(deviceId: number, channelId: number): Promise<BtsRecord[]> { return getSimulatorChannelData<BtsRecord>(deviceId, channelId, 'records') } export function getBtsSteps(deviceId: number, channelId: number): Promise<BtsStepRecord[]> { return getSimulatorChannelData<BtsStepRecord>(deviceId, channelId, 'steps') } export function getBtsCycles(deviceId: number, channelId: number): Promise<BtsCycleRecord[]> { return getSimulatorChannelData<BtsCycleRecord>(deviceId, channelId, 'cycles') } export async function saveTestConsoleDetail(testId: number, detail: TestConsoleDetail): Promise<TestConfigurationDetail> { return (await request<TestDetailResponse>(`/system/test/${testId}/console-detail`, { method: 'PUT', body: JSON.stringify(detail) })).data } src/components/ConsoleDataListPanel.vue
@@ -83,6 +83,9 @@ const CYCLE_SOURCE_UNKNOWN = '未标注' const cycleSourceTitle = '「工步层聚合」表示设备没有提供循环汇总行,该循环的容量、能量与效率由本系统按工步层同方向工步求和得出;推导结果可能带有设备工步行里的偏差。' const hasAggregatedCycles = computed(() => cycleRows.value.some(row => row.sourceKind === cycleSourceLabels.AGGREGATED)) // 本来源的工步方案里只有「步_N」标号,没有描述性名称:那里的工步名称是本系统按电流符号判定的方向(充电/放电/搁置)。 const derivedStepNames = ['充电', '放电', '搁置'] const hasDerivedStepNames = computed(() => stepRows.value.some(row => derivedStepNames.includes(String(row.stepName || '').trim()))) const filters = reactive<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }) const appliedFilters = ref<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }) const currentPage = ref(1) @@ -395,6 +398,7 @@ </header> <p v-if="activeTab === 'realtime'" class="realtime-hint">最近数据时间:{{ latestRealtimeDataAt }} · 设备约 60 秒产生一行新数据,展示刷新比它快是正常的</p> <p v-if="activeTab === 'cycle' && hasAggregatedCycles" class="realtime-hint">来源标注「工步层聚合」表示设备没有提供循环汇总行,该循环由本系统按工步层同方向工步求和得出,不是设备事实;界面与导出都带这一列。</p> <p v-if="activeTab === 'step' && hasDerivedStepNames" class="realtime-hint">工步名称「充电 / 放电 / 搁置」是本系统按电流符号判定的方向,不是设备给的名称:该来源的工步方案里只有「步_N」标号与操作类型,没有描述性名称。</p> <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 src/stores/deviceCollectors.ts
@@ -5,6 +5,7 @@ export interface DeviceCollectorRecord { id: number adapterId: number | null deviceCode: string ip: string mac: string @@ -19,4 +20,4 @@ createdAt: string } export type CollectorPayload = Pick<DeviceCollectorRecord, 'deviceCode' | 'ip' | 'mac' | 'name' | 'manufacturer' | 'model' | 'channelCount' | 'remark' | 'onlineStatus' | 'relatedTests' | 'status'> export type CollectorPayload = Pick<DeviceCollectorRecord, 'adapterId' | 'deviceCode' | 'ip' | 'mac' | 'name' | 'manufacturer' | 'model' | 'channelCount' | 'remark' | 'onlineStatus' | 'relatedTests' | 'status'> src/views/DeviceCollectorForm.vue
@@ -2,7 +2,8 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { useRoute, useRouter } from 'vue-router' import { createDeviceCollector, getDeviceCollector, getDiscoveredCollectorList, updateDeviceCollector } from '@/api/deviceCollectors' import { createDeviceCollector, getDeviceCollector, updateDeviceCollector } from '@/api/deviceCollectors' import { getDeviceAdapterOptions, type DeviceAdapterOption } from '@/api/deviceAdapters' import type { CollectorPayload } from '@/stores/deviceCollectors' import DeviceRelatedTests from '@/components/DeviceRelatedTests.vue' @@ -10,18 +11,13 @@ const router = useRouter() const loading = ref(false) const saving = ref(false) const deviceDialogVisible = ref(false) const deviceLoading = ref(false) const devicePage = ref(1) const devicePageSize = 5 const devices = ref<CollectorPayload[]>([]) const deviceTotal = ref(0) const adapterOptions = ref<DeviceAdapterOption[]>([]) const channelOptions = Array.from({ length: 99 }, (_, index) => index + 1) const editingId = computed(() => Number(route.params.id) || null) const isEditing = computed(() => editingId.value !== null) const form = reactive<CollectorPayload>({ deviceCode: '', ip: '', mac: '', name: '', manufacturer: '', model: '', channelCount: 1, adapterId: null, deviceCode: '', ip: '', mac: '', name: '', manufacturer: '', model: '', channelCount: 1, remark: '', onlineStatus: '离线', relatedTests: [], status: '启用', }) @@ -38,55 +34,27 @@ } } onMounted(loadEditingCollector) async function loadDiscoveredCollectors() { deviceLoading.value = true /** * 采集器不再有发现候选:真实来源不产出采集器候选,登记由操作员手工填写来源身份。 * 这里只加载可选的数据来源实例(采集器的辅助测量由它提供);读不到时给出可见错误, * 不能把权限拒绝或网络失败显示成"没有来源"。 */ async function loadAdapterOptions() { try { const result = await getDiscoveredCollectorList(devicePage.value, devicePageSize) devices.value = result.rows deviceTotal.value = result.total adapterOptions.value = await getDeviceAdapterOptions() } catch (error) { devices.value = [] deviceTotal.value = 0 ElMessage.error(error instanceof Error ? error.message : '可发现采集器加载失败') } finally { deviceLoading.value = false adapterOptions.value = [] ElMessage.error(error instanceof Error ? error.message : '数据来源列表加载失败') } } async function openDeviceDialog() { devicePage.value = 1 deviceDialogVisible.value = true await loadDiscoveredCollectors() } async function handleDevicePageChange(page: number) { devicePage.value = page await loadDiscoveredCollectors() } function selectDevice(device: CollectorPayload) { Object.assign(form, { deviceCode: device.deviceCode, ip: device.ip, mac: device.mac, name: device.name, manufacturer: device.manufacturer, model: device.model, channelCount: device.channelCount, onlineStatus: device.onlineStatus, relatedTests: [], }) deviceDialogVisible.value = false ElMessage.success(`已选择设备 ${device.deviceCode}`) } onMounted(async () => { await Promise.all([loadEditingCollector(), loadAdapterOptions()]) }) function validate() { if (!form.deviceCode) return '请先选择设备' if (!form.deviceCode.trim()) return '请输入设备编码' if (!form.name.trim()) return '请输入采集器名称' if (!form.manufacturer.trim()) return '请输入制造商' if (!form.model.trim()) return '请输入型号' if (!Number.isInteger(form.channelCount) || form.channelCount < 1 || form.channelCount > 99) return '请选择 1~99 的通道数' return '' } @@ -118,40 +86,25 @@ <section v-loading="loading" class="detail-panel"> <header class="panel-heading"><div><i></i><strong>采集器详情</strong><span>TEST COLLECTOR DETAILS</span></div><small><b>*</b> 为必填项</small></header> <div class="device-select-row"><button class="primary-button" type="button" @click="openDeviceDialog"><el-icon><Connection /></el-icon>选择设备</button><span>显示当前网段全部可发现设备</span></div> <div class="device-summary"> <div><span>设备编码</span><strong>{{ form.deviceCode || '-' }}</strong></div> <div><span>IP</span><strong class="mono-value">{{ form.ip || '-' }}</strong></div> <div><span>MAC</span><strong class="mono-value">{{ form.mac || '-' }}</strong></div> <div><span>在线状态</span><strong :class="form.onlineStatus === '在线' ? 'online-value' : 'offline-value'">{{ form.deviceCode ? form.onlineStatus : '-' }}</strong></div> <div><span>关联测试</span><DeviceRelatedTests :tests="form.relatedTests" /></div> </div> <div class="device-select-row"><span>采集器的测量按「同一来源的辅助测量」表达:选择它所属的数据来源,在线状态由该来源的连接结果决定。设备编码由操作员按现场叫法填写,系统不自动改写。</span></div> <div class="detail-form"> <label><span><b>*</b>设备编码</span><el-input v-model="form.deviceCode" maxlength="64" :disabled="isEditing" placeholder="输入设备编码" /></label> <label><span>所属数据来源</span><el-select v-model="form.adapterId" clearable placeholder="选择来源实例"><el-option v-for="item in adapterOptions" :key="item.id" :label="`${item.name}(${item.type})`" :value="item.id" /></el-select></label> <label><span>IP</span><el-input v-model="form.ip" maxlength="45" placeholder="可留空" /></label> <label><span>MAC</span><el-input v-model="form.mac" maxlength="17" placeholder="可留空" /></label> <label><span><b>*</b>采集器名称</span><el-input v-model="form.name" maxlength="30" show-word-limit placeholder="输入采集器名称" /></label> <label><span><b>*</b>制造商</span><el-input v-model="form.manufacturer" maxlength="30" placeholder="输入制造商" /></label> <label><span><b>*</b>型号</span><el-input v-model="form.model" maxlength="30" placeholder="输入型号" /></label> <label><span>制造商</span><el-input v-model="form.manufacturer" maxlength="30" placeholder="输入制造商" /></label> <label><span>型号</span><el-input v-model="form.model" maxlength="30" placeholder="输入型号" /></label> <label><span>通道数</span><el-select v-model="form.channelCount"><el-option v-for="item in channelOptions" :key="item" :label="`${item} 通道`" :value="item" /></el-select></label> <label><span>在线状态</span><el-input :model-value="form.deviceCode ? form.onlineStatus : '-'" disabled /></label> <label class="related-field"><span>关联测试</span><DeviceRelatedTests :tests="form.relatedTests" /></label> <label><span>状态</span><el-radio-group v-model="form.status"><el-radio-button value="启用">启用</el-radio-button><el-radio-button value="禁用">禁用</el-radio-button></el-radio-group></label> <label class="remark-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入内容" /></label> </div> <footer class="form-footer"><button class="secondary-button" type="button" @click="router.push('/device-collector')">取消</button><button class="primary-button save-button" type="button" :disabled="loading || saving" @click="submit"><el-icon><Check /></el-icon>{{ saving ? '保存中' : '保存' }}</button></footer> </section> <el-dialog v-model="deviceDialogVisible" class="device-dialog" title="选择设备" width="720px" align-center> <div v-loading="deviceLoading" class="device-table-wrap"> <table class="device-table"> <thead><tr><th>设备编码</th><th>IP</th><th>MAC</th><th>操作</th></tr></thead> <tbody> <tr v-for="device in devices" :key="device.deviceCode"><td>{{ device.deviceCode }}</td><td>{{ device.ip }}</td><td>{{ device.mac }}</td><td><button type="button" @click="selectDevice(device)">选择</button></td></tr> <tr v-if="!deviceLoading && !devices.length"><td colspan="4" class="empty-cell">暂无可发现采集器</td></tr> </tbody> </table> </div> <footer class="device-pagination"><span>共 {{ deviceTotal }} 个设备</span><el-pagination :current-page="devicePage" :page-size="devicePageSize" :total="deviceTotal" layout="prev, pager, next" background @current-change="handleDevicePageChange" /></footer> </el-dialog> </div> </template> @@ -176,13 +129,6 @@ .primary-button:disabled { opacity: .55; cursor: wait; } .secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); } .secondary-button:hover { color: var(--blue); border-color: #9dc8f5; } .device-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 30px; padding: 16px 18px 20px; border-bottom: 1px solid var(--line); } .device-summary > div { min-width: 0; display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 8px; align-items: center; } .device-summary span { color: #7b8794; font-size: 12px; } .device-summary strong { min-width: 0; color: #44515d; font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .device-summary .online-value { color: #23865e; } .device-summary .offline-value { color: #a64a4a; } .mono-value { font-family: 'Cascadia Mono', Consolas, monospace; } .detail-form { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; gap: 20px 24px; padding: 22px 18px 28px; } .detail-form label { min-width: 0; display: flex; flex-direction: column; gap: 7px; } .detail-form label > span { color: #52606d; font-size: 12px; font-weight: 500; } @@ -190,30 +136,16 @@ .collector-form-page :deep(.el-input__wrapper), .collector-form-page :deep(.el-select__wrapper), .collector-form-page :deep(.el-textarea__inner) { border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; } .collector-form-page :deep(.el-input__wrapper), .collector-form-page :deep(.el-select__wrapper) { min-height: 34px; } .remark-field { grid-column: span 2; } .related-field { grid-column: span 2; } .form-footer { min-height: 70px; display: flex; align-items: center; gap: 10px; padding: 0 18px; border-top: 1px solid var(--line); } .save-button { min-width: 82px; } .collector-form-page :deep(.device-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; } .collector-form-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); } .collector-form-page :deep(.el-dialog__body) { padding: 20px; } .device-table-wrap { overflow-x: auto; } .device-table { width: 100%; min-width: 560px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; } .device-table th, .device-table td { height: 42px; padding: 0 12px; border: 1px solid #d8e1e9; text-align: left; } .device-table th { height: 38px; color: #52606d; background: #eaf4ff; font-size: 12px; font-weight: 600; } .device-table th:last-child, .device-table td:last-child { width: 88px; text-align: center; } .device-table td:last-child button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; } .device-table tbody tr:hover { background: #f7fbff; } .device-table .empty-cell { height: 96px; color: #7b8794; text-align: center; } .device-pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-top: 18px; } .device-pagination > span { color: #7b8794; font-size: 11px; } .device-pagination :deep(.el-pagination) { --el-pagination-button-bg-color: #edf4fa; --el-pagination-hover-color: var(--blue); } @media (max-width: 960px) { .device-summary, .detail-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 960px) { .detail-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 620px) { .panel-heading { padding: 0 12px; } .panel-heading span, .panel-heading small, .device-select-row > span { display: none; } .device-summary, .detail-form { grid-template-columns: minmax(0, 1fr); padding-left: 12px; padding-right: 12px; } .remark-field { grid-column: auto; } .detail-form { grid-template-columns: minmax(0, 1fr); padding-left: 12px; padding-right: 12px; } .remark-field, .related-field { grid-column: auto; } .form-footer { padding: 0 12px; } .form-footer button { flex: 1; } .collector-form-page :deep(.el-dialog__body) { padding: 14px 12px; } } </style> src/views/TestConsole.vue
@@ -291,6 +291,9 @@ lastDataAt: cabinetSource?.latestSnapshot?.lastDataAt, collectorObservedAt: collectorSource?.latestSnapshot?.observedAt, collectorUnit: collectorSource?.latestSnapshot?.units?.join('/') || '-', // 来源不提供辅助测量是正常取值(新威实测无辅助数据;推式来源的列语义确认前同样如此), // 界面如实说明,不显示成故障,也不用零值顶替。 collectorAuxiliaryMissing: collectorSource?.latestSnapshot?.auxiliaryDataAvailable === false, collectorStatus: connectionStatusLabel(collectorSource?.deviceStatus), deviceStatus: connectionStatusLabel(source?.deviceStatus), } @@ -538,7 +541,7 @@ </header> <div class="cell-grid"> <article v-for="cell in cells" :key="cell.no" class="cell-card"> <div class="cell-summary"><strong>{{ cell.no }}</strong><span>{{ profile.batteryGroup }}</span><p>{{ cell.cabinet }} · {{ cell.channel }}</p><small>检测柜来源 · 单位:{{ cell.unit }} · 采样时间:{{ formatDateTime(cell.observedAt) }}</small><small>最近数据时间:{{ formatLastDataAt(cell.lastDataAt) }} · 通道工作状态:{{ cell.workStatus }}</small><small>采集器来源 · 状态:{{ cell.collectorStatus }} · 单位:{{ cell.collectorUnit }} · 采样时间:{{ formatDateTime(cell.collectorObservedAt) }}</small><small v-if="receptionState === '暂停接收'">暂停期间停止请求,当前快照不纳入结果</small><small>工步进行时间:{{ elapsedText }}</small></div> <div class="cell-summary"><strong>{{ cell.no }}</strong><span>{{ profile.batteryGroup }}</span><p>{{ cell.cabinet }} · {{ cell.channel }}</p><small>检测柜来源 · 单位:{{ cell.unit }} · 采样时间:{{ formatDateTime(cell.observedAt) }}</small><small>最近数据时间:{{ formatLastDataAt(cell.lastDataAt) }} · 通道工作状态:{{ cell.workStatus }}</small><small v-if="cell.collectorAuxiliaryMissing">采集器来源 · 状态:{{ cell.collectorStatus }} · 该来源无辅助测量数据(正常取值,不代表故障)</small><small v-else>采集器来源 · 状态:{{ cell.collectorStatus }} · 单位:{{ cell.collectorUnit }} · 采样时间:{{ formatDateTime(cell.collectorObservedAt) }}</small><small v-if="receptionState === '暂停接收'">暂停期间停止请求,当前快照不纳入结果</small><small>工步进行时间:{{ elapsedText }}</small></div> <dl class="metric-list"><div><dt>电压(V)</dt><dd class="voltage">{{ cell.voltage }}</dd></div><div><dt>温度(℃)</dt><dd class="temperature">{{ cell.temperature }}</dd></div><div><dt>正电位</dt><dd class="positive">{{ cell.positive }}</dd></div><div><dt>负电位</dt><dd class="negative">{{ cell.negative }}</dd></div><div><dt>容量(Ah)</dt><dd class="capacity">{{ cell.capacity }}</dd></div><div><dt>电流(A)</dt><dd class="current">{{ cell.current }}</dd></div></dl> </article> </div>