基本样式优化 & 数据表组件使用 tanstack-table 库实现
This commit is contained in:
44
.gitignore
vendored
44
.gitignore
vendored
@@ -1,7 +1,39 @@
|
||||
node_modules
|
||||
src/generated/
|
||||
.next
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# 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
|
||||
deploy.sh
|
||||
.volumes
|
||||
.vscode
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# dev
|
||||
.volumes/
|
||||
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
5
bun.lock
5
bun.lock
@@ -10,6 +10,7 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -39,7 +39,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
...item,
|
||||
overage: Math.max(0, Number(item.assigned) - Number(item.count)),
|
||||
}))
|
||||
console.log(newData, 'newData')
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
@@ -116,7 +115,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
{
|
||||
label: '城市',
|
||||
props: 'city',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: '可用IP量',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation'
|
||||
import { logout } from '@/actions/auth'
|
||||
import { LogOutIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
@@ -62,7 +63,7 @@ export default function DashboardLayout({
|
||||
{/* 主要内容区域 */}
|
||||
<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="/gatewayConfig" active={isActive('/gatewayConfig')}>网关配置</NavbarItem>
|
||||
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}>网关查询</NavbarItem>
|
||||
@@ -89,11 +90,13 @@ function NavbarItem(props: {
|
||||
return (
|
||||
<Link
|
||||
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
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
? 'text-primary bg-primary/10'
|
||||
: 'hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
|
||||
@@ -51,8 +51,10 @@
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 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);
|
||||
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'
|
||||
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 = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type Data = Record<string, unknown>
|
||||
|
||||
type Column = {
|
||||
label: string
|
||||
@@ -14,73 +15,68 @@ type Column = {
|
||||
sortable?: boolean
|
||||
}
|
||||
|
||||
export function DataTable(props: { data: Data[], columns: Column[] }) {
|
||||
const [sortKey, setSortKey] = useState<string>('')
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
const sortedData = [...props.data].sort((a, b) => {
|
||||
let aValue = a[sortKey]
|
||||
let bValue = b[sortKey]
|
||||
|
||||
if (aValue === undefined || aValue === null) aValue = ''
|
||||
if (bValue === undefined || bValue === null) bValue = ''
|
||||
|
||||
const aStr = String(aValue)
|
||||
const bStr = String(bValue)
|
||||
|
||||
const aNum = Number(aValue)
|
||||
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)
|
||||
}
|
||||
export function DataTable<T extends Data>(props: {
|
||||
data: T[]
|
||||
columns: Column[]
|
||||
}) {
|
||||
const table = useReactTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
data: props.data,
|
||||
columns: props.columns.map(col => ({
|
||||
meta: col,
|
||||
header: col.label,
|
||||
accessorKey: col.props,
|
||||
cell: info => col.render?.(info.row.original) || String(info.getValue()),
|
||||
enableSorting: col.sortable,
|
||||
})) as ColumnDef<T>[],
|
||||
})
|
||||
|
||||
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 (
|
||||
<Table>
|
||||
<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' : ''}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span> {item.label}</span>
|
||||
{item.sortable && item.props && renderSortIcon(item.props)}
|
||||
</div>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{/* 表头行 */}
|
||||
{table.getHeaderGroups().map(group => (
|
||||
<TableRow key={group.id}>
|
||||
|
||||
{/* 表头 */}
|
||||
{group.headers.map(header => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
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>
|
||||
|
||||
<TableBody>
|
||||
{sortedData.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
{props.columns.map((colume, index) => (
|
||||
<TableCell key={index}>
|
||||
{ colume.props ? String(row[colume.props]) : colume.render ? colume.render(row) : undefined }
|
||||
|
||||
{/* 表格行 */}
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<TableRow key={row.id}>
|
||||
|
||||
{/* 表格 */}
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user