@axewo/fin-micro 1.0.0 → 2.0.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.
Files changed (103) hide show
  1. package/README.md +181 -4
  2. package/cli/index.mjs +22 -6
  3. package/dist/browser.cjs +1 -1
  4. package/dist/browser.mjs +68 -66
  5. package/dist/core.cjs +1 -1
  6. package/dist/core.mjs +459 -441
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.mjs +31 -24
  9. package/dist/layout.cjs +1 -0
  10. package/dist/layout.css +1 -0
  11. package/dist/layout.mjs +719 -0
  12. package/dist/{router-view-impl-C1KG6mxu.mjs → router-view-impl-B1NzCGKV.mjs} +18 -18
  13. package/dist/router-view-impl-BvOToV_Z.cjs +1 -0
  14. package/dist/runtime.cjs +1 -1
  15. package/dist/runtime.mjs +40 -39
  16. package/dist/sdk.cjs +1 -1
  17. package/dist/sdk.mjs +92 -56
  18. package/dist/types/browser/index.d.ts +4 -0
  19. package/dist/types/core/index.d.ts +1 -1
  20. package/dist/types/index.d.ts +2 -0
  21. package/dist/types/layout/HostShell.vue.d.ts +528 -0
  22. package/dist/types/layout/ShellHeader.vue.d.ts +46 -0
  23. package/dist/types/layout/ShellMenu.vue.d.ts +12 -0
  24. package/dist/types/layout/ShellTabs.vue.d.ts +23 -0
  25. package/dist/types/layout/StandaloneMenuItem.vue.d.ts +11 -0
  26. package/dist/types/layout/StandaloneShell.vue.d.ts +40 -0
  27. package/dist/types/layout/index.d.ts +5 -0
  28. package/dist/types/layout/types.d.ts +65 -0
  29. package/dist/types/runtime/lifecycle.d.ts +1 -1
  30. package/dist/types/sdk/index.d.ts +65 -4
  31. package/dist/types/vue/index.d.ts +13 -2
  32. package/dist/types/vue/router-view-impl.d.ts +1 -1
  33. package/dist/types/watermark/FinWatermark.vue.d.ts +16 -0
  34. package/dist/types/watermark/config.d.ts +36 -0
  35. package/dist/types/watermark/index.d.ts +4 -0
  36. package/dist/vue.cjs +1 -1
  37. package/dist/vue.mjs +49 -35
  38. package/dist/watermark.cjs +2 -0
  39. package/dist/watermark.mjs +119 -0
  40. package/package.json +29 -4
  41. package/scaffold/base/index.html +2 -1
  42. package/scaffold/base/package.json.template +5 -1
  43. package/scaffold/base/pnpm-workspace.yaml +6 -0
  44. package/scaffold/base/scripts/check-runtime.mjs +43 -0
  45. package/scaffold/base/src/locales/en.ts +12 -0
  46. package/scaffold/base/src/locales/index.ts +7 -1
  47. package/scaffold/base/src/locales/zh-CN.ts +12 -0
  48. package/scaffold/base/src/main.ts +13 -5
  49. package/scaffold/base/src/router/demo.ts +6 -0
  50. package/scaffold/base/src/router/index.ts +13 -0
  51. package/scaffold/base/src/stores/demo.ts +12 -0
  52. package/scaffold/base/src/stores/index.ts +11 -0
  53. package/scaffold/base/src/utils/appConfig.ts +49 -0
  54. package/scaffold/base/src/utils/errorUtils.ts +109 -0
  55. package/scaffold/base/src/utils/requestUtils.ts +32 -11
  56. package/scaffold/base/src/utils/watermarkConfig.ts +18 -0
  57. package/scaffold/base/src/views/demo/index.vue +26 -0
  58. package/scaffold/host/.env.example +11 -3
  59. package/scaffold/host/src/App.vue +1 -13
  60. package/scaffold/host/src/bootstrap.ts +40 -8
  61. package/scaffold/host/src/hooks/usePlatformSession.ts +40 -8
  62. package/scaffold/host/src/layout/HostHeader.vue +65 -5
  63. package/scaffold/host/src/layout/HostLayout.vue +65 -21
  64. package/scaffold/host/src/layout/HostMenu.vue +11 -2
  65. package/scaffold/host/src/layout/HostTabs.vue +91 -8
  66. package/scaffold/host/src/layout/HostWorkspace.vue +29 -7
  67. package/scaffold/host/src/localPages.ts +45 -0
  68. package/scaffold/host/src/platform.ts +51 -14
  69. package/scaffold/host/src/router/guards.ts +128 -0
  70. package/scaffold/host/src/router/index.ts +25 -0
  71. package/scaffold/host/src/router/targets.ts +30 -0
  72. package/scaffold/host/src/router/workspace.ts +63 -0
  73. package/scaffold/host/src/styles/shell.css +776 -0
  74. package/scaffold/host/src/utils/request.ts +30 -3
  75. package/scaffold/host/src/views/home/index.vue +19 -0
  76. package/scaffold/host/src/views/login/components/LoginForm.vue +83 -0
  77. package/scaffold/host/src/views/login/index.vue +16 -0
  78. package/scaffold/host/src/views/not-found/index.vue +1 -0
  79. package/scaffold/host/src/views/profile/index.vue +24 -0
  80. package/scaffold/host/src/views/workspace/index.vue +11 -0
  81. package/scaffold/layouts/standard/host/src/layout/HostLayout.vue +116 -0
  82. package/scaffold/layouts/standard/host/src/styles/shell.css +168 -0
  83. package/scaffold/layouts/standard/vue/src/layout/StandaloneLayout.vue +26 -0
  84. package/scaffold/vue/src/App.vue +13 -7
  85. package/scaffold/vue/src/bootstrap.ts +25 -24
  86. package/scaffold/vue/src/bridge/auth.ts +23 -0
  87. package/scaffold/vue/src/bridge/session.ts +2 -2
  88. package/scaffold/vue/src/layout/Header.vue +15 -0
  89. package/scaffold/vue/src/layout/Sidebar.vue +21 -0
  90. package/scaffold/vue/src/layout/SidebarItem.vue +21 -0
  91. package/scaffold/vue/src/layout/StandaloneLayout.vue +34 -0
  92. package/scaffold/vue/src/main.ts +5 -2
  93. package/scaffold/vue/src/router/index.ts +9 -4
  94. package/scaffold/vue/src/router/menu.ts +38 -0
  95. package/scaffold/vue/src/views/demo/configs/table.ts +24 -0
  96. package/scaffold/vue/src/views/demo/hooks/useRecordNavigation.ts +32 -0
  97. package/scaffold/vue/src/views/demo/index.vue +17 -0
  98. package/scaffold/vue/src/views/{DetailView.vue → demo/modules/detail/index.vue} +15 -6
  99. package/scaffold/vue/src/views/demo/types.ts +5 -0
  100. package/dist/router-view-impl-CgLEalx4.cjs +0 -1
  101. package/scaffold/host/src/views/LoginView.vue +0 -38
  102. package/scaffold/vue/src/views/ListView.vue +0 -51
  103. /package/scaffold/vue/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
