@axewo/fin-micro 0.0.0-stage → 1.0.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.
Files changed (69) hide show
  1. package/README.md +125 -2
  2. package/cli/index.mjs +240 -0
  3. package/dist/browser.cjs +1 -0
  4. package/dist/browser.mjs +167 -0
  5. package/dist/core.cjs +1 -0
  6. package/dist/core.mjs +640 -0
  7. package/dist/iframe.cjs +1 -0
  8. package/dist/iframe.mjs +146 -0
  9. package/dist/index.cjs +1 -0
  10. package/dist/index.mjs +28 -0
  11. package/dist/router-view-impl-C1KG6mxu.mjs +76 -0
  12. package/dist/router-view-impl-CgLEalx4.cjs +1 -0
  13. package/dist/runtime.cjs +1 -0
  14. package/dist/runtime.mjs +148 -0
  15. package/dist/sdk.cjs +1 -0
  16. package/dist/sdk.mjs +82 -0
  17. package/dist/types/browser/index.d.ts +42 -0
  18. package/dist/types/core/index.d.ts +63 -0
  19. package/dist/types/iframe/index.d.ts +77 -0
  20. package/dist/types/index.d.ts +6 -0
  21. package/dist/types/runtime/index.d.ts +21 -0
  22. package/dist/types/runtime/lifecycle.d.ts +23 -0
  23. package/dist/types/sdk/index.d.ts +338 -0
  24. package/dist/types/vue/index.d.ts +91 -0
  25. package/dist/types/vue/router-view-impl.d.ts +41 -0
  26. package/dist/types/vue/router-view.d.ts +42 -0
  27. package/dist/types/vue/tab-context.d.ts +6 -0
  28. package/dist/vue.cjs +1 -0
  29. package/dist/vue.mjs +166 -0
  30. package/package.json +91 -3
  31. package/scaffold/base/index.html +12 -0
  32. package/scaffold/base/package.json.template +29 -0
  33. package/scaffold/base/src/components/.gitkeep +1 -0
  34. package/scaffold/base/src/components/AppProviders.vue +25 -0
  35. package/scaffold/base/src/hooks/.gitkeep +1 -0
  36. package/scaffold/base/src/layout/.gitkeep +1 -0
  37. package/scaffold/base/src/locales/.gitkeep +1 -0
  38. package/scaffold/base/src/locales/index.ts +7 -0
  39. package/scaffold/base/src/main.ts +14 -0
  40. package/scaffold/base/src/router/.gitkeep +1 -0
  41. package/scaffold/base/src/stores/.gitkeep +1 -0
  42. package/scaffold/base/src/styles/index.css +18 -0
  43. package/scaffold/base/src/types/.gitkeep +1 -0
  44. package/scaffold/base/src/utils/.gitkeep +1 -0
  45. package/scaffold/base/src/utils/requestUtils.ts +123 -0
  46. package/scaffold/base/src/views/.gitkeep +1 -0
  47. package/scaffold/base/tsconfig.json +16 -0
  48. package/scaffold/base/vite.config.ts +8 -0
  49. package/scaffold/host/.env.example +5 -0
  50. package/scaffold/host/src/App.vue +16 -0
  51. package/scaffold/host/src/bootstrap.ts +18 -0
  52. package/scaffold/host/src/hooks/usePlatformSession.ts +59 -0
  53. package/scaffold/host/src/layout/HostHeader.vue +9 -0
  54. package/scaffold/host/src/layout/HostLayout.vue +124 -0
  55. package/scaffold/host/src/layout/HostMenu.vue +14 -0
  56. package/scaffold/host/src/layout/HostTabs.vue +14 -0
  57. package/scaffold/host/src/layout/HostWorkspace.vue +14 -0
  58. package/scaffold/host/src/platform.ts +208 -0
  59. package/scaffold/host/src/types/platform.ts +12 -0
  60. package/scaffold/host/src/utils/request.ts +15 -0
  61. package/scaffold/host/src/views/LoginView.vue +38 -0
  62. package/scaffold/vue/src/App.vue +24 -0
  63. package/scaffold/vue/src/bootstrap.ts +71 -0
  64. package/scaffold/vue/src/bridge/session.ts +16 -0
  65. package/scaffold/vue/src/main.ts +10 -0
  66. package/scaffold/vue/src/router/index.ts +14 -0
  67. package/scaffold/vue/src/views/DetailView.vue +48 -0
  68. package/scaffold/vue/src/views/ListView.vue +51 -0
  69. package/scaffold/vue/src/views/NotFoundView.vue +1 -0
