@bovinphang/frontend-craft 2.6.0 → 2.7.0
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +38 -1
- package/CHANGELOG.zh-CN.md +37 -1
- package/README.md +10 -6
- package/README.openclaw.md +3 -1
- package/README.openclaw.zh-CN.md +3 -1
- package/README.zh-CN.md +10 -6
- package/agents/fec-typescript-reviewer.md +1 -1
- package/docs/example-prompts.md +81 -69
- package/docs/ja-JP/README.md +56 -52
- package/docs/ko-KR/README.md +45 -41
- package/docs/project-structure.md +8 -4
- package/docs/superpowers/plans/2026-05-29-fec-debug.md +491 -0
- package/docs/superpowers/specs/2026-05-29-fec-debug-design.md +340 -0
- package/docs/zh-CN/example-prompts.md +81 -69
- package/docs/zh-CN/project-structure.md +8 -4
- package/docs/zh-TW/README.md +33 -29
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/skills/eval_queries.json +154 -56
- package/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
- package/skills/fec-code-review/SKILL.md +1 -1
- package/skills/fec-drawio-studio/SKILL.md +69 -0
- package/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/fec-drawio-studio/data/brand-icons.json +878 -0
- package/skills/fec-drawio-studio/data/shape-index.json.gz +0 -0
- package/skills/fec-drawio-studio/references/data-residency.md +27 -0
- package/skills/fec-drawio-studio/references/diagram-patterns.md +41 -0
- package/skills/fec-drawio-studio/references/layout-and-export.md +57 -0
- package/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
- package/skills/fec-drawio-studio/references/xml-and-mermaid.md +54 -0
- package/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/skills/fec-drawio-studio/scripts/diagram-lint.mjs +143 -0
- package/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/skills/fec-drawio-studio/scripts/shape-query.mjs +81 -0
- package/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
- package/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
- package/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
- package/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/skills/fec-drawio-studio/styles/schema.json +112 -0
- package/skills/fec-e2e-testing/SKILL.md +3 -1
- package/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
- package/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
- package/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
- package/skills/fec-image-generation/SKILL.md +56 -0
- package/skills/fec-image-generation/references/artifact-routing.md +80 -0
- package/skills/fec-image-generation/references/diagram-workflows.md +43 -0
- package/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
- package/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
- package/skills/fec-implement-from-design/SKILL.md +7 -0
- package/skills/fec-legacy-to-modern-migration/SKILL.md +24 -168
- package/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
- package/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
- package/skills/fec-motion-interaction/SKILL.md +7 -0
- package/skills/fec-nextjs-project-standard/SKILL.md +7 -1
- package/skills/fec-performance-optimization/SKILL.md +5 -1
- package/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
- package/skills/fec-performance-optimization/references/report-template.md +6 -0
- package/skills/fec-react-project-standard/SKILL.md +11 -3
- package/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
- package/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
- package/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
- package/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
- package/skills/fec-responsive-layout/references/responsive-layout-patterns.md +11 -0
- package/skills/fec-source-driven-development/SKILL.md +26 -0
- package/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +5 -1
- package/skills/fec-typescript-project-standard/SKILL.md +54 -0
- package/skills/{fec-typescript-type-safety → fec-typescript-project-standard}/references/type-safety-patterns.md +1 -1
- package/skills/{fec-typescript-type-safety/SKILL.md → fec-typescript-project-standard/references/type-safety.md} +1 -10
- package/skills/fec-ui-design/SKILL.md +26 -8
- package/skills/fec-ui-design/data/style-archetypes.json +40 -0
- package/skills/fec-ui-design/data/ux-quality-rules.json +35 -0
- package/skills/fec-ui-design/references/design-intelligence.md +66 -6
- package/skills/fec-ui-design/references/pre-delivery-checklist.md +6 -0
- package/skills/fec-ui-design/scripts/design-system.mjs +119 -1
- package/skills/fec-vue3-project-standard/SKILL.md +11 -2
- package/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +163 -0
- package/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
- package/skills/fec-vue3-project-standard/references/vue3-project-structure.md +177 -0
- package/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
- package/skills/fec-web-video-presentation/SKILL.md +61 -0
- package/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
- package/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
- package/skills/fec-web-video-presentation/references/content-plan.md +38 -0
- package/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
- package/skills/fec-web-video-presentation/references/theme-system.md +36 -0
- package/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
- package/skills/metadata.json +340 -164
- package/skills/relations.json +116 -25
- package/templates/shared/rules/fec-ci-cd.md +20 -108
- package/templates/shared/rules/fec-design-system.md +47 -223
- package/templates/shared/rules/fec-error-handling.md +35 -179
- package/templates/shared/rules/fec-i18n.md +43 -91
- package/templates/shared/rules/fec-refactoring.md +4 -0
- package/templates/shared/rules/fec-rendering-patterns.md +28 -171
- package/templates/shared/rules/fec-responsive-design.md +34 -211
- package/templates/shared/rules/fec-state-management.md +41 -144
- package/templates/shared/rules/fec-typescript.md +47 -290
- package/skills/fec-e2e-testing/references/e2e-advanced.md +0 -176
- package/skills/fec-react-project-standard/references/react-project-details.md +0 -642
- package/skills/fec-vue3-project-standard/references/vue3-project-details.md +0 -547
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import zlib from "node:zlib";
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
|
|
9
|
+
if (!args.png || args.help) {
|
|
10
|
+
printHelp();
|
|
11
|
+
process.exit(args.help ? 0 : 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const image = readPng(args.png);
|
|
16
|
+
const manifest = args.manifest ? readManifest(args.manifest) : undefined;
|
|
17
|
+
const report = analyze(image, manifest, Boolean(args.strict));
|
|
18
|
+
if ((args.format ?? "markdown") === "json") {
|
|
19
|
+
console.log(JSON.stringify(report, null, 2));
|
|
20
|
+
} else {
|
|
21
|
+
console.log(renderMarkdown(report));
|
|
22
|
+
}
|
|
23
|
+
process.exit(report.ok ? 0 : 2);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(values) {
|
|
30
|
+
const parsed = { format: "markdown", strict: false, help: false };
|
|
31
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
32
|
+
const value = values[index];
|
|
33
|
+
if (value === "--help" || value === "-h") parsed.help = true;
|
|
34
|
+
else if (value === "--strict") parsed.strict = true;
|
|
35
|
+
else if (value === "--png") parsed.png = values[++index];
|
|
36
|
+
else if (value === "--manifest") parsed.manifest = values[++index];
|
|
37
|
+
else if (value === "--format") parsed.format = values[++index];
|
|
38
|
+
else throw new Error(`Unknown argument: ${value}`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.format !== "json" && parsed.format !== "markdown") {
|
|
41
|
+
throw new Error("--format must be json or markdown");
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printHelp() {
|
|
47
|
+
console.log(`Usage: node skills/fec-image-generation/scripts/png-qa.mjs --png <file> [--manifest <layout.json>] [--format json|markdown] [--strict]`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readManifest(filePath) {
|
|
51
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
52
|
+
return {
|
|
53
|
+
canvas: manifest.canvas,
|
|
54
|
+
boxes: Array.isArray(manifest.boxes) ? manifest.boxes : [],
|
|
55
|
+
connectors: Array.isArray(manifest.connectors) ? manifest.connectors : [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPng(filePath) {
|
|
60
|
+
const buffer = fs.readFileSync(filePath);
|
|
61
|
+
const signature = "89504e470d0a1a0a";
|
|
62
|
+
if (buffer.subarray(0, 8).toString("hex") !== signature) {
|
|
63
|
+
throw new Error(`Not a PNG file: ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let offset = 8;
|
|
67
|
+
let ihdr;
|
|
68
|
+
const idat = [];
|
|
69
|
+
while (offset < buffer.length) {
|
|
70
|
+
const length = buffer.readUInt32BE(offset);
|
|
71
|
+
const type = buffer.subarray(offset + 4, offset + 8).toString("ascii");
|
|
72
|
+
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
|
73
|
+
offset += 12 + length;
|
|
74
|
+
if (type === "IHDR") {
|
|
75
|
+
ihdr = {
|
|
76
|
+
width: data.readUInt32BE(0),
|
|
77
|
+
height: data.readUInt32BE(4),
|
|
78
|
+
bitDepth: data[8],
|
|
79
|
+
colorType: data[9],
|
|
80
|
+
compression: data[10],
|
|
81
|
+
filter: data[11],
|
|
82
|
+
interlace: data[12],
|
|
83
|
+
};
|
|
84
|
+
} else if (type === "IDAT") {
|
|
85
|
+
idat.push(data);
|
|
86
|
+
} else if (type === "IEND") {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!ihdr) throw new Error("PNG is missing IHDR");
|
|
92
|
+
if (ihdr.bitDepth !== 8) throw new Error(`Unsupported PNG bit depth: ${ihdr.bitDepth}`);
|
|
93
|
+
if (ihdr.compression !== 0 || ihdr.filter !== 0 || ihdr.interlace !== 0) {
|
|
94
|
+
throw new Error("Unsupported PNG compression/filter/interlace settings");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const channels = channelsForColorType(ihdr.colorType);
|
|
98
|
+
const inflated = zlib.inflateSync(Buffer.concat(idat));
|
|
99
|
+
const pixels = unfilter(inflated, ihdr.width, ihdr.height, channels);
|
|
100
|
+
return {
|
|
101
|
+
path: path.resolve(filePath),
|
|
102
|
+
width: ihdr.width,
|
|
103
|
+
height: ihdr.height,
|
|
104
|
+
colorType: ihdr.colorType,
|
|
105
|
+
channels,
|
|
106
|
+
pixels,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function channelsForColorType(colorType) {
|
|
111
|
+
if (colorType === 0) return 1;
|
|
112
|
+
if (colorType === 2) return 3;
|
|
113
|
+
if (colorType === 4) return 2;
|
|
114
|
+
if (colorType === 6) return 4;
|
|
115
|
+
throw new Error(`Unsupported PNG color type: ${colorType}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function unfilter(data, width, height, channels) {
|
|
119
|
+
const stride = width * channels;
|
|
120
|
+
const output = Buffer.alloc(stride * height);
|
|
121
|
+
let sourceOffset = 0;
|
|
122
|
+
|
|
123
|
+
for (let y = 0; y < height; y += 1) {
|
|
124
|
+
const filterType = data[sourceOffset];
|
|
125
|
+
sourceOffset += 1;
|
|
126
|
+
const rowOffset = y * stride;
|
|
127
|
+
const previousRowOffset = (y - 1) * stride;
|
|
128
|
+
|
|
129
|
+
for (let x = 0; x < stride; x += 1) {
|
|
130
|
+
const raw = data[sourceOffset + x];
|
|
131
|
+
const left = x >= channels ? output[rowOffset + x - channels] : 0;
|
|
132
|
+
const up = y > 0 ? output[previousRowOffset + x] : 0;
|
|
133
|
+
const upLeft = y > 0 && x >= channels ? output[previousRowOffset + x - channels] : 0;
|
|
134
|
+
let value;
|
|
135
|
+
if (filterType === 0) value = raw;
|
|
136
|
+
else if (filterType === 1) value = raw + left;
|
|
137
|
+
else if (filterType === 2) value = raw + up;
|
|
138
|
+
else if (filterType === 3) value = raw + Math.floor((left + up) / 2);
|
|
139
|
+
else if (filterType === 4) value = raw + paeth(left, up, upLeft);
|
|
140
|
+
else throw new Error(`Unsupported PNG filter type: ${filterType}`);
|
|
141
|
+
output[rowOffset + x] = value & 0xff;
|
|
142
|
+
}
|
|
143
|
+
sourceOffset += stride;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function paeth(left, up, upLeft) {
|
|
150
|
+
const estimate = left + up - upLeft;
|
|
151
|
+
const leftDistance = Math.abs(estimate - left);
|
|
152
|
+
const upDistance = Math.abs(estimate - up);
|
|
153
|
+
const upLeftDistance = Math.abs(estimate - upLeft);
|
|
154
|
+
if (leftDistance <= upDistance && leftDistance <= upLeftDistance) return left;
|
|
155
|
+
if (upDistance <= upLeftDistance) return up;
|
|
156
|
+
return upLeft;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function analyze(image, manifest, strict) {
|
|
160
|
+
const issues = [];
|
|
161
|
+
const nextActions = new Set();
|
|
162
|
+
const ink = measureInk(image, strict ? 10 : 18);
|
|
163
|
+
|
|
164
|
+
if (image.width < 64 || image.height < 64) {
|
|
165
|
+
addIssue(issues, nextActions, "image-too-small", "PNG is too small for reliable visual QA.", "Export a larger PNG before review.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (ink.coverage < (strict ? 0.001 : 0.0002)) {
|
|
169
|
+
addIssue(issues, nextActions, "blank-image", "PNG appears blank or nearly blank.", "Check the renderer/export target and regenerate the image.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (ink.bounds) {
|
|
173
|
+
const pad = strict ? 6 : 3;
|
|
174
|
+
if (ink.bounds.minX <= pad || ink.bounds.minY <= pad || image.width - ink.bounds.maxX <= pad || image.height - ink.bounds.maxY <= pad) {
|
|
175
|
+
addIssue(issues, nextActions, "edge-clipping", "Visible content is very close to the PNG edge.", "Increase canvas/viewBox padding or reduce subject scale.");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (manifest) analyzeManifest(image, manifest, issues, nextActions, strict);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: issues.length === 0,
|
|
183
|
+
image: {
|
|
184
|
+
path: image.path,
|
|
185
|
+
width: image.width,
|
|
186
|
+
height: image.height,
|
|
187
|
+
colorType: image.colorType,
|
|
188
|
+
inkCoverage: Number(ink.coverage.toFixed(6)),
|
|
189
|
+
inkBounds: ink.bounds,
|
|
190
|
+
},
|
|
191
|
+
issues,
|
|
192
|
+
nextActions: [...nextActions],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function measureInk(image, threshold) {
|
|
197
|
+
const first = samplePixel(image, 0, 0);
|
|
198
|
+
let inkPixels = 0;
|
|
199
|
+
let minX = image.width;
|
|
200
|
+
let minY = image.height;
|
|
201
|
+
let maxX = -1;
|
|
202
|
+
let maxY = -1;
|
|
203
|
+
|
|
204
|
+
for (let y = 0; y < image.height; y += 1) {
|
|
205
|
+
for (let x = 0; x < image.width; x += 1) {
|
|
206
|
+
const pixel = samplePixel(image, x, y);
|
|
207
|
+
const differs = colorDistance(pixel, first) > threshold && pixel.a > 8;
|
|
208
|
+
if (differs) {
|
|
209
|
+
inkPixels += 1;
|
|
210
|
+
minX = Math.min(minX, x);
|
|
211
|
+
minY = Math.min(minY, y);
|
|
212
|
+
maxX = Math.max(maxX, x);
|
|
213
|
+
maxY = Math.max(maxY, y);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
coverage: inkPixels / (image.width * image.height),
|
|
220
|
+
bounds: inkPixels > 0 ? { minX, minY, maxX, maxY } : null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function samplePixel(image, x, y) {
|
|
225
|
+
const offset = (y * image.width + x) * image.channels;
|
|
226
|
+
if (image.channels === 1) {
|
|
227
|
+
const value = image.pixels[offset];
|
|
228
|
+
return { r: value, g: value, b: value, a: 255 };
|
|
229
|
+
}
|
|
230
|
+
if (image.channels === 2) {
|
|
231
|
+
const value = image.pixels[offset];
|
|
232
|
+
return { r: value, g: value, b: value, a: image.pixels[offset + 1] };
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
r: image.pixels[offset],
|
|
236
|
+
g: image.pixels[offset + 1],
|
|
237
|
+
b: image.pixels[offset + 2],
|
|
238
|
+
a: image.channels === 4 ? image.pixels[offset + 3] : 255,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function colorDistance(left, right) {
|
|
243
|
+
return Math.abs(left.r - right.r) + Math.abs(left.g - right.g) + Math.abs(left.b - right.b) + Math.abs(left.a - right.a);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function analyzeManifest(image, manifest, issues, nextActions, strict) {
|
|
247
|
+
if (manifest.canvas && (manifest.canvas.width !== image.width || manifest.canvas.height !== image.height)) {
|
|
248
|
+
addIssue(
|
|
249
|
+
issues,
|
|
250
|
+
nextActions,
|
|
251
|
+
"manifest-canvas-mismatch",
|
|
252
|
+
`Manifest canvas ${manifest.canvas.width}x${manifest.canvas.height} does not match PNG ${image.width}x${image.height}.`,
|
|
253
|
+
"Regenerate the manifest from the final export dimensions.",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const boxes = manifest.boxes.map(normalizeBox).filter(Boolean);
|
|
258
|
+
for (const box of boxes) {
|
|
259
|
+
if (box.x < 0 || box.y < 0 || box.x + box.width > image.width || box.y + box.height > image.height) {
|
|
260
|
+
addIssue(issues, nextActions, "box-out-of-bounds", `Box "${box.id}" is outside the canvas.`, "Move out-of-bounds nodes inside the canvas or expand the canvas.");
|
|
261
|
+
}
|
|
262
|
+
if (box.label && estimateLabelOverflow(box, strict)) {
|
|
263
|
+
addIssue(issues, nextActions, "label-overflow", `Box "${box.id}" label is likely to overflow.`, "Wrap or shorten labels, widen boxes, or increase export scale.");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < boxes.length; i += 1) {
|
|
268
|
+
for (let j = i + 1; j < boxes.length; j += 1) {
|
|
269
|
+
const overlap = overlapArea(boxes[i], boxes[j]);
|
|
270
|
+
if (overlap > 0) {
|
|
271
|
+
addIssue(issues, nextActions, "box-overlap", `Boxes "${boxes[i].id}" and "${boxes[j].id}" overlap by ${Math.round(overlap)}px^2.`, "Increase node spacing or split dense groups.");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const boxById = new Map(boxes.map((box) => [box.id, box]));
|
|
277
|
+
const segments = [];
|
|
278
|
+
for (const connector of manifest.connectors) {
|
|
279
|
+
const points = normalizePoints(connector.points);
|
|
280
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
281
|
+
const segment = { connector, a: points[index], b: points[index + 1] };
|
|
282
|
+
segments.push(segment);
|
|
283
|
+
for (const box of boxes) {
|
|
284
|
+
if (box.id === connector.from || box.id === connector.to) continue;
|
|
285
|
+
if (segmentIntersectsBox(segment, box)) {
|
|
286
|
+
addIssue(issues, nextActions, "connector-through-label", `Connector "${connector.id ?? "unnamed"}" crosses box "${box.id}".`, "Add waypoints around labels or use orthogonal routing.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if ((connector.from && !boxById.has(connector.from)) || (connector.to && !boxById.has(connector.to))) {
|
|
291
|
+
addIssue(issues, nextActions, "connector-endpoint-missing", `Connector "${connector.id ?? "unnamed"}" references a missing endpoint.`, "Update connector endpoints to match manifest box ids.");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
296
|
+
for (let j = i + 1; j < segments.length; j += 1) {
|
|
297
|
+
if (segments[i].connector === segments[j].connector) continue;
|
|
298
|
+
if (segmentsStack(segments[i], segments[j], strict ? 4 : 2)) {
|
|
299
|
+
addIssue(issues, nextActions, "connector-stacking", "Two connector segments are stacked or nearly stacked.", "Offset parallel connectors or bundle them into a labeled shared route.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function normalizeBox(box) {
|
|
306
|
+
if (!box || typeof box.id !== "string") return null;
|
|
307
|
+
const normalized = {
|
|
308
|
+
id: box.id,
|
|
309
|
+
x: Number(box.x),
|
|
310
|
+
y: Number(box.y),
|
|
311
|
+
width: Number(box.width),
|
|
312
|
+
height: Number(box.height),
|
|
313
|
+
label: typeof box.label === "string" ? box.label : "",
|
|
314
|
+
};
|
|
315
|
+
if ([normalized.x, normalized.y, normalized.width, normalized.height].some((value) => !Number.isFinite(value))) return null;
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizePoints(points) {
|
|
320
|
+
if (!Array.isArray(points)) return [];
|
|
321
|
+
return points
|
|
322
|
+
.map((point) => {
|
|
323
|
+
if (Array.isArray(point)) return { x: Number(point[0]), y: Number(point[1]) };
|
|
324
|
+
return { x: Number(point?.x), y: Number(point?.y) };
|
|
325
|
+
})
|
|
326
|
+
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function estimateLabelOverflow(box, strict) {
|
|
330
|
+
const label = box.label.trim();
|
|
331
|
+
if (!label) return false;
|
|
332
|
+
const lines = label.split(/\r?\n/);
|
|
333
|
+
const maxLine = Math.max(...lines.map((line) => visualLength(line)));
|
|
334
|
+
const estimatedWidth = maxLine * (strict ? 7.5 : 7);
|
|
335
|
+
const estimatedHeight = lines.length * (strict ? 18 : 16);
|
|
336
|
+
return estimatedWidth > Math.max(0, box.width - 12) || estimatedHeight > Math.max(0, box.height - 8);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function visualLength(value) {
|
|
340
|
+
return [...value].reduce((total, char) => total + (/[\u4e00-\u9fff]/.test(char) ? 2 : 1), 0);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function overlapArea(left, right) {
|
|
344
|
+
const width = Math.max(0, Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x));
|
|
345
|
+
const height = Math.max(0, Math.min(left.y + left.height, right.y + right.height) - Math.max(left.y, right.y));
|
|
346
|
+
return width * height;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function segmentIntersectsBox(segment, box) {
|
|
350
|
+
if (pointInsideBox(segment.a, box) || pointInsideBox(segment.b, box)) return true;
|
|
351
|
+
const edges = [
|
|
352
|
+
[{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }],
|
|
353
|
+
[{ x: box.x + box.width, y: box.y }, { x: box.x + box.width, y: box.y + box.height }],
|
|
354
|
+
[{ x: box.x + box.width, y: box.y + box.height }, { x: box.x, y: box.y + box.height }],
|
|
355
|
+
[{ x: box.x, y: box.y + box.height }, { x: box.x, y: box.y }],
|
|
356
|
+
];
|
|
357
|
+
return edges.some(([a, b]) => segmentsIntersect(segment.a, segment.b, a, b));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function pointInsideBox(point, box) {
|
|
361
|
+
return point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function segmentsIntersect(a, b, c, d) {
|
|
365
|
+
const o1 = orientation(a, b, c);
|
|
366
|
+
const o2 = orientation(a, b, d);
|
|
367
|
+
const o3 = orientation(c, d, a);
|
|
368
|
+
const o4 = orientation(c, d, b);
|
|
369
|
+
if (o1 !== o2 && o3 !== o4) return true;
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function orientation(a, b, c) {
|
|
374
|
+
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
|
|
375
|
+
if (Math.abs(value) < 0.0001) return 0;
|
|
376
|
+
return value > 0 ? 1 : 2;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function segmentsStack(left, right, tolerance) {
|
|
380
|
+
const leftHorizontal = Math.abs(left.a.y - left.b.y) <= tolerance;
|
|
381
|
+
const rightHorizontal = Math.abs(right.a.y - right.b.y) <= tolerance;
|
|
382
|
+
const leftVertical = Math.abs(left.a.x - left.b.x) <= tolerance;
|
|
383
|
+
const rightVertical = Math.abs(right.a.x - right.b.x) <= tolerance;
|
|
384
|
+
|
|
385
|
+
if (leftHorizontal && rightHorizontal && Math.abs(left.a.y - right.a.y) <= tolerance) {
|
|
386
|
+
return rangesOverlap([left.a.x, left.b.x], [right.a.x, right.b.x]) > tolerance;
|
|
387
|
+
}
|
|
388
|
+
if (leftVertical && rightVertical && Math.abs(left.a.x - right.a.x) <= tolerance) {
|
|
389
|
+
return rangesOverlap([left.a.y, left.b.y], [right.a.y, right.b.y]) > tolerance;
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function rangesOverlap(left, right) {
|
|
395
|
+
const a = [Math.min(...left), Math.max(...left)];
|
|
396
|
+
const b = [Math.min(...right), Math.max(...right)];
|
|
397
|
+
return Math.max(0, Math.min(a[1], b[1]) - Math.max(a[0], b[0]));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function addIssue(issues, nextActions, code, message, action) {
|
|
401
|
+
issues.push({ code, message, action });
|
|
402
|
+
nextActions.add(action);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function renderMarkdown(report) {
|
|
406
|
+
const lines = [
|
|
407
|
+
"# PNG QA Report",
|
|
408
|
+
"",
|
|
409
|
+
`- Status: ${report.ok ? "ok" : "issues-found"}`,
|
|
410
|
+
`- Image: ${report.image.width}x${report.image.height}`,
|
|
411
|
+
`- Ink coverage: ${report.image.inkCoverage}`,
|
|
412
|
+
];
|
|
413
|
+
if (report.image.inkBounds) {
|
|
414
|
+
lines.push(`- Ink bounds: ${JSON.stringify(report.image.inkBounds)}`);
|
|
415
|
+
}
|
|
416
|
+
lines.push("", "## Issues");
|
|
417
|
+
if (report.issues.length === 0) {
|
|
418
|
+
lines.push("- None");
|
|
419
|
+
} else {
|
|
420
|
+
for (const issue of report.issues) {
|
|
421
|
+
lines.push(`- ${issue.code}: ${issue.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
lines.push("", "## Next Actions");
|
|
425
|
+
if (report.nextActions.length === 0) {
|
|
426
|
+
lines.push("- None");
|
|
427
|
+
} else {
|
|
428
|
+
for (const action of report.nextActions) lines.push(`- ${action}`);
|
|
429
|
+
}
|
|
430
|
+
return `${lines.join("\n")}\n`;
|
|
431
|
+
}
|
|
@@ -41,6 +41,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
|
|
|
41
41
|
- `modao` — 墨刀原型数据
|
|
42
42
|
- `sketch` — Sketch 选区截图
|
|
43
43
|
- 如以上 MCP 均不可用,请求用户提供设计截图或标注(适用于摹客等无 MCP 工具)
|
|
44
|
+
- 如果没有权威设计稿,但任务明确要求视觉型网站、品牌页、作品集或高审美重设计,可把用户提供或本地生成的分区级参考图作为临时视觉事实来源;这种来源必须先分析后实现,并在计划中标明不是权威设计系统。
|
|
44
45
|
|
|
45
46
|
### 2. 读取设计上下文
|
|
46
47
|
|
|
@@ -51,6 +52,9 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
|
|
|
51
52
|
- 如果 MCP 提供了资源文件或 SVG / 图片源,直接使用
|
|
52
53
|
- 如果 MCP 已提供真实资源,不要自行造占位资源
|
|
53
54
|
- 如果用户提供截图而非 MCP 数据,从截图中推断布局、颜色、字体等视觉信息
|
|
55
|
+
- 如果使用图像/截图参考,先提取区块结构、视觉层级、主色和功能色、字体气质、间距节奏、媒体位置、交互状态暗示和响应式风险,再写代码。
|
|
56
|
+
- 对多区块页面优先使用清晰的分区级参考图或局部细节图;不要从压缩长图里猜测按钮、字号、间距和组件状态。
|
|
57
|
+
- 视觉参考中的不可读文字、假截图、不可检查素材或冲突状态不能直接照搬;需要转译成项目真实内容、真实状态或明确占位说明。
|
|
54
58
|
|
|
55
59
|
### 3. 搜索可复用组件
|
|
56
60
|
|
|
@@ -73,6 +77,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
|
|
|
73
77
|
- 响应式行为
|
|
74
78
|
- 复用还是新建的决策
|
|
75
79
|
- 设计缺口或歧义点
|
|
80
|
+
- 图像/截图来源的可靠性,以及哪些视觉细节会被项目 token、组件约定或可访问性要求覆盖
|
|
76
81
|
|
|
77
82
|
### 5. 按框架实现
|
|
78
83
|
|
|
@@ -113,6 +118,8 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
|
|
|
113
118
|
- 除非有合理理由,不要用硬编码替代已 Token 化的样式
|
|
114
119
|
- 不要忽略 hover、active、disabled、loading、empty、error 等状态
|
|
115
120
|
- 摹客等无 MCP 工具场景下,主动向用户索要关键截图和标注信息,不要凭空编造视觉数据
|
|
121
|
+
- 不把生成图像当作可直接复制的代码规范;它只提供视觉证据,最终实现必须服从当前项目组件、token、可访问性和性能边界
|
|
122
|
+
- 不用一张不可读整页图替代分区分析;关键区块、CTA、表单、导航和状态必须可检查
|
|
116
123
|
|
|
117
124
|
## Expected Output
|
|
118
125
|
|
|
@@ -14,177 +14,33 @@ description: Use when planning or implementing an intentional migration from Jav
|
|
|
14
14
|
## Procedure
|
|
15
15
|
|
|
16
16
|
1. 先识别目标栈、迁移边界和是否保留 MPA/服务端模板形态。
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
-
5.
|
|
21
|
-
|
|
22
|
-
## 迁移策略选择
|
|
23
|
-
|
|
24
|
-
| 策略 | 适用情况 | 优点 | 风险 |
|
|
25
|
-
| --------------------------- | ------------------------------------ | ---------------------------- | ---------------------------- |
|
|
26
|
-
| **渐进式(Strangler Fig)** | 大型项目、需持续交付、团队熟悉度不足 | 风险可控、可分批上线、可回滚 | 新旧并存期长、需维护两套代码 |
|
|
27
|
-
| **一次性重写** | 中小型项目、业务稳定、有充足时间窗口 | 目标架构清晰、无历史包袱 | 周期长、上线压力大、回滚困难 |
|
|
28
|
-
|
|
29
|
-
**推荐**:优先考虑渐进式,除非项目规模小且业务简单。
|
|
30
|
-
|
|
31
|
-
## 概念映射
|
|
32
|
-
|
|
33
|
-
### jQuery → React
|
|
34
|
-
|
|
35
|
-
| 遗留模式 | React 对应 |
|
|
36
|
-
| ------------------------------------------ | -------------------------------------- |
|
|
37
|
-
| `$(selector).html(content)` | 声明式 JSX + state 驱动渲染 |
|
|
38
|
-
| `$(document).on('click', '.btn', handler)` | `onClick` + 事件委托由 React 处理 |
|
|
39
|
-
| `$.ajax()` / `$.get()` | 项目请求层、`fetch` / `axios`,可按需接 TanStack Query 或 SWR |
|
|
40
|
-
| 全局变量 / 命名空间存储状态 | 局部 state、Context 或项目既有 store,可按需使用 Zustand/Redux |
|
|
41
|
-
| `$(el).show()` / `$(el).hide()` | 条件渲染 `{visible && <Component />}` |
|
|
42
|
-
| 手动 DOM 操作 `append` / `remove` | 数据驱动,通过 setState 触发重渲染 |
|
|
43
|
-
| 模板字符串拼接 HTML | JSX 组件 + props |
|
|
44
|
-
| 多页面 + 服务端路由 | React Router 客户端路由 |
|
|
45
|
-
|
|
46
|
-
### jQuery → Vue 3
|
|
47
|
-
|
|
48
|
-
| 遗留模式 | Vue 3 对应 |
|
|
49
|
-
| ------------------------------------------ | -------------------------------------------------- |
|
|
50
|
-
| `$(selector).html(content)` | 模板 + `ref` / `reactive` 驱动渲染 |
|
|
51
|
-
| `$(document).on('click', '.btn', handler)` | `@click` + 事件修饰符 |
|
|
52
|
-
| `$.ajax()` / `$.get()` | 项目请求层、`fetch` / `axios`,可按需接 VueUse、Nuxt `useFetch` 或 TanStack Query Vue adapter |
|
|
53
|
-
| 全局变量 / 命名空间存储状态 | `ref` / `reactive` / Pinia |
|
|
54
|
-
| `$(el).show()` / `$(el).hide()` | `v-show` / `v-if` |
|
|
55
|
-
| 手动 DOM 操作 | 数据驱动,通过响应式更新视图 |
|
|
56
|
-
| 模板字符串拼接 HTML | 单文件组件 `<template>` + props |
|
|
57
|
-
| 多页面 + 服务端路由 | Vue Router 客户端路由 |
|
|
58
|
-
|
|
59
|
-
### 通用映射
|
|
60
|
-
|
|
61
|
-
| 遗留概念 | 现代对应 |
|
|
62
|
-
| --------------------------------- | ------------------------------------------ |
|
|
63
|
-
| 页面级 JS 入口(每页一个 script) | 路由/页面入口 + 懒加载模块,或保留 MPA 入口并现代化打包 |
|
|
64
|
-
| 公共 JS 模块(utils、ajax 封装) | `services/`、`utils/`、类型化 API 层 |
|
|
65
|
-
| 内联样式 / 页面级 CSS | 目标项目样式体系,如 CSS Modules、Tailwind、全局样式分层或组件库样式 |
|
|
66
|
-
| 服务端模板变量 | 通过 API 获取、SSR/loader 注入,或保留服务端模板边界并类型化数据 |
|
|
67
|
-
| 表单提交 + 整页刷新 | 表单库 + 客户端校验 + API 调用 |
|
|
68
|
-
|
|
69
|
-
## 迁移前分析
|
|
70
|
-
|
|
71
|
-
在动手迁移前,必须完成以下分析并输出**迁移分析报告**:
|
|
72
|
-
|
|
73
|
-
1. **存量盘点**
|
|
74
|
-
- 页面数量、功能模块数量
|
|
75
|
-
- 依赖的 jQuery 插件及替代方案(如 DataTables → TanStack Table)
|
|
76
|
-
- 现有 API 调用方式、是否有统一封装
|
|
77
|
-
- 是否有服务端模板(JSP/Thymeleaf/EJS 等)需要保留、局部替换或改为纯前端渲染
|
|
78
|
-
- 目标栈是 React/Vue/Next/Nuxt、现代 MPA,还是只做局部 TypeScript/构建/测试现代化
|
|
79
|
-
|
|
80
|
-
2. **依赖关系**
|
|
81
|
-
- 页面间共享的 JS/CSS 模块
|
|
82
|
-
- 跨页面复用的业务逻辑
|
|
83
|
-
- 与后端的接口契约(是否需调整)
|
|
84
|
-
|
|
85
|
-
3. **迁移优先级**
|
|
86
|
-
- 按业务价值、复杂度、耦合度排序
|
|
87
|
-
- 优先迁移独立模块、低耦合页面
|
|
88
|
-
- 识别可复用的工具函数、常量、类型定义
|
|
89
|
-
|
|
90
|
-
## 分阶段迁移流程
|
|
91
|
-
|
|
92
|
-
### 阶段 0:准备
|
|
93
|
-
|
|
94
|
-
- 搭建或识别目标项目骨架:React/Vue/Next/Nuxt、现代 MPA,或现有项目内的渐进式 TypeScript/构建入口
|
|
95
|
-
- 配置 ESLint、Prettier、测试框架
|
|
96
|
-
- 建立 `services/request.ts` 统一请求层,与现有 API 兼容
|
|
97
|
-
- 将遗留项目中的 `utils`、`constants` 逐步迁移并补充类型
|
|
98
|
-
|
|
99
|
-
### 阶段 1:基础层
|
|
100
|
-
|
|
101
|
-
- 迁移并类型化 API 调用(`$.ajax` → `axios`/`fetch`)
|
|
102
|
-
- 迁移工具函数(日期、格式化、校验等)
|
|
103
|
-
- 迁移常量、枚举、类型定义
|
|
104
|
-
- 建立目标导航/页面入口骨架;未迁移页面可继续走旧路由、重定向、iframe 或微前端嵌入
|
|
105
|
-
|
|
106
|
-
### 阶段 2:按模块/页面迁移
|
|
107
|
-
|
|
108
|
-
- 每次迁移一个完整功能或页面
|
|
109
|
-
- 从简单到复杂:静态页 → 列表页 → 表单页 → 复杂交互页
|
|
110
|
-
- 迁移时提取可复用组件,遵循目标 React 或 Vue 项目的目录结构
|
|
111
|
-
- 每完成一个模块,补充单元测试和 E2E 关键路径
|
|
112
|
-
|
|
113
|
-
### 阶段 3:收尾
|
|
114
|
-
|
|
115
|
-
- 按迁移策略移除或冻结旧代码入口;若保留 MPA,则收敛旧入口并完成现代化边界
|
|
116
|
-
- 配置 404、错误边界、全局错误处理
|
|
117
|
-
- 性能优化(懒加载、代码分割、缓存策略)
|
|
118
|
-
- 文档更新、部署流程调整
|
|
119
|
-
|
|
120
|
-
## 重构实施要求
|
|
121
|
-
|
|
122
|
-
迁移时需遵循以下实施约束,确保视觉与交互一致、代码更简洁易维护。
|
|
123
|
-
|
|
124
|
-
### 图片与图标
|
|
125
|
-
|
|
126
|
-
- 直接使用原项目的图片资源路径,不重新托管或替换
|
|
127
|
-
- 可使用 SVG 图片(`<img src="*.svg" />` 或 CSS `background-image`);图标组件或可访问交互确需内联 SVG 时,应遵循目标项目图标规范并说明理由。
|
|
128
|
-
- 若原项目使用了 iconfont 或 IcoMoon 图标,重构时优先继续使用,保持图标体系一致;替换为其他图标方案前需评估视觉一致性、包体和维护成本。
|
|
129
|
-
- 图标优先使用原项目已有的图标文件(iconfont / IcoMoon / 已有 SVG),必要时可引入 SVG 文件作为独立资源
|
|
130
|
-
|
|
131
|
-
### 国际化(i18n)
|
|
132
|
-
|
|
133
|
-
- 新栈代码(React / Vue 等)中的用户可见文案须走项目 i18n,不在新代码里硬编码中/英文案
|
|
134
|
-
- 遗留 HTML/JS 中**不要新增**用户可见硬编码文案,除非纯静态且仓库尚无可用 i18n 接入点;改动文案前先查 locales 是否已有条目,并同步主要语言文件
|
|
135
|
-
- key 按页面/模块/语义组织,复用既有 key;仅当旧 key 为对外契约时才做映射过渡,详见项目规则中的 **国际化 (i18n)**(`templates/shared/rules/fec-i18n.md` 内「遗留代码与迁移场景」)
|
|
136
|
-
|
|
137
|
-
### 样式
|
|
138
|
-
|
|
139
|
-
- 布局样式对齐原项目视觉效果,但**只参考原项目效果,不照搬其 CSS**
|
|
140
|
-
- 优先使用 **flex 弹性布局**,避免 `float`、复杂 `position`、冗余嵌套
|
|
141
|
-
- 避免不合理写法:如 `!important` 滥用、过深选择器、重复定义
|
|
142
|
-
- 组件中优先避免内联样式(`style={{ ... }}` / `style="..."`);确需使用运行时动态值、第三方组件 API 或 CSS 变量桥接时,应保持局部、可解释,并优先放入目标项目样式体系。
|
|
143
|
-
|
|
144
|
-
### 目标
|
|
145
|
-
|
|
146
|
-
- **视觉和交互**:与原项目一致,用户无感知差异
|
|
147
|
-
- **代码质量**:更简洁、易维护,符合目标框架规范
|
|
148
|
-
- **业务功能**:与原项目保持一致,**不得缺失功能**;迁移前后行为等价
|
|
149
|
-
|
|
150
|
-
## 迁移检查清单
|
|
151
|
-
|
|
152
|
-
每个迁移单元完成后,确认:
|
|
153
|
-
|
|
154
|
-
- [ ] 业务逻辑与旧实现一致,无功能遗漏,功能完整等价
|
|
155
|
-
- [ ] 类型定义完整,无 `any` 滥用
|
|
156
|
-
- [ ] API 调用使用统一 request 层,错误处理完整
|
|
157
|
-
- [ ] 表单校验、loading、空状态、错误状态已实现
|
|
158
|
-
- [ ] 可访问性:表单有 label、交互可键盘操作
|
|
159
|
-
- [ ] 无 XSS 风险(用户输入已转义或使用安全 API)
|
|
160
|
-
- [ ] 关键路径有测试覆盖
|
|
161
|
-
- [ ] 符合目标栈的项目规范(React / Vue / Next / Nuxt / MPA 现代化等)
|
|
162
|
-
- [ ] 图片使用原项目资源;图标体系优先沿用原项目或目标项目规范,例外已说明
|
|
163
|
-
- [ ] 样式参考原项目效果但不照搬 CSS,优先使用目标项目样式体系,内联样式例外已说明
|
|
164
|
-
- [ ] 视觉与交互与原项目一致,代码更简洁易维护
|
|
165
|
-
- [ ] 新代码路径文案已 i18n;遗留层未扩大硬编码;locales 检索与多语言同步已完成(参见 i18n 规则迁移补充清单)
|
|
17
|
+
2. 加载 [references/migration-strategy-and-mapping.md](references/migration-strategy-and-mapping.md),选择渐进式或一次性重写策略,并明确遗留模式到目标栈的映射。
|
|
18
|
+
3. 加载 [references/migration-execution-checklist.md](references/migration-execution-checklist.md),完成存量盘点、依赖分析、优先级排序和分阶段迁移计划。
|
|
19
|
+
4. 撰写迁移分析或迁移计划报告时,加载 [references/migration-report-template.md](references/migration-report-template.md)。
|
|
20
|
+
5. 按准备、基础层、模块/页面迁移、收尾的顺序推进,每次迁移一个可验证单元。
|
|
21
|
+
6. 每个迁移单元验证功能等价、视觉一致、类型安全、可访问性、i18n、资源和样式边界。
|
|
166
22
|
|
|
167
23
|
## Constraints
|
|
168
24
|
|
|
169
|
-
-
|
|
170
|
-
- 不要在一次迁移中同时改架构、改 UI
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
- 遇到 jQuery 插件无现成替代时,可暂时用 iframe
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
25
|
+
- 迁移前必须先输出迁移分析报告,明确策略、优先级和风险。
|
|
26
|
+
- 不要在一次迁移中同时改架构、改 UI、改接口。
|
|
27
|
+
- 优先保证功能等价,再考虑优化和现代化。
|
|
28
|
+
- 迁移过程中保持旧系统可运行,避免大爆炸式上线。
|
|
29
|
+
- 遇到 jQuery 插件无现成替代时,可暂时用 iframe 或微前端方式嵌入,待后续替换。
|
|
30
|
+
- 目标栈先识别 React/Vue/Next/Nuxt、现代 MPA 或局部现代化目标,不默认强制改成 SPA。
|
|
31
|
+
- 图片使用原项目资源;图标体系优先沿用原项目或目标项目规范;内联 SVG 例外需有可访问性、组件化或维护理由。
|
|
32
|
+
- 样式参考原项目效果但不照搬 CSS,优先使用目标项目样式体系;内联样式例外需局部、可解释。
|
|
33
|
+
- 目标是视觉与交互一致、代码更简洁易维护,业务功能不得缺失。
|
|
34
|
+
- 涉及页面、组件、路由、表单、弹窗、导航或关键用户流程的迁移,必须建立重构前后的行为清单,并使用 Playwright 或同等真实浏览器验证方式对关键路径做对比验证。
|
|
35
|
+
- 对视觉敏感页面,应补充截图对比或人工截图验收;动态内容、动画、字体和环境差异需明确屏蔽、稳定化或说明。
|
|
36
|
+
- 对纯逻辑、类型、构建或无 UI 的迁移,不强制 Playwright,应选择更贴近风险的验证层:type-check、unit test、component test、build 或 lint。
|
|
37
|
+
- 验证目标不是像素级完全一致,而是确认业务功能无缺失、关键交互等价、主要视觉布局无非预期偏差,并且代码比旧实现更清晰、可维护。
|
|
182
38
|
|
|
183
39
|
## Expected Output
|
|
184
40
|
|
|
185
|
-
- 迁移分析报告保存为 `reports/migration-plan-YYYY-MM-DD-HHmmss.md
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
- 类型定义完整,无 `any` 滥用,API
|
|
189
|
-
-
|
|
190
|
-
- 新代码文案已 i18n
|
|
41
|
+
- 迁移分析报告保存为 `reports/migration-plan-YYYY-MM-DD-HHmmss.md`,包含策略选择、存量盘点、依赖关系、分阶段步骤、风险与回滚方案。
|
|
42
|
+
- 迁移后的业务功能与原项目完全等价,无功能缺失。
|
|
43
|
+
- 视觉与交互与原项目一致,用户无感知差异。
|
|
44
|
+
- 类型定义完整,无 `any` 滥用,API 调用统一且类型安全。
|
|
45
|
+
- 图片使用原项目资源,样式进入目标项目样式体系,图标和内联样式例外有明确理由。
|
|
46
|
+
- 新代码文案已 i18n,关键路径有测试覆盖。
|