基本样式优化 & 数据表组件使用 tanstack-table 库实现

This commit is contained in:
2025-10-20 16:20:21 +08:00
parent 3c38945750
commit 8c05d0b332
9 changed files with 142 additions and 79 deletions

44
.gitignore vendored
View File

@@ -1,7 +1,39 @@
node_modules # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
src/generated/
.next # dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env .env
deploy.sh
.volumes # vercel
.vscode .vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# dev
.volumes/

26
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"ssh": "Disabled",
"previewLimit": 50,
"server": "localhost",
"port": 23306,
"driver": "MariaDB",
"name": "localhost",
"database": "app",
"username": "root"
}
],
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}

View File

@@ -10,6 +10,7 @@
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-table": "^8.21.3",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -363,6 +364,10 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="], "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", "tailwindcss": "4.1.12" } }, "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "https://registry.npmmirror.com/@tanstack/react-table/-/react-table-8.21.3.tgz", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "https://registry.npmmirror.com/@tanstack/table-core/-/table-core-8.21.3.tgz", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],

View File

@@ -15,6 +15,7 @@
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-table": "^8.21.3",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View File

@@ -39,7 +39,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
...item, ...item,
overage: Math.max(0, Number(item.assigned) - Number(item.count)), overage: Math.max(0, Number(item.assigned) - Number(item.count)),
})) }))
console.log(newData, 'newData')
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
@@ -116,7 +115,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
{ {
label: '城市', label: '城市',
props: 'city', props: 'city',
sortable: true,
}, },
{ {
label: '可用IP量', label: '可用IP量',

View File

@@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation'
import { logout } from '@/actions/auth' import { logout } from '@/actions/auth'
import { LogOutIcon } from 'lucide-react' import { LogOutIcon } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { cn } from '@/lib/utils'
export default function DashboardLayout({ export default function DashboardLayout({
children, children,
@@ -62,7 +63,7 @@ export default function DashboardLayout({
{/* 主要内容区域 */} {/* 主要内容区域 */}
<div className="flex-auto overflow-hidden flex"> <div className="flex-auto overflow-hidden flex">
{/* 侧边栏 */} {/* 侧边栏 */}
<nav className="flex-none basis-64 p-4 space-y-2 border-r flex flex-col"> <nav className="flex-none basis-64 p-3 space-y-2 border-r flex flex-col">
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}></NavbarItem> <NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}></NavbarItem>
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}></NavbarItem> <NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}></NavbarItem>
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}></NavbarItem> <NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}></NavbarItem>
@@ -89,11 +90,13 @@ function NavbarItem(props: {
return ( return (
<Link <Link
href={props.href} href={props.href}
className={`block px-3 py-2 rounded-md text-sm font-medium transition-colors text-center items-center justify-center ${ className={cn(
'transition-colors duration-150 ease-in-out',
'p-2 rounded-md text-sm flex items-center',
props.active props.active
? 'border-blue-500 text-blue-600' ? 'text-primary bg-primary/10'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-200' : 'hover:bg-muted',
}`} )}
> >
{props.children} {props.children}
</Link> </Link>

View File

@@ -51,8 +51,10 @@
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary: oklch(0.65 0.175 255);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);

View File

@@ -1,11 +1,12 @@
'use client'
import * as React from 'react' import * as React from 'react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
import { ReactNode, useState } from 'react' import { ReactNode, useState } from 'react'
import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon, Columns } from 'lucide-react' import { ArrowUpDownIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
import { ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
type Data = { type Data = Record<string, unknown>
[key: string]: unknown
}
type Column = { type Column = {
label: string label: string
@@ -14,73 +15,68 @@ type Column = {
sortable?: boolean sortable?: boolean
} }
export function DataTable(props: { data: Data[], columns: Column[] }) { export function DataTable<T extends Data>(props: {
const [sortKey, setSortKey] = useState<string>('') data: T[]
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') columns: Column[]
}) {
const sortedData = [...props.data].sort((a, b) => { const table = useReactTable({
let aValue = a[sortKey] getCoreRowModel: getCoreRowModel(),
let bValue = b[sortKey] getSortedRowModel: getSortedRowModel(),
data: props.data,
if (aValue === undefined || aValue === null) aValue = '' columns: props.columns.map(col => ({
if (bValue === undefined || bValue === null) bValue = '' meta: col,
header: col.label,
const aStr = String(aValue) accessorKey: col.props,
const bStr = String(bValue) cell: info => col.render?.(info.row.original) || String(info.getValue()),
enableSorting: col.sortable,
const aNum = Number(aValue) })) as ColumnDef<T>[],
const bNum = Number(bValue)
if (!isNaN(aNum) && !isNaN(bNum)) {
return sortDirection === 'asc' ? aNum - bNum : bNum - aNum
}
else {
return sortDirection === 'asc'
? aStr.localeCompare(bStr)
: bStr.localeCompare(aStr)
}
}) })
const handleSort = (key: string) => {
if (!key) return
if (sortKey === key) {
setSortDirection(direction => direction === 'asc' ? 'desc' : 'asc')
}
else {
setSortKey(key)
setSortDirection('desc')
}
}
const renderSortIcon = (key: string) => {
if (sortKey !== key) {
return <ArrowUpDownIcon className="h-4 w-4 text-gray-400" />
}
return sortDirection === 'asc'
? <ArrowUpIcon className="h-4 w-4 text-blue-600" />
: <ArrowDownIcon className="h-4 w-4 text-blue-600" />
}
return ( return (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow>
{props.columns.map((item, index) => ( {/* 表头行 */}
<TableHead key={index} onClick={() => item.sortable && handleSort(item.props || item.label)} className={item.sortable ? 'cursor-pointer hover:bg-gray-50' : ''}> {table.getHeaderGroups().map(group => (
<div className="flex items-center gap-2"> <TableRow key={group.id}>
<span> {item.label}</span>
{item.sortable && item.props && renderSortIcon(item.props)} {/* 表头 */}
</div> {group.headers.map(header => (
</TableHead> <TableHead
))} key={header.id}
</TableRow> className={cn(
header.column.columnDef.enableSorting && 'hover:bg-gray-200 transition-colors duration-150 ease-in-out cursor-pointer',
header.column.getIsSorted() && 'text-primary',
)}
onClick={header.column.getToggleSortingHandler()}>
<div className="flex flex-row items-center justify-between">
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.columnDef.enableSorting && (
header.column.getIsSorted() == 'asc' ? (
<ArrowUpIcon className="size-4" />
) : header.column.getIsSorted() == 'desc' ? (
<ArrowDownIcon className="size-4" />
) : (
<ArrowUpDownIcon className="size-4" />
)
)}
</div>
</TableHead>
))}
</TableRow>
))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{sortedData.map((row, index) => (
<TableRow key={index}> {/* 表格行 */}
{props.columns.map((colume, index) => ( {table.getRowModel().rows.map(row => (
<TableCell key={index}> <TableRow key={row.id}>
{ colume.props ? String(row[colume.props]) : colume.render ? colume.render(row) : undefined }
{/* 表格 */}
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>

View File

@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-border/50 transition-colors', 'hover:data-[state=selected]:bg-muted border-b border-border/50 transition-colors',
className, className,
)} )}
{...props} {...props}