@dongmingzhixiu/vuexir-compiler 0.6.0 → 0.7.1

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.
@@ -11,6 +11,12 @@
11
11
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs';
12
12
  import { join, dirname, basename } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
+ import { loadProjectAssets, materializeProjectAssets } from './project-assets.js';
15
+ import { extractCssVariables } from '../transform/css.js';
16
+ import { loadProjectStyles } from './project-styles.js';
17
+ import { flattenTextRuns, loopBinding, kotlinBoundValue } from './bindings.js';
18
+ import { pageJsAndroid } from './page-js-android.js';
19
+ import { kotlinVector } from './vector.js';
14
20
  import { vueToIr } from '../transform/vue2ir.js';
15
21
  import { validateIr } from '../ir/schema.js';
16
22
  // R-PKG:本模块的工程生成入口(runTabs)属于 CLI 职责,产物/样例根均可由环境注入,
@@ -116,6 +122,10 @@ import androidx.compose.material3.MaterialTheme
116
122
  import androidx.compose.material3.darkColorScheme
117
123
  import androidx.compose.runtime.Composable
118
124
  import androidx.compose.ui.graphics.Color
125
+ import androidx.compose.ui.graphics.asComposePath
126
+ import androidx.compose.ui.graphics.nativeCanvas
127
+ import androidx.compose.ui.graphics.toArgb
128
+ import androidx.compose.ui.graphics.asImageBitmap
119
129
 
120
130
  // Vuexir 深色鎏金主题(对位 iOS Theme.swift)
121
131
  val Gold = Color(0xFFD9B36C)
