@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.
@@ -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,13 @@ 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 '';
680
746
  return props.flex !== undefined ? `Modifier.weight(${props.flex}f)` : '';
681
747
  }
682
748
  /** 容器选择(对位 iOS VStack/HStack/ZStack) */
683
749
  function containerKind(props) {
684
- if (props.position === 'absolute')
685
- return 'Box';
686
750
  return props.flexDirection === 'row' ? 'Row' : 'Column';
687
751
  }
688
752
  /** 排列参数(按容器方向:Column 用 verticalArrangement/horizontalAlignment;Row 用 horizontalArrangement/verticalAlignment) */
@@ -785,6 +849,12 @@ function inlineHandlerKotlin(fn) {
785
849
  }
786
850
  /** 点击事件 → Kotlin 表达式(A5:onSet 内联赋值;onClick 模式识别:自增减/路由 push/无参 handler 内联/其他 JS TODO) */
787
851
  function kotlinClickAction(p, ctx, loop) {
852
+ if (ctx.runtimeAsset && p.onClick) {
853
+ const expression = /^[A-Za-z_$][\w$]*$/.test(p.onClick) ? p.onClick + '()' : p.onClick;
854
+ if (loop)
855
+ return 'vrnPageJs.template(' + kotlinStr(expression) + ', mapOf(' + kotlinStr(loop.itemKey) + ' to ' + loop.itemKey + '))';
856
+ return 'vrnPageJs.eval(' + kotlinStr(expression) + ')';
857
+ }
788
858
  if (p.onSet) {
789
859
  const v = p.onSet.value;
790
860
  const rhs = p.onSet.kind === 'expr' ? String(v) : typeof v === 'string' ? kotlinStr(v) : String(v);
@@ -1057,9 +1127,26 @@ function stateDeclarations(ctx, pad) {
1057
1127
  })
1058
1128
  .join('\n');
1059
1129
  }
1130
+ let composeSections = [];
1131
+ let composingSection = new WeakSet();
1132
+ const composeNodeCount = (n) => 1 + (n.children ?? []).reduce((sum, c) => sum + composeNodeCount(c), 0);
1060
1133
  /** 纯字符串拼接生成节点(避免嵌套模板转译问题);v-if(node.condition)在此统一包裹 */
