@zhin.js/console 1.0.50 → 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.
Files changed (67) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +22 -0
  3. package/browser.tsconfig.json +19 -0
  4. package/client/src/components/PageHeader.tsx +26 -0
  5. package/client/src/components/ui/accordion.tsx +2 -1
  6. package/client/src/components/ui/badge.tsx +1 -3
  7. package/client/src/components/ui/scroll-area.tsx +5 -2
  8. package/client/src/components/ui/select.tsx +7 -3
  9. package/client/src/components/ui/separator.tsx +5 -2
  10. package/client/src/components/ui/tabs.tsx +4 -2
  11. package/client/src/layouts/dashboard.tsx +223 -121
  12. package/client/src/main.tsx +34 -34
  13. package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
  14. package/client/src/pages/bot-detail/date-utils.ts +8 -0
  15. package/client/src/pages/bot-detail/index.tsx +798 -0
  16. package/client/src/pages/bot-detail/types.ts +92 -0
  17. package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
  18. package/client/src/pages/bots.tsx +111 -73
  19. package/client/src/pages/database/constants.ts +16 -0
  20. package/client/src/pages/database/database-page.tsx +170 -0
  21. package/client/src/pages/database/document-collection-view.tsx +155 -0
  22. package/client/src/pages/database/index.tsx +1 -0
  23. package/client/src/pages/database/json-field.tsx +11 -0
  24. package/client/src/pages/database/kv-bucket-view.tsx +169 -0
  25. package/client/src/pages/database/related-table-view.tsx +221 -0
  26. package/client/src/pages/env.tsx +38 -28
  27. package/client/src/pages/files/code-editor.tsx +85 -0
  28. package/client/src/pages/files/editor-constants.ts +9 -0
  29. package/client/src/pages/files/file-editor.tsx +133 -0
  30. package/client/src/pages/files/file-icons.tsx +25 -0
  31. package/client/src/pages/files/files-page.tsx +92 -0
  32. package/client/src/pages/files/hljs-global.d.ts +10 -0
  33. package/client/src/pages/files/index.tsx +1 -0
  34. package/client/src/pages/files/language.ts +18 -0
  35. package/client/src/pages/files/tree-node.tsx +69 -0
  36. package/client/src/pages/files/use-hljs-theme.ts +23 -0
  37. package/client/src/pages/logs.tsx +77 -22
  38. package/client/src/style.css +144 -0
  39. package/client/src/utils/parseComposerContent.ts +57 -0
  40. package/client/tailwind.config.js +1 -0
  41. package/client/tsconfig.json +3 -1
  42. package/dist/assets/index-COKXlFo2.js +124 -0
  43. package/dist/assets/style-kkLO-vsa.css +3 -0
  44. package/dist/client.js +4262 -1
  45. package/dist/index.html +2 -2
  46. package/dist/radix-ui.js +1261 -1262
  47. package/dist/react-dom-client.js +2243 -2240
  48. package/dist/react-dom.js +15 -15
  49. package/dist/style.css +1 -3
  50. package/lib/index.js +1010 -81
  51. package/lib/transform.js +16 -2
  52. package/lib/websocket.js +845 -28
  53. package/node.tsconfig.json +18 -0
  54. package/package.json +15 -16
  55. package/src/bin.ts +24 -0
  56. package/src/bot-db-models.ts +74 -0
  57. package/src/bot-hub.ts +240 -0
  58. package/src/bot-persistence.ts +270 -0
  59. package/src/build.ts +90 -0
  60. package/src/dev.ts +107 -0
  61. package/src/index.ts +337 -0
  62. package/src/transform.ts +199 -0
  63. package/src/websocket.ts +1369 -0
  64. package/client/src/pages/database.tsx +0 -708
  65. package/client/src/pages/files.tsx +0 -470
  66. package/client/src/pages/login-assist.tsx +0 -225
  67. package/dist/index.js +0 -124
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
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
+
16
+ ## 1.0.51
17
+
18
+ ### Patch Changes
19
+
20
+ - a451abf: fix: console 白屏
21
+
3
22
  ## 1.0.50
