dishui 0.0.47 → 0.0.49
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/components/pro/CodeEditor/editors/MarkdownEditor.js +1 -2
- package/dist/components/pro/CodeEditor/editors/MarkdownEditor.js.map +1 -1
- package/dist/components/pro/CodeEditor/themes.js +0 -25
- package/dist/components/pro/CodeEditor/themes.js.map +1 -1
- package/dist/components/pro/CodeEditor/types.d.ts +1 -1
- package/dist/components/pro/Flow/Flow.js +500 -483
- package/dist/components/pro/Flow/Flow.js.map +1 -1
- package/dist/components/pro/Flow/FlowCanvas.js +526 -524
- package/dist/components/pro/Flow/FlowCanvas.js.map +1 -1
- package/dist/components/pro/Flow/canvas/GridRenderer.d.ts +3 -0
- package/dist/components/pro/Flow/canvas/GridRenderer.js +28 -23
- package/dist/components/pro/Flow/canvas/GridRenderer.js.map +1 -1
- package/dist/components/pro/Flow/canvas/MiniMapRenderer.js +18 -17
- package/dist/components/pro/Flow/canvas/MiniMapRenderer.js.map +1 -1
- package/dist/components/pro/Flow/canvas/PixiApp.d.ts +2 -0
- package/dist/components/pro/Flow/canvas/PixiApp.js +14 -11
- package/dist/components/pro/Flow/canvas/PixiApp.js.map +1 -1
- package/dist/components/pro/Flow/constants.js +7 -7
- package/dist/components/pro/Flow/constants.js.map +1 -1
- package/dist/components/pro/Flow/context/FlowAIContext.d.ts +15 -0
- package/dist/components/pro/Flow/context/FlowAIContext.js +32 -0
- package/dist/components/pro/Flow/context/FlowAIContext.js.map +1 -0
- package/dist/components/pro/Flow/context/FlowUIContext.d.ts +4 -1
- package/dist/components/pro/Flow/context/FlowUIContext.js +58 -55
- package/dist/components/pro/Flow/context/FlowUIContext.js.map +1 -1
- package/dist/components/pro/Flow/edges/EdgeRenderer.js +22 -21
- package/dist/components/pro/Flow/edges/EdgeRenderer.js.map +1 -1
- package/dist/components/pro/Flow/hooks/useFlowImportExport.js +32 -32
- package/dist/components/pro/Flow/hooks/useFlowImportExport.js.map +1 -1
- package/dist/components/pro/Flow/hooks/usePixiApp.d.ts +6 -1
- package/dist/components/pro/Flow/hooks/usePixiApp.js +49 -30
- package/dist/components/pro/Flow/hooks/usePixiApp.js.map +1 -1
- package/dist/components/pro/Flow/index.d.ts +2 -0
- package/dist/components/pro/Flow/lines/FreehandTool.js +21 -20
- package/dist/components/pro/Flow/lines/FreehandTool.js.map +1 -1
- package/dist/components/pro/Flow/lines/GeometricLineTool.js +22 -18
- package/dist/components/pro/Flow/lines/GeometricLineTool.js.map +1 -1
- package/dist/components/pro/Flow/lines/LineRenderer.js +19 -18
- package/dist/components/pro/Flow/lines/LineRenderer.js.map +1 -1
- package/dist/components/pro/Flow/nodes/MediaContent.js +143 -124
- package/dist/components/pro/Flow/nodes/MediaContent.js.map +1 -1
- package/dist/components/pro/Flow/nodes/MediaOverlay.js +22 -17
- package/dist/components/pro/Flow/nodes/MediaOverlay.js.map +1 -1
- package/dist/components/pro/Flow/nodes/NodeRenderer.js +77 -76
- package/dist/components/pro/Flow/nodes/NodeRenderer.js.map +1 -1
- package/dist/components/pro/Flow/nodes/aiClient.d.ts +22 -0
- package/dist/components/pro/Flow/nodes/aiClient.js +107 -54
- package/dist/components/pro/Flow/nodes/aiClient.js.map +1 -1
- package/dist/components/pro/Flow/nodes/svgIcons.js +6 -6
- package/dist/components/pro/Flow/nodes/svgIcons.js.map +1 -1
- package/dist/components/pro/Flow/panels/AIPanel.d.ts +2 -0
- package/dist/components/pro/Flow/panels/AIPanel.js +498 -0
- package/dist/components/pro/Flow/panels/AIPanel.js.map +1 -0
- package/dist/components/pro/Flow/panels/FlowPanelGroup.js +51 -39
- package/dist/components/pro/Flow/panels/FlowPanelGroup.js.map +1 -1
- package/dist/components/pro/Flow/panels/FlowToolbar.js +91 -85
- package/dist/components/pro/Flow/panels/FlowToolbar.js.map +1 -1
- package/dist/components/pro/Flow/panels/InspectorPanel.js +372 -332
- package/dist/components/pro/Flow/panels/InspectorPanel.js.map +1 -1
- package/dist/components/pro/Flow/panels/OutlinePanel.js +28 -27
- package/dist/components/pro/Flow/panels/OutlinePanel.js.map +1 -1
- package/dist/components/pro/Flow/panels/ShapePalette.js +107 -104
- package/dist/components/pro/Flow/panels/ShapePalette.js.map +1 -1
- package/dist/components/pro/Flow/panels/toolbarIcons.d.ts +1 -0
- package/dist/components/pro/Flow/panels/toolbarIcons.js +25 -20
- package/dist/components/pro/Flow/panels/toolbarIcons.js.map +1 -1
- package/dist/components/pro/Flow/types.d.ts +13 -0
- package/dist/components/pro/Flow/types.js.map +1 -1
- package/dist/components/pro/Flow/utils/themeColor.d.ts +87 -0
- package/dist/components/pro/Flow/utils/themeColor.js +212 -0
- package/dist/components/pro/Flow/utils/themeColor.js.map +1 -0
- package/dist/contexts/AppContext.d.ts +1 -1
- package/dist/contexts/AppContext.js +28 -29
- package/dist/contexts/AppContext.js.map +1 -1
- package/dist/dishui.css +1 -1
- package/dist/favicon.svg +10 -0
- package/dist/flow.js +56 -51
- package/dist/styles/themeStyles.js +6 -11
- package/dist/styles/themeStyles.js.map +1 -1
- package/dist/utils/themes.js +3 -23
- package/dist/utils/themes.js.map +1 -1
- package/package.json +17 -18
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeColor.js","names":[],"sources":["../../../../../src/components/pro/Flow/utils/themeColor.ts"],"sourcesContent":["/**\n * Resolve a CSS color value (including theme custom properties like\n * `var(--color-base-100)` that may use `oklch(...)`) into a concrete sRGB color\n * for the WebGL canvas.\n *\n * PixiJS needs a numeric/hex color and cannot read CSS variables. Resolution:\n * 1. A hidden probe element inherits the themed scope. Its `color` is set to\n * `color-mix(in srgb, <value> 100%, transparent)`, which forces the\n * browser to convert the color into the sRGB space at computed-value\n * time. `getComputedStyle` then serializes it as a plain `rgb(...)` or\n * `color(srgb ...)` string.\n * 2. That string is parsed directly (no canvas). Canvas rasterization is\n * only a last resort: some engines silently reject CSS Color 4 functions\n * (`oklch()`, `lab()`, `color()`) in `fillStyle`, which previously turned\n * every probed theme color black.\n */\n\nlet _rasterCtx: CanvasRenderingContext2D | null = null;\nfunction getRasterCtx(): CanvasRenderingContext2D | null {\n if (_rasterCtx) return _rasterCtx;\n if (typeof document === 'undefined') return null;\n const canvas = document.createElement('canvas');\n canvas.width = 1;\n canvas.height = 1;\n _rasterCtx = canvas.getContext('2d', { willReadFrequently: true });\n return _rasterCtx;\n}\n\n/** Rasterize any CSS color string to concrete sRGB bytes. */\nfunction rasterizeColor(cssColor: string): [number, number, number] | null {\n const ctx = getRasterCtx();\n if (!ctx) return null;\n try {\n ctx.clearRect(0, 0, 1, 1);\n ctx.fillStyle = '#000000';\n ctx.fillStyle = cssColor; // ignored if the browser can't parse it\n ctx.fillRect(0, 0, 1, 1);\n const { data } = ctx.getImageData(0, 0, 1, 1);\n return [data[0], data[1], data[2]];\n } catch {\n return null;\n }\n}\n\nfunction byte(v: number): number {\n return Math.max(0, Math.min(255, Math.round(v)));\n}\n\n/**\n * Parse the sRGB strings a browser serializes a computed color to when the\n * color was forced into the sRGB space: `rgb(...)` / `rgba(...)` (0-255\n * channels) or `color(srgb r g b / a)` (0-1 float channels).\n */\nfunction parseSrgbString(value: string): [number, number, number] | null {\n const rgb = /^rgba?\\(\\s*([\\d.eE+-]+)[\\s,]+([\\d.eE+-]+)[\\s,]+([\\d.eE+-]+)/.exec(value);\n if (rgb) return [byte(+rgb[1]), byte(+rgb[2]), byte(+rgb[3])];\n const fn = /^color\\(\\s*srgb\\s+([\\d.eE+-]+)\\s+([\\d.eE+-]+)\\s+([\\d.eE+-]+)/i.exec(value);\n if (fn) return [byte(+fn[1] * 255), byte(+fn[2] * 255), byte(+fn[3] * 255)];\n return null;\n}\n\n/** Whether the browser computes color-mix() (used to force sRGB output). */\nconst SUPPORTS_COLOR_MIX =\n typeof CSS !== 'undefined' &&\n typeof CSS.supports === 'function' &&\n CSS.supports('color', 'color-mix(in srgb, red, blue)');\n\nfunction parseRgb(host: HTMLElement | null, cssValue: string): [number, number, number] | null {\n if (typeof document === 'undefined') return null;\n try {\n const probe = document.createElement('span');\n // Keep out of layout without `display:none` so `color` still resolves.\n probe.style.position = 'absolute';\n probe.style.width = '0';\n probe.style.height = '0';\n probe.style.overflow = 'hidden';\n probe.style.pointerEvents = 'none';\n (host ?? document.body).appendChild(probe);\n // Bail out when the requested custom property is undefined in this scope:\n // var() would invalidate the declaration and the probe would report its\n // inherited text color instead of the theme color.\n const varMatch = /^var\\(\\s*(--[\\w-]+)\\s*\\)$/.exec(cssValue);\n if (varMatch && !getComputedStyle(probe).getPropertyValue(varMatch[1]).trim()) {\n probe.remove();\n return null;\n }\n // Force the resolved color into the sRGB space at computed-value time, so\n // the serialized result is a plain rgb()/color(srgb) string we can parse\n // ourselves. Do NOT rasterize oklch()/lab() via canvas fillStyle here:\n // some engines (e.g. WebKit) silently reject CSS Color 4 functions in\n // canvas, which used to turn every probed theme color black.\n probe.style.color = SUPPORTS_COLOR_MIX\n ? `color-mix(in srgb, ${cssValue} 100%, transparent)`\n : cssValue;\n const resolved = getComputedStyle(probe).color;\n probe.remove();\n return parseSrgbString(resolved) ?? rasterizeColor(resolved);\n } catch {\n return null;\n }\n}\n\n/** Resolve a CSS color to a 0xRRGGBB number (for PixiJS). */\nexport function cssColorToNumber(host: HTMLElement | null, cssValue: string, fallback: number): number {\n const rgb = parseRgb(host, cssValue);\n if (!rgb) return fallback;\n return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];\n}\n\n/** Resolve a CSS color to a `#rrggbb` string (for persisted node data). */\nexport function cssColorToHexString(host: HTMLElement | null, cssValue: string, fallback: string): string {\n const rgb = parseRgb(host, cssValue);\n if (!rgb) return fallback;\n return '#' + rgb.map((c) => c.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * The colors the Flow canvas derives from the active theme:\n * - `background`: the canvas backdrop, follows the theme's muted background.\n * - `gridDot`: grid dots, derived from the theme foreground color.\n * - `nodeFill`: default fill for new shapes, follows the theme base background.\n */\nexport interface FlowThemeColors {\n background: number;\n gridDot: number;\n nodeFill: string;\n nodeText: string;\n}\n\nexport function resolveFlowThemeColors(host: HTMLElement | null): FlowThemeColors {\n return {\n background: cssColorToNumber(host, 'var(--color-base-200)', 0xf8f9fa),\n gridDot: cssColorToNumber(host, 'var(--color-base-content)', 0xd1d5db),\n nodeFill: cssColorToHexString(host, 'var(--color-base-100)', '#ffffff'),\n nodeText: cssColorToHexString(host, 'var(--color-base-content)', '#111827'),\n };\n}\n\n/* ============================================================================\n * Theme color tokens\n *\n * Node/edge colors may be stored either as a literal color (`#rrggbb`,\n * `transparent`, …) or as a THEME TOKEN of the form `theme:<name>` (e.g.\n * `theme:base-100`). Theme tokens are resolved to the live theme color at\n * render time, so shapes that use them re-color automatically whenever the\n * DishUI theme changes. These tokens are the default colors for new shapes.\n * ========================================================================== */\n\nexport const THEME_COLOR_PREFIX = 'theme:';\n\n/** A selectable theme color, shown as a swatch in the inspector. */\nexport interface ThemeColorToken {\n /** Stored value, e.g. `theme:base-100`. */\n token: string;\n /** CSS custom property backing the token. */\n cssVar: string;\n /** Short label for the inspector tooltip. */\n label: string;\n}\n\n/** The theme colors offered as presets/defaults for Flow color fields. */\nexport const THEME_COLOR_TOKENS: ThemeColorToken[] = [\n { token: 'theme:base-100', cssVar: 'var(--color-base-100)', label: 'Base 100' },\n { token: 'theme:base-200', cssVar: 'var(--color-base-200)', label: 'Base 200' },\n { token: 'theme:base-300', cssVar: 'var(--color-base-300)', label: 'Base 300' },\n { token: 'theme:base-content', cssVar: 'var(--color-base-content)', label: 'Base Content' },\n { token: 'theme:primary', cssVar: 'var(--color-primary)', label: 'Primary' },\n { token: 'theme:secondary', cssVar: 'var(--color-secondary)', label: 'Secondary' },\n { token: 'theme:accent', cssVar: 'var(--color-accent)', label: 'Accent' },\n { token: 'theme:neutral', cssVar: 'var(--color-neutral)', label: 'Neutral' },\n { token: 'theme:info', cssVar: 'var(--color-info)', label: 'Info' },\n { token: 'theme:success', cssVar: 'var(--color-success)', label: 'Success' },\n { token: 'theme:warning', cssVar: 'var(--color-warning)', label: 'Warning' },\n { token: 'theme:error', cssVar: 'var(--color-error)', label: 'Error' },\n];\n\nconst TOKEN_TO_CSSVAR = new Map(THEME_COLOR_TOKENS.map((t) => [t.token, t.cssVar]));\nconst TOKEN_TO_LABEL = new Map(THEME_COLOR_TOKENS.map((t) => [t.token, t.label]));\n\n/**\n * The \"follow theme\" default token for each color field type. Each field syncs\n * to a theme role so shapes stay legible against the canvas:\n * - `fill` -> theme background (base-100)\n * - `stroke` -> theme font color (base-content) — a visible outline\n * - `text` -> theme font color (base-content)\n * - `line` -> theme font color (base-content) — lines/edges\n */\nexport type ThemeColorFieldKind = 'fill' | 'stroke' | 'text' | 'line';\n\nexport const THEME_SYNC_TOKEN: Record<ThemeColorFieldKind, string> = {\n fill: 'theme:base-100',\n stroke: 'theme:base-content',\n text: 'theme:base-content',\n line: 'theme:base-content',\n};\n\n/** The CSS var backing a given theme token (for live-updating DOM swatches). */\nexport function themeTokenCssVar(token: string): string | undefined {\n return TOKEN_TO_CSSVAR.get(token);\n}\n\n/** Human label for a theme token (for tooltips). */\nexport function themeTokenLabel(token: string): string | undefined {\n return TOKEN_TO_LABEL.get(token);\n}\n\n/**\n * Common named colors offered after the theme-sync swatch: a compact rainbow\n * plus black/white. Order follows 黑白赤橙黄绿青蓝紫粉 (black, white, red,\n * orange, yellow, green, cyan, blue, purple, pink).\n */\nexport interface NamedColor {\n value: string;\n /** Chinese label shown on hover. */\n label: string;\n}\n\nexport const COMMON_COLORS: NamedColor[] = [\n { value: '#000000', label: '黑' },\n { value: '#ffffff', label: '白' },\n { value: '#ef4444', label: '赤' },\n { value: '#f97316', label: '橙' },\n { value: '#eab308', label: '黄' },\n { value: '#22c55e', label: '绿' },\n { value: '#06b6d4', label: '青' },\n { value: '#3b82f6', label: '蓝' },\n { value: '#8b5cf6', label: '紫' },\n { value: '#ec4899', label: '粉' },\n];\n\n/** True when `value` is a `theme:*` token. */\nexport function isThemeToken(value: string | undefined | null): value is string {\n return typeof value === 'string' && value.startsWith(THEME_COLOR_PREFIX);\n}\n\n// Module-level cache of the currently resolved token -> `#rrggbb` mapping. The\n// render loop resolves tokens against this cache (cheap, synchronous) instead\n// of probing the DOM every frame. `refreshThemePalette` rebuilds it whenever\n// the theme changes.\nconst _palette = new Map<string, string>();\n\n/**\n * Re-resolve every theme token against the given host's themed scope and cache\n * the results. Call on init and whenever the active theme changes.\n */\nexport function refreshThemePalette(host: HTMLElement | null): void {\n for (const { token, cssVar } of THEME_COLOR_TOKENS) {\n _palette.set(token, cssColorToHexString(host, cssVar, '#000000'));\n }\n}\n\n/**\n * Resolve a stored color value to a concrete `#rrggbb`/literal color. Theme\n * tokens are looked up in the cached palette; anything else is returned as-is.\n */\nexport function resolveThemeColor(value: string | undefined | null, fallback = '#000000'): string {\n if (!value) return fallback;\n if (isThemeToken(value)) return _palette.get(value) ?? fallback;\n return value;\n}\n\n/**\n * Resolve a stored color to a value usable directly in the DOM. Theme tokens\n * map to their CSS variable so the browser tracks theme changes natively.\n */\nexport function resolveThemeColorCss(value: string | undefined | null, fallback = '#000000'): string {\n if (!value) return fallback;\n if (isThemeToken(value)) return TOKEN_TO_CSSVAR.get(value) ?? fallback;\n return value;\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { Router } from '@tanstack/react-router';
|
|
|
3
3
|
import type { SearchConfig } from '../layouts/headbar';
|
|
4
4
|
import { MenuItem } from '../types/menu';
|
|
5
5
|
import { ThemeConfig } from '../utils/themes';
|
|
6
|
-
export type ThemeType = 'light' | 'dark' | 'retro' | 'cyberpunk' | 'matrix' | 'oceanblue' | 'eyecare' | 'ghibli' | 'pinkgirl' | 'mintgirl' | '
|
|
6
|
+
export type ThemeType = 'light' | 'dark' | 'retro' | 'cyberpunk' | 'matrix' | 'oceanblue' | 'eyecare' | 'ghibli' | 'pinkgirl' | 'mintgirl' | 'wuxing' | 'jukebox';
|
|
7
7
|
export type User = {
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
@@ -8,34 +8,34 @@ var H = Z(void 0), R = () => {
|
|
|
8
8
|
const r = _(H);
|
|
9
9
|
if (!r) throw new Error("useAppContext must be used within an AppShell");
|
|
10
10
|
return r;
|
|
11
|
-
}, oe = () => R().auth, d = {},
|
|
12
|
-
const { title: r, language: v, theme:
|
|
13
|
-
const
|
|
14
|
-
return
|
|
15
|
-
}), [m,
|
|
11
|
+
}, oe = () => R().auth, d = {}, le = () => {
|
|
12
|
+
const { title: r, language: v, theme: c, menuItems: b, getMenuItems: S, themes: L, sidebarWidth: i, sidebarCollapsedWidth: f, navbarHeight: o } = R(), [a, h] = x(() => {
|
|
13
|
+
const p = localStorage.getItem("sidebar-collapsed");
|
|
14
|
+
return p ? JSON.parse(p) : !1;
|
|
15
|
+
}), [m, n] = x(i || 280), g = B.useRef(null);
|
|
16
16
|
B.useEffect(() => {
|
|
17
|
-
i &&
|
|
17
|
+
i && n(i);
|
|
18
18
|
}, [i]), B.useEffect(() => {
|
|
19
19
|
localStorage.setItem("sidebar-collapsed", JSON.stringify(a));
|
|
20
20
|
}, [a]);
|
|
21
21
|
const y = () => {
|
|
22
22
|
h(!a);
|
|
23
|
-
}, I = (
|
|
24
|
-
|
|
25
|
-
},
|
|
23
|
+
}, I = (p) => {
|
|
24
|
+
n(p);
|
|
25
|
+
}, l = o || 64, N = d.layout || "sidebar", u = d.headbarLeft, W = typeof u == "function" ? u(y) : u;
|
|
26
26
|
return N === "topbar" ? /* @__PURE__ */ J("div", {
|
|
27
27
|
className: "h-screen flex flex-col bg-base-100",
|
|
28
|
-
"data-theme":
|
|
28
|
+
"data-theme": c,
|
|
29
29
|
children: [/* @__PURE__ */ t("div", {
|
|
30
30
|
style: {
|
|
31
|
-
height: `${
|
|
31
|
+
height: `${l}px`,
|
|
32
32
|
flexShrink: 0
|
|
33
33
|
},
|
|
34
34
|
children: /* @__PURE__ */ t(j, {
|
|
35
35
|
left: W,
|
|
36
36
|
middle: d.headbarMiddle,
|
|
37
37
|
right: d.headbarRight,
|
|
38
|
-
height:
|
|
38
|
+
height: l
|
|
39
39
|
})
|
|
40
40
|
}), /* @__PURE__ */ t("main", {
|
|
41
41
|
className: "flex-1 overflow-y-auto",
|
|
@@ -49,21 +49,21 @@ var H = Z(void 0), R = () => {
|
|
|
49
49
|
})]
|
|
50
50
|
}) : /* @__PURE__ */ J("div", {
|
|
51
51
|
className: "h-screen flex flex-col bg-base-100",
|
|
52
|
-
"data-theme":
|
|
52
|
+
"data-theme": c,
|
|
53
53
|
children: [/* @__PURE__ */ t("div", {
|
|
54
54
|
style: {
|
|
55
|
-
height: `${
|
|
55
|
+
height: `${l}px`,
|
|
56
56
|
flexShrink: 0
|
|
57
57
|
},
|
|
58
58
|
children: /* @__PURE__ */ t(j, {
|
|
59
59
|
left: W,
|
|
60
60
|
middle: d.headbarMiddle,
|
|
61
61
|
right: d.headbarRight,
|
|
62
|
-
height:
|
|
62
|
+
height: l
|
|
63
63
|
})
|
|
64
64
|
}), /* @__PURE__ */ J("div", {
|
|
65
65
|
className: "flex flex-1 overflow-hidden",
|
|
66
|
-
style: { height: `calc(100vh - ${
|
|
66
|
+
style: { height: `calc(100vh - ${l}px)` },
|
|
67
67
|
children: [
|
|
68
68
|
/* @__PURE__ */ t("div", {
|
|
69
69
|
className: "flex-shrink-0",
|
|
@@ -102,9 +102,9 @@ var H = Z(void 0), R = () => {
|
|
|
102
102
|
]
|
|
103
103
|
})]
|
|
104
104
|
});
|
|
105
|
-
}, F = ({ router: r, children: v, title:
|
|
105
|
+
}, F = ({ router: r, children: v, title: c = "DiShUI", initialLang: b, initialTheme: S, menuItems: L, getMenuItems: i, themes: f, enableAuth: o = !1, onAuthRequired: a, protectedRoutes: h = [], publicRoutes: m = ["/login", "/register"], loginUrl: n = "/login", authFailedCallback: g, layout: y = "sidebar", sidebarWidth: I = 280, sidebarCollapsedWidth: l = 64, navbarHeight: N = 64, searchConfig: u, headbarLeft: W, headbarMiddle: p, headbarRight: $ }) => {
|
|
106
106
|
d = {
|
|
107
|
-
title:
|
|
107
|
+
title: c,
|
|
108
108
|
initialLang: b,
|
|
109
109
|
initialTheme: S,
|
|
110
110
|
menuItems: L,
|
|
@@ -114,15 +114,15 @@ var H = Z(void 0), R = () => {
|
|
|
114
114
|
onAuthRequired: a,
|
|
115
115
|
protectedRoutes: h,
|
|
116
116
|
publicRoutes: m,
|
|
117
|
-
loginUrl:
|
|
117
|
+
loginUrl: n,
|
|
118
118
|
authFailedCallback: g,
|
|
119
119
|
layout: y,
|
|
120
120
|
sidebarWidth: I,
|
|
121
|
-
sidebarCollapsedWidth:
|
|
121
|
+
sidebarCollapsedWidth: l,
|
|
122
122
|
navbarHeight: N,
|
|
123
123
|
searchConfig: u,
|
|
124
124
|
headbarLeft: W,
|
|
125
|
-
headbarMiddle:
|
|
125
|
+
headbarMiddle: p,
|
|
126
126
|
headbarRight: $
|
|
127
127
|
};
|
|
128
128
|
const [E, z] = x(() => {
|
|
@@ -141,7 +141,6 @@ var H = Z(void 0), R = () => {
|
|
|
141
141
|
"ghibli",
|
|
142
142
|
"pinkgirl",
|
|
143
143
|
"mintgirl",
|
|
144
|
-
"shadcn",
|
|
145
144
|
"wuxing",
|
|
146
145
|
"jukebox"
|
|
147
146
|
].includes(e) ? e : S || "light";
|
|
@@ -170,7 +169,7 @@ var H = Z(void 0), R = () => {
|
|
|
170
169
|
if ((h.length === 0 || h.some((k) => T.pathname.startsWith(k))) && !V && !s) {
|
|
171
170
|
if (a && a())
|
|
172
171
|
return;
|
|
173
|
-
g ? g(T.pathname,
|
|
172
|
+
g ? g(T.pathname, n) : window.location.href = n;
|
|
174
173
|
}
|
|
175
174
|
});
|
|
176
175
|
return () => {
|
|
@@ -183,11 +182,11 @@ var H = Z(void 0), R = () => {
|
|
|
183
182
|
h,
|
|
184
183
|
m,
|
|
185
184
|
a,
|
|
186
|
-
|
|
185
|
+
n,
|
|
187
186
|
g
|
|
188
187
|
]);
|
|
189
188
|
const U = {
|
|
190
|
-
title:
|
|
189
|
+
title: c,
|
|
191
190
|
language: E,
|
|
192
191
|
theme: w,
|
|
193
192
|
setLanguage: z,
|
|
@@ -204,17 +203,17 @@ var H = Z(void 0), R = () => {
|
|
|
204
203
|
getMenuItems: i,
|
|
205
204
|
themes: f,
|
|
206
205
|
sidebarWidth: I,
|
|
207
|
-
sidebarCollapsedWidth:
|
|
206
|
+
sidebarCollapsedWidth: l,
|
|
208
207
|
navbarHeight: N
|
|
209
208
|
};
|
|
210
209
|
return /* @__PURE__ */ t(H.Provider, {
|
|
211
210
|
value: U,
|
|
212
211
|
children: r ? v || /* @__PURE__ */ t(q, { router: r }) : v
|
|
213
212
|
});
|
|
214
|
-
},
|
|
213
|
+
}, ne = F;
|
|
215
214
|
export {
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
le as AppLayoutWrapper,
|
|
216
|
+
ne as AppProvider,
|
|
218
217
|
F as AppShell,
|
|
219
218
|
F as default,
|
|
220
219
|
R as useAppContext,
|
|
@@ -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 { SidebarToggleButtons } from '../components/SidebarToggleButton';\n\n// 主题类型\nexport type ThemeType = 'light' | 'dark' | 'retro' | 'cyberpunk' | 'matrix' | 'oceanblue' | 'eyecare' | 'ghibli' | 'pinkgirl' | 'mintgirl' | 'shadcn' | '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 // 当配置的 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 toggleSidebar = () => {\n setSidebarCollapsed(!sidebarCollapsed);\n };\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: '128px', paddingRight: '128px', 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 - 独立滚动 */}\n <div\n className=\"flex-shrink-0\"\n ref={sidebarRef}\n style={{ height: '100%', overflow: 'hidden' }}\n >\n <Sidebar\n isCollapsed={sidebarCollapsed}\n language={language}\n items={menuItems}\n getMenuItems={getMenuItems}\n onWidthChange={handleSidebarWidthChange}\n initialWidth={configSidebarWidth || 280}\n collapsedWidth={sidebarCollapsedWidth || 64}\n />\n </div>\n\n {/* Content - 独立滚动 */}\n <main className=\"flex-1 overflow-y-auto\" style={{ height: '100%', paddingLeft: '128px', paddingRight: '128px', paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n\n {/* 悬浮侧边栏切换按钮 */}\n <SidebarToggleButtons\n isCollapsed={sidebarCollapsed}\n onToggle={toggleSidebar}\n sidebarWidth={sidebarWidth}\n collapsedWidth={sidebarCollapsedWidth || 64}\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', 'retro', 'cyberpunk', 'matrix', 'oceanblue', 'eyecare', 'ghibli', 'pinkgirl', 'mintgirl', 'shadcn', '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 { SidebarToggleButtons } from '../components/SidebarToggleButton';\n\n// 主题类型\nexport type ThemeType = 'light' | 'dark' | '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 // 当配置的 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 toggleSidebar = () => {\n setSidebarCollapsed(!sidebarCollapsed);\n };\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: '128px', paddingRight: '128px', 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 - 独立滚动 */}\n <div\n className=\"flex-shrink-0\"\n ref={sidebarRef}\n style={{ height: '100%', overflow: 'hidden' }}\n >\n <Sidebar\n isCollapsed={sidebarCollapsed}\n language={language}\n items={menuItems}\n getMenuItems={getMenuItems}\n onWidthChange={handleSidebarWidthChange}\n initialWidth={configSidebarWidth || 280}\n collapsedWidth={sidebarCollapsedWidth || 64}\n />\n </div>\n\n {/* Content - 独立滚动 */}\n <main className=\"flex-1 overflow-y-auto\" style={{ height: '100%', paddingLeft: '128px', paddingRight: '128px', paddingTop: '16px', paddingBottom: '16px' }}>\n <Outlet />\n </main>\n\n {/* 悬浮侧边栏切换按钮 */}\n <SidebarToggleButtons\n isCollapsed={sidebarCollapsed}\n onToggle={toggleSidebar}\n sidebarWidth={sidebarWidth}\n collapsedWidth={sidebarCollapsedWidth || 64}\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', '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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"}
|