@@ -0,0 +1,6 @@
1
+ import { InjectionKey } from 'vue';
2
+ import { MicroTab } from '../sdk';
3
+ /** 页签边界注入创建时的身份,不会随宿主活动页改变。 */
4
+ export declare const microTabKey: InjectionKey<MicroTab>;
5
+ /** 在缓存页面 setup 中取得创建时的页签快照;必须位于 MicroTabCache 默认插槽内。 */
6
+ export declare function useMicroTab(): MicroTab;
package/dist/vue.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),s=Symbol("micro-tab");function d(){const t=e.inject(s);if(!t)throw new Error("useMicroTab 必须在 MicroTabCache 内调用");return t}const m=e.defineComponent({name:"MicroRouteView",props:{name:{type:String,default:"default"}},slots:Object,setup(t,{slots:n}){const i=d(),a=e.shallowRef(),r=e.shallowRef(!1);let u=!1,c=!1;async function o(){if(!(u||c)){c=!0,r.value=!1;try{const l=await Promise.resolve().then(()=>require("./router-view-impl-CgLEalx4.cjs"));u||(a.value=l.MicroRouteViewImpl)}catch{u||(r.value=!0)}finally{c=!1}}}return e.onBeforeUnmount(()=>{u=!0}),o(),()=>{var l,f;return a.value?e.h(a.value,{name:t.name},n):r.value?((l=n.error)==null?void 0:l.call(n,{tab:i,code:"LOAD_FAILED",retry:o}))??e.h("div",{role:"alert","data-micro-route-error":"LOAD_FAILED"},[e.h("p","页面加载失败(LOAD_FAILED)"),e.h("button",{type:"button",onClick:o},"重新加载")]):((f=n.loading)==null?void 0:f.call(n,{tab:i}))??e.h("p",{role:"status"},"页面加载中…")}}}),h=e.defineComponent({name:"MicroAppContainer",props:{appId:{type:String,required:!0},runtime:{type:Object,required:!0},retryDisabled:Boolean},setup(t){const n=e.shallowRef(),i=e.shallowRef(""),a=e.shallowRef(!1);let r=!1;async function u(c=!1){if(!(r||a.value||!c&&t.retryDisabled)){a.value=!0,i.value="";try{await t.runtime.mount(t.appId,n.value)}catch(o){r||(i.value=String(o))}finally{r||(a.value=!1)}}}return e.onMounted(()=>{u(!0)}),e.onBeforeUnmount(()=>{r=!0,t.runtime.unmount(t.appId).catch(console.error)}),()=>e.h("section",{style:{height:"100%",minHeight:"420px"},"aria-busy":a.value},[i.value?e.h("div",{role:"alert"},[e.h("p",i.value),e.h("button",{type:"button",disabled:t.retryDisabled||a.value,onClick:()=>{u()}},"重新加载")]):null,a.value?e.h("p",{role:"status"},"子应用加载中…"):null,e.h("div",{ref:n,style:{height:"100%",minHeight:"420px"}})])}});function v(t){const n=e.shallowRef(t.getState()),i=t.subscribe(a=>{n.value=a});return e.onBeforeUnmount(i),n}const b=e.defineComponent({name:"MicroTabBoundary",props:{tab:{type:Object,required:!0},active:Boolean},setup(t,{slots:n}){const i=Object.freeze({...t.tab});e.provide(s,i);const a=e.defineComponent({name:"MicroTabContent",setup:()=>()=>{var r;return(r=n.default)==null?void 0:r.call(n,{tab:i})}});return()=>e.h("section",{hidden:!t.active,"data-micro-tab":i.key},[e.h(e.KeepAlive,null,{default:()=>t.active?e.h(a):null})])}}),p=e.defineComponent({name:"MicroTabCache",props:{tabs:{type:Array,required:!0},activeKey:String,max:{type:Number,default:20}},setup(t,{slots:n}){const i=e.shallowRef([]);return e.watch(()=>[t.tabs,t.activeKey,t.max],()=>{const a=new Set(t.tabs.map(o=>o.key));let r=i.value.filter(o=>a.has(o.key));const u=t.tabs.find(o=>o.key===t.activeKey);u&&(r=[...r.filter(o=>o.key!==u.key),u]);const c=Number.isFinite(t.max)?Math.max(1,Math.floor(t.max)):20;for(;r.length>c;){const o=r.findIndex(l=>l.key!==t.activeKey);if(o<0)break;r.splice(o,1)}i.value=r},{immediate:!0,flush:"sync"}),()=>i.value.map(a=>e.h(b,{key:a.key,tab:a,active:a.key===t.activeKey},n))}});function M(t,n,i=console.error){let a=!1,r,u=!1,c=0;async function o(){if(!u){u=!0;try{let f=-1;for(;!a&&r&&f!==c;){f=c;try{await n.replace(r.route,r)}catch(y){i(y)}}}finally{u=!1}}}const l=t.ready(f=>{r=f,c++,o()});return()=>{a=!0,l()}}function w(t,n){const i=t.page(n.key);return e.onBeforeUnmount(i.dispose),i}exports.MicroAppContainer=h;exports.MicroRouteView=m;exports.MicroTabCache=p;exports.connectMicroRouter=M;exports.useMicroHost=v;exports.useMicroPage=w;exports.useMicroTab=d;
package/dist/vue.mjs ADDED
@@ -0,0 +1,166 @@
1
+ import { inject as b, defineComponent as s, shallowRef as d, onBeforeUnmount as y, h as u, onMounted as p, provide as h, KeepAlive as M, watch as g } from "vue";
2
+ const m = Symbol("micro-tab");
3
+ function k() {
4
+ const e = b(m);
5
+ if (!e) throw new Error("useMicroTab 必须在 MicroTabCache 内调用");
6
+ return e;
7
+ }
8
+ const A = s({
9
+ name: "MicroRouteView",
10
+ props: { name: { type: String, default: "default" } },
11
+ slots: Object,
12
+ setup(e, { slots: t }) {
13
+ const r = k(), a = d(), n = d(!1);
14
+ let o = !1, c = !1;
15
+ async function i() {
16
+ if (!(o || c)) {
17
+ c = !0, n.value = !1;
18
+ try {
19
+ const l = await import("./router-view-impl-C1KG6mxu.mjs");
20
+ o || (a.value = l.MicroRouteViewImpl);
21
+ } catch {
22
+ o || (n.value = !0);
23
+ } finally {
24
+ c = !1;
25
+ }
26
+ }
27
+ }
28
+ return y(() => {
29
+ o = !0;
30
+ }), i(), () => {
31
+ var l, f;
32
+ return a.value ? u(a.value, { name: e.name }, t) : n.value ? ((l = t.error) == null ? void 0 : l.call(t, { tab: r, code: "LOAD_FAILED", retry: i })) ?? u("div", { role: "alert", "data-micro-route-error": "LOAD_FAILED" }, [
33
+ u("p", "页面加载失败(LOAD_FAILED)"),
34
+ u("button", { type: "button", onClick: i }, "重新加载")
35
+ ]) : ((f = t.loading) == null ? void 0 : f.call(t, { tab: r })) ?? u("p", { role: "status" }, "页面加载中…");
36
+ };
37
+ }
38
+ }), C = s({
39
+ name: "MicroAppContainer",
40
+ props: {
41
+ appId: { type: String, required: !0 },
42
+ runtime: { type: Object, required: !0 },
43
+ retryDisabled: Boolean
44
+ },
45
+ setup(e) {
46
+ const t = d(), r = d(""), a = d(!1);
47
+ let n = !1;
48
+ async function o(c = !1) {
49
+ if (!(n || a.value || !c && e.retryDisabled)) {
50
+ a.value = !0, r.value = "";
51
+ try {
52
+ await e.runtime.mount(e.appId, t.value);
53
+ } catch (i) {
54
+ n || (r.value = String(i));
55
+ } finally {
56
+ n || (a.value = !1);
57
+ }
58
+ }
59
+ }
60
+ return p(() => {
61
+ o(!0);
62
+ }), y(() => {
63
+ n = !0, e.runtime.unmount(e.appId).catch(console.error);
64
+ }), () => u("section", { style: { height: "100%", minHeight: "420px" }, "aria-busy": a.value }, [
65
+ r.value ? u("div", { role: "alert" }, [
66
+ u("p", r.value),
67
+ u("button", { type: "button", disabled: e.retryDisabled || a.value, onClick: () => {
68
+ o();
69
+ } }, "重新加载")
70
+ ]) : null,
71
+ a.value ? u("p", { role: "status" }, "子应用加载中…") : null,
72
+ u("div", { ref: t, style: { height: "100%", minHeight: "420px" } })
73
+ ]);
74
+ }
75
+ });
76
+ function D(e) {
77
+ const t = d(e.getState()), r = e.subscribe((a) => {
78
+ t.value = a;
79
+ });
80
+ return y(r), t;
81
+ }
82
+ const w = s({
83
+ name: "MicroTabBoundary",
84
+ props: {
85
+ tab: { type: Object, required: !0 },
86
+ active: Boolean
87
+ },
88
+ setup(e, { slots: t }) {
89
+ const r = Object.freeze({ ...e.tab });
90
+ h(m, r);
91
+ const a = s({ name: "MicroTabContent", setup: () => () => {
92
+ var n;
93
+ return (n = t.default) == null ? void 0 : n.call(t, { tab: r });
94
+ } });
95
+ return () => u("section", { hidden: !e.active, "data-micro-tab": r.key }, [
96
+ u(M, null, { default: () => e.active ? u(a) : null })
97
+ ]);
98
+ }
99
+ }), T = s({
100
+ name: "MicroTabCache",
101
+ props: {
102
+ tabs: { type: Array, required: !0 },
103
+ activeKey: String,
104
+ max: { type: Number, default: 20 }
105
+ },
106
+ setup(e, { slots: t }) {
107
+ const r = d([]);
108
+ return g(() => [e.tabs, e.activeKey, e.max], () => {
109
+ const a = new Set(e.tabs.map((i) => i.key));
110
+ let n = r.value.filter((i) => a.has(i.key));
111
+ const o = e.tabs.find((i) => i.key === e.activeKey);
112
+ o && (n = [...n.filter((i) => i.key !== o.key), o]);
113
+ const c = Number.isFinite(e.max) ? Math.max(1, Math.floor(e.max)) : 20;
114
+ for (; n.length > c; ) {
115
+ const i = n.findIndex((l) => l.key !== e.activeKey);
116
+ if (i < 0) break;
117
+ n.splice(i, 1);
118
+ }
119
+ r.value = n;
120
+ }, { immediate: !0, flush: "sync" }), () => r.value.map((a) => u(w, {
121
+ key: a.key,
122
+ tab: a,
123
+ active: a.key === e.activeKey
124
+ }, t));
125
+ }
126
+ });
127
+ function I(e, t, r = console.error) {
128
+ let a = !1, n, o = !1, c = 0;
129
+ async function i() {
130
+ if (!o) {
131
+ o = !0;
132
+ try {
133
+ let f = -1;
134
+ for (; !a && n && f !== c; ) {
135
+ f = c;
136
+ try {
137
+ await t.replace(n.route, n);
138
+ } catch (v) {
139
+ r(v);
140
+ }
141
+ }
142
+ } finally {
143
+ o = !1;
144
+ }
145
+ }
146
+ }
147
+ const l = e.ready((f) => {
148
+ n = f, c++, i();
149
+ });
150
+ return () => {
151
+ a = !0, l();
152
+ };
153
+ }
154
+ function K(e, t) {
155
+ const r = e.page(t.key);
156
+ return y(r.dispose), r;
157
+ }
158
+ export {
159
+ C as MicroAppContainer,
160
+ A as MicroRouteView,
161
+ T as MicroTabCache,
162
+ I as connectMicroRouter,
163
+ D as useMicroHost,
164
+ K as useMicroPage,
165
+ k as useMicroTab
166
+ };
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@axewo/fin-micro",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./browser": {
15
+ "types": "./dist/types/browser/index.d.ts",
16
+ "import": "./dist/browser.mjs",
17
+ "require": "./dist/browser.cjs"
18
+ },
19
+ "./iframe": {
20
+ "types": "./dist/types/iframe/index.d.ts",
21
+ "import": "./dist/iframe.mjs",
22
+ "require": "./dist/iframe.cjs"
23
+ },
24
+ "./sdk": {
25
+ "types": "./dist/types/sdk/index.d.ts",
26
+ "import": "./dist/sdk.mjs",
27
+ "require": "./dist/sdk.cjs"
28
+ },
29
+ "./core": {
30
+ "types": "./dist/types/core/index.d.ts",
31
+ "import": "./dist/core.mjs",
32
+ "require": "./dist/core.cjs"
33
+ },
34
+ "./vue": {
35
+ "types": "./dist/types/vue/index.d.ts",
36
+ "import": "./dist/vue.mjs",
37
+ "require": "./dist/vue.cjs"
38
+ },
39
+ "./runtime": {
40
+ "types": "./dist/types/runtime/index.d.ts",
41
+ "import": "./dist/runtime.mjs",
42
+ "require": "./dist/runtime.cjs"
43
+ }
44
+ },
45
+ "bin": {
46
+ "fin-micro": "./cli/index.mjs"
47
+ },
48
+ "files": [
49
+ "dist/**/*.mjs",
50
+ "dist/**/*.cjs",
51
+ "dist/**/*.d.ts",
52
+ "cli/index.mjs",
53
+ "scaffold"
54
+ ],
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "sideEffects": false,
62
+ "peerDependencies": {
63
+ "vue": "^3.4.0",
64
+ "vue-router": "^4.5.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "vue": {
68
+ "optional": true
69
+ },
70
+ "vue-router": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "dependencies": {
75
+ "wujie": "2.1.0"
76
+ },
77
+ "devDependencies": {
78
+ "vue-router": "^4.5.0",
79
+ "vue": "^3.5.0",
80
+ "vite": "^5.4.0",
81
+ "vite-plugin-dts": "^4.3.0",
82
+ "vitest": "^2.1.0",
83
+ "vue-tsc": "^2.1.0",
84
+ "typescript": "^5.6.0"
85
+ },
86
+ "license": "MIT",
87
+ "scripts": {
88
+ "build": "vite build",
89
+ "test": "vitest run",
90
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
91
+ "test:cli": "node --test cli/create.test.mjs cli/scaffold.test.mjs",
92
+ "check:package": "node scripts/check-package.mjs"
93
+ }
6
94
  }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__PROJECT_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc --noEmit && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "vue-tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@axewo/fin-core": "2.1.0",