@@ -0,0 +1,119 @@
1
+ import { defineComponent as W, ref as j, onMounted as B, watch as $, onBeforeUnmount as N, openBlock as I, createElementBlock as P } from "vue";
2
+ function h(s, e, f, m) {
3
+ return typeof s == "number" && Number.isFinite(s) ? Math.min(m, Math.max(f, s)) : e;
4
+ }
5
+ function C(s) {
6
+ var r, o;
7
+ const e = s && typeof s == "object" ? s : {}, m = (typeof e.text == "string" ? e.text.split(`
8
+ `) : Array.isArray(e.text) ? e.text : ["内部资料"]).filter((l) => typeof l == "string").slice(0, 4).map((l) => l.slice(0, 120)), c = typeof e.fontFamily == "string" && e.fontFamily.length <= 160 && !/[;{}\\\n\r]/.test(e.fontFamily) ? e.fontFamily : "sans-serif";
9
+ return Object.freeze({
10
+ enabled: e.enabled !== !1,
11
+ text: Object.freeze(m),
12
+ color: typeof e.color == "string" && e.color.length <= 80 ? e.color : "#64748b",
13
+ opacity: h(e.opacity, 0.12, 0, 1),
14
+ rotate: h(e.rotate, -22, -180, 180),
15
+ fontSize: h(e.fontSize, 16, 10, 64),
16
+ fontFamily: c,
17
+ gap: Object.freeze([h((r = e.gap) == null ? void 0 : r[0], 100, 16, 600), h((o = e.gap) == null ? void 0 : o[1], 80, 16, 600)])
18
+ });
19
+ }
20
+ const H = /* @__PURE__ */ W({
21
+ __name: "FinWatermark",
22
+ props: {
23
+ config: {}
24
+ },
25
+ emits: ["error"],
26
+ setup(s, { emit: e }) {
27
+ const f = s, m = e, c = j();
28
+ let r = null, o, l, y = "", p = !1, v = !1;
29
+ function T() {
30
+ o == null || o.disconnect(), o = void 0, l !== void 0 && clearInterval(l), l = void 0;
31
+ }
32
+ function _(t) {
33
+ var S;
34
+ const a = C(f.config), i = t.ownerDocument.createElement("canvas"), n = i.getContext("2d");
35
+ if (!n) throw new Error("当前环境不支持 Canvas 2D,无法绘制水印。");
36
+ const g = `${a.fontSize}px ${a.fontFamily}`;
37
+ n.font = g;
38
+ const u = Math.min(1e3, Math.max(1, ...a.text.map((F) => n.measureText(F).width))), x = a.fontSize * 1.5, b = Math.max(1, a.text.length) * x, d = a.rotate * Math.PI / 180, k = Math.ceil(Math.abs(u * Math.cos(d)) + Math.abs(b * Math.sin(d)) + a.gap[0]), z = Math.ceil(Math.abs(u * Math.sin(d)) + Math.abs(b * Math.cos(d)) + a.gap[1]), w = Math.min(2, Math.max(1, ((S = t.ownerDocument.defaultView) == null ? void 0 : S.devicePixelRatio) ?? 1));
39
+ i.width = Math.ceil(k * w), i.height = Math.ceil(z * w), n.scale(w, w), n.translate(k / 2, z / 2), n.rotate(d), n.font = g, n.fillStyle = "#64748b", n.fillStyle = a.color, n.globalAlpha = a.opacity, n.textAlign = "center", n.textBaseline = "middle", a.text.forEach((F, O) => n.fillText(F, 0, (O - (a.text.length - 1) / 2) * x, 1e3));
40
+ const E = i.toDataURL("image/png");
41
+ if (!E.startsWith("data:image/png")) throw new Error("Canvas 未生成有效水印图像。");
42
+ return { url: E, width: k, height: z };
43
+ }
44
+ function D(t) {
45
+ o == null || o.observe(t, { attributes: !0 }), r && (o == null || o.observe(r, { childList: !0 }));
46
+ }
47
+ function M() {
48
+ var a;
49
+ const t = c.value;
50
+ if (!(!p || v || !t || !r)) {
51
+ v = !0, o == null || o.disconnect();
52
+ try {
53
+ t.parentNode !== r && r.appendChild(t), t.style.cssText !== y && (t.style.cssText = y), t.className && t.removeAttribute("class"), t.hasAttribute("hidden") && t.removeAttribute("hidden"), t.setAttribute("aria-hidden", "true");
54
+ const i = (a = t.ownerDocument.defaultView) == null ? void 0 : a.getComputedStyle(t);
55
+ i && (i.display === "none" || i.visibility !== "visible" || Number(i.opacity) === 0) && (t.style.cssText = y);
56
+ } finally {
57
+ v = !1, p && D(t);
58
+ }
59
+ }
60
+ }
61
+ function A() {
62
+ var i;
63
+ T();
64
+ const t = c.value;
65
+ if (!t || !p) return;
66
+ t.style.cssText = "display:none !important;";
67
+ const a = C(f.config);
68
+ if (!(!a.enabled || !a.text.some(Boolean)))
69
+ try {
70
+ const n = _(t), g = {
71
+ position: "absolute",
72
+ inset: "0",
73
+ width: "100%",
74
+ height: "100%",
75
+ margin: "0",
76
+ padding: "0",
77
+ border: "0",
78
+ display: "block",
79
+ visibility: "visible",
80
+ opacity: "1",
81
+ "pointer-events": "none",
82
+ "z-index": "2000",
83
+ transform: "none",
84
+ filter: "none",
85
+ "clip-path": "none",
86
+ "background-color": "transparent",
87
+ "background-image": `url("${n.url}")`,
88
+ "background-size": `${n.width}px ${n.height}px`,
89
+ "background-repeat": "repeat",
90
+ "background-position": "0 0",
91
+ "box-sizing": "border-box"
92
+ };
93
+ t.style.cssText = "";
94
+ for (const [x, b] of Object.entries(g)) t.style.setProperty(x, b, "important");
95
+ y = t.style.cssText;
96
+ const u = (i = t.ownerDocument.defaultView) == null ? void 0 : i.MutationObserver;
97
+ u && (o = new u(M)), M(), l = setInterval(M, 1e3);
98
+ } catch (n) {
99
+ t.style.cssText = "display:none !important;", m("error", n instanceof Error ? n : new Error("水印绘制失败。")), console.warn("[fin-micro/watermark] 水印绘制失败,本次检查已停止。", n);
100
+ }
101
+ }
102
+ return B(() => {
103
+ var t;
104
+ p = !0, r = ((t = c.value) == null ? void 0 : t.parentElement) ?? null, A();
105
+ }), $(() => f.config, A, { deep: !0 }), N(() => {
106
+ var t;
107
+ p = !1, T(), (t = c.value) == null || t.remove(), r = null;
108
+ }), (t, a) => (I(), P("div", {
109
+ ref_key: "layer",
110
+ ref: c,
111
+ "aria-hidden": "true",
112
+ "data-fin-watermark": ""
113
+ }, null, 512));
114
+ }
115
+ });
116
+ export {
117
+ H as FinWatermark,
118
+ C as normalizeWatermarkConfig
119
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axewo/fin-micro",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -36,6 +36,17 @@
36
36
  "import": "./dist/vue.mjs",
37
37
  "require": "./dist/vue.cjs"
38
38
  },
