@qilitt-mickey/vue3-temp-skill 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -115
- package/SKILL.md +41 -159
- package/bin/cli.js +297 -657
- package/package.json +5 -2
- package/references/adapters/_new-adapter.md +17 -82
- package/references/adapters/vue-antdv.md +21 -107
- package/references/adapters/vue-element-plus.md +39 -179
- package/references/code-quality.md +52 -168
- package/references/core-kernel.md +12 -3
- package/references/crud-pages.md +10 -11
- package/references/data-screen.md +8 -9
- package/references/design-apply.md +59 -358
- package/references/layout-theme.md +6 -4
- package/references/visual-verification.md +49 -0
- package/scripts/check-landing.mjs +234 -47
- package/scripts/generate-css.mjs +17 -397
- package/scripts/match.py +30 -11
- package/scripts/modules.json +3 -3
- package/scripts/verify-render.mjs +17 -524
- package/scripts/generate-page.mjs +0 -734
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 旧格式文本诊断 —— 默认禁用,不提供业务/视觉验收结论。
|
|
5
5
|
*
|
|
6
6
|
* 用法:
|
|
7
|
-
* node check-landing.mjs --
|
|
8
|
-
* node check-landing.mjs --project
|
|
7
|
+
* node check-landing.mjs --help
|
|
8
|
+
* node check-landing.mjs --legacy-diagnostics --project <项目根目录> [--json]
|
|
9
9
|
*
|
|
10
|
-
* 退出码: 0 =
|
|
10
|
+
* 退出码: 0 = 帮助 / 2 = 未验收(包括旧文本检查没有 FAIL 的情况)
|
|
11
11
|
*
|
|
12
|
-
* 设计原则:
|
|
12
|
+
* 设计原则: 显式启用后仅只读诊断。不修改、不联网。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
19
|
|
|
19
20
|
const IGNORE_DIRS = new Set([
|
|
20
21
|
"node_modules", "dist", ".git", ".vite", "coverage",
|
|
@@ -30,27 +31,37 @@ const SOURCE_EXT = new Set([".vue", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"
|
|
|
30
31
|
const STYLE_EXT = new Set([".css", ".scss", ".less"]);
|
|
31
32
|
|
|
32
33
|
const EXIT_OK = 0;
|
|
33
|
-
const
|
|
34
|
+
const EXIT_UNVERIFIED = 2;
|
|
35
|
+
const NOTICE = "check-landing.mjs 仅为旧格式文本诊断,不是业务/视觉验收,不得据此宣称还原通过。";
|
|
36
|
+
const MIGRATION = "请迁移到 references/design-apply.md 和 references/visual-verification.md,以原稿为事实源手写项目视图,并逐区域、逐实例、逐状态进行浏览器验收。";
|
|
37
|
+
const HELP = `用法: node check-landing.mjs --legacy-diagnostics --project <项目根目录> [--json]
|
|
38
|
+
${NOTICE}
|
|
39
|
+
默认不读取项目;只有显式 --legacy-diagnostics 才运行旧的只读文本诊断。
|
|
40
|
+
旧格式检查与建议仅供理解历史数据,不是当前实施指令。
|
|
41
|
+
${MIGRATION}
|
|
42
|
+
--help / -h 退出 0;其他调用均退出 2,visualStatus=unverified。`;
|
|
34
43
|
|
|
35
44
|
// ────────────────────────────── 参数解析
|
|
36
45
|
|
|
37
46
|
function parseArgs(argv) {
|
|
38
|
-
const out = { project: process.cwd(), json: false };
|
|
47
|
+
const out = { project: process.cwd(), json: false, legacyDiagnostics: false, help: false };
|
|
39
48
|
for (let i = 0; i < argv.length; i++) {
|
|
40
49
|
const a = argv[i];
|
|
41
50
|
if (a === "--project" || a === "-p") {
|
|
42
51
|
const v = argv[i + 1];
|
|
43
|
-
if (!v || v.startsWith("
|
|
44
|
-
|
|
45
|
-
process.exit(EXIT_FAIL);
|
|
52
|
+
if (!v || v.startsWith("-")) {
|
|
53
|
+
throw new Error("--project 后面必须跟项目根目录路径");
|
|
46
54
|
}
|
|
47
55
|
out.project = path.resolve(v);
|
|
48
56
|
i++;
|
|
49
57
|
} else if (a === "--json") {
|
|
50
58
|
out.json = true;
|
|
59
|
+
} else if (a === "--legacy-diagnostics") {
|
|
60
|
+
out.legacyDiagnostics = true;
|
|
51
61
|
} else if (a === "--help" || a === "-h") {
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
out.help = true;
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`未知参数: ${a}`);
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
return out;
|
|
@@ -319,10 +330,9 @@ function resolveLibraryDir() {
|
|
|
319
330
|
return { dir: null, tried: candidates };
|
|
320
331
|
}
|
|
321
332
|
|
|
322
|
-
const { dir: LIBRARY_DIR } = resolveLibraryDir();
|
|
323
|
-
|
|
324
333
|
/** 加载角色库名集合:<场景>.json + components.json;目录不可达返回 null(降级 WARN) */
|
|
325
334
|
function loadRoleLibrary(scene) {
|
|
335
|
+
const { dir: LIBRARY_DIR } = resolveLibraryDir();
|
|
326
336
|
const merged = new Set();
|
|
327
337
|
if (!LIBRARY_DIR) return null;
|
|
328
338
|
for (const f of [`${scene}.json`, "components.json"]) {
|
|
@@ -431,6 +441,159 @@ function checkCssRules(cssPathOf, contract, roleNames) {
|
|
|
431
441
|
return missing;
|
|
432
442
|
}
|
|
433
443
|
|
|
444
|
+
// ────────────────────────────── 第 9 项:demo 内容泄漏(no-demo-leak)
|
|
445
|
+
|
|
446
|
+
/** 通用 UI 词 —— 项目自身业务同样会用到,命中不计泄漏 */
|
|
447
|
+
const GENERIC_WORDS = new Set([
|
|
448
|
+
"操作", "查看", "编辑", "删除", "新增", "修改", "保存", "取消", "确认", "确定",
|
|
449
|
+
"搜索", "查询", "重置", "刷新", "导入", "导出", "展开", "收起", "全选", "反选",
|
|
450
|
+
"状态", "类型", "时间", "名称", "编号", "序号", "备注", "图片", "地址", "金额",
|
|
451
|
+
"密度", "列设置", "批量删除", "批量导出", "全部", "启用", "停用", "更多",
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
/** 承载演示文案的键名白名单(其余键是控件类型/变体/对齐/图标等形态值,不判) */
|
|
455
|
+
const TEXT_KEYS = new Set([
|
|
456
|
+
"label", "title", "subtitle", "text", "placeholder", "total", "unit", "options",
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
/** 整段演示数据的键名 —— 其下字符串全部视为演示内容 */
|
|
460
|
+
const SAMPLE_KEYS = new Set(["rowsSampleOnly", "sampleRows", "sampleRowsOnly"]);
|
|
461
|
+
|
|
462
|
+
/** 从契约 structure 收集 sampleOnly 演示文案:text -> 声明位置。
|
|
463
|
+
* inSample 上下文必须穿透数组/对象层级向下传播 —— 整段演示数据(rowsSampleOnly 等)
|
|
464
|
+
* 内部无论嵌多深、键名叫什么,全部按演示内容处理。 */
|
|
465
|
+
function collectDemoTexts(node, key, out, inSample = false) {
|
|
466
|
+
if (node === null || node === undefined) return out;
|
|
467
|
+
if (Array.isArray(node)) {
|
|
468
|
+
const here = inSample || SAMPLE_KEYS.has(key);
|
|
469
|
+
for (const item of node) collectDemoTexts(item, key, out, here);
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
if (typeof node === "object") {
|
|
473
|
+
const here = inSample || SAMPLE_KEYS.has(key);
|
|
474
|
+
for (const [k, v] of Object.entries(node)) collectDemoTexts(v, k, out, here);
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
if (typeof node !== "string") return out;
|
|
478
|
+
if (!inSample && !TEXT_KEYS.has(key)) return out;
|
|
479
|
+
const t = node.trim();
|
|
480
|
+
if (t.length < 2) return out; // 单字符不判
|
|
481
|
+
if (/^[\d\s.,:%/+\-–—:]+$/.test(t)) return out; // 纯数字/时间/标点不判
|
|
482
|
+
if (/^(M[\d.]|rect:|circle:)/.test(t)) return out; // icon path 记法不判
|
|
483
|
+
if (GENERIC_WORDS.has(t)) return out; // 通用 UI 词不判
|
|
484
|
+
if (!out.has(t)) out.set(t, key);
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function gitTry(projectRoot, args) {
|
|
489
|
+
try {
|
|
490
|
+
// 禁止刷新索引及调用项目配置的 fsmonitor,保持旧诊断只读。
|
|
491
|
+
return execSync(`git --no-optional-locks -c core.fsmonitor=false ${args}`, {
|
|
492
|
+
cwd: projectRoot,
|
|
493
|
+
encoding: "utf-8",
|
|
494
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
495
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
496
|
+
});
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const SCANNABLE_EXT = new Set([...SOURCE_EXT, ...STYLE_EXT, ".json", ".md"]);
|
|
503
|
+
|
|
504
|
+
/** 解析 git diff 输出 → [{ file, line }](仅新增行,按文件归属) */
|
|
505
|
+
function parseAddedLines(diffText) {
|
|
506
|
+
const out = [];
|
|
507
|
+
let file = null;
|
|
508
|
+
for (const raw of diffText.split("\n")) {
|
|
509
|
+
if (raw.startsWith("+++ b/")) {
|
|
510
|
+
file = raw.slice(6).trim();
|
|
511
|
+
} else if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
512
|
+
if (file) out.push({ file, line: raw.slice(1).trim() });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** 第 9 项:设计稿 demo 内容(sampleOnly 文案/数据)不得出现在项目新增代码里 */
|
|
519
|
+
function checkDemoLeak(projectRoot, contract) {
|
|
520
|
+
const structure = contract && typeof contract.structure === "object" ? contract.structure : null;
|
|
521
|
+
if (!structure) {
|
|
522
|
+
return { status: "WARN", detail: "契约没有 structure 字段,无从提取演示文案,跳过本项", fix: null };
|
|
523
|
+
}
|
|
524
|
+
const demoTexts = collectDemoTexts(structure, "", new Map());
|
|
525
|
+
if (demoTexts.size === 0) {
|
|
526
|
+
return { status: "PASS", detail: "契约 structure 无可提取的演示文案,无需拦截", fix: null };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const insideRepo = gitTry(projectRoot, "rev-parse --is-inside-work-tree");
|
|
530
|
+
if (!insideRepo || insideRepo.trim() !== "true") {
|
|
531
|
+
return {
|
|
532
|
+
status: "WARN",
|
|
533
|
+
detail: `项目不是 git 仓库,无法定位新增代码做泄漏比对(已提取 ${demoTexts.size} 条演示文案)。请人工确认:项目里没有出现设计稿的文案/数据/演示页面`,
|
|
534
|
+
fix: null,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const leaks = [];
|
|
539
|
+
const isDesignDir = (f) => f.replace(/\\/g, "/").startsWith(".design/");
|
|
540
|
+
|
|
541
|
+
// ① untracked 文件:整份内容都在比对范围(设计稿整页被生成进项目就是这个形态)
|
|
542
|
+
const statusOut = gitTry(projectRoot, "status --porcelain");
|
|
543
|
+
if (statusOut) {
|
|
544
|
+
for (const raw of statusOut.split("\n")) {
|
|
545
|
+
if (!raw.startsWith("?? ")) continue;
|
|
546
|
+
let f = raw.slice(3).trim();
|
|
547
|
+
if (f.startsWith('"') && f.endsWith('"')) f = f.slice(1, -1);
|
|
548
|
+
f = f.replace(/\\/g, "/").replace(/\/$/, "");
|
|
549
|
+
if (isDesignDir(f)) continue;
|
|
550
|
+
const abs = path.join(projectRoot, f.replace(/\//g, path.sep));
|
|
551
|
+
let isFile = false;
|
|
552
|
+
try { isFile = fs.statSync(abs).isFile(); } catch { continue; }
|
|
553
|
+
const ext = path.extname(f).toLowerCase();
|
|
554
|
+
if (!isFile || !SCANNABLE_EXT.has(ext)) continue;
|
|
555
|
+
const content = readSafe(abs);
|
|
556
|
+
for (const [text, src] of demoTexts) {
|
|
557
|
+
if (content.includes(text)) leaks.push({ text, src, where: `${f}(untracked)` });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ② 已跟踪文件的改动行:禁用外部 diff/textconv,避免执行项目配置的程序。
|
|
563
|
+
const diffOut = gitTry(projectRoot, "diff --no-ext-diff --no-textconv HEAD --unified=0");
|
|
564
|
+
if (diffOut) {
|
|
565
|
+
for (const { file, line } of parseAddedLines(diffOut)) {
|
|
566
|
+
if (isDesignDir(file)) continue;
|
|
567
|
+
const ext = path.extname(file).toLowerCase();
|
|
568
|
+
if (!SCANNABLE_EXT.has(ext)) continue;
|
|
569
|
+
for (const [text, src] of demoTexts) {
|
|
570
|
+
if (line.includes(text)) leaks.push({ text, src, where: `${file} +${line.slice(0, 60)}` });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (leaks.length === 0) {
|
|
576
|
+
return {
|
|
577
|
+
status: "PASS",
|
|
578
|
+
detail: `已提取 ${demoTexts.size} 条演示文案,项目新增代码中零命中`,
|
|
579
|
+
fix: null,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// 同一文案多处泄漏只报一处
|
|
583
|
+
const seen = new Set();
|
|
584
|
+
const uniq = leaks.filter((l) => (seen.has(l.text) ? false : (seen.add(l.text), true)));
|
|
585
|
+
return {
|
|
586
|
+
status: "FAIL",
|
|
587
|
+
detail:
|
|
588
|
+
`拦截 ${uniq.length} 处设计稿 demo 内容泄漏: ` +
|
|
589
|
+
uniq.slice(0, 6).map((l) => `「${l.text}」← ${l.where}`).join(";") +
|
|
590
|
+
(uniq.length > 6 ? ` …等共 ${uniq.length} 处` : ""),
|
|
591
|
+
fix:
|
|
592
|
+
"把设计稿当需求在项目里开发了一遍 —— 删除泄漏的演示页面/演示数据/新注册的路由与菜单," +
|
|
593
|
+
"把效果还原到项目既有业务页面上(锚点规则见 design-apply.md 第 6 步)。设计稿内容与文案是 demo,一个字都不进项目。",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
434
597
|
// ────────────────────────────── 主流程
|
|
435
598
|
|
|
436
599
|
function run(projectRoot) {
|
|
@@ -439,8 +602,7 @@ function run(projectRoot) {
|
|
|
439
602
|
checks.push({ id, title, status, detail, fix });
|
|
440
603
|
|
|
441
604
|
if (!exists(projectRoot)) {
|
|
442
|
-
|
|
443
|
-
process.exit(EXIT_FAIL);
|
|
605
|
+
throw new Error(`项目根目录不存在: ${projectRoot}`);
|
|
444
606
|
}
|
|
445
607
|
|
|
446
608
|
// 1. 契约存在
|
|
@@ -585,51 +747,76 @@ function run(projectRoot) {
|
|
|
585
747
|
);
|
|
586
748
|
}
|
|
587
749
|
|
|
588
|
-
|
|
750
|
+
// 9. demo 内容泄漏(设计稿 sampleOnly 文案/数据不得进项目)
|
|
751
|
+
const leak = checkDemoLeak(projectRoot, contract);
|
|
752
|
+
add(
|
|
753
|
+
"no-demo-leak",
|
|
754
|
+
"设计稿 demo 内容未泄漏进项目",
|
|
755
|
+
leak.status,
|
|
756
|
+
leak.detail,
|
|
757
|
+
leak.fix
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const ok = checks.every((c) => c.status !== "FAIL");
|
|
589
761
|
return { checks, ok, projectRoot, contractInfo: parsed.info, mount };
|
|
590
762
|
}
|
|
591
763
|
|
|
592
764
|
// ────────────────────────────── 输出
|
|
593
765
|
|
|
594
766
|
function printHuman(result) {
|
|
595
|
-
const L = [];
|
|
596
|
-
L.push(
|
|
597
|
-
L.push(
|
|
598
|
-
L.push(
|
|
599
|
-
L.push(`项目根: ${result.projectRoot}`);
|
|
600
|
-
L.push("");
|
|
601
|
-
const mark = { PASS: "PASS", WARN: "WARN", FAIL: "FAIL" };
|
|
767
|
+
const L = [result.notice];
|
|
768
|
+
if (result.projectRoot) L.push(`项目根: ${result.projectRoot}`);
|
|
769
|
+
if (result.reason) L.push(result.reason);
|
|
770
|
+
if (result.error) L.push(`错误: ${result.error}`);
|
|
602
771
|
for (const c of result.checks) {
|
|
603
|
-
L.push(`[
|
|
772
|
+
L.push(`[LEGACY-TEXT-${c.status}] ${c.id} · ${c.title}`);
|
|
604
773
|
if (c.detail) L.push(` ${c.detail}`);
|
|
605
|
-
if (c.fix) L.push(`
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const firstFail = result.checks.find((c) => c.status === "FAIL");
|
|
610
|
-
if (result.ok) {
|
|
611
|
-
L.push("结论: 八项全过 —— 契约、结构、样式、挂载闭环均已接通。");
|
|
612
|
-
} else if (firstFail) {
|
|
613
|
-
L.push(`结论: 断在「${firstFail.title}」。先修这一项,重跑本脚本看下一项。`);
|
|
614
|
-
L.push("");
|
|
615
|
-
L.push("提醒: 项目侧的修正动作是「改契约或改 adapter 后重新生成」,");
|
|
616
|
-
L.push(" 不要直接改已生成的 CSS —— 下次重新生成会被覆盖。");
|
|
617
|
-
} else {
|
|
618
|
-
L.push("结论: 有 WARN 项,落地链路未全覆盖。");
|
|
774
|
+
if (c.fix) L.push(` 旧格式建议(非当前实施指令) → ${c.fix}`);
|
|
775
|
+
}
|
|
776
|
+
if (result.diagnosticOk) {
|
|
777
|
+
L.push("旧格式文本检查未发现 FAIL;这不证明业务正确、无 demo 泄漏或视觉还原。");
|
|
619
778
|
}
|
|
620
|
-
L.push(
|
|
779
|
+
L.push(result.migration);
|
|
780
|
+
L.push("visualStatus=unverified;退出 2,不提供验收通过结论。");
|
|
621
781
|
process.stdout.write(L.join("\n") + "\n");
|
|
622
782
|
}
|
|
623
783
|
|
|
624
|
-
function main() {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
784
|
+
export function main(argv = process.argv.slice(2)) {
|
|
785
|
+
const result = {
|
|
786
|
+
mode: "disabled",
|
|
787
|
+
ok: false,
|
|
788
|
+
visualStatus: "unverified",
|
|
789
|
+
notice: NOTICE,
|
|
790
|
+
migration: MIGRATION,
|
|
791
|
+
checks: [],
|
|
792
|
+
};
|
|
793
|
+
let json = argv.includes("--json");
|
|
794
|
+
try {
|
|
795
|
+
const args = parseArgs(argv);
|
|
796
|
+
json = args.json;
|
|
797
|
+
if (args.help) {
|
|
798
|
+
console.log(HELP);
|
|
799
|
+
return EXIT_OK;
|
|
800
|
+
}
|
|
801
|
+
if (args.legacyDiagnostics) {
|
|
802
|
+
result.mode = "legacy-diagnostics";
|
|
803
|
+
const { ok: diagnosticOk, ...diagnostic } = run(args.project);
|
|
804
|
+
Object.assign(result, diagnostic, { diagnosticOk });
|
|
805
|
+
} else {
|
|
806
|
+
result.reason = "默认不读取项目;只有显式 --legacy-diagnostics 才运行旧的只读文本诊断。";
|
|
807
|
+
}
|
|
808
|
+
} catch (error) {
|
|
809
|
+
result.error = error.message;
|
|
810
|
+
result.diagnosticOk = false;
|
|
811
|
+
}
|
|
812
|
+
if (json) {
|
|
628
813
|
console.log(JSON.stringify(result, null, 2));
|
|
629
814
|
} else {
|
|
630
815
|
printHuman(result);
|
|
631
816
|
}
|
|
632
|
-
|
|
817
|
+
return EXIT_UNVERIFIED;
|
|
633
818
|
}
|
|
634
819
|
|
|
635
|
-
|
|
820
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
821
|
+
process.exitCode = main();
|
|
822
|
+
}
|