Commit 1c07dd8c authored by Data Governance Dev's avatar Data Governance Dev

feat(web): 检查项加 info icon + hover 4 段式说明(作用/查什么/规则/格式)

要点:

- 后端:StepDetail 从 3 段扩到 4 段(加 check 段,校验规则细节)
- BaseStandard 加可选 check_detail 字段(子类可覆写,默认空)
- 4 个基础 step 手填 4 段详细说明(含步骤编号 / 阈值 / 处理策略)
- 24 个 indicator 通过 _derive_check() 派生:每个 indicator 一段
  「业务背景 + 校验要点」摘要(含正则 / MOD 算法 / 号段表 /
  国标号 / 业务标准引用)
- 前端:每个 leaf 行 checkbox 后加灰色圆 i 图标,hover 弹出 4 段
  tooltip,规则段用 <pre> 暗色等宽字体渲染多行

修两个 root cause:

1. <span class="tree-caret-spacer" /> 自闭合写法在 Vue 模板里被吞——
   所有后续兄弟节点(el-checkbox / el-tooltip / info-icon)全被塞进
   20px 宽的 spacer 里 overflow:hidden 截掉
   → 改为显式 <span class="tree-caret-spacer"></span>

2. flatCheckRows push leaf 时没透出 c.detail,<el-tooltip v-if="row.detail">
   永远不渲染 → 加 detail: c.detail || null

验证:jsdom + 真 Element Plus 跑 leaf 段渲染,info-icon + tooltip
内容完整;后端 4 段全填(merge_redundancy / IND-001-a / IND-301 /
IND-502 等抽样验证)。

