Skip to content

Commit c6600ad

Browse files
committed
feat: 完成前端移动端适配和响应式布局优化
- 新增 Sheet 组件实现移动端抽屉导航菜单 - 优化导航栏在移动端显示,添加汉堡菜单切换 - 改进所有主要页面的移动端布局(任务页、插件页、系统配置、系统日志) - 表格布局在移动端自动切换为卡片布局 - 优化按钮组和操作区域的响应式显示 - 统一使用 Tailwind CSS 响应式断点实现一套代码适配多端 - 修复移动端滚动和触摸交互体验
1 parent 5adf80c commit c6600ad

6 files changed

Lines changed: 392 additions & 250 deletions

File tree

frontend/src/App.tsx

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import SystemLogs from './pages/SystemLogs'
88
import TokensPage from './pages/TokensPage'
99
import LoginPage from './pages/LoginPage'
1010
import { authApi, User } from './lib/api'
11-
import { LogOut } from 'lucide-react'
11+
import { LogOut, Menu, X } from 'lucide-react'
1212
import { Button } from './components/ui/button'
13+
import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet'
1314

1415
function Navigation({ user, onLogout, onRefreshUser }: { user: User | null; onLogout: () => void; onRefreshUser: () => void }) {
1516
const location = useLocation()
17+
const [open, setOpen] = useState(false)
1618

1719
// 当路由变化时,检查是否需要刷新用户信息
1820
useEffect(() => {
@@ -26,74 +28,98 @@ function Navigation({ user, onLogout, onRefreshUser }: { user: User | null; onLo
2628
return location.pathname === path
2729
}
2830

31+
const navLinks = [
32+
{ path: '/', label: '下载任务' },
33+
{ path: '/plugins', label: '插件管理' },
34+
{ path: '/settings', label: '系统配置' },
35+
{ path: '/logs', label: '系统日志' },
36+
{ path: '/tokens', label: 'API Token' },
37+
]
38+
2939
return (
30-
<nav className="border-b bg-card">
40+
<nav className="border-b bg-card sticky top-0 z-50">
3141
<div className="container mx-auto px-4">
3242
<div className="flex h-16 items-center justify-between">
33-
<div className="flex items-center space-x-8">
34-
<div className="flex items-center space-x-2">
35-
<h1 className="text-xl font-bold">🪹 MyNest</h1>
36-
<span className="text-sm text-muted-foreground">链接的归巢</span>
37-
</div>
38-
<div className="flex items-center space-x-6">
39-
<Link
40-
to="/"
41-
className={`text-sm font-medium transition-colors hover:text-primary ${
42-
isActive('/') ? 'text-primary' : 'text-muted-foreground'
43-
}`}
44-
>
45-
下载任务
46-
</Link>
47-
<Link
48-
to="/plugins"
49-
className={`text-sm font-medium transition-colors hover:text-primary ${
50-
isActive('/plugins') ? 'text-primary' : 'text-muted-foreground'
51-
}`}
52-
>
53-
插件管理
54-
</Link>
55-
<Link
56-
to="/settings"
57-
className={`text-sm font-medium transition-colors hover:text-primary ${
58-
isActive('/settings') ? 'text-primary' : 'text-muted-foreground'
59-
}`}
60-
>
61-
系统配置
62-
</Link>
63-
<Link
64-
to="/logs"
65-
className={`text-sm font-medium transition-colors hover:text-primary ${
66-
isActive('/logs') ? 'text-primary' : 'text-muted-foreground'
67-
}`}
68-
>
69-
系统日志
70-
</Link>
43+
{/* Logo */}
44+
<Link to="/" className="flex items-center space-x-2">
45+
<h1 className="text-xl font-bold">🪹 MyNest</h1>
46+
<span className="hidden sm:inline text-sm text-muted-foreground">链接的归巢</span>
47+
</Link>
48+
49+
{/* 桌面端导航 */}
50+
<div className="hidden md:flex items-center space-x-6">
51+
{navLinks.map((link) => (
7152
<Link
72-
to="/tokens"
53+
key={link.path}
54+
to={link.path}
7355
className={`text-sm font-medium transition-colors hover:text-primary ${
74-
isActive('/tokens') ? 'text-primary' : 'text-muted-foreground'
56+
isActive(link.path) ? 'text-primary' : 'text-muted-foreground'
7557
}`}
7658
>
77-
API Token
59+
{link.label}
7860
</Link>
79-
</div>
61+
))}
8062
</div>
81-
<div className="flex items-center space-x-4">
63+
64+
{/* 用户信息和移动端菜单 */}
65+
<div className="flex items-center space-x-2">
8266
{user && (
8367
<>
84-
<span className="text-sm text-muted-foreground">
68+
<span className="hidden sm:inline text-sm text-muted-foreground">
8569
{user.username}
8670
</span>
8771
<Button
8872
variant="ghost"
8973
size="sm"
9074
onClick={onLogout}
75+
className="hidden sm:flex"
9176
>
9277
<LogOut className="w-4 h-4 mr-2" />
9378
退出
9479
</Button>
9580
</>
9681
)}
82+
83+
{/* 移动端汉堡菜单 */}
84+
<Sheet open={open} onOpenChange={setOpen}>
85+
<SheetTrigger asChild>
86+
<Button variant="ghost" size="sm" className="md:hidden">
87+
<Menu className="h-5 w-5" />
88+
</Button>
89+
</SheetTrigger>
90+
<SheetContent side="right" className="w-64">
91+
<div className="flex flex-col space-y-4 mt-8">
92+
{user && (
93+
<div className="pb-4 border-b">
94+
<p className="text-sm font-medium">{user.username}</p>
95+
</div>
96+
)}
97+
{navLinks.map((link) => (
98+
<Link
99+
key={link.path}
100+
to={link.path}
101+
onClick={() => setOpen(false)}
102+
className={`text-base font-medium transition-colors hover:text-primary py-2 ${
103+
isActive(link.path) ? 'text-primary' : 'text-muted-foreground'
104+
}`}
105+
>
106+
{link.label}
107+
</Link>
108+
))}
109+
<Button
110+
variant="ghost"
111+
onClick={() => {
112+
setOpen(false)
113+
onLogout()
114+
}}
115+
className="justify-start px-0 text-base font-medium text-muted-foreground hover:text-primary"
116+
>
117+
<LogOut className="w-4 h-4 mr-2" />
118+
退出
119+
</Button>
120+
</div>
121+
</SheetContent>
122+
</Sheet>
97123
</div>
98124
</div>
99125
</div>
@@ -159,9 +185,9 @@ function App() {
159185
path="/*"
160186
element={
161187
<ProtectedRoute>
162-
<div className="min-h-screen bg-background">
188+
<div className="min-h-screen bg-background overflow-x-hidden">
163189
<Navigation user={user} onLogout={handleLogout} onRefreshUser={checkAuth} />
164-
<main className="container mx-auto px-4 py-8">
190+
<main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-full">
165191
<Routes>
166192
<Route path="/" element={<TasksPage />} />
167193
<Route path="/plugins" element={<PluginsPage />} />
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as React from "react"
2+
import * as DialogPrimitive from "@radix-ui/react-dialog"
3+
import { X } from "lucide-react"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const Sheet = DialogPrimitive.Root
8+
9+
const SheetTrigger = DialogPrimitive.Trigger
10+
11+
const SheetClose = DialogPrimitive.Close
12+
13+
const SheetPortal = DialogPrimitive.Portal
14+
15+
const SheetOverlay = React.forwardRef<
16+
React.ElementRef<typeof DialogPrimitive.Overlay>,
17+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18+
>(({ className, ...props }, ref) => (
19+
<DialogPrimitive.Overlay
20+
className={cn(
21+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22+
className
23+
)}
24+
{...props}
25+
ref={ref}
26+
/>
27+
))
28+
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
29+
30+
interface SheetContentProps
31+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
32+
side?: "top" | "right" | "bottom" | "left"
33+
}
34+
35+
const SheetContent = React.forwardRef<
36+
React.ElementRef<typeof DialogPrimitive.Content>,
37+
SheetContentProps
38+
>(({ side = "right", className, children, ...props }, ref) => (
39+
<SheetPortal>
40+
<SheetOverlay />
41+
<DialogPrimitive.Content
42+
ref={ref}
43+
className={cn(
44+
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
45+
side === "top" &&
46+
"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
47+
side === "bottom" &&
48+
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
49+
side === "left" &&
50+
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
51+
side === "right" &&
52+
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
53+
className
54+
)}
55+
{...props}
56+
>
57+
{children}
58+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
59+
<X className="h-4 w-4" />
60+
<span className="sr-only">Close</span>
61+
</DialogPrimitive.Close>
62+
</DialogPrimitive.Content>
63+
</SheetPortal>
64+
))
65+
SheetContent.displayName = DialogPrimitive.Content.displayName
66+
67+
export { Sheet, SheetTrigger, SheetClose, SheetContent }

frontend/src/pages/PluginsPage.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,15 @@ export default function PluginsPage() {
9292
}
9393

9494
return (
95-
<div className="space-y-6">
96-
<h2 className="text-3xl font-bold tracking-tight">插件管理</h2>
95+
<div className="space-y-4 sm:space-y-6">
96+
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">插件管理</h2>
9797

9898
{plugins.length === 0 ? (
99-
<div className="rounded-lg border border-dashed p-12 text-center">
100-
<p className="text-muted-foreground">暂无已安装插件</p>
99+
<div className="rounded-lg border border-dashed p-8 sm:p-12 text-center">
100+
<p className="text-sm sm:text-base text-muted-foreground">暂无已安装插件</p>
101101
</div>
102102
) : (
103-
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
103+
<div className="grid gap-4 sm:gap-6 sm:grid-cols-2 lg:grid-cols-3">
104104
{plugins.map((plugin) => (
105105
<Card key={plugin.id}>
106106
<CardHeader>
@@ -128,12 +128,13 @@ export default function PluginsPage() {
128128
版本: {plugin.version || 'unknown'}
129129
</CardDescription>
130130
</CardHeader>
131-
<CardFooter className="flex gap-2">
131+
<CardFooter className="flex flex-col sm:flex-row gap-2">
132132
{plugin.enabled && !plugin.running && (
133133
<Button
134134
onClick={() => handleStart(plugin)}
135135
variant="default"
136-
className="flex-1"
136+
className="flex-1 w-full sm:w-auto"
137+
size="sm"
137138
>
138139
启动
139140
</Button>
@@ -142,7 +143,8 @@ export default function PluginsPage() {
142143
<Button
143144
onClick={() => handleStop(plugin)}
144145
variant="outline"
145-
className="flex-1"
146+
className="flex-1 w-full sm:w-auto"
147+
size="sm"
146148
>
147149
停止
148150
</Button>
@@ -151,13 +153,14 @@ export default function PluginsPage() {
151153
<Button
152154
onClick={() => handleToggle(plugin)}
153155
variant="default"
154-
className="flex-1"
156+
className="flex-1 w-full sm:w-auto"
157+
size="sm"
155158
>
156159
启用
157160
</Button>
158161
)}
159162
{plugin.enabled && (
160-
<>
163+
<div className="flex gap-2 w-full sm:w-auto">
161164
<Button
162165
variant="outline"
163166
size="icon"
@@ -166,6 +169,7 @@ export default function PluginsPage() {
166169
setLogsOpen(true)
167170
}}
168171
title="查看日志"
172+
className="flex-1 sm:flex-initial"
169173
>
170174
<FileText className="h-4 w-4" />
171175
</Button>
@@ -177,6 +181,7 @@ export default function PluginsPage() {
177181
setConfigOpen(true)
178182
}}
179183
title="配置"
184+
className="flex-1 sm:flex-initial"
180185
>
181186
<Settings className="h-4 w-4" />
182187
</Button>
@@ -185,18 +190,20 @@ export default function PluginsPage() {
185190
size="icon"
186191
onClick={() => handleRestart(plugin)}
187192
title="重启"
193+
className="flex-1 sm:flex-initial"
188194
>
189195
<RotateCcw className="h-4 w-4" />
190196
</Button>
191197
<Button
192198
variant="destructive"
193-
size="icon"
199+
size="sm"
194200
onClick={() => handleToggle(plugin)}
195201
title="禁用"
202+
className="flex-1 sm:flex-initial text-xs"
196203
>
197204
禁用
198205
</Button>
199-
</>
206+
</div>
200207
)}
201208
</CardFooter>
202209
</Card>

0 commit comments

Comments
 (0)