@dongmingzhixiu/vuexir-compiler 0.5.4 → 0.5.6
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 +81 -15
- 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,16 @@
|
|
|
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
|
+
// R-THEME:var(--x) 保留语义 → ThemeColor.color("x"),皮肤切换时原生颜色跟随
|
|
30
|
+
const m = trimmed.match(/^var\(\s*(--[\w-]+)/);
|
|
31
|
+
if (m)
|
|
32
|
+
return { mode: 'theme', name: m[1].slice(2), r: 0, g: 0, b: 0, a: 1 };
|
|
33
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
34
|
+
}
|
|
26
35
|
if (c.startsWith('#')) {
|
|
27
36
|
let hex = c.slice(1);
|
|
28
37
|
if (hex.length === 3)
|
|
@@ -36,7 +45,10 @@ function parseColor(c) {
|
|
|
36
45
|
return { r: 0, g: 0, b: 0, a: 1 };
|
|
37
46
|
}
|
|
38
47
|
function colorExpr(c) {
|
|
39
|
-
const
|
|
48
|
+
const p = parseColor(c);
|
|
49
|
+
if (p.mode === 'theme' && p.name)
|
|
50
|
+
return `ThemeColor.color("${p.name}")`;
|
|
51
|
+
const { r, g, b, a } = p;
|
|
40
52
|
// R42:色板已收集时 → Theme.<name> 引用(抽离重复颜色字面量)
|
|
41
53
|
const name = currentColorNames?.get(rgbHex(r, g, b));
|
|
42
54
|
const base = name !== undefined ? `Theme.${name}` : `Color(red: ${r}/255.0, green: ${g}/255.0, blue: ${b}/255.0)`;
|
|
@@ -276,6 +288,7 @@ function toSwiftIdentifier(id) {
|
|
|
276
288
|
const FW_MAP = { normal: '.regular', medium: '.medium', bold: '.bold' };
|
|
277
289
|
const ALIGN_MAP = { start: '.leading', center: '.center', end: '.trailing', left: '.leading', right: '.trailing' };
|
|
278
290
|
const FLEX_START_MAP = { start: '.leading', center: '.center', end: '.trailing' };
|
|
291
|
+
const V_ALIGN_MAP = { start: '.top', center: '.center', end: '.bottom' };
|
|
279
292
|
/** R-H:节点自身高度是否确定(数字 height / height:'fill';或 flex 子项且父链确定) */
|
|
280
293
|
function heightDefinite(props, parentDefinite) {
|
|
281
294
|
// 'fill' = 撑满父高(root 高度固定,fill 仅出现在 root 内层包裹 → 高度确定链成立)
|
|
@@ -516,10 +529,15 @@ function emitNode(node, ctx) {
|
|
|
516
529
|
}
|
|
517
530
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
518
531
|
function swiftCondExpr(expr, known) {
|
|
519
|
-
// R128:下沉的条件状态 →
|
|
532
|
+
// R128:下沉的条件状态 → 判真;不在已知集(组件/无状态上下文)→ true 保留内容
|
|
520
533
|
const sunk = expr.match(/^__vrnCond:(\w+)$/);
|
|
521
|
-
if (sunk)
|
|
522
|
-
|
|
534
|
+
if (sunk) {
|
|
535
|
+
const nm = sunk[1];
|
|
536
|
+
const kset = known && known.size > 0 ? known : currentStateNames;
|
|
537
|
+
if (kset && kset.size > 0 && !kset.has(nm))
|
|
538
|
+
return 'true';
|
|
539
|
+
return `vrnTruthy(${nm})`;
|
|
540
|
+
}
|
|
523
541
|
// R114:条件引用原生端不存在的标识符 → true(保留内容),避免整页编译失败
|
|
524
542
|
const knownSet = known && known.size > 0 ? known : currentStateNames;
|
|
525
543
|
if (knownSet && knownSet.size > 0) {
|
|
@@ -735,7 +753,7 @@ function emitNodeInner(node, ctx) {
|
|
|
735
753
|
// CSS align-items 默认 stretch(子项拉伸到容器交叉轴全宽),SwiftUI VStack 无 stretch
|
|
736
754
|
// → 子项显式 .frame(maxWidth:.infinity) 实现(内容仍左对齐)
|
|
737
755
|
const alignRaw = props.alignItems ?? 'stretch';
|
|
738
|
-
const align = isRow ? (
|
|
756
|
+
const align = isRow ? (V_ALIGN_MAP[alignRaw] ?? '.top') : (FLEX_START_MAP[alignRaw] ?? '.leading');
|
|
739
757
|
const stack = hasAbsolute ? 'ZStack' : isRow ? 'HStack' : 'VStack';
|
|
740
758
|
const stackAlign = hasAbsolute ? '.topLeading' : align;
|
|
741
759
|
// R132:ZStack 里必须把「非绝对定位」的兄弟包成一个流式子容器,
|
|
@@ -881,7 +899,7 @@ function emitNodeInner(node, ctx) {
|
|
|
881
899
|
// M3:onSet 内联赋值后推送 stateStore(JS 侧状态同步,供 handler 读取)
|
|
882
900
|
if (props.onSet || props.onClick) {
|
|
883
901
|
// R-VFOBJ:onClick 含循环变量字段(router.push(item.path))→ Swift 内联展开(JS 无 item 作用域)
|
|
884
|
-
const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop) : null;
|
|
902
|
+
const loopAction = props.onClick ? loopClickAction(props.onClick, ctx.loop, ctx.runtime) : null;
|
|
885
903
|
const action = props.onSet
|
|
886
904
|
? `action: { ${props.onSet.var} = ${props.onSet.kind === 'expr'
|
|
887
905
|
? String(props.onSet.value)
|
|
@@ -1117,7 +1135,7 @@ function emitNodeInner(node, ctx) {
|
|
|
1117
1135
|
// 需 maxWidth:.infinity 以在父容器占满宽(组件 View 默认大小=内容)——父 row/column 的
|
|
1118
1136
|
// alignItems:stretch 语义由使用点 frame 承担;组件根自身不携带布局。
|
|
1119
1137
|
if (props.onClick) {
|
|
1120
|
-
const loopAction = loopClickAction(props.onClick, ctx.loop);
|
|
1138
|
+
const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
|
|
1121
1139
|
const action = loopAction
|
|
1122
1140
|
? `action: { ${loopAction} }`
|
|
1123
1141
|
: `action: { ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
|
|
@@ -1151,6 +1169,13 @@ function emitNodeInner(node, ctx) {
|
|
|
1151
1169
|
}));
|
|
1152
1170
|
let body;
|
|
1153
1171
|
if (vf.listState) {
|
|
1172
|
+
// 组件/无状态上下文:_vrnE# 下沉状态不可求值 → 空 ForEach(编译保底,不渲染该项)
|
|
1173
|
+
const knownS = ctx.stateNames && ctx.stateNames.size > 0 ? ctx.stateNames : currentStateNames;
|
|
1174
|
+
const unresolved = /^_vrnE\d+$/.test(vf.listState) && (!knownS || !knownS.has(vf.listState));
|
|
1175
|
+
if (unresolved) {
|
|
1176
|
+
ctx.warnings.push(`${ctx.path}: v-for 数据源「${vf.listState}」原生端不可求值(组件内无状态上下文),该项已置空`);
|
|
1177
|
+
return 'EmptyView()';
|
|
1178
|
+
}
|
|
1154
1179
|
// 动态列表:解码失败或空数组时保持空(ForEach 不渲染);if 语句不能直接挂修饰符 → Group 包裹
|
|
1155
1180
|
body =
|
|
1156
1181
|
`Group {\n` +
|
|
@@ -1259,6 +1284,7 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1259
1284
|
currentColorNames = opts.theme?.colorNames;
|
|
1260
1285
|
currentFontNames = opts.theme?.fontNames;
|
|
1261
1286
|
currentFrameShort = opts.theme !== undefined;
|
|
1287
|
+
currentCssVars = opts.cssVars;
|
|
1262
1288
|
currentStateTypes = Object.fromEntries((screen.state ?? []).map((s) => [
|
|
1263
1289
|
s.name,
|
|
1264
1290
|
typeof s.init === 'string' ? 'string' : typeof s.init === 'boolean' ? 'boolean' : 'number',
|
|
@@ -1302,6 +1328,8 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1302
1328
|
`// source: ${screen.id}.ir.json (${screen.width ?? 360}x${screen.height ?? 640})`,
|
|
1303
1329
|
'',
|
|
1304
1330
|
`struct ${structName}: View {`,
|
|
1331
|
+
' // R-THEME:皮肤切换触发视图重建(UserDefaults "vrn.skin" 变化 → body 重算 → ThemeColor.color 取新皮肤色)',
|
|
1332
|
+
' @AppStorage("vrn.skin") private var skin = "warm"',
|
|
1305
1333
|
...(stateDecls ? [stateDecls, ''] : []),
|
|
1306
1334
|
...(needDismiss ? [' @Environment(' + '\\' + '.dismiss) private var dismiss', ''] : []),
|
|
1307
1335
|
// R-VFOBJ-D:静态 v-for 数据属性(数据与布局分离,body 只留 ForEach + 组件引用)
|
|
@@ -1383,6 +1411,10 @@ function actionExpr(props, ctx) {
|
|
|
1383
1411
|
return `{ ${props.onSet.var} = ${lit}; ${ctx.runtime}.pushState("${props.onSet.var}", ${props.onSet.var}) }`;
|
|
1384
1412
|
}
|
|
1385
1413
|
if (props.onClick !== undefined) {
|
|
1414
|
+
// R-LOOPITEM:onTapGesture 路径同样支持循环变量(row 只在 Swift 闭包作用域)
|
|
1415
|
+
const loopAction = loopClickAction(props.onClick, ctx.loop, ctx.runtime);
|
|
1416
|
+
if (loopAction)
|
|
1417
|
+
return `{ ${loopAction} }`;
|
|
1386
1418
|
return `{ ${ctx.runtime}.invoke("${swiftStringEscape(props.onClick)}") }`;
|
|
1387
1419
|
}
|
|
1388
1420
|
return undefined;
|
|
@@ -1390,22 +1422,51 @@ function actionExpr(props, ctx) {
|
|
|
1390
1422
|
/** R-VFOBJ:onClick 表达式含循环变量字段 → 返回 Swift 内联 action 体;否则 null。
|
|
1391
1423
|
* 支持 router.push(item.path) → Runtime.routerPushHandler?(item["path"] ?? "");
|
|
1392
1424
|
* 其他含循环字段表达式不支持(warning 由调用方给出)。 */
|
|
1393
|
-
function loopClickAction(onClick, loop) {
|
|
1425
|
+
function loopClickAction(onClick, loop, runtime) {
|
|
1394
1426
|
if (!loop)
|
|
1395
1427
|
return null;
|
|
1396
|
-
const
|
|
1428
|
+
const itemKey = loop.itemKey;
|
|
1429
|
+
const re = new RegExp(`${itemKey}\\.([A-Za-z_][\\w$]*)`, 'g');
|
|
1397
1430
|
if (!re.test(onClick))
|
|
1398
1431
|
return null;
|
|
1399
1432
|
const m = onClick.match(/^router\.push\((.*)\)$/s);
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1433
|
+
if (m) {
|
|
1434
|
+
const arg = m[1].replace(re, `${loop.itemKey}["$1"] ?? ""`);
|
|
1435
|
+
return `Runtime.routerPushHandler?(${arg})`;
|
|
1436
|
+
}
|
|
1437
|
+
// R-LOOPITEM:非 router.push 且引用循环变量(goDetail(row.item.id))——
|
|
1438
|
+
// row 只在 Swift ForEach 闭包作用域,JS 全局不存在 → 先注入 __vrnLoop 再 invoke
|
|
1439
|
+
const replaced = onClick.replace(re, '__vrnLoop.$1');
|
|
1440
|
+
return `${runtime}.setLoopItem(${itemKey}); ${runtime}.invoke("${swiftStringEscape(replaced)}")`;
|
|
1404
1441
|
}
|
|
1405
1442
|
/** 收集组件 IR 树中所有 text ref → prop 名(组件 View 的 let 属性集合) */
|
|
1443
|
+
/** 表达式 → 根标识符(dialog.title → dialog;!allOpen → allOpen;__vrnField__:dialog:title → dialog;下沉条件 → '') */
|
|
1444
|
+
function rootIdent(expr) {
|
|
1445
|
+
const e = expr.replace(/^!/, '');
|
|
1446
|
+
const f = e.match(/^__vrnField__:(\w+)/);
|
|
1447
|
+
if (f)
|
|
1448
|
+
return f[1];
|
|
1449
|
+
if (e.startsWith('__vrnCond:') || e.startsWith('__vrnField__:'))
|
|
1450
|
+
return '';
|
|
1451
|
+
const id = e.match(/^([A-Za-z_$][\w$]*)/);
|
|
1452
|
+
return id ? id[1] : '';
|
|
1453
|
+
}
|
|
1406
1454
|
function collectRefs(node, out) {
|
|
1407
|
-
if (node.type === 'text' && node.props?.ref !== undefined)
|
|
1408
|
-
|
|
1455
|
+
if (node.type === 'text' && node.props?.ref !== undefined) {
|
|
1456
|
+
const r = rootIdent(node.props.ref);
|
|
1457
|
+
if (r)
|
|
1458
|
+
out.add(r);
|
|
1459
|
+
}
|
|
1460
|
+
if (node.condition) {
|
|
1461
|
+
const r = rootIdent(node.condition);
|
|
1462
|
+
if (r)
|
|
1463
|
+
out.add(r);
|
|
1464
|
+
}
|
|
1465
|
+
if (node.type === 'for' && node.vfor?.listState && !/^_vrnE\d+$/.test(node.vfor.listState)) {
|
|
1466
|
+
const r = rootIdent(node.vfor.listState);
|
|
1467
|
+
if (r)
|
|
1468
|
+
out.add(r);
|
|
1469
|
+
}
|
|
1409
1470
|
for (const c of node.children ?? [])
|
|
1410
1471
|
collectRefs(c, out);
|
|
1411
1472
|
return out;
|
|
@@ -1425,7 +1486,9 @@ export function generateComponentSource(name, ui, opts = {}) {
|
|
|
1425
1486
|
currentFrameShort = true;
|
|
1426
1487
|
currentStateTypes = {};
|
|
1427
1488
|
}
|
|
1489
|
+
currentCssVars = opts.cssVars;
|
|
1428
1490
|
const props = collectRefs(ui, new Set());
|
|
1491
|
+
currentStateNames = props; // 组件上下文:已知标识符 = props(清掉页面残留 state 名,下沉条件正确降级)
|
|
1429
1492
|
const dataProps = [];
|
|
1430
1493
|
const body = emitNode(ui, {
|
|
1431
1494
|
parentAxis: 'column',
|
|
@@ -1435,6 +1498,7 @@ export function generateComponentSource(name, ui, opts = {}) {
|
|
|
1435
1498
|
warnings,
|
|
1436
1499
|
path: `${name}View.body`,
|
|
1437
1500
|
dataProps,
|
|
1501
|
+
stateNames: props, // 组件已知标识符 = props 集合(v-if/v-for 下沉状态不在内 → 降级 true)
|
|
1438
1502
|
});
|
|
1439
1503
|
const propDecls = [...props]
|
|
1440
1504
|
.map((p) => ` let ${p}: String`)
|
|
@@ -1446,6 +1510,8 @@ export function generateComponentSource(name, ui, opts = {}) {
|
|
|
1446
1510
|
`// source: components/${name}.vue`,
|
|
1447
1511
|
'',
|
|
1448
1512
|
`struct ${name}View: View {`,
|
|
1513
|
+
' // R-THEME:皮肤切换触发组件视图重建(@AppStorage("vrn.skin") 变化 → body 重算 → ThemeColor.color 取新皮肤色)',
|
|
1514
|
+
' @AppStorage("vrn.skin") private var skin = "warm"',
|
|
1449
1515
|
...(propDecls ? [propDecls, ''] : []),
|
|
1450
1516
|
// R-VFOBJ-D:静态 v-for 数据属性(组件内同样分离)
|
|
1451
1517
|
...(dataProps.length ? [dataProps.join('\n'), ''] : []),
|
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 });
|