1061
1134
  function emitNode(node, ctx, depth, loop, useParamModifier = false, parentStretch = false) {
1062
1135
  const pad = ' '.repeat(depth);
1136
+ if (node.type === 'root') {
1137
+ composeSections = [];
1138
+ composingSection = new WeakSet();
1139
+ }
1140
+ if (ctx.runtimeAsset && depth >= 3 && !loop && node.type !== 'for' && composeNodeCount(node) >= 10 && !composingSection.has(node)) {
1141
+ composingSection.add(node);
1142
+ const source = emitNode(node, ctx, depth, loop, useParamModifier, parentStretch);
1143
+ const name = '_vrnSection' + composeSections.length;
1144
+ const receiver = ctx.parentAxis === 'box' ? 'BoxScope' : ctx.parentAxis === 'row' ? 'RowScope' : 'ColumnScope';
1145
+ composeSections.push('val ' + name + ': @Composable ' + receiver + '.() -> Unit = {\n' + source + '\n}');
1146
+ return pad + name + '()';
1147
+ }
1148
+ if (node.props?.show)
1149
+ 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
1150
  // v-if → Kotlin if 包裹(表达式为双端兼容子集 ==/!=/&&/||/!,=== / !== 由 kotlinCondExpr 归一)
1064
1151
  if (node.condition && node.type !== 'root') {
1065
1152
  return (pad + 'if (' + kotlinCondExpr(node.condition, ctx, loop) + ') {\n' +
@@ -1070,7 +1157,7 @@ function emitNode(node, ctx, depth, loop, useParamModifier = false, parentStretc
1070
1157
  }
1071
1158
  /** CSS 默认 align-items:stretch:column 父容器内未声明宽度/弹性/绝对定位的子项拉满父宽(R29 盒模型,对位 iOS maxWidth 无穷) */
1072
1159
  function stretchModifiers(parentStretch, node) {
1073
- if (!parentStretch || node.type === 'spacer' || node.type === 'divider')
1160
+ if (!parentStretch || (node.props?.alignSelf !== undefined && node.props.alignSelf !== 'stretch') || node.type === 'spacer' || node.type === 'divider')
1074
1161
  return [];
1075
1162
  return ['Modifier.fillMaxWidth()'];
1076
1163
  }
@@ -1085,7 +1172,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1085
1172
  const pad = ' '.repeat(depth);
1086
1173
  const p = (node.props ?? {});
1087
1174
  /** 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));
1175
+ const absM = (pp) => ((ctx.parentAxis !== 'box' || (useParamModifier && depth <= 2)) ? absModifiers(pp).filter((m) => !m.includes('.align(')) : absModifiers(pp));
1089
1176
  const alphaMod = alphaModifier(p);
1090
1177
  const alphaArr = alphaMod ? [alphaMod] : [];
1091
1178
  /** 容器类型:存在 absolute 子节点 → Box(ZStack 语义,对位 iOS);否则按 flexDirection */
@@ -1099,7 +1186,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1099
1186
  const groupBackground = (nd, props) => {
1100
1187
  const activeBg = nd.active ? nd.active.props?.background : undefined;
1101
1188
  if (typeof activeBg === 'string' && nd.active) {
1102
- const when = kotlinCondExpr(nd.active.when);
1189
+ const when = kotlinCondExpr(nd.active.when, ctx, loop);
1103
1190
  const active = colorExpr(activeBg);
1104
1191
  // 无基础背景时回退透明(对位 iOS `.background(when ? color : Color.clear)`)——
1105
1192
  // 否则选中态会变成「所有行都高亮」(此前 bug)
@@ -1113,6 +1200,12 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1113
1200
  const childrenBody = (nd, d, kind) => {
1114
1201
  if (!nd.children)
1115
1202
  return '';
1203
+ if (kind === 'Box' && nd.children.some((c) => c.props?.position === 'absolute') && nd.children.some((c) => c.props?.position !== 'absolute')) {
1204
+ const flow = nd.children.filter((c) => c.props?.position !== 'absolute');
1205
+ const first = nd.children.findIndex((c) => c.props?.position !== 'absolute');
1206
+ 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 }] : []);
1207
+ return decorated.map((child) => emitNode(child, { ...ctx, parentAxis: 'box' }, d, loop)).join('\n');
1208
+ }
1116
1209
  return nd.children
1117
1210
  .map((c) => {
1118
1211
  const cp = (c.props ?? {});
@@ -1120,10 +1213,10 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1120
1213
  let pre = '';
1121
1214
  let post = '';
1122
1215
  if (kind === 'Column') {
1123
- if (cp.marginTopAuto) {
1216
+ if (cp.marginTopAuto && !ctx.inVerticalScroll) {
1124
1217
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.weight(1f),\n' + cpad + ')\n';
1125
1218
  }
1126
- if (cp.marginTop !== undefined) {
1219
+ if (cp.marginTop !== undefined && cp.marginTop !== 0) {
1127
1220
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.height(' + dp(cp.marginTop) + '),\n' + cpad + ')\n';
1128
1221
  }
1129
1222
  }
@@ -1131,36 +1224,39 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1131
1224
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.weight(1f),\n' + cpad + ')\n';
1132
1225
  }
1133
1226
  // R70 其余单边外边距(前置/后置固定间隔,对位 iOS Color.clear 间隔)
1134
- if (kind === 'Row' && cp.marginLeft !== undefined) {
1227
+ if (kind === 'Row' && cp.marginLeft !== undefined && cp.marginLeft !== 0) {
1135
1228
  pre += cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.width(' + dp(cp.marginLeft) + '),\n' + cpad + ')\n';
1136
1229
  }
1137
- if (kind === 'Row' && cp.marginRight !== undefined) {
1230
+ if (kind === 'Row' && cp.marginRight !== undefined && cp.marginRight !== 0) {
1138
1231
  post = post + '\n' + cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.width(' + dp(cp.marginRight) + '),\n' + cpad + ')\n';
1139
1232
  }
1140
- if (kind === 'Column' && cp.marginBottom !== undefined) {
1233
+ if (kind === 'Column' && cp.marginBottom !== undefined && cp.marginBottom !== 0) {
1141
1234
  post = post + '\n' + cpad + 'Spacer(\n' + cpad + ' modifier = Modifier.height(' + dp(cp.marginBottom) + '),\n' + cpad + ')\n';
1142
1235
  }
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;
1236
+ 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;
1237
+ const childCtx = { ...ctx, parentAxis: kind === 'Row' ? 'row' : kind === 'Box' ? 'box' : 'column' };
1238
+ const result = emitNode(c, childCtx, d, loop, false, stretch);
1239
+ if (childCtx.needsScope)
1240
+ ctx.needsScope = true;
1241
+ return pre + result + post;
1145
1242
  })
1146
1243
  .join('\n');
1147
1244
  };
1148
1245
  switch (node.type) {
1149
1246
  case 'root': {
1150
1247
  // 单层容器:设计稿固定高度(640)在真机上会导致内容被限高挤压 → 移除 height,fillMaxSize 铺满(对位 iOS 背景铺满)
1151
- const size = sizeModifiers(p).filter((m) => !m.includes('.height('));
1248
+ const size = sizeModifiers(p, ctx).filter((m) => !m.includes('.height('));
1152
1249
  // R-PAGESCROLL:页面根 = 可滚动视口(对位 Web body 默认滚动 / 对位 iOS root ScrollView)。
1153
1250
  // 守卫:子树存在 flex/marginTopAuto(→ Modifier.weight)时不加滚动——滚动容器沿主轴给无界高度约束,
1154
1251
  // 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())'];
1252
+ const containsScroll = (n) => n.type === 'scroll' || (n.children ?? []).some(containsScroll);
1253
+ const outerScroll = !(node.children ?? []).some(containsScroll);
1254
+ const scrollMods = outerScroll ? ['Modifier.verticalScroll(rememberScrollState())'] : [];
1255
+ ctx.inVerticalScroll = outerScroll;
1160
1256
  const chain = modChain([...size, ...scrollMods, ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p)], pad + ' ');
1161
1257
  const kind = kindFor(node, p);
1162
1258
  const args = arrangementExpr(p, kind);
1163
- let out = pad + '@Composable\n';
1259
+ let out = pad + '@OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)\n' + pad + '@Composable\n';
1164
1260
  out += pad + 'fun ' + toPascal(ctx.pageId) + 'Content(\n';
1165
1261
  out += pad + ' modifier: Modifier = Modifier\n' + pad + ') {\n';
1166
1262
  const states = stateDeclarations(ctx, pad);
@@ -1170,6 +1266,16 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1170
1266
  const children = childrenBody(node, depth + 1, kind);
1171
1267
  // R72 异步:本页含 async handler / async onMounted → 声明协程作用域(按钮 onClick 用 vrnScope.launch)
1172
1268
  const pre = [];
1269
+ if (ctx.runtimeAsset) {
1270
+ pre.push(pad + ' val vrnPageContext = androidx.compose.ui.platform.LocalContext.current');
1271
+ pre.push(pad + ' val vrnPageJs = remember { VrnPageJs(vrnPageContext, ' + kotlinStr(ctx.runtimeAsset) + ') }');
1272
+ pre.push(pad + ' DisposableEffect(vrnPageJs) { onDispose { vrnPageJs.close() } }');
1273
+ const updates = (ctx.states ?? []).map((st) => {
1274
+ const get = st.type === 'string' ? `updates.optString("${st.name}")` : st.type === 'boolean' ? `updates.optBoolean("${st.name}")` : `updates.optInt("${st.name}")`;
1275
+ return pad + ` if (updates.has("${st.name}")) ${st.name} = ${get}`;
1276
+ }).join('\n');
1277
+ 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 + ' }');
1278
+ }
1173
1279
  const asyncOm = ctx.onMountedAsync && ctx.onMountedBody ? asyncHandlerKotlin({ name: '__onMounted', params: '', body: ctx.onMountedBody }, ctx) : null;
1174
1280
  if (ctx.onMountedAsync && ctx.onMountedBody && asyncOm === null) {
1175
1281
  ctx.warnings.push('async onMounted 含超出协程子集的形状(R72 子集:await fetch / JSON.parse / 状态赋值 / setTimeout),已跳过生成');
@@ -1179,7 +1285,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1179
1285
  if (asyncOm !== null) {
1180
1286
  pre.push(pad + ' LaunchedEffect(Unit) {\n' + asyncOm.split('\n').map((l) => pad + ' ' + l).join('\n') + '\n' + pad + ' }');
1181
1287
  }
1182
- const decls = [states, ...pre].filter((x) => x && x.length > 0).join('\n');
1288
+ const decls = [states, ...pre, ...composeSections].filter((x) => x && x.length > 0).join('\n');
1183
1289
  out += pad + ' ) {' + (decls ? '\n' + decls : '') + '\n' + children + '\n' + pad + ' }\n';
1184
1290
  out += pad + '}';
1185
1291
  return out;
@@ -1189,16 +1295,29 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1189
1295
  const gm = depth === 1 && useParamModifier ? 'modifier' : 'Modifier';
1190
1296
  const chain = modChain([
1191
1297
  ...stretchModifiers(parentStretch, node),
1192
- ...sizeModifiers(p),
1298
+ ...sizeModifiers(p, ctx),
1193
1299
  ...absM(p),
1194
1300
  ...clipModifiers(p),
1195
1301
  ...groupBackground(node, p),
1196
1302
  ...borderModifiers(p),
1197
- ...paddingModifiers(p),
1198
- flexModifier(p),
1303
+ ...(node.children?.some((c) => c.props?.position === 'absolute') ? [] : paddingModifiers(p)),
1304
+ flexModifier(p, ctx),
1199
1305
  ...alphaArr,
1200
1306
  ...transformModifiers(p),
1201
1307
  ].concat(click ? [gm + '.clickable { ' + click + ' }'] : []), pad + ' ');
1308
+ if (p.gridColumns && node.children?.length) {
1309
+ const columns = p.gridColumns, gap = dp(p.gap ?? 0);
1310
+ const rows = [];
1311
+ for (let i = 0; i < node.children.length; i += columns) {
1312
+ 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');
1313
+ rows.push(pad + ' Row(horizontalArrangement = Arrangement.spacedBy(' + gap + ')) {\n' + cells + '\n' + pad + ' }');
1314
+ }
1315
+ return pad + 'Column(modifier = Modifier' + chain + ', verticalArrangement = Arrangement.spacedBy(' + gap + ')) {\n' + rows.join('\n') + '\n' + pad + '}';
1316
+ }
1317
+ if (p.flexWrap) {
1318
+ const cells = (node.children ?? []).map((child) => emitNode(child, { ...ctx, parentAxis: 'row' }, depth + 1, loop)).join('\n');
1319
+ return pad + 'FlowRow(modifier = Modifier' + chain + ', horizontalArrangement = Arrangement.spacedBy(' + dp(p.gap ?? 0) + '), verticalArrangement = Arrangement.spacedBy(' + dp(p.gap ?? 0) + ')) {\n' + cells + '\n' + pad + '}';
1320
+ }
1202
1321
  const kind = kindFor(node, p);
1203
1322
  const args = arrangementExpr(p, kind);
1204
1323
  let out = pad + kind + '(\n';
@@ -1209,7 +1328,25 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1209
1328
  case 'text': {
1210
1329
  // 文本表达式(M2/M4b 动态:bindParts 混合段 / bindExpr 三元 / dynField 循环字段 / bind state / ref 组件参数 / 静态 text)
1211
1330
  let textExpr;
1212
- if (p.bindParts) {
1331
+ if (p.textRuns) {
1332
+ const runs = flattenTextRuns(p.textRuns).map((run) => {
1333
+ const child = emitNode({ type: 'text', props: { ...run } }, ctx, 0, loop);
1334
+ const expr = child.match(/text = ([\s\S]*?),\n/)?.[1] ?? '""';
1335
+ const style = [];
1336
+ const fs = run.fontSize ?? p.fontSize;
1337
+ if (fs !== undefined)
1338
+ style.push(`fontSize = ${sp(fs)}`);
1339
+ const col = run.color ?? p.color;
1340
+ if (col)
1341
+ style.push(`color = ${colorExpr(col)}`);
1342
+ const weight = run.fontWeight ?? p.fontWeight;
1343
+ if (weight)
1344
+ style.push('fontWeight = FontWeight.' + (weight === 'bold' ? 'Bold' : weight === 'medium' ? 'Medium' : 'Normal'));
1345
+ return `withStyle(androidx.compose.ui.text.SpanStyle(${style.join(', ')})) { append(${expr}) }`;
1346
+ });
1347
+ textExpr = 'androidx.compose.ui.text.buildAnnotatedString { ' + runs.join('; ') + ' }';
1348
+ }
1349
+ else if (p.bindParts) {
1213
1350
  const segs = p.bindParts
1214
1351
  .map((seg) => {
1215
1352
  if ('text' in seg)
@@ -1224,7 +1361,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1224
1361
  '"' +
1225
1362
  (seg.fieldOf.fallback !== undefined ? ', "' + seg.fieldOf.fallback.replace(/"/g, '\\"') + '"' : '') +
1226
1363
  ')}');
1227
- return '${' + loopField(seg.dynField.var, loop) + '["' + seg.dynField.field + '"] ?: ""}';
1364
+ return '${vrnItemField(' + seg.dynField.var + ', \"' + seg.dynField.field + '\")}';
1228
1365
  })
1229
1366
  .join('');
1230
1367
  textExpr = '"' + segs + '"';
@@ -1240,7 +1377,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1240
1377
  ')}"';
1241
1378
  }
1242
1379
  else if (p.dynField) {
1243
- textExpr = '"${' + loopField(p.dynField.var, loop) + '["' + p.dynField.field + '"] ?: ""}"';
1380
+ textExpr = '"${vrnItemField(' + p.dynField.var + ', \"' + p.dynField.field + '\")}"';
1244
1381
  }
1245
1382
  else if (p.bindExpr !== undefined) {
1246
1383
  textExpr = kotlinStringExpr(p.bindExpr);
@@ -1256,18 +1393,23 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1256
1393
  }
1257
1394
  // R-TEXTCLICK:@click 挂在 text 上(<span @click>)→ clickable(此前静默丢失交互)
1258
1395
  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 + ' ');
1396
+ 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
1397
  const style = [];
1398
+ if (p.fontFamily)
1399
+ style.push('fontFamily = androidx.compose.ui.text.font.FontFamily.' + (p.fontFamily === 'serif' ? 'Serif' : p.fontFamily === 'monospace' ? 'Monospace' : 'SansSerif'));
1400
+ if (p.letterSpacing !== undefined)
1401
+ style.push('letterSpacing = ' + sp(p.letterSpacing));
1261
1402
  if (p.fontSize !== undefined)
1262
1403
  style.push('fontSize = ' + sp(p.fontSize));
1263
1404
  else if (p.fontSizeDynamic !== undefined)
1264
1405
  style.push('fontSize = ' + p.fontSizeDynamic + '.sp');
1265
1406
  if (p.fontWeight !== undefined)
1266
1407
  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));
1408
+ const lineHeight = p.lineHeightMultiplier !== undefined ? (p.fontSize ?? 15) * p.lineHeightMultiplier : p.lineHeight;
1409
+ if (lineHeight !== undefined)
1410
+ style.push('lineHeight = ' + sp(Number(lineHeight.toFixed(4))));
1269
1411
  if (p.colorActive) {
1270
- style.push('color = if (' + kotlinCondExpr(p.colorActive.when) + ') ' + colorExpr(p.colorActive.color) + ' else ' + colorExpr(p.color ?? '#ffffff'));
1412
+ style.push('color = if (' + kotlinCondExpr(p.colorActive.when, ctx, loop) + ') ' + colorExpr(p.colorActive.color) + ' else ' + colorExpr(p.color ?? '#ffffff'));
1271
1413
  }
1272
1414
  else if (p.colorDynamic !== undefined) {
1273
1415
  style.push('color = dynamicColor(' + p.colorDynamic + ')');
@@ -1287,11 +1429,19 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1287
1429
  return out;
1288
1430
  }
1289
1431
  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 + ' ');
1432
+ const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1291
1433
  // R108:动态按钮文案(props.bind)优先;Kotlin 里用字符串模板插值
1292
1434
  const label = p.bind !== undefined ? String(p.bind) : (p.label ?? '');
1293
1435
  const click = kotlinClickAction(p, ctx, loop);
1436
+ if (p.icon) {
1437
+ const icon = kotlinVector({ vector: p.icon.vector, color: p.color }, colorExpr, `Modifier.size(${dp(p.icon.width)}, ${dp(p.icon.height)})`);
1438
+ 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 + '}';
1439
+ }
1294
1440
  const labelStyle = [];
1441
+ if (p.fontFamily)
1442
+ labelStyle.push('fontFamily = androidx.compose.ui.text.font.FontFamily.' + (p.fontFamily === 'serif' ? 'Serif' : p.fontFamily === 'monospace' ? 'Monospace' : 'SansSerif'));
1443
+ if (p.fontWeight)
1444
+ labelStyle.push('fontWeight = FontWeight.' + (p.fontWeight === 'bold' ? 'Bold' : p.fontWeight === 'medium' ? 'Medium' : 'Normal'));
1295
1445
  if (p.fontSize !== undefined)
1296
1446
  labelStyle.push('fontSize = ' + sp(p.fontSize));
1297
1447
  else if (p.fontSizeDynamic !== undefined)
@@ -1300,13 +1450,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1300
1450
  labelStyle.push('color = ' + colorExpr(p.color));
1301
1451
  else if (p.colorDynamic !== undefined)
1302
1452
  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 + '}';
1453
+ let out = pad + 'Box(modifier = Modifier' + chain + (click && p.disabled !== true ? '.clickable(role = androidx.compose.ui.semantics.Role.Button) { ' + click + ' }' : '') + ', contentAlignment = Alignment.Center) {\n';
1454
+ out += pad + ' Text(' + (p.bind ? kotlinBoundValue(label) : kotlinStr(label)) + (labelStyle.length ? ', ' + labelStyle.join(', ') : '') + ')\n' + pad + '}';
1310
1455
  return out;
1311
1456
  }
1312
1457
  case 'input': {
@@ -1317,7 +1462,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1317
1462
  ctx.warnings.push('input: v-model「' + String(st) + '」非页面级 string 状态(Android 暂只支持该形态),已生成占位注释');
1318
1463
  return pad + '// input: v-model ' + kotlinStr(String(st)) + ' 未支持(Android 仅页面级 string state)';
1319
1464
  }
1320
- const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absM(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1465
+ const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p, ctx), ...absM(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1321
1466
  const ts = [];
1322
1467
  if (p.fontSize !== undefined)
1323
1468
  ts.push('fontSize = ' + sp(p.fontSize));
@@ -1335,24 +1480,38 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1335
1480
  out += pad + ')';
1336
1481
  return out;
1337
1482
  }
1483
+ case 'vector': {
1484
+ const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...alphaArr], pad + ' ');
1485
+ return pad + kotlinVector(p, colorExpr, 'Modifier' + chain, kotlinBoundValue).split('\n').join('\n' + pad);
1486
+ }
1338
1487
  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;
1488
+ const source = p.src ?? '';
1489
+ const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...paddingModifiers(p), ...alphaArr], pad + ' ');
1490
+ if (source.startsWith('asset://')) {
1491
+ const name = source.slice(8);
1492
+ 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 + ') }';
1493
+ }
1494
+ if (/^data:image\/[^;]+;base64,/.test(source)) {
1495
+ const encoded = source.slice(source.indexOf(',') + 1);
1496
+ return pad + 'run {\n' + pad + ' val imageBytes = remember { android.util.Base64.decode(' + kotlinStr(encoded) + ', android.util.Base64.DEFAULT) }\n' +
1497
+ pad + ' val bitmap = remember { android.graphics.BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) }\n' +
1498
+ pad + ' if (bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' +
1499
+ (p.objectFit === 'contain' ? 'Fit' : 'Crop') + ', modifier = Modifier' + chain + ')\n' + pad + '}';
1500
+ }
1501
+ ctx.warnings.push(`图片 ${source.slice(0, 80)} 需要 data URI 资源映射,Android 未生成图片内容`);
1502
+ return pad + 'Box(modifier = Modifier' + chain + ')';
1345
1503
  }
1346
1504
  case 'spacer': {
1347
- const chain = modChain([flexModifier(p), ...sizeModifiers(p)], pad + ' ');
1505
+ 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
1506
  return pad + 'Spacer(\n' + pad + ' modifier = Modifier' + chain + ',\n' + pad + ')';
1349
1507
  }
1350
1508
  case 'divider': {
1351
- const chain = modChain([...sizeModifiers(p), ...visualModifiers(p), ...paddingModifiers(p)], pad + ' ');
1509
+ const chain = modChain([...sizeModifiers(p, ctx), ...visualModifiers(p), ...paddingModifiers(p)], pad + ' ');
1352
1510
  return pad + 'HorizontalDivider(\n' + pad + ' modifier = Modifier' + chain + ',\n' + pad + ')';
1353
1511
  }
1354
1512
  case 'scroll': {
1355
- const chain = modChain([...sizeModifiers(p), ...paddingModifiers(p)], pad + ' ');
1513
+ ctx = { ...ctx, inVerticalScroll: true };
1514
+ const chain = modChain([...sizeModifiers(p, ctx), ...paddingModifiers(p)], pad + ' ');
1356
1515
  return pad + 'Column(\n' + pad + ' modifier = Modifier' + chain + '.verticalScroll(rememberScrollState()),\n' + pad + ') {\n' + childrenBody(node, depth + 1, 'Column') + '\n' + pad + '}';
1357
1516
  }
1358
1517
  case 'for': {
@@ -1362,8 +1521,11 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1362
1521
  return pad + '// for: 缺 vfor 或项模板';
1363
1522
  }
1364
1523
  if (vf.listState) {
1365
- ctx.warnings.push('v-for 动态列表 ' + vf.listState + ' 未实现(需数据桥,A5 TODO)——该列表不生成');
1366
- return pad + '// for: 动态列表 ' + vf.listState + ' TODO(A5 后需数据桥)';
1524
+ const key = vf.itemKey, index = vf.indexKey ?? '_vrnIndex';
1525
+ const lc = { itemKey: key, indexKey: index };
1526
+ const inner = node.children.map((c) => emitNode(c, { ...ctx, loop: lc }, depth + 1, lc)).join('\n');
1527
+ 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' +
1528
+ pad + ' _vrnItems.forEachIndexed { ' + index + ', ' + key + ' ->\n' + inner + '\n' + pad + ' }\n' + pad + '}';
1367
1529
  }
1368
1530
  const list = vf.list ?? [];
1369
1531
  if (list.length === 0)
@@ -1416,8 +1578,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1416
1578
  // 布局 props(vue2ir 将组件根的 flex/width/height/marginTop 剥离到使用点)→ modifier 参数
1417
1579
  const mods = [
1418
1580
  ...stretchModifiers(parentStretch, node),
1419
- ...sizeModifiers(p),
1420
- flexModifier(p),
1581
+ ...sizeModifiers(p, ctx),
1582
+ flexModifier(p, ctx),
1421
1583
  ...absM(p),
1422
1584
  ...alphaArr,
1423
1585
  ...transformModifiers(p),
@@ -1527,6 +1689,7 @@ function emitComponentFile(name, ui, bundleId) {
1527
1689
  const IMPORTS_KT = `import androidx.compose.foundation.background
1528
1690
  import androidx.compose.foundation.clickable
1529
1691
  import androidx.compose.foundation.layout.*
1692
+ import androidx.compose.foundation.Image
1530
1693
  import androidx.compose.foundation.rememberScrollState
1531
1694
  import androidx.compose.foundation.verticalScroll
1532
1695
  import androidx.compose.foundation.shape.RoundedCornerShape
@@ -1536,16 +1699,24 @@ import androidx.compose.material3.*
1536
1699
  import androidx.compose.runtime.*
1537
1700
  import kotlinx.coroutines.*
1538
1701
  import androidx.compose.ui.Modifier
1702
+ import androidx.compose.ui.zIndex
1539
1703
  import androidx.compose.ui.draw.alpha
1540
1704
  import androidx.compose.ui.draw.clip
1705
+ import androidx.compose.ui.draw.shadow
1541
1706
  import androidx.compose.ui.draw.clipToBounds
1542
1707
  import androidx.compose.ui.draw.drawBehind
1708
+ import androidx.compose.ui.graphics.drawscope.withTransform
1543
1709
  import androidx.compose.ui.geometry.Offset
1544
1710
  import androidx.compose.foundation.border
1545
1711
  import androidx.compose.ui.Alignment
1546
1712
  import androidx.compose.ui.graphics.Color
1713
+ import androidx.compose.ui.graphics.asComposePath
1714
+ import androidx.compose.ui.graphics.nativeCanvas
1715
+ import androidx.compose.ui.graphics.toArgb
1716
+ import androidx.compose.ui.graphics.asImageBitmap
1547
1717
  import androidx.compose.ui.graphics.graphicsLayer
1548
1718
  import androidx.compose.ui.text.font.FontWeight
1719
+ import androidx.compose.ui.text.withStyle
1549
1720
  import androidx.compose.ui.text.style.TextAlign
1550
1721
  import androidx.compose.ui.unit.dp
1551
1722
  import androidx.compose.ui.unit.sp
@@ -1747,6 +1918,8 @@ function runTabs(appDir, opts = {}) {
1747
1918
  writeFileSync(join(pkgDir, 'Theme.kt'), THEME_KT(bundleId));
1748
1919
  writeFileSync(join(pkgDir, 'MainActivity.kt'), MAIN_ACTIVITY(bundleId));
1749
1920
  writeFileSync(join(pkgDir, 'Runtime.kt'), RUNTIME_KT(bundleId));
1921
+ if (opts.buildPageRuntime)
1922
+ writeFileSync(join(pkgDir, 'PageJs.kt'), pageJsAndroid(bundleId));
1750
1923
  // NavTest 断言绑定 vuexir-demo 文案:仅对参考样例生成,其他样例跳过(防无意义失败用例)
1751
1924
  if (projectName.toLowerCase() === 'vuexirdemo') {
1752
1925
  const navTestPath = join(outDir, 'app/src/androidTest/java', ...bundleId.split('.'), 'NavTest.kt');
@@ -1778,6 +1951,11 @@ function runTabs(appDir, opts = {}) {
1778
1951
  const r = vueToIr(src, {
1779
1952
  screenId: tab.id,
1780
1953
  title: tab.title,
1954
+ inlineComponents: true,
1955
+ liveStoreObjects: opts.buildPageRuntime !== undefined,
1956
+ pageScriptBundle: opts.buildPageRuntime !== undefined,
1957
+ assets: loadProjectAssets(tab.file, findProjectRoot(dirname(tab.file))),
1958
+ globalStyles: loadProjectStyles(findProjectRoot(dirname(tab.file))),
1781
1959
  components: loadComponents(tab.file),
1782
1960
  storeSources: loadStoreSources(tab.file),
1783
1961
  allowUnknownComponents: true,
@@ -1794,15 +1972,22 @@ function runTabs(appDir, opts = {}) {
1794
1972
  console.error(` ${i.path}: ${i.message}`);
1795
1973
  process.exit(1);
1796
1974
  }
1975
+ materializeProjectAssets(r.screen.ui, join(outDir, 'app/src/main/assets'), 'android');
1797
1976
  writeComponents(r.components);
1798
1977
  const ctx = {
1799
1978
  warnings: [],
1800
1979
  pageId: tab.id,
1980
+ runtimeAsset: opts.buildPageRuntime ? 'runtime_' + tab.id + '.js' : undefined,
1801
1981
  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
1982
  handlers: r.screen.handlers,
1803
1983
  onMountedBody: r.screen.onMounted,
1804
1984
  onMountedAsync: r.screen.onMountedAsync === true,
1805
1985
  };
1986
+ if (opts.buildPageRuntime && ctx.runtimeAsset) {
1987
+ const assetsDir = join(outDir, 'app/src/main/assets');
1988
+ mkdirSync(assetsDir, { recursive: true });
1989
+ writeFileSync(join(assetsDir, ctx.runtimeAsset), opts.buildPageRuntime(tab.file, r.runtime ?? '', tab.id));
1990
+ }
1806
1991
  const body = emitNode(r.screen.ui, ctx, 1);
1807
1992
  for (const w of ctx.warnings)
1808
1993
  console.log(`[gen-android] 生成提示(${tab.id}): ${w}`);
@@ -1817,6 +2002,11 @@ function runTabs(appDir, opts = {}) {
1817
2002
  const r = vueToIr(src, {
1818
2003
  screenId: d.id,
1819
2004
  title: d.title,
2005
+ inlineComponents: true,
2006
+ liveStoreObjects: opts.buildPageRuntime !== undefined,
2007
+ pageScriptBundle: opts.buildPageRuntime !== undefined,
2008
+ assets: loadProjectAssets(d.file, findProjectRoot(dirname(d.file))),
2009
+ globalStyles: loadProjectStyles(findProjectRoot(dirname(d.file))),
1820
2010
  components: loadComponents(d.file),
1821
2011
  storeSources: loadStoreSources(d.file),
1822
2012
  allowUnknownComponents: true,
@@ -1833,15 +2023,22 @@ function runTabs(appDir, opts = {}) {
1833
2023
  console.error(` ${i.path}: ${i.message}`);
1834
2024
  process.exit(1);
1835
2025
  }
2026
+ materializeProjectAssets(r.screen.ui, join(outDir, 'app/src/main/assets'), 'android');
1836
2027
  writeComponents(r.components);
1837
2028
  const ctx = {
1838
2029
  warnings: [],
1839
2030
  pageId: `${d.id}Detail`,
2031
+ runtimeAsset: opts.buildPageRuntime ? 'runtime_' + d.id + '.js' : undefined,
1840
2032
  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
2033
  handlers: r.screen.handlers,
1842
2034
  onMountedBody: r.screen.onMounted,
1843
2035
  onMountedAsync: r.screen.onMountedAsync === true,
1844
2036
  };
2037
+ if (opts.buildPageRuntime && ctx.runtimeAsset) {
2038
+ const assetsDir = join(outDir, 'app/src/main/assets');
2039
+ mkdirSync(assetsDir, { recursive: true });
2040
+ writeFileSync(join(assetsDir, ctx.runtimeAsset), opts.buildPageRuntime(d.file, r.runtime ?? '', d.id));
2041
+ }
1845
2042
  const body = emitNode(r.screen.ui, ctx, 1);
1846
2043
  for (const w of ctx.warnings)
1847
2044
  console.log(`[gen-android] 生成提示(${d.id}): ${w}`);
@@ -1853,7 +2050,9 @@ function runTabs(appDir, opts = {}) {
1853
2050
  console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
1854
2051
  }
1855
2052
  // 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
1856
- const navHost = buildNavHost(bundleId, tabs, detailViews);
2053
+ let navHost = buildNavHost(bundleId, tabs, detailViews, { tabBar: opts.tabBar });
2054
+ if (opts.nativeNavigationBar === false)
2055
+ navHost = navHost.replace(/ topBar = \{[\s\S]*? \},\n bottomBar =/, ' bottomBar =');
1857
2056
  writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
1858
2057
  // P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
1859
2058
  const metaDir = join(outDir, '.vrn');
@@ -1872,7 +2071,7 @@ function runTabs(appDir, opts = {}) {
1872
2071
  console.log(`[gen-android] 完成: ${outDir}(${viewFiles.length} tab + ${detailViews.length} detail)`);
1873
2072
  return outDir;
1874
2073
  }
1875
- function buildNavHost(bundleId, tabs, details) {
2074
+ function buildNavHost(bundleId, tabs, details, options = {}) {
1876
2075
  // R-CHROME:tab 顶/底栏由 tab 路由自己的 TabScaffold 承载(而非容器层按 currentRoute 开关)。
1877
2076
  // 容器层开关的缺陷:导航瞬间 currentRoute 已变 detail,退出动画中的 tab 页仍在绘制,
1878
2077
  // 其顶/底栏立即消失、innerPadding 塌缩 → 内容瞬时顶到系统栏,随后详情页才滑入(视觉跳变)。
@@ -1931,10 +2130,33 @@ function buildNavHost(bundleId, tabs, details) {
1931
2130
  )
1932
2131
  )`)
1933
2132
  .join('\n ');
2133
+ let customBar = '';
2134
+ if (options.tabBar) {
2135
+ const spec = options.tabBar;
2136
+ const cells = tabs.map((tab) => {
2137
+ const item = spec.items.find((item) => item.id === tab.id);
2138
+ const tint = `if (currentTab == "${tab.id}") ${colorExpr(spec.activeStyle.color ?? '#f3dca0')} else ${colorExpr(spec.itemStyle.color ?? '#5b647e')}`;
2139
+ 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)`;
2140
+ 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}`;
2141
+ }).join('\n');
2142
+ const chain = modChain([...clipModifiers(spec.style), ...visualModifiers(spec.style), ...borderModifiers(spec.style), ...paddingModifiers(spec.style)], ' ');
2143
+ 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}`;
2144
+ }
1934
2145
  const tabTitleExpr = `when (currentTab) {\n${tabs.map((t) => ` "${t.id}" -> ${kotlinStr(t.title)}`).join('\n')}\n else -> ""\n }`;
1935
2146
  return `package ${bundleId}
1936
2147
 
1937
2148
  import androidx.compose.foundation.layout.*
2149
+ import androidx.compose.foundation.Image
2150
+ import androidx.compose.foundation.background
2151
+ import androidx.compose.foundation.border
2152
+ import androidx.compose.foundation.clickable
2153
+ import androidx.compose.ui.draw.clip
2154
+ import androidx.compose.ui.draw.shadow
2155
+ import androidx.compose.ui.draw.drawBehind
2156
+ import androidx.compose.ui.geometry.Offset
2157
+ import androidx.compose.ui.graphics.drawscope.withTransform
2158
+ import androidx.compose.foundation.shape.RoundedCornerShape
2159
+ import androidx.compose.ui.unit.dp
1938
2160
  import androidx.compose.material.icons.Icons
1939
2161
  import androidx.compose.material.icons.automirrored.filled.*
1940
2162
  import androidx.compose.material.icons.filled.*
@@ -1942,8 +2164,14 @@ import androidx.compose.material3.*
1942
2164
  import androidx.compose.runtime.*
1943
2165
  import androidx.compose.ui.Alignment
1944
2166
  import androidx.compose.ui.Modifier
2167
+ import androidx.compose.ui.zIndex
1945
2168
  import androidx.compose.ui.graphics.Color
2169
+ import androidx.compose.ui.graphics.asComposePath
2170
+ import androidx.compose.ui.graphics.nativeCanvas
2171
+ import androidx.compose.ui.graphics.toArgb
2172
+ import androidx.compose.ui.graphics.asImageBitmap
1946
2173
  import androidx.compose.ui.text.font.FontWeight
2174
+ import androidx.compose.ui.text.withStyle
1947
2175
  import androidx.compose.ui.text.style.TextAlign
1948
2176
  import androidx.compose.ui.unit.sp
1949
2177
  import androidx.navigation.compose.*
@@ -1967,9 +2195,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
1967
2195
  )
1968
2196
  },
1969
2197
  bottomBar = {
1970
- NavigationBar(containerColor = InkBar) {
1971
- ${barItems}
1972
- }
2198
+ ${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
1973
2199
  },
1974
2200
  ) { innerPadding ->
1975
2201
  content(innerPadding)
@@ -1982,7 +2208,13 @@ fun AppNavHost() {
1982
2208
  val navController = rememberNavController()
1983
2209
  // A5 路由桥:Runtime.push(route) → NavHost navigate
1984
2210
  LaunchedEffect(Unit) {
1985
- Runtime.pushHandler = { route -> navController.navigate(route) }
2211
+ Runtime.pushHandler = { route ->
2212
+ val resolved = when(route) {
2213
+ ${details.map((d) => ` "${d.id}" -> "${d.path}"`).join('\n')}
2214
+ else -> route
2215
+ }
2216
+ navController.navigate(resolved)
2217
+ }
1986
2218
  }
1987
2219
  // tab 切换:popUpTo 首 tab 保留栈底 + saveState/restoreState 保留各页状态(对位 iOS 常驻页面实例)
1988
2220
  val onSelectTab: (String) -> Unit = { tabId ->