mvframe 1.0.14 → 1.0.15

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { u as Ne } from "./util.js";
2
2
  import { d as Ke } from "./directive.js";
3
- import { s as Te, p as Ae, a as Fe } from "./store.js";
3
+ import { s as Te, p as Ae, a as Fe } from "./store-shared.js";
4
4
  import { createRouter as De, createWebHistory as Be, useRoute as Me, useRouter as Ve } from "vue-router";
5
5
  import { computed as O, openBlock as b, createElementBlock as M, mergeProps as oe, unref as n, Fragment as Y, renderList as se, normalizeClass as F, createCommentVNode as K, createTextVNode as W, toDisplayString as L, reactive as H, onUnmounted as ce, watch as ne, markRaw as me, resolveComponent as E, createVNode as z, withCtx as V, createElementVNode as r, renderSlot as R, createBlock as B, resolveDynamicComponent as Oe, nextTick as ge, getCurrentInstance as ae, ref as G, onMounted as ee, normalizeStyle as Le, defineComponent as Ie, cloneVNode as _e, h as we, inject as he, withModifiers as $e, defineAsyncComponent as Pe, resolveDirective as Re, normalizeProps as de, guardReactiveProps as fe, createSlots as ie, withDirectives as He, useSlots as Ue, Transition as Ge, withKeys as Je, createStaticVNode as qe, useAttrs as ve, isRef as je, onBeforeMount as We } from "vue";
6
6
  /* empty css */
