# 导入预览 · 错误规则字段级报红设计书
> 版本: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
⚠ description 为空
```
- `edit-field` 加 `field-error` 类;输入控件本身加 `field-error-input` 类
- 提示文字 `⚠ {message}
` 插在输入控件之后、`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 事件 → 修复后红框清除