@zhin.js/console 1.0.51 → 1.0.52
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/CHANGELOG.md +13 -0
- package/README.md +22 -0
- package/browser.tsconfig.json +19 -0
- package/client/src/components/PageHeader.tsx +26 -0
- package/client/src/components/ui/accordion.tsx +2 -1
- package/client/src/components/ui/badge.tsx +1 -3
- package/client/src/components/ui/scroll-area.tsx +5 -2
- package/client/src/components/ui/select.tsx +7 -3
- package/client/src/components/ui/separator.tsx +5 -2
- package/client/src/components/ui/tabs.tsx +4 -2
- package/client/src/layouts/dashboard.tsx +223 -121
- package/client/src/main.tsx +34 -34
- package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
- package/client/src/pages/bot-detail/date-utils.ts +8 -0
- package/client/src/pages/bot-detail/index.tsx +798 -0
- package/client/src/pages/bot-detail/types.ts +92 -0
- package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
- package/client/src/pages/bots.tsx +111 -73
- package/client/src/pages/database/constants.ts +16 -0
- package/client/src/pages/database/database-page.tsx +170 -0
- package/client/src/pages/database/document-collection-view.tsx +155 -0
- package/client/src/pages/database/index.tsx +1 -0
- package/client/src/pages/database/json-field.tsx +11 -0
- package/client/src/pages/database/kv-bucket-view.tsx +169 -0
- package/client/src/pages/database/related-table-view.tsx +221 -0
- package/client/src/pages/env.tsx +38 -28
- package/client/src/pages/files/code-editor.tsx +85 -0
- package/client/src/pages/files/editor-constants.ts +9 -0
- package/client/src/pages/files/file-editor.tsx +133 -0
- package/client/src/pages/files/file-icons.tsx +25 -0
- package/client/src/pages/files/files-page.tsx +92 -0
- package/client/src/pages/files/hljs-global.d.ts +10 -0
- package/client/src/pages/files/index.tsx +1 -0
- package/client/src/pages/files/language.ts +18 -0
- package/client/src/pages/files/tree-node.tsx +69 -0
- package/client/src/pages/files/use-hljs-theme.ts +23 -0
- package/client/src/pages/logs.tsx +77 -22
- package/client/src/style.css +144 -0
- package/client/src/utils/parseComposerContent.ts +57 -0
- package/client/tailwind.config.js +1 -0
- package/client/tsconfig.json +3 -1
- package/dist/assets/index-COKXlFo2.js +124 -0
- package/dist/assets/style-kkLO-vsa.css +3 -0
- package/dist/client.js +482 -464
- package/dist/index.html +2 -2
- package/dist/style.css +1 -1
- package/lib/index.js +1010 -81
- package/lib/transform.js +16 -2
- package/lib/websocket.js +845 -28
- package/node.tsconfig.json +18 -0
- package/package.json +13 -15
- package/src/bin.ts +24 -0
- package/src/bot-db-models.ts +74 -0
- package/src/bot-hub.ts +240 -0
- package/src/bot-persistence.ts +270 -0
- package/src/build.ts +90 -0
- package/src/dev.ts +107 -0
- package/src/index.ts +337 -0
- package/src/transform.ts +199 -0
- package/src/websocket.ts +1369 -0
- package/client/src/pages/database.tsx +0 -708
- package/client/src/pages/files.tsx +0 -470
- package/client/src/pages/login-assist.tsx +0 -225
- package/dist/assets/index-DS4RbHWX.js +0 -124
- package/dist/assets/style-DS-m6WEr.css +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @zhin.js/console
|
|
2
2
|
|
|
3
|
+
## 1.0.52
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bb6bfa8: chore: 控制台路由 key、client tsc、页面模块化拆分;client/satori 的 clean 与构建产物约定对齐
|
|
8
|
+
- Updated dependencies [bb6bfa8]
|
|
9
|
+
- Updated dependencies [bb6bfa8]
|
|
10
|
+
- Updated dependencies [bb6bfa8]
|
|
11
|
+
- @zhin.js/core@1.0.52
|
|
12
|
+
- zhin.js@1.0.52
|
|
13
|
+
- @zhin.js/client@1.0.13
|
|
14
|
+
- @zhin.js/http@1.0.46
|
|
15
|
+
|
|
3
16
|
## 1.0.51
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -12,6 +12,28 @@ Zhin 机器人框架的 Web 控制台插件,提供开发环境下的可视化
|
|
|
12
12
|
- 🛠️ 开发工具集成
|
|
13
13
|
- 📱 WebSocket 实时通信
|
|
14
14
|
|
|
15
|
+
### ICQQ 登录辅助(不在 Console 提供)
|
|
16
|
+
|
|
17
|
+
- **登录辅助的 HTTP API 与 Web UI 由 `@zhin.js/adapter-icqq` 注册**,Console 插件不再挂载 `/api/login-assist/*`。
|
|
18
|
+
- 启用 **console + icqq** 后,在控制台侧栏进入 **ICQQ 管理**(扩展入口 `/icqq`),在 **「登录辅助」** Tab 中完成扫码 / 验证码等流程。
|
|
19
|
+
- 未启用 icqq 时,不应存在 `/api/login-assist` 路由(为预期行为,不保留旧路径兼容)。
|
|
20
|
+
|
|
21
|
+
## 控制台扩展的 TypeScript 基线
|
|
22
|
+
|
|
23
|
+
适配器或插件若带有 **`client/`**(浏览器端),可在 `client/tsconfig.json` 中:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"extends": "@zhin.js/console/browser.tsconfig.json",
|
|
28
|
+
"compilerOptions": { "outDir": "../dist" },
|
|
29
|
+
"include": ["./**/*"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Node 侧 **`src/`** 可参考 **`@zhin.js/console/node.tsconfig.json`**,并在本包 `tsconfig` 中补全 `rootDir`、`outDir`、`include`。
|
|
34
|
+
|
|
35
|
+
上述文件由本包 `exports` 随包发布。
|
|
36
|
+
|
|
15
37
|
## 技术架构
|
|
16
38
|
|
|
17
39
|
- **构建工具**: Vite 7.x
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "Zhin Console — 适配器 / 插件「浏览器端 client/」推荐基线",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"target": "ES2022",
|
|
10
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"jsxImportSource": "react",
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"isolatedModules": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '@zhin.js/client'
|
|
3
|
+
|
|
4
|
+
export interface PageHeaderProps {
|
|
5
|
+
title: string
|
|
6
|
+
description?: string
|
|
7
|
+
actions?: ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 统一页面标题区(与各业务页配合使用)
|
|
13
|
+
*/
|
|
14
|
+
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-4', className)}>
|
|
17
|
+
<div className="min-w-0 space-y-1">
|
|
18
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
19
|
+
{description ? (
|
|
20
|
+
<p className="text-sm text-muted-foreground max-w-2xl">{description}</p>
|
|
21
|
+
) : null}
|
|
22
|
+
</div>
|
|
23
|
+
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -8,9 +8,10 @@ const Accordion = AccordionPrimitive.Root
|
|
|
8
8
|
const AccordionItem = React.forwardRef<
|
|
9
9
|
React.ComponentRef<typeof AccordionPrimitive.Item>,
|
|
10
10
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
11
|
-
>(({ className, ...props }, ref) => (
|
|
11
|
+
>(({ className, value, ...props }, ref) => (
|
|
12
12
|
<AccordionPrimitive.Item
|
|
13
13
|
ref={ref}
|
|
14
|
+
value={value}
|
|
14
15
|
className={cn("border-b", className)}
|
|
15
16
|
{...props}
|
|
16
17
|
/>
|
|
@@ -21,9 +21,7 @@ const badgeVariants = cva(
|
|
|
21
21
|
}
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
export
|
|
25
|
-
extends React.HTMLAttributes<HTMLDivElement>,
|
|
26
|
-
VariantProps<typeof badgeVariants> {}
|
|
24
|
+
export type BadgeProps = React.ComponentPropsWithoutRef<'div'> & VariantProps<typeof badgeVariants>
|
|
27
25
|
|
|
28
26
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
29
27
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
@@ -23,7 +23,9 @@ ScrollArea.displayName = "ScrollArea"
|
|
|
23
23
|
const ScrollBar = React.forwardRef<
|
|
24
24
|
React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,
|
|
25
25
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>
|
|
26
|
-
>(({ className, orientation
|
|
26
|
+
>(({ className, orientation: orientationProp, ...props }, ref) => {
|
|
27
|
+
const orientation = (orientationProp ?? "vertical") as "vertical" | "horizontal"
|
|
28
|
+
return (
|
|
27
29
|
<ScrollAreaPrimitive.Scrollbar
|
|
28
30
|
ref={ref}
|
|
29
31
|
orientation={orientation}
|
|
@@ -37,7 +39,8 @@ const ScrollBar = React.forwardRef<
|
|
|
37
39
|
>
|
|
38
40
|
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-border" />
|
|
39
41
|
</ScrollAreaPrimitive.Scrollbar>
|
|
40
|
-
)
|
|
42
|
+
)
|
|
43
|
+
})
|
|
41
44
|
ScrollBar.displayName = "ScrollBar"
|
|
42
45
|
|
|
43
46
|
export { ScrollArea, ScrollBar }
|
|
@@ -58,7 +58,9 @@ SelectScrollDownButton.displayName = "SelectScrollDownButton"
|
|
|
58
58
|
const SelectContent = React.forwardRef<
|
|
59
59
|
React.ComponentRef<typeof SelectPrimitive.Content>,
|
|
60
60
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
61
|
-
>(({ className, children, position
|
|
61
|
+
>(({ className, children, position: positionProp, ...props }, ref) => {
|
|
62
|
+
const position = (positionProp ?? "popper") as "popper" | "item-aligned"
|
|
63
|
+
return (
|
|
62
64
|
<SelectPrimitive.Portal>
|
|
63
65
|
<SelectPrimitive.Content
|
|
64
66
|
ref={ref}
|
|
@@ -84,15 +86,17 @@ const SelectContent = React.forwardRef<
|
|
|
84
86
|
<SelectScrollDownButton />
|
|
85
87
|
</SelectPrimitive.Content>
|
|
86
88
|
</SelectPrimitive.Portal>
|
|
87
|
-
)
|
|
89
|
+
)
|
|
90
|
+
})
|
|
88
91
|
SelectContent.displayName = "SelectContent"
|
|
89
92
|
|
|
90
93
|
const SelectItem = React.forwardRef<
|
|
91
94
|
React.ComponentRef<typeof SelectPrimitive.Item>,
|
|
92
95
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
93
|
-
>(({ className, children, ...props }, ref) => (
|
|
96
|
+
>(({ className, children, value, ...props }, ref) => (
|
|
94
97
|
<SelectPrimitive.Item
|
|
95
98
|
ref={ref}
|
|
99
|
+
value={value}
|
|
96
100
|
className={cn(
|
|
97
101
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
98
102
|
className
|
|
@@ -5,7 +5,9 @@ import { cn } from "@zhin.js/client"
|
|
|
5
5
|
const Separator = React.forwardRef<
|
|
6
6
|
React.ComponentRef<typeof SeparatorPrimitive.Root>,
|
|
7
7
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
8
|
-
>(({ className, orientation
|
|
8
|
+
>(({ className, orientation: orientationProp, decorative = true, ...props }, ref) => {
|
|
9
|
+
const orientation = (orientationProp ?? "horizontal") as "vertical" | "horizontal"
|
|
10
|
+
return (
|
|
9
11
|
<SeparatorPrimitive.Root
|
|
10
12
|
ref={ref}
|
|
11
13
|
decorative={decorative}
|
|
@@ -17,7 +19,8 @@ const Separator = React.forwardRef<
|
|
|
17
19
|
)}
|
|
18
20
|
{...props}
|
|
19
21
|
/>
|
|
20
|
-
)
|
|
22
|
+
)
|
|
23
|
+
})
|
|
21
24
|
Separator.displayName = "Separator"
|
|
22
25
|
|
|
23
26
|
export { Separator }
|
|
@@ -22,9 +22,10 @@ TabsList.displayName = "TabsList"
|
|
|
22
22
|
const TabsTrigger = React.forwardRef<
|
|
23
23
|
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
|
24
24
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
25
|
-
>(({ className, ...props }, ref) => (
|
|
25
|
+
>(({ className, value, ...props }, ref) => (
|
|
26
26
|
<TabsPrimitive.Trigger
|
|
27
27
|
ref={ref}
|
|
28
|
+
value={value}
|
|
28
29
|
className={cn(
|
|
29
30
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
30
31
|
className
|
|
@@ -37,9 +38,10 @@ TabsTrigger.displayName = "TabsTrigger"
|
|
|
37
38
|
const TabsContent = React.forwardRef<
|
|
38
39
|
React.ComponentRef<typeof TabsPrimitive.Content>,
|
|
39
40
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
40
|
-
>(({ className, ...props }, ref) => (
|
|
41
|
+
>(({ className, value, ...props }, ref) => (
|
|
41
42
|
<TabsPrimitive.Content
|
|
42
43
|
ref={ref}
|
|
44
|
+
value={value}
|
|
43
45
|
className={cn(
|
|
44
46
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
45
47
|
className
|
|
@@ -1,139 +1,241 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Outlet,
|
|
3
|
+
Link,
|
|
4
|
+
useSelector,
|
|
5
|
+
useDispatch,
|
|
6
|
+
toggleSidebar,
|
|
7
|
+
setActiveMenu,
|
|
8
|
+
type RouteMenuItem,
|
|
9
|
+
} from "@zhin.js/client"
|
|
10
|
+
import { useMemo, useState, useCallback, type KeyboardEvent } from "react"
|
|
11
|
+
import { useLocation, useNavigate, matchPath } from "react-router"
|
|
3
12
|
import { Menu, Search, LogOut } from 'lucide-react'
|
|
4
13
|
import { cn } from "@zhin.js/client"
|
|
5
14
|
import { ThemeToggle } from "../components/ThemeToggle"
|
|
6
15
|
import { Button } from "../components/ui/button"
|
|
7
16
|
import { Input } from "../components/ui/input"
|
|
8
17
|
import { ScrollArea } from "../components/ui/scroll-area"
|
|
18
|
+
import { Separator } from "../components/ui/separator"
|
|
9
19
|
import { clearToken } from "../utils/auth"
|
|
10
20
|
|
|
21
|
+
const GROUP_ORDER = ["系统", "扩展", "配置与数据", "其他"] as const
|
|
22
|
+
|
|
23
|
+
function collectMenuRoutes(children: RouteMenuItem[]): RouteMenuItem[] {
|
|
24
|
+
return children.filter((r) => !r.meta?.hideInMenu && r.key !== "dashboardLayout")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useContentFullWidth(routes: RouteMenuItem[], pathname: string): boolean {
|
|
28
|
+
return useMemo(() => {
|
|
29
|
+
const dash = routes.find((r) => r.key === "dashboardLayout")
|
|
30
|
+
const children = dash?.children ?? []
|
|
31
|
+
for (const c of children) {
|
|
32
|
+
if (!(c.meta as { fullWidth?: boolean } | undefined)?.fullWidth || !c.path) continue
|
|
33
|
+
const m = matchPath({ path: c.path, end: true }, pathname)
|
|
34
|
+
if (m) return true
|
|
35
|
+
}
|
|
36
|
+
return false
|
|
37
|
+
}, [routes, pathname])
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
export default function DashboardLayout() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
const dispatch = useDispatch()
|
|
42
|
+
const location = useLocation()
|
|
43
|
+
const navigate = useNavigate()
|
|
44
|
+
const sidebarOpen = useSelector((state) => state.ui.sidebarOpen)
|
|
45
|
+
const activeMenu = useSelector((state) => state.ui.activeMenu)
|
|
46
|
+
const routes = useSelector((state) => state.route.routes)
|
|
47
|
+
const [searchQ, setSearchQ] = useState("")
|
|
48
|
+
|
|
49
|
+
const menuRoutes = useMemo(() => {
|
|
50
|
+
const dashboardRoute = routes.find((route) => route.key === "dashboardLayout")
|
|
51
|
+
if (!dashboardRoute?.children) return []
|
|
52
|
+
return collectMenuRoutes(dashboardRoute.children).sort(
|
|
53
|
+
(a, b) => (a.meta?.order ?? 999) - (b.meta?.order ?? 999),
|
|
54
|
+
)
|
|
55
|
+
}, [routes])
|
|
56
|
+
|
|
57
|
+
const menuByGroup = useMemo(() => {
|
|
58
|
+
const map = new Map<string, RouteMenuItem[]>()
|
|
59
|
+
for (const r of menuRoutes) {
|
|
60
|
+
const g = r.meta?.group ?? "其他"
|
|
61
|
+
if (!map.has(g)) map.set(g, [])
|
|
62
|
+
map.get(g)!.push(r)
|
|
63
|
+
}
|
|
64
|
+
for (const [, items] of map) {
|
|
65
|
+
items.sort((a, b) => (a.meta?.order ?? 999) - (b.meta?.order ?? 999))
|
|
66
|
+
}
|
|
67
|
+
return map
|
|
68
|
+
}, [menuRoutes])
|
|
69
|
+
|
|
70
|
+
const searchTargets = useMemo(
|
|
71
|
+
() => menuRoutes.map((r) => ({ title: r.title, path: r.path, key: r.key })),
|
|
72
|
+
[menuRoutes],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const searchHits = useMemo(() => {
|
|
76
|
+
const q = searchQ.trim().toLowerCase()
|
|
77
|
+
if (!q) return searchTargets
|
|
78
|
+
return searchTargets.filter(
|
|
79
|
+
(t) => t.title.toLowerCase().includes(q) || t.path.toLowerCase().includes(q),
|
|
80
|
+
)
|
|
81
|
+
}, [searchQ, searchTargets])
|
|
82
|
+
|
|
83
|
+
const onSearchKeyDown = useCallback(
|
|
84
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
85
|
+
if (e.key !== "Enter") return
|
|
86
|
+
const first = searchHits[0]
|
|
87
|
+
if (first?.path) {
|
|
88
|
+
navigate(first.path)
|
|
89
|
+
dispatch(setActiveMenu(first.key))
|
|
90
|
+
setSearchQ("")
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[searchHits, navigate, dispatch],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const contentFullWidth = useContentFullWidth(routes, location.pathname)
|
|
97
|
+
|
|
98
|
+
const orderedGroups = useMemo(() => {
|
|
99
|
+
const seen = new Set<string>()
|
|
100
|
+
const out: string[] = []
|
|
101
|
+
for (const g of GROUP_ORDER) {
|
|
102
|
+
if (menuByGroup.has(g) && menuByGroup.get(g)!.length) {
|
|
103
|
+
out.push(g)
|
|
104
|
+
seen.add(g)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const g of menuByGroup.keys()) {
|
|
108
|
+
if (!seen.has(g) && menuByGroup.get(g)!.length) out.push(g)
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}, [menuByGroup])
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex h-screen bg-background">
|
|
115
|
+
<div
|
|
116
|
+
className={cn(
|
|
117
|
+
"flex flex-col border-r bg-sidebar transition-all duration-300",
|
|
118
|
+
sidebarOpen ? "w-64" : "w-16",
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<div className="p-4 border-b">
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"flex items-center transition-all duration-300",
|
|
125
|
+
sidebarOpen ? "gap-3" : "justify-center",
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<div className="flex items-center justify-center w-9 h-9 min-w-9 rounded-lg bg-foreground text-background font-bold text-lg">
|
|
129
|
+
Z
|
|
130
|
+
</div>
|
|
131
|
+
{sidebarOpen && (
|
|
132
|
+
<div className="flex flex-col min-w-0">
|
|
133
|
+
<span className="text-base font-semibold truncate">Zhin.js</span>
|
|
134
|
+
<span className="text-xs text-muted-foreground">管理控制台</span>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<ScrollArea className="flex-1">
|
|
141
|
+
<div className="p-2 space-y-3">
|
|
142
|
+
{orderedGroups.map((groupName) => {
|
|
143
|
+
const items = menuByGroup.get(groupName) ?? []
|
|
144
|
+
if (!items.length) return null
|
|
145
|
+
return (
|
|
146
|
+
<div key={groupName} className="space-y-1">
|
|
147
|
+
{sidebarOpen && (
|
|
148
|
+
<div className="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
149
|
+
{groupName}
|
|
55
150
|
</div>
|
|
151
|
+
)}
|
|
152
|
+
{items.map((route, index) => {
|
|
153
|
+
const itemKey = route.key || `menu-item-${groupName}-${index}`
|
|
154
|
+
const isActive = activeMenu === itemKey
|
|
155
|
+
return (
|
|
156
|
+
<Link
|
|
157
|
+
key={itemKey}
|
|
158
|
+
to={route.path}
|
|
159
|
+
onClick={() => dispatch(setActiveMenu(itemKey))}
|
|
160
|
+
className={cn(
|
|
161
|
+
"menu-item",
|
|
162
|
+
isActive && "active",
|
|
163
|
+
!sidebarOpen && "justify-center px-2",
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<span className="shrink-0">{route.icon}</span>
|
|
167
|
+
{sidebarOpen && <span className="truncate">{route.title}</span>}
|
|
168
|
+
</Link>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
56
171
|
</div>
|
|
172
|
+
)
|
|
173
|
+
})}
|
|
174
|
+
</div>
|
|
175
|
+
</ScrollArea>
|
|
176
|
+
</div>
|
|
57
177
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
onClick={() => dispatch(setActiveMenu(item.key))}
|
|
68
|
-
className={cn(
|
|
69
|
-
"menu-item",
|
|
70
|
-
isActive && "active",
|
|
71
|
-
!sidebarOpen && "justify-center px-2"
|
|
72
|
-
)}
|
|
73
|
-
>
|
|
74
|
-
<span className="shrink-0">{item.icon}</span>
|
|
75
|
-
{sidebarOpen && <span className="truncate">{item.title}</span>}
|
|
76
|
-
</Link>
|
|
77
|
-
)
|
|
78
|
-
})}
|
|
79
|
-
</div>
|
|
80
|
-
</ScrollArea>
|
|
178
|
+
<div className="flex flex-col flex-1 overflow-hidden min-w-0">
|
|
179
|
+
<header className="flex items-center justify-between h-14 px-4 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 shrink-0">
|
|
180
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
181
|
+
<Button variant="ghost" size="icon" onClick={() => dispatch(toggleSidebar())}>
|
|
182
|
+
<Menu className="h-5 w-5" />
|
|
183
|
+
</Button>
|
|
184
|
+
<div className="flex flex-col min-w-0">
|
|
185
|
+
<h2 className="text-sm font-semibold truncate">控制台</h2>
|
|
186
|
+
<span className="text-xs text-muted-foreground truncate">跳转菜单 · Enter 打开首条</span>
|
|
81
187
|
</div>
|
|
188
|
+
</div>
|
|
82
189
|
|
|
83
|
-
|
|
84
|
-
<div className="
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
190
|
+
<div className="hidden md:flex flex-1 max-w-md mx-4">
|
|
191
|
+
<div className="relative w-full">
|
|
192
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
|
193
|
+
<Input
|
|
194
|
+
value={searchQ}
|
|
195
|
+
onChange={(e) => setSearchQ(e.target.value)}
|
|
196
|
+
onKeyDown={onSearchKeyDown}
|
|
197
|
+
placeholder="按名称或路径搜索菜单…"
|
|
198
|
+
className="pl-9 bg-muted/50"
|
|
199
|
+
list="console-nav-search"
|
|
200
|
+
autoComplete="off"
|
|
201
|
+
/>
|
|
202
|
+
<datalist id="console-nav-search">
|
|
203
|
+
{searchTargets.map((t) => (
|
|
204
|
+
<option key={t.path} value={`${t.title} ${t.path}`} />
|
|
205
|
+
))}
|
|
206
|
+
</datalist>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
101
209
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
210
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
211
|
+
<ThemeToggle />
|
|
212
|
+
<Button
|
|
213
|
+
variant="ghost"
|
|
214
|
+
size="icon"
|
|
215
|
+
title="退出登录"
|
|
216
|
+
onClick={() => {
|
|
217
|
+
clearToken()
|
|
218
|
+
window.dispatchEvent(new CustomEvent("zhin:auth-required"))
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<LogOut className="h-4 w-4" />
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
</header>
|
|
112
225
|
|
|
113
|
-
|
|
114
|
-
<div className="flex items-center gap-1">
|
|
115
|
-
<ThemeToggle />
|
|
116
|
-
<Button
|
|
117
|
-
variant="ghost"
|
|
118
|
-
size="icon"
|
|
119
|
-
title="退出登录"
|
|
120
|
-
onClick={() => {
|
|
121
|
-
clearToken()
|
|
122
|
-
window.dispatchEvent(new CustomEvent('zhin:auth-required'))
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
<LogOut className="h-4 w-4" />
|
|
126
|
-
</Button>
|
|
127
|
-
</div>
|
|
128
|
-
</header>
|
|
226
|
+
<Separator className="md:hidden" />
|
|
129
227
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
228
|
+
<main className="flex-1 overflow-auto min-h-0">
|
|
229
|
+
<div
|
|
230
|
+
className={cn(
|
|
231
|
+
"mx-auto p-6",
|
|
232
|
+
contentFullWidth ? "max-w-none w-full" : "max-w-7xl",
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
<Outlet />
|
|
236
|
+
</div>
|
|
237
|
+
</main>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
139
241
|
}
|