- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider - 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译 - SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整 - ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示 - AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错 - JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射 - 诊断按 severity + 行号排序
245 lines
9.2 KiB
Markdown
245 lines
9.2 KiB
Markdown
# 导入预览 · 错误规则字段级报红设计书
|
||
|
||
> 版本:v1.0
|
||
> 日期:2026-07-31
|
||
> 适用项目:vscode-code-reviewer
|
||
> 参考文档:`2026-07-31-import-error-rule-edit-design.md`(错误规则可编辑 + 添加流程)
|
||
|
||
---
|
||
|
||
## 一、方案概览
|
||
|
||
### 1.1 目标
|
||
|
||
导入预览中的错误规则卡片,报红从「整卡红边 + 半透明 + 顶部 issues 横幅」改为**字段级报红**:
|
||
|
||
- 整卡恢复普通样式
|
||
- 仅出错的输入框加红框高亮,错误原因文字显示在该字段下方
|
||
- 用户修复字段时**实时清除**该字段的红框与提示
|
||
- 「添加」失败与 id 冲突同样精确定位到具体字段
|
||
|
||
### 1.2 现状 → 改造
|
||
|
||
```
|
||
现状:
|
||
┌─ 规则卡片(opacity:0.7 + 红边框)─────────────┐
|
||
│ [需修复后点击添加] │
|
||
│ ⚠ description 为空 │ ← 顶部 issues 横幅
|
||
│ ⚠ message 为空 │
|
||
│ [id] [severity] [description] [message] ... │ ← 全部无高亮
|
||
└────────────────────────────────────────────────┘
|
||
|
||
改造后:
|
||
┌─ 规则卡片(普通样式)─────────────────────────┐
|
||
│ [需修复后点击添加] │
|
||
│ [id] │
|
||
│ [severity] │
|
||
│ [description] ← 红框 │
|
||
│ ⚠ description 为空 │ ← 字段下方提示
|
||
│ [message] ← 红框 │
|
||
│ ⚠ message 为空 │
|
||
└────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.3 范围
|
||
|
||
| 类型 | 内容 |
|
||
|------|------|
|
||
| 含 | 初始错误字段红框 + 字段下方原因;添加失败/id 冲突定位到字段;实时清除;卡片样式还原 |
|
||
| 不含 | 新增 languages/excludeLanguages 校验(维持现状);错误卡片以外的样式改动 |
|
||
| 不触碰 | `import-service.ts`、`import-types.ts`、i18n 结构、扩展端去重流程 |
|
||
|
||
---
|
||
|
||
## 二、详细实现
|
||
|
||
改动文件**仅 `src/rules/import-preview.ts`**(前端渲染 + JS + 少量 CSS,扩展端消息协议兼容扩展)。
|
||
|
||
### 2.1 字段 → 表单元素映射
|
||
|
||
| field | 表单元素 |
|
||
|-------|---------|
|
||
| `id` | `.id-display-input` |
|
||
| `severity` | `.edit-field select` |
|
||
| `description` | `.edit-field textarea`(第 1 个) |
|
||
| `message` | `.edit-field textarea`(第 2 个) |
|
||
|
||
### 2.2 `renderRuleCard` 错误分支改造
|
||
|
||
1. **卡片样式还原**:去掉 `opacity:0.7` 与红边框,`cardStyle` 仅保留错误徽标。
|
||
2. **去掉顶部 issues 横幅**:删除 `issuesHtml` 变量及其渲染。
|
||
3. **字段级渲染**:渲染各 `edit-field` 时,若 `validationIssues` 中存在对应 `field`,给该 `edit-field` 追加:
|
||
|
||
```html
|
||
<div class="edit-field field-error">
|
||
<label>description(规则描述)</label>
|
||
<textarea rows="2" ...>...</textarea>
|
||
<div class="field-error-msg">⚠ description 为空</div>
|
||
</div>
|
||
```
|
||
|
||
- `edit-field` 加 `field-error` 类;输入控件本身加 `field-error-input` 类
|
||
- 提示文字 `<div class="field-error-msg">⚠ {message}</div>` 插在输入控件之后、`edit-field` 内部末尾
|
||
|
||
实现方式:渲染前构建 `const issueByField = new Map((rule.validationIssues||[]).map(i => [i.field, i]))`;渲染 severity/description/message 三个字段时按 map 命中追加。
|
||
|
||
### 2.3 `validateRule` 返回字段
|
||
|
||
```ts
|
||
function validateRule(rule: ImportableRule): {
|
||
field: 'id' | 'severity' | 'description' | 'message';
|
||
message: string;
|
||
} | null {
|
||
if (!rule.id || !rule.id.trim()) {
|
||
return { field: 'id', message: t('import.validationIdEmpty') };
|
||
}
|
||
if (!['error', 'warning', 'info'].includes(rule.severity)) {
|
||
return { field: 'severity', message: t('import.validationSeverityInvalid') };
|
||
}
|
||
if (!rule.description || !rule.description.trim()) {
|
||
return { field: 'description', message: t('import.validationDescEmpty', { 0: rule.id }) };
|
||
}
|
||
if (!rule.message || !rule.message.trim()) {
|
||
return { field: 'message', message: t('import.validationMsgEmpty', { 0: rule.id }) };
|
||
}
|
||
return null;
|
||
}
|
||
```
|
||
|
||
`handleAddErrorRule` 中:
|
||
- `validateRule` 失败 → `postMessage({ type:'addError', ruleId, field, message })`
|
||
- id 冲突 → `postMessage({ type:'addError', ruleId, field:'id', message: t('import.idConflict', ...) })`
|
||
|
||
### 2.4 前端消息处理改造
|
||
|
||
`showCardError(ruleId, message)` → `showCardError(ruleId, field, message)`:
|
||
|
||
```js
|
||
function setFieldError(card, field, message) {
|
||
const el = fieldElement(card, field);
|
||
if (!el) return;
|
||
el.classList.add('field-error-input');
|
||
const wrap = el.closest('.edit-field');
|
||
if (!wrap) return;
|
||
wrap.classList.add('field-error');
|
||
let msg = wrap.querySelector('.field-error-msg');
|
||
if (!msg) {
|
||
msg = document.createElement('div');
|
||
msg.className = 'field-error-msg';
|
||
wrap.appendChild(msg);
|
||
}
|
||
msg.textContent = '⚠ ' + message;
|
||
}
|
||
|
||
function clearFieldError(card, field) {
|
||
const el = fieldElement(card, field);
|
||
if (!el) return;
|
||
el.classList.remove('field-error-input');
|
||
const wrap = el.closest('.edit-field');
|
||
if (wrap) {
|
||
wrap.classList.remove('field-error');
|
||
const msg = wrap.querySelector('.field-error-msg');
|
||
if (msg) msg.remove();
|
||
}
|
||
}
|
||
|
||
function fieldElement(card, field) {
|
||
if (field === 'id') return card.querySelector('.id-display-input');
|
||
if (field === 'severity') return card.querySelector('.edit-field select');
|
||
const tas = card.querySelectorAll('.edit-field textarea');
|
||
return field === 'description' ? (tas[0] || null) : (tas[1] || null);
|
||
}
|
||
```
|
||
|
||
`window.addEventListener('message')` 中 `addError` 分支改为透传 `field`。
|
||
|
||
### 2.5 实时清除(事件委托)
|
||
|
||
在 `document` 上委托监听 `input` 与 `change`:
|
||
|
||
```js
|
||
function liveClear(event) {
|
||
const card = event.target.closest('.rule-card');
|
||
if (!card || !card.hasAttribute('data-error')) return;
|
||
const target = event.target;
|
||
if (target.classList.contains('id-display-input') || target.classList.contains('rule-id-input')) {
|
||
if (target.value.trim()) clearFieldError(card, 'id');
|
||
} else if (target.tagName === 'SELECT') {
|
||
clearFieldError(card, 'severity');
|
||
} else if (target.tagName === 'TEXTAREA') {
|
||
const tas = card.querySelectorAll('.edit-field textarea');
|
||
const field = tas[0] === target ? 'description' : (tas[1] === target ? 'message' : null);
|
||
if (field && target.value.trim()) clearFieldError(card, field);
|
||
}
|
||
}
|
||
document.addEventListener('input', liveClear);
|
||
document.addEventListener('change', liveClear);
|
||
```
|
||
|
||
> severity 下拉天然只会给出合法值,故 `change` 即清除;id/description/message 以非空 trim 判定。
|
||
|
||
### 2.6 `moveCardToSection` 清理
|
||
|
||
卡片移入有效分区后,清除该卡全部字段级错误:
|
||
|
||
```js
|
||
function clearCardFieldErrors(card) {
|
||
card.querySelectorAll('.field-error-input').forEach(el => {
|
||
el.classList.remove('field-error-input');
|
||
});
|
||
card.querySelectorAll('.field-error').forEach(wrap => {
|
||
wrap.classList.remove('field-error');
|
||
const msg = wrap.querySelector('.field-error-msg');
|
||
if (msg) msg.remove();
|
||
});
|
||
}
|
||
```
|
||
|
||
在移除 `data-error` 后调用。
|
||
|
||
### 2.7 CSS
|
||
|
||
```css
|
||
.field-error-input {
|
||
border-color: rgba(248,81,73,0.7) !important;
|
||
box-shadow: 0 0 0 1px rgba(248,81,73,0.25);
|
||
}
|
||
.field-error-msg {
|
||
color: #f48771; font-size: 11px; margin-top: 4px;
|
||
}
|
||
```
|
||
|
||
删除不再使用的 `.error-issues` 规则(或保留无引用,推荐删除)。
|
||
|
||
---
|
||
|
||
## 三、边界与影响
|
||
|
||
| 边界 | 处理 |
|
||
|------|------|
|
||
| 同一卡片多字段出错 | 每个字段独立红框 + 独立提示,互不影响 |
|
||
| 字段修复后再点「添加」 | 校验通过即入区;实时清除逻辑保证先显示绿色状态 |
|
||
| 添加失败(未修复) | 红框/提示重新命中对应字段 |
|
||
| id 冲突 | `field:'id'` 定位到 id 输入框 |
|
||
| severity 原始非法但默认值为合法 | 红框+提示展示原始问题,下拉 change 即清除 |
|
||
| 确认导入校验(`validate()`) | 仍跳过 `data-error` 卡片,行为不变 |
|
||
| 非模板导入路径 | 无 `validationIssues`,不受影响 |
|
||
|
||
## 四、文件变更清单
|
||
|
||
| 文件 | 操作 | 内容 |
|
||
|------|------|------|
|
||
| `src/rules/import-preview.ts` | 修改 | 卡片样式还原;字段级错误渲染;`validateRule` 返回字段;`addError` 消息带 field;`setFieldError`/`clearFieldError`/`clearCardFieldErrors`/`fieldElement`;input/change 委托实时清除;`moveCardToSection` 清理;CSS `.field-error-*` |
|
||
|
||
i18n、import-service、import-types 均不改动。
|
||
|
||
## 五、验证
|
||
|
||
- `npm run lint` + `npm run compile`
|
||
- 复用既有 harness 思路,mock vscode 渲染 webview 脚本并校验语法
|
||
- 扩展端消息流验证:
|
||
- 初始错误卡片:description/message 空 → 对应文本域带 `field-error-input`,无整卡红边、无顶部横幅
|
||
- 添加失败(description 空)→ `addError` 消息带 `field:'description'`
|
||
- id 冲突 → `addError` 消息带 `field:'id'`
|
||
- 模拟 input 事件 → 修复后红框清除
|