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 { SiteHeader } from "./site-header"
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('/login')
}
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider
style={

View File

@ -1,6 +1,25 @@
"use client"
// app/dashboard/page.tsx
import { redirect } from "next/navigation"
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useUser } from "../user-context";
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>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuContent align="end" className="w-42">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuItem>Reset Password</DropdownMenuItem>
<DropdownMenuItem>Ban User</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
@ -358,39 +358,62 @@ const GET_USERS = gql`
`
const USERS_INFO = gql`
query UsersInfo {
usersInfo {
query UsersInfo($offset: Int, $limit: Int) {
usersInfo(offset: $offset, limit: $limit) {
totalUsers
totalActiveUsers
totalAdminUsers
totalUserUsers
users {
id
username
email
role
createdAt
updatedAt
}
}
}
`
export function UserTable() {
const { data, loading, error, refetch } = useQuery(GET_USERS, {
variables: {
offset: 0,
limit: 10,
},
})
const { data, loading, error: usersInfoError, refetch } = useQuery(USERS_INFO)
const [localData, setLocalData] = React.useState<any[]>([])
// 同步外部数据到本地状态
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 (
<>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && <Tabel data={data} info={usersInfo} refetch={refetch} />}
<Tabel data={localData} refetch={refetch} info={data?.usersInfo} isLoading={loading} handleDragEnd={handleDragEnd} />
</>
)
}
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 [columnVisibility, setColumnVisibility] =
@ -410,15 +433,9 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
useSensor(KeyboardSensor, {})
)
React.useEffect(() => {
refetch({
offset: pagination.pageIndex * pagination.pageSize,
limit: pagination.pageSize,
})
}, [pagination, refetch])
const table = useReactTable({
data: data?.users || [],
data: data || [],
columns,
state: {
sorting,
@ -443,16 +460,7 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
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 (
<Dialog>
@ -467,7 +475,7 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</DialogContent>
<Tabs
defaultValue="outline"
defaultValue="all_users"
className="w-full flex-col justify-start gap-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" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">All Users</SelectItem>
<SelectItem value="past-performance">Active Users</SelectItem>
<SelectItem value="key-personnel">Administrators</SelectItem>
<SelectItem value="focus-documents">Inactive Users</SelectItem>
<SelectItem value="all_users">All Users</SelectItem>
<SelectItem value="active_users">Active Users</SelectItem>
<SelectItem value="admin_users">Administrators</SelectItem>
<SelectItem value="inactive_users">Inactive Users</SelectItem>
</SelectContent>
</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">
<TabsTrigger value="outline">All Users</TabsTrigger>
<TabsTrigger value="past-performance">
Active Users <Badge variant="secondary">{info?.usersInfo?.totalActiveUsers}</Badge>
<TabsTrigger value="all_users">All Users</TabsTrigger>
<TabsTrigger value="active_users">
Active Users <Badge variant="secondary">{info?.totalActiveUsers}</Badge>
</TabsTrigger>
<TabsTrigger value="key-personnel">
Administrators <Badge variant="secondary">{info?.usersInfo?.totalAdminUsers}</Badge>
<TabsTrigger value="admin_users">
Administrators <Badge variant="secondary">{info?.totalAdminUsers}</Badge>
</TabsTrigger>
<TabsTrigger value="focus-documents">Inactive Users</TabsTrigger>
<TabsTrigger value="inactive_users">Inactive Users</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DropdownMenu>
@ -543,14 +551,14 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</div>
</div>
<TabsContent
value="outline"
value="all_users"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
>
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
// onDragEnd={handleDragEnd}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
@ -574,9 +582,15 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
))}
</TableHeader>
<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
items={data?.users.map((user: any) => user.id) || []}
items={data?.map((user: any) => user.id) || []}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
@ -676,16 +690,16 @@ function Tabel({ data, refetch, info }: { data: any, refetch: any, info: any })
</div>
</TabsContent>
<TabsContent
value="past-performance"
value="active_users"
className="flex flex-col px-4 lg:px-6"
>
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</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>
</TabsContent>
<TabsContent
value="focus-documents"
value="inactive_users"
className="flex flex-col px-4 lg:px-6"
>
<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]);
// 如果正在加载或已认证,显示加载状态
if (isLoading || isAuthenticated) {
return (
<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 justify-center gap-2 md:justify-start">
<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">
<GalleryVerticalEnd className="size-4" />
</div>
Acme Inc.
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs text-center">
<p className="text-muted-foreground">...</p>
</div>
</div>
</div>
<div className="bg-muted relative hidden lg:block">
<img
src="/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</div>
);
}
// // 如果正在加载或已认证,显示加载状态
// if (isLoading || isAuthenticated) {
// return (
// <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 justify-center gap-2 md:justify-start">
// <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">
// <GalleryVerticalEnd className="size-4" />
// </div>
// Acme Inc.
// </a>
// </div>
// <div className="flex flex-1 items-center justify-center">
// <div className="w-full max-w-xs text-center">
// <p className="text-muted-foreground">正在跳转...</p>
// </div>
// </div>
// </div>
// <div className="bg-muted relative hidden lg:block">
// <img
// src="/placeholder.svg"
// alt="Image"
// className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
// />
// </div>
// </div>
// );
// }
return (
<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 {
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'
console.log(isLoggedIn)
if (!isLoggedIn) {
redirect('/login')
}
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider
style={

View File

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

View File

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