14
+ "@axewo/fin-micro": "__FIN_MICRO_VERSION__",
15
+ "@axewo/fin-ui": "__FIN_UI_VERSION__",
16
+ "@axewo/fin-editor": "2.0.1",
17
+ "@element-plus/icons-vue": "2.3.2",
18
+ "element-plus": "2.14.2",
19
+ "vue": "3.5.13",
20
+ "vue-i18n": "^9.14.0",
21
+ "vue-router": "4.5.0"
22
+ },
23
+ "devDependencies": {
24
+ "@vitejs/plugin-vue": "5.2.1",
25
+ "typescript": "5.6.3",
26
+ "vite": "5.4.14",
27
+ "vue-tsc": "2.2.0"
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ # 预留可复用业务组件目录;宿主专属结构组件位于 layout。
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { FConfigProvider, useFinLocale, zhCN, en, type FinConfig } from '@axewo/fin-ui'
4
+ import { ElConfigProvider } from 'element-plus'
5
+ import elZhCN from 'element-plus/es/locale/lang/zh-cn'
6
+ import elEn from 'element-plus/es/locale/lang/en'
7
+
8
+ const props = withDefaults(defineProps<{ size?: FinConfig['size'] }>(), { size: 'default' })
9
+ // bootstrap 已为当前应用安装 i18n;切换全局 locale 时,两套组件库同步更新。
10
+ const { locale } = useFinLocale()
11
+ const config = computed<FinConfig>(() => ({
12
+ locale: locale.value === 'en' ? en : zhCN,
13
+ size: props.size,
14
+ }))
15
+ const elementLocale = computed(() => locale.value === 'en' ? elEn : elZhCN)
16
+ </script>
17
+
18
+ <template>
19
+ <!-- config.locale 仅注入语言包,不能代替 app 安装 vue-i18n。 -->
20
+ <FConfigProvider :config="config">
21
+ <ElConfigProvider :locale="elementLocale" :size="config.size">
22
+ <slot />
23
+ </ElConfigProvider>
24
+ </FConfigProvider>
25
+ </template>
@@ -0,0 +1 @@
1
+ # 预留业务组合函数目录;不要在此复制核心导航、权限或页签状态机。
@@ -0,0 +1 @@
1
+ # 预留布局组件目录;仅在页面有实际布局职责时添加组件。
@@ -0,0 +1 @@
1
+ # 预留语言资源目录;当前模板未启用国际化,也未引入国际化依赖。
@@ -0,0 +1,7 @@
1
+ import type { App } from 'vue'
2
+ import { installFinI18n } from '@axewo/fin-ui'
3
+
4
+ /** 每次 createApp 后独立安装,不在模块级共享可变 i18n 实例。 */
5
+ export function installAppLocale(app: App): void {
6
+ installFinI18n(app, { locale: 'zh-CN', fallbackLocale: 'zh-CN', legacy: false })
7
+ }
@@ -0,0 +1,14 @@
1
+ import 'element-plus/dist/index.css'
2
+ import '@axewo/fin-ui/style.css'
3
+ import './styles/index.css'
4
+ import { mount, unmount } from './bootstrap'
5
+
6
+ // 普通应用直接启动;Vue 子应用模板覆盖本入口,接入微前端生命周期。
7
+ // fin-ui 公开根入口依赖 editor 与 icons,依赖已在公共 package.json 中显式声明。
8
+ void mount()
9
+ window.addEventListener('unload', unmount)
10
+ if (import.meta.hot) import.meta.hot.dispose(() => {
11
+ // 基座只清理自身实例,不注册 Wujie 子应用全局回调。
12
+ window.removeEventListener('unload', unmount)
13
+ unmount()
14
+ })
@@ -0,0 +1 @@
1
+ # 路由目录由对应模板提供;宿主地址由 connectMicroBrowser 管理,不创建第二个 Vue Router。
@@ -0,0 +1 @@
1
+ # 预留跨页面业务状态目录;当前模板没有需要共享的业务状态,未引入状态管理依赖。
@@ -0,0 +1,18 @@
1
+ :root { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; color: #243247; background: #f4f6fa; font-synthesis: none; }
2
+ * { box-sizing: border-box; }
3
+ body { margin: 0; }
4
+ button, input { font: inherit; }
5
+ #app { min-height: 100vh; }
6
+ .page { padding: 24px; }
7
+ .panel { background: white; border-radius: 10px; padding: 24px; }
8
+ .actions { display: flex; align-items: center; gap: 12px; margin: 16px 0; flex-wrap: wrap; }
9
+ .feedback { color: #b45309; padding: 12px 24px; }
10
+ .login { max-width: 440px; margin: 14vh auto; }
11
+ .shell { display: grid; grid-template: 64px 1fr / 240px minmax(0, 1fr); height: 100vh; }
12
+ .shell-header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: white; border-bottom: 1px solid #e5e7eb; }
13
+ .sidebar { overflow: auto; background: white; border-right: 1px solid #e5e7eb; }
14
+ .workspace { min-width: 0; overflow: auto; }
15
+ .tabbar { display: flex; gap: 6px; padding: 12px; background: #fff; overflow: auto; }
16
+ .tabbar .tab { display: flex; align-items: center; gap: 4px; }
17
+ .micro-workspace { padding: 16px; }
18
+ @media (max-width: 760px) { .shell { grid-template-columns: 170px minmax(0, 1fr); } .page { padding: 12px; } }
@@ -0,0 +1 @@
1
+ # 预留跨模块业务类型目录;SDK 类型直接从对应依赖导入。
@@ -0,0 +1 @@
1
+ # 预留业务通用工具目录;没有复用需求时不创建空工具封装。
@@ -0,0 +1,123 @@
1
+ import { AxiosHttpClient } from '@axewo/fin-core/http/axios'
2
+
3
+ /**
4
+ * 应用独立的 Axios 请求实例;由 base 生成到基座和子应用,各项目自行定义拦截器。
5
+ * 仅共享模板与封装实现,不跨应用共享可变配置、请求实例或身份信息。
6
+ * 默认 /api/ 指向当前应用运行环境解析出的源,嵌入运行也不会自动代理到基座后端。
7
+ * 部署时须配置真实API网关/开发代理;跨域Cookie须显式启用withCredentials并由后端配置CORS。
8
+ * 如需Token,通过installRequestTokenProvider每次读取最新值;不要在模块初始化时缓存Token。
9
+ * 不擅自从window/localStorage读取基座凭据,不硬编码Token,不自动刷新登录或重试写请求。
10
+ * get/post等返回整个响应体;业务code/data解包和401会话协调由业务层明确实现。
11
+ * 响应拦截器须返回AxiosResponse,而不是直接返回response.data;错误保持AxiosError/isCancel语义。
12
+ */
13
+ export const http = new AxiosHttpClient({
14
+ baseURL: '/api/',
15
+ timeout: 30000,
16
+ withCredentials: false,
17
+ })
18
+
19
+ /** 按请求读取最新凭据;独立应用可显式安装自己的 provider,基座默认不读取 bridge。 */
20
+ export type RequestTokenProvider = (signal: AbortSignal) => Promise<string | null> | string | null
21
+ interface TokenBinding {
22
+ provider: RequestTokenProvider
23
+ api: URL
24
+ active: boolean
25
+ pending: Set<AbortController>
26
+ previous?: TokenBinding
27
+ }
28
+ let tokenBinding: TokenBinding | undefined
29
+ /**
30
+ * 在应用挂载时安装,卸载必须调用返回的还原函数;同时取消所有尚未完成的凭据读取。
31
+ * trustedApiBase 是业务明确配置的可信 API 目录(默认当前源 /api/),不是从请求的 baseURL 推断。
32
+ * 跨源网关必须显式传完整 HTTPS 地址,且与 http 的 baseURL 保持一致。
33
+ * @param provider 每次读取最新凭据,接收请求取消信号;null 代表 Cookie 模式,不伪造已登录。
34
+ * @param trustedApiBase 可信 API 目录,必须以 / 结束;SSR 无浏览器来源时只能显式使用绝对 HTTPS 地址。
35
+ * @returns 幂等释放函数,取消挂起读取并恢复仍有效的前一个绑定,不撤回已发出的网络请求。
36
+ * @throws 可信地址无效时立即抛错;provider 失败使本次请求拒绝,不降级匿名重发。
37
+ */
38
+ export function installRequestTokenProvider(provider: RequestTokenProvider, trustedApiBase = '/api/'): () => void {
39
+ const location = globalThis.location
40
+ if (!location && !/^https:\/\//i.test(trustedApiBase)) throw new Error('无浏览器来源时必须显式配置绝对 HTTPS API')
41
+ const api = new URL(trustedApiBase, location?.href)
42
+ if (!['http:', 'https:'].includes(api.protocol) || api.username || api.password || api.search || api.hash || !api.pathname.endsWith('/')) throw new Error('可信 API 必须是 HTTP(S) 目录地址')
43
+ if (api.protocol !== 'https:' && (!location || api.origin !== location.origin)) throw new Error('跨源可信 API 必须使用 HTTPS')
44
+ const binding: TokenBinding = { provider, api, active: true, pending: new Set(), previous: tokenBinding }
45
+ tokenBinding = binding
46
+ return () => {
47
+ binding.active = false
48
+ for (const controller of binding.pending) controller.abort()
49
+ binding.pending.clear()
50
+ if (tokenBinding === binding) {
51
+ tokenBinding = binding.previous
52
+ while (tokenBinding && !tokenBinding.active) tokenBinding = tokenBinding.previous
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 应用级请求拦截器:每次请求时处理当前应用的动态配置。
59
+ * 业务替换点:从应用的身份模块读取最新 Token,按后台协议设置请求头;
60
+ * 不在这里缓存初始 Token,也不默认读取基座的 window/localStorage 凭据。
61
+ * 必须返回 config;异常原样拒绝,由调用方继续处理。
62
+ */
63
+ http.interceptors.request.use(
64
+ async config => {
65
+ const binding = tokenBinding
66
+ if (!binding?.active) return config
67
+ const origin = globalThis.location?.href
68
+ if (!origin && !/^https:\/\//i.test(config.baseURL ?? '')) return config
69
+ const base = new URL(config.baseURL ?? '', origin)
70
+ const url = config.url ?? ''
71
+ // 不对绝对 URL、协议相对地址、反斜线或被覆盖的 baseURL 发送任何宿主凭据。
72
+ // 即使绝对 URL 指向可信源也拒绝自动注入,防止后续调用者无意扩大授权面。
73
+ if (url.trim() !== url || /[\u0000-\u001f\u007f]/.test(url) || /%(?:2f|5c)/i.test(url) || /^[a-z][a-z\d+.-]*:/i.test(url) || url.startsWith('//') || url.includes('\\') || base.href !== binding.api.href) return config
74
+ const target = new URL(url.replace(/^\/+/, ''), base)
75
+ if (target.origin !== binding.api.origin || !target.pathname.startsWith(binding.api.pathname)) return config
76
+ const controller = new AbortController()
77
+ const abort = () => controller.abort()
78
+ if (config.signal?.aborted) controller.abort()
79
+ config.signal?.addEventListener?.('abort', abort)
80
+ binding.pending.add(controller)
81
+ try {
82
+ if (controller.signal.aborted) throw new Error('凭据读取已取消')
83
+ // Promise 竞速确保不合作的独立 provider 也不会在卸载后继续发送请求。
84
+ const token = await new Promise<string | null>((resolve, reject) => {
85
+ const cancelled = () => reject(new Error('凭据读取已取消'))
86
+ controller.signal.addEventListener('abort', cancelled, { once: true })
87
+ // 竞速一旦结束立即释放监听,不等待不合作 provider 的底层 Promise。
88
+ const settle = <T>(callback: (value: T) => void, value: T) => {
89
+ controller.signal.removeEventListener('abort', cancelled)
90
+ callback(value)
91
+ }
92
+ Promise.resolve().then(() => binding.provider(controller.signal)).then(value => settle(resolve, value), error => settle(reject, error))
93
+ })
94
+ if (controller.signal.aborted || !binding.active || tokenBinding !== binding) throw new Error('凭据来源已释放')
95
+ if (token !== null && (typeof token !== 'string' || !token || token.length > 16384 || /[\s\u0000-\u001f\u007f]/.test(token))) throw new Error('凭据格式无效')
96
+ // Cookie 模式的 null 不添加头;不缓存凭据,不刷新登录、不自动重试写请求。
97
+ if (token !== null) config.headers.set('Authorization', `Bearer ${token}`)
98
+ return config
99
+ } finally {
100
+ binding.pending.delete(controller)
101
+ config.signal?.removeEventListener?.('abort', abort)
102
+ }
103
+ },
104
+ error => Promise.reject(error),
105
+ )
106
+
107
+ /**
108
+ * 应用级响应拦截器:业务码校验、错误提示及 401 会话协调在此按实际协议实现。
109
+ * 成功分支必须保留完整 AxiosResponse;http.get/post 等已统一返回 response.data,
110
+ * 此处再次解包会破坏客户端返回契约。业务失败可抛出应用定义的错误。
111
+ * 错误分支保持拒绝,不能吞掉错误、返回 undefined 或自动重试写请求;
112
+ * 如需登录失效处理,应协调并发请求,取消请求通常不展示错误提示。
113
+ */
114
+ http.interceptors.response.use(
115
+ response => {
116
+ // 业务替换点:检查 response.data 中的业务状态,不假设固定 code/data 结构。
117
+ return response
118
+ },
119
+ error => {
120
+ // 业务替换点:按应用规范区分取消、网络异常、HTTP 401 及其他错误。
121
+ return Promise.reject(error)
122
+ },
123
+ )
@@ -0,0 +1 @@
1
+ # 预留页面业务视图目录;布局与跨页面逻辑分别放入 layout 和 hooks。
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "isolatedModules": true,
10
+ "resolveJsonModule": true,
11
+ "esModuleInterop": true,
12
+ "noEmit": true,
13
+ "types": ["vite/client"]
14
+ },
15
+ "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
16
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ // 只引用已发布依赖,不配置指向 monorepo 源码的 alias。
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ server: { port: __PORT__, strictPort: true, cors: true },
8
+ })
@@ -0,0 +1,5 @@
1
+ # 仅本地演示可打开;正式部署必须接入 src/platform.ts 的真实认证接口。
2
+ VITE_AUTH_DEMO=false
3
+ # 业务子应用地址和 appId 必须与子应用部署配置一致;仅允许可信 HTTP(S) 地址。
4
+ VITE_MICRO_APP_ID=business
5
+ VITE_MICRO_APP_ENTRY=http://localhost:7101
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import AppProviders from './components/AppProviders.vue'
3
+ import HostLayout from './layout/HostLayout.vue'
4
+ import LoginView from './views/LoginView.vue'
5
+ import { usePlatformSession } from './hooks/usePlatformSession'
6
+
7
+ // 根组件只切换登录页与已认证工作区;身份就绪前不创建微前端宿主。
8
+ const { session, loading, error, signIn, restore, signOut } = usePlatformSession()
9
+ </script>
10
+
11
+ <template>
12
+ <AppProviders>
13
+ <HostLayout v-if="session" :key="session.user.id" :session="session" @logout="signOut" />
14
+ <LoginView v-else :loading="loading" :error="error" :sign-in="signIn" @restore="restore" />
15
+ </AppProviders>
16
+ </template>
@@ -0,0 +1,18 @@
1
+ import { createApp, type App as VueApp } from 'vue'
2
+ import ElementPlus from 'element-plus'
3
+ import App from './App.vue'
4
+ import { installAppLocale } from './locales'
5
+ let app: VueApp | undefined
6
+
7
+ /** 幂等释放整个认证与宿主组件树,组件内负责停止请求、会话订阅及 runtime。 */
8
+ export function unmount(): void { app?.unmount(); app = undefined }
9
+
10
+ /** 无参数启动平台;宿主只会在 App 完成认证后创建,不在此绕过登录门禁。 */
11
+ export async function mount(): Promise<void> {
12
+ unmount()
13
+ app = createApp(App)
14
+ // 根组件 setup 前安装,登录页和工作区共用当前应用的语言上下文。
15
+ installAppLocale(app)
16
+ app.use(ElementPlus)
17
+ app.mount('#app')
18
+ }