39
+ "./layout": {
40
+ "types": "./dist/types/layout/index.d.ts",
41
+ "import": "./dist/layout.mjs",
42
+ "require": "./dist/layout.cjs"
43
+ },
44
+ "./layout/style.css": "./dist/layout.css",
45
+ "./watermark": {
46
+ "types": "./dist/types/watermark/index.d.ts",
47
+ "import": "./dist/watermark.mjs",
48
+ "require": "./dist/watermark.cjs"
49
+ },
39
50
  "./runtime": {
40
51
  "types": "./dist/types/runtime/index.d.ts",
41
52
  "import": "./dist/runtime.mjs",
@@ -46,6 +57,7 @@
46
57
  "fin-micro": "./cli/index.mjs"
47
58
  },
48
59
  "files": [
60
+ "dist/**/*.css",
49
61
  "dist/**/*.mjs",
50
62
  "dist/**/*.cjs",
51
63
  "dist/**/*.d.ts",
@@ -58,12 +70,22 @@
58
70
  "publishConfig": {
59
71
  "access": "public"
60
72
  },
61
- "sideEffects": false,
73
+ "sideEffects": [
74
+ "**/*.css"
75
+ ],
62
76
  "peerDependencies": {
77
+ "element-plus": "^2.9.0",
78
+ "@element-plus/icons-vue": "^2.3.0",
63
79
  "vue": "^3.4.0",
64
80
  "vue-router": "^4.5.0"
65
81
  },
66
82
  "peerDependenciesMeta": {
83
+ "element-plus": {
84
+ "optional": true
85
+ },
86
+ "@element-plus/icons-vue": {
87
+ "optional": true
88
+ },
67
89
  "vue": {
68
90
  "optional": true
69
91
  },
@@ -75,6 +97,9 @@
75
97
  "wujie": "2.1.0"
76
98
  },
77
99
  "devDependencies": {
100
+ "@vitejs/plugin-vue": "^5.1.0",
101
+ "element-plus": "^2.9.0",
102
+ "@element-plus/icons-vue": "^2.3.0",
78
103
  "vue-router": "^4.5.0",
79
104
  "vue": "^3.5.0",
80
105
  "vite": "^5.4.0",
@@ -86,9 +111,9 @@
86
111
  "license": "MIT",
87
112
  "scripts": {
88
113
  "build": "vite build",
89
- "test": "vitest run",
114
+ "test": "node ../../node_modules/vitest/vitest.mjs run",
90
115
  "typecheck": "vue-tsc --noEmit -p tsconfig.json",
91
- "test:cli": "node --test cli/create.test.mjs cli/scaffold.test.mjs",
116
+ "test:cli": "node --test cli/create.test.mjs cli/scaffold.test.mjs cli/layout.test.mjs cli/startup.test.mjs",
92
117
  "check:package": "node scripts/check-package.mjs"
93
118
  }
94
119
  }
@@ -6,7 +6,8 @@
6
6
  <title>__PROJECT_NAME__</title>
7
7
  </head>
8
8
  <body>
9
- <div id="app"></div>
9
+ <!-- 启动前即显示;只占当前应用根区域,嵌入运行不会覆盖基座。Vue 挂载时替换。 -->
10
+ <div id="app"><div data-fin-startup="loading" role="status" aria-live="polite" style="min-height:240px;display:grid;place-content:center;gap:12px;text-align:center;color:#536079;background:#f4f6fa;font:14px/1.6 sans-serif"><strong>正在启动应用…</strong><span>正在加载配置与恢复会话,请稍候</span></div></div>
10
11
  <script type="module" src="/src/main.ts"></script>
11
12
  </body>
12
13
  </html>
@@ -4,18 +4,22 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "check:runtime": "node scripts/check-runtime.mjs",
8
+ "predev": "npm run check:runtime",
9
+ "prebuild": "npm run check:runtime",
7
10
  "dev": "vite",
8
11
  "build": "vue-tsc --noEmit && vite build",
9
12
  "preview": "vite preview",
10
13
  "typecheck": "vue-tsc --noEmit"
11
14
  },