兼容性:StepDetail.check / BaseStandard.check_detail 都是 Optional,
旧调用 register_step 不传 detail 也不会报错。
parent 01a2d9b0
...@@ -2,6 +2,162 @@ ...@@ -2,6 +2,162 @@
> 任务做完一次记一次。最近的在最上面。 > 任务做完一次记一次。最近的在最上面。
## 2026-08-10 · 检查项 info icon:扩 4 段 + 修两个 root cause
### 用户原话(这一轮两个迭代)
> 在每一个检查项目后面都加一个 info 的 icon,鼠标放上去的时候显示这个检查的作用,查哪些对象,检查格式是什么样的等信息
> 内容太简单了,要写详细一些
### 改动总览
#### 1. 后端元数据:StepDetail 4 段
[web/core/orchestrator.py:48-60](web/core/orchestrator.py#L48-L60) `StepDetail` 从 3 段扩到 4 段:
- `purpose` 业务目标 + 产出形式
- `target` 查哪些字段 / 表 / 注释关键字
- `check` 校验规则细节(多行 / 阈值 / 算法摘要)
- `format` 检查依据的标准 / 算法 / 阈值
#### 2. 数据来源
- 4 个基础 step(merge_redundancy / empty_fields / missing_comments / length_check)手填 4 段,每段多行详细说明(含步骤编号 + 阈值 + 阈值处理)
- 24 个 indicator 在 [web/core/orchestrator.py:226-291](web/core/orchestrator.py#L226-L291) 新增 `_derive_check()`:每个 indicator 一段「业务背景 + 校验要点」摘要(含正则 / MOD 算法 / 号段表 / 国标号 / 业务标准引用)
- IND-001-a/b/c/d → 身份证正则 + ISO 7064 MOD 11-2 + 日期合法性 + 15 位兼容
- IND-002-a/b → GB 32100-2015 31 字符集 + MOD 31-3
- IND-003-a/b → 工信部 11 位正则 + 号段表
- IND-004-a/b → GB/T 2260 区码格式 + 存在性
- IND-101/201/202/203/204 → 5 值枚举 + 各证件号码格式
- IND-301/302 → 跨字段 + 跨表判重
- IND-401/402/501/502 → 姓名 + 出生日期年龄范围
- IND-601/602/603 → GB/T 3304 / 2261 系列
- [standards/base.py:65](standards/base.py#L65) `BaseStandard` 加可选 `check_detail` 字段(子类可覆写,默认空 → orchestrator 走 _derive_check)
#### 3. API 透出
[web/api/routes.py](web/api/routes.py) `/api/analysis-tree` 把 `check` 段加到每条 leaf(主分组 + ungrouped 兜底都加)。
#### 4. 前端 4 段 tooltip
- [web/static/index.html:295-298](web/static/index.html#L295-L298) 加 `step-detail-row--block` + `<pre class="step-detail-pre">` 渲染多行规则
- [web/static/style.css:319-348](web/static/style.css#L319-L348) `.step-detail-pre`:暗色等宽字体 + 圆角 + 白灰色文字;`.step-detail` max-width 360 → 420
- [web/static/index.html:691](web/static/index.html#L691) `flatCheckRows` push leaf 时透出 `c.detail`(否则 v-if="row.detail" 不渲染)
### 关键 root cause(这次踩了两个)
#### root cause 1:`<span class="tree-caret-spacer" />` 自闭合吞掉后续兄弟
Vue 模板编译器把 `<span ... />` 当自闭合解析,但浏览器实际渲染时 `<span>` 不是 void 元素,后续兄弟节点(el-checkbox / el-tooltip / info-icon)全被吞进 tree-caret-spacer 里。
而 tree-caret-spacer CSS 是 `width: 20px`(占位对齐用),所有内容被挤在 20px 容器里 `overflow: hidden` 截掉。
**修复**:[web/static/index.html:269](web/static/index.html#L269) — `<span class="tree-caret-spacer" />` → `<span class="tree-caret-spacer"></span>` 显式闭合
**验证**:grep 全文件 `<span .../>` / `<div .../>` 自闭合 = 0 处
**用户反馈 HTML 验证**:`<div class="tree-row tree-row--leaf" style="padding-left: 32px;"><span class="tree-caret-spacer"><label class="el-checkbox is-checked">...</label><!----></span></div>` —— 用户的 DevTools 截图证实
#### root cause 2:`flatCheckRows` 没透出 `c.detail`
`<el-tooltip v-if="row.detail">` 因为 row.detail 永远是 undefined,整个 tooltip + info-icon 都不渲染。
**修复**:[web/static/index.html:691](web/static/index.html#L691) — push leaf 时加 `detail: c.detail || null`
### 验证
- `python -c "from web.core.orchestrator import get_step_defs; ..."` — 4 段全填
- `curl /api/analysis-tree` — 28 个 step 都带 detail(含 check 段)
- `node scripts/verify_detail_passthrough.cjs` — 真 Element Plus + jsdom 渲染:info-icon 数 = 2(预期 2),el-checkbox__label 文本正确
- `node scripts/verify_leaf_real_elplus.cjs` — 真 Element Plus 渲染 leaf 段:info-icon = 1,el-checkbox__label = "IND-001-a · GB 11643-1999 身份证号格式"
### 兼容性
- `StepDetail.check` / `BaseStandard.check_detail` 都是 Optional 字段(默认空),旧调用 register_step 不传 detail 也不会报错
- indicator 的 _derive_check 用 if-elif 映射表(24 个),未来新增 indicator 没命中映射 → 降级用 description 全文,不丢失信息
### 未提交
按用户要求,待 review 后再提交(与上一轮「JSON 驱动树」一并合并不了,因为中间还有 root cause 修复节点,本次单独提交)。
---
## 2026-08-10 · 检查项加 info icon + hover 三段式说明
### 用户原话
> 在每一个检查项目后面都加一个 info 的 icon,鼠标放上去的时候(hover)显示这个检查的作用,查哪些对象(字段或者注释包含哪些关键字),检查格式是什么样的等信息
### 数据建模:StepDetail 三段式
- `purpose` 这一步做什么
- `target` 查哪些字段 / 表 / 注释关键字
- `format` 检查依据的标准 / 算法 / 阈值
三段都是中文短句;缺失字段前端 fallback 隐藏那一行(不留空 div)。
### 改动
#### 1. 后端元数据来源
- [standards/base.py:60](standards/base.py#L60) `BaseStandard` 加可选字段 `format_check: str = ""`(默认空,子类可覆写)
- [standards/registry.py:74-81](standards/registry.py#L74-L81) `list_all_standards()` 输出 `format_check`
- [web/core/orchestrator.py](web/core/orchestrator.py):
- 新增 `StepDetail` dataclass
- `StepDef` 加 `detail: Optional[StepDetail]`
- `register_step()` 加可选 `detail=` 参数
- 4 个基础 step(merge_redundancy / empty_fields / missing_comments / length_check)注册时手填 detail
- 新增 `_derive_format(meta)` / `_derive_target(meta)`,给 24 个 indicator 自动派生 detail
- `_derive_format`:优先级 = 显式 format_check 字段 > standard_name 正则 `(GB|GA)(/T)?\d+(...)?-(\d{4})?` > 工信部/民政部/公安部/国标/行标 > "纯规则(无国标对应)"
- `_derive_target`:IND-301 / IND-302 / IND-401 / 402 / IND-501 / 502 / IND-601 / 602 / 603 / IND-101 等无 applies_to_fields 或语义特殊的 indicator 各走特殊描述;其余统一「字段名含 xxx」
#### 2. API 透出 detail
[web/api/routes.py:114-128](web/api/routes.py#L114-L128) `/api/analysis-tree` 把 `detail` 加到每个 leaf,未分组兜底组同样处理。
#### 3. 前端 info icon + tooltip
[web/static/index.html:286-302](web/static/index.html#L286-L302) leaf 行 `</el-checkbox>` 之后加 `<el-tooltip>`,slot content 三行(v-if 各段),trigger 用 `<span class="info-icon">i</span>`(不再用 `<el-icon><InfoFilled/></el-icon>`——svg icon 在某些环境下渲染不出来)。
#### 4. CSS
[web/static/style.css:289-336](web/static/style.css#L289-L336)
- `.info-icon`:16x16 圆形,灰底白字 `i`,hover/focus 变蓝
- `.step-detail`:max-width 360px,line-height 1.6
- `.step-detail-row`:flex 行 + 上下虚线分隔
- `.step-detail-label`:蓝色加粗,「作用 / 查什么 / 格式」36px 最小宽
### 验证
```
$ curl http://localhost:8765/api/analysis-tree | jq
{
"merge_redundancy": {
"detail": {
"purpose": "找出结构高度相似的可合并表...",
"target": "用户勾选范围内的全部表结构...",
"format": "LLM 语义匹配(候选合并 + 字段归并建议)..."
}
},
"std_ind_001_a": {
"detail": {
"purpose": "长度 = 18;6 位地址 + 8 位出生日期...",
"target": "字段名含 id_card / id_card_no / id_number / identity_card",
"format": "GB 11643-1999"
}
},
"std_ind_301": {
"detail": {
"purpose": "type=01 ...(证件号码一致性跨字段校验)",
"target": "同表同行的(证件类型 + 证件号码)配对",
"format": "暂无国标对应"
}
}
}
```
```
$ node scripts/verify_info_icon.cjs
info-icon 数: 1
step-detail-row 数: 3
[0] label: "作用"
[1] label: "查什么"
[2] label: "格式"
=== tooltip 第一行内容 ===
作用长度 = 18;6 位地址 + 8 位出生日期 + 3 位顺序码 + 1 位校验位
```
### 兼容性
- StepDetail 是 Optional,向后兼容:旧调用 register_step 不传 detail 也不会报错
- format_check 在 BaseStandard 是可选字段,已有的 24 个 indicator 都不覆写(默认空)—— orchestrator 走正则兜底
- info icon 用纯 `<span>i</span>`,不依赖任何 svg icon 组件(沿用 caret 的策略)
### 未提交
按用户要求,待 review 后再提交。
---
## 2026-08-10 · 树表改造 v3:用 Vue 3.5.40 prod build 真渲染,DOM 完全正确 ## 2026-08-10 · 树表改造 v3:用 Vue 3.5.40 prod build 真渲染,DOM 完全正确
### 用户反馈 ### 用户反馈
......
// 简化:直接传对象给 Vue data,Vue 会把它包成 reactive proxy
const fs = require('fs');
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html>', {
runScripts: 'dangerously',
resources: 'usable',
url: 'http://localhost:8765/',
});
const win = dom.window;
const css = fs.readFileSync('web/static/lib/element-plus/index.css', 'utf8');
const style = win.document.createElement('style');
style.textContent = css;
win.document.head.appendChild(style);
const vue = fs.readFileSync('web/static/lib/vue.global.prod.js', 'utf8');
win.eval(vue);
const ep = fs.readFileSync('web/static/lib/element-plus/index.full.min.js', 'utf8');
win.eval(ep);
function waitFor(pred, name, tries = 200) {
return new Promise((resolve, reject) => {
let n = 0;
const t = () => {
if (pred()) return resolve();
if (++n > tries) return reject(new Error(name + ' 未加载'));
setTimeout(t, 50);
};
t();
});
}
(async () => {
await waitFor(() => win.Vue && win.ElementPlus, 'Vue + ElementPlus');
const Vue = win.Vue;
const ElementPlus = win.ElementPlus;
const analysisTree = {
groups: [
{
key: 'g1', title: '基础检查', default_expand: true, description: 'desc1',
checks: [
{ step_id: 'merge_redundancy', title: '表合并与冗余字段分析',
llm_mode: 'required', required: false,
detail: { purpose: '找出可合并表', target: '全部表', format: 'LLM' } },
{ step_id: 'empty_fields', title: '大范围空字段扫描',
llm_mode: 'none', required: false,
detail: { purpose: '扫描空字段', target: '全部字段', format: '阈值 80%' } },
],
},
],
};
const app = Vue.createApp({
data() { return { analysisTree, form: { steps: [] }, expandedGroups: ['g1'] }; },
computed: {
flatCheckRows() {
const out = [];
for (const g of this.analysisTree.groups || []) {
out.push({ key: g.key, _level: 0, is_group: true, title: g.title });
if (this.expandedGroups.includes(g.key)) {
for (const c of (g.checks || [])) {
out.push({
key: c.step_id, _level: 1, is_group: false,
step_id: c.step_id, title: c.title,
llm_mode: c.llm_mode, required: c.required,
detail: c.detail || null,
});
}
}
}
return out;
},
},
methods: { toggleStep() {} },
template: `
<div class="checks-tree-table">
<div v-for="row in flatCheckRows" :key="row.key" class="tree-row"
:class="row.is_group ? 'tree-row--group' : 'tree-row--leaf'">
<template v-if="!row.is_group">
<span class="tree-caret-spacer"></span>
<el-checkbox :model-value="false">
<span>{{ row.title }}</span>
</el-checkbox>
<el-tooltip v-if="row.detail" placement="top" effect="dark">
<template #content>
<div class="step-detail">
<div v-if="row.detail.purpose" class="step-detail-row">
<span class="step-detail-label">作用</span>
<span>{{ row.detail.purpose }}</span>
</div>
<div v-if="row.detail.target" class="step-detail-row">
<span class="step-detail-label">查什么</span>
<span>{{ row.detail.target }}</span>
</div>
<div v-if="row.detail.format" class="step-detail-row">
<span class="step-detail-label">格式</span>
<span>{{ row.detail.format }}</span>
</div>
</div>
</template>
<span class="info-icon" tabindex="0" role="button">i</span>
</el-tooltip>
</template>
</div>
</div>
`,
});
app.use(ElementPlus);
const container = win.document.createElement('div');
win.document.body.appendChild(container);
app.mount(container);
await new Promise(r => setTimeout(r, 200));
const infoIcons = container.querySelectorAll('.info-icon');
const tooltips = container.querySelectorAll('.el-tooltip');
const detailRows = container.querySelectorAll('.step-detail-row');
const labels = container.querySelectorAll('.el-checkbox__label');
console.log('=== 验证结果 ===');
console.log('info-icon 数:', infoIcons.length, '(预期 2)');
console.log('el-tooltip 数:', tooltips.length);
console.log('step-detail-row 数:', detailRows.length, '(预期 6)');
console.log('el-checkbox__label 数:', labels.length, '(预期 2)');
if (labels.length) {
console.log(' label[0]:', JSON.stringify(labels[0].textContent.trim()));
}
})();
\ No newline at end of file
// 验证 info icon + tooltip 渲染
const fs = require('fs');
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html>', { runScripts: 'dangerously', url: 'http://localhost:8765/' });
const win = dom.window;
const vue = fs.readFileSync('web/static/lib/vue.global.prod.js', 'utf8');
win.eval(vue);
const Vue = win.Vue;
// 拉一次真服务器的 HTML 拿实际模板
const { execSync } = require('child_process');
const html = execSync('curl -s http://localhost:8765/static/index.html', { encoding: 'utf8' });
// 抽出 leaf 模板段
const start = html.indexOf('<span class="tree-caret-spacer"');
const end = html.indexOf('</template>\n </div>', start);
const tpl = html.substring(start, end + '</template>'.length);
console.log('=== 截取到的 leaf 模板 (前 500 字符) ===');
console.log(tpl.substring(0, 500));
console.log('\n=== 含 info-icon 关键字 ===', tpl.includes('info-icon') ? 'YES' : 'NO');
console.log('=== 含 step-detail 关键字 ===', tpl.includes('step-detail') ? 'YES' : 'NO');
console.log('=== 含 row.detail ===', tpl.includes('row.detail') ? 'YES' : 'NO');
console.log('=== 含 el-tooltip ===', (tpl.match(/el-tooltip/g) || []).length, '处');
const ElCheckbox = {
props: ['modelValue', 'indeterminate', 'disabled'],
emits: ['change', 'update:modelValue'],
template: '<label class="el-checkbox"><input type="checkbox" /><span><slot /></span></label>',
};
const ElTooltip = {
props: ['placement', 'effect'],
template: '<span class="el-tooltip"><slot /><div class="el-tooltip__popper"><slot name="content" /></div></span>',
};
const row = {
key: 'std_ind_001_a',
step_id: 'std_ind_001_a',
title: 'IND-001-a · GB 11643-1999 身份证号格式',
llm_mode: 'none',
required: false,
detail: {
purpose: '长度 = 18;6 位地址 + 8 位出生日期 + 3 位顺序码 + 1 位校验位',
target: '字段名含 id_card / id_card_no / id_number / identity_card',
format: 'GB 11643-1999',
},
};
const App = {
data() { return { row, form: { steps: ['std_ind_001_a'] } }; },
methods: { toggleStep:()=>{} },
template: '<div>' + tpl + '</div>',
};
const app = Vue.createApp(App);
app.component('el-checkbox', ElCheckbox);
app.component('el-tooltip', ElTooltip);
const container = win.document.createElement('div');
win.document.body.appendChild(container);
app.mount(container);
const infoIcon = container.querySelector('.info-icon');
const tooltipRows = container.querySelectorAll('.step-detail-row');
const tooltipLabels = container.querySelectorAll('.step-detail-label');
console.log('\n=== 渲染结果 ===');
console.log('info-icon 数:', infoIcon ? 1 : 0);
console.log('step-detail-row 数:', tooltipRows.length);
tooltipLabels.forEach((l, i) => console.log(` [${i}] label: "${l.textContent.trim()}"`));
if (tooltipRows.length) {
console.log('\n=== tooltip 第一行内容 ===');
console.log(tooltipRows[0].textContent.trim());
console.log('\n=== tooltip 第二行内容 ===');
console.log(tooltipRows[1].textContent.trim());
console.log('\n=== tooltip 第三行内容 ===');
console.log(tooltipRows[2].textContent.trim());
}
\ No newline at end of file
// 用 prod Vue 渲染真实 leaf 模板(不加任何 mock el-checkbox)
// 模拟 Element Plus 的 el-checkbox 真实 DOM 结构
const fs = require('fs');
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html>', { runScripts: 'dangerously', url: 'http://localhost:8765/' });
const win = dom.window;
const vue = fs.readFileSync('web/static/lib/vue.global.prod.js', 'utf8');
win.eval(vue);
const Vue = win.Vue;
// 拉真服务器 HTML 取模板
const { execSync } = require('child_process');
const html = execSync('curl -s http://localhost:8765/static/index.html', { encoding: 'utf8' });
const start = html.indexOf('<span class="tree-caret-spacer"');
const end = html.indexOf('</template>\n </div>', start);
const tpl = html.substring(start, end + '</template>'.length);
const row = {
key: 'std_ind_001_a',
step_id: 'std_ind_001_a',
title: 'IND-001-a · GB 11643-1999 身份证号格式',
llm_mode: 'none',
required: false,
detail: {
purpose: '长度 = 18',
target: '字段名含 id_card',
format: 'GB 11643-1999',
},
};
// 模拟 Element Plus el-checkbox 真实 DOM 结构
const ElCheckbox = {
name: 'ElCheckbox',
props: ['modelValue', 'indeterminate', 'disabled', 'label'],
emits: ['change', 'update:modelValue'],
template: `
<label class="el-checkbox" :class="{ 'is-disabled': disabled, 'is-checked': modelValue, 'is-indeterminate': indeterminate }">
<span class="el-checkbox__input">
<input type="checkbox" :disabled="disabled" :checked="modelValue" class="el-checkbox__original" />
<span class="el-checkbox__inner"></span>
</span>
<span class="el-checkbox__label">
<slot />
</span>
</label>
`,
};
const ElTooltip = {
name: 'ElTooltip',
props: ['placement', 'effect', 'content'],
template: '<span class="el-tooltip"><slot /><slot name="content" /></span>',
};
const ElTag = {
name: 'ElTag',
props: ['size', 'type', 'effect'],
template: '<span class="el-tag" :class="type ? `el-tag--${type}` : \'\'"><slot /></span>',
};
const App = {
data() { return { row, form: { steps: ['std_ind_001_a'] } }; },
methods: { toggleStep:()=>{} },
template: '<div>' + tpl + '</div>',
};
const app = Vue.createApp(App);
app.component('el-checkbox', ElCheckbox);
app.component('el-tooltip', ElTooltip);
app.component('el-tag', ElTag);
const container = win.document.createElement('div');
win.document.body.appendChild(container);
try {
app.mount(container);
console.log('=== 渲染结果 ===');
console.log(container.innerHTML);
console.log('\n=== 关键元素 ===');
console.log('el-checkbox 数:', container.querySelectorAll('.el-checkbox').length);
console.log('el-checkbox__label 数:', container.querySelectorAll('.el-checkbox__label').length);
const label = container.querySelector('.el-checkbox__label');
if (label) console.log('label textContent:', JSON.stringify(label.textContent.trim()));
console.log('info-icon 数:', container.querySelectorAll('.info-icon').length);
} catch (e) {
console.error('mount ERROR:', e.message);
console.error(e.stack);
}
\ No newline at end of file
// 用真 Element Plus 跑完整模板,看 el-checkbox 实际怎么渲染
const fs = require('fs');
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html>', {
runScripts: 'dangerously',
resources: 'usable',
pretendToBeVisual: true,
url: 'http://localhost:8765/',
});
const win = dom.window;
// 注入 Element Plus CSS(jsdom 模拟)
const css = fs.readFileSync('web/static/lib/element-plus/index.css', 'utf8');
const style = win.document.createElement('style');
style.textContent = css;
win.document.head.appendChild(style);
// 加载 Vue + Element Plus
const vue = fs.readFileSync('web/static/lib/vue.global.prod.js', 'utf8');
win.eval(vue);
const ep = fs.readFileSync('web/static/lib/element-plus/index.full.min.js', 'utf8');
win.eval(ep);
function waitFor(predicate, name, maxTries = 200) {
return new Promise((resolve, reject) => {
let tries = 0;
const tick = () => {
if (predicate()) return resolve();
if (++tries > maxTries) return reject(new Error(`${name} 未加载`));
setTimeout(tick, 50);
};
tick();
});
}
(async () => {
try {
await waitFor(() => win.Vue && win.ElementPlus, 'Vue + ElementPlus');
console.log('Vue + ElementPlus loaded');
} catch (e) {
console.error(e.message);
process.exit(1);
}
const Vue = win.Vue;
const ElementPlus = win.ElementPlus;
// 拉真服务器 HTML 取模板
const { execSync } = require('child_process');
const html = execSync('curl -s http://localhost:8765/static/index.html', { encoding: 'utf8' });
const start = html.indexOf('<span class="tree-caret-spacer"');
const end = html.indexOf('</template>\n </div>', start);
const tpl = html.substring(start, end + '</template>'.length);
const row = {
key: 'std_ind_001_a',
step_id: 'std_ind_001_a',
title: 'IND-001-a · GB 11643-1999 身份证号格式',
llm_mode: 'none',
required: false,
detail: {
purpose: '长度 = 18',
target: '字段名含 id_card',
format: 'GB 11643-1999',
},
};
const App = {
data() { return { row, form: { steps: ['std_ind_001_a'] } }; },
methods: { toggleStep:()=>{} },
template: '<div>' + tpl + '</div>',
};
const app = Vue.createApp(App);
app.use(ElementPlus);
const container = win.document.createElement('div');
win.document.body.appendChild(container);
try {
app.mount(container);
// 等一帧让 popper 之类 ready
await new Promise(r => setTimeout(r, 300));
console.log('=== 渲染结果 ===');
console.log(container.innerHTML.substring(0, 2000));
console.log('\n=== 关键元素 ===');
console.log('el-checkbox 数:', container.querySelectorAll('.el-checkbox').length);
console.log('el-checkbox__label 数:', container.querySelectorAll('.el-checkbox__label').length);
const labels = container.querySelectorAll('.el-checkbox__label');
labels.forEach((l, i) => {
console.log(` label[${i}] textContent: ${JSON.stringify(l.textContent.trim())}`);
});
console.log('info-icon 数:', container.querySelectorAll('.info-icon').length);
} catch (e) {
console.error('mount ERROR:', e.message);
console.error(e.stack);
}
})();
\ No newline at end of file
...@@ -55,6 +55,11 @@ class BaseStandard(ABC): ...@@ -55,6 +55,11 @@ class BaseStandard(ABC):
group: str = "通用" group: str = "通用"
severity: str = "warning" severity: str = "warning"
depends_on: list[str] | None = None depends_on: list[str] | None = None
# 检查格式 / 标准的精炼描述(前端 hover tooltip 用)。
# 留空时 orchestrator 会从 standard_name 正则提取 GB/GA 号作兜底。
format_check: str = ""
# 详细校验规则说明(前端 tooltip「规则」段;留空时降级用 description 全文)
check_detail: str = ""
def __init__(self): def __init__(self):
# 子类可省略 __init__;基类负责把 None 兜底为 [] # 子类可省略 __init__;基类负责把 None 兜底为 []
......
...@@ -77,6 +77,8 @@ def list_all_standards() -> list[dict]: ...@@ -77,6 +77,8 @@ def list_all_standards() -> list[dict]:
"description": inst.description, "description": inst.description,
"group": getattr(inst, "group", "通用"), "group": getattr(inst, "group", "通用"),
"severity": getattr(inst, "severity", "warning"), "severity": getattr(inst, "severity", "warning"),
"format_check": getattr(inst, "format_check", "") or "",
"check_detail": getattr(inst, "check_detail", "") or "",
}) })
return out return out
......
...@@ -113,6 +113,7 @@ async def analysis_tree(): ...@@ -113,6 +113,7 @@ async def analysis_tree():
missing.append(sid) missing.append(sid)
continue continue
referenced.add(sid) referenced.add(sid)
detail = sd.detail
children.append({ children.append({
"step_id": sd.step_id, "step_id": sd.step_id,
"title": sd.title, "title": sd.title,
...@@ -120,6 +121,13 @@ async def analysis_tree(): ...@@ -120,6 +121,13 @@ async def analysis_tree():
"llm_mode": sd.llm_mode, "llm_mode": sd.llm_mode,
"required": sd.required, "required": sd.required,
"order": sd.order, "order": sd.order,
"detail": (
{"purpose": detail.purpose,
"target": detail.target,
"check": detail.check,
"format": detail.format}
if detail else None
),
}) })
out_groups.append({ out_groups.append({
"key": g.get("key"), "key": g.get("key"),
...@@ -142,6 +150,13 @@ async def analysis_tree(): ...@@ -142,6 +150,13 @@ async def analysis_tree():
{ {
"step_id": s.step_id, "title": s.title, "description": s.description, "step_id": s.step_id, "title": s.title, "description": s.description,
"llm_mode": s.llm_mode, "required": s.required, "order": s.order, "llm_mode": s.llm_mode, "required": s.required, "order": s.order,
"detail": (
{"purpose": s.detail.purpose,
"target": s.detail.target,
"check": s.detail.check,
"format": s.detail.format}
if s.detail else None
),
} for s in sorted(orphan, key=lambda x: x.order) } for s in sorted(orphan, key=lambda x: x.order)
], ],
}) })
......
This diff is collapsed.
...@@ -266,7 +266,7 @@ ...@@ -266,7 +266,7 @@
</el-checkbox> </el-checkbox>
</template> </template>
<template v-else> <template v-else>
<span class="tree-caret-spacer" /> <span class="tree-caret-spacer"></span>
<el-checkbox <el-checkbox
:model-value="form.steps.includes(row.step_id)" :model-value="form.steps.includes(row.step_id)"
:disabled="row.required" :disabled="row.required"
...@@ -282,6 +282,29 @@ ...@@ -282,6 +282,29 @@
<el-tag v-if="row.llm_mode === 'required'" size="small" type="warning" effect="plain" class="badge">需 LLM</el-tag> <el-tag v-if="row.llm_mode === 'required'" size="small" type="warning" effect="plain" class="badge">需 LLM</el-tag>
<el-tag v-if="row.required" size="small" type="danger" effect="dark" class="badge">必选</el-tag> <el-tag v-if="row.required" size="small" type="danger" effect="dark" class="badge">必选</el-tag>
</el-checkbox> </el-checkbox>
<el-tooltip v-if="row.detail" placement="top" effect="dark">
<template #content>
<div class="step-detail">
<div v-if="row.detail.purpose" class="step-detail-row">
<span class="step-detail-label">作用</span>
<span>{{ row.detail.purpose }}</span>
</div>
<div v-if="row.detail.target" class="step-detail-row">
<span class="step-detail-label">查什么</span>
<span>{{ row.detail.target }}</span>
</div>
<div v-if="row.detail.check" class="step-detail-row step-detail-row--block">
<span class="step-detail-label">规则</span>
<pre class="step-detail-pre">{{ row.detail.check }}</pre>
</div>
<div v-if="row.detail.format" class="step-detail-row">
<span class="step-detail-label">格式</span>
<span>{{ row.detail.format }}</span>
</div>
</div>
</template>
<span class="info-icon" tabindex="0" role="button" aria-label="检查项说明">i</span>
</el-tooltip>
</template> </template>
</div> </div>
</div> </div>
...@@ -669,6 +692,7 @@ ...@@ -669,6 +692,7 @@
title: c.title, title: c.title,
llm_mode: c.llm_mode, llm_mode: c.llm_mode,
required: c.required, required: c.required,
detail: c.detail || null,
}); });
} }
} }
......
...@@ -290,6 +290,69 @@ body { ...@@ -290,6 +290,69 @@ body {
margin: 0 4px; margin: 0 4px;
flex-shrink: 0; flex-shrink: 0;
} }
/* 检查项说明 info icon(hover 显示 purpose/target/format) */
.info-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
background: #909399;
color: #fff;
font-size: 11px;
font-style: italic;
font-weight: 600;
margin-left: 6px;
cursor: help;
user-select: none;
flex-shrink: 0;
transition: background 0.15s;
}
.info-icon:hover,
.info-icon:focus {
background: #409eff;
outline: none;
}
.step-detail {
max-width: 420px;
line-height: 1.6;
font-size: 13px;
}
.step-detail-row {
display: flex;
gap: 8px;
align-items: baseline;
margin: 2px 0;
}
.step-detail-row + .step-detail-row {
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed rgba(255, 255, 255, 0.15);
}
.step-detail-row--block {
display: block;
}
.step-detail-pre {
margin: 4px 0 0;
padding: 6px 8px;
background: rgba(0, 0, 0, 0.25);
border-radius: 3px;
font-family: "Consolas", "Monaco", monospace;
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
color: #e6e6e6;
}
.step-detail-label {
flex-shrink: 0;
color: #409eff;
font-weight: 600;
min-width: 36px;
}
/* ── 进度条(上) ── */ /* ── 进度条(上) ── */
.progress-block { .progress-block {
......
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