yeg
2026-09-01 1a81d4ecc16a25e60a6f8765d62bda9d03c03c8b
将单条编辑改为详情式弹窗

行尾编辑不再跳转页面,改用旧数据详情布局的弹窗表单;批量修改继续使用表格内按格编辑。
1个文件已修改
11 ■■■■■ 已修改文件
src/components/ConsoleDataListPanel.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ConsoleDataListPanel.vue
@@ -31,6 +31,7 @@
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)
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 }))
const pagedRows = computed(() => filteredRows.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
@@ -47,7 +48,7 @@
function openBatchEditor() { singleEditing.value = false; editing.value = true; draftRows.value = Object.fromEntries(sourceRows.value.map(row => [row.id, { ...row }])) }
function openRowEditor(row: DataRow) { singleEditing.value = true; editing.value = false; draftRows.value = { [row.id]: { ...row } }; selectedIds.value = new Set([row.id]); editForm.field = String(fieldOptions.value[0]?.value || 'voltage') as keyof DataRow; editForm.value = String(row[editForm.field] ?? ''); editForm.remark = ''; editorVisible.value = true }
async function saveBatchEdit() { if (!Object.keys(draftRows.value).length) return; try { const result = await ElMessageBox.prompt('请输入本次修改原因', '提交数据修改', { inputPlaceholder: '必填,将写入数据修订记录', inputValidator: value => value.trim() ? true : '修改原因不能为空' }); editForm.remark = result.value.trim(); await submitBatchEdit() } catch { /* 用户取消原因填写 */ } }
async function submitBatchEdit() { if (singleEditing.value) { const row = Object.values(draftRows.value)[0]; if (!editForm.value.trim() || !Number.isFinite(Number(editForm.value))) { ElMessage.warning('修改值必须是有效数字'); return }; if (row) row[editForm.field] = editForm.value.trim() as never } if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; const originals = new Map(sourceRows.value.map(row => [row.id, row])); const changed = Object.values(draftRows.value).filter(row => JSON.stringify(originals.get(row.id)) !== JSON.stringify(row)); if (!changed.length) { ElMessage.warning('没有检测到修改'); return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'EDIT', scopeSummary: `按格修改${changed.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(changed.map(row => row.id)), changesSummary: JSON.stringify(changed.map(row => ({ identity: row.id, before: originals.get(row.id), after: row }))), reason: editForm.remark.trim(), overrides: changed.map(row => ({ stableIdentity: row.id, fieldValues: JSON.stringify(Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]]))), deleted: false })) }); editorVisible.value = false; cancelBatchEdit(); await loadRecords(); ElMessage.success(`已提交 ${changed.length} 条数据修改`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据修改提交失败') } }
async function submitBatchEdit() { if (singleEditing.value && singleRow.value && fieldOptions.value.some(field => { const value = singleRow.value?.[field.value as keyof DataRow]; return !String(value ?? '').trim() || !Number.isFinite(Number(value)) })) { ElMessage.warning('请填写完整、有效的数据'); return } if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; const originals = new Map(sourceRows.value.map(row => [row.id, row])); const changed = Object.values(draftRows.value).filter(row => JSON.stringify(originals.get(row.id)) !== JSON.stringify(row)); if (!changed.length) { ElMessage.warning('没有检测到修改'); return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'EDIT', scopeSummary: `按格修改${changed.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(changed.map(row => row.id)), changesSummary: JSON.stringify(changed.map(row => ({ identity: row.id, before: originals.get(row.id), after: row }))), reason: editForm.remark.trim(), overrides: changed.map(row => ({ stableIdentity: row.id, fieldValues: JSON.stringify(Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]]))), deleted: false })) }); editorVisible.value = false; cancelBatchEdit(); await loadRecords(); ElMessage.success(`已提交 ${changed.length} 条数据修改`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据修改提交失败') } }
function cancelBatchEdit() { editing.value = false; singleEditing.value = false; draftRows.value = {}; selectedIds.value = new Set() }
async function deleteSelected(requestedIds?: string[] | Event) { const ids = Array.isArray(requestedIds) && requestedIds.length ? requestedIds : [...selectedIds.value]; if (!ids.length) { ElMessage.warning('请先勾选需要删除的数据'); return }; let result: { value: string }; try { result = await ElMessageBox.prompt(`确认物理删除 ${ids.length} 条${activeTab.value}数据?删除后不可恢复。`, '删除确认', { inputPlaceholder: '请输入删除原因', inputValidator: value => value.trim() ? true : '删除原因不能为空' }) } catch { return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'DELETE', scopeSummary: `物理删除${ids.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(ids), changesSummary: null, reason: result.value.trim(), overrides: ids.map(stableIdentity => ({ stableIdentity, fieldValues: null, deleted: true })) }); const target = sourceRows.value; const kept = target.filter(row => !ids.includes(row.id)); if (activeTab.value === 'realtime') rows.value = kept; else if (activeTab.value === 'step') stepRows.value = kept; else cycleRows.value = kept; selectedIds.value = new Set(); ElMessage.success(`已物理删除 ${ids.length} 条数据`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据删除失败') } }
function changeTab(tab: DataTab) { if (editing.value) cancelBatchEdit(); activeTab.value = tab; currentPage.value = 1 }
@@ -63,6 +64,13 @@
    <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>
    <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="520px" align-center><div class="edit-form"><p>修改已暂存,请填写本次行为原因后一次提交。</p><label><span>修改字段</span><el-select v-model="editForm.field"><el-option v-for="item in fieldOptions" :key="String(item.value)" :label="item.label" :value="item.value" /></el-select></label><label><span>修改值</span><el-input v-model="editForm.value" inputmode="decimal" placeholder="请输入修改后的数值" /></label><label><span>修改原因</span><el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="120" show-word-limit placeholder="必填,将写入数据修订记录" /></label></div><template #footer><button class="secondary-button" type="button" @click="editorVisible=false">取消</button><button class="primary-button" type="button" @click="submitBatchEdit"><el-icon><Check /></el-icon>提交修改</button></template></el-dialog>
    <el-dialog v-model="editorVisible" title="编辑数据" width="860px" align-center>
      <template v-if="singleRow">
        <dl class="single-step-summary"><div><dt>记录编号</dt><dd>{{ singleRow.recordNo }}</dd></div><div><dt>电池号</dt><dd>{{ singleRow.batteryNo }}</dd></div><div><dt>循环号</dt><dd>{{ singleRow.cycleNo }}</dd></div><div><dt>工步号</dt><dd>{{ singleRow.stepNo }}</dd></div><div><dt>工步名称</dt><dd>{{ singleRow.stepName }}</dd></div><div><dt>记录时间</dt><dd>{{ singleRow.recordedAt }}</dd></div></dl>
        <div class="single-form-grid"><label v-for="field in fieldOptions" :key="String(field.value)"><span>{{ field.label }}</span><el-input v-model="singleRow[field.value as keyof DataRow]" inputmode="decimal" /></label><label class="remark-field"><span>修改原因</span><el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="120" show-word-limit placeholder="请输入数据修改原因,提交后将写入修订记录" /></label></div>
      </template>
      <template #footer><button class="secondary-button" type="button" @click="editorVisible = false">取消</button><button class="primary-button" type="button" @click="submitBatchEdit"><el-icon><Check /></el-icon>提交修改</button></template>
    </el-dialog>
  </section>
</template>
<style scoped>
@@ -71,6 +79,7 @@
.toolbar-group{display:flex;align-items:center;gap:8px}.selection-column{width:46px;padding:0}.edit-form{display:grid;gap:14px}.edit-form label{display:grid;grid-template-columns:82px minmax(0,1fr);align-items:center;gap:10px}.edit-form label>span{color:#52606d;font-size:12px}.edit-form :deep(.el-select){width:100%}
.inline-editor,.row-actions-panel{margin:14px 16px;border:1px solid #cbddeb;background:#f8fbfe}.inline-editor>header{display:flex;align-items:center;gap:12px;padding:12px 14px;border-bottom:1px solid #d8e5ef}.inline-editor>header strong{color:#26333f;font-size:13px}.inline-editor>header span{color:#7b8794;font-size:11px}.inline-editor-row{display:flex;align-items:center;gap:10px;overflow-x:auto;padding:10px 12px;border-bottom:1px solid #e2ebf2;white-space:nowrap}.inline-editor-row>span{min-width:72px}.inline-editor-row label{display:grid;gap:4px;min-width:112px}.inline-editor-row label small{color:#71808d;font-size:10px}.inline-editor-row :deep(.el-input){width:112px}.row-actions-panel{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px;padding:10px 12px}.row-actions-panel>div{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;background:#fff;border:1px solid #e2ebf2}.row-action{padding:2px 6px;border:0;color:var(--blue);background:transparent;cursor:pointer;font-size:12px}.danger-text{color:#d94b4b}
.action-column{position:sticky;right:0;z-index:2;width:112px;background:#fff;overflow:visible;box-shadow:inset 1px 0 0 #b4c7d9,-8px 0 14px rgba(23,49,74,.1)}th.action-column{z-index:3;background:#eaf4ff}.row-actions{display:flex;align-items:center;justify-content:center;gap:12px}.row-actions button{border:0;color:var(--blue);background:transparent;font-size:12px;cursor:pointer}.row-actions .delete-action{color:#ad4a4a}
.single-step-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px 22px;margin:0;padding:4px 0 20px;border-bottom:1px solid #d8e1e9}.single-step-summary>div{display:flex;align-items:center;gap:10px;min-width:0}.single-step-summary dt{color:#71808d;font-size:12px;white-space:nowrap}.single-step-summary dd{min-width:0;color:#405261;font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.single-form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px 24px;padding-top:22px}.single-form-grid label{display:grid;gap:8px}.single-form-grid label>span{color:#52606d;font-size:12px;font-weight:500}.single-form-grid .remark-field{grid-column:1/-1}.single-form-grid :deep(.el-input__wrapper),.single-form-grid :deep(.el-textarea__inner){border-radius:2px;background:#f6f8fa;box-shadow:0 0 0 1px #e0e7ed inset}@media(max-width:760px){.single-step-summary,.single-form-grid{grid-template-columns:1fr}}
/* 三个数据子页使用连续分段控件,保持当前页在标题栏中央突出显示。 */
.data-tabs{width:228px;gap:0;padding:0;border:1px solid #2188f5;border-radius:5px;overflow:hidden;background:#fff;box-shadow:none}
.data-tabs button{flex:1;min-width:0;height:30px;padding:0 8px;border:0;border-radius:0;color:#2188f5;background:#fff;box-shadow:none;font-size:13px;font-weight:500}