12
15
  "dependencies": {
13
- "@axewo/fin-core": "2.1.0",
16
+ "@axewo/fin-core": "2.1.1",
14
17
  "@axewo/fin-micro": "__FIN_MICRO_VERSION__",
15
18
  "@axewo/fin-ui": "__FIN_UI_VERSION__",
16
19
  "@axewo/fin-editor": "2.0.1",
17
20
  "@element-plus/icons-vue": "2.3.2",
18
21
  "element-plus": "2.14.2",
22
+ "pinia": "2.3.1",
19
23
  "vue": "3.5.13",
20
24
  "vue-i18n": "^9.14.0",
21
25
  "vue-router": "4.5.0"
@@ -0,0 +1,6 @@
1
+ # pnpm >=10.26(包含12.3.4)使用allowBuilds;旧版pnpm请按其审批机制单独配置。
2
+ # 仅授权当前模板依赖的固定版本,升级依赖后须重新检查安装脚本。
3
+ # esbuild准备平台构建二进制;vue-demi适配当前Vue版本。禁止全量放开依赖脚本。
4
+ allowBuilds:
5
+ 'esbuild@0.21.5': true
6
+ 'vue-demi@0.14.10': true
@@ -0,0 +1,43 @@
1
+ import { readdir, readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ // 构建前检查安装包的实际导出,避免 Vite 链接失败被误认为登录或网络问题。
6
+ const imports = new Map()
7
+ let hostRouter = false
8
+ async function scan(directory) {
9
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
10
+ const file = join(directory, entry.name)
11
+ if (entry.isDirectory()) await scan(file)
12
+ else if (/\.(ts|vue)$/.test(entry.name)) {
13
+ const source = await readFile(file, 'utf8')
14
+ if (source.includes('connectWorkspaceRouter')) hostRouter = true
15
+ for (const match of source.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"](@axewo\/[^'"]+)['"]/g)) {
16
+ const names = imports.get(match[2]) ?? new Set()
17
+ for (const part of match[1].split(',')) {
18
+ const name = part.trim()
19
+ if (name && !name.startsWith('type ')) names.add(name.split(/\s+as\s+/)[0])
20
+ }
21
+ imports.set(match[2], names)
22
+ }
23
+ }
24
+ }
25
+ }
26
+ export async function checkRuntime() {
27
+ await scan(fileURLToPath(new URL('../src', import.meta.url)))
28
+ const problems = []
29
+ for (const [specifier, names] of imports) {
30
+ try {
31
+ const module = await import(specifier)
32
+ for (const name of names) if (!(name in module)) problems.push(`${specifier} 缺少导出 ${name}`)
33
+ if (specifier === '@axewo/fin-micro' && hostRouter && module.hostRouterProtocol !== 1) {
34
+ problems.push('@axewo/fin-micro 缺少 hostRouterProtocol=1(真实 host 路由、local 页面和 url:false 连接器能力)')
35
+ }
36
+ } catch { problems.push(`${specifier} 无法加载,请检查安装版本及其依赖(不输出原始异常)`) }
37
+ }
38
+ if (problems.length) {
39
+ console.error('启动依赖能力检查失败:\n' + problems.join('\n') + '\n请安装与生成器配套构建的包,并重新运行 npm run check:runtime;不会自动改依赖。')
40
+ process.exitCode = 1
41
+ } else console.log('启动依赖能力检查通过')
42
+ }
43
+ await checkRuntime()
@@ -0,0 +1,12 @@
1
+ // 与中文保持相同的业务词条结构。
2
+ export default {
3
+ demo: {
4
+ title: 'Locale and state demo',
5
+ description: 'Switch language, increment or reset. State belongs only to this app.',
6
+ count: 'Count: {count}',
7
+ doubled: 'Double count: {count}',
8
+ increment: 'Increment',
9
+ reset: 'Reset',
10
+ switchLanguage: 'Switch language',
11
+ },
12
+ }
@@ -1,7 +1,13 @@
1
1
  import type { App } from 'vue'
