Skip to content

Commit 9ab83f3

Browse files
committed
feat: unify ui interactions
1 parent c404ea4 commit 9ab83f3

37 files changed

Lines changed: 1102 additions & 145 deletions

File tree

apps/web/app/components/AdminLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { cn } from "~/lib/utils";
1919
import { CNodeLogo } from "./CNodeLogo";
2020
import { CommandPalette } from "./CommandPalette";
2121
import { PageContainer } from "./PageShell";
22+
import { ScrollTopButton } from "./ScrollTopButton";
2223
import { useState } from "react";
2324
import { useAuthStore } from "~/lib/stores/auth-store";
2425

@@ -135,6 +136,7 @@ export function AdminLayout({ children }: { children: React.ReactNode }) {
135136
<main className={cn("min-h-[calc(100vh-8rem)] min-w-0 pb-8 transition-opacity duration-200", isNavigating ? "opacity-60" : "opacity-100")}>{children}</main>
136137
</div>
137138
</PageContainer>
139+
<ScrollTopButton />
138140
</div>
139141
);
140142
}

apps/web/app/components/CommandPalette.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useMemo, useState } from "react";
2-
import { Link, useNavigate } from "react-router";
2+
import { Link, useNavigate, useRouteLoaderData } from "react-router";
33
import {
44
FileText,
55
HelpCircle,
@@ -19,9 +19,10 @@ const quickActions = [
1919
{ label: "发布话题", to: "/topic/create", icon: Pencil },
2020
{ label: "我的消息", to: "/my/messages", icon: MessageSquare },
2121
{ label: "新手指南", to: "/getstart", icon: HelpCircle },
22-
{ label: "API 文档", to: "/api", icon: FileText },
22+
{ label: "API", to: "/api", icon: FileText },
23+
{ label: "常见问题", to: "/faq", icon: HelpCircle },
2324
{ label: "关于 CNode", to: "/about", icon: Info },
24-
{ label: "管理后台", to: "/admin", icon: LayoutDashboard },
25+
{ label: "管理后台", to: "/admin", icon: LayoutDashboard, adminOnly: true },
2526
];
2627

2728
export function CommandPalette({
@@ -33,9 +34,16 @@ export function CommandPalette({
3334
}) {
3435
const [query, setQuery] = useState("");
3536
const navigate = useNavigate();
37+
const rootData = useRouteLoaderData("root") as { user?: { is_admin?: boolean; is_mod?: boolean } } | undefined;
38+
const canAccessAdmin = !!(rootData?.user?.is_admin || rootData?.user?.is_mod);
3639
const actions = useMemo(
37-
() => quickActions.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())),
38-
[query],
40+
() =>
41+
quickActions.filter(
42+
(item) =>
43+
(!item.adminOnly || canAccessAdmin) &&
44+
item.label.toLowerCase().includes(query.toLowerCase()),
45+
),
46+
[canAccessAdmin, query],
3947
);
4048

4149
useEffect(() => {

apps/web/app/components/JobFilterBar.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export function JobFilterBar({ locations }: JobFilterBarProps) {
3838
setSearchParams(next);
3939
}
4040

41-
const controlClass = "h-10 w-full rounded-md border border-input bg-background px-3 text-sm";
41+
const controlClass =
42+
"h-9 w-full rounded-xl border border-input bg-card px-3 text-sm shadow-sm transition-colors hover:border-cnode-green/35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
4243

4344
const filterControls = (
4445
<div className="grid gap-3 md:grid-cols-[minmax(8rem,1fr)_minmax(8rem,1fr)_8rem_minmax(13rem,1.4fr)_6rem] md:items-end">
@@ -81,7 +82,7 @@ export function JobFilterBar({ locations }: JobFilterBarProps) {
8182
value={salaryMin}
8283
onChange={(e) => updateParam("salary_min", e.target.value)}
8384
placeholder="如 30"
84-
className="h-10 w-full"
85+
className="h-9 w-full"
8586
/>
8687
</div>
8788

@@ -91,19 +92,19 @@ export function JobFilterBar({ locations }: JobFilterBarProps) {
9192
value={tags}
9293
onChange={(e) => updateParam("tags", e.target.value)}
9394
placeholder="如 Node,PostgreSQL"
94-
className="h-10 w-full"
95+
className="h-9 w-full"
9596
/>
9697
</div>
9798

98-
<Button type="button" variant="ghost" className="h-10 w-full" onClick={clearAll}>
99+
<Button type="button" variant="ghost" className="h-9 w-full" onClick={clearAll}>
99100
清除筛选
100101
</Button>
101102
</div>
102103
);
103104

104105
return (
105106
<>
106-
<div className="hidden rounded-xl border border-border bg-card p-3 shadow-sm md:block">
107+
<div className="hidden rounded-2xl border border-border bg-card p-3 shadow-sm md:block">
107108
{filterControls}
108109
</div>
109110

apps/web/app/components/Layout.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
2626
import { CNodeLogo } from "./CNodeLogo";
2727
import { CommandPalette } from "./CommandPalette";
2828
import { PageContainer } from "./PageShell";
29+
import { ScrollTopButton } from "./ScrollTopButton";
2930
import { cn } from "~/lib/utils";
3031
import {
3132
DropdownMenu,
@@ -47,6 +48,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
4748
<PageContainer className="py-6">{children}</PageContainer>
4849
</main>
4950
<Footer />
51+
<ScrollTopButton />
5052
</div>
5153
);
5254
}
@@ -84,6 +86,10 @@ function Header() {
8486
{z.name}
8587
</NavLink>
8688
))}
89+
<NavLink to="/api">
90+
<Code className="h-4 w-4" />
91+
API
92+
</NavLink>
8793
<DropdownMenu>
8894
<DropdownMenuTrigger asChild>
8995
<button className="inline-flex h-9 items-center gap-1 rounded-lg px-3 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
@@ -93,21 +99,15 @@ function Header() {
9399
</button>
94100
</DropdownMenuTrigger>
95101
<DropdownMenuContent align="end" className="w-36">
96-
<DropdownMenuItem asChild>
97-
<Link to="/help" className="w-full justify-start">指引总览</Link>
98-
</DropdownMenuItem>
99102
<DropdownMenuItem asChild>
100103
<Link to="/getstart" className="w-full justify-start">新手指南</Link>
101104
</DropdownMenuItem>
102105
<DropdownMenuItem asChild>
103-
<Link to="/api" className="w-full justify-start">API 文档</Link>
106+
<Link to="/faq" className="w-full justify-start">常见问题</Link>
104107
</DropdownMenuItem>
105108
<DropdownMenuItem asChild>
106109
<Link to="/about" className="w-full justify-start">关于我们</Link>
107110
</DropdownMenuItem>
108-
<DropdownMenuItem asChild>
109-
<Link to="/faq" className="w-full justify-start">常见问题</Link>
110-
</DropdownMenuItem>
111111
</DropdownMenuContent>
112112
</DropdownMenu>
113113
</nav>
@@ -237,7 +237,7 @@ export function HeaderUserArea() {
237237
<DropdownMenuItem asChild>
238238
<Link to="/setting">
239239
<Settings />
240-
设置
240+
用户设置
241241
</Link>
242242
</DropdownMenuItem>
243243
</DropdownMenuGroup>
@@ -315,12 +315,6 @@ function MobileNavTrigger({ visibleZones = [] }: { visibleZones?: any[] }) {
315315
>
316316
<Bell className="h-5 w-5 text-primary" /> 消息
317317
</Link>
318-
<Link
319-
to="/help"
320-
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
321-
>
322-
<HelpCircle className="h-5 w-5 text-primary" /> 指引总览
323-
</Link>
324318
<Link
325319
to="/getstart"
326320
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
@@ -331,19 +325,19 @@ function MobileNavTrigger({ visibleZones = [] }: { visibleZones?: any[] }) {
331325
to="/api"
332326
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
333327
>
334-
<Code className="h-5 w-5 text-primary" /> API 文档
328+
<Code className="h-5 w-5 text-primary" /> API
335329
</Link>
336330
<Link
337-
to="/about"
331+
to="/faq"
338332
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
339333
>
340-
<Info className="h-5 w-5 text-primary" /> 关于我们
334+
<HelpCircle className="h-5 w-5 text-primary" /> 常见问题
341335
</Link>
342336
<Link
343-
to="/faq"
337+
to="/about"
344338
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
345339
>
346-
<HelpCircle className="h-5 w-5 text-primary" /> 常见问题
340+
<Info className="h-5 w-5 text-primary" /> 关于我们
347341
</Link>
348342
{user ? (
349343
<>
@@ -357,7 +351,7 @@ function MobileNavTrigger({ visibleZones = [] }: { visibleZones?: any[] }) {
357351
to="/setting"
358352
className="flex min-h-12 items-center gap-3 rounded-xl border border-cnode-green/15 bg-card px-3 text-sm text-foreground shadow-sm hover:bg-cnode-soft hover:text-cnode-ink"
359353
>
360-
<Settings className="h-5 w-5 text-primary" /> 设置
354+
<Settings className="h-5 w-5 text-primary" /> 用户设置
361355
</Link>
362356
{(user.is_admin || user.is_mod) && (
363357
<Link

apps/web/app/components/Pagination.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ interface PaginationProps {
66
limit: number;
77
basePath: string;
88
searchParams?: Record<string, string>;
9+
variant?: "numbered" | "simple";
910
}
1011

11-
export function Pagination({ page, total, limit, basePath, searchParams = {} }: PaginationProps) {
12+
export function Pagination({
13+
page,
14+
total,
15+
limit,
16+
basePath,
17+
searchParams = {},
18+
variant = "numbered",
19+
}: PaginationProps) {
1220
const totalPages = Math.ceil(total / limit);
1321
if (totalPages <= 1) return null;
1422

@@ -24,12 +32,31 @@ export function Pagination({ page, total, limit, basePath, searchParams = {} }:
2432
for (let i = start; i <= end; i++) pages.push(i);
2533

2634
const linkClass =
27-
"px-3 py-1 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-sm";
35+
"inline-flex h-9 items-center rounded-xl border border-input bg-card px-3 text-sm shadow-sm hover:bg-accent hover:text-accent-foreground";
2836
const activeClass =
2937
"border-cnode-ink bg-cnode-ink text-white hover:bg-cnode-ink/90 hover:text-white";
3038

39+
if (variant === "simple") {
40+
return (
41+
<div className="mt-4 flex items-center justify-between gap-2">
42+
{page > 1 ? (
43+
<Link to={buildUrl(page - 1)} className={linkClass}>
44+
← 上一页
45+
</Link>
46+
) : (
47+
<span />
48+
)}
49+
{page < totalPages ? (
50+
<Link to={buildUrl(page + 1)} className={linkClass}>
51+
下一页 →
52+
</Link>
53+
) : null}
54+
</div>
55+
);
56+
}
57+
3158
return (
32-
<div className="flex items-center gap-2 mt-4">
59+
<div className="mt-4 flex items-center gap-2">
3360
{page > 1 && (
3461
<Link to={buildUrl(page - 1)} className={linkClass}>
3562
← 上一页
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useState } from "react";
2+
import { ArrowUp } from "lucide-react";
3+
import { Button } from "./ui/button";
4+
import { cn } from "~/lib/utils";
5+
6+
export function ScrollTopButton({ className }: { className?: string }) {
7+
const [visible, setVisible] = useState(false);
8+
9+
useEffect(() => {
10+
const update = () => setVisible(window.scrollY > 480);
11+
update();
12+
window.addEventListener("scroll", update, { passive: true });
13+
return () => window.removeEventListener("scroll", update);
14+
}, []);
15+
16+
if (!visible) return null;
17+
18+
return (
19+
<Button
20+
type="button"
21+
variant="default"
22+
className={cn(
23+
"fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-4 z-50 h-11 rounded-full px-3 shadow-floating sm:bottom-6 sm:right-6 sm:px-4",
24+
className,
25+
)}
26+
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
27+
aria-label="回到顶部"
28+
title="回到顶部"
29+
>
30+
<ArrowUp className="h-4 w-4" />
31+
<span className="hidden sm:inline">顶部</span>
32+
</Button>
33+
);
34+
}

apps/web/app/routes/_index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export default function Index({ loaderData }: Route.ComponentProps) {
9292
limit={limit}
9393
basePath="/"
9494
searchParams={{ ...(tab !== "all" ? { tab } : {}) }}
95+
variant="simple"
9596
/>
9697
</div>
9798

0 commit comments

Comments
 (0)