修复对比报表切换与字段快照回显
按报表编号重新加载详情并保留显式清空字段,完整替换图表配置以避免曲线颜色合并错误。补充全局搜索切换、后退和窄屏图表回归验证。
| | |
| | | path: 'report-compare/:id/edit', |
| | | name: 'report-compare-edit', |
| | | component: () => import('@/views/ReportCompareForm.vue'), |
| | | meta: { title: '编辑对比报表' }, |
| | | meta: { title: '对比报表详情' }, |
| | | }, |
| | | { |
| | | path: 'report-formation', |
| | |
| | | <script setup lang="ts"> |
| | | import { toTestDisplayValue } from '@/utils/testDataUnits' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { use } from 'echarts/core' |
| | |
| | | if (!detailFieldValues[field]) detailFieldValues[field] = {} |
| | | detailTests.value.forEach(item => { |
| | | const key = detailKey(item) |
| | | if (detailFieldValues[field][key] === undefined) detailFieldValues[field][key] = '' |
| | | // 未填写才读取当前预览或保存快照的业务值,显式清空不被后续预览覆盖。 |
| | | if (detailFieldValues[field][key] === undefined) { |
| | | const source = sourceItems.value.find(value => compareSelectionKey(value.source.selection) === key) |
| | | detailFieldValues[field][key] = source?.businessFields.find(value => value.fieldName === field)?.fieldValue ?? '' |
| | | } |
| | | }) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | detailFields.value.forEach(ensureDetailFieldValues) |
| | | onMounted(async () => { |
| | | watch(editingId, async (id, _previous, onCleanup) => { |
| | | let cancelled = false |
| | | onCleanup(() => { cancelled = true }) |
| | | editingRecord.value = null |
| | | selectedTests.value = [] |
| | | sourceItems.value = [] |
| | | reportName.value = '' |
| | | description.value = '' |
| | | experimentName.value = '' |
| | | batteryFields.value = ['额定容量', '电流', '内阻'] |
| | | specialFields.value = ['温升'] |
| | | seriesColors.value = ['#3978f6', '#7d42d8', '#f08a3e'] |
| | | metricDefinitions.forEach(metric => { metricVisible[metric.key] = true }) |
| | | Object.keys(detailFieldValues).forEach(field => delete detailFieldValues[field]) |
| | | loadError.value = '' |
| | | detailFields.value.forEach(ensureDetailFieldValues) |
| | | previewError.value = '' |
| | | canCreate.value = false |
| | | reportLoading.value = true |
| | | try { |
| | | if (editingId.value) { |
| | | editingRecord.value = await getCompareReport(editingId.value) |
| | | if (id) { |
| | | const record = await getCompareReport(id) |
| | | if (cancelled) return |
| | | editingRecord.value = record |
| | | loadEditingReport() |
| | | } else { |
| | | const info = await getUserInfo() |
| | | if (cancelled) return |
| | | canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add')) |
| | | if (!canCreate.value) throw new Error('无新增对比报表权限') |
| | | await loadCandidates() |
| | | } |
| | | } catch (error) { loadError.value = error instanceof Error ? error.message : '报表加载失败' } |
| | | finally { reportLoading.value = false } |
| | | }) |
| | | } catch (error) { if (!cancelled) loadError.value = error instanceof Error ? error.message : '报表加载失败' } |
| | | finally { if (!cancelled) reportLoading.value = false } |
| | | }, { immediate: true }) |
| | | |
| | | async function refreshPreview() { |
| | | const selections = selectedTests.value.map(item => item.selection) |
| | | previewLoading.value = true |
| | | previewError.value = '' |
| | | sourceItems.value = [] |
| | | try { |
| | | sourceItems.value = await previewCompareReport(selectedTests.value.map(item => item.selection)) |
| | | const items = await previewCompareReport(selections) |
| | | if (readonly.value) return |
| | | sourceItems.value = items |
| | | detailFields.value.forEach(ensureDetailFieldValues) |
| | | sourceItems.value.forEach(item => item.businessFields.forEach(field => { |
| | | const key = compareSelectionKey(item.source.selection) |
| | | if (batteryFields.value.includes(field.fieldName) && !detailFieldValues[field.fieldName]?.[key]) |
| | | detailFieldValues[field.fieldName][key] = field.fieldValue ?? '' |
| | | })) |
| | | } catch (error) { previewError.value = error instanceof Error ? error.message : '曲线预览失败' } |
| | | finally { previewLoading.value = false } |
| | | } |
| | |
| | | } |
| | | selectedTests.value.push(toSelectedTest(candidate)) |
| | | if (!seriesColors.value[selectedTests.value.length - 1]) seriesColors.value.push('#109854') |
| | | detailFields.value.forEach(ensureDetailFieldValues) |
| | | testDialogVisible.value = false |
| | | await refreshPreview() |
| | | } |
| | |
| | | <section class="metrics-panel"> |
| | | <div v-if="!readonly" class="metric-switches"><label v-for="metric in metricDefinitions" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="metricVisible[metric.key]" inline-prompt active-text="显" inactive-text="隐" /></label></div> |
| | | <div v-if="chartSeries.length" class="chart-legend-bar"><strong>对比图例</strong><div v-for="(item, index) in chartSeries" :key="detailKey(item)" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="seriesColors[index]" :show-alpha="false" popper-class="legend-color-popper" :aria-label="`修改${item.name}颜色`" @change="updateSeriesColor(index, $event)" /><i v-else class="series-swatch" :style="{ background: seriesColors[index] }" /><span>{{ item.name }} · {{ item.batteryNo }} · 循环{{ item.cycle }} · {{ item.process }}</span></div></div> |
| | | <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="chartOption(metric)" autoresize /></article></div> |
| | | <!-- 报表切换和比对项移除时完整替换配置,避免图表库将颜色数组缩短误判为组件合并。 --> |
| | | <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="chartOption(metric)" :update-options="{ notMerge: true }" autoresize /></article></div> |
| | | </section> |
| | | |
| | | <footer class="form-actions"><button v-if="readonly" class="primary-button" type="button" @click="router.push('/report-compare')">返回列表</button><button v-else-if="canCreate" class="primary-button save-button" type="button" :disabled="saving || previewLoading || !!previewError" @click="saveReport"><el-icon><Check /></el-icon>{{ saving ? '保存中' : '保存报表' }}</button></footer> |
| | |
| | | function seedSources() { |
| | | stack.executeIsolatedSql(` |
| | | INSERT INTO t_battery_group(group_id,group_name,model) VALUES(8101,'对比验收电池组','验收型号'); |
| | | INSERT INTO t_battery_group_field(field_id,field_name,field_type) VALUES(8101,'额定容量','number'); |
| | | INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) VALUES(8101,8101,'12.5'); |
| | | INSERT INTO t_battery_group_field(field_id,field_name,field_type) VALUES(8101,'额定容量','number'),(8102,'质量','number'); |
| | | INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) VALUES(8101,8101,'12.5'),(8101,8102,'88'); |
| | | INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(8101,'对比验收类型'); |
| | | INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status,logic_step_mappings_json) |
| | | VALUES(8101,'对比来源测试',1,1,8101,'隔离受控数据','COMPLETED','ENDED','[{"logicalNo":1,"logicalName":"合并充放电","startStepNo":1,"endStepNo":2}]'), |
| | |
| | | } |
| | | |
| | | test('首次保存冻结真实来源,失败回滚且重启与删除来源后仍可重开', async ({ page }) => { |
| | | const pageErrors: string[] = [] |
| | | page.on('pageerror', error => { |
| | | if (page.url().includes('/report-compare')) pageErrors.push(error.message) |
| | | }) |
| | | seedSources() |
| | | await login(page) |
| | | const sources = (await api(page, '/system/compare-report/sources')).data |
| | |
| | | await expect(page.getByRole('button', { name: '添加比较', exact: true })).toBeEnabled() |
| | | } |
| | | await page.locator('.metric-switches label').filter({ hasText: '正电位' }).locator('.el-switch__core').click() |
| | | await page.getByRole('button', { name: '电池组字段', exact: true }).click() |
| | | await page.getByRole('dialog').getByText('质量', { exact: true }).click() |
| | | await page.getByRole('dialog').getByRole('button', { name: '确认', exact: true }).click() |
| | | await expect(page.locator('.field-table tbody tr').filter({ hasText: '质量' }).locator('input').first()).toHaveValue('88') |
| | | await page.route('**/dev-api/system/compare-report', async route => { |
| | | await route.fulfill({ json: { code: 500, msg: '验收保存失败' } }) |
| | | }, { times: 1 }) |
| | |
| | | expect(browserSaved.items).toHaveLength(3) |
| | | expect(browserSaved.settings.metrics).not.toContain('positiveVoltage') |
| | | expect(browserSaved.description).toBe('浏览器说明') |
| | | expect(browserSaved.settings.detailFieldValues['质量']['8101|8101|1|1|']).toBe('88') |
| | | |
| | | await api(page, '/system/test/8101/console-data/actions', 'POST', { |
| | | dataType: 'REALTIME', operationType: 'EDIT', scopeSummary: '首行电流修订', reason: '验证报表独立', |
| | |
| | | await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('首版独立快照') |
| | | await expect(page.getByPlaceholder('请输入说明')).toHaveValue('保存后保留说明') |
| | | await expect(page.getByPlaceholder('请输入报表名称')).toHaveAttribute('readonly') |
| | | await expect(page.locator('.field-table tbody tr').filter({ hasText: '额定容量' }).locator('input').first()).toHaveValue('12.5') |
| | | for (const width of [1440, 390]) { |
| | | await page.setViewportSize({ width, height: 900 }) |
| | | await page.screenshot({ path: `tests/e2e/.artifacts/compare-reopen-${width}.png`, fullPage: true }) |
| | | await expect.poll(async () => (await page.locator('.metric-chart canvas').first().boundingBox())!.width).toBeGreaterThan(200) |
| | | await page.screenshot({ path: `tests/e2e/.artifacts/compare-reopen-${width}.png`, fullPage: true, animations: 'disabled' }) |
| | | expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) |
| | | const coloredPixels = await page.locator('.metric-chart canvas').first().evaluate((canvas: HTMLCanvasElement) => { |
| | | const pixels = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height).data |
| | |
| | | }) |
| | | expect(coloredPixels).toBeGreaterThan(20) |
| | | } |
| | | |
| | | await page.setViewportSize({ width: 1440, height: 900 }) |
| | | await page.getByRole('button', { name: '页面搜索', exact: true }).click() |
| | | await page.getByPlaceholder('搜索页面、测试名称或报表').fill('浏览器保存报表') |
| | | await page.locator('.search-result').filter({ hasText: '浏览器保存报表' }).click() |
| | | await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('浏览器保存报表') |
| | | await expect(page.locator('.compare-table tbody tr')).toHaveCount(3) |
| | | await expect(page.locator('.field-table tbody tr').filter({ hasText: '质量' }).locator('input').first()).toHaveValue('88') |
| | | await page.goBack() |
| | | await expect(page.getByPlaceholder('请输入报表名称')).toHaveValue('首版独立快照') |
| | | |
| | | const menus = (await api(page, '/system/menu/permissionList')).data |
| | | const comparePage = menus.flatMap((module: any) => module.pages).find((item: any) => item.pageName === '对比报表') |
| | |
| | | } |
| | | if (username === 'compare_author') expect((await api(page, '/system/compare-report/sources')).data).toEqual([]) |
| | | } |
| | | expect(pageErrors).toEqual([]) |
| | | }) |