2
2
  import { installFinI18n } from '@axewo/fin-ui'
3
+ import zhCN from './zh-CN'
4
+ import en from './en'
3
5
 
4
6
  /** 每次 createApp 后独立安装,不在模块级共享可变 i18n 实例。 */
5
7
  export function installAppLocale(app: App): void {
6
- installFinI18n(app, { locale: 'zh-CN', fallbackLocale: 'zh-CN', legacy: false })
8
+ // 工厂递归合并业务词条,不覆盖 fin-ui 内置语言包。
9
+ installFinI18n(app, {
10
+ locale: 'zh-CN', fallbackLocale: 'zh-CN', legacy: false,
11
+ messages: { 'zh-CN': zhCN, en },
12
+ })
7
13
  }
@@ -0,0 +1,12 @@
1
+ // 业务词条使用独立命名空间,与 fin-ui 内置 fin.* 词条合并。
2
+ export default {
3
+ demo: {
4
+ title: '国际化与状态示例',
5
+ description: '切换语言、增加计数或重置,状态仅属于当前应用。',
6
+ count: '当前计数:{count}',
7
+ doubled: '双倍计数:{count}',
8
+ increment: '增加',
9
+ reset: '重置',
10
+ switchLanguage: '切换语言',
11
+ },
12
+ }
@@ -1,14 +1,22 @@
1
1
  import 'element-plus/dist/index.css'
2
2
  import '@axewo/fin-ui/style.css'
3
3
  import './styles/index.css'
4
- import { mount, unmount } from './bootstrap'
4
+ import { createStartupLifecycle } from './utils/errorUtils'
5
+
6
+ // 动态导入使配置解析等模块求值错误也进入脱敏启动边界。
7
+ const { mount, unmount } = createStartupLifecycle(() => import('./bootstrap'))
5
8
 
6
9
  // 普通应用直接启动;Vue 子应用模板覆盖本入口,接入微前端生命周期。
