feat: Linter 规则精细化增强 + 模板导入/导出闭环
ESLint: +29 条 P1/P2 规则 + 12 条 TS 专属规则(含 no-shadow/no-array-constructor 冲突处理) Stylelint: 集成 stylelint-config-recommended + 27 条额外规则 PMD: 排除 20 弃用 + 17 噪音规则,补启 Security/Multithreading,274+12 条精选 SQL-lint: 内置精选 57 条规则配置 + 按 tier 分级 severity + 无项目配置时自动注入临时配置 模板导出: export-service.ts 导出 2-sheet xlsx(复用 xlsx 零新依赖) 模板导入: template-converter.ts 固定列映射解析 + dedup-prompt.ts AI 语义去重 导入预览增强: 错误规则分组置顶只读、跳过行提示、空数据提示 i18n: 新增 18 条模板导入/导出相关翻译 WebView: 静态分析/自定义规则项默认可展开显示 suggestion
This commit is contained in:
@@ -15,7 +15,9 @@ export async function showImportPreview(
|
||||
|
||||
const keepRule: Record<string, boolean> = {};
|
||||
for (const rule of result.rules) {
|
||||
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
|
||||
if (!rule.validationIssues?.length) {
|
||||
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
|
||||
}
|
||||
}
|
||||
|
||||
panel.webview.html = renderPreviewHtml(result, keepRule);
|
||||
@@ -51,15 +53,31 @@ function renderPreviewHtml(
|
||||
result: ConversionResult,
|
||||
keepRule: Record<string, boolean>,
|
||||
): string {
|
||||
const exactRules = result.rules.filter(r => r.duplicateLevel === 'exact');
|
||||
const overlapRules = result.rules.filter(r => r.duplicateLevel === 'overlap');
|
||||
const noneRules = result.rules.filter(
|
||||
const errorRules = result.rules.filter(r => r.validationIssues?.length);
|
||||
const cleanRules = result.rules.filter(r => !r.validationIssues?.length);
|
||||
|
||||
const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact');
|
||||
const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap');
|
||||
const noneRules = cleanRules.filter(
|
||||
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
|
||||
);
|
||||
|
||||
const totalKept = Object.values(keepRule).filter(Boolean).length;
|
||||
const totalCommented = Object.values(keepRule).filter(v => !v).length;
|
||||
|
||||
const skippedHint = result.skippedCount
|
||||
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
|
||||
: '';
|
||||
|
||||
const hasValidRules = cleanRules.length > 0;
|
||||
const emptyValidHint = !hasValidRules
|
||||
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
|
||||
: '';
|
||||
|
||||
const confirmBtnAttrs = hasValidRules
|
||||
? 'onclick="doConfirm()"'
|
||||
: 'disabled style="opacity:0.5;cursor:not-allowed;"';
|
||||
|
||||
function renderRuleCard(rule: ImportableRule): string {
|
||||
const kept = keepRule[rule.id];
|
||||
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
|
||||
@@ -156,6 +174,46 @@ function renderPreviewHtml(
|
||||
`;
|
||||
}
|
||||
|
||||
function renderErrorCard(rule: ImportableRule): string {
|
||||
const issues = (rule.validationIssues || []).map(i =>
|
||||
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">${t('import.issuePrefix')} ${i.message}</div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="rule-card" data-error="true" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
|
||||
<div class="rule-card-header" style="cursor:default;">
|
||||
<div class="rule-card-summary">
|
||||
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
|
||||
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
|
||||
${issues}
|
||||
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
|
||||
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderErrorSection(rules: ImportableRule[]): string {
|
||||
if (rules.length === 0) { return ''; }
|
||||
const sectionId = 'section-error';
|
||||
return `
|
||||
<div style="margin-bottom:12px;">
|
||||
<div class="section-header" onclick="toggleSection('${sectionId}')">
|
||||
<span style="font-size:14px;">🚫</span>
|
||||
<span class="section-title">${t('import.sectionInvalid')}(${rules.length})</span>
|
||||
<span class="section-arrow">▼</span>
|
||||
</div>
|
||||
<div id="${sectionId}">
|
||||
${rules.map(renderErrorCard).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
|
||||
if (rules.length === 0) { return ''; }
|
||||
const sectionId = `section-${title.replace(/\s/g, '')}`;
|
||||
@@ -381,13 +439,17 @@ body {
|
||||
|
||||
<div id="validationError" class="validation-error" style="display:none;"></div>
|
||||
|
||||
${skippedHint}
|
||||
${renderErrorSection(errorRules)}
|
||||
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
|
||||
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
|
||||
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
|
||||
|
||||
${emptyValidHint}
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="doConfirm()">${t('importPreview.confirm')}</button>
|
||||
<button class="btn btn-primary" ${confirmBtnAttrs}>${t('importPreview.confirm')}</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -462,6 +524,7 @@ function updateRule(ruleId, field, value) {
|
||||
function collectEditedRules() {
|
||||
const result = [];
|
||||
document.querySelectorAll('.rule-card').forEach(card => {
|
||||
if (card.hasAttribute('data-error')) { return; }
|
||||
const originalId = card.dataset.ruleid;
|
||||
const idInput = card.querySelector('.id-display-input');
|
||||
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
|
||||
@@ -522,6 +585,7 @@ function cancel() {
|
||||
function updateSummary() {
|
||||
let keepCount = 0, commentCount = 0;
|
||||
document.querySelectorAll('.rule-card').forEach(card => {
|
||||
if (card.hasAttribute('data-error')) { return; }
|
||||
const ruleId = card.dataset.ruleid;
|
||||
const keepBtns = card.querySelectorAll('.toggle-btn');
|
||||
let isKept = true;
|
||||
|
||||
Reference in New Issue
Block a user