@dongmingzhixiu/vuexir 0.5.5 → 0.5.7

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.
@@ -27,10 +27,13 @@ export function generateAndroid(opts = {}) {
27
27
  if (/\.(ts|js|mts)$/.test(input) && existsSync(input)) {
28
28
  return runTabs(dirname(input), { routerFile: input, outDirRoot: opts.outDirRoot, projectName: opts.projectName, bundleId: opts.bundleId, renderGradle: (p, b) => buildGradle(b), templateVersion: TEMPLATE_VERSION });
29
29
  }
30
- if (!existsSync(join(input, 'router.ts'))) {
31
- throw new Error(`[vrn] ${input} 缺少 router.ts(多页模式需要 router.ts;单页请用 gen-ios 路径或补 router.ts)`);
30
+ // 目录入口:兼容 router.ts / index.ts(Vue 工程标准 src/router/index.ts)
31
+ const routerFile = ['router.ts', 'router.js', 'index.ts', 'index.js'].find((f) => existsSync(join(input, f)));
32
+ if (!routerFile) {
33
+ throw new Error(`[vrn] ${input} 缺少 router.ts/index.ts(多页模式需要路由文件;单页请用 gen-ios 路径或补 router.ts)`);
32
34
  }
33
35
  return runTabs(input, {
36
+ routerFile: join(input, routerFile),
34
37
  outDirRoot: opts.outDirRoot,
35
38
  projectName: opts.projectName,
36
39
  bundleId: opts.bundleId,
package/dist/gen-ios.js CHANGED
@@ -228,7 +228,14 @@ const RUNTIME_SWIFT = `final class Runtime {
228
228
 
229
229
  /// M3 注册 @State 绑定:JS stateStore.<name> 写回 → <setter>(onAppear 时由 View 注册)
230
230
  func bindState(_ name: String, _ setter: @escaping (Any) -> Void) {
231
- stateBindings[name] = setter
231
+ // R-THEME:ui_skin 变化 → ThemeColor.currentSkin + UserDefaults 同步 → 各视图 @AppStorage("vrn.skin") 自动重绘换肤
232
+ stateBindings[name] = { v in
233
+ setter(v)
234
+ if name == "ui_skin" || name == "ui.skin", let s = v as? String {
235
+ ThemeColor.currentSkin = s
236
+ UserDefaults.standard.set(s, forKey: "vrn.skin")
237
+ }
238
+ }
232
239
  }
233
240
 
234
241
  /// M3 Swift → JS 推送:@State 变更(v-model 输出等)→ stateStore 同步(__fromSwift 防回环)
@@ -293,18 +300,18 @@ struct ${projectName}App: App {
293
300
  }
294
301
  }
295
302
 
296
- /// R68:全局 UINavigationBar 深色外观(背景 #0a0f1e、浅色标题、金色 tint)
303
+ /// R68:全局 UINavigationBar 外观(R-THEME:bg/ink/primary 走 ThemeColor 皮肤表)
297
304
  func configureGlobalNavigationBar() {
298
305
  let appearance = UINavigationBarAppearance()
299
306
  appearance.configureWithOpaqueBackground()
300
- appearance.backgroundColor = UIColor(red: 10/255.0, green: 15/255.0, blue: 30/255.0, alpha: 1)
301
- let titleColor = UIColor(red: 233/255.0, green: 230/255.0, blue: 221/255.0, alpha: 1)
307
+ appearance.backgroundColor = UIColor(ThemeColor.color("bg"))
308
+ let titleColor = UIColor(ThemeColor.color("ink"))
302
309
  appearance.titleTextAttributes = [.foregroundColor: titleColor]
303
310
  appearance.largeTitleTextAttributes = [.foregroundColor: titleColor]
304
311
  UINavigationBar.appearance().standardAppearance = appearance
305
312
  UINavigationBar.appearance().scrollEdgeAppearance = appearance
306
313
  UINavigationBar.appearance().compactAppearance = appearance
307
- UINavigationBar.appearance().tintColor = UIColor(red: 217/255.0, green: 179/255.0, blue: 108/255.0, alpha: 1)
314
+ UINavigationBar.appearance().tintColor = UIColor(ThemeColor.color("primary"))
308
315
  }
309
316
  `;
310
317
  /** 屏/工程名 → PascalCase(my-app → MyApp;用于 Swift 类型名与 bundle id 派生) */
@@ -508,6 +515,8 @@ function runOne(input) {
508
515
  const theme = buildTheme(themeTokens);
509
516
  const themeSrc = generateThemeSource(theme);
510
517
  const cssVars = loadCssVars(input);
518
+ // R-THEME:皮肤 token 表(var(--x) 按皮肤分组 → ThemeColors.swift,换肤原生跟随)
519
+ writeFileSync(join(srcDir, 'ThemeColors.swift'), buildThemeColorsSwift(loadSkinVars(input)));
511
520
  const swift = generateScreenSource(screen, { warnings: genWarnings, theme, cssVars });
512
521
  if (genWarnings.length > 0) {
513
522
  console.log(`[gen-ios] 布局降级提示(${genWarnings.length} 条,R-H3/H4/H5):`);
@@ -518,7 +527,7 @@ function runOne(input) {
518
527
  writeFileSync(join(srcDir, 'App.swift'), APP_SWIFT(projectName, viewName, screen.title));
519
528
  writeFileSync(join(srcDir, 'recite.json'), JSON.stringify(screen, null, 2));
520
529
  // 事件运行时(<script setup> 逻辑源码;无逻辑时生成空文件保持 Bundle 引用成立)
521
- writeFileSync(join(srcDir, 'runtime.js'), runtimeSrc ?? '');
530
+ writeFileSync(join(srcDir, 'runtime.js'), injectSkinBridge(runtimeSrc ?? ''));
522
531
  // R41 组件:每个 components/Xxx.vue → 独立 XxxView.swift(struct XxxView;页面引用 XxxView(attrs))
523
532
  const compFiles = [];
524
533
  for (const comp of components) {
@@ -599,11 +608,11 @@ ${tabs
599
608
  .join('\n')}
600
609
  }
601
610
  .frame(height: 56)
602
- .background(Color(red: 16/255.0, green: 24/255.0, blue: 40/255.0))
611
+ .background(ThemeColor.color("surface-2"))
603
612
  .overlay(Divider(), alignment: .top)
604
613
  }
605
- // R68:容器背景兜底——页面内容不足或底部安全区不露白
606
- .background(${swiftColorExpr(rootBg)})
614
+ // R68:容器背景兜底——页面内容不足或底部安全区不露白(R-THEME:默认深色 → ThemeColor 皮肤 bg)
615
+ .background(${rootBg === '#0a0f1e' || rootBg === '#0a0F1E' || !rootBg ? 'ThemeColor.color("bg")' : swiftColorExpr(rootBg)})
607
616
  ${details.length > 0
608
617
  ? ` .navigationDestination(item: $pushedRoute) { route in
609
618
  Group {
@@ -648,18 +657,18 @@ struct ${projectName}App: App {
648
657
  }
649
658
  }
650
659
 
651
- /// R68:全局 UINavigationBar 深色外观(背景 #0a0f1e、浅色标题、金色 tint)
660
+ /// R68:全局 UINavigationBar 外观(R-THEME:bg/ink/primary 走 ThemeColor 皮肤表)
652
661
  func configureGlobalNavigationBar() {
653
662
  let appearance = UINavigationBarAppearance()
654
663
  appearance.configureWithOpaqueBackground()
655
- appearance.backgroundColor = UIColor(red: 10/255.0, green: 15/255.0, blue: 30/255.0, alpha: 1)
656
- let titleColor = UIColor(red: 233/255.0, green: 230/255.0, blue: 221/255.0, alpha: 1)
664
+ appearance.backgroundColor = UIColor(ThemeColor.color("bg"))
665
+ let titleColor = UIColor(ThemeColor.color("ink"))
657
666
  appearance.titleTextAttributes = [.foregroundColor: titleColor]
658
667
  appearance.largeTitleTextAttributes = [.foregroundColor: titleColor]
659
668
  UINavigationBar.appearance().standardAppearance = appearance
660
669
  UINavigationBar.appearance().scrollEdgeAppearance = appearance
661
670
  UINavigationBar.appearance().compactAppearance = appearance
662
- UINavigationBar.appearance().tintColor = UIColor(red: 217/255.0, green: 179/255.0, blue: 108/255.0, alpha: 1)
671
+ UINavigationBar.appearance().tintColor = UIColor(ThemeColor.color("primary"))
663
672
  }
664
673
  `;
665
674
  /** 计算页面 state 在全部 state 平铺数组中的下标(TAB_APP_SWIFT 生成 @State 命名 p<i>_<name> 用) */
@@ -689,6 +698,123 @@ function loadCssVars(appDir) {
689
698
  }
690
699
  return map;
691
700
  }
701
+ /**
702
+ * R-THEME:JS 侧换肤桥——页面 bundle 的 ui.setSkin(skin) 调用后,
703
+ * 把皮肤广播进 stateStore.ui_skin → Proxy set → __setState → Swift bindState → ThemeColor.currentSkin → 原生换肤
704
+ * 注入位置:esbuild 产物中 ui store 的 setSkin 方法体开头(stateStore 在 RUNTIME 模板段,函数被调用时已就绪)
705
+ */
706
+ function injectSkinBridge(js) {
707
+ if (!js || js.indexOf('setSkin') === -1)
708
+ return js;
709
+ return js.replace(/setSkin\s*\(\s*skin\s*\)\s*\{/g, 'setSkin(skin) { if(typeof stateStore!=="undefined"&&stateStore){try{stateStore["ui_skin"]=skin;}catch(e){}}');
710
+ }
711
+ /**
712
+ * R-THEME:按皮肤分组解析 tokens.css 的 var(--x)
713
+ * 选择器 → 皮肤名::root/.skin-warm → warm;.skin-mint → mint;.skin-lavender → lavender;.skin-night → night
714
+ * 返回 Map<skin, Map<varName, value>>;:root 覆盖块(半径/字体等)并入 warm
715
+ */
716
+ function loadSkinVars(appDir) {
717
+ const root = findProjectRoot(appDir);
718
+ const skins = new Map();
719
+ const roots = [join(root, 'src/styles/tokens.css'), join(root, 'src/tokens.css'), join(root, 'styles/tokens.css')];
720
+ for (const p of roots) {
721
+ if (!existsSync(p))
722
+ continue;
723
+ const css = readFileSync(p, 'utf-8');
724
+ // 按 } 切块,取块头选择器判断皮肤归属
725
+ for (const block of css.split('}')) {
726
+ const selLine = block.split('{')[0] ?? '';
727
+ let skin = '';
728
+ if (/\.skin-warm/.test(selLine) || /:root/.test(selLine))
729
+ skin = 'warm';
730
+ else if (/\.skin-mint/.test(selLine))
731
+ skin = 'mint';
732
+ else if (/\.skin-lavender/.test(selLine))
733
+ skin = 'lavender';
734
+ else if (/\.skin-night/.test(selLine))
735
+ skin = 'night';
736
+ if (!skin)
737
+ continue;
738
+ const body = block.slice(block.indexOf('{') + 1);
739
+ let map = skins.get(skin);
740
+ if (!map) {
741
+ map = new Map();
742
+ skins.set(skin, map);
743
+ }
744
+ for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;{}]+);/g)) {
745
+ map.set(m[1], m[2].trim());
746
+ }
747
+ }
748
+ }
749
+ return skins;
750
+ }
751
+ /** var 值 → Swift Color 字面量(hex/rgb/rgba;非颜色返回 null) */
752
+ function cssValueToSwiftColor(v) {
753
+ const t = v.trim();
754
+ if (t.startsWith('#')) {
755
+ let hex = t.slice(1);
756
+ if (hex.length === 3)
757
+ hex = hex.split('').map((h) => h + h).join('');
758
+ const n = parseInt(hex, 16);
759
+ return `Color(red: ${(n >> 16) & 255}/255.0, green: ${(n >> 8) & 255}/255.0, blue: ${n & 255}/255.0)`;
760
+ }
761
+ const m = t.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
762
+ if (m) {
763
+ const a = m[4] !== undefined ? +m[4] : 1;
764
+ return `Color(red: ${+m[1]}/255.0, green: ${+m[2]}/255.0, blue: ${+m[3]}/255.0, opacity: ${a})`;
765
+ }
766
+ if (/^rgba?\(\s*[\d.]+\s*[,\s]/.test(t))
767
+ return null;
768
+ if (t.startsWith('var(') || /^[a-z]+$/i.test(t))
769
+ return null;
770
+ return null;
771
+ }
772
+ /** 生成 ThemeColors.swift:4 皮肤 token 表 + currentSkin + color(name) 解析 */
773
+ function buildThemeColorsSwift(skins) {
774
+ const order = ['warm', 'mint', 'lavender', 'night'];
775
+ const lines = [];
776
+ lines.push('import SwiftUI');
777
+ lines.push('');
778
+ lines.push('/// Vuexir 皮肤 token 表:由 tokens.css 的 var(--x) 按皮肤分组生成');
779
+ lines.push('/// 换肤:JS ui.setSkin → stateStore.ui_skin → __setState → ThemeColor.currentSkin → 视图重建自动取新色');
780
+ lines.push('enum ThemeColor {');
781
+ lines.push(' static var currentSkin = UserDefaults.standard.string(forKey: "vrn.skin") ?? "warm"');
782
+ lines.push(' static func color(_ name: String) -> Color {');
783
+ lines.push(' let skin = UserDefaults.standard.string(forKey: "vrn.skin") ?? currentSkin');
784
+ lines.push(' switch skin {');
785
+ const names = new Set();
786
+ for (const skin of order)
787
+ names.add(skin);
788
+ for (const skin of order) {
789
+ const map = skins.get(skin);
790
+ if (!map)
791
+ continue;
792
+ const ref = skin === 'warm' ? 'warmMap' : `${skin}Map`;
793
+ const fallback = skin === 'warm' ? 'Color.black' : 'warmMap[name] ?? Color.black';
794
+ lines.push(` case "${skin}": return ${ref}[name] ?? ${fallback}`);
795
+ }
796
+ lines.push(' default: return warmMap[name] ?? Color.black');
797
+ lines.push(' }');
798
+ lines.push(' }');
799
+ for (const skin of order) {
800
+ const map = skins.get(skin);
801
+ if (!map)
802
+ continue;
803
+ const entries = [];
804
+ for (const [k, v] of map) {
805
+ const c = cssValueToSwiftColor(v);
806
+ if (c)
807
+ entries.push(` "${k.slice(2)}": ${c}`);
808
+ }
809
+ if (entries.length === 0)
810
+ continue;
811
+ lines.push(` private static let ${skin === 'warm' ? 'warmMap' : `${skin}Map`}: [String: Color] = [`);
812
+ lines.push(entries.join(',\n'));
813
+ lines.push(' ]');
814
+ }
815
+ lines.push('}');
816
+ return lines.join('\n') + '\n';
817
+ }
692
818
  function runTabs(appDir, opts = {}) {
693
819
  const routerSrc = readFileSync(opts.routerFile ?? join(appDir, 'router.ts'), 'utf-8');
694
820
  const { tabs, details, warnings: routerWarnings } = parseRouterV2(routerSrc, findProjectRoot(appDir), opts.routerFile ?? join(appDir, 'router.ts'));
@@ -696,6 +822,9 @@ function runTabs(appDir, opts = {}) {
696
822
  console.log(`[gen-ios] 路由提示: ${w}`);
697
823
  console.log(`[gen-ios] 多页模式: router.ts → ${tabs.length} 个 tab 页面` + (details.length ? ` + ${details.length} 个 push 页面` : ''));
698
824
  const cssVars = loadCssVars(appDir);
825
+ // R-THEME:皮肤 token 表(var(--x) 按皮肤分组 → ThemeColors.swift,换肤原生跟随)
826
+ const skinVars = loadSkinVars(appDir);
827
+ const themeColorsSwift = buildThemeColorsSwift(skinVars);
699
828
  const projectName = FORCED_PROJECT_NAME ?? toIdentifier(basename(appDir));
700
829
  const bundleId = FORCED_BUNDLE_ID ?? `com.vrn.${projectName.toLowerCase()}`;
701
830
  const outDir = join(OUT_ROOT, projectName);
@@ -703,6 +832,7 @@ function runTabs(appDir, opts = {}) {
703
832
  // 写前清空源目录:删除页面后残留的旧 View/runtime 仍会进 Xcode 工程(幽灵页面)
704
833
  rmSync(srcDir, { recursive: true, force: true });
705
834
  mkdirSync(srcDir, { recursive: true });
835
+ writeFileSync(join(srcDir, 'ThemeColors.swift'), themeColorsSwift);
706
836
  // R97 P2:把可独立运行的 utils 打成 IIFE,前置到每个页面 runtime 之前,
707
837
  // 让 computed 里引用的外部函数(countChars / normalize …)在原生 JS 运行时可用
708
838
  const utilsBundle = bundleProjectUtils(findProjectRoot(appDir));
@@ -761,7 +891,7 @@ function runTabs(appDir, opts = {}) {
761
891
  const pageBundle = bundlePageScript(tab.file, findProjectRoot(appDir));
762
892
  if (pageBundle)
763
893
  console.log(`[gen-ios] 页面脚本打包:${tab.id}(${pageBundle.names.length} 个绑定)`);
764
- writeFileSync(join(srcDir, `runtime_${pageId}.js`), utilsBundle.code + '\n' + (pageBundle?.code ?? '') + '\n' + (r.runtime ?? ''));
894
+ writeFileSync(join(srcDir, `runtime_${pageId}.js`), injectSkinBridge(utilsBundle.code + '\n' + (pageBundle?.code ?? '') + '\n' + (r.runtime ?? '')));
765
895
  pageFiles.push(`${pageId}View.swift`);
766
896
  pageRuntimes.push({ id: pageId, src: r.runtime ?? '' });
767
897
  pageStates.push({
@@ -821,7 +951,7 @@ function runTabs(appDir, opts = {}) {
821
951
  for (const w of genWarnings)
822
952
  console.log(`[gen-ios] 生成提示(${d.id},push 页): ${w}`);
823
953
  writeFileSync(join(srcDir, viewFile), swift.replace(/Text\("(_vrn[EAC][0-9]+)"\)/g, 'Text($1)'));
824
- writeFileSync(join(srcDir, `runtime_${pageId}Detail.js`), utilsBundle.code + '\n' + (r.runtime ?? ''));
954
+ writeFileSync(join(srcDir, `runtime_${pageId}Detail.js`), injectSkinBridge(utilsBundle.code + '\n' + (r.runtime ?? '')));
825
955
  pageFiles.push(viewFile);
826
956
  pageRuntimes.push({ id: `${pageId}Detail`, src: r.runtime ?? '' });
827
957
  detailPages.push({ id: d.id, title: d.title, view: viewName, path: d.path });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "vrn 命令行:build ios|android、preview、doctor",
5
5
  "type": "module",
6
6
  "license": "MIT",