7
10
  // fin-ui 公开根入口依赖 editor 与 icons,依赖已在公共 package.json 中显式声明。
8
11
  void mount()
9
- window.addEventListener('unload', unmount)
10
- if (import.meta.hot) import.meta.hot.dispose(() => {
12
+ function onPageHide(event: PageTransitionEvent) {
13
+ // BFCache 返回时继续使用同一应用;保留监听以处理之后的普通离开。
14
+ if (!event.persisted) dispose()
15
+ }
16
+ function dispose() {
11
17
  // 基座只清理自身实例,不注册 Wujie 子应用全局回调。
12
- window.removeEventListener('unload', unmount)
18
+ window.removeEventListener('pagehide', onPageHide)
13
19
  unmount()
14
- })
20
+ }
21
+ window.addEventListener('pagehide', onPageHide)
22
+ if (import.meta.hot) import.meta.hot.dispose(dispose)
@@ -0,0 +1,6 @@
1
+ import type { RouteRecordRaw } from 'vue-router'
2
+
3
+ // 仅定义本地页面,不授予宿主菜单/页签权限;host 默认不自动安装。
4
+ export const demoRoutes: RouteRecordRaw[] = [
5
+ { path: '/demo', name: 'demo', component: () => import('../views/demo/index.vue') },
6
+ ]
@@ -0,0 +1,13 @@
1
+ import { createMemoryHistory, createRouter, type RouteRecordRaw, type Router } from 'vue-router'
2
+
3
+ /**
4
+ * 每轮挂载创建独立路由,默认使用空路由表,业务可通过参数传入本地页面。
5
+ * 此通用工厂不拥有地址栏;host overlay 用真实 history 路由覆盖本文件,vue overlay 按嵌入模式选历史。
6
+ * 独立使用本工厂时须由调用方负责 URL 适配;本地路由不替代平台认证及 canOpen 检查。
7
+ * @param routes 本地页面路由表;省略时为空,不自动添加业务页面。
8
+ * @returns 独立 Router;调用方安装到 App,并在卸载时销毁 history。
9
+ * @throws 无效路由配置导致的 Vue Router 初始化异常原样抛出。
10
+ */
11
+ export function createAppRouter(routes: RouteRecordRaw[] = []): Router {
12
+ return createRouter({ history: createMemoryHistory(), routes })
13
+ }
@@ -0,0 +1,12 @@
1
+ import { computed, ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ // 只定义 store,不在模块顶层实例化;状态由当前应用安装的 Pinia 隔离。
5
+ export const useDemoStore = defineStore('demo', () => {
6
+ const count = ref(0)
7
+ const doubled = computed(() => count.value * 2)
8
+ function increment(): void { count.value++ }
9
+ // setup store 显式提供重置 action,不使用 options store 的默认 $reset。
10
+ function reset(): void { count.value = 0 }
11
+ return { count, doubled, increment, reset }
12
+ })
@@ -0,0 +1,11 @@
1
+ import { createPinia, type Pinia } from 'pinia'
2
+
3
+ /**
4
+ * 每个 Vue App、每轮挂载独立创建状态容器,禁止导出跨应用共享的模块级单例。
5
+ * @returns 未安装的独立 Pinia;调用方通过 app.use 安装,卸载时通过 disposePinia 释放。
6
+ * @throws 不额外包装底层初始化异常。
7
+ * @remarks 无参数;业务 store 需在安装后使用,组件外调用须显式传入所属 Pinia。
8
+ */
9
+ export function createAppPinia(): Pinia {
10
+ return createPinia()
11
+ }
@@ -0,0 +1,49 @@
1
+ /** 构建时公开配置;VITE_* 均会进入前端产物,禁止放入任何密码或 Token。 */
2
+ export interface AppConfig {
3
+ /** Axios API 目录;相对地址仅允许当前源的绝对路径。 */
4
+ readonly apiBase: string
5
+ /** 凭据允许发送的精确 API 目录,不从请求参数推断或扩大。 */
6
+ readonly trustedApiBase: string
7
+ /** 请求超时毫秒数,必须为正有限整数且不超过原生计时器上限。 */
8
+ readonly timeout: number
9
+ /** 是否允许跨源 Cookie;服务端仍须配置精确 CORS 与 CSRF 防护。 */
10
+ readonly withCredentials: boolean
11
+ }
12
+
13
+ /** 校验公开 HTTP(S) 地址;拒绝凭据、查询、片段、协议相对地址及编码路径歧义,不回显输入。 */
14
+ export function validateAddress(value: string, directory = true): string {
15
+ if (typeof value !== 'string' || !value || value.trim() !== value || /[\\\s\u0000-\u001f\u007f]/.test(value) || /%(?:2f|5c|2e)/i.test(value) || /(?:^|\/)\.{1,2}(?:\/|$)/.test(value)) throw new Error('应用地址配置无效')
16
+ const relative = value.startsWith('/') && !value.startsWith('//')
17
+ if (!relative && !/^https?:\/\//i.test(value)) throw new Error('应用地址必须为 HTTP(S) 或同源绝对路径')
18
+ const url = new URL(value, 'https://config.invalid')
19
+ if (url.username || url.password || /[?#]/.test(value) || (directory && !url.pathname.endsWith('/'))) throw new Error('应用地址必须为无凭据、查询和片段的目录')
20
+ // 只有本机开发地址允许 HTTP;生产网关和代码入口必须使用 HTTPS。
21
+ if (!relative && url.protocol !== 'https:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) throw new Error('非本机应用地址必须使用 HTTPS')
22
+ return value
23
+ }
24
+
25
+ /** 将 Vite 环境转换成只读配置;非法值立即拒绝启动,默认值不扩大凭据目录。 */
26
+ export function resolveAppConfig(env: Record<string, unknown> = {}): AppConfig {
27
+ const apiBase = validateAddress((env.VITE_API_BASE ?? '/api/') as string)
28
+ const trustedApiBase = validateAddress((env.VITE_TRUSTED_API_BASE ?? apiBase) as string)
29
+ if (apiBase !== trustedApiBase) throw new Error('API 与可信凭据目录必须一致')
30
+ const rawTimeout = env.VITE_API_TIMEOUT ?? 30000
31
+ if ((typeof rawTimeout !== 'string' && typeof rawTimeout !== 'number') || !/^\d+$/.test(String(rawTimeout))) throw new Error('API 超时配置无效')
32
+ const timeout = Number(rawTimeout)
33
+ if (!Number.isInteger(timeout) || timeout <= 0 || timeout > 2147483647) throw new Error('API 超时配置无效')
34
+ const credentials = env.VITE_API_CREDENTIALS ?? 'false'
35
+ if (credentials !== 'true' && credentials !== 'false') throw new Error('API credentials 配置必须为 true 或 false')
36
+ return Object.freeze({ apiBase, trustedApiBase, timeout, withCredentials: credentials === 'true' })
37
+ }
38
+
39
+ /** 解析部署者控制的可信代码入口;只读取构建环境,后台 appIds/menus 不得覆盖 entry。 */
40
+ export function resolveTrustedApp(env: Record<string, unknown> = {}): { readonly appId: string; readonly entry: string } {
41
+ const appId = env.VITE_MICRO_APP_ID ?? 'business'
42
+ const entry = env.VITE_MICRO_APP_ENTRY ?? 'http://localhost:7101'
43
+ if (typeof appId !== 'string' || appId.length > 214 || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(appId)) throw new Error('可信应用 appId 配置无效')
44
+ if (typeof entry !== 'string' || !/^https?:\/\//i.test(entry)) throw new Error('可信应用 entry 必须为完整 HTTP(S) 地址')
45
+ return Object.freeze({ appId, entry: validateAddress(entry, false) })
46
+ }
47
+
48
+ /** 应用模块专属公开配置,不包含宿主认证状态;Node 测试环境使用相同默认值。 */
49
+ export const appConfig = resolveAppConfig(import.meta.env)
@@ -0,0 +1,109 @@
1
+ /** 可直接展示/记录的错误分类,不附带原始异常、URL、请求头、响应体或堆栈。 */
2
+ export interface SafeError {
3
+ readonly kind: 'cancelled' | 'unauthenticated' | 'forbidden' | 'timeout' | 'network' | 'unknown'
4
+ readonly message: string
5
+ }
6
+
7
+ /** 复用 Axios、HttpError 和 SDK 的公开判别字段;绝不采用外部 message/cause 作为用户文案。 */
8
+ export function toSafeError(error: unknown): SafeError {
9
+ const value = error && typeof error === 'object' ? error as Record<string, unknown> : {}
10
+ const response = value.response && typeof value.response === 'object' ? value.response as Record<string, unknown> : {}
11
+ const code = value.code
12
+ const status = response.status ?? value.status
13
+ // 分段读取 Axios 旧版取消标识,避免被脚手架的大写双下划线占位符校验误识别。
14
+ if (code === 'ERR_CANCELED' || code === 'CANCELLED' || code === 'SESSION_CHANGED' || value.name === 'AbortError' || value['__CANCEL' + '__'] === true) return { kind: 'cancelled', message: '' }
15
+ if (status === 401 || code === 'UNAUTHENTICATED') return { kind: 'unauthenticated', message: '登录已失效,请重新登录' }
16
+ if (status === 403 || code === 'FORBIDDEN') return { kind: 'forbidden', message: '403:无权执行此操作,请联系管理员' }
17
+ if (code === 'ECONNABORTED' || code === 'ETIMEDOUT' || code === 'TIMEOUT') return { kind: 'timeout', message: '操作超时,请稍后重试' }
18
+ if (code === 'ERR_NETWORK' || value.name === 'TypeError') return { kind: 'network', message: '连接失败,请检查网络后重试' }
19
+ return { kind: 'unknown', message: '操作失败,请稍后重试或联系管理员' }
20
+ }
21
+
22
+ /** Vue/启动边界的保底接收器;只记录白名单分类与固定文案,不日志原始敏感对象。 */
23
+ export function reportError(error: unknown): void {
24
+ const safe = toSafeError(error)
25
+ if (safe.kind !== 'cancelled') console.error(safe.message + devDiagnostic(error, 'runtime'))
26
+ }
27
+
28
+ /** DEV 仅输出固定阶段和标准错误名;不采用外部 name/message、请求、响应或栈。 */
29
+ export function devDiagnostic(error: unknown, stage: 'load-bootstrap' | 'mount' | 'runtime'): string {
30
+ if (!import.meta.env?.DEV) return ''
31
+ const name = error && typeof error === 'object' && 'name' in error ? error.name : ''
32
+ const allowed = ['Error', 'TypeError', 'SyntaxError', 'ReferenceError', 'RangeError', 'URIError', 'AbortError']
33
+ const kind = typeof name === 'string' && allowed.includes(name) ? name : 'UnknownError'
34
+ return ` [阶段=${stage}; 错误=${kind}]${kind === 'SyntaxError' ? ' 请先执行 npm run check:runtime 检查模块导出及依赖能力' : ''}`
35
+ }
36
+
37
+ /**
38
+ * 在当前应用根节点显示启动占位;不使用全屏定位,不依赖 Vue 或 UI 插件。
39
+ * @returns 无返回值;无 DOM 或根节点时安全忽略,Vue 挂载后自然替换占位。
40
+ */
41
+ export function showStartupLoading(): void {
42
+ if (typeof document === 'undefined') return
43
+ const container = document.getElementById('app')
44
+ if (!container) return
45
+ const text = document.createElement('p')
46
+ text.setAttribute('role', 'status')
47
+ text.setAttribute('aria-live', 'polite')
48
+ text.setAttribute('data-fin-startup', 'loading')
49
+ text.textContent = '正在启动应用,请稍候…'
50
+ container.replaceChildren(text)
51
+ }
52
+
53
+ /** 启动失败的纯文本可重试边界;不插入错误 HTML,不依赖尚未成功安装的 Vue/UI 插件。 */
54
+ export function showStartupError(error: unknown, retry: () => Promise<void>, stage: 'load-bootstrap' | 'mount' = 'mount'): void {
55
+ if (typeof document === 'undefined') return
56
+ const container = document.getElementById('app')
57
+ if (!container) return
58
+ const text = document.createElement('p')
59
+ text.setAttribute('role', 'alert')
60
+ text.textContent = toSafeError(error).kind === 'cancelled' ? '应用启动已取消,可重试启动' : `应用启动失败:${toSafeError(error).message}${devDiagnostic(error, stage)}`
61
+ const button = document.createElement('button')
62
+ button.textContent = '重试启动'
63
+ button.onclick = () => { void retry().catch(reportError) }
64
+ container.replaceChildren(text, button)
65
+ }
66
+
67
+ /**
68
+ * 在入口动态加载 bootstrap,捕获其静态依赖中的配置解析/代码加载失败。
69
+ * 入口卸载立即推进代次;尚未完成的 import 不得在卸载或下一轮启动后挂载旧应用。
70
+ * 仅协调启动边界,不替代 bootstrap 内的 router、Pinia 和身份资源清理。
71
+ */
72
+ export function createStartupLifecycle(load: () => Promise<{ mount(): Promise<void>; unmount(): void }>) {
73
+ let generation = 0
74
+ let current: { unmount(): void } | undefined
75
+ function unmount(): void {
76
+ generation++
77
+ const previous = current
78
+ current = undefined
79
+ previous?.unmount()
80
+ }
81
+ async function mount(): Promise<void> {
82
+ unmount()
83
+ const version = generation
84
+ showStartupLoading()
85
+ let stage: 'load-bootstrap' | 'mount' = 'load-bootstrap'
86
+ try {
87
+ const bootstrap = await load()
88
+ if (generation !== version) return
89
+ current = bootstrap
90
+ stage = 'mount'
91
+ await bootstrap.mount()
92
+ } catch (error) {
93
+ if (generation !== version) return
94
+ unmount()
95
+ reportError(error)
96
+ // 仅当前失败轮次可重试;卸载后遗留按钮或旧回调不得重启已释放的应用。
97
+ const failedVersion = generation
98
+ showStartupError(error, () => generation === failedVersion ? mount() : Promise.resolve(), stage)
99
+ }
100
+ }
101
+ return { mount, unmount }
102
+ }
103
+
104
+ /** 独立执行全部清理回调;一个回调失败不能阻断后续凭据撤销或资源释放。 */
105
+ export function cleanupAll(callbacks: readonly (() => void)[]): void {
106
+ for (const callback of callbacks) {
107
+ try { callback() } catch (error) { reportError(error) }
108
+ }
109
+ }