@dongmingzhixiu/vuexir-compiler 0.5.3 → 0.5.5
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/dist/codegen/compose.js +11 -4
- package/dist/codegen/swiftui.d.ts +2 -0
- package/dist/codegen/swiftui.js +75 -14
- package/dist/ir/schema.js +7 -6
- package/dist/transform/css.js +2 -0
- package/dist/transform/vue2ir.js +16 -3
- package/package.json +1 -1
package/dist/codegen/compose.js
CHANGED
|
@@ -1542,9 +1542,16 @@ function parseRouter(src, dir, routerFile) {
|
|
|
1542
1542
|
}
|
|
1543
1543
|
return cands.find((c) => existsSync(c)) ?? cands[0];
|
|
1544
1544
|
};
|
|
1545
|
-
// 配平扫描所有「含 path
|
|
1545
|
+
// 配平扫描所有「含 path:」的对象块(从 routes: [...] 数组起点开始,避免把 createRouter 顶层对象整块当路由)
|
|
1546
1546
|
const blocks = [];
|
|
1547
|
-
|
|
1547
|
+
let scanFrom = 0;
|
|
1548
|
+
const routesMarker = src.indexOf('routes:');
|
|
1549
|
+
if (routesMarker !== -1) {
|
|
1550
|
+
const arrIdx = src.indexOf('[', routesMarker);
|
|
1551
|
+
if (arrIdx !== -1)
|
|
1552
|
+
scanFrom = arrIdx;
|
|
1553
|
+
}
|
|
1554
|
+
for (let i = scanFrom; i < src.length; i += 1) {
|
|
1548
1555
|
if (src[i] !== '{')
|
|
1549
1556
|
continue;
|
|
1550
1557
|
let depth = 0;
|
|
@@ -1609,7 +1616,7 @@ function parseRouter(src, dir, routerFile) {
|
|
|
1609
1616
|
}
|
|
1610
1617
|
const hasText = (file) => {
|
|
1611
1618
|
if (!file || !existsSync(file))
|
|
1612
|
-
return
|
|
1619
|
+
return false;
|
|
1613
1620
|
const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
|
|
1614
1621
|
return tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().length > 0;
|
|
1615
1622
|
};
|
|
@@ -1623,7 +1630,7 @@ function parseRouter(src, dir, routerFile) {
|
|
|
1623
1630
|
path: r.path,
|
|
1624
1631
|
}));
|
|
1625
1632
|
const details = routes
|
|
1626
|
-
.filter((r) => r.meta.tab !== 'true')
|
|
1633
|
+
.filter((r) => r.meta.tab !== 'true' && hasText(r.file))
|
|
1627
1634
|
.map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
|
|
1628
1635
|
if (tabs.length === 0) {
|
|
1629
1636
|
warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
|
|
@@ -49,6 +49,7 @@ export declare function generateScreenSource(screen: IrScreen, opts?: {
|
|
|
49
49
|
colorNames: Map<string, string>;
|
|
50
50
|
fontNames: Map<string, string>;
|
|
51
51
|
};
|
|
52
|
+
cssVars?: Map<string, string>;
|
|
52
53
|
/** M4a 多页:页面运行时引用(默认 Runtime.shared 单例;多页传 Runtime.forPage("<id>") 隔离 JS 上下文) */
|
|
53
54
|
runtime?: string;
|
|
54
55
|
/** M4b Tabs:状态提升到容器(页面用 @Binding、不生成 onAppear;绑定+onMounted 由容器统一驱动) */
|
|
@@ -68,4 +69,5 @@ export declare function generateComponentSource(name: string, ui: IrNode, opts?:
|
|
|
68
69
|
colorNames: Map<string, string>;
|
|
69
70
|
fontNames: Map<string, string>;
|
|
70
71
|
};
|
|
72
|
+
cssVars?: Map<string, string>;
|
|
71
73
|
}): string;
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -22,7 +22,18 @@
|
|
|
22
22
|
* flex 仅支持容器主轴方向的 maxHeight/maxWidth:.infinity。
|
|
23
23
|
*/
|
|
24
24
|
/* ============ 颜色表达式 ============ */
|
|
25
|
+
let currentCssVars;
|
|
25
26
|
function parseColor(c) {
|
|
27
|
+
const trimmed = c.trim();
|
|
28
|
+
if (trimmed.startsWith('var(')) {
|
|
29
|
+
const m = trimmed.match(/^var\(\s*(--[\w-]+)/);
|
|
30
|
+
if (m && currentCssVars) {
|
|
31
|
+
const val = currentCssVars.get(m[1]);
|
|
32
|
+
if (val !== undefined)
|
|
33
|
+
return parseColor(val);
|
|
34
|
+
}
|
|
35
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
36
|
+
}
|
|
26
37
|
if (c.startsWith('#')) {
|
|
27
38
|
let hex = c.slice(1);
|
|
28
39
|
if (hex.length === 3)
|
|
@@ -276,6 +287,7 @@ function toSwiftIdentifier(id) {
|
|
|
276
287
|
const FW_MAP = { normal: '.regular', medium: '.medium', bold: '.bold' };
|
|
277
288
|
const ALIGN_MAP = { start: '.leading', center: '.center', end: '.trailing', left: '.leading', right: '.trailing' };
|
|
278
289
|
const FLEX_START_MAP = { start: '.leading', center: '.center', end: '.trailing' };
|
|
290
|
+
const V_ALIGN_MAP = { start: '.top', center: '.center', end: '.bottom' };
|
|
279
291
|
/** R-H:节点自身高度是否确定(数字 height / height:'fill';或 flex 子项且父链确定) */
|
|
280
292
|
function heightDefinite(props, parentDefinite) {
|
|
281
293
|
// 'fill' = 撑满父高(root 高度固定,fill 仅出现在 root 内层包裹 → 高度确定链成立)
|
|
@@ -516,10 +528,15 @@ function emitNode(node, ctx) {
|
|
|
516
528
|
}
|
|
517
529
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
518
530
|
function swiftCondExpr(expr, known) {
|
|
519
|
-
// R128:下沉的条件状态 →
|
|
531
|
+
// R128:下沉的条件状态 → 判真;不在已知集(组件/无状态上下文)→ true 保留内容
|
|
520
532
|
const sunk = expr.match(/^__vrnCond:(\w+)$/);
|
|
521
|
-
if (sunk)
|
|
522
|
-
|
|
533
|
+
if (sunk) {
|
|
534
|
+
const nm = sunk[1];
|
|
535
|
+
const kset = known && known.size > 0 ? known : currentStateNames;
|
|
536
|
+
if (kset && kset.size > 0 && !kset.has(nm))
|
|
537
|
+
return 'true';
|
|
538
|
+
return `vrnTruthy(${nm})`;
|
|
539
|
+
}
|
|
523
540
|
// R114:条件引用原生端不存在的标识符 → true(保留内容),避免整页编译失败
|
|
524
541
|
const knownSet = known && known.size > 0 ? known : currentStateNames;
|
|
525
542
|
if (knownSet && knownSet.size > 0) {
|
|
@@ -735,7 +752,7 @@ function emitNodeInner(node, ctx) {
|
|
|
735
752
|
// CSS align-items 默认 stretch(子项拉伸到容器交叉轴全宽),SwiftUI VStack 无 stretch
|
|
736
753
|
// → 子项显式 .frame(maxWidth:.infinity) 实现(内容仍左对齐)
|
|
737
754
|
const alignRaw = props.alignItems ?? 'stretch';
|
|
738
|
-
const align = isRow ? (
|
|
755
|
+
const align = isRow ? (V_ALIGN_MAP[alignRaw] ?? '.top') : (FLEX_START_MAP[alignRaw] ?? '.leading');
|
|
739
756
|
const stack = hasAbsolute ? 'ZStack' : isRow ? 'HStack' : 'VStack';
|
|
740
757
|
const stackAlign = hasAbsolute ? '.topLeading' : align;
|
|
741
758
|
// R132:ZStack 里必须把「非绝对定位」的兄弟包成一个流式子容器,
|
|
@@ -881,7 +898,7 @@ function emitNodeInner(node, ctx) {
|
|
|
881
898
|
// M3:onSet 内联赋值后推送 stateStore(JS 侧状态同步,供 handler 读取)
|
|
882
899
|
if (props.onSet || props.onClick) {
|
|
883
900
|
// R-VFOBJ:onClick 含循环变量字段(router.push(item.path))→ Swift 内联展开(JS 无 item 作用域)
|
|
884
|
-
const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop) : null;
|
|
901
|
+
const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop, ctx.runtime) : null;
|
|
885
902
|
const action = props.onSet
|
|
886
903
|
? `action: { ${props.onSet.var} = ${props.onSet.kind === 'expr'
|
|
887
904
|
? String(props.onSet.value)
|
|
@@ -1117,7 +1134,7 @@ function emitNodeInner(node, ctx) {
|
|
|
1117
1134
|
// 需 maxWidth:.infinity 以在父容器占满宽(组件 View 默认大小=内容)——父 row/column 的
|
|
1118
1135
|
// alignItems:stretch 语义由使用点 frame 承担;组件根自身不携带布局。
|
|
1119
1136
|
if (props.onClick) {
|
|
1120
|
-
const loopAction = loopClickAction(props.onClick, ctx.loop);
|
|
1137
|
+
const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
|
|
1121
1138
|
const action = loopAction
|
|
1122
1139
|
? `action: { ${loopAction} }`
|
|
1123
1140
|
: `action: { ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
|
|
@@ -1151,6 +1168,13 @@ function emitNodeInner(node, ctx) {
|
|
|
1151
1168
|
}));
|
|
1152
1169
|
let body;
|
|
1153
1170
|
if (vf.listState) {
|
|
1171
|
+
// 组件/无状态上下文:_vrnE# 下沉状态不可求值 → 空 ForEach(编译保底,不渲染该项)
|
|
1172
|
+
const knownS = ctx.stateNames && ctx.stateNames.size > 0 ? ctx.stateNames : currentStateNames;
|
|
1173
|
+
const unresolved = /^_vrnE\d+$/.test(vf.listState) && (!knownS || !knownS.has(vf.listState));
|
|
1174
|
+
if (unresolved) {
|
|
1175
|
+
ctx.warnings.push(`${ctx.path}: v-for 数据源「${vf.listState}」原生端不可求值(组件内无状态上下文),该项已置空`);
|
|
1176
|
+
return 'EmptyView()';
|
|
1177
|
+
}
|
|
1154
1178
|
// 动态列表:解码失败或空数组时保持空(ForEach 不渲染);if 语句不能直接挂修饰符 → Group 包裹
|
|
1155
1179
|
body =
|
|
1156
1180
|
`Group {\n` +
|
|
@@ -1259,6 +1283,7 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1259
1283
|
currentColorNames = opts.theme?.colorNames;
|
|
1260
1284
|
currentFontNames = opts.theme?.fontNames;
|
|
1261
1285
|
currentFrameShort = opts.theme !== undefined;
|
|
1286
|
+
currentCssVars = opts.cssVars;
|
|
1262
1287
|
currentStateTypes = Object.fromEntries((screen.state ?? []).map((s) => [
|
|
1263
1288
|
s.name,
|
|
1264
1289
|
typeof s.init === 'string' ? 'string' : typeof s.init === 'boolean' ? 'boolean' : 'number',
|
|
@@ -1383,6 +1408,10 @@ function actionExpr(props, ctx) {
|
|
|
1383
1408
|
return `{ ${props.onSet.var} = ${lit}; ${ctx.runtime}.pushState("${props.onSet.var}", ${props.onSet.var}) }`;
|
|
1384
1409
|
}
|
|
1385
1410
|
if (props.onClick !== undefined) {
|
|
1411
|
+
// R-LOOPITEM:onTapGesture 路径同样支持循环变量(row 只在 Swift 闭包作用域)
|
|
1412
|
+
const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
|
|
1413
|
+
if (loopAction)
|
|
1414
|
+
return `{ ${loopAction} }`;
|
|
1386
1415
|
return `{ ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
|
|
1387
1416
|
}
|
|
1388
1417
|
return undefined;
|
|
@@ -1390,22 +1419,51 @@ function actionExpr(props, ctx) {
|
|
|
1390
1419
|
/** R-VFOBJ:onClick 表达式含循环变量字段 → 返回 Swift 内联 action 体;否则 null。
|
|
1391
1420
|
* 支持 router.push(item.path) → Runtime.routerPushHandler?(item["path"] ?? "");
|
|
1392
1421
|
* 其他含循环字段表达式不支持(warning 由调用方给出)。 */
|
|
1393
|
-
function loopClickAction(onClick, loop) {
|
|
1422
|
+
function loopClickAction(onClick, loop, runtime) {
|
|
1394
1423
|
if (!loop)
|
|
1395
1424
|
return null;
|
|
1396
|
-
const
|
|
1425
|
+
const itemKey = loop.itemKey;
|
|
1426
|
+
const re = new RegExp(`${itemKey}\\.([A-Za-z_][\\w$]*)`, 'g');
|
|
1397
1427
|
if (!re.test(onClick))
|
|
1398
1428
|
return null;
|
|
1399
1429
|
const m = onClick.match(/^router\.push\((.*)\)$/s);
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1430
|
+
if (m) {
|
|
1431
|
+
const arg = m[1].replace(re, `${loop.itemKey}["$1"] ?? ""`);
|
|
1432
|
+
return `Runtime.routerPushHandler?(${arg})`;
|
|
1433
|
+
}
|
|
1434
|
+
// R-LOOPITEM:非 router.push 且引用循环变量(goDetail(row.item.id))——
|
|
1435
|
+
// row 只在 Swift ForEach 闭包作用域,JS 全局不存在 → 先注入 __vrnLoop 再 invoke
|
|
1436
|
+
const replaced = onClick.replace(re, '__vrnLoop.$1');
|
|
1437
|
+
return `${runtime}.setLoopItem(${itemKey}); ${runtime}.invoke("${swiftStringEscape(replaced)}")`;
|
|
1404
1438
|
}
|
|
1405
1439
|
/** 收集组件 IR 树中所有 text ref → prop 名(组件 View 的 let 属性集合) */
|
|
1440
|
+
/** 表达式 → 根标识符(dialog.title → dialog;!allOpen → allOpen;__vrnField__:dialog:title → dialog;下沉条件 → '') */
|
|
1441
|
+
function rootIdent(expr) {
|
|
1442
|
+
const e = expr.replace(/^!/, '');
|
|
1443
|
+
const f = e.match(/^__vrnField__:(\w+)/);
|
|
1444
|
+
if (f)
|
|
1445
|
+
return f[1];
|
|
1446
|
+
if (e.startsWith('__vrnCond:') || e.startsWith('__vrnField__:'))
|
|
1447
|
+
return '';
|
|
1448
|
+
const id = e.match(/^([A-Za-z_$][\w$]*)/);
|
|
1449
|
+
return id ? id[1] : '';
|
|
1450
|
+
}
|
|
1406
1451
|
function collectRefs(node, out) {
|
|
1407
|
-
if (node.type === 'text' && node.props?.ref !== undefined)
|
|
1408
|
-
|
|
1452
|
+
if (node.type === 'text' && node.props?.ref !== undefined) {
|
|
1453
|
+
const r = rootIdent(node.props.ref);
|
|
1454
|
+
if (r)
|
|
1455
|
+
out.add(r);
|
|
1456
|
+
}
|
|
1457
|
+
if (node.condition) {
|
|
1458
|
+
const r = rootIdent(node.condition);
|
|
1459
|
+
if (r)
|
|
1460
|
+
out.add(r);
|
|
1461
|
+
}
|
|
1462
|
+
if (node.type === 'for' && node.vfor?.listState && !/^_vrnE\d+$/.test(node.vfor.listState)) {
|
|
1463
|
+
const r = rootIdent(node.vfor.listState);
|
|
1464
|
+
if (r)
|
|
1465
|
+
out.add(r);
|
|
1466
|
+
}
|
|
1409
1467
|
for (const c of node.children ?? [])
|
|
1410
1468
|
collectRefs(c, out);
|
|
1411
1469
|
return out;
|
|
@@ -1425,7 +1483,9 @@ export function generateComponentSource(name, ui, opts = {}) {
|
|
|
1425
1483
|
currentFrameShort = true;
|
|
1426
1484
|
currentStateTypes = {};
|
|
1427
1485
|
}
|
|
1486
|
+
currentCssVars = opts.cssVars;
|
|
1428
1487
|
const props = collectRefs(ui, new Set());
|
|
1488
|
+
currentStateNames = props; // 组件上下文:已知标识符 = props(清掉页面残留 state 名,下沉条件正确降级)
|
|
1429
1489
|
const dataProps = [];
|
|
1430
1490
|
const body = emitNode(ui, {
|
|
1431
1491
|
parentAxis: 'column',
|
|
@@ -1435,6 +1495,7 @@ export function generateComponentSource(name, ui, opts = {}) {
|
|
|
1435
1495
|
warnings,
|
|
1436
1496
|
path: `${name}View.body`,
|
|
1437
1497
|
dataProps,
|
|
1498
|
+
stateNames: props, // 组件已知标识符 = props 集合(v-if/v-for 下沉状态不在内 → 降级 true)
|
|
1438
1499
|
});
|
|
1439
1500
|
const propDecls = [...props]
|
|
1440
1501
|
.map((p) => ` let ${p}: String`)
|
package/dist/ir/schema.js
CHANGED
|
@@ -24,7 +24,8 @@ export const NODE_TYPES = [
|
|
|
24
24
|
const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25
25
|
const RGB_COLOR = /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*[\d.]+)?\s*\)$/;
|
|
26
26
|
function isColor(v) {
|
|
27
|
-
return typeof v === 'string' &&
|
|
27
|
+
return (typeof v === 'string' &&
|
|
28
|
+
(HEX_COLOR.test(v) || RGB_COLOR.test(v) || /^var\([^)]*\)$/.test(v.trim())));
|
|
28
29
|
}
|
|
29
30
|
function isSize(v) {
|
|
30
31
|
return v === 'fill' || v === 'auto' || (typeof v === 'number' && v >= 0 && Number.isFinite(v));
|
|
@@ -116,14 +117,14 @@ const PROPS_VALIDATORS = {
|
|
|
116
117
|
const NODE_ALLOWED_PROPS = {
|
|
117
118
|
root: ['width', 'height', 'background'],
|
|
118
119
|
group: ['width', 'height', 'padding', 'gap', 'flex', 'flexDirection', 'justifyContent', 'alignItems', 'background', 'radius', 'borderWidth', 'borderColor', 'marginTopAuto', 'marginTop', 'marginLeftAuto', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'gridColumns'],
|
|
119
|
-
text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
|
|
120
|
+
text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'background', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
|
|
120
121
|
image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
|
|
121
122
|
button: ['width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
|
|
122
123
|
input: ['width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
|
|
123
|
-
scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex'],
|
|
124
|
-
spacer: ['width', 'height', 'flex'],
|
|
125
|
-
divider: ['width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop'],
|
|
126
|
-
component: ['name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'onClick'],
|
|
124
|
+
scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex', 'justifyContent', 'alignItems', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'borderSides', 'clip'],
|
|
125
|
+
spacer: ['width', 'height', 'flex', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'alignSelf', 'opacity'],
|
|
126
|
+
divider: ['width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'radius', 'flex', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
|
|
127
|
+
component: ['name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'marginBottom', 'marginLeft', 'marginRight', 'background', 'radius', 'borderWidth', 'borderColor', 'padding', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'onClick'],
|
|
127
128
|
for: ['flexDirection', 'gap', 'padding', 'background', 'flex', 'width', 'height', 'showsIndicators', 'borderSides', 'clip', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
|
|
128
129
|
};
|
|
129
130
|
/** 必须有 children 的节点 */
|
package/dist/transform/css.js
CHANGED
|
@@ -25,6 +25,8 @@ const num = (v) => {
|
|
|
25
25
|
/** 与 schema isColor 同源的 CSS 颜色校验:仅 #rgb / #rrggbb / rgb[a]() / hsl[a]()(named/var() 原生端不可解析,拒绝) */
|
|
26
26
|
function isCssColor(v) {
|
|
27
27
|
const s = v.trim();
|
|
28
|
+
if (/^var\([^)]*\)$/.test(s))
|
|
29
|
+
return true; // CSS 变量(含 fallback 形态):值留给 codegen 按项目 tokens 解析
|
|
28
30
|
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s))
|
|
29
31
|
return true;
|
|
30
32
|
if (/^rgba?\(\s*[\d.]+%?\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*(,\s*([\d.]+%?|0))?\s*\)$/.test(s))
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -1747,9 +1747,15 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1747
1747
|
const expr = gm[3].trim();
|
|
1748
1748
|
// Vue3 语义:`v-for="i in 3"` 迭代 1..3
|
|
1749
1749
|
const rangeM = expr.match(/^(\d+)$/);
|
|
1750
|
+
// R143:数据源可能是数组、数值(v-for="i in total" 迭代 1..N)或 props 字符串,
|
|
1751
|
+
// 用运行时自适应表达式兜住全部形态
|
|
1750
1752
|
const sunk = addJsExprState(rangeM
|
|
1751
1753
|
? `JSON.stringify(Array.from({ length: ${rangeM[1]} }, function (_, i) { return i + 1; }))`
|
|
1752
|
-
: `JSON.stringify(
|
|
1754
|
+
: `JSON.stringify((function (v) {` +
|
|
1755
|
+
` if (Array.isArray(v)) return v;` +
|
|
1756
|
+
` var n = Number(v);` +
|
|
1757
|
+
` if (!isNaN(n) && n > 0) return Array.from({ length: n }, function (_, i) { return i + 1; });` +
|
|
1758
|
+
` return []; })(${expr}))`);
|
|
1753
1759
|
vf = {
|
|
1754
1760
|
listState: sunk,
|
|
1755
1761
|
itemKey: (gm[1] ? gm[1].split(',')[0] : gm[2]).trim(),
|
|
@@ -1763,10 +1769,17 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1763
1769
|
}
|
|
1764
1770
|
// M4b:动态列表数据源必须已声明为 ref 数组状态(编译期校验,防悬空引用)
|
|
1765
1771
|
// 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
|
|
1772
|
+
// R143:未声明的数据源(组件 props / 数值 / 未注册变量)→ 运行时自适应下沉,不再整页失败
|
|
1766
1773
|
if (vf.listState &&
|
|
1767
1774
|
!/^_vrnE\d+$/.test(vf.listState) &&
|
|
1768
|
-
!(state && state.states.some((s) => s.name === vf.listState))
|
|
1769
|
-
|
|
1775
|
+
!(state && state.states.some((s) => s.name === vf.listState)) &&
|
|
1776
|
+
propRefs === undefined // 页面模式才下沉;组件模式(Set 存在,含空)保留数据源原名
|
|
1777
|
+
) {
|
|
1778
|
+
const srcName = vf.listState;
|
|
1779
|
+
const sunk2 = addJsExprState(`JSON.stringify((function (v) { if (Array.isArray(v)) return v; var n = Number(v); ` +
|
|
1780
|
+
`if (!isNaN(n) && n > 0) return Array.from({ length: n }, function (_, i) { return i + 1; }); return []; })(${srcName}))`);
|
|
1781
|
+
warnings.push(`v-for 数据源「${srcName}」未声明为状态 → 已在运行时自适应求值(R143)`);
|
|
1782
|
+
vf.listState = sunk2;
|
|
1770
1783
|
}
|
|
1771
1784
|
const innerNode = { ...node, props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')) };
|
|
1772
1785
|
activeLoops.push({ itemKey: vf.itemKey, indexKey: vf.indexKey });
|