@@ -2983,7 +2983,7 @@ const Tt = Ie({
2983
2983
  }, sl = {
2984
2984
  name: "Matt Avias Frame",
2985
2985
  copyright: "©2026",
2986
- version: "1.0.14",
2986
+ version: "1.0.15",
2987
2987
  author: "Matt Avias",
2988
2988
  date: "2026-02-26",
2989
2989
  /** 默认语言 key,与 `$getLang`、localStorage `lang` 一致;业务在 app.use(mvframe, { config }) 里覆盖 */
@@ -0,0 +1,162 @@
1
+ import { createPinia as p, defineStore as h } from "pinia";
2
+ const T = () => ({
3
+ lang: "en_us",
4
+ /** 路由切换 / 异步页面 chunk 加载期间为 true,由 router guard 维护 */
5
+ pageLoading: !1
6
+ }), y = {
7
+ saveData(t, e) {
8
+ this[t] = e;
9
+ },
10
+ setLang(t) {
11
+ this.lang = t;
12
+ },
13
+ setPageLoading(t) {
14
+ this.pageLoading = !!t;
15
+ }
16
+ }, $ = {
17
+ state: T,
18
+ actions: y
19
+ }, d = () => ({
20
+ useTab: !1,
21
+ // 是否使用tab
22
+ tabs: [],
23
+ ctab: {}
24
+ // current tab
25
+ }), S = {
26
+ saveData(t, e) {
27
+ this[t] = e;
28
+ },
29
+ saveCurrentTab(t) {
30
+ this.ctab = t, localStorage.setItem("ctab", JSON.stringify(t));
31
+ },
32
+ saveTab(t) {
33
+ const { data: e, index: s } = this.tabs.filter1((r) => r.name === t.name);
34
+ if (e)
35
+ if (JSON.stringify(e.params) === JSON.stringify(t.params) && JSON.stringify(e.query) === JSON.stringify(t.query)) {
36
+ this.saveCurrentTab(e);
37
+ return;
38
+ } else
39
+ this.closeTab(e, s);
40
+ const { fullPath: a, name: n, meta: i, params: c, query: u } = t;
41
+ if (n !== "Login") {
42
+ const r = a.split("/");
43
+ let l;
44
+ r[r.length - 1] === "Home" ? l = r[r.length - 2] : l = r[r.length - 1], i.icon && (l = i.icon);
45
+ const b = {
46
+ name: n,
47
+ meta: i,
48
+ path: r,
49
+ params: c,
50
+ query: u,
51
+ icon: l
52
+ }, { index: m } = this.tabs.filter1((g) => g.name === this.ctab.name);
53
+ this.tabs.splice(m + 1, 0, b), this.saveCurrentTab(b);
54
+ }
55
+ },
56
+ closeTab(t, e) {
57
+ var s;
58
+ this.tabs.splice(e, 1), t.name === this.ctab.name && ((s = globalThis.$router) == null || s.push({ name: this.tabs[e - 1 > 0 ? e - 1 : 0].name }));
59
+ },
60
+ closeRightTab(t, e) {
61
+ var n;
62
+ const s = [];
63
+ let a = !1;
64
+ this.tabs.forEach((i, c) => {
65
+ e >= c ? s.push(i) : i.name === this.ctab.name && (a = !0);
66
+ }), this.tabs = s, a && ((n = globalThis.$router) == null || n.push({ name: t.name }));
67
+ },
68
+ closeLeftTab(t, e) {
69
+ var n;
70
+ const s = [];
71
+ let a = !1;
72
+ this.tabs.forEach((i, c) => {
73
+ e <= c ? s.push(i) : i.name === this.ctab.name && (a = !0);
74
+ }), this.tabs = s, a && ((n = globalThis.$router) == null || n.push({ name: t.name }));
75
+ },
76
+ closeOtherTab(t, e) {
77
+ var s;
78
+ this.tabs = [t], t.name !== this.ctab.name && ((s = globalThis.$router) == null || s.push({ name: t.name }));
79
+ },
80
+ closeAllTab() {
81
+ this.tabs = [];
82
+ }
83
+ }, v = {}, w = {
84
+ state: d,
85
+ actions: S,
86
+ getters: v
87
+ }, O = () => ({
88
+ type: "",
89
+ visible: !1,
90
+ options: [],
91
+ el: null,
92
+ position: {
93
+ x: 0,
94
+ y: 0
95
+ }
96
+ }), L = {
97
+ saveData(t, e) {
98
+ this[t] = e;
99
+ },
100
+ show({ el: t, position: e }) {
101
+ this.visible = !0, e ? this.position = e : t && (this.el = t);
102
+ },
103
+ hide() {
104
+ this.visible = !1, this.el = null, this.position = {
105
+ x: 0,
106
+ y: 0
107
+ }, this.type = "";
108
+ }
109
+ }, N = {}, J = {
110
+ state: O,
111
+ actions: L,
112
+ getters: N
113
+ }, x = p(), o = {}, D = (t) => {
114
+ const e = t.match(/(?:^|[/\\])([^/\\]+)\.js$/);
115
+ return e ? e[1] : null;
116
+ }, E = (t) => {
117
+ Object.entries(t).forEach(([e, s]) => {
118
+ const a = D(e);
119
+ if (!a || !(s != null && s.default)) {
120
+ console.warn(`Invalid store module: ${e}`);
121
+ return;
122
+ }
123
+ try {
124
+ o[a] = h(a, s.default);
125
+ } catch (n) {
126
+ console.error(`Failed to register store module '${a}':`, n);
127
+ }
128
+ });
129
+ };
130
+ o.init = h("init", $);
131
+ o.tab = h("tab", w);
132
+ o.rmenu = h("rmenu", J);
133
+ const f = (t) => {
134
+ try {
135
+ switch (window.$getType(t)) {
136
+ case "Array":
137
+ t.forEach(f);
138
+ break;
139
+ case "String":
140
+ const s = localStorage.getItem(t);
141
+ s && o.tab().saveData(t, JSON.parse(s));
142
+ break;
143
+ default:
144
+ t = [], console.warn(`Invalid key: ${t}`);
145
+ break;
146
+ }
147
+ } catch (e) {
148
+ console.error(`Failed to parse '${t}' from localStorage:`, e);
149
+ }
150
+ }, F = (t, { storeChips: e, useTab: s } = {}) => {
151
+ try {
152
+ t.use(x).provide("store", o);
153
+ } catch (a) {
154
+ throw new Error("Failed to inject store into app:", a);
155
+ }
156
+ return s && (o.tab().saveData("useTab", !0), f(["tabs", "ctab"])), e && E(e), o;
157
+ };
158
+ export {
159
+ F as a,
160
+ x as p,
161
+ o as s
162
+ };
package/dist/store.js CHANGED
@@ -1,162 +1,5 @@
1
- import { createPinia as p, defineStore as h } from "pinia";
2
- const T = () => ({
3
- lang: "en_us",
4
- /** 路由切换 / 异步页面 chunk 加载期间为 true,由 router guard 维护 */
5
- pageLoading: !1
6
- }), y = {
7
- saveData(t, e) {
8
- this[t] = e;
9
- },
10
- setLang(t) {
11
- this.lang = t;
12
- },
13
- setPageLoading(t) {
14
- this.pageLoading = !!t;
15
- }
16
- }, $ = {
17
- state: T,
18
- actions: y
19
- }, d = () => ({
20
- useTab: !1,
21
- // 是否使用tab
22
- tabs: [],
23
- ctab: {}
24
- // current tab
25
- }), S = {
26
- saveData(t, e) {
27
- this[t] = e;
28
- },
29
- saveCurrentTab(t) {
30
- this.ctab = t, localStorage.setItem("ctab", JSON.stringify(t));
31
- },
32
- saveTab(t) {
33
- const { data: e, index: s } = this.tabs.filter1((r) => r.name === t.name);
34
- if (e)
35
- if (JSON.stringify(e.params) === JSON.stringify(t.params) && JSON.stringify(e.query) === JSON.stringify(t.query)) {
36
- this.saveCurrentTab(e);
37
- return;
38
- } else
39
- this.closeTab(e, s);
40
- const { fullPath: a, name: n, meta: i, params: c, query: u } = t;
41
- if (n !== "Login") {
42
- const r = a.split("/");
43
- let l;
44
- r[r.length - 1] === "Home" ? l = r[r.length - 2] : l = r[r.length - 1], i.icon && (l = i.icon);
45
- const b = {
46
- name: n,
47
- meta: i,
48
- path: r,
49
- params: c,
50
- query: u,
51
- icon: l
52
- }, { index: m } = this.tabs.filter1((g) => g.name === this.ctab.name);
53
- this.tabs.splice(m + 1, 0, b), this.saveCurrentTab(b);
54
- }
55
- },
56
- closeTab(t, e) {
57
- var s;
58
- this.tabs.splice(e, 1), t.name === this.ctab.name && ((s = globalThis.$router) == null || s.push({ name: this.tabs[e - 1 > 0 ? e - 1 : 0].name }));
59
- },
60
- closeRightTab(t, e) {
61
- var n;
62
- const s = [];
63
- let a = !1;
64
- this.tabs.forEach((i, c) => {
65
- e >= c ? s.push(i) : i.name === this.ctab.name && (a = !0);
66
- }), this.tabs = s, a && ((n = globalThis.$router) == null || n.push({ name: t.name }));
67
- },
68
- closeLeftTab(t, e) {
69
- var n;
70
- const s = [];
71
- let a = !1;
72
- this.tabs.forEach((i, c) => {
73
- e <= c ? s.push(i) : i.name === this.ctab.name && (a = !0);
74
- }), this.tabs = s, a && ((n = globalThis.$router) == null || n.push({ name: t.name }));
75
- },
76
- closeOtherTab(t, e) {
77
- var s;
78
- this.tabs = [t], t.name !== this.ctab.name && ((s = globalThis.$router) == null || s.push({ name: t.name }));
79
- },
80
- closeAllTab() {
81
- this.tabs = [];
82
- }
83
- }, v = {}, w = {
84
- state: d,
85
- actions: S,
86
- getters: v
87
- }, O = () => ({
88
- type: "",
89
- visible: !1,
90
- options: [],
91
- el: null,
92
- position: {
93
- x: 0,
94
- y: 0
95
- }
96
- }), L = {
97
- saveData(t, e) {
98
- this[t] = e;
99
- },
100
- show({ el: t, position: e }) {
101
- this.visible = !0, e ? this.position = e : t && (this.el = t);
102
- },
103
- hide() {
104
- this.visible = !1, this.el = null, this.position = {
105
- x: 0,
106
- y: 0
107
- }, this.type = "";
108
- }
109
- }, N = {}, J = {
110
- state: O,
111
- actions: L,
112
- getters: N
113
- }, x = p(), o = {}, D = (t) => {
114
- const e = t.match(/(?:^|[/\\])([^/\\]+)\.js$/);
115
- return e ? e[1] : null;
116
- }, E = (t) => {
117
- Object.entries(t).forEach(([e, s]) => {
118
- const a = D(e);
119
- if (!a || !(s != null && s.default)) {
120
- console.warn(`Invalid store module: ${e}`);
121
- return;
122
- }
123
- try {
124
- o[a] = h(a, s.default);
125
- } catch (n) {
126
- console.error(`Failed to register store module '${a}':`, n);
127
- }
128
- });
129
- };
130
- o.init = h("init", $);
131
- o.tab = h("tab", w);
132
- o.rmenu = h("rmenu", J);
133
- const f = (t) => {
134
- try {
135
- switch (window.$getType(t)) {
136
- case "Array":
137
- t.forEach(f);
138
- break;
139
- case "String":
140
- const s = localStorage.getItem(t);
141
- s && o.tab().saveData(t, JSON.parse(s));
142
- break;
143
- default:
144
- t = [], console.warn(`Invalid key: ${t}`);
145
- break;
146
- }
147
- } catch (e) {
148
- console.error(`Failed to parse '${t}' from localStorage:`, e);
149
- }
150
- }, F = (t, { storeChips: e, useTab: s } = {}) => {
151
- try {
152
- t.use(x).provide("store", o);
153
- } catch (a) {
154
- throw new Error("Failed to inject store into app:", a);
155
- }
156
- return s && (o.tab().saveData("useTab", !0), f(["tabs", "ctab"])), e && E(e), o;
157
- };
1
+ import { p as o, s as p } from "./store-shared.js";
158
2
  export {
159
- F as a,
160
- x as p,
161
- o as s
3
+ o as pinia,
4
+ p as store
162
5
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mvframe",
3
3
  "packageManager": "yarn@4.4.1",
4
- "version": "1.0.14",
4
+ "version": "1.0.15",
5
5
  "author": "matt avis",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -128,6 +128,7 @@ import ElementPlus from "element-plus";
128
128
  import "element-plus/dist/index.css";
129
129
  import App from "./App.vue";
130
130
  import mvframe from "mvframe";
131
+ import { pinia, store } from "mvframe/store";
131
132
  import routes from "./router/index.js";
132
133
  import appConfig from "./config/index.js";
133
134
  import "./assets/style/index.scss";
@@ -136,9 +137,42 @@ import "mvframe/style/cpt";
136
137
 
137
138
  const app = createApp(App);
138
139
  app.use(ElementPlus);
140
+
141
+ /** \`meta.public\` 为公开路由;已登录访问登录页则进首页。与 demo 一致:\`store.launch(pinia)\` 来自 \`mvframe/store\`(与插件注入的是同一单例)。 */
142
+ const launchRouteGuard = (to, from, next) => {
143
+ if (!store.launch) {
144
+ next();
145
+ return;
146
+ }
147
+ const launch = store.launch(pinia);
148
+ const authed = Boolean(launch.login);
149
+ const isPublic = to.matched.some((r) => r.meta?.public);
150
+ if (!authed && !isPublic) {
151
+ next({ name: "Entry", replace: true });
152
+ return;
153
+ }
154
+ if (authed && to.name === "Entry") {
155
+ next({ name: "Home_Home", replace: true });
156
+ return;
157
+ }
158
+ next();
159
+ };
160
+
139
161
  app.use(mvframe, {
140
162
  vueRouter: {
141
163
  routes,
164
+ guard: (router) => {
165
+ router.beforeEach(launchRouteGuard);
166
+ },
167
+ useAdmin: true,
168
+ adminPermission: () => Boolean(store.launch(pinia).login),
169
+ noaccess: (to, from, next) => {
170
+ if (from.path === "/") {
171
+ next();
172
+ } else {
173
+ next({ name: "Entry" });
174
+ }
175
+ },
142
176
  },
143
177
  pinia: {
144
178
  useTab: true,
@@ -153,66 +187,40 @@ app.mount("#app");
153
187
  write(
154
188
  "src/App.vue",
155
189
  `<template>
156
- <Frame class="App" :menu="menu" :page="{}">
157
- <template #logo>
158
- <span class="logo-placeholder">Logo</span>
159
- </template>
160
- <template #logomini>
161
- <span class="logo-mini">L</span>
162
- </template>
163
- </Frame>
190
+ <AdminEntry v-if="isLoggedIn" />
191
+ <Entry v-else />
164
192
  </template>
165
193
  <script setup>
166
- import routes from "./router/index.js";
194
+ import Entry from "./views/Launch/Entry.vue";
195
+ import AdminEntry from "./views/Launch/AdminEntry.vue";
167
196
 
168
197
  defineOptions({
169
198
  name: "App",
170
199
  inheritAttrs: false,
171
200
  });
172
201
 
173
- const menu = {
174
- iconClass: "imicon",
175
- routes,
176
- };
202
+ const store = inject("store");
203
+ const launch = store.launch();
204
+
205
+ const isLoggedIn = computed(() => Boolean(launch.login));
177
206
  </script>
178
- <style lang="scss" scoped>
179
- .logo-placeholder {
180
- display: inline-block;
181
- min-width: 7.5rem;
182
- padding: 0.25rem 0.5rem;
183
- border-radius: 0.25rem;
184
- background: var(--color-primary, #16b1ff);
185
- color: #fff;
186
- font-size: 0.875rem;
187
- }
188
- .logo-mini {
189
- display: inline-flex;
190
- width: 1.875rem;
191
- height: 1.875rem;
192
- align-items: center;
193
- justify-content: center;
194
- border-radius: 0.25rem;
195
- background: var(--color-green, #20c997);
196
- color: #fff;
197
- font-size: 0.75rem;
198
- }
199
- </style>
207
+ <style lang="scss" scoped></style>
200
208
  `,
201
209
  );
202
210
 
203
211
  write(
204
212
  "src/router/baseRouter.js",
205
213
  `/**
206
- * 基础路由:登录页、公开页、与后台权限无关的入口。
207
- * 权限接口返回后若要合并,可在本文件导出函数内拼接,或与 adminRouter 在 index 中组合。
214
+ * 基础路由:登录等 \`meta.public\` 公开页(不写 public 则需登录,由 main.js 内 launch 守卫处理)。
208
215
  */
209
216
  export default [
210
217
  {
211
218
  path: "/",
212
- name: "Home_Home",
213
- component: () => import("@/views/Home/Home.vue"),
219
+ name: "Entry",
220
+ component: () => import("@/views/Launch/LoginPage.vue"),
214
221
  meta: {
215
- title: "Home",
222
+ title: "Login",
223
+ public: true,
216
224
  },
217
225
  },
218
226
  ];
@@ -222,10 +230,18 @@ export default [
222
230
  write(
223
231
  "src/router/adminRouter.js",
224
232
  `/**
225
- * 后台 / 业务路由:可按接口权限过滤后追加,或整段替换。
233
+ * 后台 / 业务路由(需登录后访问):可按接口权限过滤后追加,或整段替换。
226
234
  * meta.admin 等字段可与 mvframe vueRouter.useAdmin / adminPermission 配合。
227
235
  */
228
236
  export default [
237
+ {
238
+ path: "/home",
239
+ name: "Home_Home",
240
+ component: () => import("@/views/Home/Home.vue"),
241
+ meta: {
242
+ title: "Home",
243
+ },
244
+ },
229
245
  {
230
246
  path: "/admin",
231
247
  name: "Admin_Dashboard",
@@ -288,6 +304,136 @@ export default {
288
304
  `,
289
305
  );
290
306
 
307
+ write(
308
+ "src/pinia/chip/launch.js",
309
+ `/** 登录态:与 demo/pinia/launch.js、main.js launchRouteGuard、LoginPage 一致 */
310
+ export default {
311
+ state: () => ({
312
+ userinfo: {},
313
+ login: false,
314
+ }),
315
+ actions: {
316
+ /** 登录成功:合并写入 userinfo,并置 login: true */
317
+ setLogin(payload = {}) {
318
+ this.userinfo = {
319
+ ...this.userinfo,
320
+ ...payload,
321
+ };
322
+ this.login = true;
323
+ },
324
+ clearLogin() {
325
+ this.userinfo = {};
326
+ this.login = false;
327
+ },
328
+ },
329
+ };
330
+ `,
331
+ );
332
+
333
+ write(
334
+ "src/views/Launch/Entry.vue",
335
+ `<template>
336
+ <router-view />
337
+ </template>
338
+ <script setup>
339
+ defineOptions({
340
+ name: "LaunchEntry",
341
+ inheritAttrs: false,
342
+ });
343
+ </script>
344
+ `,
345
+ );
346
+
347
+ write(
348
+ "src/views/Launch/AdminEntry.vue",
349
+ `<template>
350
+ <Frame class="App wp100 vh100" :menu="frameMenu" :page="{}">
351
+ <template #logo>
352
+ <span class="logo-placeholder">Logo</span>
353
+ </template>
354
+ <template #logomini>
355
+ <span class="logo-mini">L</span>
356
+ </template>
357
+ </Frame>
358
+ </template>
359
+ <script setup>
360
+ import routes from "@/router/index.js";
361
+
362
+ defineOptions({
363
+ name: "AdminEntry",
364
+ inheritAttrs: false,
365
+ });
366
+
367
+ const frameMenu = {
368
+ iconClass: "imicon",
369
+ routes: routes.filter((r) => r.meta?.public !== true),
370
+ };
371
+ </script>
372
+ <style lang="scss" scoped>
373
+ .logo-placeholder {
374
+ display: inline-block;
375
+ min-width: 7.5rem;
376
+ padding: 0.25rem 0.5rem;
377
+ border-radius: 0.25rem;
378
+ background: var(--color-primary, #16b1ff);
379
+ color: #fff;
380
+ font-size: 0.875rem;
381
+ }
382
+ .logo-mini {
383
+ display: inline-flex;
384
+ width: 1.875rem;
385
+ height: 1.875rem;
386
+ align-items: center;
387
+ justify-content: center;
388
+ border-radius: 0.25rem;
389
+ background: var(--color-green, #20c997);
390
+ color: #fff;
391
+ font-size: 0.75rem;
392
+ }
393
+ </style>
394
+ `,
395
+ );
396
+
397
+ write(
398
+ "src/views/Launch/LoginPage.vue",
399
+ `<template>
400
+ <div class="LoginPage wp100 vh100">
401
+ <Login
402
+ :form-fileds="formFileds"
403
+ :login-methods="[0]"
404
+ @submit="onSubmit"
405
+ />
406
+ </div>
407
+ </template>
408
+ <script setup>
409
+ defineOptions({
410
+ name: "LoginPage",
411
+ inheritAttrs: false,
412
+ });
413
+
414
+ const router = useRouter();
415
+ const store = inject("store");
416
+ const launch = store.launch();
417
+
418
+ const onSubmit = (payload) => {
419
+ launch.setLogin(payload ?? {});
420
+ router.replace({ name: "Home_Home" }).catch(() => {});
421
+ };
422
+
423
+ /** 与 mvframe demo 一致:自定义提交字段名 account / pwd */
424
+ const formFileds = {
425
+ username: { valueKey: "account" },
426
+ password: { valueKey: "pwd" },
427
+ };
428
+ </script>
429
+ <style lang="scss" scoped>
430
+ .LoginPage {
431
+ box-sizing: border-box;
432
+ }
433
+ </style>
434
+ `,
435
+ );
436
+
291
437
  write(
292
438
  "src/api/index.js",
293
439
  `/**
@@ -335,7 +481,10 @@ code {
335
481
  "src/views/Home/Home.vue",
336
482
  `<template>
337
483
  <Page title="Home" subtitle="MVFrame 雏形页,可从此扩展 views 模块">
338
- <p>公开路由见 <code>src/router/baseRouter.js</code>;后台见 <code>adminRouter.js</code>,合并于 <code>index.js</code>。</p>
484
+ <p>
485
+ 登录入口为 <code>baseRouter</code> 的 <code>Entry</code>(<code>meta.public</code>);业务路由见
486
+ <code>adminRouter.js</code>,合并于 <code>router/index.js</code>,与侧栏菜单同源。
487
+ </p>
339
488
  </Page>
340
489
  </template>
341
490
  <script setup>
@@ -428,6 +577,10 @@ yarn install
428
577
 
429
578
  自动生成的 \`vite.config.js\` 已包含 \`unplugin-auto-import\`;\`dts: true\` 时类型默认写在项目根 \`auto-imports.d.ts\`。
430
579
 
580
+ ## Launch(登录壳)
581
+
582
+ 雏形与 **mvframe demo** 对齐:\`pinia/chip/launch.js\`(顶层 \`login\` + \`userinfo\`)、\`App.vue\`(\`<AdminEntry v-if />\` / \`<Entry v-else />\`)、\`main.js\` 内 \`import { store, pinia } from "mvframe/store"\` + \`launchRouteGuard\` + \`useAdmin\` / \`adminPermission\` / \`noaccess\`。未登录仅 \`meta.public\`;已登录进 \`Entry\` 会重定向到 \`Home_Home\`。若不需门禁,将 \`useAdmin\` 改为 \`false\` 并删除 \`adminPermission\` 与 \`noaccess\`。
583
+
431
584
  ### 报错 \`Could not resolve '@vue/shared'\`
432
585
 
433
586
  这是 **Vue 3 自带的底层包**,一般不必手写;在 **pnpm / 严格 node_modules** 等环境下可能未被提升到可被 Vite 解析的位置。脚手架已在 \`dependencies\` 中合并 \`@vue/shared\`(与 \`vue\` 同主版本);老项目可手动执行 \`yarn add @vue/shared@^3.5\`。