# 导入预览 · 错误规则字段级报红设计书 > 版本: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 事件 → 修复后红框清除