基本样式优化 & 数据表组件使用 tanstack-table 库实现
This commit is contained in:
44
.gitignore
vendored
44
.gitignore
vendored
@@ -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
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-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=="],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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量',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)}
|
{/* 表头 */}
|
||||||
|
{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>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user