@dongmingzhixiu/vuexir-compiler 0.6.0 → 0.7.0
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/bindings.d.ts +11 -0
- package/dist/codegen/bindings.js +32 -0
- package/dist/codegen/compose.d.ts +10 -1
- package/dist/codegen/compose.js +297 -65
- package/dist/codegen/dynamic-vector.d.ts +3 -0
- package/dist/codegen/dynamic-vector.js +81 -0
- package/dist/codegen/page-js-android.d.ts +2 -0
- package/dist/codegen/page-js-android.js +59 -0
- package/dist/codegen/project-assets.d.ts +5 -0
- package/dist/codegen/project-assets.js +31 -0
- package/dist/codegen/project-styles.d.ts +2 -0
- package/dist/codegen/project-styles.js +41 -0
- package/dist/codegen/swiftui.d.ts +4 -25
- package/dist/codegen/swiftui.js +251 -71
- package/dist/codegen/vector.d.ts +3 -0
- package/dist/codegen/vector.js +165 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/dist/ir/schema.d.ts +41 -1
- package/dist/ir/schema.js +39 -20
- package/dist/transform/css.d.ts +21 -1
- package/dist/transform/css.js +217 -14
- package/dist/transform/dynamic-svg.d.ts +3 -0
- package/dist/transform/dynamic-svg.js +53 -0
- package/dist/transform/svg.d.ts +16 -0
- package/dist/transform/svg.js +96 -0
- package/dist/transform/tab-bar.d.ts +17 -0
- package/dist/transform/tab-bar.js +44 -0
- package/dist/transform/visuals.d.ts +17 -0
- package/dist/transform/visuals.js +91 -0
- package/dist/transform/vue2ir.d.ts +12 -1
- package/dist/transform/vue2ir.js +581 -188
- package/package.json +4 -2
package/dist/codegen/swiftui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dynamicVectorSwiftHelpers, wrapLayoutSwiftHelpers } from './dynamic-vector.js';
|
|
1
2
|
/**
|
|
2
3
|
* SwiftUI 生成器 v0(P0.3)
|
|
3
4
|
*
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
* P0 简化声明:justifyContent 仅 start/center/spaceBetween;breakpoints 未实现;
|
|
22
23
|
* flex 仅支持容器主轴方向的 maxHeight/maxWidth:.infinity。
|
|
23
24
|
*/
|
|
25
|
+
import { flattenTextRuns, loopBinding, swiftBoundValue } from './bindings.js';
|
|
26
|
+
import { swiftVector } from './vector.js';
|
|
24
27
|
/* ============ 颜色表达式 ============ */
|
|
25
28
|
let currentCssVars;
|
|
26
29
|
function parseColor(c) {
|
|
@@ -44,7 +47,7 @@ function parseColor(c) {
|
|
|
44
47
|
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
|
|
45
48
|
return { r: 0, g: 0, b: 0, a: 1 };
|
|
46
49
|
}
|
|
47
|
-
function colorExpr(c) {
|
|
50
|
+
export function colorExpr(c) {
|
|
48
51
|
const p = parseColor(c);
|
|
49
52
|
if (p.mode === 'theme' && p.name)
|
|
50
53
|
return `ThemeColor.color("${p.name}")`;
|
|
@@ -129,7 +132,7 @@ export function buildTheme(tokens) {
|
|
|
129
132
|
return { colorNames, fontNames };
|
|
130
133
|
}
|
|
131
134
|
/** R42 生成 Theme.swift(颜色/字体去重常量) */
|
|
132
|
-
export const VRN_HELPERS_SWIFT = `import Foundation
|
|
135
|
+
export const VRN_HELPERS_SWIFT = dynamicVectorSwiftHelpers + wrapLayoutSwiftHelpers + `import Foundation
|
|
133
136
|
import SwiftUI
|
|
134
137
|
|
|
135
138
|
// vuexir 运行时辅助(R78 对象字段取值)
|
|
@@ -166,11 +169,43 @@ private func vrnFieldRaw(_ json: String, _ path: String) -> String {
|
|
|
166
169
|
}
|
|
167
170
|
if let d = cur as? [String: Any], let next = d[seg] { cur = next } else { return "" }
|
|
168
171
|
}
|
|
172
|
+
if cur is NSNull { return "" }
|
|
169
173
|
if let s = cur as? String { return s }
|
|
170
|
-
if let
|
|
171
|
-
|
|
174
|
+
if let n = cur as? NSNumber {
|
|
175
|
+
if CFGetTypeID(n) == CFBooleanGetTypeID() { return n.boolValue ? "true" : "false" }
|
|
176
|
+
return n.stringValue
|
|
177
|
+
}
|
|
172
178
|
return String(describing: cur)
|
|
173
179
|
}
|
|
180
|
+
|
|
181
|
+
func vrnItemField(_ item: Any, _ path: String) -> String {
|
|
182
|
+
var value: Any = item
|
|
183
|
+
for part in path.split(separator: ".").map(String.init) {
|
|
184
|
+
if let map = value as? [String: Any], let next = map[part] { value = next }
|
|
185
|
+
else if let array = value as? [Any], let index = Int(part), array.indices.contains(index) { value = array[index] }
|
|
186
|
+
else { return "" }
|
|
187
|
+
}
|
|
188
|
+
if value is NSNull { return "" }
|
|
189
|
+
if let number = value as? NSNumber {
|
|
190
|
+
if CFGetTypeID(number) == CFBooleanGetTypeID() { return number.boolValue ? "true" : "false" }
|
|
191
|
+
return number.stringValue
|
|
192
|
+
}
|
|
193
|
+
return String(describing: value)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
struct VrnFractionWidth: Layout {
|
|
197
|
+
var fraction: Double
|
|
198
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
199
|
+
guard let view = subviews.first else { return .zero }
|
|
200
|
+
let width = (proposal.width ?? view.sizeThatFits(.unspecified).width) * fraction
|
|
201
|
+
let size = view.sizeThatFits(ProposedViewSize(width: width, height: proposal.height))
|
|
202
|
+
return CGSize(width: width, height: size.height)
|
|
203
|
+
}
|
|
204
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
205
|
+
subviews.first?.place(at: bounds.origin, anchor: .topLeading, proposal: ProposedViewSize(bounds.size))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
174
209
|
`;
|
|
175
210
|
export function generateThemeSource(palette) {
|
|
176
211
|
const lines = ['import SwiftUI', '', '// GENERATED by vue-render-native compiler · R42 theme tokens', 'enum Theme {'];
|
|
@@ -314,10 +349,16 @@ function gapInheritFor(child, parentGap, siblings) {
|
|
|
314
349
|
function sizeModifiers(props, axis, ctx) {
|
|
315
350
|
const mods = [];
|
|
316
351
|
const { width, height, flex } = props;
|
|
352
|
+
if (props.widthDynamic)
|
|
353
|
+
mods.push(`.frame(width: max(0, Double(String(${swiftBoundValue(props.widthDynamic, ctx.runtime)})) ?? 0))`);
|
|
354
|
+
if (props.heightDynamic)
|
|
355
|
+
mods.push(`.frame(height: max(0, Double(String(${swiftBoundValue(props.heightDynamic, ctx.runtime)})) ?? 0))`);
|
|
317
356
|
if (width === 'fill')
|
|
318
357
|
mods.push(frameExpr('fillW'));
|
|
319
358
|
else if (typeof width === 'number')
|
|
320
359
|
mods.push(`.frame(width: ${width})`);
|
|
360
|
+
if (height === 'fill')
|
|
361
|
+
mods.push('.frame(maxHeight: .infinity)');
|
|
321
362
|
if (typeof height === 'number')
|
|
322
363
|
mods.push(`.frame(height: ${height})`);
|
|
323
364
|
if (flex !== undefined) {
|
|
@@ -420,12 +461,28 @@ function capsuleColorExpr(c) {
|
|
|
420
461
|
const name = currentColorNames?.get(hex);
|
|
421
462
|
return name !== undefined ? `Theme.${name}` : `Color(red: ${r}/255.0, green: ${g}/255.0, blue: ${b}/255.0)`;
|
|
422
463
|
}
|
|
423
|
-
function boxModifiers(props) {
|
|
464
|
+
export function boxModifiers(props) {
|
|
424
465
|
const mods = [];
|
|
466
|
+
if (props.gradient) {
|
|
467
|
+
const a = props.gradient.angle * Math.PI / 180;
|
|
468
|
+
const x = Math.sin(a) / 2, y = -Math.cos(a) / 2;
|
|
469
|
+
const n = (v) => Number(v.toFixed(5));
|
|
470
|
+
const stops = props.gradient.stops.map((s) => `.init(color: ${colorExpr(s.color === 'transparent' ? 'rgba(0,0,0,0)' : s.color)}, location: ${s.offset})`).join(', ');
|
|
471
|
+
if (props.gradient.kind === 'radial')
|
|
472
|
+
mods.push(`.background(GeometryReader { geo in RadialGradient(stops: [${stops}], center: UnitPoint(x: ${props.gradient.center?.x ?? .5}, y: ${props.gradient.center?.y ?? .5}), startRadius: 0, endRadius: max(geo.size.width, geo.size.height)) })`);
|
|
473
|
+
else
|
|
474
|
+
mods.push(`.background(LinearGradient(stops: [${stops}], startPoint: UnitPoint(x: ${n(.5 - x)}, y: ${n(.5 - y)}), endPoint: UnitPoint(x: ${n(.5 + x)}, y: ${n(.5 + y)})))`);
|
|
475
|
+
}
|
|
476
|
+
if (props.minWidth !== undefined || props.minHeight !== undefined) {
|
|
477
|
+
const args = [props.minWidth !== undefined ? `minWidth: ${props.minWidth}` : '', props.minHeight !== undefined ? `minHeight: ${props.minHeight}` : ''].filter(Boolean);
|
|
478
|
+
mods.push(`.frame(${args.join(', ')})`);
|
|
479
|
+
}
|
|
425
480
|
if (props.background !== undefined)
|
|
426
481
|
mods.push(`.background(${colorExpr(props.background)})`);
|
|
427
482
|
if (props.radius !== undefined)
|
|
428
483
|
mods.push(`.cornerRadius(${props.radius})`);
|
|
484
|
+
if (props.shadow)
|
|
485
|
+
mods.push(`.shadow(color: ${colorExpr(props.shadow.color)}, radius: ${props.shadow.blur / 2}, x: ${props.shadow.x}, y: ${props.shadow.y})`);
|
|
429
486
|
if (props.borderWidth !== undefined && props.borderColor !== undefined && props.borderWidth > 0) {
|
|
430
487
|
mods.push(`.overlay(RoundedRectangle(cornerRadius: ${props.radius ?? 0}).stroke(${colorExpr(props.borderColor)}, lineWidth: ${props.borderWidth}))`);
|
|
431
488
|
}
|
|
@@ -457,6 +514,7 @@ function emitChildren(children, axis, ctx, alignRaw = 'stretch', stretchCross =
|
|
|
457
514
|
return children
|
|
458
515
|
.map((c, i) => {
|
|
459
516
|
const childCtx = {
|
|
517
|
+
...ctx,
|
|
460
518
|
parentAxis: axis,
|
|
461
519
|
level: ctx.level,
|
|
462
520
|
definiteHeight: ctx.definiteHeight,
|
|
@@ -501,7 +559,7 @@ function emitChildren(children, axis, ctx, alignRaw = 'stretch', stretchCross =
|
|
|
501
559
|
// column:仅默认 stretch 时子项填满父宽(div block 语义);显式 start/center/end → 内容宽对齐
|
|
502
560
|
const explicitCol = alignRaw === 'start' || alignRaw === 'center' || alignRaw === 'end';
|
|
503
561
|
if (!explicitCol) {
|
|
504
|
-
return body + '\n' + indent(ctx.level) + frameExpr('fillW', '.leading');
|
|
562
|
+
return body + '\n' + indent(ctx.level) + frameExpr('fillW', c.props?.textAlign === 'center' ? '.center' : c.props?.textAlign === 'right' ? '.trailing' : '.leading');
|
|
505
563
|
}
|
|
506
564
|
return body;
|
|
507
565
|
}
|
|
@@ -517,19 +575,44 @@ function emitChildren(children, axis, ctx, alignRaw = 'stretch', stretchCross =
|
|
|
517
575
|
})
|
|
518
576
|
.join('\n');
|
|
519
577
|
}
|
|
578
|
+
let sectionNodes = new WeakMap();
|
|
579
|
+
let sectionSources = [];
|
|
580
|
+
function conditionNames(ctx) {
|
|
581
|
+
const names = new Set(ctx.stateNames ?? currentStateNames);
|
|
582
|
+
if (ctx.loop) {
|
|
583
|
+
names.add(ctx.loop.itemKey);
|
|
584
|
+
if (ctx.loop.indexKey)
|
|
585
|
+
names.add(ctx.loop.indexKey);
|
|
586
|
+
}
|
|
587
|
+
return names;
|
|
588
|
+
}
|
|
520
589
|
function emitNode(node, ctx) {
|
|
590
|
+
const section = sectionNodes.get(node);
|
|
591
|
+
if (section) {
|
|
592
|
+
sectionNodes.delete(node);
|
|
593
|
+
const source = emitNode(node, { ...ctx, level: 2 });
|
|
594
|
+
const aliases = ctx.loopAliases ?? [];
|
|
595
|
+
sectionSources.push(aliases.length ? ` @ViewBuilder private func ${section}(${aliases.map((a) => `_ ${a}: Any`).join(", ")}) -> some View {\n${source}\n }` : ` @ViewBuilder private var ${section}: some View {\n${source}\n }`);
|
|
596
|
+
return indent(ctx.level) + section + (aliases.length ? `(${aliases.join(", ")})` : "");
|
|
597
|
+
}
|
|
521
598
|
if (node.condition) {
|
|
522
599
|
const L = ctx.level;
|
|
523
600
|
const inner = emitNodeInner(node, ctx);
|
|
524
601
|
const known = ctx.stateNames && ctx.stateNames.size > 0 ? ctx.stateNames : undefined;
|
|
525
|
-
const swiftCond = swiftCondExpr(node.condition,
|
|
602
|
+
const swiftCond = swiftCondExpr(node.condition, conditionNames(ctx), ctx.runtime);
|
|
526
603
|
return `${indent(L)}if ${swiftCond} {\n${inner}\n${indent(L)}}`;
|
|
527
604
|
}
|
|
528
605
|
return emitNodeInner(node, ctx);
|
|
529
606
|
}
|
|
530
607
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
531
|
-
function swiftCondExpr(expr, known) {
|
|
608
|
+
function swiftCondExpr(expr, known, runtime = 'Runtime.shared') {
|
|
609
|
+
const binding = expr.startsWith('__vrnCond:') ? expr.slice(10) : expr;
|
|
610
|
+
if (loopBinding(binding))
|
|
611
|
+
return `vrnTruthy(${swiftBoundValue(binding, runtime)})`;
|
|
532
612
|
// R128:下沉的条件状态 → 判真;不在已知集(组件/无状态上下文)→ true 保留内容
|
|
613
|
+
const field = expr.match(/^__vrnField__:(\w+):(\w+)$/);
|
|
614
|
+
if (field)
|
|
615
|
+
return `vrnTruthy(vrnField(${field[1]}, "${field[2]}"))`;
|
|
533
616
|
const sunk = expr.match(/^__vrnCond:(\w+)$/);
|
|
534
617
|
if (sunk) {
|
|
535
618
|
const nm = sunk[1];
|
|
@@ -570,7 +653,7 @@ function swiftCondExpr(expr, known) {
|
|
|
570
653
|
if (/&&|\|\|/.test(s) && !/__vrnField__/.test(s) && !/[()]/.test(s)) {
|
|
571
654
|
const parts = s.split(/(&&|\|\|)/);
|
|
572
655
|
return parts
|
|
573
|
-
.map((seg) => (seg === '&&' || seg === '||' ? seg : swiftCondExpr(seg.trim(), known)))
|
|
656
|
+
.map((seg) => (seg === '&&' || seg === '||' ? seg : swiftCondExpr(seg.trim(), known, runtime)))
|
|
574
657
|
.join(' ');
|
|
575
658
|
}
|
|
576
659
|
if (/^![A-Za-z_$][\w$]*$/.test(s))
|
|
@@ -601,6 +684,15 @@ function emitNodeInner(node, ctx) {
|
|
|
601
684
|
const L = ctx.level;
|
|
602
685
|
const pad = (s) => indent(L) + s;
|
|
603
686
|
const props = node.props ?? {};
|
|
687
|
+
if (props.show) {
|
|
688
|
+
const content = emitNode({ ...node, props: { ...props, show: undefined } }, { ...ctx, level: L + 1 });
|
|
689
|
+
return pad(`if ${swiftCondExpr(props.show, ctx.stateNames, ctx.runtime)} {`) + `\n${content}\n${pad('}')}`;
|
|
690
|
+
}
|
|
691
|
+
if (props.widthFraction !== undefined || props.widthFractionDynamic) {
|
|
692
|
+
const fraction = props.widthFractionDynamic ? `(Double(String(${swiftBoundValue(props.widthFractionDynamic, ctx.runtime)})) ?? 0)` : String(props.widthFraction);
|
|
693
|
+
const inner = emitNodeInner({ ...node, props: { ...props, widthFraction: undefined, widthFractionDynamic: undefined } }, { ...ctx, level: ctx.level + 1 });
|
|
694
|
+
return pad(`VrnFractionWidth(fraction: max(0,min(1,${fraction}))) {`) + `\n${inner}\n${pad('}')}`;
|
|
695
|
+
}
|
|
604
696
|
const axis = ctx.parentAxis ?? 'column';
|
|
605
697
|
// box-sizing:border-box(CSS 默认):固定 height 应包含 padding。
|
|
606
698
|
// 故 height 为数字且存在 padding 时,padding 置于 frame(height) 之前、
|
|
@@ -629,9 +721,15 @@ function emitNodeInner(node, ctx) {
|
|
|
629
721
|
}
|
|
630
722
|
}
|
|
631
723
|
// M5 position:absolute → 脱离文档流(父容器已转 ZStack;offset 实现 top/left 定位)
|
|
724
|
+
if (props.zIndex !== undefined)
|
|
725
|
+
box.push(`.zIndex(${props.zIndex})`);
|
|
632
726
|
if (props.position === 'absolute') {
|
|
727
|
+
const fullInset = props.absTop === 0 && props.absLeft === 0 && props.absRight === 0 && props.absBottom === 0;
|
|
633
728
|
const hasRB = props.absRight !== undefined || props.absBottom !== undefined;
|
|
634
|
-
if (
|
|
729
|
+
if (fullInset) {
|
|
730
|
+
box.push('.frame(maxWidth: .infinity, maxHeight: .infinity)');
|
|
731
|
+
}
|
|
732
|
+
else if (hasRB) {
|
|
635
733
|
// R131:right/bottom 定位 → 用对齐 + 内边距表达(此前直接丢弃,导致元素重叠、内容被盖住)
|
|
636
734
|
const v = props.absBottom !== undefined ? 'bottom' : props.absTop !== undefined ? 'top' : 'center';
|
|
637
735
|
const h = props.absRight !== undefined ? 'trailing' : props.absLeft !== undefined ? 'leading' : 'center';
|
|
@@ -658,7 +756,7 @@ function emitNodeInner(node, ctx) {
|
|
|
658
756
|
}
|
|
659
757
|
// M5 v-show → opacity 动画切换(保留占位;状态名引用页面/容器同名 @State/@Binding)
|
|
660
758
|
if (props.show !== undefined) {
|
|
661
|
-
box.push(`.opacity(${props.show} ? 1 : 0)`);
|
|
759
|
+
box.push(`.opacity(vrnTruthy(String(${props.show})) ? 1 : 0)`);
|
|
662
760
|
box.push(`.animation(.easeInOut(duration: 0.25), value: ${props.show})`);
|
|
663
761
|
}
|
|
664
762
|
// CSS opacity(0~1):静态透明度(与 v-show 的条件透明叠加)
|
|
@@ -683,7 +781,8 @@ function emitNodeInner(node, ctx) {
|
|
|
683
781
|
warnings: ctx.warnings,
|
|
684
782
|
path: `${ctx.path}[${i}]`,
|
|
685
783
|
inheritGap: gapInheritFor(c, props.gap, node.children ?? []),
|
|
686
|
-
loop: ctx.loop,
|
|
784
|
+
loop: ctx.loop,
|
|
785
|
+
loopAliases: ctx.loopAliases, // M2:v-for 作用域透传
|
|
687
786
|
runtime: ctx.runtime, // M4a:页面运行时引用透传
|
|
688
787
|
dataProps: ctx.dataProps, // R-VFOBJ-D:数据声明收集透传
|
|
689
788
|
});
|
|
@@ -698,7 +797,7 @@ function emitNodeInner(node, ctx) {
|
|
|
698
797
|
// (浅色模式下是白条),表现为「详情页底部颜色与页面不一致、像高度不够」
|
|
699
798
|
const bgLayer = props.background !== undefined
|
|
700
799
|
? indent(L + 1) + `${colorExpr(props.background)}.frame(maxWidth: .infinity, maxHeight: .infinity).ignoresSafeArea()`
|
|
701
|
-
: null;
|
|
800
|
+
: props.gradient ? indent(L + 1) + 'Color.clear.ignoresSafeArea()' + boxModifiers({ gradient: props.gradient }).join('') : null;
|
|
702
801
|
// R-PAGESCROLL:页面根 = 可滚动视口(对位 Web 的 body 默认滚动)
|
|
703
802
|
// - 内容高于视口 → ScrollView 可滚动(此前仅 external tab 页按按钮数启发式包装,详情页不可滚动)
|
|
704
803
|
// - 内容不足 → minHeight = 视口高,页面仍铺满,Spacer/flex 贴底语义不变
|
|
@@ -721,6 +820,14 @@ function emitNodeInner(node, ctx) {
|
|
|
721
820
|
.join('\n');
|
|
722
821
|
}
|
|
723
822
|
case 'group': {
|
|
823
|
+
if (props.flexWrap && node.children) {
|
|
824
|
+
const children = node.children.map((child, i) => {
|
|
825
|
+
const cp = child.props ?? {};
|
|
826
|
+
const source = emitNode({ ...child, props: { ...cp, flex: undefined } }, { ...ctx, level: L + 1, parentAxis: 'row', path: ctx.path + '.wrap' + i });
|
|
827
|
+
return indent(L + 1) + 'Group {\n' + source + '\n' + indent(L + 1) + '}' + `.layoutValue(key: VrnWrapFlex.self, value: ${cp.flex ?? 0}).layoutValue(key: VrnWrapMin.self, value: ${cp.minWidth ?? 0}).layoutValue(key: VrnWrapBreak.self, value: ${cp.width === 'fill' ? 'true' : 'false'})`;
|
|
828
|
+
});
|
|
829
|
+
return pad(`VrnWrapLayout(spacing: ${props.gap ?? 0}) {`) + '\n' + children.join('\n') + '\n' + pad('}') + box.map((m) => ' ' + m).join('');
|
|
830
|
+
}
|
|
724
831
|
// R136:CSS grid(grid-template-columns: repeat(N,1fr))→ LazyVGrid
|
|
725
832
|
if (props.gridColumns !== undefined && (node.children?.length ?? 0) > 0) {
|
|
726
833
|
const cols = props.gridColumns;
|
|
@@ -734,13 +841,14 @@ function emitNodeInner(node, ctx) {
|
|
|
734
841
|
path: `${ctx.path}[${i}]`,
|
|
735
842
|
inheritGap: undefined,
|
|
736
843
|
loop: ctx.loop,
|
|
844
|
+
loopAliases: ctx.loopAliases,
|
|
737
845
|
dataProps: ctx.dataProps,
|
|
738
846
|
}))
|
|
739
847
|
.join('\n');
|
|
740
848
|
return [
|
|
741
849
|
pad(`LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: ${props.gap ?? 0}), count: ${cols}), spacing: ${props.gap ?? 0}) {`),
|
|
742
850
|
inner,
|
|
743
|
-
pad('}'),
|
|
851
|
+
pad('}') + box.map((m) => ' ' + m).join(''),
|
|
744
852
|
].join('\n');
|
|
745
853
|
}
|
|
746
854
|
const dir = props.flexDirection ?? 'column';
|
|
@@ -776,7 +884,7 @@ function emitNodeInner(node, ctx) {
|
|
|
776
884
|
if (hasAbsolute && flowChildren.length > 0) {
|
|
777
885
|
const flowNode = {
|
|
778
886
|
type: 'group',
|
|
779
|
-
props: {
|
|
887
|
+
props: { flexDirection: props.flexDirection, justifyContent: props.justifyContent, alignItems: props.alignItems, gap: props.gap, padding: props.padding },
|
|
780
888
|
children: flowChildren,
|
|
781
889
|
};
|
|
782
890
|
const flowCode = emitNode(flowNode, {
|
|
@@ -788,31 +896,19 @@ function emitNodeInner(node, ctx) {
|
|
|
788
896
|
path: `${ctx.path}{flow}`,
|
|
789
897
|
inheritGap: ctx.inheritGap,
|
|
790
898
|
loop: ctx.loop,
|
|
899
|
+
loopAliases: ctx.loopAliases,
|
|
791
900
|
dataProps: ctx.dataProps,
|
|
792
901
|
});
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
dataProps: ctx.dataProps,
|
|
804
|
-
}))
|
|
805
|
-
.join('\n');
|
|
806
|
-
const bg = props.background !== undefined ? pad(`Color(${props.background})`) : null;
|
|
807
|
-
return [
|
|
808
|
-
pad('ZStack(alignment: .topLeading) {'),
|
|
809
|
-
bg ? indent(L + 1) + bg.trim() : null,
|
|
810
|
-
flowCode,
|
|
811
|
-
absCode || null,
|
|
812
|
-
pad('}'),
|
|
813
|
-
]
|
|
814
|
-
.filter((x) => x !== null)
|
|
815
|
-
.join('\n');
|
|
902
|
+
const firstFlow = children.findIndex((c) => c.props?.position !== 'absolute');
|
|
903
|
+
let result = [pad('ZStack(alignment: .topLeading) {'), flowCode, pad('}')].join('\n');
|
|
904
|
+
for (const [i, c] of children.entries()) {
|
|
905
|
+
if (c.props?.position !== 'absolute')
|
|
906
|
+
continue;
|
|
907
|
+
const mode = i < firstFlow && (c.props.zIndex ?? 0) <= 0 ? 'background' : 'overlay';
|
|
908
|
+
const code = emitNode(c, { ...ctx, level: L + 1, parentAxis: dir, definiteHeight: true, path: `${ctx.path}{abs${i}}`, inheritGap: undefined });
|
|
909
|
+
result += `\n${indent(L)}.${mode}(alignment: .topLeading) {\n${code}\n${indent(L)}}`;
|
|
910
|
+
}
|
|
911
|
+
return result + box.filter((m) => !m.startsWith('.padding(')).map((m) => ' ' + m).join('');
|
|
816
912
|
}
|
|
817
913
|
// justifyContent:center → 首尾 Spacer;spaceBetween → 子间 Spacer(仅 column 需高度确定)
|
|
818
914
|
const body = [];
|
|
@@ -822,7 +918,7 @@ function emitNodeInner(node, ctx) {
|
|
|
822
918
|
children.forEach((c, i) => {
|
|
823
919
|
if (just === 'spaceBetween' && justActive && i > 0)
|
|
824
920
|
body.push(indent(L + 1) + 'Spacer(minLength: 0)');
|
|
825
|
-
|
|
921
|
+
let childCode = emitNode(c, {
|
|
826
922
|
parentAxis: dir,
|
|
827
923
|
level: L + 1,
|
|
828
924
|
definiteHeight: selfDefinite,
|
|
@@ -830,9 +926,18 @@ function emitNodeInner(node, ctx) {
|
|
|
830
926
|
runtime: ctx.runtime,
|
|
831
927
|
path: `${ctx.path}[${i}]`,
|
|
832
928
|
inheritGap: gapInheritFor(c, props.gap, children),
|
|
833
|
-
loop: ctx.loop,
|
|
929
|
+
loop: ctx.loop,
|
|
930
|
+
loopAliases: ctx.loopAliases, // M2:v-for 作用域透传
|
|
834
931
|
dataProps: ctx.dataProps, // R-VFOBJ-D
|
|
835
932
|
});
|
|
933
|
+
if (c.condition && (c.props?.marginBottom || c.props?.marginRight || c.props?.marginLeft))
|
|
934
|
+
childCode = indent(L + 1) + 'Group {\n' + childCode + '\n' + indent(L + 1) + '}';
|
|
935
|
+
if (dir === 'column' && c.props?.marginBottom)
|
|
936
|
+
childCode += `.padding(.bottom, ${c.props.marginBottom})`;
|
|
937
|
+
if (dir === 'row' && c.props?.marginRight)
|
|
938
|
+
childCode += `.padding(.trailing, ${c.props.marginRight})`;
|
|
939
|
+
if (dir === 'row' && c.props?.marginLeft)
|
|
940
|
+
childCode += `.padding(.leading, ${c.props.marginLeft})`;
|
|
836
941
|
// 盒模型(R29 重建 div 语义):margin 用「透明包装容器」表达,而非独立兄弟占位——
|
|
837
942
|
// VStack(spacing) 对每个独立兄弟前后各计一次 spacing,独立占位会与 gap 叠加双计
|
|
838
943
|
// (实测:gap16 + margin16 被算成 16+16+16=48pt,CSS 应为 32pt)。
|
|
@@ -872,9 +977,9 @@ function emitNodeInner(node, ctx) {
|
|
|
872
977
|
else if (c.props?.width === undefined && c.props?.position !== 'absolute') {
|
|
873
978
|
// column:仅默认 stretch(未显式声明 align-items)时子项填满父宽(div block 语义);
|
|
874
979
|
// 显式 start/center/end → 子项按内容宽对齐(CSS 交叉轴对齐语义)
|
|
875
|
-
const explicitCol = props.alignItems === 'start' || props.alignItems === 'center' || props.alignItems === 'end';
|
|
980
|
+
const explicitCol = (c.props?.alignSelf !== undefined && c.props.alignSelf !== 'stretch') || props.alignItems === 'start' || props.alignItems === 'center' || props.alignItems === 'end';
|
|
876
981
|
if (!explicitCol) {
|
|
877
|
-
body.push(indent(L + 1) + frameExpr('fillW', '.leading'));
|
|
982
|
+
body.push(indent(L + 1) + frameExpr('fillW', c.props?.textAlign === 'center' ? '.center' : c.props?.textAlign === 'right' ? '.trailing' : '.leading'));
|
|
878
983
|
}
|
|
879
984
|
}
|
|
880
985
|
});
|
|
@@ -891,7 +996,7 @@ function emitNodeInner(node, ctx) {
|
|
|
891
996
|
if (actBg !== undefined) {
|
|
892
997
|
const actColor = colorExpr(actBg);
|
|
893
998
|
const base = props.background !== undefined ? colorExpr(props.background) : 'Color.clear';
|
|
894
|
-
actBox = [`.background(${node.active.when} ? ${actColor} : ${base})`];
|
|
999
|
+
actBox = [`.background(${swiftCondExpr(node.active.when, conditionNames(ctx), ctx.runtime)} ? ${actColor} : ${base})`];
|
|
895
1000
|
}
|
|
896
1001
|
const effBox = actBox !== null ? [...actBox, ...box.filter((m) => !m.startsWith('.background('))] : box;
|
|
897
1002
|
let out = [pad(open), ...body, pad('}') + effBox.map((m) => ' ' + m).join('')].join('\n');
|
|
@@ -915,9 +1020,25 @@ function emitNodeInner(node, ctx) {
|
|
|
915
1020
|
return out;
|
|
916
1021
|
}
|
|
917
1022
|
case 'text': {
|
|
1023
|
+
if (props.textRuns) {
|
|
1024
|
+
const allowed = ['text', 'bind', 'bindParts', 'textRuns', 'fieldOf', 'ref', 'dynField', 'bindExpr', 'fontSize', 'fontWeight', 'fontFamily', 'color', 'letterSpacing'];
|
|
1025
|
+
const runs = flattenTextRuns(props.textRuns).map((run) => {
|
|
1026
|
+
const rp = {};
|
|
1027
|
+
for (const key of allowed)
|
|
1028
|
+
if (run[key] !== undefined)
|
|
1029
|
+
rp[key] = run[key];
|
|
1030
|
+
for (const key of ['fontSize', 'fontWeight', 'fontFamily', 'color'])
|
|
1031
|
+
rp[key] ??= props[key];
|
|
1032
|
+
return emitNode({ type: 'text', props: rp }, { ...ctx, level: 0 });
|
|
1033
|
+
});
|
|
1034
|
+
const locals = runs.map((code, i) => `${indent(L + 1)}let _vrnRun${i}: Text = ${code}`).join('\n');
|
|
1035
|
+
const combined = runs.map((_, i) => `_vrnRun${i}`).join(' + ') || 'Text("")';
|
|
1036
|
+
return pad('Group {') + '\n' + locals + '\n' + indent(L + 1) + combined + '\n' + pad('}') + box.map((m) => ' ' + m).join('');
|
|
1037
|
+
}
|
|
918
1038
|
// R41:组件 View 内 {{ prop }} → props.ref → Text(prop)(Swift 属性引用,非字面量)
|
|
919
1039
|
// M1:页面级 {{ state }} → props.bind / node.bind → Text(<@State 名>)(状态引用,非字面量)
|
|
920
1040
|
const isRef = props.ref !== undefined;
|
|
1041
|
+
const loopBound = props.bind && loopBinding(props.bind);
|
|
921
1042
|
const bindKey = props.bind ?? node.bind?.key;
|
|
922
1043
|
// R133:bind 指向的名字必须是真实状态(或循环变量),否则说明作用域丢失 →
|
|
923
1044
|
// 输出空串并告警,绝不能把状态名当文本渲染(日历页出现 Text("_vrnE1") 即是此因)
|
|
@@ -925,12 +1046,12 @@ function emitNodeInner(node, ctx) {
|
|
|
925
1046
|
(currentStateNames.has(bindKey.split('.')[0]) ||
|
|
926
1047
|
(ctx.loop !== undefined && (ctx.loop.itemKey === bindKey.split('.')[0] || ctx.loop.indexKey === bindKey.split('.')[0])));
|
|
927
1048
|
const isBind = bindKey !== undefined && bindValid;
|
|
928
|
-
if (bindKey !== undefined && !bindValid) {
|
|
1049
|
+
if (bindKey !== undefined && !bindValid && !loopBound) {
|
|
929
1050
|
ctx.warnings.push(`${ctx.path}: 绑定「${bindKey}」在原生端无对应状态/循环变量 → 输出空串(R133)`);
|
|
930
1051
|
}
|
|
931
1052
|
const content = props.text !== undefined ? props.text : '';
|
|
932
1053
|
const fs = props.fontSize ?? 15;
|
|
933
|
-
let line = isRef
|
|
1054
|
+
let line = loopBound ? pad(`Text(${swiftBoundValue(props.bind, ctx.runtime)})`) : isRef
|
|
934
1055
|
? pad(`Text(${props.ref})`)
|
|
935
1056
|
: props.bindExpr !== undefined
|
|
936
1057
|
? pad(`Text(${String(props.bindExpr).includes('.') ? '""' : props.bindExpr})`)
|
|
@@ -943,7 +1064,7 @@ function emitNodeInner(node, ctx) {
|
|
|
943
1064
|
.map((p) => 'text' in p
|
|
944
1065
|
? swiftStringEscape(p.text)
|
|
945
1066
|
: 'bind' in p
|
|
946
|
-
? currentStateNames.has(p.bind.split('.')[0])
|
|
1067
|
+
? loopBinding(p.bind) ? `\\(${swiftBoundValue(p.bind, ctx.runtime)})` : currentStateNames.has(p.bind.split('.')[0])
|
|
947
1068
|
? `\\(${p.bind})`
|
|
948
1069
|
: '' // R133:无效绑定 → 空串,避免把状态名当文本
|
|
949
1070
|
: false
|
|
@@ -969,16 +1090,20 @@ function emitNodeInner(node, ctx) {
|
|
|
969
1090
|
return `Text("${swiftStringEscape(content)}")`;
|
|
970
1091
|
})());
|
|
971
1092
|
line += `.font(${fontExpr(fs, props.fontWeight, 'normal')})`;
|
|
1093
|
+
if (props.fontFamily)
|
|
1094
|
+
line += `.fontDesign(.${props.fontFamily === 'serif' ? 'serif' : props.fontFamily === 'monospace' ? 'monospaced' : 'default'})`;
|
|
1095
|
+
if (props.letterSpacing !== undefined)
|
|
1096
|
+
line += `.tracking(${props.letterSpacing})`;
|
|
972
1097
|
// M2 :style 动态 → 运行时从 state 读取(fontSize 数值 / color hex 字符串)
|
|
973
1098
|
if (props.fontSizeDynamic !== undefined) {
|
|
974
|
-
line += `.font(.system(size: ${props.fontSizeDynamic}))`;
|
|
1099
|
+
line += `.font(.system(size: ${swiftBoundValue(props.fontSizeDynamic, ctx.runtime)}))`;
|
|
975
1100
|
}
|
|
976
1101
|
// :class 动态色 → when ? activeColor : baseColor(base 由 vue2ir 注入,缺省白)
|
|
977
1102
|
if (props.colorActive) {
|
|
978
|
-
line += `.foregroundColor(${props.colorActive.when} ? ${colorExpr(props.colorActive.color)} : ${colorExpr(props.color ?? '#ffffff')})`;
|
|
1103
|
+
line += `.foregroundColor(${swiftCondExpr(props.colorActive.when, conditionNames(ctx), ctx.runtime)} ? ${colorExpr(props.colorActive.color)} : ${colorExpr(props.color ?? '#ffffff')})`;
|
|
979
1104
|
}
|
|
980
1105
|
else if (props.colorDynamic !== undefined) {
|
|
981
|
-
line += `.foregroundColor(Color(hex: ${props.colorDynamic}))`;
|
|
1106
|
+
line += `.foregroundColor(Color(hex: ${swiftBoundValue(props.colorDynamic, ctx.runtime)}))`;
|
|
982
1107
|
}
|
|
983
1108
|
else if (props.color !== undefined) {
|
|
984
1109
|
line += `.foregroundColor(${colorExpr(props.color)})`;
|
|
@@ -986,8 +1111,9 @@ function emitNodeInner(node, ctx) {
|
|
|
986
1111
|
if (props.textAlign !== undefined && props.textAlign !== 'left') {
|
|
987
1112
|
line += `.multilineTextAlignment(${ALIGN_MAP[props.textAlign]})`;
|
|
988
1113
|
}
|
|
989
|
-
|
|
990
|
-
|
|
1114
|
+
const lineHeight = props.lineHeightMultiplier !== undefined ? fs * props.lineHeightMultiplier : props.lineHeight;
|
|
1115
|
+
if (lineHeight !== undefined && lineHeight > fs) {
|
|
1116
|
+
line += `.lineSpacing(${Number((lineHeight - fs).toFixed(4))})`;
|
|
991
1117
|
}
|
|
992
1118
|
// R-FLEXALIGN:text-align 对单行 Text 无效(multilineTextAlignment 只影响多行行内对齐),
|
|
993
1119
|
// 需通过 frame alignment 生效。flex:1(fillW0)或 fillW 的 frame 默认 .center → 右/左对齐丢失。
|
|
@@ -1030,13 +1156,21 @@ function emitNodeInner(node, ctx) {
|
|
|
1030
1156
|
// R-BTNEMPTY:无事件的 button 也必须带 action 参数(Button(action:) 必填,空 action 会生成 Button() 不可编译)
|
|
1031
1157
|
const action = actionExpr(props, ctx) ?? (ctx.warnings.push(`${ctx.path}: button 无 onSet/onClick,生成空 action(不可交互)`), '{}');
|
|
1032
1158
|
let line = pad(`Button(action: ${action}) {`);
|
|
1033
|
-
|
|
1159
|
+
if (props.icon) {
|
|
1160
|
+
line += `\n${indent(L + 1)}${swiftVector({ vector: props.icon.vector, color: props.color }, colorExpr).split('\n').join('\n' + indent(L + 1))}.frame(width: ${props.icon.width}, height: ${props.icon.height})`;
|
|
1161
|
+
if (label.trim() && label !== '按钮')
|
|
1162
|
+
line += `\n${indent(L + 1)}Text(${props.bind ? swiftBoundValue(label, ctx.runtime) : `"${swiftStringEscape(label)}"`})`;
|
|
1163
|
+
}
|
|
1164
|
+
else
|
|
1165
|
+
line += `\n${indent(L + 1)}Text(${props.bind ? swiftBoundValue(label, ctx.runtime) : `"${swiftStringEscape(label)}"`})`;
|
|
1034
1166
|
line += `.font(${fontExpr(fs, props.fontWeight, 'medium')})`;
|
|
1167
|
+
if (props.fontFamily)
|
|
1168
|
+
line += `.fontDesign(.${props.fontFamily === 'serif' ? 'serif' : props.fontFamily === 'monospace' ? 'monospaced' : 'default'})`;
|
|
1035
1169
|
if (props.fontSizeDynamic !== undefined) {
|
|
1036
|
-
line += `.font(.system(size: ${props.fontSizeDynamic}))`;
|
|
1170
|
+
line += `.font(.system(size: ${swiftBoundValue(props.fontSizeDynamic, ctx.runtime)}))`;
|
|
1037
1171
|
}
|
|
1038
1172
|
if (props.colorDynamic !== undefined) {
|
|
1039
|
-
line += `.foregroundColor(Color(hex: ${props.colorDynamic}))`;
|
|
1173
|
+
line += `.foregroundColor(Color(hex: ${swiftBoundValue(props.colorDynamic, ctx.runtime)}))`;
|
|
1040
1174
|
}
|
|
1041
1175
|
else if (props.color !== undefined) {
|
|
1042
1176
|
line += `.foregroundColor(${colorExpr(props.color)})`;
|
|
@@ -1058,11 +1192,15 @@ function emitNodeInner(node, ctx) {
|
|
|
1058
1192
|
? pad(`TextField("${swiftStringEscape(placeholder)}", text: $${props.value})`)
|
|
1059
1193
|
: pad(`TextField("${swiftStringEscape(placeholder)}", text: .constant(""))`);
|
|
1060
1194
|
line += `.font(${fontExpr(fs, props.fontWeight, 'normal')})`;
|
|
1195
|
+
if (props.fontFamily)
|
|
1196
|
+
line += `.fontDesign(.${props.fontFamily === 'serif' ? 'serif' : props.fontFamily === 'monospace' ? 'monospaced' : 'default'})`;
|
|
1197
|
+
if (props.letterSpacing !== undefined)
|
|
1198
|
+
line += `.tracking(${props.letterSpacing})`;
|
|
1061
1199
|
if (props.fontSizeDynamic !== undefined) {
|
|
1062
|
-
line += `.font(.system(size: ${props.fontSizeDynamic}))`;
|
|
1200
|
+
line += `.font(.system(size: ${swiftBoundValue(props.fontSizeDynamic, ctx.runtime)}))`;
|
|
1063
1201
|
}
|
|
1064
1202
|
if (props.colorDynamic !== undefined) {
|
|
1065
|
-
line += `.foregroundColor(Color(hex: ${props.colorDynamic}))`;
|
|
1203
|
+
line += `.foregroundColor(Color(hex: ${swiftBoundValue(props.colorDynamic, ctx.runtime)}))`;
|
|
1066
1204
|
}
|
|
1067
1205
|
else if (props.color !== undefined) {
|
|
1068
1206
|
line += `.foregroundColor(${colorExpr(props.color)})`;
|
|
@@ -1086,12 +1224,15 @@ function emitNodeInner(node, ctx) {
|
|
|
1086
1224
|
parentAxis: dir,
|
|
1087
1225
|
level: L + 1,
|
|
1088
1226
|
definiteHeight: scrollDefinite,
|
|
1227
|
+
dataProps: ctx.dataProps,
|
|
1228
|
+
stateNames: ctx.stateNames,
|
|
1089
1229
|
runtime: ctx.runtime,
|
|
1090
1230
|
warnings: ctx.warnings,
|
|
1091
1231
|
path: ctx.path,
|
|
1092
1232
|
// 孙级透传源:scroll 自身 gap 优先,其次父级透传 gap
|
|
1093
1233
|
inheritGap: props.gap ?? ctx.inheritGap,
|
|
1094
|
-
loop: ctx.loop,
|
|
1234
|
+
loop: ctx.loop,
|
|
1235
|
+
loopAliases: ctx.loopAliases, // M2:v-for 作用域透传
|
|
1095
1236
|
}, alignRaw, props.alignItems === 'stretch');
|
|
1096
1237
|
const body = pad(`ScrollView(.vertical, showsIndicators: ${props.showsIndicators ?? true}) {`) +
|
|
1097
1238
|
`\n${indent(L + 1)}VStack(alignment: ${FLEX_START_MAP[alignRaw] ?? '.leading'}, spacing: ${spacing}) {` +
|
|
@@ -1101,10 +1242,13 @@ function emitNodeInner(node, ctx) {
|
|
|
1101
1242
|
return body + box.map((m) => ' ' + m).join('');
|
|
1102
1243
|
}
|
|
1103
1244
|
case 'spacer': {
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1245
|
+
const decorated = props.background !== undefined || props.gradient !== undefined || props.radius !== undefined || props.borderWidth !== undefined;
|
|
1246
|
+
if (!decorated && props.flex !== undefined)
|
|
1247
|
+
return pad('Spacer(minLength: 0)') + box.map((m) => ' ' + m).join('');
|
|
1248
|
+
let code = pad('Color.clear');
|
|
1249
|
+
if (props.height === undefined && props.heightDynamic === undefined && props.minHeight === undefined && props.flex === undefined)
|
|
1250
|
+
code += '.frame(height: 0)';
|
|
1251
|
+
return code + box.map((m) => ' ' + m).join('');
|
|
1108
1252
|
}
|
|
1109
1253
|
case 'divider': {
|
|
1110
1254
|
let line = pad('Rectangle()');
|
|
@@ -1113,15 +1257,25 @@ function emitNodeInner(node, ctx) {
|
|
|
1113
1257
|
line += frameExpr('fillW');
|
|
1114
1258
|
return line + box.map((m) => ' ' + m).join('');
|
|
1115
1259
|
}
|
|
1260
|
+
case 'vector':
|
|
1261
|
+
if (props.aspectRatio)
|
|
1262
|
+
box.unshift(`.aspectRatio(${props.aspectRatio}, contentMode: .fit)`);
|
|
1263
|
+
return pad(swiftVector(props, colorExpr, (name) => swiftBoundValue(name, ctx.runtime)).split('\n').join('\n' + indent(L))) + box.map((m) => ' ' + m).join('');
|
|
1116
1264
|
case 'image': {
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1265
|
+
const source = props.src ?? '';
|
|
1266
|
+
let image;
|
|
1267
|
+
if (/^data:image\/[^;]+;base64,/.test(source)) {
|
|
1268
|
+
const base64 = source.slice(source.indexOf(',') + 1);
|
|
1269
|
+
image = `Image(uiImage: UIImage(data: Data(base64Encoded: "${base64}") ?? Data()) ?? UIImage()).resizable()`;
|
|
1270
|
+
}
|
|
1271
|
+
else if (/^https?:\/\//.test(source)) {
|
|
1272
|
+
image = `AsyncImage(url: URL(string: "${swiftStringEscape(source)}")) { image in image.resizable() } placeholder: { Color.clear }`;
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
image = `Image(uiImage: UIImage(named: "${swiftStringEscape(source.replace(/^bundle:\/\//, ''))}") ?? UIImage()).resizable()`;
|
|
1276
|
+
}
|
|
1277
|
+
image += props.objectFit === 'contain' ? '.scaledToFit()' : '.scaledToFill()';
|
|
1278
|
+
return pad(image) + box.map((m) => ' ' + m).join('');
|
|
1125
1279
|
}
|
|
1126
1280
|
case 'component': {
|
|
1127
1281
|
// R41 使用点:<Name>View(prop1: "…", prop2: …) + 位置修饰(flex/width 来自组件根布局 props)
|
|
@@ -1165,6 +1319,7 @@ function emitNodeInner(node, ctx) {
|
|
|
1165
1319
|
warnings: ctx.warnings,
|
|
1166
1320
|
path: `${ctx.path}.for[${i}]`,
|
|
1167
1321
|
loop: { itemKey: vf.itemKey, indexKey: vf.indexKey, indexVar },
|
|
1322
|
+
loopAliases: [...(ctx.loopAliases ?? []), vf.itemKey, ...(vf.indexKey ? [vf.indexKey] : [])],
|
|
1168
1323
|
dataProps: ctx.dataProps, // R-VFOBJ-D
|
|
1169
1324
|
}));
|
|
1170
1325
|
let body;
|
|
@@ -1180,7 +1335,7 @@ function emitNodeInner(node, ctx) {
|
|
|
1180
1335
|
body =
|
|
1181
1336
|
`Group {\n` +
|
|
1182
1337
|
indent(L + 1) +
|
|
1183
|
-
`if let _vrnArr = try? JSONSerialization.jsonObject(with: Data(${vf.listState}.utf8)) as? [Any] {\n` +
|
|
1338
|
+
`if let _vrnArr = try? JSONSerialization.jsonObject(with: Data(${swiftBoundValue(vf.listState, ctx.runtime)}.utf8)) as? [Any] {\n` +
|
|
1184
1339
|
indent(L + 2) +
|
|
1185
1340
|
`ForEach(Array(_vrnArr.enumerated()), id: \\.offset) { _vrnPair in\n` +
|
|
1186
1341
|
indent(L + 3) +
|
|
@@ -1220,6 +1375,8 @@ function emitNodeInner(node, ctx) {
|
|
|
1220
1375
|
/** M7 导航栏与沉浸式:navBar 配置 → SwiftUI 修饰符(参数标准 R66) */
|
|
1221
1376
|
function navBarModifiers(screen, warnings) {
|
|
1222
1377
|
const nav = screen.navBar;
|
|
1378
|
+
if (nav?.hidden)
|
|
1379
|
+
return ' .toolbar(.hidden, for: .navigationBar)';
|
|
1223
1380
|
if (!nav) {
|
|
1224
1381
|
// R66 兜底:页面未声明 navBar 时用 IR title(多页模式 = 路由 meta.title)渲染页面级导航栏,
|
|
1225
1382
|
// 对位 Android 详情页 TopAppBar(标题 + 系统返回箭头)。iOS 26 容器级 navigationTitle 不生效(R68),
|
|
@@ -1293,6 +1450,22 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1293
1450
|
const warnings = opts.warnings ?? [];
|
|
1294
1451
|
// R-PAGESCROLL:根容器始终为可滚动视口(见 emitNode root 分支),无需内容高度启发式
|
|
1295
1452
|
const dataProps = [];
|
|
1453
|
+
sectionNodes = new WeakMap();
|
|
1454
|
+
sectionSources = [];
|
|
1455
|
+
const count = (n) => 1 + (n.children ?? []).reduce((sum, c) => sum + count(c), 0);
|
|
1456
|
+
// 把较大页面的独立顶层段落拆成 ViewBuilder 属性,避免 SwiftUI 泛型表达式超时。
|
|
1457
|
+
if (count(screen.ui) > 6) {
|
|
1458
|
+
let sequence = 0;
|
|
1459
|
+
const split = (node, depth) => {
|
|
1460
|
+
const large = count(node) >= 2 || (node.props?.textRuns?.length ?? 0) > 3 ||
|
|
1461
|
+
(node.props?.vector?.shapes.reduce((sum, s) => sum + s.commands.length, 0) ?? 0) > 12;
|
|
1462
|
+
if (depth >= 2 && large)
|
|
1463
|
+
sectionNodes.set(node, `_vrnSection${sequence++}`);
|
|
1464
|
+
for (const child of node.children ?? [])
|
|
1465
|
+
split(child, depth + 1);
|
|
1466
|
+
};
|
|
1467
|
+
split(screen.ui, 0);
|
|
1468
|
+
}
|
|
1296
1469
|
const body = emitNode(screen.ui, {
|
|
1297
1470
|
stateNames: __stateNames,
|
|
1298
1471
|
parentAxis: 'column',
|
|
@@ -1343,6 +1516,7 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1343
1516
|
: (() => { const nav = navBarModifiers(screen, warnings); return nav && '\n' + nav; })()) +
|
|
1344
1517
|
(!opts.bindingMode && hasLogic ? '\n' + onAppearBlock(screen, rt) : ''),
|
|
1345
1518
|
' }',
|
|
1519
|
+
...sectionSources,
|
|
1346
1520
|
'}',
|
|
1347
1521
|
'',
|
|
1348
1522
|
].join('\n');
|
|
@@ -1351,6 +1525,8 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1351
1525
|
* 页面级 toolbarColorScheme 保证标题浅色(iOS 26 容器级不生效);页面级不再挂 navigationTitle,
|
|
1352
1526
|
* 避免 ZStack 常驻多页面时标题取第一个导致切换不跟随 */
|
|
1353
1527
|
function externalNavBarTitle(screen) {
|
|
1528
|
+
if (screen.navBar?.hidden)
|
|
1529
|
+
return `\n .toolbar(.hidden, for: .navigationBar)`;
|
|
1354
1530
|
return `\n .toolbarColorScheme(.dark, for: .navigationBar)`;
|
|
1355
1531
|
}
|
|
1356
1532
|
/** M3:onAppear 注册 @State ↔ stateStore 绑定 + 触发 onMounted(有 JS 逻辑时追加到 root 链尾)
|
|
@@ -1397,7 +1573,7 @@ function loopFieldExpr(expr, loop) {
|
|
|
1397
1573
|
}
|
|
1398
1574
|
/** M4b dynField 字段取值:元素可能是动态列表解码出的 Any → 统一 as? [String: Any] 取值(静态 [String:String] 字典同样命中) */
|
|
1399
1575
|
function dictFieldExpr(varName, field) {
|
|
1400
|
-
return `(
|
|
1576
|
+
return `vrnItemField(${varName}, "${field}")`;
|
|
1401
1577
|
}
|
|
1402
1578
|
/** 事件 action 体(onSet 状态赋值优先 / onClick JS 运行时):button 用 `action: <体>`,其余节点用 `.onTapGesture <体>` */
|
|
1403
1579
|
function actionExpr(props, ctx) {
|
|
@@ -1426,6 +1602,10 @@ function loopClickAction(onClick, loop, runtime) {
|
|
|
1426
1602
|
if (!loop)
|
|
1427
1603
|
return null;
|
|
1428
1604
|
const itemKey = loop.itemKey;
|
|
1605
|
+
if (!onClick.startsWith('router.push(')) {
|
|
1606
|
+
const expression = /^[A-Za-z_$][\w$]*$/.test(onClick) ? onClick + '()' : onClick;
|
|
1607
|
+
return `_ = ${runtime}.evaluateTemplate(${JSON.stringify(expression)}, [${JSON.stringify(itemKey)}: ${itemKey}])`;
|
|
1608
|
+
}
|
|
1429
1609
|
const re = new RegExp(`${itemKey}\\.([A-Za-z_][\\w$]*)`, 'g');
|
|
1430
1610
|
if (!re.test(onClick))
|
|
1431
1611
|
return null;
|