From d67f933e50c7b5bf8c90e7d7db0000721ce796cc Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 09 九月 2026 18:07:31 +0800
Subject: [PATCH] 修正化成报表跨循环显隐与保存权限流程
---
src/utils/formationStepChart.ts | 7 +++++++
src/utils/formationSavedReportPrint.ts | 4 ++--
tests/formation-real-report.test.mjs | 22 +++++++++++++++++++++-
tests/load-typescript.cjs | 4 ++--
src/views/FormationReportForm.vue | 14 ++++++++------
src/api/formationReports.ts | 2 +-
src/views/FormationReport.vue | 5 +++--
7 files changed, 44 insertions(+), 14 deletions(-)
diff --git a/src/api/formationReports.ts b/src/api/formationReports.ts
index 8a8e835..0de56fe 100644
--- a/src/api/formationReports.ts
+++ b/src/api/formationReports.ts
@@ -20,7 +20,7 @@
export interface FormationMatrix { items: FormationItem[]; cycleNos: number[] }
export interface FormationStepSelection { charge: number[]; discharge: number[] }
export interface FormationSettings { fields: FormationField[]; values: Record<string, string>; chargeStepSelections: Record<string, number>; metrics?: ReportMetricKey[]; seriesColors?: Record<string, string>; manualChargeSteps?: Record<string, number[]>; curveStepSelections?: Record<string, FormationStepSelection>; hiddenSeriesKeys?: string[]; activeCycle?: number | null; sourceDetailOverrides?: ReportTestDetails }
-export interface FormationSummary { id: number; name: string; creator: string; createdAt: number; testName?: string; batteryGroup?: string }
+export interface FormationSummary { id: number; name: string; creator: string; createdAt: number; testName?: string; batteryGroup?: string; versionNo: number }
export interface FormationVersion { versionNo: number; operatorName: string; createdAt: number; changeSummary: string }
export interface SavedFormationReport extends FormationSummary, FormationMatrix {
description: string; settings: FormationSettings; versionNo: number; operatorName: string; versionCreatedAt: number; changeSummary: string
diff --git a/src/utils/formationSavedReportPrint.ts b/src/utils/formationSavedReportPrint.ts
index 47d9ccf..0bde36b 100644
--- a/src/utils/formationSavedReportPrint.ts
+++ b/src/utils/formationSavedReportPrint.ts
@@ -1,6 +1,6 @@
import { init, use } from 'echarts/core'
import { LineChart } from 'echarts/charts'
-import { GridComponent, LegendComponent } from 'echarts/components'
+import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { SVGRenderer } from 'echarts/renderers'
import { getFormationPrintVersion, type FormationItem, type FormationField, type SavedFormationReport } from '@/api/formationReports'
import { formationFieldLabel, formationSourceValue } from './formationReportFields'
@@ -9,7 +9,7 @@
import { formationChargeSteps, formationStepGroups, formationStepSeries, formationStepChartOption } from './formationStepChart'
import { openSavedReportPrint } from './savedReportPrint'
-use([LineChart, GridComponent, LegendComponent, SVGRenderer])
+use([LineChart, GridComponent, LegendComponent, TooltipComponent, SVGRenderer])
function escapeHtml(value: unknown) {
const entities: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
return String(value ?? '').replace(/[&<>"']/g, character => entities[character]!)
diff --git a/src/utils/formationStepChart.ts b/src/utils/formationStepChart.ts
index 4b77ca5..a52158e 100644
--- a/src/utils/formationStepChart.ts
+++ b/src/utils/formationStepChart.ts
@@ -7,6 +7,13 @@
export type FormationStepGroup = typeof formationStepGroups[number]['key']
export function formationCycleKey(item: FormationItem, cycle: number) { return formationSeriesKey(item) + '|' + cycle }
+/** 当前循环图例事件只更新其中出现的来源,保留其他循环独有电池的显示设置。 */
+export function formationHiddenSeries(hidden: string[], selected: Record<string, boolean>) {
+ const next = new Set(hidden)
+ for (const [key, shown] of Object.entries(selected)) shown ? next.delete(key) : next.add(key)
+ return [...next]
+}
+
/** 化成只使用可靠识别的实际工步,逻辑合并与人工范围均不参与。 */
export function formationChargeSteps(item: FormationItem, cycle: number, _manual?: Record<string, number[]>) {
return (item.chargeSteps?.[cycle] ?? []).filter(step => step.direction === 'CHARGE').sort((a,b)=>a.stepNo-b.stepNo)
diff --git a/src/views/FormationReport.vue b/src/views/FormationReport.vue
index b2086f0..72fc1c1 100644
--- a/src/views/FormationReport.vue
+++ b/src/views/FormationReport.vue
@@ -2,7 +2,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
-import { listFormationReports, deleteFormationReport, getFormationReport, type FormationSummary } from '@/api/formationReports'
+import { listFormationReports, deleteFormationReport, type FormationSummary } from '@/api/formationReports'
import { getUserInfo } from '@/api/auth'
import { openFormationSavedReportPrint } from '@/utils/formationSavedReportPrint'
@@ -37,7 +37,8 @@
function openEdit(item: FormationReportRecord) { router.push(`/report-formation/${item.id}/edit`) }
function openView(item: FormationReportRecord) { router.push({ path: `/report-formation/${item.id}/edit`, query: { mode: 'view' } }) }
async function downloadPdf(item: FormationReportRecord) {
- try{const record=await getFormationReport(item.id);await openFormationSavedReportPrint(item.id,record.versionNo)}
+ // 列表提供版本身份,导出只走独立导出权限,不额外请求查看接口。
+ try{await openFormationSavedReportPrint(item.id,item.versionNo)}
catch(cause){ElMessage.error(cause instanceof Error?cause.message:'打印版加载失败')}
}
async function deleteReport(item: FormationReportRecord) {
diff --git a/src/views/FormationReportForm.vue b/src/views/FormationReportForm.vue
index 2409878..051f415 100644
--- a/src/views/FormationReportForm.vue
+++ b/src/views/FormationReportForm.vue
@@ -14,13 +14,13 @@
import { defaultFormationFields, formationSourceValue } from '@/utils/formationReportFields'
import { formationCellKey, formationInputValue, formationCalculatedValue, formationNumberText } from '@/utils/formationReportCalculations'
import { formationCurveMetrics, formationSeriesKey, formationSeriesColor } from '@/utils/formationCycleChart'
-import { formationChargeSteps, formationStepChartOption, formationStepGroups } from '@/utils/formationStepChart'
+import { formationChargeSteps, formationStepChartOption, formationStepGroups, formationHiddenSeries } from '@/utils/formationStepChart'
import { openFormationSavedReportPrint } from '@/utils/formationSavedReportPrint'
use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
const route = useRoute(), router = useRouter()
const editingId = computed(() => Number(route.params.id) || null)
-const readonly = computed(() => route.query.mode === 'view')
+const readonly = computed(() => route.query.mode === 'view' || (saved.value !== null && !can('edit')))
const reportName = ref(''), description = ref(''), error = ref('')
const loading = ref(false), saving = ref(false), previewLoading = ref(false)
const permissions = ref<string[]>([])
@@ -102,7 +102,7 @@
function setMetric(key:typeof formationCurveMetrics[number]['key'],show:boolean){settings.value.metrics=formationCurveMetrics.filter(metric=>metric.key===key?show:settings.value.metrics?.includes(metric.key)).map(metric=>metric.key);if(!readonly.value)dirty.value=true}
function setColor(item:FormationItem,color:string|null){if(color){settings.value.seriesColors={...settings.value.seriesColors,[formationSeriesKey(item)]:color};dirty.value=true}}
function toggleSeries(item:FormationItem){const key=formationSeriesKey(item),hidden=new Set(settings.value.hiddenSeriesKeys??[]);hidden.has(key)?hidden.delete(key):hidden.add(key);settings.value.hiddenSeriesKeys=[...hidden];if(!readonly.value)dirty.value=true}
-function legendChanged(event:{selected:Record<string,boolean>}){settings.value.hiddenSeriesKeys=Object.entries(event.selected).filter(([,shown])=>!shown).map(([key])=>key);if(!readonly.value)dirty.value=true}
+function legendChanged(event:{selected:Record<string,boolean>}){settings.value.hiddenSeriesKeys=formationHiddenSeries(settings.value.hiddenSeriesKeys??[],event.selected);if(!readonly.value)dirty.value=true}
async function saveReport(){
if(!reportName.value.trim()){ElMessage.warning('请输入报表名称');return}
@@ -116,7 +116,9 @@
const payload={name:reportName.value.trim(),description:description.value,settings:JSON.parse(JSON.stringify(settings.value)) as FormationSettings}
const record=saved.value?await editFormationReport(saved.value.id,{...payload,baseVersionNo:saved.value.versionNo,changeSummary:reason}):await createFormationReport({...payload,selections:matrix.value.items.map(item=>item.source.selection)})
loadSaved(record);ElMessage.success('化成报表已保存')
- await router.replace({path:`/report-formation/${record.id}/edit`,query:{mode:'view'}})
+ // 动作权限独立;无查询权限时不因保存成功而跳入未授权的查看请求。
+ if(can('query'))await router.replace({path:`/report-formation/${record.id}/edit`,query:{mode:'view'}})
+ else if(can('edit'))await router.replace({path:`/report-formation/${record.id}/edit`})
}catch(cause){ElMessage.error(message(cause))}finally{saving.value=false}
}
async function refreshSource(){
@@ -173,11 +175,11 @@
<div class="page-tools"><button class="back-button" type="button" @click="router.push('/report-formation')"><el-icon><ArrowLeft /></el-icon>返回化成报表</button><span>{{ readonly ? 'VIEW FORMATION REPORT' : editingId ? 'EDIT FORMATION REPORT' : 'NEW FORMATION REPORT' }}</span></div>
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
<fieldset :disabled="busy || !!error" class="report-fieldset">
- <section class="report-name-panel"><label><span>报表名称</span><el-input v-model="reportName" :disabled="readonly" clearable maxlength="100" placeholder="请输入报表名称" @input="dirty=true" /></label><label><span>选择测试</span><el-select v-model="selectedTestIds" multiple filterable collapse-tags collapse-tags-tooltip :disabled="!!editingId || busy" placeholder="选择已完成的化成测试" @change="selectTests"><el-option v-for="test in testOptions" :key="test.id" :label="test.name" :value="test.id" /></el-select></label></section>
+ <section class="report-name-panel"><label><span>报表名称</span><el-input v-model="reportName" :disabled="readonly" clearable maxlength="100" placeholder="请输入报表名称" @input="dirty=true" /></label><label><span>选择测试</span><el-select v-model="selectedTestIds" multiple filterable collapse-tags collapse-tags-tooltip :disabled="!!editingId || !!saved || busy" placeholder="选择已完成的化成测试" @change="selectTests"><el-option v-for="test in testOptions" :key="test.id" :label="test.name" :value="test.id" /></el-select></label></section>
<el-empty v-if="!matrix.items.length && !previewLoading" :description="editingId ? '该版本暂无来源数据' : '请选择化成测试'" />
<div v-loading="previewLoading" v-if="matrix.items.length">
<section class="report-data-panel"><header class="panel-heading"><div><strong>报表数据</strong><span>电池组字段</span></div></header>
- <div class="report-data-table-wrap"><table class="report-data-table" :style="{minWidth:`${Math.max(980,matrix.items.length*150+160)}px`}"><thead><tr><th>测试 / 电池组</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.items.length">{{ column.name }} / {{ column.items[0]?.source.batteryGroup }}</th></tr><tr><th>电池号</th><th v-for="item in matrix.items" :key="formationSeriesKey(item)">{{ item.source.batteryNo }}</th></tr></thead><tbody>
+ <div class="report-data-table-wrap"><table class="report-data-table" :style="{minWidth:`${Math.max(980,matrix.items.length*150+160)}px`}"><thead><tr><th>测试</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.items.length">{{ column.name }}</th></tr><tr><th>电池组 / 电池号</th><th v-for="item in matrix.items" :key="formationSeriesKey(item)">{{ item.source.batteryGroup }} / {{ item.source.batteryNo }}</th></tr></thead><tbody>
<tr v-for="field in detailFields" :key="field.key"><td>{{ field.displayName }}</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)"><span v-if="field.kind==='calculated'" class="auto-value" :title="calculation(item,0,field).reason">{{ displayValue(item,0,field) }}</span><el-input v-else :model-value="inputValue(item,0,field)" :disabled="readonly" size="small" maxlength="500" placeholder="请输入" @update:model-value="updateValue(item,0,field,$event)" /></td></tr>
</tbody></table></div>
<div v-if="!readonly" class="report-data-actions"><button class="primary-button" type="button" @click="addReportField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">另存特殊字段模板</button></div>
diff --git a/tests/formation-real-report.test.mjs b/tests/formation-real-report.test.mjs
index 8738458..2af053c 100644
--- a/tests/formation-real-report.test.mjs
+++ b/tests/formation-real-report.test.mjs
@@ -2,7 +2,10 @@
import { test } from 'node:test'
import load from './load-typescript.cjs'
const { formationCalculatedValue } = load(new URL('../src/utils/formationReportCalculations.ts', import.meta.url))
-const { formationStepChartOption, formationStepSeries } = load(new URL('../src/utils/formationStepChart.ts', import.meta.url))
+const { formationStepChartOption, formationStepSeries, formationHiddenSeries } = load(new URL('../src/utils/formationStepChart.ts', import.meta.url))
+const { formationSavedReportHtml } = load(new URL('../src/utils/formationSavedReportPrint.ts', import.meta.url), {
+ '@/api/formationReports': {getFormationPrintVersion:()=>{throw new Error('打印内容不应重新请求来源')}}
+})
const source = { selection: { testId: 1, testBatteryId: 11 }, name: '化成甲', batteryNo: '电池1', batteryGroup: '组1' }
const item = { source, cycleNos: [1], metrics: {}, businessFields: Object.entries({H:'2',Z:'3',A:'4',F:'5',B:'6'}).map(([fieldName,fieldValue])=>({fieldName,fieldValue})),
chargeSteps: {1:[{stepNo:1,direction:'CHARGE',endVoltage:4.1},{stepNo:2,direction:'UNKNOWN'}, {stepNo:3,direction:'CHARGE',endVoltage:4.2},{stepNo:4,direction:'DISCHARGE',capacity:0.25}]},
@@ -34,3 +37,20 @@
assert.deepEqual(formationStepChartOption({key:'current',unit:'mA'},[item],1,'discharge',settings).series[0].data,[[0,-100]])
assert.deepEqual(formationStepChartOption({key:'temperature',unit:'℃'},[item],1,'charge',settings).series[0].data.filter(point=>point[1]!==null),[[0,25],[1,26],[4,27]])
})
+test('当前循环的图例操作不清除其他循环独有来源的隐藏设置',()=>{
+ assert.deepEqual(formationHiddenSeries(['2|22'],{'1|11':false}),['2|22','1|11'])
+ assert.deepEqual(formationHiddenSeries(['2|22','1|11'],{'1|11':true}),['2|22'])
+})
+test('保存版打印完整输出跨组电池和循环,使用冻结结果且转义人工文本',()=>{
+ const items=Array.from({length:5},(_,index)=>({...item,source:{...source,selection:{testId:index+1,testBatteryId:11},name:`测试${index+1}`}}))
+ const settings={fields:[{key:'F',scope:'detail',kind:'battery',displayName:'F',unit:''},{key:'C',scope:'detail',kind:'calculated',displayName:'C',unit:''},{key:'method',scope:'cycle',kind:'manual',displayName:'测试方法',unit:''},{key:'capacity1v',scope:'cycle',kind:'calculated',displayName:'1.0V容量',unit:'mAh'}],values:{'1|11|0|F':'<自定义>','1|11|1|method':'<script>不能执行</script>'},metrics:['voltage'],chargeStepSelections:{}}
+ const html=formationSavedReportHtml({id:1,name:'已保存化成',items,cycleNos:[1],settings,calculatedValues:{'1|11|0|C':777,'1|11|1|capacity1v':222},versionNo:2,createdAt:0,versionCreatedAt:0,creator:'甲',operatorName:'甲',changeSummary:'测试',description:''})
+ assert.ok(html.includes('测试5'))
+ assert.ok(html.includes('777'))
+ assert.ok(html.includes('222'))
+ assert.ok(html.includes('<自定义>'))
+ assert.ok(html.includes('<script>不能执行</script>'))
+ assert.ok(!html.includes('<script>不能执行'))
+ assert.ok(html.includes('data-group="charge"')&&html.includes('data-group="discharge"'))
+ assert.equal((html.match(/<td>终止电压<\/td>/g)??[]).length,4)
+})
diff --git a/tests/load-typescript.cjs b/tests/load-typescript.cjs
index a495ad7..81a8c76 100644
--- a/tests/load-typescript.cjs
+++ b/tests/load-typescript.cjs
@@ -2,11 +2,11 @@
const path = require('node:path');
const ts = require('typescript');
// 无服务的公共绘图函数测试;加载器只负责转译本地 TypeScript 模块。
-module.exports = function load(file) {
+module.exports = function load(file, external = {}) {
if (file instanceof URL) file = require('node:url').fileURLToPath(file);
const source = fs.readFileSync(file, 'utf8');
const script = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
const exports = {};
- new Function('exports', 'require', script)(exports, id => id.startsWith('.') ? load(path.resolve(path.dirname(file), id.endsWith('.ts') ? id : id + '.ts')) : require(id));
+ new Function('exports', 'require', script)(exports, id => Object.hasOwn(external, id) ? external[id] : id.startsWith('.') ? load(path.resolve(path.dirname(file), id.endsWith('.ts') ? id : id + '.ts'), external) : require(id));
return exports;
};
--
Gitblit v1.7.1