@dongmingzhixiu/vuexir-compiler 0.7.7 → 0.7.9
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/compose.d.ts +1 -0
- package/dist/codegen/compose.js +40 -21
- package/dist/codegen/swiftui.js +12 -6
- package/dist/ir/schema.d.ts +1 -1
- package/dist/ir/schema.js +1 -1
- package/dist/transform/css.js +1 -1
- package/dist/transform/vue2ir.js +18 -4
- package/package.json +1 -1
package/dist/codegen/compose.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
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 { NodeTypes } from '@vue/compiler-core';
|
|
15
|
+
import { parse } from '@vue/compiler-sfc';
|
|
14
16
|
import { loadProjectAssets, materializeProjectAssets } from './project-assets.js';
|
|
15
17
|
import { extractCssVariables } from '../transform/css.js';
|
|
16
18
|
import { loadProjectStyles } from './project-styles.js';
|
|
@@ -476,8 +478,16 @@ function dp(n) {
|
|
|
476
478
|
function sp(n) {
|
|
477
479
|
return `${n}.sp`;
|
|
478
480
|
}
|
|
481
|
+
function kotlinStringContents(value) {
|
|
482
|
+
return value
|
|
483
|
+
.replace(/\\/g, '\\\\')
|
|
484
|
+
.replace(/"/g, '\\"')
|
|
485
|
+
.replace(/\$/g, () => '\\$')
|
|
486
|
+
.replace(/\n/g, '\\n')
|
|
487
|
+
.replace(/\r/g, '\\r');
|
|
488
|
+
}
|
|
479
489
|
function kotlinStr(s) {
|
|
480
|
-
return `"${s
|
|
490
|
+
return `"${kotlinStringContents(s)}"`;
|
|
481
491
|
}
|
|
482
492
|
/** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
|
|
483
493
|
/** R114:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
|
|
@@ -1381,7 +1391,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1381
1391
|
const segs = p.bindParts
|
|
1382
1392
|
.map((seg) => {
|
|
1383
1393
|
if ('text' in seg)
|
|
1384
|
-
return String(seg.text)
|
|
1394
|
+
return kotlinStringContents(String(seg.text));
|
|
1385
1395
|
if ('bind' in seg)
|
|
1386
1396
|
return '${' + safeBind(seg.bind, ctx, loop) + '}';
|
|
1387
1397
|
if ('fieldOf' in seg)
|
|
@@ -1519,17 +1529,18 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1519
1529
|
}
|
|
1520
1530
|
case 'image': {
|
|
1521
1531
|
const source = p.src ?? '';
|
|
1532
|
+
const contentScale = p.objectFit === 'fill' ? 'FillBounds' : p.objectFit === 'contain' ? 'Fit' : 'Crop';
|
|
1522
1533
|
const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...paddingModifiers(p), ...alphaArr], pad + ' ');
|
|
1523
1534
|
if (source.startsWith('asset://')) {
|
|
1524
1535
|
const name = source.slice(8);
|
|
1525
|
-
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.' +
|
|
1536
|
+
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.' + contentScale + ', modifier = Modifier' + chain + ') }';
|
|
1526
1537
|
}
|
|
1527
1538
|
if (/^data:image\/[^;]+;base64,/.test(source)) {
|
|
1528
1539
|
const encoded = source.slice(source.indexOf(',') + 1);
|
|
1529
1540
|
return pad + 'run {\n' + pad + ' val imageBytes = remember { android.util.Base64.decode(' + kotlinStr(encoded) + ', android.util.Base64.DEFAULT) }\n' +
|
|
1530
1541
|
pad + ' val bitmap = remember { android.graphics.BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) }\n' +
|
|
1531
1542
|
pad + ' if (bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' +
|
|
1532
|
-
|
|
1543
|
+
contentScale + ', modifier = Modifier' + chain + ')\n' + pad + '}';
|
|
1533
1544
|
}
|
|
1534
1545
|
ctx.warnings.push(`图片 ${source.slice(0, 80)} 需要 data URI 资源映射,Android 未生成图片内容`);
|
|
1535
1546
|
return pad + 'Box(modifier = Modifier' + chain + ')';
|
|
@@ -1755,10 +1766,10 @@ import androidx.compose.ui.unit.dp
|
|
|
1755
1766
|
import androidx.compose.ui.unit.sp
|
|
1756
1767
|
`;
|
|
1757
1768
|
const TAB_ICONS = ['home.fill', 'book.fill', 'square.grid.2x2.fill', 'bell.fill', 'gearshape.fill', 'explore', 'work'];
|
|
1758
|
-
/**
|
|
1769
|
+
/** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
|
|
1759
1770
|
function tabLabel(title) {
|
|
1760
|
-
const
|
|
1761
|
-
return
|
|
1771
|
+
const compact = title.replace(/\s+/g, '');
|
|
1772
|
+
return /^\p{Script=Han}+$/u.test(compact) ? compact.match(/.{1,2}/gu).join('\n') : title;
|
|
1762
1773
|
}
|
|
1763
1774
|
/**
|
|
1764
1775
|
* 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
|
|
@@ -1878,14 +1889,20 @@ function parseRouter(src, dir, routerFile) {
|
|
|
1878
1889
|
meta,
|
|
1879
1890
|
});
|
|
1880
1891
|
}
|
|
1881
|
-
const
|
|
1892
|
+
const hasRenderableNode = (node) => {
|
|
1893
|
+
if (node.type === NodeTypes.ELEMENT) {
|
|
1894
|
+
return node.tag !== 'template' || node.children.some(hasRenderableNode);
|
|
1895
|
+
}
|
|
1896
|
+
return node.type === NodeTypes.INTERPOLATION || (node.type === NodeTypes.TEXT && node.content.trim() !== '');
|
|
1897
|
+
};
|
|
1898
|
+
const hasRenderableTemplate = (file) => {
|
|
1882
1899
|
if (!file || !existsSync(file))
|
|
1883
1900
|
return false;
|
|
1884
|
-
const
|
|
1885
|
-
return
|
|
1901
|
+
const { descriptor } = parse(readFileSync(file, 'utf-8'), { filename: file });
|
|
1902
|
+
return descriptor.template?.ast?.children.some(hasRenderableNode) ?? false;
|
|
1886
1903
|
};
|
|
1887
1904
|
const tabs = routes
|
|
1888
|
-
.filter((r) => r.meta.tab === 'true' &&
|
|
1905
|
+
.filter((r) => r.meta.tab === 'true' && hasRenderableTemplate(r.file))
|
|
1889
1906
|
.map((r, i) => ({
|
|
1890
1907
|
id: r.name,
|
|
1891
1908
|
title: r.meta.title ?? r.name,
|
|
@@ -1894,7 +1911,7 @@ function parseRouter(src, dir, routerFile) {
|
|
|
1894
1911
|
path: r.path,
|
|
1895
1912
|
}));
|
|
1896
1913
|
const details = routes
|
|
1897
|
-
.filter((r) => r.meta.tab !== 'true' &&
|
|
1914
|
+
.filter((r) => r.meta.tab !== 'true' && hasRenderableTemplate(r.file))
|
|
1898
1915
|
.map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
|
|
1899
1916
|
if (tabs.length === 0) {
|
|
1900
1917
|
warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
|
|
@@ -2083,9 +2100,10 @@ function runTabs(appDir, opts = {}) {
|
|
|
2083
2100
|
console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
|
|
2084
2101
|
}
|
|
2085
2102
|
// 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2103
|
+
const navHost = buildNavHost(bundleId, tabs, detailViews, {
|
|
2104
|
+
tabBar: opts.tabBar,
|
|
2105
|
+
nativeNavigationBar: opts.nativeNavigationBar,
|
|
2106
|
+
});
|
|
2089
2107
|
writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
|
|
2090
2108
|
// P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
|
|
2091
2109
|
const metaDir = join(outDir, '.vrn');
|
|
@@ -2112,6 +2130,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
|
|
|
2112
2130
|
.map((t) => `composable("${t.id}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }
|
|
2113
2131
|
composable("${t.path}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }`)
|
|
2114
2132
|
.join('\n ');
|
|
2133
|
+
const showNavigationBar = options.nativeNavigationBar !== false;
|
|
2115
2134
|
// M7 对位 iOS NavigationStack:详情页内层 Scaffold + TopAppBar(标题 + 返回箭头/backText、mode=transparent 透明背景)
|
|
2116
2135
|
const detailRoutes = details
|
|
2117
2136
|
.map((d) => {
|
|
@@ -2128,7 +2147,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
|
|
|
2128
2147
|
const barColor = d.mode === 'transparent' ? 'Color.Transparent' : d.bg ? colorExpr(d.bg) : 'InkBar';
|
|
2129
2148
|
return `composable("${d.path}") {
|
|
2130
2149
|
Scaffold(
|
|
2131
|
-
containerColor = InkBg
|
|
2150
|
+
containerColor = InkBg,${showNavigationBar ? `
|
|
2132
2151
|
topBar = {
|
|
2133
2152
|
TopAppBar(
|
|
2134
2153
|
title = { Text(${kotlinStr(d.title)}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
|
|
@@ -2141,7 +2160,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
|
|
|
2141
2160
|
titleContentColor = Gold,
|
|
2142
2161
|
),
|
|
2143
2162
|
)
|
|
2144
|
-
}
|
|
2163
|
+
},` : ''}
|
|
2145
2164
|
) { innerPadding ->
|
|
2146
2165
|
${toPascal(d.id)}DetailContent(modifier = Modifier.padding(innerPadding))
|
|
2147
2166
|
}
|
|
@@ -2152,8 +2171,8 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
|
|
|
2152
2171
|
.map((t) => `NavigationBarItem(
|
|
2153
2172
|
selected = currentTab == "${t.id}",
|
|
2154
2173
|
onClick = { onSelectTab("${t.id}") },
|
|
2155
|
-
icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription =
|
|
2156
|
-
label = { Text(
|
|
2174
|
+
icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = ${kotlinStr(t.title)}) },
|
|
2175
|
+
label = { Text(${kotlinStr(tabLabel(t.title))}, fontSize = 10.sp, textAlign = TextAlign.Center) },
|
|
2157
2176
|
colors = NavigationBarItemDefaults.colors(
|
|
2158
2177
|
selectedIconColor = Gold,
|
|
2159
2178
|
selectedTextColor = Gold,
|
|
@@ -2217,7 +2236,7 @@ import androidx.navigation.compose.*
|
|
|
2217
2236
|
@Composable
|
|
2218
2237
|
fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Composable (PaddingValues) -> Unit) {
|
|
2219
2238
|
Scaffold(
|
|
2220
|
-
containerColor = InkBg
|
|
2239
|
+
containerColor = InkBg,${showNavigationBar ? `
|
|
2221
2240
|
topBar = {
|
|
2222
2241
|
TopAppBar(
|
|
2223
2242
|
title = { Text(${tabTitleExpr}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
|
|
@@ -2226,7 +2245,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
|
|
|
2226
2245
|
titleContentColor = Gold,
|
|
2227
2246
|
),
|
|
2228
2247
|
)
|
|
2229
|
-
}
|
|
2248
|
+
},` : ''}
|
|
2230
2249
|
bottomBar = {
|
|
2231
2250
|
${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
|
|
2232
2251
|
},
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -346,7 +346,7 @@ function gapInheritFor(child, parentGap, siblings) {
|
|
|
346
346
|
return parentGap;
|
|
347
347
|
}
|
|
348
348
|
/* ============ 节点发射 ============ */
|
|
349
|
-
function sizeModifiers(props, axis, ctx) {
|
|
349
|
+
function sizeModifiers(props, axis, ctx, fixedFrameAlignment) {
|
|
350
350
|
const mods = [];
|
|
351
351
|
const { width, height, flex } = props;
|
|
352
352
|
if (props.widthDynamic)
|
|
@@ -356,11 +356,11 @@ function sizeModifiers(props, axis, ctx) {
|
|
|
356
356
|
if (width === 'fill')
|
|
357
357
|
mods.push(frameExpr('fillW'));
|
|
358
358
|
else if (typeof width === 'number')
|
|
359
|
-
mods.push(`.frame(width: ${width})`);
|
|
359
|
+
mods.push(`.frame(width: ${width}${fixedFrameAlignment})`);
|
|
360
360
|
if (height === 'fill')
|
|
361
361
|
mods.push('.frame(maxHeight: .infinity)');
|
|
362
362
|
if (typeof height === 'number')
|
|
363
|
-
mods.push(`.frame(height: ${height})`);
|
|
363
|
+
mods.push(`.frame(height: ${height}${fixedFrameAlignment})`);
|
|
364
364
|
if (flex !== undefined) {
|
|
365
365
|
// R-H3:flex 沿父容器主轴拉伸占满剩余空间;但父容器高度不确定时(auto 链)无剩余空间可分配,
|
|
366
366
|
// SwiftUI 的 .infinity 会沿祖先链漂移到屏幕高度 → 降级为内容高并告警(与 CSS flex-grow 语义一致)
|
|
@@ -707,10 +707,14 @@ function emitNodeInner(node, ctx) {
|
|
|
707
707
|
return pad(`VrnFractionWidth(fraction: max(0,min(1,${fraction}))) {`) + `\n${inner}\n${pad('}')}`;
|
|
708
708
|
}
|
|
709
709
|
const axis = ctx.parentAxis ?? 'column';
|
|
710
|
+
// 绝对定位偏移以固定容器的左上角为原点。SwiftUI 默认会居中放置偏移前的 ZStack。
|
|
711
|
+
const fixedFrameAlignment = node.type === 'group' && !props.flexWrap && props.gridColumns === undefined &&
|
|
712
|
+
node.children?.length && node.children.every((child) => child.props?.position === 'absolute')
|
|
713
|
+
? ', alignment: .topLeading' : '';
|
|
710
714
|
// box-sizing:border-box(CSS 默认):固定 height 应包含 padding。
|
|
711
715
|
// 故 height 为数字且存在 padding 时,padding 置于 frame(height) 之前、
|
|
712
716
|
// frame 限定总视觉高度(否则 padding 加在 frame 外 → 视觉高 = height + 2×padding)。
|
|
713
|
-
const size = sizeModifiers(props, axis, ctx);
|
|
717
|
+
const size = sizeModifiers(props, axis, ctx, fixedFrameAlignment);
|
|
714
718
|
let padMods = paddingModifiers(props);
|
|
715
719
|
let boxMods = boxModifiers(props);
|
|
716
720
|
if (node.type === 'image' && props.objectFit === 'cover' && !props.clip) {
|
|
@@ -723,7 +727,7 @@ function emitNodeInner(node, ctx) {
|
|
|
723
727
|
boxMods = boxMods.filter((m) => !m.startsWith('.background(') && !m.startsWith('.cornerRadius(') && !m.startsWith('.overlay('));
|
|
724
728
|
}
|
|
725
729
|
const box = typeof props.height === 'number' && (padMods.length > 0 || capsule !== undefined)
|
|
726
|
-
? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height})`, ...boxMods]
|
|
730
|
+
? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height}${fixedFrameAlignment})`, ...boxMods]
|
|
727
731
|
: [...size, ...(capsule !== undefined ? [capsule] : []), ...padMods, ...boxMods];
|
|
728
732
|
// M5 transform:scale / rotate / translate → 原生修饰符(视觉变换,不影响布局占位)
|
|
729
733
|
const t = props.transform;
|
|
@@ -1288,7 +1292,9 @@ function emitNodeInner(node, ctx) {
|
|
|
1288
1292
|
else {
|
|
1289
1293
|
image = `Image(uiImage: UIImage(named: "${swiftStringEscape(source.replace(/^bundle:\/\//, ''))}") ?? UIImage()).resizable()`;
|
|
1290
1294
|
}
|
|
1291
|
-
|
|
1295
|
+
if (props.objectFit !== 'fill') {
|
|
1296
|
+
image += props.objectFit === 'contain' ? '.scaledToFit()' : '.scaledToFill()';
|
|
1297
|
+
}
|
|
1292
1298
|
return pad(image) + box.map((m) => ' ' + m).join('');
|
|
1293
1299
|
}
|
|
1294
1300
|
case 'component': {
|
package/dist/ir/schema.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type Justify = 'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround
|
|
|
17
17
|
export type Align = 'start' | 'center' | 'end' | 'stretch';
|
|
18
18
|
export type FontWeight = 'normal' | 'medium' | 'bold';
|
|
19
19
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
20
|
-
export type ObjectFit = 'cover' | 'contain';
|
|
20
|
+
export type ObjectFit = 'cover' | 'contain' | 'fill';
|
|
21
21
|
export interface IrProps {
|
|
22
22
|
/** 尺寸 */
|
|
23
23
|
width?: Size;
|
package/dist/ir/schema.js
CHANGED
|
@@ -101,7 +101,7 @@ const PROPS_VALIDATORS = {
|
|
|
101
101
|
lineHeight: (v) => typeof v === 'number' && v > 0,
|
|
102
102
|
color: isColor,
|
|
103
103
|
src: (v) => typeof v === 'string',
|
|
104
|
-
objectFit: (v) => v === 'cover' || v === 'contain',
|
|
104
|
+
objectFit: (v) => v === 'cover' || v === 'contain' || v === 'fill',
|
|
105
105
|
label: (v) => typeof v === 'string',
|
|
106
106
|
onClick: (v) => typeof v === 'string',
|
|
107
107
|
showsIndicators: (v) => typeof v === 'boolean',
|
package/dist/transform/css.js
CHANGED
|
@@ -122,7 +122,7 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
122
122
|
return []; // 变量声明由 token 解析器处理,不是未支持的视觉属性。
|
|
123
123
|
switch (prop) {
|
|
124
124
|
case 'object-fit':
|
|
125
|
-
return v === 'contain' || v === 'cover' ? [['objectFit', v]] : null;
|
|
125
|
+
return v === 'contain' || v === 'cover' || v === 'fill' ? [['objectFit', v]] : null;
|
|
126
126
|
case 'aspect-ratio': {
|
|
127
127
|
const parts = v.split('/').map((part) => Number(part.trim()));
|
|
128
128
|
const ratio = parts.length === 1 ? parts[0] : parts.length === 2 ? parts[0] / parts[1] : NaN;
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -2362,10 +2362,18 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2362
2362
|
const inlineTags = new Set(['span', 'b', 'strong', 'i', 'em', 'small', 'u', 'code', 'br']);
|
|
2363
2363
|
const content = node.children.filter((c) => c.type !== NodeTypes.COMMENT && !(c.type === NodeTypes.TEXT && !c.content.trim()));
|
|
2364
2364
|
const readable = (n) => n.type === NodeTypes.INTERPOLATION || (n.type === NodeTypes.TEXT && !!n.content.trim()) || (n.type === NodeTypes.ELEMENT && n.children.some(readable));
|
|
2365
|
-
|
|
2366
|
-
|
|
2365
|
+
// 行内分支或循环需要独立节点;合并为富文本会丢弃这些动态子树。
|
|
2366
|
+
const hasInlineStructure = (child) => {
|
|
2367
|
+
if (child.type !== NodeTypes.ELEMENT)
|
|
2368
|
+
return false;
|
|
2369
|
+
return child.props.some((p) => p.type === NodeTypes.DIRECTIVE &&
|
|
2370
|
+
['if', 'else-if', 'else', 'show', 'for'].includes(p.name)) ||
|
|
2371
|
+
(inlineTags.has(child.tag) && child.children.some(hasInlineStructure));
|
|
2372
|
+
};
|
|
2373
|
+
const inlineStructure = content.some(hasInlineStructure);
|
|
2374
|
+
if (inlineComponentsFlag && mapping.type === 'group' && !inlineStructure && props.flexDirection === undefined && props.gridColumns === undefined && content.some(readable) && content.every((c) => c.type === NodeTypes.TEXT || c.type === NodeTypes.INTERPOLATION || (c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))))
|
|
2367
2375
|
mapping = { type: 'text' };
|
|
2368
|
-
if (mapping.type === 'text' &&
|
|
2376
|
+
if (mapping.type === 'text' && inlineStructure)
|
|
2369
2377
|
mapping = { type: 'group' };
|
|
2370
2378
|
if (mapping.type === 'text' && content.length === 0 && ['i', 'span', 'b'].includes(node.tag))
|
|
2371
2379
|
mapping = { type: 'spacer' };
|
|
@@ -2680,7 +2688,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2680
2688
|
warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
|
|
2681
2689
|
const keep = {};
|
|
2682
2690
|
const src = props;
|
|
2683
|
-
|
|
2691
|
+
// 空盒仍需脱离文档流并保持叠放顺序,避免占位改变父容器布局。
|
|
2692
|
+
for (const k of [
|
|
2693
|
+
'width', 'height', 'widthFraction', 'widthFractionDynamic', 'flex', 'marginTop',
|
|
2694
|
+
'background', 'gradient', 'radius', 'shadow', 'borderWidth', 'borderColor',
|
|
2695
|
+
'padding', 'minHeight', 'minWidth', 'opacity',
|
|
2696
|
+
'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'zIndex',
|
|
2697
|
+
]) {
|
|
2684
2698
|
if (src[k] !== undefined)
|
|
2685
2699
|
keep[k] = src[k];
|
|
2686
2700
|
}
|