This commit is contained in:
tsuki 2025-07-29 22:17:58 +08:00
parent 1b35c937c1
commit 58edc3040e
8 changed files with 166 additions and 91 deletions

View File

@ -1,12 +1,20 @@
"use client" import { cookies } from "next/headers"
import { AppSidebar } from "./sidebar" import { AppSidebar } from "./sidebar"
import { SiteHeader } from "./site-header" import { SiteHeader } from "./site-header"
import { import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { redirect } from "next/navigation"
export default async function Layout({ children }: { children: React.ReactNode }) {
const isLoggedIn = (await cookies()).get('is_logged_in')?.value === 'true'
if (!isLoggedIn) {
redirect('/login')
}
export default function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<SidebarProvider <SidebarProvider
style={ style={

View File

@ -1,6 +1,25 @@
"use client"
// app/dashboard/page.tsx // app/dashboard/page.tsx
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useUser } from "../user-context";
export default function Dashboard() { export default function Dashboard() {
redirect("/admin/dashboard")
const { isAuthenticated, isLoading, user } = useUser()
const router = useRouter();
useEffect(() => {
if (!isLoading) {
if (!isAuthenticated) {
router.push('/login');
console.log(user?.role)
return;
}
}
}, [isAuthenticated, isLoading, router, user]);
return (<></>)
} }

View File

@ -307,10 +307,10 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32"> <DropdownMenuContent align="end" className="w-42">
<DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem> <DropdownMenuItem>Reset Password</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem> <DropdownMenuItem>Ban User</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem> <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -358,39 +358,62 @@ const GET_USERS = gql`
` `
const USERS_INFO = gql` const USERS_INFO = gql`
query UsersInfo { query UsersInfo($offset: Int, $limit: Int) {
usersInfo { usersInfo(offset: $offset, limit: $limit) {
totalUsers totalUsers
totalActiveUsers totalActiveUsers
totalAdminUsers totalAdminUsers
totalUserUsers totalUserUsers
users {
id
username
email
role
createdAt
updatedAt
}
} }
} }
` `
export function UserTable() { export function UserTable() {
const { data, loading, error, refetch } = useQuery(GET_USERS, { const { data, loading, error: usersInfoError, refetch } = useQuery(USERS_INFO)
variables: {
offset: 0, const [localData, setLocalData] = React.useState<any[]>([])
limit: 10,
}, // 同步外部数据到本地状态
}) React.useEffect(() => {
if (data && Array.isArray(data.usersInfo.users)) {
setLocalData(data.usersInfo.users)
}
}, [data])
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setLocalData((currentData) => {
const oldIndex = currentData.findIndex((item) => item.id === active.id)
const newIndex = currentData.findIndex((item) => item.id === over.id)
if (oldIndex === -1 || newIndex === -1) return currentData
// 只做本地排序,不保存到数据库
return arrayMove(currentData, oldIndex, newIndex)
})
}
}
const { data: usersInfo, loading: usersInfoLoading, error: usersInfoError } = useQuery(USERS_INFO)
return ( return (
<> <>
{loading && <div>Loading...</div>} <Tabel data={localData} refetch={refetch} info={data?.usersInfo} isLoading={loading} handleDragEnd={handleDragEnd} />
{error && <div>Error: {error.message}</div>}
{data && <Tabel data={data} info={usersInfo} refetch={refetch} />}
</> </>
) )
} }
function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any }) { function Tabel({ data, refetch, info, isLoading, handleDragEnd }: { data: any, refetch: any, info: any, isLoading: boolean, handleDragEnd: (event: DragEndEvent) => void }) {
const [rowSelection, setRowSelection] = React.useState({}) const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = const [columnVisibility, setColumnVisibility] =
@ -410,15 +433,9 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
useSensor(KeyboardSensor, {}) useSensor(KeyboardSensor, {})
) )
React.useEffect(() => {
refetch({
offset: pagination.pageIndex * pagination.pageSize,
limit: pagination.pageSize,
})
}, [pagination, refetch])
const table = useReactTable({ const table = useReactTable({
data: data?.users || [], data: data || [],
columns, columns,
state: { state: {
sorting, sorting,
@ -443,16 +460,7 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
manualPagination: true, manualPagination: true,
}) })
// function handleDragEnd(event: DragEndEvent) {
// const { active, over } = event
// if (active && over && active.id !== over.id) {
// setData((data) => {
// const oldIndex = dataIds.indexOf(active.id)
// const newIndex = dataIds.indexOf(over.id)
// return arrayMove(data, oldIndex, newIndex)
// })
// }
// }
return ( return (
<Dialog> <Dialog>
@ -467,7 +475,7 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</DialogContent> </DialogContent>
<Tabs <Tabs
defaultValue="outline" defaultValue="all_users"
className="w-full flex-col justify-start gap-6" className="w-full flex-col justify-start gap-6"
> >
<div className="flex items-center justify-between px-4 lg:px-6"> <div className="flex items-center justify-between px-4 lg:px-6">
@ -483,21 +491,21 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
<SelectValue placeholder="Select a view" /> <SelectValue placeholder="Select a view" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="outline">All Users</SelectItem> <SelectItem value="all_users">All Users</SelectItem>
<SelectItem value="past-performance">Active Users</SelectItem> <SelectItem value="active_users">Active Users</SelectItem>
<SelectItem value="key-personnel">Administrators</SelectItem> <SelectItem value="admin_users">Administrators</SelectItem>
<SelectItem value="focus-documents">Inactive Users</SelectItem> <SelectItem value="inactive_users">Inactive Users</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex"> <TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsTrigger value="outline">All Users</TabsTrigger> <TabsTrigger value="all_users">All Users</TabsTrigger>
<TabsTrigger value="past-performance"> <TabsTrigger value="active_users">
Active Users <Badge variant="secondary">{info?.usersInfo?.totalActiveUsers}</Badge> Active Users <Badge variant="secondary">{info?.totalActiveUsers}</Badge>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="key-personnel"> <TabsTrigger value="admin_users">
Administrators <Badge variant="secondary">{info?.usersInfo?.totalAdminUsers}</Badge> Administrators <Badge variant="secondary">{info?.totalAdminUsers}</Badge>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="focus-documents">Inactive Users</TabsTrigger> <TabsTrigger value="inactive_users">Inactive Users</TabsTrigger>
</TabsList> </TabsList>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DropdownMenu> <DropdownMenu>
@ -543,14 +551,14 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</div> </div>
</div> </div>
<TabsContent <TabsContent
value="outline" value="all_users"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
> >
<div className="overflow-hidden rounded-lg border"> <div className="overflow-hidden rounded-lg border">
<DndContext <DndContext
collisionDetection={closestCenter} collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]} modifiers={[restrictToVerticalAxis]}
// onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
sensors={sensors} sensors={sensors}
id={sortableId} id={sortableId}
> >
@ -574,9 +582,15 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
))} ))}
</TableHeader> </TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8"> <TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? ( {isLoading ? (<>
<TableRow>
<TableCell colSpan={columns.length} className="h-24 w-full flex items-center justify-center">
<IconLoader className="size-4 animate-spin" />
</TableCell>
</TableRow>
</>) : table.getRowModel().rows?.length ? (
<SortableContext <SortableContext
items={data?.users.map((user: any) => user.id) || []} items={data?.map((user: any) => user.id) || []}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
@ -676,16 +690,16 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</div> </div>
</TabsContent> </TabsContent>
<TabsContent <TabsContent
value="past-performance" value="active_users"
className="flex flex-col px-4 lg:px-6" className="flex flex-col px-4 lg:px-6"
> >
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent> </TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6"> <TabsContent value="admin_users" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent> </TabsContent>
<TabsContent <TabsContent
value="focus-documents" value="inactive_users"
className="flex flex-col px-4 lg:px-6" className="flex flex-col px-4 lg:px-6"
> >
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> <div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>

19
app/login/layout.tsx Normal file
View File

@ -0,0 +1,19 @@
import { cookies } from "next/headers"
import {
SidebarInset,
SidebarProvider,
} from "@/components/ui/sidebar"
import { redirect } from "next/navigation"
export default async function Layout({ children }: { children: React.ReactNode }) {
const isLoggedIn = (await cookies()).get('is_logged_in')?.value === 'true'
if (isLoggedIn) {
redirect('/')
}
return (
<>{children}</>
)
}

View File

@ -17,35 +17,35 @@ export default function LoginPage() {
} }
}, [isAuthenticated, isLoading, router]); }, [isAuthenticated, isLoading, router]);
// 如果正在加载或已认证,显示加载状态 // // 如果正在加载或已认证,显示加载状态
if (isLoading || isAuthenticated) { // if (isLoading || isAuthenticated) {
return ( // return (
<div className="grid min-h-svh lg:grid-cols-2"> // <div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10"> // <div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start"> // <div className="flex justify-center gap-2 md:justify-start">
<a href="#" className="flex items-center gap-2 font-medium"> // <a href="#" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md"> // <div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" /> // <GalleryVerticalEnd className="size-4" />
</div> // </div>
Acme Inc. // Acme Inc.
</a> // </a>
</div> // </div>
<div className="flex flex-1 items-center justify-center"> // <div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs text-center"> // <div className="w-full max-w-xs text-center">
<p className="text-muted-foreground">...</p> // <p className="text-muted-foreground">正在跳转...</p>
</div> // </div>
</div> // </div>
</div> // </div>
<div className="bg-muted relative hidden lg:block"> // <div className="bg-muted relative hidden lg:block">
<img // <img
src="/placeholder.svg" // src="/placeholder.svg"
alt="Image" // alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale" // className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/> // />
</div> // </div>
</div> // </div>
); // );
} // }
return ( return (
<div className="grid min-h-svh lg:grid-cols-2"> <div className="grid min-h-svh lg:grid-cols-2">

View File

@ -1,11 +1,21 @@
"use client" import { cookies } from "next/headers"
import { AppSidebar } from "./sidebar" import { AppSidebar } from "./sidebar"
import { import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { redirect } from "next/navigation"
export default async function Layout({ children }: { children: React.ReactNode }) {
const isLoggedIn = (await cookies()).get('is_logged_in')?.value === 'true'
console.log(isLoggedIn)
if (!isLoggedIn) {
redirect('/login')
}
export default function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<SidebarProvider <SidebarProvider
style={ style={

View File

@ -1,17 +1,18 @@
"use client" "use client"
import { redirect } from "next/navigation" import { redirect, useRouter } from "next/navigation"
import { useUser } from "../user-context" import { useUser } from "../user-context"
import { useEffect } from "react" import { useEffect } from "react"
export default function MePage() { export default function MePage() {
const { isAuthenticated, user } = useUser() const { isAuthenticated, isLoading, user } = useUser()
const router = useRouter();
useEffect(() => { useEffect(() => {
if (!isAuthenticated) { if (!isLoading && !isAuthenticated) {
redirect("/login") router.push('/login');
} }
}, [isAuthenticated]) }, [isAuthenticated, isLoading, router, user]);
return ( return (
<div> <div>

View File

@ -188,6 +188,8 @@ export function UserProvider({ children }: UserProviderProps) {
isLoading: false isLoading: false
}) })
document.cookie = 'is_logged_in=true; path=/; max-age=3600'
// 登录成功后重置 Apollo 缓存 // 登录成功后重置 Apollo 缓存
resetApolloCache() resetApolloCache()
} catch (error) { } catch (error) {
@ -261,6 +263,8 @@ export function UserProvider({ children }: UserProviderProps) {
isLoading: false isLoading: false
}) })
document.cookie = 'is_logged_in=false; path=/; max-age=3600'
// 登出后重置 Apollo 缓存 // 登出后重置 Apollo 缓存
resetApolloCache() resetApolloCache()
} }