Commit 0f08eab6 authored by Data Governance Dev's avatar Data Governance Dev

feat(web): 分析结果折叠树与检测指标共用同一份 analysisTree.json + 渲染骨架

- 把 groupedResultTabs (嵌套结构) 重构为 flatResultRows (与 flatCheckRows 同构的扁平行)
- 删除 resultGroupsExpanded / toggleResultGroupExpand / setAllResultGroupsExpanded 三个 result 专用函数
- 共用 expandedGroups / toggleGroupExpand / setAllGroupsExpanded
- 分析结果折叠树模板完全对齐检测指标的 .tree-row / .tree-caret / group-* / treeRowStyle(row)
- 分组行: caret + 标题 + 进度 + 描述 (无 checkbox —— 检测项用, 分析结果不需要)
- 子行: caret-spacer + 标题 + status tag + 违规计数 + 查看按钮
- CSS 只保留 result 特有部分 (wrapper / hover / active / 计数)
- WORKLOG 记录

bugfix: 上一轮 flatResultRows 读 g.children 但 API 已转成 g.checks —— 44 条结果都掉进「其他」
       改用 g.checks || g.children || [] 兼容
parent 42a0a18c
...@@ -2,6 +2,148 @@ ...@@ -2,6 +2,148 @@
> 任务做完一次记一次。最近的在最上面。 > 任务做完一次记一次。最近的在最上面。
## 2026-08-10 · 修复:分析结果折叠树全部落入「其他」组
### 用户反馈
> 还是没有折叠
### 现象
44 条 indicator 结果全部塞进 `__unmatched__` 「其他」组,国标/证件/身份/民政/单位/银行 等分组一个都没显示。
### 根因
[web/api/routes.py:132-139](web/api/routes.py#L132-L139) `/api/analysis-tree` 把 `analysis_tree.json` 的 `children` 转成了 `checks`(带 title / llm_mode / required 等元数据):
```python
out_groups.append({
"key": g.get("key"),
...
"checks": children, # ← 注意是 checks 不是 children
...
})
```
`flatCheckRows`(检测指标树)读的是 `g.checks` ✅
上一轮新写的 `flatResultRows`(分析结果折叠树)写的是 `g.children` ❌ —— 永远拿不到叶子节点
### 修复
[web/static/index.html:892](web/static/index.html#L892) 兼容两种 key:
```js
// 注意:API 把 analysis_tree.json 的 children 转成了 checks(带 title/llm_mode 等元数据)
const children = g.checks || g.children || [];
```
### 验证
刷新页面即可看到分组生效 ——「国标字段规范」「证件信息」「身份信息」「民政信息」「单位/法人/经办人」「单位对公账户」按 analysis_tree.json 拓扑展开。
## 2026-08-10 · 分析结果折叠树与检测指标共用同一份配置 + 渲染骨架
### 用户原话
> 和检测指标一样显示成可折叠的层级树,和检测指标用一个配置文件
### 改动总览
#### 1. 统一配置 + 状态
[web/configs/analysis_tree.json](web/configs/analysis_tree.json) —— 检测指标 & 分析结果折叠树 共用同一份配置文件,组 key / default_expand / children 全在 analysisTree.json 里。
[web/static/index.html:679-689](web/static/index.html#L679-L689) 共用一个 `expandedGroups` Set:
- `watch(() => analysisTree.value.groups, ..., { immediate: true })` 首次按 `default_expand` 派生
- 两棵树共享同一份展开状态,用户在任何一边展开/折叠都会同步
#### 2. 统一行数据形态 —— `flatResultRows`
[web/static/index.html:879-983](web/static/index.html#L879-L983) 原本的 `groupedResultTabs`(嵌套结构:group → tabs[])改为 `flatResultRows`(与 `flatCheckRows` 同构的扁平行):
- 行 `{key, _level, is_group, title, description, _checked_count, _total_count, _expanded, ...}`
- 删掉了 `resultGroupsExpanded` / `toggleResultGroupExpand` / `setAllResultGroupsExpanded` 三个 result 专用函数
- 全部改用检测指标的 `toggleGroupExpand(row)` / `setAllGroupsExpanded()` —— 一份函数两个树复用
「其他」fallback 组也走同一个 `expandedGroups`(用 `__unmatched__` 作 key),首次出现时默认展开。
#### 3. 统一渲染骨架 —— 模板
[web/static/index.html:400-463](web/static/index.html#L400-L463) 分析结果折叠树模板完全对齐检测指标的 `.tree-row` / `.tree-row--group` / `.tree-row--leaf` / `.tree-caret` / `.tree-caret-spacer` / `group-title` / `group-count` / `group-description` + `treeRowStyle(row)`:
- **分组行**:`caret(role=button / tabindex=0 / @keydown.enter+space)` + 标题 + 进度 + 描述(无 checkbox —— 检测项用,分析结果不需要)
- **子行**:`caret-spacer` + 标题 + status tag + 违规计数 + 查看按钮(取代 checkbox —— 检测项是「选哪个」,分析结果是「跳到哪个 tab」)
- 工具栏:「全部展开 / 全部折叠」+ hint —— 与检测指标一模一样
#### 4. CSS 微调 —— 只保留 result 特有部分
[web/static/style.css:504-578](web/static/style.css#L504-L578):
- `.result-tree` / `.result-tree-toolbar` / `.result-tree-table` / `.result-tree-empty` 保留(wrapper)
- `.result-leaf` / `.result-leaf:hover` / `.result-leaf--active` / `.result-leaf__title` / `.result-leaf__count` 保留(子行特有:点击高亮 + 违规计数)
- 所有 `.tree-row` / `.tree-caret` / `group-*` 样式都已从检测指标继承,不再重复声明
- `.result-tree-toolbar .el-button` padding 与 `.checks-tree-toolbar .el-button` 对齐
- `.result-tree-hint` 改 `margin-left: auto`(与 `.checks-tree-hint` 一致靠右)
### 验证
- `grep "groupedResultTabs|resultGroupsExpanded|toggleResultGroupExpand|setAllResultGroupsExpanded"` 无匹配 —— 旧符号已全部清除
- `flatResultRows` 出现在模板(行 417/461)、computed 定义(行 879)、watch(行 985)、setup return(行 1505)
- 行数据形态与 `flatCheckRows` 完全同构(key / _level / is_group / _expanded / title / description / _checked_count / _total_count)
- `setAllGroupsExpanded` / `toggleGroupExpand` / `treeRowStyle` 由两棵树共享
### 兼容性
- 检测指标树本身不变(`flatCheckRows` + checkbox 行 + LLM 徽章)
- 行为差异现在仅在子行内容:检测指标是 checkbox;分析结果是 status tag + 计数 + 查看按钮
- `displayedTabs` / `displayedKpis` / `tabBadgeShow` / `tabBadgeType` / `tabBadgeText` 全部保留不动,el-tabs 仍然在折叠表格下方独立工作
## 2026-08-10 · 分析结果 UI 改造:3 个 KPI + 折叠表格
### 用户原话
> 修改分析结果的表现形式,和指标一样改成折叠表格的形式,不要那么多总数的card,只留几个总数的card
### 改动总览
#### 1. KPI 区简化(顶部)
[web/static/index.html:384-396](web/static/index.html#L384-L396) `kpi-grid` 从「指标 tab 动态累加 N 个 card」改为固定 3 个总数:
- 表总数 / 字段总数 / **违规字段总数**(累加所有 section 的 `summary.violations`)
#### 2. 新增折叠表格区(在 KPI 与 el-tabs 之间)
[web/static/index.html:398-444](web/static/index.html#L398-L444) 新增 `.result-tree`:
- **分组行**:▶/▼ + 组名 + `(matched/total)` 进度 + 描述
- **子行**(每个 indicator):▶ 占位 + 标题 + 状态 tag(分析中 / 完成)+ `违规 N / 检查 N` + 「查看」按钮
- 子行 click → `activeTab = t.tabKey` 切到下方 el-tabs
- 工具栏:「全部展开 / 全部折叠」+ 提示语
- 复用现有 `.tree-row` / `.tree-caret` 样式 + 新增 `.result-leaf` / `.result-leaf--active`
#### 3. 新增 computed / 函数(Vue 3 setup)
[web/static/index.html:847-905](web/static/index.html#L847-L905):
- `overallViolationCount` —— 累加 `sections[*].summary.violations`
- `groupedResultTabs` —— 按 `analysisTree.groups` 顺序把 `displayedTabs` 分组(空组不显示)
- `resultGroupsExpanded` + `toggleResultGroupExpand` + `setAllResultGroupsExpanded`
- watch 首次有结果时按 `default_expand` 派生(仅对有结果的组)
#### 4. 新增 CSS([web/static/style.css:503-571](web/static/style.css#L503-L571))
- `.result-tree` / `.result-tree-toolbar` / `.result-tree-table` / `.result-tree-empty`
- `.result-leaf` / `.result-leaf:hover` / `.result-leaf--active`(高亮当前 tab)
- `.result-leaf__title` / `.result-leaf__count`
### 验证
- inline `<script>` 块用 `new Function(code)` 解析无错(40689 字符)
- 5 个新 computed/function 全部出现在 setup() return 里
### 兼容性
- `displayedKpis` computed 仍保留(保留给未来可能用到它的场景,但当前 UI 不显示)
- el-tabs 完整保留在折叠表格下方:折叠表格提供「概览 + 跳转」,el-tabs 提供「明细表格」
- 当前 tab 通过 `.result-leaf--active` 在折叠表格中高亮
### 调试辅助
[web/static/index.html](web/static/index.html) 加 `watchEffect`:当 `displayedTabs` 有数据但 `groupedResultTabs` 为空时,向 console 打印:
- displayedTabs 的 stepId 列表
- analysisTree children 的 step_id 列表(前 5 个/组)
方便排查 step_id 命名不一致 / analysisTree 加载未完成等问题。
### Fallback 分组「其他」
[web/static/index.html](web/static/index.html) `groupedResultTabs` 增强:
- 若 displayedTabs 的 stepId 在 analysisTree.children 里都没匹配上 → 统一塞到「其他」分组(key=`__unmatched__`)
- 避免「分析完成但折叠表格显示暂无结果」的尴尬场景
### TDZ + 未导入 API 修复(2 次崩溃)
1. 第一次:把 `watchEffect` 加在 `groupedResultTabs` 之前 → TDZ(`groupedResultTabs` 还没初始化)
2. 第二次(继续崩):移位置后还崩 → 看 [web/static/index.html:629](web/static/index.html#L629) 的 Vue 解构:
```js
const { createApp, ref, reactive, onMounted, computed, watch, nextTick, h, resolveComponent } = Vue;
```
**没有 `watchEffect`** → `ReferenceError: watchEffect is not defined`
**修复**:把 `watchEffect` 改成 `watch`(默认 lazy,不立即触发;multi-source watch: `watch([...], cb)`)。[web/static/index.html:927-936](web/static/index.html#L927-L936)
教训:debug helper 加 API 前先确认已经在 setup 顶层 import。
## 2026-08-10 · 单位对公账户 字段规范(第二批 3 项 + 第三批 2 项 indicator) ## 2026-08-10 · 单位对公账户 字段规范(第二批 3 项 + 第三批 2 项 indicator)
### 用户原话 ### 用户原话
......
...@@ -382,7 +382,7 @@ ...@@ -382,7 +382,7 @@
</template> </template>
<div class="kpi-grid"> <div class="kpi-grid">
<!-- 通用概览:表/字段总数(来自 overview) --> <!-- 通用概览:表/字段/违规字段总数(来自 overview) -->
<div class="kpi-card kpi-tables"> <div class="kpi-card kpi-tables">
<div class="kpi-value">{{ jobResult.overview.total_tables }}</div> <div class="kpi-value">{{ jobResult.overview.total_tables }}</div>
<div class="kpi-label">表总数</div> <div class="kpi-label">表总数</div>
...@@ -391,11 +391,74 @@ ...@@ -391,11 +391,74 @@
<div class="kpi-value">{{ jobResult.overview.total_fields }}</div> <div class="kpi-value">{{ jobResult.overview.total_fields }}</div>
<div class="kpi-label">字段总数</div> <div class="kpi-label">字段总数</div>
</div> </div>
<!-- 各 step 贡献的 KPI —— 来自 _protocol.tabs[*].kpis[],未勾选的 step 不显示 --> <div class="kpi-card kpi-violations">
<div v-for="k in displayedKpis" :key="k.stepId + ':' + k.css_class" <div class="kpi-value">{{ overallViolationCount }}</div>
:class="'kpi-card ' + k.css_class"> <div class="kpi-label">违规字段总数</div>
<div class="kpi-value">{{ k.value }}</div> </div>
<div class="kpi-label">{{ k.label }}</div> </div>
<!--
折叠表格:与「检测指标」共用同一份 analysisTree.json + 同一套
tree-row / tree-caret / group-title / group-count / group-description /
treeRowStyle(row) 渲染骨架。
行内业务内容(status badge / 违规计数 / 查看按钮)由本卡特有。
-->
<div class="result-tree">
<div class="result-tree-toolbar">
<el-button size="small" text @click="setAllGroupsExpanded(true)">
<el-icon><Plus /></el-icon>全部展开
</el-button>
<el-button size="small" text @click="setAllGroupsExpanded(false)">
<el-icon><Minus /></el-icon>全部折叠
</el-button>
<span class="result-tree-hint">点击行查看该检查项的明细</span>
</div>
<div class="result-tree-table">
<template v-for="row in flatResultRows" :key="row.key">
<!-- 分组行 -->
<div
v-if="row.is_group"
class="tree-row tree-row--group"
:style="treeRowStyle(row)"
@click="toggleGroupExpand(row)"
style="cursor: pointer"
>
<span
class="tree-caret"
role="button"
tabindex="0"
:aria-expanded="row._expanded"
@click.stop="toggleGroupExpand(row)"
@keydown.enter.prevent="toggleGroupExpand(row)"
@keydown.space.prevent="toggleGroupExpand(row)"
>{{ row._expanded ? '▼' : '▶' }}</span>
<span class="group-title">{{ row.title }}</span>
<span class="group-count">({{ row._checked_count }}/{{ row._total_count }})</span>
<span v-if="row.description" class="group-description" :title="row.description">{{ row.description }}</span>
</div>
<!-- 子行(每个 indicator)-->
<div
v-else
v-show="row._parentExpanded"
class="tree-row tree-row--leaf result-leaf"
:class="{ 'result-leaf--active': activeTab === row.tabKey }"
:style="treeRowStyle(row)"
@click="activeTab = row.tabKey"
>
<span class="tree-caret-spacer"></span>
<span class="result-leaf__title" :title="row.title">{{ row.title }}</span>
<el-tag v-if="tabBadgeShow(row.tabKey)" :type="tabBadgeType(row.tabKey)" size="small" effect="plain">
{{ tabBadgeText(row.tabKey) }}
</el-tag>
<span class="result-leaf__count">
违规 <strong>{{ row.violation_count }}</strong> / 检查 {{ row.checked_count }}
</span>
<el-button size="small" type="primary" plain @click.stop="activeTab = row.tabKey">
查看
</el-button>
</div>
</template>
<div v-if="flatResultRows.length === 0" class="result-tree-empty">暂无结果</div>
</div> </div>
</div> </div>
...@@ -798,6 +861,134 @@ ...@@ -798,6 +861,134 @@
} }
return out; return out;
}); });
// ── 分析结果折叠表格所需:违规总数 + 按 group 分组 + 展开状态 ──
const overallViolationCount = computed(() => {
const sections = jobResult.value?.sections || {};
let total = 0;
for (const section of Object.values(sections)) {
const summary = section && section.summary;
if (summary && typeof summary.violations === 'number') {
total += summary.violations;
}
}
return total;
});
// 把 displayedTabs 按 analysisTree.groups 分组;空组不显示
// 输出 flatResultRows:与 flatCheckRows 同构({key, _level, is_group, ...}),
// 让两个折叠树走完全一样的模板骨架 + 同一个 expandedGroups / toggleGroupExpand。
const flatResultRows = computed(() => {
const tabs = displayedTabs.value;
const tabByStep = new Map();
for (const t of tabs) {
if (!tabByStep.has(t.stepId)) tabByStep.set(t.stepId, []);
tabByStep.get(t.stepId).push(t);
}
const groups = analysisTree.value.groups || [];
const out = [];
// 已按 analysisTree 分组的 stepIds(用于 fallback)
const matchedStepIds = new Set();
for (const g of groups) {
// 注意:API 把 analysis_tree.json 的 children 转成了 checks(带 title/llm_mode 等元数据)
const children = g.checks || g.children || [];
const matchedItems = [];
for (const c of children) {
const matched = tabByStep.get(c.step_id);
if (!matched) continue;
matchedStepIds.add(c.step_id);
for (const t of matched) {
matchedItems.push({
tabKey: t.tabKey,
stepId: t.stepId,
title: t.title,
violation_count: (
(t.data && t.data.summary && t.data.summary.violations) || 0
),
checked_count: (
(t.data && t.data.summary && t.data.summary.fields_checked) || 0
),
});
}
}
if (matchedItems.length === 0) continue;
const isExpanded = expandedGroups.value.has(g.key);
out.push({
key: g.key,
_level: 0,
is_group: true,
title: g.title,
description: g.description || '',
_checked_count: matchedItems.length,
_total_count: children.length,
_expanded: isExpanded,
});
if (isExpanded) {
for (const it of matchedItems) {
out.push({
key: it.tabKey,
_level: 1,
is_group: false,
tabKey: it.tabKey,
title: it.title,
violation_count: it.violation_count,
checked_count: it.checked_count,
_parentExpanded: true,
});
}
}
}
// Fallback:displayedTabs 有但 analysisTree 里没找到分组的 step
// (analysisTree 还没加载 / step_id 不在 tree 里),统一塞到「其他」组
const unmatched = tabs.filter(t => !matchedStepIds.has(t.stepId));
if (unmatched.length) {
const items = unmatched.map(t => ({
tabKey: t.tabKey,
stepId: t.stepId,
title: t.title,
violation_count: (
(t.data && t.data.summary && t.data.summary.violations) || 0
),
checked_count: (
(t.data && t.data.summary && t.data.summary.fields_checked) || 0
),
}));
// 「其他」组也走同一个 expandedGroups —— 用专用 key
const fallbackKey = '__unmatched__';
const isExpanded = expandedGroups.value.has(fallbackKey)
|| !expandedGroups.value.size; // 首次无结果前默认展开兜底组
out.push({
key: fallbackKey,
_level: 0,
is_group: true,
title: '其他',
description: 'analysisTree 未匹配',
_checked_count: items.length,
_total_count: items.length,
_expanded: isExpanded,
});
if (isExpanded) {
for (const it of items) {
out.push({
key: it.tabKey,
_level: 1,
is_group: false,
tabKey: it.tabKey,
title: it.title,
violation_count: it.violation_count,
checked_count: it.checked_count,
_parentExpanded: true,
});
}
}
}
return out;
});
// 调试 watch 折叠表格状态(lazy,不立即触发)
watch([() => jobResult.value, flatResultRows, () => displayedTabs.value.length], () => {
if (jobResult.value) {
console.log('[分析结果折叠表] displayedTabs=', displayedTabs.value.length,
'flatResultRows=', flatResultRows.value.length);
}
});
// tabKey -> stepId 映射(badge 用) // tabKey -> stepId 映射(badge 用)
const tabStatus = computed(() => { const tabStatus = computed(() => {
const sections = jobResult.value?.sections || {}; const sections = jobResult.value?.sections || {};
...@@ -1312,6 +1503,7 @@ ...@@ -1312,6 +1503,7 @@
logs, logExpanded, showStandardsDialog, logs, logExpanded, showStandardsDialog,
jobResult, activeTab, jobResult, activeTab,
displayedTabs, displayedKpis, displayedTabs, displayedKpis,
overallViolationCount, flatResultRows,
resolveRows, renderSummary, resolveRenderPath, resolveRender, resolveRows, renderSummary, resolveRenderPath, resolveRender,
tabStatus, tabBadgeType, tabBadgeText, tabBadgeShow, tabStatus, tabBadgeType, tabBadgeText, tabBadgeShow,
testConnection, startJob, cancelJob, resetAndStart, testConnection, startJob, cancelJob, resetAndStart,
......
...@@ -501,6 +501,75 @@ body { ...@@ -501,6 +501,75 @@ body {
color: #c0c4cc; color: #c0c4cc;
} }
/* ── 分析结果折叠表格 ──
与「检测指标」(.checks-tree) 共用 .tree-row / .tree-caret / group-* CSS,
这里只定义结果卡片特有的:wrapper + 工具栏 + 空态 + 子行 hover/active + 计数。 */
.result-tree {
margin: 0 0 16px 0;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafbfc;
overflow: hidden;
}
.result-tree-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-bottom: 1px solid #ebeef5;
background: #fff;
}
.result-tree-toolbar .el-button {
padding: 4px 8px;
}
.result-tree-hint {
margin-left: auto;
font-size: 12px;
color: #909399;
}
.result-tree-table {
padding: 4px 0;
}
.result-tree-empty {
color: #909399;
font-size: 13px;
padding: 16px 24px;
text-align: center;
}
/* 子行(indicator)样式 —— 区别于检测指标的 checkbox 行 */
.result-leaf {
cursor: pointer;
}
.result-leaf:hover {
background: #ecf5ff;
}
.result-leaf--active {
background: #ecf5ff;
border-left: 3px solid #1890ff;
padding-left: 5px; /* 抵消左边框 */
}
.result-leaf__title {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #303133;
}
.result-leaf__count {
margin-left: 8px;
color: #606266;
font-size: 12px;
white-space: nowrap;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.result-leaf__count strong {
color: #f5222d;
font-weight: 600;
margin-right: 2px;
}
code { code {
background: #f5f7fa; background: #f5f7fa;
padding: 2px 6px; padding: 2px 6px;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment