@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.
- package/dist/codegen/bindings.d.ts +11 -0
- package/dist/codegen/bindings.js +32 -0
- package/dist/codegen/compose.d.ts +10 -1
- package/dist/codegen/compose.js +304 -65
- package/dist/codegen/dynamic-vector.d.ts +3 -0
- package/dist/codegen/dynamic-vector.js +81 -0
- package/dist/codegen/page-js-android.d.ts +2 -0
- package/dist/codegen/page-js-android.js +59 -0
- package/dist/codegen/project-assets.d.ts +5 -0
- package/dist/codegen/project-assets.js +31 -0
- package/dist/codegen/project-styles.d.ts +2 -0
- package/dist/codegen/project-styles.js +41 -0
- package/dist/codegen/swiftui.d.ts +4 -25
- package/dist/codegen/swiftui.js +251 -71
- package/dist/codegen/vector.d.ts +3 -0
- package/dist/codegen/vector.js +165 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/dist/ir/schema.d.ts +41 -1
- package/dist/ir/schema.js +39 -20
- package/dist/transform/css.d.ts +21 -1
- package/dist/transform/css.js +217 -14
- package/dist/transform/dynamic-svg.d.ts +3 -0
- package/dist/transform/dynamic-svg.js +53 -0
- package/dist/transform/svg.d.ts +16 -0
- package/dist/transform/svg.js +96 -0
- package/dist/transform/tab-bar.d.ts +17 -0
- package/dist/transform/tab-bar.js +44 -0
- package/dist/transform/visuals.d.ts +17 -0
- package/dist/transform/visuals.js +91 -0
- package/dist/transform/vue2ir.d.ts +12 -1
- package/dist/transform/vue2ir.js +584 -188
- package/package.json +4 -2
package/dist/codegen/compose.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1156
|
-
|
|
1157
|
-
|
|
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.
|
|
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 '${' +
|
|
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 = '"${' +
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
|
1304
|
-
out += pad + '
|
|
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
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
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 ->
|
|
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 ->
|