@@ -152,6 +162,20 @@ const RUNTIME_KT = (bundleId) => `package ${bundleId}
152
162
  // R85 点路径取值:path 支持 "a.b.c",末段可为 length/size(数组/对象/字符串长度)
153
163
  fun vrnTruthy(v: String): Boolean = v.isNotEmpty() && v != "false" && v != "0" && v != "null" && v != "undefined"
154
164
 
165
+ fun vrnItemField(item: Any?, path: String): String {
166
+ if (item is Map<*, *> && item.containsKey(path)) return item[path]?.toString() ?: ""
167
+ if (item is org.json.JSONObject && item.has(path)) return item.opt(path)?.takeUnless { it == org.json.JSONObject.NULL }?.toString() ?: ""
168
+ var value: Any? = item
169
+ for (part in path.split(".")) value = when (val current = value) {
170
+ is Map<*, *> -> current[part]
171
+ is org.json.JSONObject -> current.opt(part)
172
+ is org.json.JSONArray -> part.toIntOrNull()?.let { current.opt(it) }
173
+ else -> null
174
+ }
175
+ return value?.takeUnless { it == org.json.JSONObject.NULL }?.toString() ?: ""
176
+ }
177
+
178
+
155
179
  fun vrnField(json: String, path: String, fallback: String = ""): String {
156
180
  val raw = vrnFieldRaw(json, path)
157
181
  return if (raw.isEmpty()) fallback else raw
@@ -395,7 +419,7 @@ class NavTest {
395
419
  /** R-THEME-ANDROID:项目 tokens.css 的 var(--x) 表(默认皮肤 warm 优先,后块不覆盖) */
396
420
  let currentCssVars;
397
421
  function loadCssVarsAndroid(root) {
398
- const map = new Map();
422
+ const map = new Map(Object.entries(extractCssVariables(loadProjectStyles(root))));
399
423
  const roots = [join(root, 'src/styles/tokens.css'), join(root, 'src/tokens.css'), join(root, 'styles/tokens.css')];
400
424
  for (const p of roots) {
401
425
  if (!existsSync(p))
@@ -416,6 +440,8 @@ function loadCssVarsAndroid(root) {
416
440
  */
417
441
  function colorExpr(c) {
418
442
  const raw = c.trim();
443
+ if (raw.toLowerCase() === 'transparent')
444
+ return 'Color.Transparent';
419
445
  if (raw.startsWith('var(')) {
420
446
  const m = raw.match(/^var\(\s*(--[\w-]+)/);
421
447
  if (m && currentCssVars) {
@@ -456,6 +482,8 @@ function kotlinStr(s) {
456
482
  /** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
457
483
  /** R114:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
458
484
  function safeBind(name, ctx, loop) {
485
+ if (loopBinding(name))
486
+ return kotlinBoundValue(name);
459
487
  const base = name.split('.')[0];
460
488
  const isState = (ctx.states ?? []).some((st) => st.name === base);
461
489
  const isLoop = loop && (loop.itemKey === base || loop.indexKey === base);
@@ -466,17 +494,23 @@ function safeBind(name, ctx, loop) {
466
494
  return '""';
467
495
  }
468
496
  function kotlinCondExpr(expr, ctx, loop) {
497
+ const binding = expr.startsWith('__vrnCond:') ? expr.slice(10) : expr;
498
+ if (loopBinding(binding))
499
+ return `vrnTruthy(${kotlinBoundValue(binding)})`;
469
500
  // R-ELSE-AND:v-else 下沉互补状态(_vrnE#)在组件内不可求值 → true(R15 恒显语义,对位 iOS;否则未定义标识符编译失败)
470
- if (/^_vrnE\d+$/.test(expr.trim()))
501
+ if (/^_vrnE\d+$/.test(expr.trim()) && !ctx?.states?.some((s) => s.name === expr.trim()))
471
502
  return 'true';
472
503
  // R128:下沉的条件状态 → 判真
473
504
  const sunk = expr.match(/^__vrnCond:(\w+)$/);
474
505
  if (sunk) {
475
506
  // v-else 互补状态下沉为 _vrnE# → 组件内不可求值,恒显(R15,对位 iOS)
476
- if (/^_vrnE\d+$/.test(sunk[1]))
507
+ if (/^_vrnE\d+$/.test(sunk[1]) && !ctx?.states?.some((s) => s.name === sunk[1]))
477
508
  return 'true';
478
509
  return `vrnTruthy(${sunk[1]}.toString())`;
479
510
  }
511
+ const field = expr.match(/^__vrnField__:(\w+):(\w+)$/);
512
+ if (field)
513
+ return `vrnTruthy(vrnField(${field[1]}, "${field[2]}"))`;
480
514
  // R114:条件里引用了原生端不存在的标识符 → 回退为 true(保留内容)并告警,避免整页编译失败
481
515
  {
482
516
  // 先剥掉字符串字面量,避免把 "success" 之类当成标识符
@@ -549,7 +583,7 @@ function kotlinStringExpr(expr) {
549
583
  function alphaModifier(p) {
550
584
  const parts = [];
551
585
  if (p.show !== undefined)
552
- parts.push(`if (${p.show}) 1f else 0f`);
586
+ parts.push(`if (vrnTruthy(${p.show}.toString())) 1f else 0f`);
553
587
  if (p.opacity !== undefined)
554
588
  parts.push(`${p.opacity}f`);
555
589
  if (parts.length === 0)
@@ -596,13 +630,25 @@ function toPascal(id) {
596
630
  function compKotlinName(name) {
597
631
  return `${name.replace(/\s/g, '')}View`;
598
632
  }
599
- function sizeModifiers(props) {
633
+ function sizeModifiers(props, ctx) {
600
634
  const mods = [];
635
+ if (props.aspectRatio)
636
+ mods.push(`Modifier.aspectRatio(${props.aspectRatio}f)`);
637
+ if (props.position === 'absolute' && props.absTop === 0 && props.absLeft === 0 && props.absRight === 0 && props.absBottom === 0)
638
+ mods.push('Modifier.matchParentSize()');
639
+ if (props.widthFraction !== undefined)
640
+ mods.push(`Modifier.fillMaxWidth(${props.widthFraction}f)`);
641
+ if (props.widthFractionDynamic)
642
+ mods.push(`Modifier.fillMaxWidth((${kotlinBoundValue(props.widthFractionDynamic)}.toString().toFloatOrNull() ?: 0f).coerceIn(0f,1f))`);
643
+ if (props.widthDynamic)
644
+ mods.push(`Modifier.width((${kotlinBoundValue(props.widthDynamic)}.toString().toFloatOrNull() ?: 0f).coerceAtLeast(0f).dp)`);
645
+ if (props.heightDynamic)
646
+ mods.push(`Modifier.height((${kotlinBoundValue(props.heightDynamic)}.toString().toFloatOrNull() ?: 0f).coerceAtLeast(0f).dp)`);
601
647
  if (props.width === 'fill')
602
648
  mods.push('Modifier.fillMaxWidth()');
603
649
  else if (typeof props.width === 'number')
604
650
  mods.push(`Modifier.width(${dp(props.width)})`);
605
- if (props.height === 'fill')
651
+ if (props.height === 'fill' && !(ctx?.inVerticalScroll && ctx.parentAxis === 'column' && !props.vector))
606
652
  mods.push('Modifier.fillMaxHeight()');
607
653
  else if (typeof props.height === 'number')
608
654
  mods.push(`Modifier.height(${dp(props.height)})`);
@@ -627,6 +673,24 @@ function paddingModifiers(props) {
627
673
  }
628
674
  function visualModifiers(props) {
629
675
  const mods = [];
676
+ if (props.zIndex !== undefined)
677
+ mods.push(`Modifier.zIndex(${props.zIndex}f)`);
678
+ if (props.minWidth !== undefined || props.minHeight !== undefined) {
679
+ const args = [props.minWidth !== undefined ? `minWidth = ${dp(props.minWidth)}` : '', props.minHeight !== undefined ? `minHeight = ${dp(props.minHeight)}` : ''].filter(Boolean);
680
+ mods.push(`Modifier.sizeIn(${args.join(', ')})`);
681
+ }
682
+ if (props.shadow)
683
+ mods.push(`Modifier.shadow(${dp(props.shadow.blur / 2)}, RoundedCornerShape(${dp(props.radius ?? 0)}), clip = false, ambientColor = ${colorExpr(props.shadow.color)}, spotColor = ${colorExpr(props.shadow.color)})`);
684
+ if (props.gradient) {
685
+ const a = props.gradient.angle * Math.PI / 180;
686
+ const x = Math.sin(a) / 2, y = -Math.cos(a) / 2;
687
+ const n = (v) => `${Number(v.toFixed(5))}f`;
688
+ const stops = props.gradient.stops.map((s) => `${s.offset}f to ${colorExpr(s.color === 'transparent' ? 'rgba(0,0,0,0)' : s.color)}`).join(', ');
689
+ if (props.gradient.kind === 'radial')
690
+ mods.push(`Modifier.drawBehind { drawRect(brush = androidx.compose.ui.graphics.Brush.radialGradient(${stops}, center = Offset(size.width * ${props.gradient.center?.x ?? .5}f,size.height * ${props.gradient.center?.y ?? .5}f), radius = maxOf(size.width,size.height))) }`);
691
+ else
692
+ mods.push(`Modifier.drawBehind { drawRect(brush = androidx.compose.ui.graphics.Brush.linearGradient(${stops}, start = Offset(size.width * ${n(.5 - x)}, size.height * ${n(.5 - y)}), end = Offset(size.width * ${n(.5 + x)}, size.height * ${n(.5 + y)}))) }`);
693
+ }
630
694
  if (props.background !== undefined)
631
695
  mods.push(`Modifier.background(${colorExpr(props.background)})`);
632
696
  return mods;
@@ -676,13 +740,15 @@ function borderModifiers(props) {
676
740
  }
677
741
  return mods;
678
742
  }
679
- function flexModifier(props) {
743
+ function flexModifier(props, ctx) {
744
+ if (ctx?.parentAxis === 'box' || (ctx?.inVerticalScroll && ctx.parentAxis !== 'row'))
745
+ return '';
746
+ if (props.flex === undefined && props.minWidth === 0 && ctx?.parentAxis === 'row')
747
+ return 'Modifier.weight(1f, fill = false)';
680
748
  return props.flex !== undefined ? `Modifier.weight(${props.flex}f)` : '';
681
749
  }
682
750
  /** 容器选择(对位 iOS VStack/HStack/ZStack) */
683
751
  function containerKind(props) {
684
- if (props.position === 'absolute')
685
- return 'Box';
686
752
  return props.flexDirection === 'row' ? 'Row' : 'Column';
687
753
  }
688
754
  /** 排列参数(按容器方向:Column 用 verticalArrangement/horizontalAlignment;Row 用 horizontalArrangement/verticalAlignment) */
@@ -785,6 +851,12 @@ function inlineHandlerKotlin(fn) {
785
851
  }
786
852
  /** 点击事件 → Kotlin 表达式(A5:onSet 内联赋值;onClick 模式识别:自增减/路由 push/无参 handler 内联/其他 JS TODO) */
787
853
  function kotlinClickAction(p, ctx, loop) {
854
+ if (ctx.runtimeAsset && p.onClick) {
855
+ const expression = /^[A-Za-z_$][\w$]*$/.test(p.onClick) ? p.onClick + '()' : p.onClick;
856
+ if (loop)
857
+ return 'vrnPageJs.template(' + kotlinStr(expression) + ', mapOf(' + kotlinStr(loop.itemKey) + ' to ' + loop.itemKey + '))';
858
+ return 'vrnPageJs.eval(' + kotlinStr(expression) + ')';
859
+ }
788
860
  if (p.onSet) {
789
861
  const v = p.onSet.value;
790
862
  const rhs = p.onSet.kind === 'expr' ? String(v) : typeof v === 'string' ? kotlinStr(v) : String(v);
@@ -1057,9 +1129,26 @@ function stateDeclarations(ctx, pad) {
1057
1129
  })
1058
1130
  .join('\n');
1059
1131
  }
1132
+ let composeSections = [];
1133
+ let composingSection = new WeakSet();
1134
+ const composeNodeCount = (n) => 1 + (n.children ?? []).reduce((sum, c) => sum + composeNodeCount(c), 0);
1060
1135
  /** 纯字符串拼接生成节点(避免嵌套模板转译问题);v-if(node.condition)在此统一包裹 */
1061
1136
  function emitNode(node, ctx, depth, loop, useParamModifier = false, parentStretch = false) {
1062
1137
  const pad = ' '.repeat(depth);
1138
+ if (node.type === 'root') {
1139
+ composeSections = [];
1140
+ composingSection = new WeakSet();
1141
+ }
1142
+ if (ctx.runtimeAsset && depth >= 3 && !loop && node.type !== 'for' && composeNodeCount(node) >= 10 && !composingSection.has(node)) {
1143
+ composingSection.add(node);
1144
+ const source = emitNode(node, ctx, depth, loop, useParamModifier, parentStretch);
1145
+ const name = '_vrnSection' + composeSections.length;
1146
+ const receiver = ctx.parentAxis === 'box' ? 'BoxScope' : ctx.parentAxis === 'row' ? 'RowScope' : 'ColumnScope';
1147
+ composeSections.push('val ' + name + ': @Composable ' + receiver + '.() -> Unit = {\n' + source + '\n}');
1148
+ return pad + name + '()';
1149
+ }
1150
+ if (node.props?.show)
1151
+ return pad + 'if (' + kotlinCondExpr(node.props.show, ctx, loop) + ') {\n' + emitNode({ ...node, props: { ...node.props, show: undefined } }, ctx, depth + 1, loop, useParamModifier, parentStretch) + '\n' + pad + '}';
1063
1152
  // v-if → Kotlin if 包裹(表达式为双端兼容子集 ==/!=/&&/||/!,=== / !== 由 kotlinCondExpr 归一)
1064
1153
  if (node.condition && node.type !== 'root') {
1065
1154
  return (pad + 'if (' + kotlinCondExpr(node.condition, ctx, loop) + ') {\n' +
@@ -1070,7 +1159,7 @@ function emitNode(node, ctx, depth, loop, useParamModifier = false, parentStretc
1070
1159
  }
1071
1160
  /** CSS 默认 align-items:stretch:column 父容器内未声明宽度/弹性/绝对定位的子项拉满父宽(R29 盒模型,对位 iOS maxWidth 无穷) */
1072
1161
  function stretchModifiers(parentStretch, node) {
1073
- if (!parentStretch || node.type === 'spacer' || node.type === 'divider')
1162
+ if (!parentStretch || (node.props?.alignSelf !== undefined && node.props.alignSelf !== 'stretch') || node.type === 'spacer' || node.type === 'divider')
1074
1163
  return [];
1075
1164
  return ['Modifier.fillMaxWidth()'];
1076
1165
  }
@@ -1085,7 +1174,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1085
1174
  const pad = ' '.repeat(depth);
1086
1175
  const p = (node.props ?? {});
1087
1176
  /** R-COMP-ROOT-ABS:组件根(useParamModifier && depth===1)去掉 Modifier.align(顶层无 BoxScope,编译非法);保留 offset 平移 */
1088
- const absM = (pp) => (useParamModifier && depth <= 2 ? absModifiers(pp).filter((m) => !m.includes('.align(')) : absModifiers(pp));
1177
+ const absM = (pp) => ((ctx.parentAxis !== 'box' || (useParamModifier && depth <= 2)) ? absModifiers(pp).filter((m) => !m.includes('.align(')) : absModifiers(pp));
1089
1178
  const alphaMod = alphaModifier(p);
1090
1179
  const alphaArr = alphaMod ? [alphaMod] : [];
1091
1180
  /** 容器类型:存在 absolute 子节点 → Box(ZStack 语义,对位 iOS);否则按 flexDirection */
@@ -1099,7 +1188,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1099
1188
  const groupBackground = (nd, props) => {
1100
1189
  const activeBg = nd.active ? nd.active.props?.background : undefined;
1101
1190
  if (typeof activeBg === 'string' && nd.active) {
1102
- const when = kotlinCondExpr(nd.active.when);
1191
+ const when = kotlinCondExpr(nd.active.when, ctx, loop);
1103
1192
  const active = colorExpr(activeBg);
1104
1193
  // 无基础背景时回退透明(对位 iOS `.background(when ? color : Color.clear)`)——
1105
1194
  // 否则选中态会变成「所有行都高亮」(此前 bug)
@@ -1113,6 +1202,12 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1113
1202
  const childrenBody = (nd, d, kind) => {
1114
1203
  if (!nd.children)
1115
1204
  return '';
1205
+ if (kind === 'Box' && nd.children.some((c) => c.props?.position === 'absolute') && nd.children.some((c) => c.props?.position !== 'absolute')) {
1206
+ const flow = nd.children.filter((c) => c.props?.position !== 'absolute');
1207
+ const first = nd.children.findIndex((c) => c.props?.position !== 'absolute');
1208
+ const decorated = nd.children.flatMap((c, i) => c.props?.position === 'absolute' ? [c] : i === first ? [{ type: 'group', props: { flexDirection: p.flexDirection, alignItems: p.alignItems, justifyContent: p.justifyContent, gap: p.gap, padding: p.padding, ...((p.width !== undefined || p.widthDynamic) && (p.height !== undefined || p.heightDynamic) ? { width: 'fill', height: 'fill' } : {}) }, children: flow }] : []);
1209
+ return decorated.map((child) => emitNode(child, { ...ctx, parentAxis: 'box' }, d, loop)).join('\n');
1210
+ }
1116
1211
  return nd.children
1117
1212
  .map((c) => {
1118
1213
  const cp = (c.props ?? {});
@@ -1120,10 +1215,10 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1120
1215
  let pre = '';
1121
1216
  let post = '';
1122
1217
  if (kind === 'Column') {
1123
- if (cp.marginTopAuto) {
1218
+ if (cp.marginTopAuto && !ctx.inVerticalScroll) {
1124
1219
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.weight(1f),\n' + cpad + ')\n';
1125
1220
  }
1126
- if (cp.marginTop !== undefined) {
1221
+ if (cp.marginTop !== undefined && cp.marginTop !== 0) {
1127
1222
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.height(' + dp(cp.marginTop) + '),\n' + cpad + ')\n';
1128
1223
  }
1129
1224
  }
@@ -1131,36 +1226,39 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1131
1226
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.weight(1f),\n' + cpad + ')\n';
1132
1227
  }
1133
1228
  // R70 其余单边外边距(前置/后置固定间隔,对位 iOS Color.clear 间隔)
1134
- if (kind === 'Row' && cp.marginLeft !== undefined) {
1229
+ if (kind === 'Row' && cp.marginLeft !== undefined && cp.marginLeft !== 0) {
1135
1230
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.width(' + dp(cp.marginLeft) + '),\n' + cpad + ')\n';
1136
1231
  }
1137
- if (kind === 'Row' && cp.marginRight !== undefined) {
1232
+ if (kind === 'Row' && cp.marginRight !== undefined && cp.marginRight !== 0) {
1138
1233
  post = post + '\n' + cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.width(' + dp(cp.marginRight) + '),\n' + cpad + ')\n';
1139
1234
  }
1140
- if (kind === 'Column' && cp.marginBottom !== undefined) {
1235
+ if (kind === 'Column' && cp.marginBottom !== undefined && cp.marginBottom !== 0) {
1141
1236
  post = post + '\n' + cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.height(' + dp(cp.marginBottom) + '),\n' + cpad + ')\n';
1142
1237
  }
1143
- const stretch = kind === 'Column' && cp.width === undefined && cp.flex === undefined && cp.position !== 'absolute' && !cp.marginLeftAuto;
1144
- return pre + emitNode(c, ctx, d, undefined, false, stretch) + post;
1238
+ const stretch = kind === 'Column' && (p.alignItems === undefined || p.alignItems === 'stretch') && !(ctx.parentAxis === 'row' && p.width === undefined && p.flex === undefined) && cp.width === undefined && cp.flex === undefined && cp.position !== 'absolute' && !cp.marginLeftAuto;
1239
+ const childCtx = { ...ctx, parentAxis: kind === 'Row' ? 'row' : kind === 'Box' ? 'box' : 'column' };
1240
+ const result = emitNode(c, childCtx, d, loop, false, stretch);
1241
+ if (childCtx.needsScope)
1242
+ ctx.needsScope = true;
1243
+ return pre + result + post;
1145
1244
  })
1146
1245
  .join('\n');
1147
1246
  };
1148
1247
  switch (node.type) {
1149
1248
  case 'root': {
1150
1249
  // 单层容器:设计稿固定高度(640)在真机上会导致内容被限高挤压 → 移除 height,fillMaxSize 铺满(对位 iOS 背景铺满)
1151
- const size = sizeModifiers(p).filter((m) => !m.includes('.height('));
1250
+ const size = sizeModifiers(p, ctx).filter((m) => !m.includes('.height('));
1152
1251
  // R-PAGESCROLL:页面根 = 可滚动视口(对位 Web body 默认滚动 / 对位 iOS root ScrollView)。
1153
1252
  // 守卫:子树存在 flex/marginTopAuto(→ Modifier.weight)时不加滚动——滚动容器沿主轴给无界高度约束,
1154
1253
  // weight 在其中会抛异常(Vertically scrollable component was measured with infinity constraints)
1155
- const weighty = subtreeHasWeight(node);
1156
- if (weighty) {
1157
- ctx.warnings.push('页面含 flex/marginTopAuto(weight 布局)→ 根容器不启用滚动(滚动容器内 weight 会因高度无界崩溃);内容超高时请把长内容放进 overflow:auto 容器');
1158
- }
1159
- const scrollMods = weighty ? [] : ['Modifier.verticalScroll(rememberScrollState())'];
1254
+ const containsScroll = (n) => n.type === 'scroll' || (n.children ?? []).some(containsScroll);
1255
+ const outerScroll = !(node.children ?? []).some(containsScroll);
1256
+ const scrollMods = outerScroll ? ['Modifier.verticalScroll(rememberScrollState())'] : [];
1257
+ ctx.inVerticalScroll = outerScroll;
1160
1258
  const chain = modChain([...size, ...scrollMods, ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p)], pad + ' ');
1161
1259
  const kind = kindFor(node, p);
1162
1260
  const args = arrangementExpr(p, kind);
1163
- let out = pad + '@Composable\n';
1261
+ let out = pad + '@OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)\n' + pad + '@Composable\n';
1164
1262
  out += pad + 'fun ' + toPascal(ctx.pageId) + 'Content(\n';
1165
1263
  out += pad + ' modifier: Modifier = Modifier\n' + pad + ') {\n';
1166
1264
  const states = stateDeclarations(ctx, pad);
@@ -1170,6 +1268,16 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1170
1268
  const children = childrenBody(node, depth + 1, kind);
1171
1269
  // R72 异步:本页含 async handler / async onMounted → 声明协程作用域(按钮 onClick 用 vrnScope.launch)
1172
1270
  const pre = [];
1271
+ if (ctx.runtimeAsset) {
1272
+ pre.push(pad + ' val vrnPageContext = androidx.compose.ui.platform.LocalContext.current');
1273
+ pre.push(pad + ' val vrnPageJs = remember { VrnPageJs(vrnPageContext, ' + kotlinStr(ctx.runtimeAsset) + ') }');
1274
+ pre.push(pad + ' DisposableEffect(vrnPageJs) { onDispose { vrnPageJs.close() } }');
1275
+ const updates = (ctx.states ?? []).map((st) => {
1276
+ const get = st.type === 'string' ? `updates.optString("${st.name}")` : st.type === 'boolean' ? `updates.optBoolean("${st.name}")` : `updates.optInt("${st.name}")`;
1277
+ return pad + ` if (updates.has("${st.name}")) ${st.name} = ${get}`;
1278
+ }).join('\n');
1279
+ pre.push(pad + ' LaunchedEffect(vrnPageJs) {\n' + pad + ' while (true) {\n' + pad + ' vrnPageJs.eval("__vrnNativeTick()")\n' + pad + ' val updates = vrnPageJs.drain()\n' + updates + '\n' + pad + ' if (updates.has("__nav")) Runtime.push(updates.optString("__nav"))\n' + pad + ' if (updates.optBoolean("__navBack", false)) Runtime.back()\n' + pad + ' delay(50)\n' + pad + ' }\n' + pad + ' }');
1280
+ }
1173
1281
  const asyncOm = ctx.onMountedAsync && ctx.onMountedBody ? asyncHandlerKotlin({ name: '__onMounted', params: '', body: ctx.onMountedBody }, ctx) : null;
1174
1282
  if (ctx.onMountedAsync && ctx.onMountedBody && asyncOm === null) {
1175
1283
  ctx.warnings.push('async onMounted 含超出协程子集的形状(R72 子集:await fetch / JSON.parse / 状态赋值 / setTimeout),已跳过生成');
@@ -1179,7 +1287,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1179
1287
  if (asyncOm !== null) {
1180
1288
  pre.push(pad + ' LaunchedEffect(Unit) {\n' + asyncOm.split('\n').map((l) => pad + ' ' + l).join('\n') + '\n' + pad + ' }');
1181
1289
  }
1182
- const decls = [states, ...pre].filter((x) => x && x.length > 0).join('\n');
1290
+ const decls = [states, ...pre, ...composeSections].filter((x) => x && x.length > 0).join('\n');
1183
1291
  out += pad + ' ) {' + (decls ? '\n' + decls : '') + '\n' + children + '\n' + pad + ' }\n';
1184
1292
  out += pad + '}';
1185
1293
  return out;
@@ -1189,16 +1297,34 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1189
1297
  const gm = depth === 1 && useParamModifier ? 'modifier' : 'Modifier';
1190
1298
  const chain = modChain([
1191
1299
  ...stretchModifiers(parentStretch, node),
1192
- ...sizeModifiers(p),
1300
+ ...sizeModifiers(p, ctx),
1193
1301
  ...absM(p),
1194
1302
  ...clipModifiers(p),
1195
1303
  ...groupBackground(node, p),
1196
1304
  ...borderModifiers(p),
1197
- ...paddingModifiers(p),
1198
- flexModifier(p),
1305
+ ...(node.children?.some((c) => c.props?.position === 'absolute') ? [] : paddingModifiers(p)),
1306
+ flexModifier(p, ctx),
1199
1307
  ...alphaArr,
1200
1308
  ...transformModifiers(p),
1201
1309
  ].concat(click ? [gm + '.clickable { ' + click + ' }'] : []), pad + ' ');
1310
+ if (p.gridColumns && node.children?.length) {
1311
+ const columns = p.gridColumns, gap = dp(p.gap ?? 0);
1312
+ if (node.children.length === 1 && node.children[0].type === 'for') {
1313
+ const list = node.children[0];
1314
+ const cells = { ...list, children: list.children?.map((child) => ({ ...child, props: { ...child.props, flex: 1, height: child.props?.height === 'fill' ? undefined : child.props?.height, minHeight: child.props?.minHeight ?? p.minHeight } })) };
1315
+ return pad + 'FlowRow(modifier = Modifier' + chain + ',maxItemsInEachRow = ' + columns + ',horizontalArrangement = Arrangement.spacedBy(' + gap + '),verticalArrangement = Arrangement.spacedBy(' + gap + ')) {\n' + emitNode(cells, { ...ctx, parentAxis: 'row' }, depth + 1, loop) + '\n' + pad + '}';
1316
+ }
1317
+ const rows = [];
1318
+ for (let i = 0; i < node.children.length; i += columns) {
1319
+ const cells = node.children.slice(i, i + columns).map((c) => pad + ' Box(modifier = Modifier.weight(1f)) {\n' + emitNode(c, { ...ctx, parentAxis: 'box' }, depth + 3, loop, false, true) + '\n' + pad + ' }').join('\n');
1320
+ rows.push(pad + ' Row(horizontalArrangement = Arrangement.spacedBy(' + gap + ')) {\n' + cells + '\n' + pad + ' }');
1321
+ }
1322
+ return pad + 'Column(modifier = Modifier' + chain + ', verticalArrangement = Arrangement.spacedBy(' + gap + ')) {\n' + rows.join('\n') + '\n' + pad + '}';
1323
+ }
1324
+ if (p.flexWrap) {
1325
+ const cells = (node.children ?? []).map((child) => emitNode(child, { ...ctx, parentAxis: 'row' }, depth + 1, loop)).join('\n');
1326
+ return pad + 'FlowRow(modifier = Modifier' + chain + ', horizontalArrangement = Arrangement.spacedBy(' + dp(p.gap ?? 0) + '), verticalArrangement = Arrangement.spacedBy(' + dp(p.gap ?? 0) + ')) {\n' + cells + '\n' + pad + '}';
1327
+ }
1202
1328
  const kind = kindFor(node, p);
1203
1329
  const args = arrangementExpr(p, kind);
1204
1330
  let out = pad + kind + '(\n';
@@ -1209,7 +1335,25 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1209
1335
  case 'text': {
1210
1336
  // 文本表达式(M2/M4b 动态:bindParts 混合段 / bindExpr 三元 / dynField 循环字段 / bind state / ref 组件参数 / 静态 text)
1211
1337
  let textExpr;
1212
- if (p.bindParts) {
1338
+ if (p.textRuns) {
1339
+ const runs = flattenTextRuns(p.textRuns).map((run) => {
1340
+ const child = emitNode({ type: 'text', props: { ...run } }, ctx, 0, loop);
1341
+ const expr = child.match(/text = ([\s\S]*?),\n/)?.[1] ?? '""';
1342
+ const style = [];
1343
+ const fs = run.fontSize ?? p.fontSize;
1344
+ if (fs !== undefined)
1345
+ style.push(`fontSize = ${sp(fs)}`);
1346
+ const col = run.color ?? p.color;
1347
+ if (col)
1348
+ style.push(`color = ${colorExpr(col)}`);
1349
+ const weight = run.fontWeight ?? p.fontWeight;
1350
+ if (weight)
1351
+ style.push('fontWeight = FontWeight.' + (weight === 'bold' ? 'Bold' : weight === 'medium' ? 'Medium' : 'Normal'));
1352
+ return `withStyle(androidx.compose.ui.text.SpanStyle(${style.join(', ')})) { append(${expr}) }`;
1353
+ });
1354
+ textExpr = 'androidx.compose.ui.text.buildAnnotatedString { ' + runs.join('; ') + ' }';
1355
+ }
1356
+ else if (p.bindParts) {
1213
1357
  const segs = p.bindParts
1214
1358
  .map((seg) => {
1215
1359
  if ('text' in seg)
@@ -1224,7 +1368,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1224
1368
  '"' +
1225
1369
  (seg.fieldOf.fallback !== undefined ? ', "' + seg.fieldOf.fallback.replace(/"/g, '\\"') + '"' : '') +
1226
1370
  ')}');
1227
- return '${' + loopField(seg.dynField.var, loop) + '["' + seg.dynField.field + '"] ?: ""}';
1371
+ return '${vrnItemField(' + seg.dynField.var + ', \"' + seg.dynField.field + '\")}';
1228
1372
  })
1229
1373
  .join('');
1230
1374
  textExpr = '"' + segs + '"';
@@ -1240,7 +1384,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1240
1384
  ')}"';
1241
1385
  }
1242
1386
  else if (p.dynField) {
1243
- textExpr = '"${' + loopField(p.dynField.var, loop) + '["' + p.dynField.field + '"] ?: ""}"';
1387
+ textExpr = '"${vrnItemField(' + p.dynField.var + ', \"' + p.dynField.field + '\")}"';
1244
1388
  }
1245
1389
  else if (p.bindExpr !== undefined) {
1246
1390
  textExpr = kotlinStringExpr(p.bindExpr);
@@ -1256,18 +1400,23 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1256
1400
  }
1257
1401
  // R-TEXTCLICK:@click 挂在 text 上(<span @click>)→ clickable(此前静默丢失交互)
1258
1402
  const click = kotlinClickAction(p, ctx, loop);
1259
- const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), flexModifier(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)].concat(click ? ['Modifier.clickable { ' + click + ' }'] : []), pad + ' ');
1403
+ const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p, ctx), flexModifier(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)].concat(click ? ['Modifier.clickable { ' + click + ' }'] : []), pad + ' ');
1260
1404
  const style = [];
1405
+ if (p.fontFamily)
1406
+ style.push('fontFamily = androidx.compose.ui.text.font.FontFamily.' + (p.fontFamily === 'serif' ? 'Serif' : p.fontFamily === 'monospace' ? 'Monospace' : 'SansSerif'));
1407
+ if (p.letterSpacing !== undefined)
1408
+ style.push('letterSpacing = ' + sp(p.letterSpacing));
1261
1409
  if (p.fontSize !== undefined)
1262
1410
  style.push('fontSize = ' + sp(p.fontSize));
1263
1411
  else if (p.fontSizeDynamic !== undefined)
1264
1412
  style.push('fontSize = ' + p.fontSizeDynamic + '.sp');
1265
1413
  if (p.fontWeight !== undefined)
1266
1414
  style.push('fontWeight = FontWeight.' + (p.fontWeight === 'bold' ? 'Bold' : p.fontWeight === 'medium' ? 'Medium' : 'Normal'));
1267
- if (p.lineHeight !== undefined)
1268
- style.push('lineHeight = ' + sp(p.lineHeight));
1415
+ const lineHeight = p.lineHeightMultiplier !== undefined ? (p.fontSize ?? 15) * p.lineHeightMultiplier : p.lineHeight;
1416
+ if (lineHeight !== undefined)
1417
+ style.push('lineHeight = ' + sp(Number(lineHeight.toFixed(4))));
1269
1418
  if (p.colorActive) {
1270
- style.push('color = if (' + kotlinCondExpr(p.colorActive.when) + ') ' + colorExpr(p.colorActive.color) + ' else ' + colorExpr(p.color ?? '#ffffff'));
1419
+ style.push('color = if (' + kotlinCondExpr(p.colorActive.when, ctx, loop) + ') ' + colorExpr(p.colorActive.color) + ' else ' + colorExpr(p.color ?? '#ffffff'));
1271
1420
  }
1272
1421
  else if (p.colorDynamic !== undefined) {
1273
1422
  style.push('color = dynamicColor(' + p.colorDynamic + ')');
@@ -1287,11 +1436,19 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1287
1436
  return out;
1288
1437
  }
1289
1438
  case 'button': {
1290
- const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1439
+ const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1291
1440
  // R108:动态按钮文案(props.bind)优先;Kotlin 里用字符串模板插值
1292
1441
  const label = p.bind !== undefined ? String(p.bind) : (p.label ?? '');
1293
1442
  const click = kotlinClickAction(p, ctx, loop);
1443
+ if (p.icon) {
1444
+ const icon = kotlinVector({ vector: p.icon.vector, color: p.color }, colorExpr, `Modifier.size(${dp(p.icon.width)}, ${dp(p.icon.height)})`);
1445
+ return pad + 'Box(modifier = Modifier' + chain + (click ? '.clickable(role = androidx.compose.ui.semantics.Role.Button) { ' + click + ' }' : '') + ', contentAlignment = Alignment.Center) {\n' + pad + ' ' + icon.split('\n').join('\n' + pad + ' ') + '\n' + pad + '}';
1446
+ }
1294
1447
  const labelStyle = [];
1448
+ if (p.fontFamily)
1449
+ labelStyle.push('fontFamily = androidx.compose.ui.text.font.FontFamily.' + (p.fontFamily === 'serif' ? 'Serif' : p.fontFamily === 'monospace' ? 'Monospace' : 'SansSerif'));
1450
+ if (p.fontWeight)
1451
+ labelStyle.push('fontWeight = FontWeight.' + (p.fontWeight === 'bold' ? 'Bold' : p.fontWeight === 'medium' ? 'Medium' : 'Normal'));
1295
1452
  if (p.fontSize !== undefined)
1296
1453
  labelStyle.push('fontSize = ' + sp(p.fontSize));
1297
1454
  else if (p.fontSizeDynamic !== undefined)
@@ -1300,13 +1457,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1300
1457
  labelStyle.push('color = ' + colorExpr(p.color));
1301
1458
  else if (p.colorDynamic !== undefined)
1302
1459
  labelStyle.push('color = dynamicColor(' + p.colorDynamic + ')');
1303
- let out = pad + 'Button(\n';
1304
- out += pad + ' onClick = { ' + click + ' },\n';
1305
- if (p.disabled === true)
1306
- out += pad + ' enabled = false,\n';
1307
- if (chain)
1308
- out += pad + ' modifier = Modifier' + chain + ',\n';
1309
- out += pad + ') {\n' + pad + ' Text(' + kotlinStr(label) + (labelStyle.length ? ', ' + labelStyle.join(', ') : '') + ')\n' + pad + '}';
1460
+ let out = pad + 'Box(modifier = Modifier' + chain + (click && p.disabled !== true ? '.clickable(role = androidx.compose.ui.semantics.Role.Button) { ' + click + ' }' : '') + ', contentAlignment = Alignment.Center) {\n';
1461
+ out += pad + ' Text(' + (p.bind ? kotlinBoundValue(label) : kotlinStr(label)) + (labelStyle.length ? ', ' + labelStyle.join(', ') : '') + ')\n' + pad + '}';
1310
1462
  return out;
1311
1463
  }
1312
1464
  case 'input': {
@@ -1317,7 +1469,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1317
1469
  ctx.warnings.push('input: v-model「' + String(st) + '」非页面级 string 状态(Android 暂只支持该形态),已生成占位注释');
1318
1470
  return pad + '// input: v-model ' + kotlinStr(String(st)) + ' 未支持(Android 仅页面级 string state)';
1319
1471
  }
1320
- const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absM(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1472
+ const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p, ctx), ...absM(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1321
1473
  const ts = [];
1322
1474
  if (p.fontSize !== undefined)
1323
1475
  ts.push('fontSize = ' + sp(p.fontSize));
@@ -1335,24 +1487,38 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1335
1487
  out += pad + ')';
1336
1488
  return out;
1337
1489
  }
1490
+ case 'vector': {
1491
+ const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...alphaArr], pad + ' ');
1492
+ return pad + kotlinVector(p, colorExpr, 'Modifier' + chain, kotlinBoundValue).split('\n').join('\n' + pad);
1493
+ }
1338
1494
  case 'image': {
1339
- const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1340
- if (p.src)
1341
- ctx.warnings.push('image src="' + p.src + '" 资源桥未实现(A5 TODO),生成占位 Box');
1342
- let out = pad + '// image src=' + (p.src ?? '') + '(A5 资源桥 TODO)\n';
1343
- out += pad + 'Box(\n' + pad + ' modifier = Modifier' + chain + ',\n' + pad + ') {}';
1344
- return out;
1495
+ const source = p.src ?? '';
1496
+ const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...paddingModifiers(p), ...alphaArr], pad + ' ');
1497
+ if (source.startsWith('asset://')) {
1498
+ const name = source.slice(8);
1499
+ return pad + 'run { val imageContext = androidx.compose.ui.platform.LocalContext.current; val bitmap = remember { imageContext.assets.open(' + kotlinStr(name) + ').use { android.graphics.BitmapFactory.decodeStream(it) } }; if(bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' + (p.objectFit === 'contain' ? 'Fit' : 'Crop') + ', modifier = Modifier' + chain + ') }';
1500
+ }
1501
+ if (/^data:image\/[^;]+;base64,/.test(source)) {
1502
+ const encoded = source.slice(source.indexOf(',') + 1);
1503
+ return pad + 'run {\n' + pad + ' val imageBytes = remember { android.util.Base64.decode(' + kotlinStr(encoded) + ', android.util.Base64.DEFAULT) }\n' +
1504
+ pad + ' val bitmap = remember { android.graphics.BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) }\n' +
1505
+ pad + ' if (bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' +
1506
+ (p.objectFit === 'contain' ? 'Fit' : 'Crop') + ', modifier = Modifier' + chain + ')\n' + pad + '}';
1507
+ }
1508
+ ctx.warnings.push(`图片 ${source.slice(0, 80)} 需要 data URI 资源映射,Android 未生成图片内容`);
1509
+ return pad + 'Box(modifier = Modifier' + chain + ')';
1345
1510
  }
1346
1511
  case 'spacer': {
1347
- const chain = modChain([flexModifier(p), ...sizeModifiers(p)], pad + ' ');
1512
+ const chain = modChain([flexModifier(p, ctx), ...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...(kotlinClickAction(p, ctx, loop) ? ['Modifier.clickable { ' + kotlinClickAction(p, ctx, loop) + ' }'] : [])], pad + ' ');
1348
1513
  return pad + 'Spacer(\n' + pad + ' modifier = Modifier' + chain + ',\n' + pad + ')';
1349
1514
  }
1350
1515
  case 'divider': {
1351
- const chain = modChain([...sizeModifiers(p), ...visualModifiers(p), ...paddingModifiers(p)], pad + ' ');
1516
+ const chain = modChain([...sizeModifiers(p, ctx), ...visualModifiers(p), ...paddingModifiers(p)], pad + ' ');
1352
1517
  return pad + 'HorizontalDivider(\n' + pad + ' modifier = Modifier' + chain + ',\n' + pad + ')';
1353
1518
  }
1354
1519
  case 'scroll': {
1355
- const chain = modChain([...sizeModifiers(p), ...paddingModifiers(p)], pad + ' ');
1520
+ ctx = { ...ctx, inVerticalScroll: true };
1521
+ const chain = modChain([...sizeModifiers(p, ctx), ...paddingModifiers(p)], pad + ' ');
1356
1522
  return pad + 'Column(\n' + pad + ' modifier = Modifier' + chain + '.verticalScroll(rememberScrollState()),\n' + pad + ') {\n' + childrenBody(node, depth + 1, 'Column') + '\n' + pad + '}';
1357
1523
  }
1358
1524
  case 'for': {
@@ -1362,8 +1528,11 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1362
1528
  return pad + '// for: 缺 vfor 或项模板';
1363
1529
  }
1364
1530
  if (vf.listState) {
1365
- ctx.warnings.push('v-for 动态列表 ' + vf.listState + ' 未实现(需数据桥,A5 TODO)——该列表不生成');
1366
- return pad + '// for: 动态列表 ' + vf.listState + ' TODO(A5 后需数据桥)';
1531
+ const key = vf.itemKey, index = vf.indexKey ?? '_vrnIndex';
1532
+ const lc = { itemKey: key, indexKey: index };
1533
+ const inner = node.children.map((c) => emitNode(c, { ...ctx, loop: lc }, depth + 1, lc)).join('\n');
1534
+ return pad + 'run {\n' + pad + ' val _vrnItems = remember(' + kotlinBoundValue(vf.listState) + ') { runCatching { val a = org.json.JSONArray(' + kotlinBoundValue(vf.listState) + '.toString()); List(a.length()) { a.opt(it) } }.getOrDefault(emptyList<Any>()) }\n' +
1535
+ pad + ' _vrnItems.forEachIndexed { ' + index + ', ' + key + ' ->\n' + inner + '\n' + pad + ' }\n' + pad + '}';
1367
1536
  }
1368
1537
  const list = vf.list ?? [];
1369
1538
  if (list.length === 0)
@@ -1416,8 +1585,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1416
1585
  // 布局 props(vue2ir 将组件根的 flex/width/height/marginTop 剥离到使用点)→ modifier 参数
1417
1586
  const mods = [
1418
1587
  ...stretchModifiers(parentStretch, node),
1419
- ...sizeModifiers(p),
1420
- flexModifier(p),
1588
+ ...sizeModifiers(p, ctx),
1589
+ flexModifier(p, ctx),
1421
1590
  ...absM(p),
1422
1591
  ...alphaArr,
1423
1592
  ...transformModifiers(p),
@@ -1527,6 +1696,7 @@ function emitComponentFile(name, ui, bundleId) {
1527
1696
  const IMPORTS_KT = `import androidx.compose.foundation.background
