dishui 0.0.83 → 0.0.89

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 (100) hide show
  1. package/dist/components/Demo/index.js +2 -2
  2. package/dist/components/Demo/index.js.map +1 -1
  3. package/dist/components/Input/index.js +52 -62
  4. package/dist/components/Input/index.js.map +1 -1
  5. package/dist/components/LayoutSuite/index.d.ts +10 -0
  6. package/dist/components/LayoutSuite/index.js +27 -18
  7. package/dist/components/LayoutSuite/index.js.map +1 -1
  8. package/dist/components/Markdown/markdownComponents.js +1 -0
  9. package/dist/components/Markdown/markdownComponents.js.map +1 -1
  10. package/dist/components/Markdown/useMarkdownSource.js +46 -31
  11. package/dist/components/Markdown/useMarkdownSource.js.map +1 -1
  12. package/dist/components/Markdown/useMarkmapRenderer.js +44 -31
  13. package/dist/components/Markdown/useMarkmapRenderer.js.map +1 -1
  14. package/dist/components/Markdown/useMermaidRenderer.js +49 -31
  15. package/dist/components/Markdown/useMermaidRenderer.js.map +1 -1
  16. package/dist/components/Page/index.d.ts +18 -4
  17. package/dist/components/Page/index.js +47 -35
  18. package/dist/components/Page/index.js.map +1 -1
  19. package/dist/components/ProtectedRoute/index.d.ts +2 -1
  20. package/dist/components/ProtectedRoute/index.js +16 -6
  21. package/dist/components/ProtectedRoute/index.js.map +1 -1
  22. package/dist/components/Select/index.d.ts +6 -0
  23. package/dist/components/Select/index.js +166 -136
  24. package/dist/components/Select/index.js.map +1 -1
  25. package/dist/components/Tabs/index.d.ts +12 -1
  26. package/dist/components/Tabs/index.js +42 -17
  27. package/dist/components/Tabs/index.js.map +1 -1
  28. package/dist/components/Text/index.d.ts +1 -1
  29. package/dist/components/Text/index.js +2 -2
  30. package/dist/components/Text/index.js.map +1 -1
  31. package/dist/components/pro/ArticleContextMenu/index.js +7 -7
  32. package/dist/components/pro/CodeEditor/editors/CodeEditor.js +200 -189
  33. package/dist/components/pro/CodeEditor/editors/CodeEditor.js.map +1 -1
  34. package/dist/components/pro/CodeEditor/types.d.ts +3 -0
  35. package/dist/components/pro/DataTable/DataTable.d.ts +1 -1
  36. package/dist/components/pro/DataTable/DataTable.js +98 -240
  37. package/dist/components/pro/DataTable/DataTable.js.map +1 -1
  38. package/dist/components/pro/DataTable/components/TableBody.js +14 -16
  39. package/dist/components/pro/DataTable/components/TableBody.js.map +1 -1
  40. package/dist/components/pro/DataTable/components/TablePagination.d.ts +3 -9
  41. package/dist/components/pro/DataTable/components/TablePagination.js +29 -25
  42. package/dist/components/pro/DataTable/components/TablePagination.js.map +1 -1
  43. package/dist/components/pro/DataTable/tanstack-table.d.ts +2 -2
  44. package/dist/components/pro/DataTable/tanstack-table.js +29 -21
  45. package/dist/components/pro/DataTable/tanstack-table.js.map +1 -1
  46. package/dist/components/pro/DataTable/treeDataUtils.d.ts +2 -0
  47. package/dist/components/pro/DataTable/treeDataUtils.js +18 -19
  48. package/dist/components/pro/DataTable/treeDataUtils.js.map +1 -1
  49. package/dist/components/pro/DataTable/types.d.ts +24 -0
  50. package/dist/components/pro/DataTable/useDataTableModel.d.ts +16 -0
  51. package/dist/components/pro/DataTable/useDataTableModel.js +148 -0
  52. package/dist/components/pro/DataTable/useDataTableModel.js.map +1 -0
  53. package/dist/components/pro/FileExplorer/Breadcrumb.js +12 -12
  54. package/dist/components/pro/Flow/nodes/DataNodeView.js +87 -82
  55. package/dist/components/pro/Flow/nodes/DataNodeView.js.map +1 -1
  56. package/dist/components/pro/Flow/nodes/MediaContent.js +1 -1
  57. package/dist/components/pro/Flow/nodes/MediaContent.js.map +1 -1
  58. package/dist/components/pro/Flow/nodes/MediaOverlay.js +1 -1
  59. package/dist/components/pro/Flow/nodes/MediaOverlay.js.map +1 -1
  60. package/dist/components/pro/Flow/nodes/Model3DView.js +69 -63
  61. package/dist/components/pro/Flow/nodes/Model3DView.js.map +1 -1
  62. package/dist/components/pro/Flow/nodes/chartGeometryBudget.d.ts +14 -0
  63. package/dist/components/pro/Flow/nodes/chartGeometryBudget.js +76 -0
  64. package/dist/components/pro/Flow/nodes/chartGeometryBudget.js.map +1 -0
  65. package/dist/components/pro/Flow/nodes/demandRender.d.ts +11 -0
  66. package/dist/components/pro/Flow/nodes/demandRender.js +26 -0
  67. package/dist/components/pro/Flow/nodes/demandRender.js.map +1 -0
  68. package/dist/components/pro/RichTextEditor/SlashMenu.js +68 -58
  69. package/dist/components/pro/RichTextEditor/SlashMenu.js.map +1 -1
  70. package/dist/contexts/AppContext.d.ts +1 -0
  71. package/dist/contexts/AppContext.js +144 -145
  72. package/dist/contexts/AppContext.js.map +1 -1
  73. package/dist/contexts/authentication.d.ts +7 -0
  74. package/dist/contexts/authentication.js +22 -0
  75. package/dist/contexts/authentication.js.map +1 -0
  76. package/dist/dishui.css +1 -1
  77. package/dist/hooks/useControllableState.d.ts +6 -14
  78. package/dist/hooks/useControllableState.js +21 -15
  79. package/dist/hooks/useControllableState.js.map +1 -1
  80. package/dist/hooks/useTableColumns.d.ts +3 -3
  81. package/dist/hooks/useTableColumns.js +2 -2
  82. package/dist/hooks/useTableColumns.js.map +1 -1
  83. package/dist/hooks/useTableData.d.ts +11 -4
  84. package/dist/hooks/useTableData.js +44 -26
  85. package/dist/hooks/useTableData.js.map +1 -1
  86. package/dist/index-nostyled.d.ts +2 -2
  87. package/dist/index.d.ts +2 -2
  88. package/dist/index.js +97 -97
  89. package/dist/layouts/Sidebar.d.ts +3 -1
  90. package/dist/layouts/Sidebar.js +131 -122
  91. package/dist/layouts/Sidebar.js.map +1 -1
  92. package/dist/utils/i18n.js +1 -1
  93. package/dist/utils/i18n.js.map +1 -1
  94. package/dist/utils/routerBuilder.d.ts +7 -5
  95. package/dist/utils/routerBuilder.js +11 -15
  96. package/dist/utils/routerBuilder.js.map +1 -1
  97. package/dist/utils/safeStorage.d.ts +8 -0
  98. package/dist/utils/safeStorage.js +45 -0
  99. package/dist/utils/safeStorage.js.map +1 -0
  100. package/package.json +22 -5
