fix(skills): 修复 design-handoff 医生端原型输出质量
- 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 确认
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -84,8 +84,14 @@ rules:
|
||||
name: "登录/注册触发"
|
||||
patterns:
|
||||
- "登录"
|
||||
- "立即"
|
||||
- "注册"
|
||||
- "微信登录"
|
||||
- "立即登录"
|
||||
exclude_patterns:
|
||||
- "立即关注"
|
||||
- "立即处理"
|
||||
- "立即查看"
|
||||
- "需要立即"
|
||||
require_all: false
|
||||
infer:
|
||||
component: "PrimaryButton"
|
||||
|
||||
@@ -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 : [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -263,18 +263,7 @@ function extractInlineStyles(source) {
|
||||
function extractScreens(source) {
|
||||
const screens = [];
|
||||
|
||||
// 策略:找所有 screen-wrap 块,从中提取 screen-label 文本和组件名
|
||||
// 两种形式:
|
||||
// 1) <div className="screen-wrap"> ... <span className="screen-label">文本</span> ... <IosFrame> ... </div>
|
||||
// 2) <div class="screen-wrap"> ... <span class="screen-label">文本</span> ... <IosFrame> ... </div>
|
||||
|
||||
// 匹配所有 screen-wrap 块
|
||||
const wrapPattern = /<div\s+(?:className|class)="screen-wrap"[^>]*>([\s\S]*?)<\/div>\s*(?=<div\s+(?:className|class)="screen-wrap"|<\/div>\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 组件
|
||||
// 模式: <IosFrame ...>\n <ComponentName />\n</IosFrame>
|
||||
// 或: <IosFrame ...><ComponentName /></IosFrame>
|
||||
// 策略 B: 找 IosFrame 的 label prop(JSX 属性模式)
|
||||
// 匹配: <IosFrame ... label="文本" ...>
|
||||
const iosFrameLabelPattern = /<IosFrame[^>]*\blabel=["']([^"']+)["'][^>]*>/g;
|
||||
const iosLabels = [];
|
||||
let iosLabelMatch;
|
||||
while ((iosLabelMatch = iosFrameLabelPattern.exec(source)) !== null) {
|
||||
iosLabels.push(iosLabelMatch[1].trim());
|
||||
}
|
||||
|
||||
// 提取 IosFrame children 组件
|
||||
const iosFramePattern = /<IosFrame[^>]*>\s*(?:<React\.Fragment[^>]*>)?\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] || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user