1528
1697
  import androidx.compose.foundation.clickable
1529
1698
  import androidx.compose.foundation.layout.*
1699
+ import androidx.compose.foundation.Image
1530
1700
  import androidx.compose.foundation.rememberScrollState
1531
1701
  import androidx.compose.foundation.verticalScroll
1532
1702
  import androidx.compose.foundation.shape.RoundedCornerShape
@@ -1536,16 +1706,24 @@ import androidx.compose.material3.*
1536
1706
  import androidx.compose.runtime.*
1537
1707
  import kotlinx.coroutines.*
1538
1708
  import androidx.compose.ui.Modifier
1709
+ import androidx.compose.ui.zIndex
1539
1710
  import androidx.compose.ui.draw.alpha
1540
1711
  import androidx.compose.ui.draw.clip
1712
+ import androidx.compose.ui.draw.shadow
1541
1713
  import androidx.compose.ui.draw.clipToBounds
1542
1714
  import androidx.compose.ui.draw.drawBehind
1715
+ import androidx.compose.ui.graphics.drawscope.withTransform
1543
1716
  import androidx.compose.ui.geometry.Offset
1544
1717
  import androidx.compose.foundation.border
1545
1718
  import androidx.compose.ui.Alignment
1546
1719
  import androidx.compose.ui.graphics.Color
