sync me
This commit is contained in:
parent
1b35c937c1
commit
58edc3040e
@ -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={
|
||||||
|
|||||||
@ -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 (<></>)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
19
app/login/layout.tsx
Normal 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}</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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={
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user