4
23
 
5
24
  ### 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 interface BadgeProps
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 = "vertical", ...props }, ref) => (
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 = "popper", ...props }, ref) => (
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 = "horizontal", decorative = true, ...props }, ref) => (
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 { Outlet, Link, useSelector, useDispatch, toggleSidebar, setActiveMenu } from "@zhin.js/client"
2
- import { useMemo } from "react"
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
- const dispatch = useDispatch()
13
- const sidebarOpen = useSelector(state => state.ui.sidebarOpen)
14
- const activeMenu = useSelector(state => state.ui.activeMenu)
15
- const routes = useSelector(state => state.route.routes)
16
-
17
- const menuItems = useMemo(() => {
18
- const dashboardRoute = routes.find(route => route.key === 'dashboard-layout')
19
- if (!dashboardRoute || !dashboardRoute.children) {
20
- return []
21
- }
22
- return dashboardRoute.children
23
- .filter(route => !route.meta?.hideInMenu && route.key !== 'dashboard-layout')
24
- .map((route, index) => ({
25
- key: route.key || `menu-item-${index}`,
26
- title: route.title,
27
- index: route.index,
28
- icon: route.icon,
29
- href: route.path
30
- }))
31
- }, [routes])
32
-
33
- return (
34
- <div className="flex h-screen bg-background">
35
- {/* Sidebar */}
36
- <div className={cn(
37
- "flex flex-col border-r bg-sidebar transition-all duration-300",
38
- sidebarOpen ? "w-64" : "w-16"
39
- )}>
40
- {/* Logo */}
41
- <div className="p-4 border-b">
42
- <div className={cn(
43
- "flex items-center transition-all duration-300",
44
- sidebarOpen ? "gap-3" : "justify-center"
45
- )}>
46
- <div className="flex items-center justify-center w-9 h-9 min-w-9 rounded-lg bg-foreground text-background font-bold text-lg">
47
- Z
48
- </div>
49
- {sidebarOpen && (
50
- <div className="flex flex-col">
51
- <span className="text-base font-semibold">Zhin.js</span>
52
- <span className="text-xs text-muted-foreground">管理控制台</span>
53
- </div>
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
- {/* Menu */}
59
- <ScrollArea className="flex-1">
60
- <div className="p-2 space-y-1">
61
- {menuItems.map((item) => {
62
- const isActive = activeMenu === item.key
63
- return (
64
- <Link
65
- key={item.key}
66
- to={item.href}
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
- {/* Main content area */}
84
- <div className="flex flex-col flex-1 overflow-hidden">
85
- {/* Top bar */}
86
- <header className="flex items-center justify-between h-14 px-4 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
87
- {/* Left */}
88
- <div className="flex items-center gap-3">
89
- <Button
90
- variant="ghost"
91
- size="icon"
92
- onClick={() => dispatch(toggleSidebar())}
93
- >
94
- <Menu className="h-5 w-5" />
95
- </Button>
96
- <div className="flex flex-col">
97
- <h2 className="text-sm font-semibold">控制台</h2>
98
- <span className="text-xs text-muted-foreground">欢迎回来</span>
99
- </div>
100
- </div>
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
- {/* Center search */}
103
- <div className="hidden md:flex flex-1 max-w-md mx-6">
104
- <div className="relative w-full">
105
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
106
- <Input
107
- placeholder="搜索功能、插件、设置..."
108
- className="pl-9 bg-muted/50"
109
- />
110
- </div>
111
- </div>
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
- {/* Right actions */}
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
- {/* Page content */}
131
- <main className="flex-1 overflow-auto">
132
- <div className="max-w-7xl mx-auto p-6">
133
- <Outlet />
134
- </div>
135
- </main>
136
- </div>
137
- </div>
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
  }