From b499ab853f9bb9e1ecfee49938681f5451490635 Mon Sep 17 00:00:00 2001 From: tsuki Date: Wed, 30 Jul 2025 21:33:40 +0800 Subject: [PATCH] sync me --- app/admin/dashboard/area.tsx | 291 ++++++++++++++ app/admin/dashboard/page.tsx | 9 + app/admin/dashboard/sections.tsx | 102 +++++ app/admin/page.tsx | 2 +- app/admin/users/user-table.tsx | 653 +++++++++++++++++++------------ app/app-sidebar.tsx | 17 +- app/layout.tsx | 2 + app/nav-user.tsx | 36 +- app/page.tsx | 13 +- app/user-context.tsx | 12 +- components/ui/sonner.tsx | 25 ++ package-lock.json | 21 +- package.json | 1 + 13 files changed, 900 insertions(+), 284 deletions(-) create mode 100644 app/admin/dashboard/area.tsx create mode 100644 app/admin/dashboard/sections.tsx create mode 100644 components/ui/sonner.tsx diff --git a/app/admin/dashboard/area.tsx b/app/admin/dashboard/area.tsx new file mode 100644 index 0000000..1e3c632 --- /dev/null +++ b/app/admin/dashboard/area.tsx @@ -0,0 +1,291 @@ +"use client" + +import * as React from "react" +import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" + +import { useIsMobile } from "@/hooks/use-mobile" +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + ToggleGroup, + ToggleGroupItem, +} from "@/components/ui/toggle-group" + +export const description = "An interactive area chart" + +const chartData = [ + { date: "2024-04-01", desktop: 222, mobile: 150 }, + { date: "2024-04-02", desktop: 97, mobile: 180 }, + { date: "2024-04-03", desktop: 167, mobile: 120 }, + { date: "2024-04-04", desktop: 242, mobile: 260 }, + { date: "2024-04-05", desktop: 373, mobile: 290 }, + { date: "2024-04-06", desktop: 301, mobile: 340 }, + { date: "2024-04-07", desktop: 245, mobile: 180 }, + { date: "2024-04-08", desktop: 409, mobile: 320 }, + { date: "2024-04-09", desktop: 59, mobile: 110 }, + { date: "2024-04-10", desktop: 261, mobile: 190 }, + { date: "2024-04-11", desktop: 327, mobile: 350 }, + { date: "2024-04-12", desktop: 292, mobile: 210 }, + { date: "2024-04-13", desktop: 342, mobile: 380 }, + { date: "2024-04-14", desktop: 137, mobile: 220 }, + { date: "2024-04-15", desktop: 120, mobile: 170 }, + { date: "2024-04-16", desktop: 138, mobile: 190 }, + { date: "2024-04-17", desktop: 446, mobile: 360 }, + { date: "2024-04-18", desktop: 364, mobile: 410 }, + { date: "2024-04-19", desktop: 243, mobile: 180 }, + { date: "2024-04-20", desktop: 89, mobile: 150 }, + { date: "2024-04-21", desktop: 137, mobile: 200 }, + { date: "2024-04-22", desktop: 224, mobile: 170 }, + { date: "2024-04-23", desktop: 138, mobile: 230 }, + { date: "2024-04-24", desktop: 387, mobile: 290 }, + { date: "2024-04-25", desktop: 215, mobile: 250 }, + { date: "2024-04-26", desktop: 75, mobile: 130 }, + { date: "2024-04-27", desktop: 383, mobile: 420 }, + { date: "2024-04-28", desktop: 122, mobile: 180 }, + { date: "2024-04-29", desktop: 315, mobile: 240 }, + { date: "2024-04-30", desktop: 454, mobile: 380 }, + { date: "2024-05-01", desktop: 165, mobile: 220 }, + { date: "2024-05-02", desktop: 293, mobile: 310 }, + { date: "2024-05-03", desktop: 247, mobile: 190 }, + { date: "2024-05-04", desktop: 385, mobile: 420 }, + { date: "2024-05-05", desktop: 481, mobile: 390 }, + { date: "2024-05-06", desktop: 498, mobile: 520 }, + { date: "2024-05-07", desktop: 388, mobile: 300 }, + { date: "2024-05-08", desktop: 149, mobile: 210 }, + { date: "2024-05-09", desktop: 227, mobile: 180 }, + { date: "2024-05-10", desktop: 293, mobile: 330 }, + { date: "2024-05-11", desktop: 335, mobile: 270 }, + { date: "2024-05-12", desktop: 197, mobile: 240 }, + { date: "2024-05-13", desktop: 197, mobile: 160 }, + { date: "2024-05-14", desktop: 448, mobile: 490 }, + { date: "2024-05-15", desktop: 473, mobile: 380 }, + { date: "2024-05-16", desktop: 338, mobile: 400 }, + { date: "2024-05-17", desktop: 499, mobile: 420 }, + { date: "2024-05-18", desktop: 315, mobile: 350 }, + { date: "2024-05-19", desktop: 235, mobile: 180 }, + { date: "2024-05-20", desktop: 177, mobile: 230 }, + { date: "2024-05-21", desktop: 82, mobile: 140 }, + { date: "2024-05-22", desktop: 81, mobile: 120 }, + { date: "2024-05-23", desktop: 252, mobile: 290 }, + { date: "2024-05-24", desktop: 294, mobile: 220 }, + { date: "2024-05-25", desktop: 201, mobile: 250 }, + { date: "2024-05-26", desktop: 213, mobile: 170 }, + { date: "2024-05-27", desktop: 420, mobile: 460 }, + { date: "2024-05-28", desktop: 233, mobile: 190 }, + { date: "2024-05-29", desktop: 78, mobile: 130 }, + { date: "2024-05-30", desktop: 340, mobile: 280 }, + { date: "2024-05-31", desktop: 178, mobile: 230 }, + { date: "2024-06-01", desktop: 178, mobile: 200 }, + { date: "2024-06-02", desktop: 470, mobile: 410 }, + { date: "2024-06-03", desktop: 103, mobile: 160 }, + { date: "2024-06-04", desktop: 439, mobile: 380 }, + { date: "2024-06-05", desktop: 88, mobile: 140 }, + { date: "2024-06-06", desktop: 294, mobile: 250 }, + { date: "2024-06-07", desktop: 323, mobile: 370 }, + { date: "2024-06-08", desktop: 385, mobile: 320 }, + { date: "2024-06-09", desktop: 438, mobile: 480 }, + { date: "2024-06-10", desktop: 155, mobile: 200 }, + { date: "2024-06-11", desktop: 92, mobile: 150 }, + { date: "2024-06-12", desktop: 492, mobile: 420 }, + { date: "2024-06-13", desktop: 81, mobile: 130 }, + { date: "2024-06-14", desktop: 426, mobile: 380 }, + { date: "2024-06-15", desktop: 307, mobile: 350 }, + { date: "2024-06-16", desktop: 371, mobile: 310 }, + { date: "2024-06-17", desktop: 475, mobile: 520 }, + { date: "2024-06-18", desktop: 107, mobile: 170 }, + { date: "2024-06-19", desktop: 341, mobile: 290 }, + { date: "2024-06-20", desktop: 408, mobile: 450 }, + { date: "2024-06-21", desktop: 169, mobile: 210 }, + { date: "2024-06-22", desktop: 317, mobile: 270 }, + { date: "2024-06-23", desktop: 480, mobile: 530 }, + { date: "2024-06-24", desktop: 132, mobile: 180 }, + { date: "2024-06-25", desktop: 141, mobile: 190 }, + { date: "2024-06-26", desktop: 434, mobile: 380 }, + { date: "2024-06-27", desktop: 448, mobile: 490 }, + { date: "2024-06-28", desktop: 149, mobile: 200 }, + { date: "2024-06-29", desktop: 103, mobile: 160 }, + { date: "2024-06-30", desktop: 446, mobile: 400 }, +] + +const chartConfig = { + visitors: { + label: "Visitors", + }, + desktop: { + label: "Desktop", + color: "var(--primary)", + }, + mobile: { + label: "Mobile", + color: "var(--primary)", + }, +} satisfies ChartConfig + +export function ChartAreaInteractive() { + const isMobile = useIsMobile() + const [timeRange, setTimeRange] = React.useState("90d") + + React.useEffect(() => { + if (isMobile) { + setTimeRange("7d") + } + }, [isMobile]) + + const filteredData = chartData.filter((item) => { + const date = new Date(item.date) + const referenceDate = new Date("2024-06-30") + let daysToSubtract = 90 + if (timeRange === "30d") { + daysToSubtract = 30 + } else if (timeRange === "7d") { + daysToSubtract = 7 + } + const startDate = new Date(referenceDate) + startDate.setDate(startDate.getDate() - daysToSubtract) + return date >= startDate + }) + + return ( + + + Total Visitors + + + Total for the last 3 months + + Last 3 months + + + + Last 3 months + Last 30 days + Last 7 days + + + + + + + + + + + + + + + + + + + { + const date = new Date(value) + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + }} + /> + { + return new Date(value).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + }} + indicator="dot" + /> + } + /> + + + + + + + ) +} diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx index 9b16337..430d24a 100644 --- a/app/admin/dashboard/page.tsx +++ b/app/admin/dashboard/page.tsx @@ -1,14 +1,23 @@ "use client" +import { SectionCards } from "./sections" +import { ChartAreaInteractive } from "./area" + export default function Page() { return ( +
+ +
+ +
+ ) } \ No newline at end of file diff --git a/app/admin/dashboard/sections.tsx b/app/admin/dashboard/sections.tsx new file mode 100644 index 0000000..25da063 --- /dev/null +++ b/app/admin/dashboard/sections.tsx @@ -0,0 +1,102 @@ +import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react" + +import { Badge } from "@/components/ui/badge" +import { + Card, + CardAction, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +export function SectionCards() { + return ( +
+ + + Total Revenue + + $1,250.00 + + + + + +12.5% + + + + +
+ Trending up this month +
+
+ Visitors for the last 6 months +
+
+
+ + + New Customers + + 1,234 + + + + + -20% + + + + +
+ Down 20% this period +
+
+ Acquisition needs attention +
+
+
+ + + Active Accounts + + 45,678 + + + + + +12.5% + + + + +
+ Strong user retention +
+
Engagement exceed targets
+
+
+ + + Growth Rate + + 4.5% + + + + + +4.5% + + + + +
+ Steady performance increase +
+
Meets growth projections
+
+
+
+ ) +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 4cbe2ed..7fc5425 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -20,6 +20,6 @@ export default function Dashboard() { } }, [isAuthenticated, isLoading, router, user]); - return (<>) + redirect('/admin/dashboard') } diff --git a/app/admin/users/user-table.tsx b/app/admin/users/user-table.tsx index f19677d..00ce802 100644 --- a/app/admin/users/user-table.tsx +++ b/app/admin/users/user-table.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { useQuery, gql, useMutation } from '@apollo/client'; +import { useQuery, gql } from '@apollo/client'; import { Dialog, DialogClose, @@ -117,9 +117,10 @@ import { TabsTrigger, } from "@/components/ui/tabs" import CreateUserForm from "./create-user-form"; +import { useUser } from "@/app/user-context"; export const schema = z.object({ - id: z.number(), + id: z.string(), username: z.string(), email: z.string(), role: z.string(), @@ -128,7 +129,7 @@ export const schema = z.object({ }) // Create a separate component for the drag handle -function DragHandle({ id }: { id: number }) { +function DragHandle({ id }: { id: string }) { const { attributes, listeners } = useSortable({ id, }) @@ -211,7 +212,21 @@ const columns: ColumnDef>[] = [ }, { accessorKey: "createdAt", - header: "Created At", + header: ({ column }) => { + return ( + + ) + }, cell: ({ row }) => { const date = new Date(row.original.createdAt); const now = new Date(); @@ -252,10 +267,31 @@ const columns: ColumnDef>[] = [ ); }, + enableSorting: true, + sortingFn: (rowA, rowB, columnId) => { + const dateA = new Date(rowA.original.createdAt); + const dateB = new Date(rowB.original.createdAt); + return dateA.getTime() - dateB.getTime(); + }, }, { - accessorKey: "lastLogin", - header: "Last Login", + id: "lastLogin", + accessorFn: (row) => row.updatedAt, + header: ({ column }) => { + return ( + + ) + }, cell: ({ row }) => { const date = new Date(row.original.updatedAt); const now = new Date(); @@ -292,6 +328,12 @@ const columns: ColumnDef>[] = [ ); }, + enableSorting: true, + sortingFn: (rowA, rowB, columnId) => { + const dateA = new Date(rowA.original.updatedAt); + const dateB = new Date(rowB.original.updatedAt); + return dateA.getTime() - dateB.getTime(); + }, }, { id: "actions", @@ -345,8 +387,8 @@ function DraggableRow({ row }: { row: Row> }) { } const GET_USERS = gql` - query GetUsers($offset: Int, $limit: Int) { - users(offset: $offset, limit: $limit) { + query GetUsers($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) { + users(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) { id username email @@ -358,8 +400,8 @@ const GET_USERS = gql` ` const USERS_INFO = gql` - query UsersInfo($offset: Int, $limit: Int) { - usersInfo(offset: $offset, limit: $limit) { + query UsersInfo($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) { + usersInfo(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) { totalUsers totalActiveUsers totalAdminUsers @@ -378,7 +420,7 @@ const USERS_INFO = gql` export function UserTable() { - const { data, loading, error: usersInfoError, refetch } = useQuery(USERS_INFO) + const { data, loading, error, refetch } = useQuery(USERS_INFO) const [localData, setLocalData] = React.useState([]) @@ -407,60 +449,20 @@ export function UserTable() { return ( <> - + {error &&
{error.message}
} + ) } -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] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, - }) - const sortableId = React.useId() - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - - const table = useReactTable({ - data: data || [], - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - pagination, - }, - getRowId: (row) => row.id.toString(), - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - manualPagination: true, - }) - - +function UserTabs({ initialData, initialLoading, info, handleDragEnd, refetch }: { initialData: any[], initialLoading: boolean, info: any, handleDragEnd: (event: DragEndEvent) => void, refetch: any }) { return ( @@ -508,206 +510,351 @@ function Tabel({ data, refetch, info, isLoading, handleDragEnd }: { data: any, r Inactive Users
- - - - - - {table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && - column.getCanHide() - ) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - -
- -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {isLoading ? (<> - - - - - - ) : table.getRowModel().rows?.length ? ( - user.id) || []} - strategy={verticalListSortingStrategy} - > - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - - No results. - - - )} - -
-
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
-
- - -
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
- - - - -
-
-
+ + + - -
+ + + - -
+ + + - -
+ + +
) +} +// 可重用的数据表格组件 +function UserDataTable({ + data: propData, + isLoading: propIsLoading = false, + handleDragEnd, + filter, + useInitialData = false, + enableServerSideSort = false, + refetchFn +}: { + data?: any[] + isLoading?: boolean + handleDragEnd: (event: DragEndEvent) => void + filter?: string + useInitialData?: boolean + enableServerSideSort?: boolean + refetchFn?: any +}) { + const [localData, setLocalData] = React.useState([]) + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + + // 为非初始数据的tab查询数据 + const { data: queryData, loading: queryLoading, refetch } = useQuery(GET_USERS, { + variables: { + offset: pagination.pageIndex * pagination.pageSize, + limit: pagination.pageSize, + sortBy: sorting[0]?.id || "createdAt", + sortOrder: sorting[0]?.desc ? "DESC" : "ASC", + filter: filter + }, + skip: useInitialData, + fetchPolicy: 'cache-and-network' + }) + + const data = useInitialData ? propData : queryData?.users + const isLoading = useInitialData ? propIsLoading : queryLoading + + // 同步数据到本地状态 + React.useEffect(() => { + if (data && Array.isArray(data)) { + setLocalData(data) + } + }, [data]) + + // 当筛选条件变化时重置分页 + React.useEffect(() => { + setPagination({ pageIndex: 0, pageSize: 10 }) + }, [filter]) + + // 当排序或分页变化时,重新查询数据 + React.useEffect(() => { + const refetchFunc = refetchFn || refetch + if ((!useInitialData || enableServerSideSort) && refetchFunc) { + refetchFunc({ + offset: pagination.pageIndex * pagination.pageSize, + limit: pagination.pageSize, + sortBy: sorting[0]?.id || "createdAt", + sortOrder: sorting[0]?.desc ? "DESC" : "ASC", + filter: filter + }) + } + }, [sorting, pagination, filter, useInitialData, enableServerSideSort, refetch, refetchFn]) + + function handleLocalDragEnd(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) + }) + } + // 也调用父组件的handleDragEnd + handleDragEnd(event) + } + + const sortableId = React.useId() + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ) + + const table = useReactTable({ + data: localData || [], + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + getRowId: (row) => row.id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + manualPagination: true, + }) + + return ( + <> + {/*
+ + + + + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && + column.getCanHide() + ) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + +
*/} + +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {isLoading ? ( + + + Loading... + {/* */} + + + ) : table.getRowModel().rows?.length ? ( + user.id) || []} + strategy={verticalListSortingStrategy} + > + {table.getRowModel().rows.map((row) => ( + + ))} + + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+
+ + + + +
+
+
+ + ) } const chartData = [ @@ -732,14 +879,26 @@ const chartConfig = { function TableCellViewer({ item }: { item: z.infer }) { const isMobile = useIsMobile() + const { user } = useUser() return ( - +
+ + { + item.id === user?.id ? ( + + Me + + ) : <> + } + +
+ {item.username} diff --git a/app/app-sidebar.tsx b/app/app-sidebar.tsx index 38b6654..6139a3f 100644 --- a/app/app-sidebar.tsx +++ b/app/app-sidebar.tsx @@ -54,12 +54,8 @@ export function AppSidebar({ ...props }: React.ComponentProps) { return ( - { - user ? : null - } - {/* */}