@@ -1,138 +1,138 @@
1
1
  import { MobileSidebarToggleButton as e } from "../components/SidebarToggleButton/index.js";
2
2
  import t from "../layouts/Sidebar.js";
3
3
  import n from "../layouts/headbar/Headbar.js";
4
- import r, { createContext as i, useContext as a, useEffect as o, useState as s } from "react";
5
- import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
- import { Outlet as u, RouterProvider as d } from "@tanstack/react-router";
4
+ import { readStoredJSON as r, safeStorage as i, writeStoredJSON as a } from "../utils/safeStorage.js";
5
+ import { isUser as o, readStoredUser as s, requiresAuthentication as c } from "./authentication.js";
6
+ import l, { createContext as u, useContext as d, useEffect as f, useState as p } from "react";
7
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
8
+ import { Outlet as g, RouterProvider as _ } from "@tanstack/react-router";
7
9
  //#region src/contexts/AppContext.tsx
8
- var f = i(void 0), p = () => {
9
- let e = a(f);
10
+ var v = u(void 0), y = () => {
11
+ let e = d(v);
10
12
  if (!e) throw Error("useAppContext must be used within an AppShell");
11
13
  return e;
12
- }, m = () => p().auth, h = {}, g = () => {
13
- let { title: i, language: a, theme: d, menuItems: f, getMenuItems: m, themes: g, sidebarWidth: _, sidebarCollapsedWidth: v, navbarHeight: y } = p(), [b, x] = s(() => {
14
- let e = localStorage.getItem("sidebar-collapsed");
15
- return e ? JSON.parse(e) : !1;
16
- }), [S, C] = s(_ || 280), w = r.useRef(null), [T, E] = s(() => typeof window < "u" && window.innerWidth < 768), [D, O] = s(null);
17
- o(() => {
18
- let e = () => E(window.innerWidth < 768);
14
+ }, b = () => y().auth, x = {}, S = () => {
15
+ let { title: i, language: o, theme: s, menuItems: c, getMenuItems: u, themes: d, sidebarWidth: _, sidebarCollapsedWidth: v, navbarHeight: b } = y(), [S, C] = p(() => r("sidebar-collapsed", (e) => typeof e == "boolean", !1)), [w, T] = p(_ || 280), E = l.useRef(null), [D, O] = p(() => typeof window < "u" && window.innerWidth < 768), [k, A] = p(null);
16
+ f(() => {
17
+ let e = () => O(window.innerWidth < 768);
19
18
  return window.addEventListener("resize", e), () => window.removeEventListener("resize", e);
20
- }, []), r.useEffect(() => {
21
- _ && C(_);
22
- }, [_]), r.useEffect(() => {
23
- localStorage.setItem("sidebar-collapsed", JSON.stringify(b));
24
- }, [b]);
25
- let k = D ?? (T || b), A = () => {
26
- O(!k);
27
- }, j = T ? 16 : 128, M = (e) => {
28
- C(e);
29
- }, N = y || 64, P = h.layout || "sidebar", F = h.headbarLeft, I = typeof F == "function" ? F(A) : F;
30
- return P === "topbar" ? /* @__PURE__ */ l("div", {
19
+ }, []), l.useEffect(() => {
20
+ _ && T(_);
21
+ }, [_]), l.useEffect(() => {
22
+ a("sidebar-collapsed", S);
23
+ }, [S]);
24
+ let j = k ?? (D || S), M = () => {
25
+ let e = !j;
26
+ A(e), C(e);
27
+ }, N = D ? 16 : 128, P = (e) => {
28
+ T(e);
29
+ }, F = b || 64, I = x.layout || "sidebar", L = x.headbarLeft, R = typeof L == "function" ? L(M) : L;
30
+ return I === "topbar" ? /* @__PURE__ */ h("div", {
31
31
  className: "h-screen flex flex-col bg-base-100",
32
- "data-theme": d,
33
- children: [/* @__PURE__ */ c("div", {
32
+ "data-theme": s,
33
+ children: [/* @__PURE__ */ m("div", {
34
34
  style: {
35
- height: `${N}px`,
35
+ height: `${F}px`,
36
36
  flexShrink: 0
37
37
  },
38
- children: /* @__PURE__ */ c(n, {
39
- left: I,
40
- middle: h.headbarMiddle,
41
- right: h.headbarRight,
42
- height: N
38
+ children: /* @__PURE__ */ m(n, {
39
+ left: R,
40
+ middle: x.headbarMiddle,
41
+ right: x.headbarRight,
42
+ height: F
43
43
  })
44
- }), /* @__PURE__ */ c("main", {
44
+ }), /* @__PURE__ */ m("main", {
45
45
  className: "flex-1 overflow-y-auto",
46
46
  style: {
47
- paddingLeft: `${j}px`,
48
- paddingRight: `${j}px`,
47
+ paddingLeft: `${N}px`,
48
+ paddingRight: `${N}px`,
49
49
  paddingTop: "16px",
50
50
  paddingBottom: "16px"
51
51
  },
52
- children: /* @__PURE__ */ c(u, {})
52
+ children: /* @__PURE__ */ m(g, {})
53
53
  })]
54
- }) : /* @__PURE__ */ l("div", {
54
+ }) : /* @__PURE__ */ h("div", {
55
55
  className: "h-screen flex flex-col bg-base-100",
56
- "data-theme": d,
57
- children: [/* @__PURE__ */ c("div", {
56
+ "data-theme": s,
57
+ children: [/* @__PURE__ */ m("div", {
58
58
  style: {
59
- height: `${N}px`,
59
+ height: `${F}px`,
60
60
  flexShrink: 0
61
61
  },
62
- children: /* @__PURE__ */ c(n, {
63
- left: I,
64
- middle: h.headbarMiddle,
65
- right: h.headbarRight,
66
- height: N
62
+ children: /* @__PURE__ */ m(n, {
63
+ left: R,
64
+ middle: x.headbarMiddle,
65
+ right: x.headbarRight,
66
+ height: F
67
67
  })
68
- }), /* @__PURE__ */ l("div", {
68
+ }), /* @__PURE__ */ h("div", {
69
69
  className: "flex flex-1 overflow-hidden",
70
- style: { height: `calc(100vh - ${N}px)` },
70
+ style: { height: `calc(100vh - ${F}px)` },
71
71
  children: [
72
- /* @__PURE__ */ c("div", {
72
+ /* @__PURE__ */ m("div", {
73
73
  className: "flex-shrink-0",
74
- ref: w,
74
+ ref: E,
75
75
  style: {
76
76
  height: "100%",
77
77
  overflow: "visible"
78
78
  },
79
- children: /* @__PURE__ */ c(t, {
80
- isCollapsed: k,
81
- language: a,
82
- items: f,
83
- getMenuItems: m,
84
- onWidthChange: M,
85
- onToggle: A,
86
- initialWidth: S,
79
+ children: /* @__PURE__ */ m(t, {
80
+ isCollapsed: j,
81
+ language: o,
82
+ items: c,
83
+ getMenuItems: u,
84
+ onWidthChange: P,
85
+ onToggle: M,
86
+ initialWidth: w,
87
87
  collapsedWidth: v || 64
88
88
  })
89
89
  }),
90
- /* @__PURE__ */ c("main", {
90
+ /* @__PURE__ */ m("main", {
91
91
  className: "flex-1 overflow-y-auto",
92
92
  style: {
93
93
  height: "100%",
94
- paddingLeft: `${j}px`,
95
- paddingRight: `${j}px`,
94
+ paddingLeft: `${N}px`,
95
+ paddingRight: `${N}px`,
96
96
  paddingTop: "16px",
97
97
  paddingBottom: "16px"
98
98
  },
99
- children: /* @__PURE__ */ c(u, {})
99
+ children: /* @__PURE__ */ m(g, {})
100
100
  }),
101
- /* @__PURE__ */ c(e, {
102
- isCollapsed: k,
103
- onToggle: A
101
+ /* @__PURE__ */ m(e, {
102
+ isCollapsed: j,
103
+ onToggle: M
104
104
  })
105
105
  ]
106
106
  })]
107
107
  });
108
- }, _ = ({ router: e, children: t, title: n = "DiShUI", initialLang: r, initialTheme: i, menuItems: a, getMenuItems: l, themes: u, enableAuth: p = !1, onAuthRequired: m, protectedRoutes: g = [], publicRoutes: _ = ["/login", "/register"], loginUrl: v = "/login", authFailedCallback: y, layout: b = "sidebar", sidebarWidth: x = 280, sidebarCollapsedWidth: S = 64, navbarHeight: C = 64, searchConfig: w, headbarLeft: T, headbarMiddle: E, headbarRight: D }) => {
109
- h = {
108
+ }, C = ({ router: e, children: t, title: n = "DiShUI", initialLang: r, initialTheme: u, menuItems: d, getMenuItems: h, themes: g, enableAuth: y = !1, onAuthRequired: b, protectedRoutes: S = [], publicRoutes: C = ["/login", "/register"], loginUrl: w = "/login", authFailedCallback: T, layout: E = "sidebar", sidebarWidth: D = 280, sidebarCollapsedWidth: O = 64, navbarHeight: k = 64, searchConfig: A, headbarLeft: j, headbarMiddle: M, headbarRight: N }) => {
109
+ x = {
110
110
  title: n,
111
111
  initialLang: r,
112
- initialTheme: i,
113
- menuItems: a,
114
- getMenuItems: l,
115
- themes: u,
116
- enableAuth: p,
117
- onAuthRequired: m,
118
- protectedRoutes: g,
119
- publicRoutes: _,
120
- loginUrl: v,
121
- authFailedCallback: y,
122
- layout: b,
123
- sidebarWidth: x,
124
- sidebarCollapsedWidth: S,
125
- navbarHeight: C,
126
- searchConfig: w,
127
- headbarLeft: T,
128
- headbarMiddle: E,
129
- headbarRight: D
112
+ initialTheme: u,
113
+ menuItems: d,
114
+ getMenuItems: h,
115
+ themes: g,
116
+ enableAuth: y,
117
+ onAuthRequired: b,
118
+ protectedRoutes: S,
119
+ publicRoutes: C,
120
+ loginUrl: w,
121
+ authFailedCallback: T,
122
+ layout: E,
123
+ sidebarWidth: D,
124
+ sidebarCollapsedWidth: O,
125
+ navbarHeight: k,
126
+ searchConfig: A,
127
+ headbarLeft: j,
128
+ headbarMiddle: M,
129
+ headbarRight: N
130
130
  };
131
- let [O, k] = s(() => {
132
- let e = localStorage.getItem("app-language");
131
+ let [P, F] = p(() => {
132
+ let e = i.getItem("app-language");
133
133
  return e === "zh" || e === "en" ? e : r || "zh";
134
- }), [A, j] = s(() => {
135
- let e = localStorage.getItem("app-theme");
134
+ }), [I, L] = p(() => {
135
+ let e = i.getItem("app-theme");
136
136
  return e && [
137
137
  "light",
138
138
  "dark",
@@ -147,74 +147,73 @@ var f = i(void 0), p = () => {
147
147
  "mintgirl",
148
148
  "wuxing",
149
149
  "jukebox"
150
- ].includes(e) ? e : i || "light";
151
- }), [M, N] = s(() => {
152
- if (!p) return null;
153
- let e = localStorage.getItem("app-user");
154
- return e ? JSON.parse(e) : null;
155
- });
156
- o(() => {
157
- localStorage.setItem("app-language", O);
158
- }, [O]), o(() => {
159
- localStorage.setItem("app-theme", A), document.documentElement.setAttribute("data-theme", A);
160
- }, [A]), o(() => {
161
- p && (M ? localStorage.setItem("app-user", JSON.stringify(M)) : localStorage.removeItem("app-user"));
162
- }, [M, p]);
163
- let P = (e) => {
164
- N(e);
165
- }, F = () => {
166
- N(null), localStorage.removeItem("app-user");
167
- }, I = (e) => M?.roles.includes(e) || !1, L = (e) => e.some((e) => M?.roles.includes(e)) || !1;
168
- o(() => {
169
- if (!p || !e) return;
170
- let t = e.subscribe("onBeforeLoad", ({ pathChanged: e, toLocation: t }) => {
171
- if (!e) return;
172
- let n = _.some((e) => t.pathname.startsWith(e));
173
- if ((g.length === 0 || g.some((e) => t.pathname.startsWith(e))) && !n && !M) {
174
- if (m && m()) return;
175
- y ? y(t.pathname, v) : window.location.href = v;
150
+ ].includes(e) ? e : u || "light";
151
+ }), [R, z] = p(() => y ? s() : null), B = l.useRef(R), V = l.useRef(null);
152
+ f(() => {
153
+ i.setItem("app-language", P);
154
+ }, [P]), f(() => {
155
+ i.setItem("app-theme", I), typeof document < "u" && document.documentElement.setAttribute("data-theme", I);
156
+ }, [I]), f(() => {
157
+ y && (R ? a("app-user", R) : i.removeItem("app-user"));
158
+ }, [R, y]);
159
+ let H = (e) => {
160
+ if (!o(e)) throw TypeError("Invalid user: expected id, name, email and string roles");
161
+ B.current = e, V.current = null, z(e), y && a("app-user", e);
162
+ }, U = () => {
163
+ B.current = null, z(null), i.removeItem("app-user");
164
+ }, W = (e) => R?.roles.includes(e) || !1, G = (e) => e.some((e) => R?.roles.includes(e)) || !1;
165
+ f(() => {
166
+ if (!y || !e) {
167
+ V.current = null;
168
+ return;
169
+ }
170
+ let t = (e, t = B.current) => {
171
+ if (t || !c(e, S, C)) {
172
+ V.current = null;
173
+ return;
176
174
  }
175
+ V.current !== e && (V.current = e, !b?.() && (T ? T(e, w) : typeof window < "u" && (window.location.href = w)));
176
+ }, n = e.subscribe("onBeforeLoad", ({ toLocation: e }) => {
177
+ t(e.pathname);
177
178
  });
178
- return () => {
179
- t && t();
180
- };
179
+ return t(e.state.location.pathname, R), n;
181
180
  }, [
182
- p,
183
- M,
181
+ y,
182
+ R,
184
183
  e,
185
- g,
186
- _,
187
- m,
188
- v,
189
- y
184
+ S,
185
+ C,
186
+ b,
187
+ w,
188
+ T
190
189
  ]);
191
- let R = {
190
+ let K = {
192
191
  title: n,
193
- language: O,
194
- theme: A,
195
- setLanguage: k,
196
- setTheme: j,
192
+ language: P,
193
+ theme: I,
194
+ setLanguage: F,
195
+ setTheme: L,
197
196
  auth: {
198
- user: M,
199
- isAuthenticated: !!M,
200
- login: P,
201
- logout: F,
202
- hasRole: I,
203
- hasAnyRole: L
197
+ user: R,
198
+ isAuthenticated: !!R,
199
+ login: H,
200
+ logout: U,
201
+ hasRole: W,
202
+ hasAnyRole: G
204
203
  },
205
- menuItems: a,
206
- getMenuItems: l,
207
- themes: u,
208
- sidebarWidth: x,
209
- sidebarCollapsedWidth: S,
210
- navbarHeight: C
204
+ menuItems: d,
205
+ getMenuItems: h,
206
+ themes: g,
207
+ sidebarWidth: D,
208
+ sidebarCollapsedWidth: O,
209
+ navbarHeight: k
211
210
  };
212
- return /* @__PURE__ */ c(f.Provider, {
213
- value: R,
214
- children: e ? t || /* @__PURE__ */ c(d, { router: e }) : t
211
+ return /* @__PURE__ */ m(v.Provider, {
212
+ value: K,
213
+ children: e ? t || /* @__PURE__ */ m(_, { router: e }) : t
215
214
  });
216
- }, v = _;
215
+ }, w = C;
217
216
  //#endregion
218
- export { g as AppLayoutWrapper, v as AppProvider, _ as AppShell, _ as default, p as useAppContext, m as useAuth };
217
+ export { S as AppLayoutWrapper, w as AppProvider, C as AppShell, C as default, y as useAppContext, b as useAuth };
219
218
 
220
219
  //# sourceMappingURL=AppContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppContext.js","names":[],"sources":["../../src/contexts/AppContext.tsx"],"sourcesContent":["import React, { createContext, useContext, useState, useEffect } from 'react';\nimport { RouterProvider } from '@tanstack/react-router';\nimport { Router } from '@tanstack/react-router';\nimport Sidebar from '../layouts/Sidebar';\nimport { Headbar } from '../layouts/headbar';\nimport type { SearchConfig } from '../layouts/headbar';\nimport { Outlet } from '@tanstack/react-router';\nimport { MenuItem } from '../types/menu';\nimport { ThemeConfig } from '../utils/themes';\nimport { MobileSidebarToggleButton } from '../components/SidebarToggleButton';\n\n// 主题类型\nexport type ThemeType = 'light' | 'dark' | 'cursor' | 'retro' | 'cyberpunk' | 'matrix' | 'oceanblue' | 'eyecare' | 'ghibli' | 'pinkgirl' | 'mintgirl' | 'wuxing' | 'jukebox';\n\n// 用户类型定义\nexport type User = {\n id: string;\n name: string;\n email: string;\n roles: string[];\n};\n\n// 认证上下文类型\nexport type AuthContextType = {\n user: User | null;\n isAuthenticated: boolean;\n login: (user: User) => void;\n logout: () => void;\n hasRole: (role: string) => boolean;\n hasAnyRole: (roles: string[]) => boolean;\n};\n\n// 应用上下文类型\nexport type AppContextType = {\n title?: string;\n language: 'zh' | 'en';\n theme: ThemeType;\n setLanguage: (lang: 'zh' | 'en') => void;\n setTheme: (theme: ThemeType) => void;\n auth: AuthContextType;\n menuItems?: MenuItem[];\n getMenuItems?: (language: 'zh' | 'en') => MenuItem[];\n themes?: ThemeConfig[];\n sidebarWidth?: number;\n sidebarCollapsedWidth?: number;\n navbarHeight?: number;\n};\n\nconst AppContext = createContext<AppContextType | undefined>(undefined);\n\nexport const useAppContext = () => {\n const context = useContext(AppContext);\n if (!context) {\n throw new Error('useAppContext must be used within an AppShell');\n }\n return context;\n};\n\n// 导出独立的认证 hook\nexport const useAuth = () => {\n const context = useAppContext();\n return context.auth;\n};\n\n// AppShell 配置类型\nexport type AppShellConfig = {\n title?: string;\n initialLang?: 'zh' | 'en';\n initialTheme?: ThemeType;\n menuItems?: MenuItem[];\n getMenuItems?: (language: 'zh' | 'en') => MenuItem[];\n themes?: ThemeConfig[];\n enableAuth?: boolean;\n onAuthRequired?: () => boolean | void;\n protectedRoutes?: string[];\n publicRoutes?: string[];\n loginUrl?: string;\n authFailedCallback?: (path: string, loginUrl: string) => void;\n /** 布局模式: 'sidebar'(默认,顶栏+侧边栏) | 'topbar'(仅顶栏,无侧边栏) */\n layout?: 'sidebar' | 'topbar';\n sidebarWidth?: number;\n sidebarCollapsedWidth?: number;\n navbarHeight?: number; // Navbar 高度(像素),默认 64px\n searchConfig?: SearchConfig;\n /** Headbar 左侧插槽(传入 ReactNode 或接收 toggleSidebar 的渲染函数) */\n headbarLeft?: React.ReactNode | ((toggleSidebar: () => void) => React.ReactNode);\n /** Headbar 中间插槽 */\n headbarMiddle?: React.ReactNode;\n /** Headbar 右侧插槽 */\n headbarRight?: React.ReactNode;\n};\n\n// 全局配置存储\nlet globalConfig: AppShellConfig = {};\nlet globalAuthState: {\n user: User | null;\n setUser: React.Dispatch<React.SetStateAction<User | null>>;\n} | null = null;\n\n// 导出的布局包装器组件 - 用于路由\nexport const AppLayoutWrapper: React.FC = () => {\n const { title, language, theme, menuItems, getMenuItems, themes, sidebarWidth: configSidebarWidth, sidebarCollapsedWidth, navbarHeight: configNavbarHeight } = useAppContext();\n\n // 从 localStorage 读取侧边栏折叠状态\n const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {\n const saved = localStorage.getItem('sidebar-collapsed');\n return saved ? JSON.parse(saved) : false;\n });\n\n const [sidebarWidth, setSidebarWidth] = useState(configSidebarWidth || 280);\n const sidebarRef = React.useRef<HTMLDivElement>(null);\n\n // 窄屏(手机)适配:内容留白收窄,侧边栏默认折叠,用户可手动展开\n const [narrowViewport, setNarrowViewport] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n // null = 未手动干预,跟随默认规则(窄屏折叠 / 桌面跟随保存的偏好)\n const [sidebarUserOverride, setSidebarUserOverride] = useState<boolean | null>(null);\n\n useEffect(() => {\n const onResize = () => setNarrowViewport(window.innerWidth < 768);\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n // 当配置的 sidebarWidth 改变时,更新本地 state\n React.useEffect(() => {\n if (configSidebarWidth) {\n setSidebarWidth(configSidebarWidth);\n }\n }, [configSidebarWidth]);\n\n // 保存侧边栏折叠状态到 localStorage\n React.useEffect(() => {\n localStorage.setItem('sidebar-collapsed', JSON.stringify(sidebarCollapsed));\n }, [sidebarCollapsed]);\n\n const effectiveSidebarCollapsed = sidebarUserOverride ?? (narrowViewport || sidebarCollapsed);\n\n const toggleSidebar = () => {\n setSidebarUserOverride(!effectiveSidebarCollapsed);\n };\n\n const contentPaddingX = narrowViewport ? 16 : 128;\n\n const handleSidebarWidthChange = (width: number) => {\n setSidebarWidth(width);\n };\n\n const navbarHeight = configNavbarHeight || 64; // 默认 64px\n const layout = globalConfig.layout || 'sidebar';\n\n // 解析 headbar 插槽:支持 ReactNode 或渲染函数\n const headbarLeftConfig = globalConfig.headbarLeft;\n const resolvedLeft = typeof headbarLeftConfig === 'function'\n ? headbarLeftConfig(toggleSidebar)\n : headbarLeftConfig;\n\n // topbar 模式:仅顶部导航,无侧边栏\n if (layout === 'topbar') {\n return (\n <div className=\"h-screen flex flex-col bg-base-100\" data-theme={theme}>\n <div style={{ height: `${navbarHeight}px`, flexShrink: 0 }}>\n <Headbar\n left={resolvedLeft}\n middle={globalConfig.headbarMiddle}\n right={globalConfig.headbarRight}\n height={navbarHeight}\n />\n </div>\n <main className=\"flex-1 overflow-y-auto\" style={{ paddingLeft: `${contentPaddingX}px`, paddingRight: `${contentPaddingX}px`, paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n </div>\n );\n }\n\n // sidebar 模式(默认):顶栏 + 侧边栏\n return (\n <div className=\"h-screen flex flex-col bg-base-100\" data-theme={theme}>\n {/* 固定高度的 Headbar */}\n <div style={{ height: `${navbarHeight}px`, flexShrink: 0 }}>\n <Headbar\n left={resolvedLeft}\n middle={globalConfig.headbarMiddle}\n right={globalConfig.headbarRight}\n height={navbarHeight}\n />\n </div>\n\n {/* Sidebar 和 Content 区域 - 独立滚动 */}\n <div className=\"flex flex-1 overflow-hidden\" style={{ height: `calc(100vh - ${navbarHeight}px)` }}>\n {/* Sidebar - 独立滚动(overflow visible 以允许右边缘 pill 轻微越界) */}\n <div\n className=\"flex-shrink-0\"\n ref={sidebarRef}\n style={{ height: '100%', overflow: 'visible' }}\n >\n <Sidebar\n isCollapsed={effectiveSidebarCollapsed}\n language={language}\n items={menuItems}\n getMenuItems={getMenuItems}\n onWidthChange={handleSidebarWidthChange}\n onToggle={toggleSidebar}\n initialWidth={sidebarWidth}\n collapsedWidth={sidebarCollapsedWidth || 64}\n />\n </div>\n\n {/* Content - 独立滚动 */}\n <main className=\"flex-1 overflow-y-auto\" style={{ height: '100%', paddingLeft: `${contentPaddingX}px`, paddingRight: `${contentPaddingX}px`, paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n\n {/* 侧边栏切换控件:桌面端 pill 已由 Sidebar 内部渲染,此处仅保留移动端按钮 */}\n <MobileSidebarToggleButton\n isCollapsed={effectiveSidebarCollapsed}\n onToggle={toggleSidebar}\n />\n </div>\n </div>\n );\n};\n\n// AppShell 属性类型\nexport type AppShellProps = {\n router?: Router<any, any>;\n children?: React.ReactNode;\n} & AppShellConfig;\n\n// AppShell 主组件\nexport const AppShell: React.FC<AppShellProps> = ({\n router,\n children,\n title = 'DiShUI',\n initialLang,\n initialTheme,\n menuItems,\n getMenuItems,\n themes,\n enableAuth = false,\n onAuthRequired,\n protectedRoutes = [],\n publicRoutes = ['/login', '/register'],\n loginUrl = '/login',\n authFailedCallback,\n layout = 'sidebar',\n sidebarWidth = 280,\n sidebarCollapsedWidth = 64,\n navbarHeight = 64,\n searchConfig,\n headbarLeft,\n headbarMiddle,\n headbarRight,\n}) => {\n // 保存全局配置\n globalConfig = {\n title,\n initialLang,\n initialTheme,\n menuItems,\n getMenuItems,\n themes,\n enableAuth,\n onAuthRequired,\n protectedRoutes,\n publicRoutes,\n loginUrl,\n authFailedCallback,\n layout,\n sidebarWidth,\n sidebarCollapsedWidth,\n navbarHeight,\n searchConfig,\n headbarLeft,\n headbarMiddle,\n headbarRight,\n };\n\n // 语言状态管理\n const [language, setLanguage] = useState<'zh' | 'en'>(() => {\n const savedLanguage = localStorage.getItem('app-language');\n if (savedLanguage === 'zh' || savedLanguage === 'en') return savedLanguage;\n return initialLang || 'zh';\n });\n\n // 主题状态管理\n const [theme, setTheme] = useState<ThemeType>(() => {\n const savedTheme = localStorage.getItem('app-theme');\n const validThemes: ThemeType[] = ['light', 'dark', 'cursor', 'retro', 'cyberpunk', 'matrix', 'oceanblue', 'eyecare', 'ghibli', 'pinkgirl', 'mintgirl', 'wuxing', 'jukebox'];\n if (savedTheme && validThemes.includes(savedTheme as ThemeType)) {\n return savedTheme as ThemeType;\n }\n return initialTheme || 'light';\n });\n\n // 用户认证状态管理\n const [user, setUser] = useState<User | null>(() => {\n if (!enableAuth) return null;\n const savedUser = localStorage.getItem('app-user');\n return savedUser ? JSON.parse(savedUser) : null;\n });\n\n // 保存全局认证状态\n globalAuthState = { user, setUser };\n\n // 保存语言设置\n useEffect(() => {\n localStorage.setItem('app-language', language);\n }, [language]);\n\n // 保存主题设置\n useEffect(() => {\n localStorage.setItem('app-theme', theme);\n document.documentElement.setAttribute('data-theme', theme);\n }, [theme]);\n\n // 保存用户信息\n useEffect(() => {\n if (enableAuth) {\n if (user) {\n localStorage.setItem('app-user', JSON.stringify(user));\n } else {\n localStorage.removeItem('app-user');\n }\n }\n }, [user, enableAuth]);\n\n // 认证方法\n const login = (newUser: User) => {\n setUser(newUser);\n };\n\n const logout = () => {\n setUser(null);\n localStorage.removeItem('app-user');\n };\n\n const hasRole = (role: string): boolean => {\n return user?.roles.includes(role) || false;\n };\n\n const hasAnyRole = (roles: string[]): boolean => {\n return roles.some(role => user?.roles.includes(role)) || false;\n };\n\n // 路由守卫 - 检查认证状态\n useEffect(() => {\n if (!enableAuth || !router) return;\n\n const unsubscribe = router.subscribe('onBeforeLoad', ({ pathChanged, toLocation }) => {\n if (!pathChanged) return;\n\n const isPublicRoute = publicRoutes.some(route =>\n toLocation.pathname.startsWith(route)\n );\n\n const isProtectedRoute = protectedRoutes.length === 0 ||\n protectedRoutes.some(route => toLocation.pathname.startsWith(route));\n\n if (isProtectedRoute && !isPublicRoute && !user) {\n // 允许调用方先自行处理认证需求。返回 true 表示已处理,直接放行。\n if (onAuthRequired) {\n const handled = onAuthRequired();\n if (handled) return;\n }\n\n // 未被自定义逻辑处理时,执行认证失败回调或默认重定向。\n if (authFailedCallback) {\n authFailedCallback(toLocation.pathname, loginUrl);\n } else {\n window.location.href = loginUrl;\n }\n }\n });\n\n return () => {\n if (unsubscribe) unsubscribe();\n };\n }, [enableAuth, user, router, protectedRoutes, publicRoutes, onAuthRequired, loginUrl, authFailedCallback]);\n\n const authContext: AuthContextType = {\n user,\n isAuthenticated: !!user,\n login,\n logout,\n hasRole,\n hasAnyRole,\n };\n\n const value: AppContextType = {\n title,\n language,\n theme,\n setLanguage,\n setTheme,\n auth: authContext,\n menuItems,\n getMenuItems,\n themes,\n sidebarWidth,\n sidebarCollapsedWidth,\n navbarHeight,\n };\n\n return (\n <AppContext.Provider value={value}>\n {router ? (\n children || <RouterProvider router={router} />\n ) : (\n children\n )}\n </AppContext.Provider>\n );\n};\n\n// 保持向后兼容的 AppProvider 别名\nexport const AppProvider = AppShell;\n\nexport default AppShell;\n"],"mappings":"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"}
1
+ {"version":3,"file":"AppContext.js","names":[],"sources":["../../src/contexts/AppContext.tsx"],"sourcesContent":["import React, { createContext, useContext, useState, useEffect } from 'react';\nimport { RouterProvider } from '@tanstack/react-router';\nimport { Router } from '@tanstack/react-router';\nimport Sidebar from '../layouts/Sidebar';\nimport { Headbar } from '../layouts/headbar';\nimport type { SearchConfig } from '../layouts/headbar';\nimport { Outlet } from '@tanstack/react-router';\nimport { MenuItem } from '../types/menu';\nimport { ThemeConfig } from '../utils/themes';\nimport { MobileSidebarToggleButton } from '../components/SidebarToggleButton';\nimport { readStoredJSON, writeStoredJSON, safeStorage } from '../utils/safeStorage';\nimport { isUser, readStoredUser, requiresAuthentication } from './authentication';\n\n// 主题类型\nexport type ThemeType = 'light' | 'dark' | 'cursor' | 'retro' | 'cyberpunk' | 'matrix' | 'oceanblue' | 'eyecare' | 'ghibli' | 'pinkgirl' | 'mintgirl' | 'wuxing' | 'jukebox';\n\n// 用户类型定义\nexport type User = {\n id: string;\n name: string;\n email: string;\n roles: string[];\n};\n\n// 认证上下文类型\nexport type AuthContextType = {\n user: User | null;\n isAuthenticated: boolean;\n login: (user: User) => void;\n logout: () => void;\n hasRole: (role: string) => boolean;\n hasAnyRole: (roles: string[]) => boolean;\n};\n\n// 应用上下文类型\nexport type AppContextType = {\n title?: string;\n language: 'zh' | 'en';\n theme: ThemeType;\n setLanguage: (lang: 'zh' | 'en') => void;\n setTheme: (theme: ThemeType) => void;\n auth: AuthContextType;\n menuItems?: MenuItem[];\n getMenuItems?: (language: 'zh' | 'en') => MenuItem[];\n themes?: ThemeConfig[];\n sidebarWidth?: number;\n sidebarCollapsedWidth?: number;\n navbarHeight?: number;\n};\n\nconst AppContext = createContext<AppContextType | undefined>(undefined);\n\nexport const useAppContext = () => {\n const context = useContext(AppContext);\n if (!context) {\n throw new Error('useAppContext must be used within an AppShell');\n }\n return context;\n};\n\n// 导出独立的认证 hook\nexport const useAuth = () => {\n const context = useAppContext();\n return context.auth;\n};\n\n// AppShell 配置类型\nexport type AppShellConfig = {\n title?: string;\n initialLang?: 'zh' | 'en';\n initialTheme?: ThemeType;\n menuItems?: MenuItem[];\n getMenuItems?: (language: 'zh' | 'en') => MenuItem[];\n themes?: ThemeConfig[];\n /** Enables client auth state and navigation notifications, not loader protection. Use route beforeLoad for loaders. */\n enableAuth?: boolean;\n onAuthRequired?: () => boolean | void;\n protectedRoutes?: string[];\n publicRoutes?: string[];\n loginUrl?: string;\n authFailedCallback?: (path: string, loginUrl: string) => void;\n /** 布局模式: 'sidebar'(默认,顶栏+侧边栏) | 'topbar'(仅顶栏,无侧边栏) */\n layout?: 'sidebar' | 'topbar';\n sidebarWidth?: number;\n sidebarCollapsedWidth?: number;\n navbarHeight?: number; // Navbar 高度(像素),默认 64px\n searchConfig?: SearchConfig;\n /** Headbar 左侧插槽(传入 ReactNode 或接收 toggleSidebar 的渲染函数) */\n headbarLeft?: React.ReactNode | ((toggleSidebar: () => void) => React.ReactNode);\n /** Headbar 中间插槽 */\n headbarMiddle?: React.ReactNode;\n /** Headbar 右侧插槽 */\n headbarRight?: React.ReactNode;\n};\n\n// 全局配置存储\nlet globalConfig: AppShellConfig = {};\n\n// 导出的布局包装器组件 - 用于路由\nexport const AppLayoutWrapper: React.FC = () => {\n const { title, language, theme, menuItems, getMenuItems, themes, sidebarWidth: configSidebarWidth, sidebarCollapsedWidth, navbarHeight: configNavbarHeight } = useAppContext();\n\n // 从 localStorage 读取侧边栏折叠状态\n const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>\n readStoredJSON('sidebar-collapsed', (value): value is boolean => typeof value === 'boolean', false)\n );\n\n const [sidebarWidth, setSidebarWidth] = useState(configSidebarWidth || 280);\n const sidebarRef = React.useRef<HTMLDivElement>(null);\n\n // 窄屏(手机)适配:内容留白收窄,侧边栏默认折叠,用户可手动展开\n const [narrowViewport, setNarrowViewport] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);\n // null = 未手动干预,跟随默认规则(窄屏折叠 / 桌面跟随保存的偏好)\n const [sidebarUserOverride, setSidebarUserOverride] = useState<boolean | null>(null);\n\n useEffect(() => {\n const onResize = () => setNarrowViewport(window.innerWidth < 768);\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, []);\n\n // 当配置的 sidebarWidth 改变时,更新本地 state\n React.useEffect(() => {\n if (configSidebarWidth) {\n setSidebarWidth(configSidebarWidth);\n }\n }, [configSidebarWidth]);\n\n // 保存侧边栏折叠状态到 localStorage\n React.useEffect(() => {\n writeStoredJSON('sidebar-collapsed', sidebarCollapsed);\n }, [sidebarCollapsed]);\n\n const effectiveSidebarCollapsed = sidebarUserOverride ?? (narrowViewport || sidebarCollapsed);\n\n const toggleSidebar = () => {\n const collapsed = !effectiveSidebarCollapsed;\n setSidebarUserOverride(collapsed);\n setSidebarCollapsed(collapsed);\n };\n\n const contentPaddingX = narrowViewport ? 16 : 128;\n\n const handleSidebarWidthChange = (width: number) => {\n setSidebarWidth(width);\n };\n\n const navbarHeight = configNavbarHeight || 64; // 默认 64px\n const layout = globalConfig.layout || 'sidebar';\n\n // 解析 headbar 插槽:支持 ReactNode 或渲染函数\n const headbarLeftConfig = globalConfig.headbarLeft;\n const resolvedLeft = typeof headbarLeftConfig === 'function'\n ? headbarLeftConfig(toggleSidebar)\n : headbarLeftConfig;\n\n // topbar 模式:仅顶部导航,无侧边栏\n if (layout === 'topbar') {\n return (\n <div className=\"h-screen flex flex-col bg-base-100\" data-theme={theme}>\n <div style={{ height: `${navbarHeight}px`, flexShrink: 0 }}>\n <Headbar\n left={resolvedLeft}\n middle={globalConfig.headbarMiddle}\n right={globalConfig.headbarRight}\n height={navbarHeight}\n />\n </div>\n <main className=\"flex-1 overflow-y-auto\" style={{ paddingLeft: `${contentPaddingX}px`, paddingRight: `${contentPaddingX}px`, paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n </div>\n );\n }\n\n // sidebar 模式(默认):顶栏 + 侧边栏\n return (\n <div className=\"h-screen flex flex-col bg-base-100\" data-theme={theme}>\n {/* 固定高度的 Headbar */}\n <div style={{ height: `${navbarHeight}px`, flexShrink: 0 }}>\n <Headbar\n left={resolvedLeft}\n middle={globalConfig.headbarMiddle}\n right={globalConfig.headbarRight}\n height={navbarHeight}\n />\n </div>\n\n {/* Sidebar 和 Content 区域 - 独立滚动 */}\n <div className=\"flex flex-1 overflow-hidden\" style={{ height: `calc(100vh - ${navbarHeight}px)` }}>\n {/* Sidebar - 独立滚动(overflow visible 以允许右边缘 pill 轻微越界) */}\n <div\n className=\"flex-shrink-0\"\n ref={sidebarRef}\n style={{ height: '100%', overflow: 'visible' }}\n >\n <Sidebar\n isCollapsed={effectiveSidebarCollapsed}\n language={language}\n items={menuItems}\n getMenuItems={getMenuItems}\n onWidthChange={handleSidebarWidthChange}\n onToggle={toggleSidebar}\n initialWidth={sidebarWidth}\n collapsedWidth={sidebarCollapsedWidth || 64}\n />\n </div>\n\n {/* Content - 独立滚动 */}\n <main className=\"flex-1 overflow-y-auto\" style={{ height: '100%', paddingLeft: `${contentPaddingX}px`, paddingRight: `${contentPaddingX}px`, paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n\n {/* 侧边栏切换控件:桌面端 pill 已由 Sidebar 内部渲染,此处仅保留移动端按钮 */}\n <MobileSidebarToggleButton\n isCollapsed={effectiveSidebarCollapsed}\n onToggle={toggleSidebar}\n />\n </div>\n </div>\n );\n};\n\n// AppShell 属性类型\nexport type AppShellProps = {\n router?: Router<any, any>;\n children?: React.ReactNode;\n} & AppShellConfig;\n\n// AppShell 主组件\nexport const AppShell: React.FC<AppShellProps> = ({\n router,\n children,\n title = 'DiShUI',\n initialLang,\n initialTheme,\n menuItems,\n getMenuItems,\n themes,\n enableAuth = false,\n onAuthRequired,\n protectedRoutes = [],\n publicRoutes = ['/login', '/register'],\n loginUrl = '/login',\n authFailedCallback,\n layout = 'sidebar',\n sidebarWidth = 280,\n sidebarCollapsedWidth = 64,\n navbarHeight = 64,\n searchConfig,\n headbarLeft,\n headbarMiddle,\n headbarRight,\n}) => {\n // 保存全局配置\n globalConfig = {\n title,\n initialLang,\n initialTheme,\n menuItems,\n getMenuItems,\n themes,\n enableAuth,\n onAuthRequired,\n protectedRoutes,\n publicRoutes,\n loginUrl,\n authFailedCallback,\n layout,\n sidebarWidth,\n sidebarCollapsedWidth,\n navbarHeight,\n searchConfig,\n headbarLeft,\n headbarMiddle,\n headbarRight,\n };\n\n // 语言状态管理\n const [language, setLanguage] = useState<'zh' | 'en'>(() => {\n const savedLanguage = safeStorage.getItem('app-language');\n if (savedLanguage === 'zh' || savedLanguage === 'en') return savedLanguage;\n return initialLang || 'zh';\n });\n\n // 主题状态管理\n const [theme, setTheme] = useState<ThemeType>(() => {\n const savedTheme = safeStorage.getItem('app-theme');\n const validThemes: ThemeType[] = ['light', 'dark', 'cursor', 'retro', 'cyberpunk', 'matrix', 'oceanblue', 'eyecare', 'ghibli', 'pinkgirl', 'mintgirl', 'wuxing', 'jukebox'];\n if (savedTheme && validThemes.includes(savedTheme as ThemeType)) {\n return savedTheme as ThemeType;\n }\n return initialTheme || 'light';\n });\n\n // 用户认证状态管理\n const [user, setUser] = useState<User | null>(() => {\n if (!enableAuth) return null;\n return readStoredUser();\n });\n\n // Navigation can happen synchronously after login/logout, before React commits.\n const authUserRef = React.useRef(user);\n const lastAuthFailure = React.useRef<string | null>(null);\n\n // 保存语言设置\n useEffect(() => {\n safeStorage.setItem('app-language', language);\n }, [language]);\n\n // 保存主题设置\n useEffect(() => {\n safeStorage.setItem('app-theme', theme);\n if (typeof document !== 'undefined') document.documentElement.setAttribute('data-theme', theme);\n }, [theme]);\n\n // 保存用户信息\n useEffect(() => {\n if (enableAuth) {\n if (user) {\n writeStoredJSON('app-user', user);\n } else {\n safeStorage.removeItem('app-user');\n }\n }\n }, [user, enableAuth]);\n\n // 认证方法\n const login = (newUser: User) => {\n if (!isUser(newUser)) throw new TypeError('Invalid user: expected id, name, email and string roles');\n authUserRef.current = newUser;\n lastAuthFailure.current = null;\n setUser(newUser);\n if (enableAuth) writeStoredJSON('app-user', newUser);\n };\n\n const logout = () => {\n authUserRef.current = null;\n setUser(null);\n safeStorage.removeItem('app-user');\n };\n\n const hasRole = (role: string): boolean => {\n return user?.roles.includes(role) || false;\n };\n\n const hasAnyRole = (roles: string[]): boolean => {\n return roles.some(role => user?.roles.includes(role)) || false;\n };\n\n // UX notification only: router events cannot cancel/await loaders. Install native\n // beforeLoad guards when constructing protected routes, including preload paths.\n useEffect(() => {\n if (!enableAuth || !router) {\n lastAuthFailure.current = null;\n return;\n }\n\n const checkAuthentication = (pathname: string, currentUser = authUserRef.current) => {\n if (currentUser || !requiresAuthentication(pathname, protectedRoutes, publicRoutes)) {\n lastAuthFailure.current = null;\n return;\n }\n // Inline configuration arrays/callbacks and StrictMode must not repeat a notification.\n if (lastAuthFailure.current === pathname) return;\n lastAuthFailure.current = pathname;\n\n // Keep the existing contract: true means the caller handled the auth requirement.\n if (onAuthRequired?.()) return;\n if (authFailedCallback) {\n authFailedCallback(pathname, loginUrl);\n } else if (typeof window !== 'undefined') {\n window.location.href = loginUrl;\n }\n };\n\n const unsubscribe = router.subscribe('onBeforeLoad', ({ toLocation }) => {\n checkAuthentication(toLocation.pathname);\n });\n // Covers the initial visit and auth changes without a pathname change (logout).\n checkAuthentication(router.state.location.pathname, user);\n return unsubscribe;\n }, [enableAuth, user, router, protectedRoutes, publicRoutes, onAuthRequired, loginUrl, authFailedCallback]);\n\n const authContext: AuthContextType = {\n user,\n isAuthenticated: !!user,\n login,\n logout,\n hasRole,\n hasAnyRole,\n };\n\n const value: AppContextType = {\n title,\n language,\n theme,\n setLanguage,\n setTheme,\n auth: authContext,\n menuItems,\n getMenuItems,\n themes,\n sidebarWidth,\n sidebarCollapsedWidth,\n navbarHeight,\n };\n\n return (\n <AppContext.Provider value={value}>\n {router ? (\n children || <RouterProvider router={router} />\n ) : (\n children\n )}\n </AppContext.Provider>\n );\n};\n\n// 保持向后兼容的 AppProvider 别名\nexport const AppProvider = AppShell;\n\nexport default AppShell;\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { User } from './AppContext';
2
+ /** Shape validation only; browser storage is not proof of identity or permissions. */
3
+ export declare function isUser(value: unknown): value is User;
4
+ export declare function readStoredUser(): User | null;
5
+ /** A prefix matches complete path segments; '/' only matches the home page. */
6
+ export declare function matchesAuthPath(pathname: string, route: string): boolean;
7
+ export declare function requiresAuthentication(pathname: string, protectedRoutes: string[], publicRoutes: string[]): boolean;
@@ -0,0 +1,22 @@
1
+ import { readStoredJSON as e } from "../utils/safeStorage.js";
2
+ //#region src/contexts/authentication.ts
3
+ function t(e) {
4
+ if (!e || typeof e != "object" || Array.isArray(e)) return !1;
5
+ let t = e;
6
+ return typeof t.id == "string" && t.id.trim().length > 0 && typeof t.name == "string" && typeof t.email == "string" && Array.isArray(t.roles) && t.roles.every((e) => typeof e == "string");
7
+ }
8
+ function n() {
9
+ return e("app-user", t, null);
10
+ }
11
+ function r(e, t) {
12
+ if (!t.startsWith("/")) return !1;
13
+ let n = t.replace(/\/+$/, "") || "/";
14
+ return e === n || n !== "/" && e.startsWith(`${n}/`);
15
+ }
16
+ function i(e, t, n) {
17
+ return !n.some((t) => r(e, t)) && (t.length === 0 || t.some((t) => r(e, t)));
18
+ }
19
+ //#endregion
20
+ export { t as isUser, r as matchesAuthPath, n as readStoredUser, i as requiresAuthentication };
21
+
22
+ //# sourceMappingURL=authentication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authentication.js","names":[],"sources":["../../src/contexts/authentication.ts"],"sourcesContent":["import type { User } from './AppContext';\nimport { readStoredJSON } from '../utils/safeStorage';\n\n/** Shape validation only; browser storage is not proof of identity or permissions. */\nexport function isUser(value: unknown): value is User {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const user = value as Record<string, unknown>;\n return typeof user.id === 'string' && user.id.trim().length > 0\n && typeof user.name === 'string'\n && typeof user.email === 'string'\n && Array.isArray(user.roles) && user.roles.every(role => typeof role === 'string');\n}\n\nexport function readStoredUser(): User | null {\n return readStoredJSON<User | null>('app-user', isUser, null);\n}\n\n/** A prefix matches complete path segments; '/' only matches the home page. */\nexport function matchesAuthPath(pathname: string, route: string): boolean {\n if (!route.startsWith('/')) return false;\n const prefix = route.replace(/\\/+$/, '') || '/';\n return pathname === prefix || (prefix !== '/' && pathname.startsWith(`${prefix}/`));\n}\n\nexport function requiresAuthentication(pathname: string, protectedRoutes: string[], publicRoutes: string[]): boolean {\n return !publicRoutes.some(route => matchesAuthPath(pathname, route))\n && (protectedRoutes.length === 0 || protectedRoutes.some(route => matchesAuthPath(pathname, route)));\n}\n"],"mappings":";;AAIA,SAAgB,EAAO,GAA+B;CACpD,IAAI,CAAC,KAAS,OAAO,KAAU,YAAY,MAAM,QAAQ,CAAK,GAAG,OAAO;CACxE,IAAM,IAAO;CACb,OAAO,OAAO,EAAK,MAAO,YAAY,EAAK,GAAG,KAAK,CAAC,CAAC,SAAS,KACzD,OAAO,EAAK,QAAS,YACrB,OAAO,EAAK,SAAU,YACtB,MAAM,QAAQ,EAAK,KAAK,KAAK,EAAK,MAAM,OAAM,MAAQ,OAAO,KAAS,QAAQ;AACrF;AAEA,SAAgB,IAA8B;CAC5C,OAAO,EAA4B,YAAY,GAAQ,IAAI;AAC7D;AAGA,SAAgB,EAAgB,GAAkB,GAAwB;CACxE,IAAI,CAAC,EAAM,WAAW,GAAG,GAAG,OAAO;CACnC,IAAM,IAAS,EAAM,QAAQ,QAAQ,EAAE,KAAK;CAC5C,OAAO,MAAa,KAAW,MAAW,OAAO,EAAS,WAAW,GAAG,EAAO,EAAE;AACnF;AAEA,SAAgB,EAAuB,GAAkB,GAA2B,GAAiC;CACnH,OAAO,CAAC,EAAa,MAAK,MAAS,EAAgB,GAAU,CAAK,CAAC,MAC7D,EAAgB,WAAW,KAAK,EAAgB,MAAK,MAAS,EAAgB,GAAU,CAAK,CAAC;AACtG"}