From 6151fde7c4831c42c1073c267bd4627436610a45 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 18 May 2026 01:00:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(skills):=20=E4=BF=AE=E5=A4=8D=20design-hand?= =?UTF-8?q?off=20=E5=8C=BB=E7=94=9F=E7=AB=AF=E5=8E=9F=E5=9E=8B=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - match-tokens: alias 增加值条件映射,支持 doctor/patient variant 自动检测 - parse-prototype: 支持 IosFrame label prop 提取(JSX 属性模式) - interaction-rules: login-cta 增加 exclude_patterns 避免"立即关注"误报 - tokens.yml: T.pri/T.priL/T.priD alias 改为数组格式,按值匹配 variant 验证: 患者端 T.pri→--tk-pri 确认,医生端 T.pri→--tk-pri.doctor 确认 --- .../skills/design-handoff/defaults/tokens.yml | 30 ++++++++-- .../rules/interaction-rules.yml | 8 ++- .../scripts/infer-interactions.mjs | 17 ++++++ .../design-handoff/scripts/match-tokens.mjs | 56 ++++++++++++++++--- .../scripts/parse-prototype.mjs | 37 ++++++------ .design/tokens.yml | 30 ++++++++-- 6 files changed, 138 insertions(+), 40 deletions(-) diff --git a/.claude/skills/design-handoff/defaults/tokens.yml b/.claude/skills/design-handoff/defaults/tokens.yml index 72acfe4..4505edf 100644 --- a/.claude/skills/design-handoff/defaults/tokens.yml +++ b/.claude/skills/design-handoff/defaults/tokens.yml @@ -327,16 +327,34 @@ shadow_unmapped: aliases: prototype_keys: T.pri: - token: --tk-pri - status: exact_match + - value: "#C4623A" + token: --tk-pri + status: exact_match + variant: patient + - value: "#3A6B8C" + token: --tk-pri.doctor + status: exact_match + variant: doctor T.priL: - token: --tk-pri-l - status: exact_match + - value: "#F0DDD4" + token: --tk-pri-l + status: exact_match + variant: patient + - value: "#D4E5F0" + token: --tk-pri-l.doctor + status: exact_match + variant: doctor T.priD: - token: --tk-pri-d - status: exact_match + - value: "#8B3E1F" + token: --tk-pri-d + status: exact_match + variant: patient + - value: "#2A4F6A" + token: --tk-pri-d.doctor + status: exact_match + variant: doctor T.bg: token: null diff --git a/.claude/skills/design-handoff/rules/interaction-rules.yml b/.claude/skills/design-handoff/rules/interaction-rules.yml index 1960a56..e502eeb 100644 --- a/.claude/skills/design-handoff/rules/interaction-rules.yml +++ b/.claude/skills/design-handoff/rules/interaction-rules.yml @@ -84,8 +84,14 @@ rules: name: "登录/注册触发" patterns: - "登录" - - "立即" - "注册" + - "微信登录" + - "立即登录" + exclude_patterns: + - "立即关注" + - "立即处理" + - "立即查看" + - "需要立即" require_all: false infer: component: "PrimaryButton" diff --git a/.claude/skills/design-handoff/scripts/infer-interactions.mjs b/.claude/skills/design-handoff/scripts/infer-interactions.mjs index 7c70d03..8010f65 100644 --- a/.claude/skills/design-handoff/scripts/infer-interactions.mjs +++ b/.claude/skills/design-handoff/scripts/infer-interactions.mjs @@ -121,6 +121,23 @@ function matchRule(rule, source, funcRanges) { ? matchedPatterns.length === rule.patterns.length : matchedPatterns.length > 0; + // 排除模式检查:如果任一 exclude_pattern 命中,该规则不匹配 + if (allMatched && Array.isArray(rule.exclude_patterns)) { + for (const exclPattern of rule.exclude_patterns) { + try { + const exclRegex = new RegExp(exclPattern); + if (exclRegex.test(source)) { + return { + matched: false, + matchedPatterns: [], + locations: [], + excluded_by: exclPattern, + }; + } + } catch (_e) { /* ignore regex errors */ } + } + } + return { matched: allMatched, matchedPatterns: allMatched ? matchedPatterns : [], diff --git a/.claude/skills/design-handoff/scripts/match-tokens.mjs b/.claude/skills/design-handoff/scripts/match-tokens.mjs index dd66e0a..68759bd 100644 --- a/.claude/skills/design-handoff/scripts/match-tokens.mjs +++ b/.claude/skills/design-handoff/scripts/match-tokens.mjs @@ -268,18 +268,53 @@ function buildTokenRegistry(tokensConfig) { // --------------------------------------------------------------------------- /** - * Layer 1: 别名直查 - * @returns {object|null} 匹配结果或 null + * Layer 1: 别名直查(支持值条件映射) + * + * alias 格式有两种: + * 单条: { token: "...", status: "exact_match" } + * 数组: [{ value: "#C4623A", token: "--tk-pri", variant: "patient" }, + * { value: "#3A6B8C", token: "--tk-pri.doctor", variant: "doctor" }] + * + * 数组格式按值匹配,单条格式直接匹配(兼容旧格式)。 */ -function matchByAlias(key, aliases) { +function matchByAlias(key, prototypeValue, aliases) { if (!aliases || !aliases.prototype_keys) return null; const alias = aliases.prototype_keys[key]; if (!alias) return null; - const result = { - method: 'alias', - confidence: null, - }; + // 数组格式:按值条件匹配 + if (Array.isArray(alias)) { + const normalizedInput = normalizeColor(prototypeValue) || normalizeNumericValue(prototypeValue) || String(prototypeValue); + for (const entry of alias) { + const normalizedAlias = normalizeColor(entry.value) || normalizeNumericValue(entry.value) || String(entry.value); + if (normalizedInput === normalizedAlias) { + const result = { + method: 'alias', + confidence: entry.status === 'exact_match' ? 'confirmed' : 'pending', + token: entry.token || null, + scssVar: entry.scss_var || null, + }; + if (entry.variant) result.variant = entry.variant; + if (entry.note) result.note = entry.note; + return result; + } + } + // 数组中无值匹配 → 降级到 Layer 2 + return null; + } + + // 单条格式(兼容旧格式) + const result = { method: 'alias', confidence: null }; + + // 值校验:如果 alias 有 value 字段,比较值 + if (alias.value && prototypeValue != null) { + const normAlias = normalizeColor(alias.value) || normalizeNumericValue(alias.value) || String(alias.value); + const normProto = normalizeColor(prototypeValue) || normalizeNumericValue(prototypeValue) || String(prototypeValue); + if (normAlias !== normProto) { + // 值不匹配 → 降级到 Layer 2 + return null; + } + } if (alias.status === 'exact_match') { result.token = alias.token || null; @@ -549,7 +584,7 @@ function main() { const prototypeTokens = parseResult.tokens || {}; for (const [key, value] of Object.entries(prototypeTokens)) { // Layer 1: 别名直查 - const aliasResult = matchByAlias(key, aliases); + const aliasResult = matchByAlias(key, value, aliases); if (aliasResult) { matched[key] = { ...aliasResult, @@ -617,9 +652,14 @@ function main() { const totalAlias = Object.keys(prototypeTokens).length; const totalInline = Object.values(inlineStyles).reduce((sum, arr) => sum + arr.length, 0); + // 检测 variant + const variantEntries = Object.values(matched).filter(e => e.variant); + const variant = variantEntries.length > 0 ? variantEntries[0].variant : null; + // ---- 输出 ---- const output = { source: parseResult.source || null, + variant, matched, unmatched, inlineTokenMap, diff --git a/.claude/skills/design-handoff/scripts/parse-prototype.mjs b/.claude/skills/design-handoff/scripts/parse-prototype.mjs index cef1fa8..276760f 100644 --- a/.claude/skills/design-handoff/scripts/parse-prototype.mjs +++ b/.claude/skills/design-handoff/scripts/parse-prototype.mjs @@ -263,18 +263,7 @@ function extractInlineStyles(source) { function extractScreens(source) { const screens = []; - // 策略:找所有 screen-wrap 块,从中提取 screen-label 文本和组件名 - // 两种形式: - // 1)
... 文本 ... ...
- // 2)
... 文本 ... ...
- - // 匹配所有 screen-wrap 块 - const wrapPattern = /]*>([\s\S]*?)<\/div>\s*(?=\s*<\/div>|\s*$)/g; - - // 更可靠的方式:先找所有 screen-label,再在上下文中找组件 - // 因为 screen-wrap 的 HTML 嵌套可能很复杂 - - // 提取所有 screen-label 文本 + // 策略 A: 找静态 screen-label 文本(外部 span/div 标签) const labelPattern = /<(?:span|div)\s+(?:className|class)="screen-label"[^>]*>([^<]*)<\/(?:span|div)>/g; const labels = []; let labelMatch; @@ -282,25 +271,35 @@ function extractScreens(source) { labels.push(labelMatch[1].trim()); } - // 提取所有 screen-wrap 内的 IosFrame children 组件 - // 模式: \n \n - // 或: + // 策略 B: 找 IosFrame 的 label prop(JSX 属性模式) + // 匹配: + const iosFrameLabelPattern = /]*\blabel=["']([^"']+)["'][^>]*>/g; + const iosLabels = []; + let iosLabelMatch; + while ((iosLabelMatch = iosFrameLabelPattern.exec(source)) !== null) { + iosLabels.push(iosLabelMatch[1].trim()); + } + + // 提取 IosFrame children 组件 const iosFramePattern = /]*>\s*(?:]*>)?\s*<(\w+)\s*\/?>/g; const components = []; let compMatch; while ((compMatch = iosFramePattern.exec(source)) !== null) { const name = compMatch[1]; - // 排除 HTML 标签和 React 内置 if (name !== 'div' && name !== 'span' && name[0] === name[0].toUpperCase()) { components.push(name); } } - // 将 labels 和 components 配对 - const count = Math.max(labels.length, components.length); + // 选择 label 来源:优先静态标签,如果数量不匹配则尝试 JSX prop + const useIosLabels = labels.length === 0 || (iosLabels.length > 0 && iosLabels.length === components.length && labels.length !== components.length); + const selectedLabels = useIosLabels ? iosLabels : labels; + + // 配对 + const count = Math.max(selectedLabels.length, components.length); for (let i = 0; i < count; i++) { screens.push({ - label: labels[i] || '', + label: selectedLabels[i] || '', component: components[i] || '', }); } diff --git a/.design/tokens.yml b/.design/tokens.yml index 72acfe4..4505edf 100644 --- a/.design/tokens.yml +++ b/.design/tokens.yml @@ -327,16 +327,34 @@ shadow_unmapped: aliases: prototype_keys: T.pri: - token: --tk-pri - status: exact_match + - value: "#C4623A" + token: --tk-pri + status: exact_match + variant: patient + - value: "#3A6B8C" + token: --tk-pri.doctor + status: exact_match + variant: doctor T.priL: - token: --tk-pri-l - status: exact_match + - value: "#F0DDD4" + token: --tk-pri-l + status: exact_match + variant: patient + - value: "#D4E5F0" + token: --tk-pri-l.doctor + status: exact_match + variant: doctor T.priD: - token: --tk-pri-d - status: exact_match + - value: "#8B3E1F" + token: --tk-pri-d + status: exact_match + variant: patient + - value: "#2A4F6A" + token: --tk-pri-d.doctor + status: exact_match + variant: doctor T.bg: token: null