From f950906f00fff0f67863bce9c5b65be8956ea77d Mon Sep 17 00:00:00 2001 From: luorijun Date: Mon, 29 Dec 2025 14:37:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=B7=AF=E7=94=B1=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E4=B8=8E=E9=89=B4=E6=9D=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: luorijun --- bun.lock | 9 + package.json | 1 + src/actions/auth.ts | 71 +++++- src/app/(root)/appbar.tsx | 272 ++++++++++++++------ src/app/(root)/navigation.tsx | 410 +++++++++++++++++------------- src/components/ui/scroll-area.tsx | 58 +++++ src/{app => }/lib/base/index.ts | 0 src/proxy.ts | 44 ++++ 8 files changed, 601 insertions(+), 264 deletions(-) create mode 100644 src/components/ui/scroll-area.tsx rename src/{app => }/lib/base/index.ts (100%) create mode 100644 src/proxy.ts diff --git a/bun.lock b/bun.lock index acf86d5..a057741 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@hookform/resolvers": "^4.1.3", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", @@ -147,6 +148,8 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.1", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], @@ -155,16 +158,22 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.8.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], diff --git a/package.json b/package.json index 9af3884..bf4a675 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@hookform/resolvers": "^4.1.3", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", diff --git a/src/actions/auth.ts b/src/actions/auth.ts index 0e39672..36d6e13 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -3,8 +3,20 @@ import { cookies } from "next/headers" import type { ApiResponse } from "@/lib/api" import { callByDevice } from "./base" -export async function login(params: LoginReq): Promise { - const resp = await callByDevice("/api/auth/token", { +export type TokenResp = { + access_token: string + refresh_token: string + expires_in: number + token_type: string + scope?: string +} + +export async function login(params: { + username: string + password: string + remember: boolean +}): Promise { + const resp = await callByDevice("/api/auth/token", { grant_type: "password", login_type: "password", login_pool: "admin", @@ -34,16 +46,49 @@ export async function login(params: LoginReq): Promise { } } -export type LoginReq = { - username: string - password: string - remember: boolean -} +export async function refreshAuth() { + const cookie = await cookies() -export type LoginRes = { - access_token: string - refresh_token: string - expires_in: number - token_type: string - scope?: string + const userRefresh = cookie.get("auth_refresh")?.value + if (!userRefresh) { + throw new Error("未授权访问") + } + + // 请求刷新访问令牌 + const resp = await callByDevice(`/api/auth/token`, { + grant_type: "refresh_token", + refresh_token: userRefresh, + }) + + // 处理请求 + if (!resp.success) { + if (resp.status === 401) { + cookie.delete("auth_refresh") + } + throw new Error("未授权访问") + } + + // 解析响应 + const data = resp.data + const nextAccessToken = data.access_token + const nextRefreshToken = data.refresh_token + const expiresIn = data.expires_in + + // 保存令牌到 cookies + cookie.set("auth_token", nextAccessToken, { + httpOnly: true, + sameSite: "strict", + maxAge: Math.max(expiresIn, 0), + }) + cookie.set("auth_refresh", nextRefreshToken, { + httpOnly: true, + sameSite: "strict", + maxAge: Number.MAX_SAFE_INTEGER, + }) + + // 返回新的访问令牌 + return { + access_token: nextAccessToken, + refresh_token: nextRefreshToken, + } } diff --git a/src/app/(root)/appbar.tsx b/src/app/(root)/appbar.tsx index c6a9bed..fcded27 100644 --- a/src/app/(root)/appbar.tsx +++ b/src/app/(root)/appbar.tsx @@ -1,24 +1,40 @@ -'use client' -import {useState, useRef, useEffect} from 'react' -import Image from 'next/image' -import Link from 'next/link' -import {usePathname} from 'next/navigation' +"use client" +import Image from "next/image" +import Link from "next/link" +import { usePathname } from "next/navigation" +import { useEffect, useRef, useState } from "react" -export type AppbarProps = {} - -export default function Appbar(props: AppbarProps) { +export default function Appbar() { const [currentUser] = useState({ - name: '张三', - avatar: '/avatar.png', - role: '管理员', + name: "张三", + avatar: "/avatar.png", + role: "管理员", }) const [showDropdown, setShowDropdown] = useState(false) const [showNotifications, setShowNotifications] = useState(false) const [notifications] = useState([ - {id: 1, title: '系统通知', content: '您有新的待审核内容', time: '10分钟前', read: false}, - {id: 2, title: '安全提醒', content: '您的账号于昨天登录了新设备', time: '1小时前', read: true}, - {id: 3, title: '系统更新', content: '系统将在今晚进行例行维护', time: '2小时前', read: true}, + { + id: 1, + title: "系统通知", + content: "您有新的待审核内容", + time: "10分钟前", + read: false, + }, + { + id: 2, + title: "安全提醒", + content: "您的账号于昨天登录了新设备", + time: "1小时前", + read: true, + }, + { + id: 3, + title: "系统更新", + content: "系统将在今晚进行例行维护", + time: "2小时前", + read: true, + }, ]) const pathname = usePathname() @@ -28,28 +44,34 @@ export default function Appbar(props: AppbarProps) { // 处理点击外部关闭下拉菜单 useEffect(() => { function handleClickOutside(event: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { setShowDropdown(false) } - if (notificationRef.current && !notificationRef.current.contains(event.target as Node)) { + if ( + notificationRef.current && + !notificationRef.current.contains(event.target as Node) + ) { setShowNotifications(false) } } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) }, []) // 根据路径生成面包屑 const generateBreadcrumbs = () => { - const paths = pathname.split('/').filter(Boolean) + const paths = pathname.split("/").filter(Boolean) const breadcrumbs = [ - {path: '/', label: '首页'}, + { path: "/", label: "首页" }, ...paths.map((path, index) => { - const url = `/${paths.slice(0, index + 1).join('/')}` + const url = `/${paths.slice(0, index + 1).join("/")}` const label = getBreadcrumbLabel(path) - return {path: url, label} + return { path: url, label } }), ] @@ -58,14 +80,14 @@ export default function Appbar(props: AppbarProps) { const getBreadcrumbLabel = (path: string) => { const labels: Record = { - 'dashboard': '控制台', - 'content': '内容管理', - 'articles': '文章管理', - 'media': '媒体库', - 'users': '用户管理', - 'roles': '角色权限', - 'settings': '系统设置', - 'logs': '系统日志', + dashboard: "控制台", + content: "内容管理", + articles: "文章管理", + media: "媒体库", + users: "用户管理", + roles: "角色权限", + settings: "系统设置", + logs: "系统日志", } return labels[path] || path @@ -81,15 +103,26 @@ export default function Appbar(props: AppbarProps) { {breadcrumbs.map((crumb, index) => (
{index > 0 && ( - - + + )} {crumb.label} @@ -108,11 +141,17 @@ export default function Appbar(props: AppbarProps) { className="pl-10 pr-4 py-2 bg-gray-100 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-56" /> + className="h-4 w-4 text-gray-400 absolute left-3 top-2.5" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" + />
@@ -123,15 +162,23 @@ export default function Appbar(props: AppbarProps) { className="relative p-2 rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition-colors" aria-label="通知" > - + {unreadCount > 0 && ( - {unreadCount} + + {unreadCount} + )} @@ -147,25 +194,39 @@ export default function Appbar(props: AppbarProps) {
{notifications.length > 0 ? ( - notifications.map((notification) => ( + notifications.map(notification => (
-

{notification.title}

- {notification.time} +

+ {notification.title} +

+ + {notification.time} +
-

{notification.content}

+

+ {notification.content} +

)) ) : (
- - +

暂无通知

@@ -174,7 +235,10 @@ export default function Appbar(props: AppbarProps) {
- + 查看全部通知
@@ -192,27 +256,37 @@ export default function Appbar(props: AppbarProps) { className="flex items-center space-x-2 rounded-lg hover:bg-gray-100 p-2 transition-colors" aria-label="用户菜单" > -
+
用户头像 { + onError={e => { const target = e.target as HTMLImageElement - target.style.display = 'none' + target.style.display = "none" target.parentElement!.innerHTML = currentUser.name.charAt(0) }} />
-

{currentUser.name}

+

+ {currentUser.name} +

{currentUser.role}

- + className="h-4 w-4 text-gray-400 hidden md:block" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + @@ -225,44 +299,88 @@ export default function Appbar(props: AppbarProps) {
- - + + + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + /> 个人资料 - + className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + > + + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" + /> + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" + /> 账号设置 - + href="/system/help" + className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + > + + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + /> 帮助中心
- - + + + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" + /> 退出登录 diff --git a/src/app/(root)/navigation.tsx b/src/app/(root)/navigation.tsx index 9240ac7..dfebc59 100644 --- a/src/app/(root)/navigation.tsx +++ b/src/app/(root)/navigation.tsx @@ -1,191 +1,253 @@ -'use client' -import {useState} from 'react' -import Link from 'next/link' -import {usePathname} from 'next/navigation' +"use client" +import { + Activity, + BarChart3, + ChevronsLeft, + ChevronsRight, + ClipboardList, + Code, + Database, + DollarSign, + FileText, + Globe, + Home, + type LucideIcon, + Package, + Server, + Settings, + Shield, + Users, +} from "lucide-react" +import Link from "next/link" +import { usePathname } from "next/navigation" +import { createContext, type ReactNode, useContext, useState } from "react" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" -export type NavigationProps = {} - -// 菜单组接口 -interface MenuGroup { - title: string; - items: MenuItem[]; +// Navigation Context +interface NavigationContextType { + collapsed: boolean + pathname: string + isActive: (path: string) => boolean } -// 菜单项接口 -interface MenuItem { - path: string; - icon: string; - label: string; +const NavigationContext = createContext( + undefined, +) + +const useNavigation = () => { + const context = useContext(NavigationContext) + if (!context) { + throw new Error("Navigation components must be used within Navigation") + } + return context } -// 定义菜单组 -const menuGroups: MenuGroup[] = [ - { - title: '概览', - items: [ - { - path: '/', - icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6', - label: '首页', - }, - { - path: '/statistics', - icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', - label: '数据统计', - }, - ], - }, - { - title: 'IP 资源', - items: [ - { - path: '/proxy/nodes', - icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9', - label: '节点列表', - }, - { - path: '/proxy/pools', - icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01', - label: 'IP池管理', - }, - { - path: '/proxy/sources', - icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10', - label: '代理源管理', - }, - ], - }, - { - title: '客户', - items: [ - { - path: '/clients', - icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z', - label: '客户管理', - }, - {path: '/packages', icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10', label: '套餐管理'}, - { - path: '/orders', - icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', - label: '订单管理', - }, - ], - }, - { - title: '运营', - items: [ - {path: '/api/management', icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4', label: 'API管理'}, - { - path: '/traffic', - icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', - label: '流量监控', - }, - { - path: '/billing', - icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z', - label: '计费系统', - }, - ], - }, - { - title: '系统', - items: [ - { - path: '/settings', - icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', - label: '系统设置', - }, - { - path: '/security', - icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z', - label: '安全管理', - }, - { - path: '/logs', - icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', - label: '系统日志', - }, - ], - }, -] +// NavigationGroup Component +interface NavigationGroupProps { + title: string + children: ReactNode +} -export default function Navigation(props: NavigationProps) { +function NavigationGroup({ title, children }: NavigationGroupProps) { + const { collapsed } = useNavigation() + + return ( +
+ {!collapsed && ( +

+ {title} +

+ )} +
    {children}
+
+ ) +} + +// NavigationItem Component +interface NavigationItemProps { + href: string + icon: LucideIcon + label: string +} + +function NavigationItem({ href, icon: Icon, label }: NavigationItemProps) { + const { collapsed, isActive } = useNavigation() + const active = isActive(href) + + return ( +
  • + + + {!collapsed && ( + {label} + )} + +
  • + ) +} + +// NavigationSeparator Component +function NavigationSeparator() { + const { collapsed } = useNavigation() + + if (collapsed) return null + + return ( +
    + +
    + ) +} + +// Main Navigation Component +export default function Navigation() { const [collapsed, setCollapsed] = useState(false) const pathname = usePathname() const isActive = (path: string) => { - return path === '/' - ? pathname === path - : pathname.startsWith(path) + return path === "/" ? pathname === path : pathname.startsWith(path) + } + + const contextValue: NavigationContextType = { + collapsed, + pathname, + isActive, } return ( - + {/* Navigation Menu */} + + + + + {/* Toggle Button */} +
    + +
    + + ) } diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8e4fa13 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/src/app/lib/base/index.ts b/src/lib/base/index.ts similarity index 100% rename from src/app/lib/base/index.ts rename to src/lib/base/index.ts diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..92b83bb --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,44 @@ +import { type NextRequest, NextResponse, type ProxyConfig } from "next/server" +import { refreshAuth } from "@/actions/auth" + +export const config: ProxyConfig = { + matcher: [ + "/((?!api|_next/static|_next/image|.well-known|sw.js|favicon.ico|sitemap.xml|robots.txt).*(?