1720
+ import androidx.compose.ui.graphics.asComposePath
1721
+ import androidx.compose.ui.graphics.nativeCanvas
1722
+ import androidx.compose.ui.graphics.toArgb
1723
+ import androidx.compose.ui.graphics.asImageBitmap
1547
1724
  import androidx.compose.ui.graphics.graphicsLayer
1548
1725
  import androidx.compose.ui.text.font.FontWeight
1726
+ import androidx.compose.ui.text.withStyle
1549
1727
  import androidx.compose.ui.text.style.TextAlign
1550
1728
  import androidx.compose.ui.unit.dp
1551
1729
  import androidx.compose.ui.unit.sp
@@ -1747,6 +1925,8 @@ function runTabs(appDir, opts = {}) {
1747
1925
  writeFileSync(join(pkgDir, 'Theme.kt'), THEME_KT(bundleId));
1748
1926
  writeFileSync(join(pkgDir, 'MainActivity.kt'), MAIN_ACTIVITY(bundleId));
1749
1927
  writeFileSync(join(pkgDir, 'Runtime.kt'), RUNTIME_KT(bundleId));
1928
+ if (opts.buildPageRuntime)
1929
+ writeFileSync(join(pkgDir, 'PageJs.kt'), pageJsAndroid(bundleId));
1750
1930
  // NavTest 断言绑定 vuexir-demo 文案:仅对参考样例生成,其他样例跳过(防无意义失败用例)
1751
1931
  if (projectName.toLowerCase() === 'vuexirdemo') {
1752
1932
  const navTestPath = join(outDir, 'app/src/androidTest/java', ...bundleId.split('.'), 'NavTest.kt');
@@ -1778,6 +1958,11 @@ function runTabs(appDir, opts = {}) {
1778
1958
  const r = vueToIr(src, {
1779
1959
  screenId: tab.id,
1780
1960
  title: tab.title,
1961
+ inlineComponents: true,
1962
+ liveStoreObjects: opts.buildPageRuntime !== undefined,
1963
+ pageScriptBundle: opts.buildPageRuntime !== undefined,
1964
+ assets: loadProjectAssets(tab.file, findProjectRoot(dirname(tab.file))),
1965
+ globalStyles: loadProjectStyles(findProjectRoot(dirname(tab.file))),
1781
1966
  components: loadComponents(tab.file),
1782
1967
  storeSources: loadStoreSources(tab.file),
1783
1968
  allowUnknownComponents: true,
@@ -1794,15 +1979,22 @@ function runTabs(appDir, opts = {}) {
1794
1979
  console.error(` ${i.path}: ${i.message}`);
1795
1980
  process.exit(1);
1796
1981
  }
1982
+ materializeProjectAssets(r.screen.ui, join(outDir, 'app/src/main/assets'), 'android');
1797
1983
  writeComponents(r.components);
1798
1984
  const ctx = {
1799
1985
  warnings: [],
1800
1986
  pageId: tab.id,
1987
+ runtimeAsset: opts.buildPageRuntime ? 'runtime_' + tab.id + '.js' : undefined,
1801
1988
  states: (r.screen.state ?? []).map((st) => ({ name: st.name, type: typeof st.init === 'string' ? 'string' : typeof st.init === 'boolean' ? 'boolean' : 'number', init: st.init })),
1802
1989
  handlers: r.screen.handlers,
1803
1990
  onMountedBody: r.screen.onMounted,
1804
1991
  onMountedAsync: r.screen.onMountedAsync === true,
1805
1992
  };
1993
+ if (opts.buildPageRuntime && ctx.runtimeAsset) {
1994
+ const assetsDir = join(outDir, 'app/src/main/assets');
1995
+ mkdirSync(assetsDir, { recursive: true });
1996
+ writeFileSync(join(assetsDir, ctx.runtimeAsset), opts.buildPageRuntime(tab.file, r.runtime ?? '', tab.id));
1997
+ }
1806
1998
  const body = emitNode(r.screen.ui, ctx, 1);
1807
1999
  for (const w of ctx.warnings)
1808
2000
  console.log(`[gen-android] 生成提示(${tab.id}): ${w}`);
@@ -1817,6 +2009,11 @@ function runTabs(appDir, opts = {}) {
1817
2009
  const r = vueToIr(src, {
1818
2010
  screenId: d.id,
1819
2011
  title: d.title,
2012
+ inlineComponents: true,
2013
+ liveStoreObjects: opts.buildPageRuntime !== undefined,
2014
+ pageScriptBundle: opts.buildPageRuntime !== undefined,
2015
+ assets: loadProjectAssets(d.file, findProjectRoot(dirname(d.file))),
2016
+ globalStyles: loadProjectStyles(findProjectRoot(dirname(d.file))),
1820
2017
  components: loadComponents(d.file),
1821
2018
  storeSources: loadStoreSources(d.file),
1822
2019
  allowUnknownComponents: true,
@@ -1833,15 +2030,22 @@ function runTabs(appDir, opts = {}) {
1833
2030
  console.error(` ${i.path}: ${i.message}`);
1834
2031
  process.exit(1);
1835
2032
  }
2033
+ materializeProjectAssets(r.screen.ui, join(outDir, 'app/src/main/assets'), 'android');
1836
2034
  writeComponents(r.components);
1837
2035
  const ctx = {
1838
2036
  warnings: [],
1839
2037
  pageId: `${d.id}Detail`,
2038
+ runtimeAsset: opts.buildPageRuntime ? 'runtime_' + d.id + '.js' : undefined,
1840
2039
  states: (r.screen.state ?? []).map((st) => ({ name: st.name, type: typeof st.init === 'string' ? 'string' : typeof st.init === 'boolean' ? 'boolean' : 'number', init: st.init })),
1841
2040
  handlers: r.screen.handlers,
1842
2041
  onMountedBody: r.screen.onMounted,
1843
2042
  onMountedAsync: r.screen.onMountedAsync === true,
1844
2043
  };
2044
+ if (opts.buildPageRuntime && ctx.runtimeAsset) {
2045
+ const assetsDir = join(outDir, 'app/src/main/assets');
2046
+ mkdirSync(assetsDir, { recursive: true });
2047
+ writeFileSync(join(assetsDir, ctx.runtimeAsset), opts.buildPageRuntime(d.file, r.runtime ?? '', d.id));
2048
+ }
1845
2049
  const body = emitNode(r.screen.ui, ctx, 1);
1846
2050
  for (const w of ctx.warnings)
1847
2051
  console.log(`[gen-android] 生成提示(${d.id}): ${w}`);
@@ -1853,7 +2057,9 @@ function runTabs(appDir, opts = {}) {
1853
2057
  console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
1854
2058
  }
1855
2059
  // 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
1856
- const navHost = buildNavHost(bundleId, tabs, detailViews);
2060
+ let navHost = buildNavHost(bundleId, tabs, detailViews, { tabBar: opts.tabBar });
2061
+ if (opts.nativeNavigationBar === false)
2062
+ navHost = navHost.replace(/ topBar = \{[\s\S]*? \},\n bottomBar =/, ' bottomBar =');
1857
2063
  writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
1858
2064
  // P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
1859
2065
  const metaDir = join(outDir, '.vrn');
@@ -1872,7 +2078,7 @@ function runTabs(appDir, opts = {}) {
1872
2078
  console.log(`[gen-android] 完成: ${outDir}(${viewFiles.length} tab + ${detailViews.length} detail)`);
1873
2079
  return outDir;
1874
2080
  }
1875
- function buildNavHost(bundleId, tabs, details) {
2081
+ function buildNavHost(bundleId, tabs, details, options = {}) {
1876
2082
  // R-CHROME:tab 顶/底栏由 tab 路由自己的 TabScaffold 承载(而非容器层按 currentRoute 开关)。
1877
2083
  // 容器层开关的缺陷:导航瞬间 currentRoute 已变 detail,退出动画中的 tab 页仍在绘制,
1878
2084
  // 其顶/底栏立即消失、innerPadding 塌缩 → 内容瞬时顶到系统栏,随后详情页才滑入(视觉跳变)。
@@ -1931,10 +2137,33 @@ function buildNavHost(bundleId, tabs, details) {
1931
2137
  )
1932
2138
  )`)
1933
2139
  .join('\n ');
2140
+ let customBar = '';
2141
+ if (options.tabBar) {
2142
+ const spec = options.tabBar;
2143
+ const cells = tabs.map((tab) => {
2144
+ const item = spec.items.find((item) => item.id === tab.id);
2145
+ const tint = `if (currentTab == "${tab.id}") ${colorExpr(spec.activeStyle.color ?? '#f3dca0')} else ${colorExpr(spec.itemStyle.color ?? '#5b647e')}`;
2146
+ const icon = item ? kotlinVector({ vector: item.icon, color: '__vrnTabTint' }, (color) => color === '__vrnTabTint' ? tint : colorExpr(color), 'Modifier.size(22.dp)') : `Icon(Icons.Default.Home,contentDescription = null)`;
2147
+ return `Column(modifier = Modifier.weight(1f).clickable { onSelectTab("${tab.id}") }.padding(vertical = 6.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(${dp(spec.itemStyle.gap ?? 3)})) {\n${icon}\nText(${kotlinStr(item?.label ?? tab.title)},fontSize = ${sp(spec.itemStyle.fontSize ?? 11)},color = ${tint})\n}`;
2148
+ }).join('\n');
2149
+ const chain = modChain([...clipModifiers(spec.style), ...visualModifiers(spec.style), ...borderModifiers(spec.style), ...paddingModifiers(spec.style)], ' ');
2150
+ customBar = `Box(modifier = Modifier.fillMaxWidth().navigationBarsPadding().padding(start = ${dp(spec.widthInset / 2)},end = ${dp(spec.widthInset / 2)},bottom = ${dp(spec.bottom)}),contentAlignment = Alignment.Center) {\nRow(modifier = Modifier.widthIn(max = ${dp(spec.maxWidth ?? 10000)}).fillMaxWidth()${chain}) {\n${cells}\n}\n}`;
2151
+ }
1934
2152
  const tabTitleExpr = `when (currentTab) {\n${tabs.map((t) => ` "${t.id}" -> ${kotlinStr(t.title)}`).join('\n')}\n else -> ""\n }`;
1935
2153
  return `package ${bundleId}
1936
2154
 
1937
2155
  import androidx.compose.foundation.layout.*
2156
+ import androidx.compose.foundation.Image
2157
+ import androidx.compose.foundation.background
2158
+ import androidx.compose.foundation.border
2159
+ import androidx.compose.foundation.clickable
2160
+ import androidx.compose.ui.draw.clip
2161
+ import androidx.compose.ui.draw.shadow
2162
+ import androidx.compose.ui.draw.drawBehind
2163
+ import androidx.compose.ui.geometry.Offset
2164
+ import androidx.compose.ui.graphics.drawscope.withTransform
2165
+ import androidx.compose.foundation.shape.RoundedCornerShape
2166
+ import androidx.compose.ui.unit.dp
1938
2167
  import androidx.compose.material.icons.Icons
1939
2168
  import androidx.compose.material.icons.automirrored.filled.*
1940
2169
  import androidx.compose.material.icons.filled.*
@@ -1942,8 +2171,14 @@ import androidx.compose.material3.*
1942
2171
  import androidx.compose.runtime.*
1943
2172
  import androidx.compose.ui.Alignment
1944
2173
  import androidx.compose.ui.Modifier
2174
+ import androidx.compose.ui.zIndex
1945
2175
  import androidx.compose.ui.graphics.Color
2176
+ import androidx.compose.ui.graphics.asComposePath
2177
+ import androidx.compose.ui.graphics.nativeCanvas
2178
+ import androidx.compose.ui.graphics.toArgb
2179
+ import androidx.compose.ui.graphics.asImageBitmap
1946
2180
  import androidx.compose.ui.text.font.FontWeight
2181
+ import androidx.compose.ui.text.withStyle
1947
2182
  import androidx.compose.ui.text.style.TextAlign
1948
2183
  import androidx.compose.ui.unit.sp
1949
2184
  import androidx.navigation.compose.*
@@ -1967,9 +2202,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
1967
2202
  )
1968
2203
  },
1969
2204
  bottomBar = {
1970
- NavigationBar(containerColor = InkBar) {
1971
- ${barItems}
1972
- }
2205
+ ${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
1973
2206
  },
1974
2207
  ) { innerPadding ->
1975
2208
  content(innerPadding)
@@ -1982,7 +2215,13 @@ fun AppNavHost() {
1982
2215
  val navController = rememberNavController()
1983
2216
  // A5 路由桥:Runtime.push(route) → NavHost navigate
1984
2217
  LaunchedEffect(Unit) {
1985
- Runtime.pushHandler = { route -> navController.navigate(route) }
2218
+ Runtime.pushHandler = { route ->
2219
+ val resolved = when(route) {
2220
+ ${details.map((d) => ` "${d.id}" -> "${d.path}"`).join('\n')}
2221
+ else -> route
2222
+ }
2223
+ navController.navigate(resolved)
2224
+ }
1986
2225
  }
1987
2226
  // tab 切换:popUpTo 首 tab 保留栈底 + saveState/restoreState 保留各页状态(对位 iOS 常驻页面实例)
1988
2227
  val onSelectTab: (String) -> Unit = { tabId ->