@dongmingzhixiu/vuexir-compiler 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/bindings.d.ts +11 -0
- package/dist/codegen/bindings.js +32 -0
- package/dist/codegen/compose.d.ts +10 -1
- package/dist/codegen/compose.js +297 -65
- package/dist/codegen/dynamic-vector.d.ts +3 -0
- package/dist/codegen/dynamic-vector.js +81 -0
- package/dist/codegen/page-js-android.d.ts +2 -0
- package/dist/codegen/page-js-android.js +59 -0
- package/dist/codegen/project-assets.d.ts +5 -0
- package/dist/codegen/project-assets.js +31 -0
- package/dist/codegen/project-styles.d.ts +2 -0
- package/dist/codegen/project-styles.js +41 -0
- package/dist/codegen/swiftui.d.ts +4 -25
- package/dist/codegen/swiftui.js +251 -71
- package/dist/codegen/vector.d.ts +3 -0
- package/dist/codegen/vector.js +165 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/dist/ir/schema.d.ts +41 -1
- package/dist/ir/schema.js +39 -20
- package/dist/transform/css.d.ts +21 -1
- package/dist/transform/css.js +217 -14
- package/dist/transform/dynamic-svg.d.ts +3 -0
- package/dist/transform/dynamic-svg.js +53 -0
- package/dist/transform/svg.d.ts +16 -0
- package/dist/transform/svg.js +96 -0
- package/dist/transform/tab-bar.d.ts +17 -0
- package/dist/transform/tab-bar.js +44 -0
- package/dist/transform/visuals.d.ts +17 -0
- package/dist/transform/visuals.js +91 -0
- package/dist/transform/vue2ir.d.ts +12 -1
- package/dist/transform/vue2ir.js +581 -188
- package/package.json +4 -2
package/dist/codegen/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,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
|
-
|
|
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
|
|
1156
|
-
|
|
1157
|
-
|
|
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.
|
|
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 '${' +
|
|
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 = '"${' +
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
|
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 + '}';
|
|
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
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
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 ->
|
|
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 ->
|