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:
|
aliases:
|
||||||
prototype_keys:
|
prototype_keys:
|
||||||
T.pri:
|
T.pri:
|
||||||
token: --tk-pri
|
- value: "#C4623A"
|
||||||
status: exact_match
|
token: --tk-pri
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#3A6B8C"
|
||||||
|
token: --tk-pri.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.priL:
|
T.priL:
|
||||||
token: --tk-pri-l
|
- value: "#F0DDD4"
|
||||||
status: exact_match
|
token: --tk-pri-l
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#D4E5F0"
|
||||||
|
token: --tk-pri-l.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.priD:
|
T.priD:
|
||||||
token: --tk-pri-d
|
- value: "#8B3E1F"
|
||||||
status: exact_match
|
token: --tk-pri-d
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#2A4F6A"
|
||||||
|
token: --tk-pri-d.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.bg:
|
T.bg:
|
||||||
token: null
|
token: null
|
||||||
|
|||||||
@@ -84,8 +84,14 @@ rules:
|
|||||||
name: "登录/注册触发"
|
name: "登录/注册触发"
|
||||||
patterns:
|
patterns:
|
||||||
- "登录"
|
- "登录"
|
||||||
- "立即"
|
|
||||||
- "注册"
|
- "注册"
|
||||||
|
- "微信登录"
|
||||||
|
- "立即登录"
|
||||||
|
exclude_patterns:
|
||||||
|
- "立即关注"
|
||||||
|
- "立即处理"
|
||||||
|
- "立即查看"
|
||||||
|
- "需要立即"
|
||||||
require_all: false
|
require_all: false
|
||||||
infer:
|
infer:
|
||||||
component: "PrimaryButton"
|
component: "PrimaryButton"
|
||||||
|
|||||||
@@ -121,6 +121,23 @@ function matchRule(rule, source, funcRanges) {
|
|||||||
? matchedPatterns.length === rule.patterns.length
|
? matchedPatterns.length === rule.patterns.length
|
||||||
: matchedPatterns.length > 0;
|
: 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 {
|
return {
|
||||||
matched: allMatched,
|
matched: allMatched,
|
||||||
matchedPatterns: allMatched ? matchedPatterns : [],
|
matchedPatterns: allMatched ? matchedPatterns : [],
|
||||||
|
|||||||
@@ -268,18 +268,53 @@ function buildTokenRegistry(tokensConfig) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layer 1: 别名直查
|
* Layer 1: 别名直查(支持值条件映射)
|
||||||
* @returns {object|null} 匹配结果或 null
|
*
|
||||||
|
* 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;
|
if (!aliases || !aliases.prototype_keys) return null;
|
||||||
const alias = aliases.prototype_keys[key];
|
const alias = aliases.prototype_keys[key];
|
||||||
if (!alias) return null;
|
if (!alias) return null;
|
||||||
|
|
||||||
const result = {
|
// 数组格式:按值条件匹配
|
||||||
method: 'alias',
|
if (Array.isArray(alias)) {
|
||||||
confidence: null,
|
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') {
|
if (alias.status === 'exact_match') {
|
||||||
result.token = alias.token || null;
|
result.token = alias.token || null;
|
||||||
@@ -549,7 +584,7 @@ function main() {
|
|||||||
const prototypeTokens = parseResult.tokens || {};
|
const prototypeTokens = parseResult.tokens || {};
|
||||||
for (const [key, value] of Object.entries(prototypeTokens)) {
|
for (const [key, value] of Object.entries(prototypeTokens)) {
|
||||||
// Layer 1: 别名直查
|
// Layer 1: 别名直查
|
||||||
const aliasResult = matchByAlias(key, aliases);
|
const aliasResult = matchByAlias(key, value, aliases);
|
||||||
if (aliasResult) {
|
if (aliasResult) {
|
||||||
matched[key] = {
|
matched[key] = {
|
||||||
...aliasResult,
|
...aliasResult,
|
||||||
@@ -617,9 +652,14 @@ function main() {
|
|||||||
const totalAlias = Object.keys(prototypeTokens).length;
|
const totalAlias = Object.keys(prototypeTokens).length;
|
||||||
const totalInline = Object.values(inlineStyles).reduce((sum, arr) => sum + arr.length, 0);
|
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 = {
|
const output = {
|
||||||
source: parseResult.source || null,
|
source: parseResult.source || null,
|
||||||
|
variant,
|
||||||
matched,
|
matched,
|
||||||
unmatched,
|
unmatched,
|
||||||
inlineTokenMap,
|
inlineTokenMap,
|
||||||
|
|||||||
@@ -263,18 +263,7 @@ function extractInlineStyles(source) {
|
|||||||
function extractScreens(source) {
|
function extractScreens(source) {
|
||||||
const screens = [];
|
const screens = [];
|
||||||
|
|
||||||
// 策略:找所有 screen-wrap 块,从中提取 screen-label 文本和组件名
|
// 策略 A: 找静态 screen-label 文本(外部 span/div 标签)
|
||||||
// 两种形式:
|
|
||||||
// 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 文本
|
|
||||||
const labelPattern = /<(?:span|div)\s+(?:className|class)="screen-label"[^>]*>([^<]*)<\/(?:span|div)>/g;
|
const labelPattern = /<(?:span|div)\s+(?:className|class)="screen-label"[^>]*>([^<]*)<\/(?:span|div)>/g;
|
||||||
const labels = [];
|
const labels = [];
|
||||||
let labelMatch;
|
let labelMatch;
|
||||||
@@ -282,25 +271,35 @@ function extractScreens(source) {
|
|||||||
labels.push(labelMatch[1].trim());
|
labels.push(labelMatch[1].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取所有 screen-wrap 内的 IosFrame children 组件
|
// 策略 B: 找 IosFrame 的 label prop(JSX 属性模式)
|
||||||
// 模式: <IosFrame ...>\n <ComponentName />\n</IosFrame>
|
// 匹配: <IosFrame ... label="文本" ...>
|
||||||
// 或: <IosFrame ...><ComponentName /></IosFrame>
|
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 iosFramePattern = /<IosFrame[^>]*>\s*(?:<React\.Fragment[^>]*>)?\s*<(\w+)\s*\/?>/g;
|
||||||
const components = [];
|
const components = [];
|
||||||
let compMatch;
|
let compMatch;
|
||||||
while ((compMatch = iosFramePattern.exec(source)) !== null) {
|
while ((compMatch = iosFramePattern.exec(source)) !== null) {
|
||||||
const name = compMatch[1];
|
const name = compMatch[1];
|
||||||
// 排除 HTML 标签和 React 内置
|
|
||||||
if (name !== 'div' && name !== 'span' && name[0] === name[0].toUpperCase()) {
|
if (name !== 'div' && name !== 'span' && name[0] === name[0].toUpperCase()) {
|
||||||
components.push(name);
|
components.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 labels 和 components 配对
|
// 选择 label 来源:优先静态标签,如果数量不匹配则尝试 JSX prop
|
||||||
const count = Math.max(labels.length, components.length);
|
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++) {
|
for (let i = 0; i < count; i++) {
|
||||||
screens.push({
|
screens.push({
|
||||||
label: labels[i] || '',
|
label: selectedLabels[i] || '',
|
||||||
component: components[i] || '',
|
component: components[i] || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,16 +327,34 @@ shadow_unmapped:
|
|||||||
aliases:
|
aliases:
|
||||||
prototype_keys:
|
prototype_keys:
|
||||||
T.pri:
|
T.pri:
|
||||||
token: --tk-pri
|
- value: "#C4623A"
|
||||||
status: exact_match
|
token: --tk-pri
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#3A6B8C"
|
||||||
|
token: --tk-pri.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.priL:
|
T.priL:
|
||||||
token: --tk-pri-l
|
- value: "#F0DDD4"
|
||||||
status: exact_match
|
token: --tk-pri-l
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#D4E5F0"
|
||||||
|
token: --tk-pri-l.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.priD:
|
T.priD:
|
||||||
token: --tk-pri-d
|
- value: "#8B3E1F"
|
||||||
status: exact_match
|
token: --tk-pri-d
|
||||||
|
status: exact_match
|
||||||
|
variant: patient
|
||||||
|
- value: "#2A4F6A"
|
||||||
|
token: --tk-pri-d.doctor
|
||||||
|
status: exact_match
|
||||||
|
variant: doctor
|
||||||
|
|
||||||
T.bg:
|
T.bg:
|
||||||
token: null
|
token: null
|
||||||
|
|||||||
Reference in New Issue
Block a user