mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
更新ui
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link"
|
||||
import { useState, useMemo, type FormEvent } from "react"
|
||||
import { GitBranch, Search, RefreshCw, Settings, Trash2, FolderOpen } from "lucide-react"
|
||||
import { GitBranch, Search, RefreshCw, Settings, Trash2, FolderOpen, Plus } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -173,7 +173,8 @@ export default function NucleiReposPage() {
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||
新增模板仓库
|
||||
<Plus className="h-4 w-4" />
|
||||
新增仓库
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -229,18 +229,22 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={current === normalize(subItem.url)}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
{item.items?.map((subItem) => {
|
||||
const subUrl = normalize(subItem.url)
|
||||
const isSubActive = current === subUrl || current.startsWith(subUrl + "/")
|
||||
return (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={isSubActive}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { TruncatedUrlCell } from "@/components/ui/truncated-cell"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import type { Directory } from "@/types/directory.types"
|
||||
|
||||
/**
|
||||
@@ -88,10 +88,9 @@ export function createDirectoryColumns({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="URL" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const url = row.getValue("url") as string
|
||||
return <TruncatedUrlCell value={url} />
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("url")} />
|
||||
),
|
||||
},
|
||||
// Status 列
|
||||
{
|
||||
@@ -99,6 +98,7 @@ export function createDirectoryColumns({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 120,
|
||||
enableResizing: false,
|
||||
meta: { title: "Status" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
@@ -156,6 +156,7 @@ export function createDirectoryColumns({
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
meta: { title: "Content Type" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Content Type" />
|
||||
@@ -190,6 +191,7 @@ export function createDirectoryColumns({
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
meta: { title: "Created At" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Created At" />
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { IconPlus } from "@tabler/icons-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UnifiedDataTable } from "@/components/ui/data-table"
|
||||
import type { FilterField } from "@/components/common/smart-filter-input"
|
||||
import type { Directory } from "@/types/directory.types"
|
||||
@@ -114,19 +112,13 @@ export function DirectoriesDataTable({
|
||||
onBulkDelete={onBulkDelete}
|
||||
bulkDeleteLabel="Delete"
|
||||
showAddButton={false}
|
||||
// 批量添加按钮
|
||||
onBulkAdd={onBulkAdd}
|
||||
bulkAddLabel="批量添加"
|
||||
// 下载
|
||||
downloadOptions={downloadOptions.length > 0 ? downloadOptions : undefined}
|
||||
// 空状态
|
||||
emptyMessage="暂无数据"
|
||||
// 自定义工具栏按钮
|
||||
toolbarRight={
|
||||
onBulkAdd ? (
|
||||
<Button onClick={onBulkAdd} size="sm" variant="outline">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
批量添加
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import React from "react"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import type { Endpoint } from "@/types/endpoint.types"
|
||||
import { TruncatedCell, TruncatedUrlCell } from "@/components/ui/truncated-cell"
|
||||
import { ExpandableCell, ExpandableTagList } from "@/components/ui/data-table/expandable-cell"
|
||||
|
||||
interface CreateColumnsProps {
|
||||
formatDate: (dateString: string) => string
|
||||
@@ -45,37 +44,6 @@ function HttpStatusBadge({ statusCode }: { statusCode: number | null | undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Body Preview 单元格组件 - 最多显示3行,超出折叠,点击展开查看完整内容
|
||||
*/
|
||||
function BodyPreviewCell({ value }: { value: string | null | undefined }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground text-sm">-</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className={`text-sm text-muted-foreground break-all leading-relaxed whitespace-normal cursor-pointer hover:text-foreground transition-colors ${!expanded ? 'line-clamp-3' : ''}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title={expanded ? "点击收起" : "点击展开"}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{value.length > 100 && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
>
|
||||
{expanded ? "收起" : "展开"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function createEndpointColumns({
|
||||
formatDate,
|
||||
}: CreateColumnsProps): ColumnDef<Endpoint>[] {
|
||||
@@ -115,10 +83,9 @@ export function createEndpointColumns({
|
||||
size: 400,
|
||||
minSize: 200,
|
||||
maxSize: 700,
|
||||
cell: ({ row }) => {
|
||||
const url = row.getValue("url") as string
|
||||
return <TruncatedUrlCell value={url} />
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("url")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
@@ -130,7 +97,7 @@ export function createEndpointColumns({
|
||||
minSize: 100,
|
||||
maxSize: 300,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("title")} maxLength="title" />
|
||||
<ExpandableCell value={row.getValue("title")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -174,7 +141,7 @@ export function createEndpointColumns({
|
||||
minSize: 100,
|
||||
maxSize: 300,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("location")} maxLength="location" />
|
||||
<ExpandableCell value={row.getValue("location")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -187,7 +154,7 @@ export function createEndpointColumns({
|
||||
minSize: 80,
|
||||
maxSize: 200,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("webserver")} maxLength="webServer" />
|
||||
<ExpandableCell value={row.getValue("webserver")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -200,7 +167,7 @@ export function createEndpointColumns({
|
||||
minSize: 80,
|
||||
maxSize: 200,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("contentType")} maxLength="contentType" />
|
||||
<ExpandableCell value={row.getValue("contentType")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -213,16 +180,12 @@ export function createEndpointColumns({
|
||||
minSize: 150,
|
||||
cell: ({ row }) => {
|
||||
const tech = (row.getValue("tech") as string[] | null | undefined) || []
|
||||
if (!tech.length) return <span className="text-sm text-muted-foreground">-</span>
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{tech.map((t, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{t}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<ExpandableTagList
|
||||
items={tech}
|
||||
maxVisible={3}
|
||||
variant="outline"
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -235,7 +198,7 @@ export function createEndpointColumns({
|
||||
size: 350,
|
||||
minSize: 250,
|
||||
cell: ({ row }) => (
|
||||
<BodyPreviewCell value={row.getValue("bodyPreview")} />
|
||||
<ExpandableCell value={row.getValue("bodyPreview")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -264,21 +227,12 @@ export function createEndpointColumns({
|
||||
maxSize: 250,
|
||||
cell: ({ row }) => {
|
||||
const tags = (row.getValue("tags") as string[] | null | undefined) || []
|
||||
if (!tags.length) {
|
||||
return <span className="text-muted-foreground text-sm">-</span>
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag, idx) => (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant={/xss|sqli|idor|rce|ssrf|lfi|rfi|xxe|csrf|open.?redirect|interesting/i.test(tag) ? "destructive" : "secondary"}
|
||||
className="text-xs"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<ExpandableTagList
|
||||
items={tags}
|
||||
maxVisible={3}
|
||||
variant="secondary"
|
||||
/>
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { IconPlus } from "@tabler/icons-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UnifiedDataTable } from "@/components/ui/data-table"
|
||||
import type { FilterField } from "@/components/common/smart-filter-input"
|
||||
import type { DownloadOption, PaginationState } from "@/types/data-table.types"
|
||||
@@ -109,14 +107,6 @@ export function EndpointsDataTable<TData extends { id: number | string }, TValue
|
||||
})
|
||||
}
|
||||
|
||||
// 自定义工具栏右侧按钮
|
||||
const toolbarRightContent = onBulkAdd ? (
|
||||
<Button onClick={onBulkAdd} size="sm" variant="outline">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
批量添加
|
||||
</Button>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<UnifiedDataTable
|
||||
data={data}
|
||||
@@ -140,12 +130,13 @@ export function EndpointsDataTable<TData extends { id: number | string }, TValue
|
||||
showBulkDelete={false}
|
||||
onAddNew={onAddNew}
|
||||
addButtonLabel={addButtonText}
|
||||
// 批量添加按钮
|
||||
onBulkAdd={onBulkAdd}
|
||||
bulkAddLabel="批量添加"
|
||||
// 下载
|
||||
downloadOptions={downloadOptions.length > 0 ? downloadOptions : undefined}
|
||||
// 空状态
|
||||
emptyMessage="No results"
|
||||
// 自定义工具栏按钮
|
||||
toolbarRight={toolbarRightContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -10,6 +11,38 @@ interface ColumnOptions {
|
||||
formatDate: (date: string) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键词列表单元格 - 默认显示3个,超出可展开
|
||||
*/
|
||||
function KeywordListCell({ keywords }: { keywords: string[] }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!keywords || keywords.length === 0) return <span className="text-muted-foreground">-</span>
|
||||
|
||||
const displayKeywords = expanded ? keywords : keywords.slice(0, 3)
|
||||
const hasMore = keywords.length > 3
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-mono text-xs space-y-0.5">
|
||||
{displayKeywords.map((kw, idx) => (
|
||||
<div key={idx} className={expanded ? "break-all" : "truncate"}>
|
||||
{kw}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
>
|
||||
{expanded ? "收起" : "展开"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 EHole 指纹表格列定义
|
||||
*/
|
||||
@@ -70,8 +103,8 @@ export function createEholeFingerprintColumns({
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 100,
|
||||
enableResizing: false,
|
||||
size: 120,
|
||||
},
|
||||
// 匹配位置
|
||||
{
|
||||
@@ -88,7 +121,7 @@ export function createEholeFingerprintColumns({
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
enableResizing: true,
|
||||
enableResizing: false,
|
||||
size: 100,
|
||||
},
|
||||
// 关键词
|
||||
@@ -98,17 +131,7 @@ export function createEholeFingerprintColumns({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Keyword" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const keywords = row.getValue("keyword") as string[]
|
||||
if (!keywords || keywords.length === 0) return "-"
|
||||
return (
|
||||
<div className="font-mono text-xs text-muted-foreground space-y-0.5">
|
||||
{keywords.map((kw, idx) => (
|
||||
<div key={idx}>{kw}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => <KeywordListCell keywords={row.getValue("keyword") || []} />,
|
||||
enableResizing: true,
|
||||
size: 300,
|
||||
},
|
||||
@@ -124,7 +147,7 @@ export function createEholeFingerprintColumns({
|
||||
if (!type || type === "-") return "-"
|
||||
return <Badge variant="outline">{type}</Badge>
|
||||
},
|
||||
enableResizing: true,
|
||||
enableResizing: false,
|
||||
size: 100,
|
||||
},
|
||||
// 重点资产
|
||||
@@ -135,13 +158,11 @@ export function createEholeFingerprintColumns({
|
||||
<DataTableColumnHeader column={column} title="Important" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const isImportant = row.getValue("isImportant") as boolean
|
||||
return isImportant ? (
|
||||
<Badge variant="destructive">重点</Badge>
|
||||
) : null
|
||||
const isImportant = row.getValue("isImportant")
|
||||
return <span>{String(isImportant)}</span>
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 80,
|
||||
enableResizing: false,
|
||||
size: 100,
|
||||
},
|
||||
// 创建时间
|
||||
{
|
||||
@@ -158,7 +179,7 @@ export function createEholeFingerprintColumns({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableResizing: true,
|
||||
enableResizing: false,
|
||||
size: 160,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -12,26 +12,22 @@ interface ColumnOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则详情单元格组件 - 默认显示3条,超出可展开
|
||||
* 规则详情单元格组件 - 显示原始 JSON 数据
|
||||
*/
|
||||
function RuleDetailsCell({ rules }: { rules: any[] }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!rules || rules.length === 0) return <span className="text-muted-foreground">-</span>
|
||||
|
||||
const displayRules = expanded ? rules : rules.slice(0, 3)
|
||||
const hasMore = rules.length > 3
|
||||
const displayRules = expanded ? rules : rules.slice(0, 2)
|
||||
const hasMore = rules.length > 2
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="font-mono text-xs text-muted-foreground space-y-0.5 max-w-md cursor-pointer hover:text-foreground transition-colors"
|
||||
onClick={() => hasMore && setExpanded(!expanded)}
|
||||
title={hasMore ? (expanded ? "点击收起" : "点击展开") : undefined}
|
||||
>
|
||||
<div className="font-mono text-xs space-y-0.5">
|
||||
{displayRules.map((r, idx) => (
|
||||
<div key={idx} className={expanded ? "break-all" : "truncate"}>
|
||||
<span className="text-primary">{r.label}</span>: {r.feature}
|
||||
{JSON.stringify(r)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -101,14 +97,10 @@ export function createGobyFingerprintColumns({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const logic = row.getValue("logic") as string
|
||||
return (
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{logic}
|
||||
</Badge>
|
||||
)
|
||||
return <span className="font-mono text-xs">{logic}</span>
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 120,
|
||||
enableResizing: false,
|
||||
size: 100,
|
||||
},
|
||||
// 规则数量
|
||||
{
|
||||
@@ -119,14 +111,10 @@ export function createGobyFingerprintColumns({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const rules = row.getValue("rule") as any[]
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
{rules?.length || 0} 条规则
|
||||
</Badge>
|
||||
)
|
||||
return <span>{rules?.length || 0}</span>
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 100,
|
||||
enableResizing: false,
|
||||
size: 80,
|
||||
},
|
||||
// 规则详情
|
||||
{
|
||||
@@ -154,7 +142,7 @@ export function createGobyFingerprintColumns({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableResizing: true,
|
||||
enableResizing: false,
|
||||
size: 160,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import React from "react"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import type { WappalyzerFingerprint } from "@/types/fingerprint.types"
|
||||
|
||||
interface ColumnOptions {
|
||||
@@ -12,25 +12,24 @@ interface ColumnOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可展开文本单元格组件
|
||||
* JSON 对象展示单元格 - 显示原始 JSON
|
||||
*/
|
||||
function ExpandableTextCell({ value, maxLength = 80 }: { value: string | null | undefined; maxLength?: number }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
function JsonCell({ data }: { data: any }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!value) return <span className="text-muted-foreground">-</span>
|
||||
if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const needsExpand = value.length > maxLength
|
||||
const jsonStr = JSON.stringify(data)
|
||||
const isLong = jsonStr.length > 50
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 overflow-hidden w-full">
|
||||
<div
|
||||
className={`text-sm text-muted-foreground break-words cursor-pointer hover:text-foreground transition-colors ${!expanded ? 'line-clamp-2' : ''}`}
|
||||
onClick={() => needsExpand && setExpanded(!expanded)}
|
||||
title={needsExpand ? (expanded ? "点击收起" : "点击展开") : undefined}
|
||||
>
|
||||
{value}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className={`font-mono text-xs ${expanded ? "break-all whitespace-pre-wrap" : "truncate"}`}>
|
||||
{expanded ? JSON.stringify(data, null, 2) : jsonStr}
|
||||
</div>
|
||||
{needsExpand && (
|
||||
{isLong && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
@@ -43,23 +42,28 @@ function ExpandableTextCell({ value, maxLength = 80 }: { value: string | null |
|
||||
}
|
||||
|
||||
/**
|
||||
* 可展开链接单元格组件
|
||||
* 数组展示单元格 - 显示原始数组
|
||||
*/
|
||||
function ExpandableLinkCell({ value, maxLength = 50 }: { value: string | null | undefined; maxLength?: number }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
function ArrayCell({ data }: { data: any[] }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!value) return <span className="text-muted-foreground">-</span>
|
||||
if (!data || data.length === 0) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const needsExpand = value.length > maxLength
|
||||
const displayItems = expanded ? data : data.slice(0, 2)
|
||||
const hasMore = data.length > 2
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 overflow-hidden w-full">
|
||||
<div
|
||||
className={`text-sm text-muted-foreground break-words ${!expanded ? 'line-clamp-1' : ''}`}
|
||||
>
|
||||
{value}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-mono text-xs space-y-0.5">
|
||||
{displayItems.map((item, idx) => (
|
||||
<div key={idx} className={expanded ? "break-all" : "truncate"}>
|
||||
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{needsExpand && (
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
@@ -116,34 +120,98 @@ export function createWappalyzerFingerprintColumns({
|
||||
enableResizing: true,
|
||||
size: 180,
|
||||
},
|
||||
// 分类
|
||||
// 分类 - 直接显示数组
|
||||
{
|
||||
accessorKey: "cats",
|
||||
meta: { title: "Categories" },
|
||||
meta: { title: "Cats" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Categories" />
|
||||
<DataTableColumnHeader column={column} title="Cats" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const cats = row.getValue("cats") as number[]
|
||||
if (!cats || cats.length === 0) return "-"
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{cats.slice(0, 3).map((cat) => (
|
||||
<Badge key={cat} variant="secondary" className="text-xs">
|
||||
{cat}
|
||||
</Badge>
|
||||
))}
|
||||
{cats.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{cats.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return <span className="font-mono text-xs">{JSON.stringify(cats)}</span>
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 120,
|
||||
},
|
||||
// Cookies
|
||||
{
|
||||
accessorKey: "cookies",
|
||||
meta: { title: "Cookies" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cookies" />
|
||||
),
|
||||
cell: ({ row }) => <JsonCell data={row.getValue("cookies")} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// Headers
|
||||
{
|
||||
accessorKey: "headers",
|
||||
meta: { title: "Headers" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Headers" />
|
||||
),
|
||||
cell: ({ row }) => <JsonCell data={row.getValue("headers")} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// Script Src
|
||||
{
|
||||
accessorKey: "scriptSrc",
|
||||
meta: { title: "ScriptSrc" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="ScriptSrc" />
|
||||
),
|
||||
cell: ({ row }) => <ArrayCell data={row.getValue("scriptSrc")} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// JS
|
||||
{
|
||||
accessorKey: "js",
|
||||
meta: { title: "JS" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="JS" />
|
||||
),
|
||||
cell: ({ row }) => <ArrayCell data={row.getValue("js")} />,
|
||||
enableResizing: true,
|
||||
size: 180,
|
||||
},
|
||||
// Implies
|
||||
{
|
||||
accessorKey: "implies",
|
||||
meta: { title: "Implies" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Implies" />
|
||||
),
|
||||
cell: ({ row }) => <ArrayCell data={row.getValue("implies")} />,
|
||||
enableResizing: true,
|
||||
size: 180,
|
||||
},
|
||||
// Meta
|
||||
{
|
||||
accessorKey: "meta",
|
||||
meta: { title: "Meta" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Meta" />
|
||||
),
|
||||
cell: ({ row }) => <JsonCell data={row.getValue("meta")} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// HTML
|
||||
{
|
||||
accessorKey: "html",
|
||||
meta: { title: "HTML" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="HTML" />
|
||||
),
|
||||
cell: ({ row }) => <ArrayCell data={row.getValue("html")} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// 描述
|
||||
{
|
||||
accessorKey: "description",
|
||||
@@ -151,7 +219,7 @@ export function createWappalyzerFingerprintColumns({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Description" />
|
||||
),
|
||||
cell: ({ row }) => <ExpandableTextCell value={row.getValue("description")} />,
|
||||
cell: ({ row }) => <ExpandableCell value={row.getValue("description")} maxLines={2} />,
|
||||
enableResizing: true,
|
||||
size: 250,
|
||||
},
|
||||
@@ -162,37 +230,20 @@ export function createWappalyzerFingerprintColumns({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Website" />
|
||||
),
|
||||
cell: ({ row }) => <ExpandableLinkCell value={row.getValue("website")} />,
|
||||
cell: ({ row }) => <ExpandableCell value={row.getValue("website")} variant="url" maxLines={1} />,
|
||||
enableResizing: true,
|
||||
size: 200,
|
||||
},
|
||||
// 检测方式数量
|
||||
// CPE
|
||||
{
|
||||
id: "detectionMethods",
|
||||
meta: { title: "Detection" },
|
||||
accessorKey: "cpe",
|
||||
meta: { title: "CPE" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Detection" />
|
||||
<DataTableColumnHeader column={column} title="CPE" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const fp = row.original
|
||||
const methods: string[] = []
|
||||
if (fp.cookies && Object.keys(fp.cookies).length > 0) methods.push("cookies")
|
||||
if (fp.headers && Object.keys(fp.headers).length > 0) methods.push("headers")
|
||||
if (fp.scriptSrc && fp.scriptSrc.length > 0) methods.push("script")
|
||||
if (fp.js && fp.js.length > 0) methods.push("js")
|
||||
if (fp.meta && Object.keys(fp.meta).length > 0) methods.push("meta")
|
||||
if (fp.html && fp.html.length > 0) methods.push("html")
|
||||
|
||||
if (methods.length === 0) return "-"
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{methods.map((m) => (
|
||||
<Badge key={m} variant="outline" className="text-xs">
|
||||
{m}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
const cpe = row.getValue("cpe") as string
|
||||
return cpe ? <span className="font-mono text-xs">{cpe}</span> : "-"
|
||||
},
|
||||
enableResizing: true,
|
||||
size: 180,
|
||||
@@ -212,7 +263,7 @@ export function createWappalyzerFingerprintColumns({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableResizing: true,
|
||||
enableResizing: false,
|
||||
size: 160,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/popover"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import type { IPAddress } from "@/types/ip-address.types"
|
||||
import { TruncatedCell } from "@/components/ui/truncated-cell"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
|
||||
export function createIPAddressColumns(params: {
|
||||
formatDate: (value: string) => string
|
||||
@@ -57,7 +57,7 @@ export function createIPAddressColumns(params: {
|
||||
<DataTableColumnHeader column={column} title="IP Address" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.original.ip} maxLength="ip" mono />
|
||||
<ExpandableCell value={row.original.ip} />
|
||||
),
|
||||
},
|
||||
// host 列
|
||||
@@ -83,7 +83,7 @@ export function createIPAddressColumns(params: {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{displayHosts.map((host, index) => (
|
||||
<TruncatedCell key={index} value={host} maxLength="host" mono />
|
||||
<ExpandableCell key={index} value={host} maxLines={1} />
|
||||
))}
|
||||
{hasMore && (
|
||||
<Popover>
|
||||
@@ -97,7 +97,7 @@ export function createIPAddressColumns(params: {
|
||||
<h4 className="font-medium text-sm">All Hosts ({hosts.length})</h4>
|
||||
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
{hosts.map((host, index) => (
|
||||
<span key={index} className="text-sm font-mono break-all">
|
||||
<span key={index} className="text-sm break-all">
|
||||
{host}
|
||||
</span>
|
||||
))}
|
||||
@@ -116,6 +116,7 @@ export function createIPAddressColumns(params: {
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
meta: { title: "Created At" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Created At" />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
// 导入图标组件
|
||||
import { MoreHorizontal, Play, Calendar, Edit, Trash2, Eye } from "lucide-react"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -161,21 +162,9 @@ export const createOrganizationColumns = ({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Description" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue("description") as string
|
||||
|
||||
if (!description) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm text-muted-foreground break-all leading-relaxed whitespace-normal">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("description")} variant="muted" />
|
||||
),
|
||||
},
|
||||
|
||||
// Total Targets 列
|
||||
|
||||
@@ -172,6 +172,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.subdomain_discovery} />
|
||||
@@ -189,6 +190,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.port_scan} />
|
||||
@@ -206,6 +208,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.site_scan} />
|
||||
@@ -223,6 +226,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.directory_scan} />
|
||||
@@ -240,6 +244,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.url_fetch} />
|
||||
@@ -257,6 +262,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.osint} />
|
||||
@@ -274,6 +280,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.vulnerability_scan} />
|
||||
@@ -291,6 +298,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.waf_detection} />
|
||||
@@ -308,6 +316,7 @@ export const createEngineColumns = ({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const features = parseEngineFeatures(row.original)
|
||||
return <FeatureStatus enabled={features.screenshot} />
|
||||
|
||||
@@ -370,6 +370,7 @@ export const createScanHistoryColumns = ({
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
maxSize: 180,
|
||||
enableResizing: false,
|
||||
meta: { title: "Engine Name" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Engine Name" />
|
||||
@@ -390,6 +391,7 @@ export const createScanHistoryColumns = ({
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
meta: { title: "Created At" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Created At" />
|
||||
@@ -407,9 +409,10 @@ export const createScanHistoryColumns = ({
|
||||
// Status 列
|
||||
{
|
||||
accessorKey: "status",
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
maxSize: 130,
|
||||
enableResizing: false,
|
||||
meta: { title: "Status" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import type { Subdomain } from "@/types/subdomain.types"
|
||||
|
||||
// 列创建函数的参数类型
|
||||
@@ -54,16 +54,9 @@ export const createSubdomainColumns = ({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Subdomain" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue("name") as string
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium font-mono break-all leading-relaxed whitespace-normal">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("name")} />
|
||||
),
|
||||
},
|
||||
|
||||
// 创建时间列
|
||||
@@ -72,6 +65,7 @@ export const createSubdomainColumns = ({
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
meta: { title: "Created At" },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Created At" />
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { IconPlus } from "@tabler/icons-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UnifiedDataTable } from "@/components/ui/data-table"
|
||||
import type { FilterField } from "@/components/common/smart-filter-input"
|
||||
import type { Subdomain } from "@/types/subdomain.types"
|
||||
@@ -101,18 +99,6 @@ export function SubdomainsDataTable({
|
||||
})
|
||||
}
|
||||
|
||||
// 自定义工具栏右侧按钮
|
||||
const toolbarRightContent = (
|
||||
<>
|
||||
{onBulkAdd && (
|
||||
<Button onClick={onBulkAdd} size="sm" variant="outline">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
批量添加
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<UnifiedDataTable
|
||||
data={data}
|
||||
@@ -135,14 +121,16 @@ export function SubdomainsDataTable({
|
||||
// 批量操作
|
||||
onBulkDelete={onBulkDelete}
|
||||
bulkDeleteLabel="Delete"
|
||||
// 添加按钮
|
||||
onAddNew={onAddNew}
|
||||
addButtonLabel={addButtonText}
|
||||
// 批量添加按钮
|
||||
onBulkAdd={onBulkAdd}
|
||||
bulkAddLabel="批量添加"
|
||||
// 下载
|
||||
downloadOptions={downloadOptions.length > 0 ? downloadOptions : undefined}
|
||||
// 空状态
|
||||
emptyMessage="No results"
|
||||
// 自定义工具栏按钮
|
||||
toolbarRight={onBulkAdd ? toolbarRightContent : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { MoreHorizontal, Eye, Trash2, Play, Calendar, Copy, Check } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { toast } from "sonner"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { ExpandableBadgeList } from "@/components/ui/data-table/expandable-cell"
|
||||
import type { Target } from "@/types/target.types"
|
||||
|
||||
/**
|
||||
@@ -278,54 +278,12 @@ export const createAllTargetsColumns = ({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const organizations = row.getValue("organizations") as Array<{ id: number; name: string }> | undefined
|
||||
if (!organizations || organizations.length === 0) {
|
||||
return <span className="text-sm text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const displayOrgs = organizations.slice(0, 2)
|
||||
const remainingCount = organizations.length - 2
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{displayOrgs.map((org) => (
|
||||
<Badge
|
||||
key={org.id}
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
title={org.name}
|
||||
>
|
||||
{org.name}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<TooltipProvider delayDuration={500} skipDelayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs cursor-default"
|
||||
>
|
||||
+{remainingCount}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="start"
|
||||
sideOffset={5}
|
||||
className="max-w-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{organizations.slice(2).map((org) => (
|
||||
<div key={org.id} className="text-xs">
|
||||
{org.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<ExpandableBadgeList
|
||||
items={organizations}
|
||||
maxVisible={2}
|
||||
variant="secondary"
|
||||
/>
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
|
||||
@@ -93,13 +93,6 @@ export function TargetsDataTable({
|
||||
pageSize: pagination.pageSize,
|
||||
} : undefined
|
||||
|
||||
// 自定义添加按钮(支持 onAddHover)
|
||||
const addButton = onAddNew ? (
|
||||
<Button onClick={onAddNew} onMouseEnter={onAddHover} size="sm">
|
||||
{addButtonText}
|
||||
</Button>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<UnifiedDataTable
|
||||
data={data}
|
||||
@@ -115,7 +108,11 @@ export function TargetsDataTable({
|
||||
// 批量操作
|
||||
onBulkDelete={onBulkDelete}
|
||||
bulkDeleteLabel="删除"
|
||||
showAddButton={false}
|
||||
// 添加按钮
|
||||
onAddNew={onAddNew}
|
||||
onAddHover={onAddHover}
|
||||
addButtonLabel={addButtonText}
|
||||
showAddButton={!!onAddNew}
|
||||
// 空状态
|
||||
emptyMessage="暂无数据"
|
||||
// 自定义工具栏
|
||||
@@ -137,7 +134,6 @@ export function TargetsDataTable({
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
toolbarRight={addButton}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { MoreHorizontal, Eye, Trash2, Copy } from "lucide-react"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import { ExpandableCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { formatDate } from "@/lib/utils"
|
||||
import { toast } from "sonner"
|
||||
@@ -110,18 +111,9 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Command Template" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const template = row.getValue("commandTemplate") as string
|
||||
if (!template) return <span className="text-muted-foreground text-sm">-</span>
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-mono break-all leading-relaxed whitespace-normal">
|
||||
{template}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("commandTemplate")} variant="mono" />
|
||||
),
|
||||
},
|
||||
|
||||
// 描述列
|
||||
@@ -133,18 +125,9 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Description" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue("description") as string
|
||||
if (!description) return <span className="text-muted-foreground text-sm">-</span>
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm text-muted-foreground break-all leading-relaxed whitespace-normal">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("description")} variant="muted" />
|
||||
),
|
||||
},
|
||||
|
||||
// 更新时间列
|
||||
|
||||
280
frontend/components/ui/data-table/expandable-cell.tsx
Normal file
280
frontend/components/ui/data-table/expandable-cell.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
export interface ExpandableCellProps {
|
||||
/** 要显示的值 */
|
||||
value: string | null | undefined
|
||||
/** 显示变体 */
|
||||
variant?: "text" | "url" | "mono" | "muted"
|
||||
/** 最大显示行数,默认 3 */
|
||||
maxLines?: number
|
||||
/** 额外的 CSS 类名 */
|
||||
className?: string
|
||||
/** 空值时显示的占位符 */
|
||||
placeholder?: string
|
||||
/** 展开按钮文本 */
|
||||
expandLabel?: string
|
||||
/** 收起按钮文本 */
|
||||
collapseLabel?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的可展开单元格组件
|
||||
*
|
||||
* 特性:
|
||||
* - 默认显示最多 3 行(可配置)
|
||||
* - 自动检测内容是否溢出
|
||||
* - 只在内容超出时显示展开/收起按钮
|
||||
* - 支持 text、url、mono、muted 四种变体
|
||||
*/
|
||||
export function ExpandableCell({
|
||||
value,
|
||||
variant = "text",
|
||||
maxLines = 3,
|
||||
className,
|
||||
placeholder = "-",
|
||||
expandLabel = "展开",
|
||||
collapseLabel = "收起",
|
||||
}: ExpandableCellProps) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
const [isOverflowing, setIsOverflowing] = React.useState(false)
|
||||
const contentRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// 检测内容是否溢出
|
||||
React.useEffect(() => {
|
||||
const el = contentRef.current
|
||||
if (!el) return
|
||||
|
||||
const checkOverflow = () => {
|
||||
// 比较 scrollHeight 和 clientHeight 来判断是否溢出
|
||||
setIsOverflowing(el.scrollHeight > el.clientHeight + 1)
|
||||
}
|
||||
|
||||
checkOverflow()
|
||||
|
||||
// 监听窗口大小变化
|
||||
const resizeObserver = new ResizeObserver(checkOverflow)
|
||||
resizeObserver.observe(el)
|
||||
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [value, expanded])
|
||||
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground text-sm">{placeholder}</span>
|
||||
}
|
||||
|
||||
const lineClampClass = {
|
||||
1: "line-clamp-1",
|
||||
2: "line-clamp-2",
|
||||
3: "line-clamp-3",
|
||||
4: "line-clamp-4",
|
||||
5: "line-clamp-5",
|
||||
6: "line-clamp-6",
|
||||
}[maxLines] || "line-clamp-3"
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"text-sm break-all leading-relaxed whitespace-normal",
|
||||
variant === "mono" && "font-mono text-xs text-muted-foreground",
|
||||
variant === "url" && "text-muted-foreground",
|
||||
variant === "muted" && "text-muted-foreground",
|
||||
!expanded && lineClampClass,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{(isOverflowing || expanded) && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
>
|
||||
{expanded ? collapseLabel : expandLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 专用的可展开单元格
|
||||
*/
|
||||
export function ExpandableUrlCell(props: Omit<ExpandableCellProps, "variant">) {
|
||||
return <ExpandableCell {...props} variant="url" />
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码/等宽字体的可展开单元格
|
||||
*/
|
||||
export function ExpandableMonoCell(props: Omit<ExpandableCellProps, "variant">) {
|
||||
return <ExpandableCell {...props} variant="mono" />
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Badge 列表相关组件
|
||||
// ============================================================================
|
||||
|
||||
export interface BadgeItem {
|
||||
id: number | string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ExpandableBadgeListProps {
|
||||
/** Badge 项目列表 */
|
||||
items: BadgeItem[] | null | undefined
|
||||
/** 默认显示的数量,默认 2 */
|
||||
maxVisible?: number
|
||||
/** Badge 变体 */
|
||||
variant?: "default" | "secondary" | "outline" | "destructive"
|
||||
/** 空值时显示的占位符 */
|
||||
placeholder?: string
|
||||
/** 额外的 CSS 类名 */
|
||||
className?: string
|
||||
/** 点击 Badge 时的回调 */
|
||||
onItemClick?: (item: BadgeItem) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 可展开的 Badge 列表组件
|
||||
*
|
||||
* 特性:
|
||||
* - 默认显示前 N 个 Badge(可配置)
|
||||
* - 超过数量时显示展开按钮
|
||||
* - 点击展开按钮显示所有 Badge
|
||||
* - 展开后显示收起按钮
|
||||
*/
|
||||
export function ExpandableBadgeList({
|
||||
items,
|
||||
maxVisible = 2,
|
||||
variant = "secondary",
|
||||
placeholder = "-",
|
||||
className,
|
||||
onItemClick,
|
||||
}: ExpandableBadgeListProps) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return <span className="text-sm text-muted-foreground">{placeholder}</span>
|
||||
}
|
||||
|
||||
const hasMore = items.length > maxVisible
|
||||
const displayItems = expanded ? items : items.slice(0, maxVisible)
|
||||
const remainingCount = items.length - maxVisible
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-1", className)}>
|
||||
{displayItems.map((item) => (
|
||||
<Badge
|
||||
key={item.id}
|
||||
variant={variant}
|
||||
className={cn(
|
||||
"text-xs",
|
||||
onItemClick && "cursor-pointer hover:bg-accent"
|
||||
)}
|
||||
title={item.name}
|
||||
onClick={onItemClick ? () => onItemClick(item) : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Badge>
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="inline-flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
<span>收起</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>展开</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 字符串列表相关组件
|
||||
// ============================================================================
|
||||
|
||||
export interface ExpandableTagListProps {
|
||||
/** 标签列表 */
|
||||
items: string[] | null | undefined
|
||||
/** 默认显示的数量,默认 3 */
|
||||
maxVisible?: number
|
||||
/** Badge 变体 */
|
||||
variant?: "default" | "secondary" | "outline" | "destructive"
|
||||
/** 空值时显示的占位符 */
|
||||
placeholder?: string
|
||||
/** 额外的 CSS 类名 */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 可展开的标签列表组件(用于字符串数组)
|
||||
*
|
||||
* 适用于 tech 列表、tags 列表等场景
|
||||
*/
|
||||
export function ExpandableTagList({
|
||||
items,
|
||||
maxVisible = 3,
|
||||
variant = "outline",
|
||||
placeholder = "-",
|
||||
className,
|
||||
}: ExpandableTagListProps) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return <span className="text-sm text-muted-foreground">{placeholder}</span>
|
||||
}
|
||||
|
||||
const hasMore = items.length > maxVisible
|
||||
const displayItems = expanded ? items : items.slice(0, maxVisible)
|
||||
const remainingCount = items.length - maxVisible
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-1", className)}>
|
||||
{displayItems.map((item, index) => (
|
||||
<Badge
|
||||
key={`${item}-${index}`}
|
||||
variant={variant}
|
||||
className="text-xs"
|
||||
title={item}
|
||||
>
|
||||
{item}
|
||||
</Badge>
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="inline-flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
<span>收起</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>展开</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -113,9 +113,15 @@ export function UnifiedDataTable<TData>({
|
||||
|
||||
// 添加操作
|
||||
onAddNew,
|
||||
onAddHover,
|
||||
addButtonLabel = "Add",
|
||||
showAddButton = true,
|
||||
|
||||
// 批量添加操作
|
||||
onBulkAdd,
|
||||
bulkAddLabel = "批量添加",
|
||||
showBulkAdd = true,
|
||||
|
||||
// 下载操作
|
||||
downloadOptions,
|
||||
|
||||
@@ -436,11 +442,19 @@ export function UnifiedDataTable<TData>({
|
||||
|
||||
{/* 添加按钮 */}
|
||||
{showAddButton && onAddNew && (
|
||||
<Button onClick={onAddNew} size="sm">
|
||||
<Button onClick={onAddNew} onMouseEnter={onAddHover} size="sm">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
{addButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 批量添加按钮 */}
|
||||
{showBulkAdd && onBulkAdd && (
|
||||
<Button onClick={onBulkAdd} size="sm" variant="outline">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
{bulkAddLabel}
|
||||
</Button>
|
||||
)}
|
||||
</DataTableToolbar>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* 预设的截断长度配置(保留用于兼容)
|
||||
*/
|
||||
export const TRUNCATE_LENGTHS = {
|
||||
url: 50,
|
||||
title: 25,
|
||||
location: 20,
|
||||
webServer: 20,
|
||||
contentType: 20,
|
||||
bodyPreview: 25,
|
||||
subdomain: 35,
|
||||
ip: 35,
|
||||
host: 30,
|
||||
default: 30,
|
||||
} as const
|
||||
|
||||
export type TruncateLengthKey = keyof typeof TRUNCATE_LENGTHS
|
||||
|
||||
interface TruncatedCellProps {
|
||||
/** 要显示的值 */
|
||||
value: string | null | undefined
|
||||
/** 最大显示长度(已废弃,不再使用) */
|
||||
maxLength?: number | TruncateLengthKey
|
||||
/** 额外的 CSS 类名 */
|
||||
className?: string
|
||||
/** 是否使用等宽字体 */
|
||||
mono?: boolean
|
||||
/** 空值时显示的占位符 */
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 单元格组件 - 多行显示
|
||||
*/
|
||||
export function TruncatedCell({
|
||||
value,
|
||||
className,
|
||||
mono = false,
|
||||
placeholder = "-",
|
||||
}: TruncatedCellProps) {
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground text-sm">{placeholder}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm break-all leading-relaxed whitespace-normal",
|
||||
mono && "font-mono",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 专用的单元格 - 多行显示
|
||||
*/
|
||||
export function TruncatedUrlCell({
|
||||
value,
|
||||
className,
|
||||
}: Omit<TruncatedCellProps, "mono">) {
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground text-sm">-</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm break-all leading-relaxed whitespace-normal",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { TruncatedUrlCell, TruncatedCell } from "@/components/ui/truncated-cell"
|
||||
import { ExpandableUrlCell } from "@/components/ui/data-table/expandable-cell"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
|
||||
import type { Vulnerability, VulnerabilitySeverity } from "@/types/vulnerability.types"
|
||||
@@ -62,9 +62,10 @@ export function createVulnerabilityColumns({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
maxSize: 120,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const severity = row.getValue("severity") as VulnerabilitySeverity
|
||||
const config = severityConfig[severity]
|
||||
@@ -84,6 +85,7 @@ export function createVulnerabilityColumns({
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
maxSize: 150,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const source = row.getValue("source") as string
|
||||
return (
|
||||
@@ -129,10 +131,9 @@ export function createVulnerabilityColumns({
|
||||
size: 500,
|
||||
minSize: 300,
|
||||
maxSize: 700,
|
||||
cell: ({ row }) => {
|
||||
const url = row.original.url
|
||||
return <TruncatedUrlCell value={url} />
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableUrlCell value={row.original.url} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
@@ -143,6 +144,7 @@ export function createVulnerabilityColumns({
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as string
|
||||
return (
|
||||
|
||||
@@ -6,38 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { DataTableColumnHeader } from "@/components/ui/data-table/column-header"
|
||||
import type { WebSite } from "@/types/website.types"
|
||||
import { TruncatedCell, TruncatedUrlCell } from "@/components/ui/truncated-cell"
|
||||
|
||||
/**
|
||||
* Body Preview 单元格组件 - 最多显示3行,超出折叠,点击展开查看完整内容
|
||||
*/
|
||||
function BodyPreviewCell({ value }: { value: string | null | undefined }) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
if (!value) {
|
||||
return <span className="text-muted-foreground text-sm">-</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className={`text-sm text-muted-foreground break-all leading-relaxed whitespace-normal cursor-pointer hover:text-foreground transition-colors ${!expanded ? 'line-clamp-3' : ''}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title={expanded ? "点击收起" : "点击展开"}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{value.length > 100 && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-primary hover:underline self-start"
|
||||
>
|
||||
{expanded ? "收起" : "展开"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { ExpandableCell, ExpandableTagList } from "@/components/ui/data-table/expandable-cell"
|
||||
|
||||
interface CreateWebSiteColumnsProps {
|
||||
formatDate: (dateString: string) => string
|
||||
@@ -82,10 +51,9 @@ export function createWebSiteColumns({
|
||||
size: 400,
|
||||
minSize: 200,
|
||||
maxSize: 700,
|
||||
cell: ({ row }) => {
|
||||
const url = row.getValue("url") as string
|
||||
return <TruncatedUrlCell value={url} />
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ExpandableCell value={row.getValue("url")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "host",
|
||||
@@ -97,7 +65,7 @@ export function createWebSiteColumns({
|
||||
minSize: 100,
|
||||
maxSize: 250,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("host")} maxLength="host" mono />
|
||||
<ExpandableCell value={row.getValue("host")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -110,7 +78,7 @@ export function createWebSiteColumns({
|
||||
minSize: 100,
|
||||
maxSize: 300,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("title")} maxLength="title" />
|
||||
<ExpandableCell value={row.getValue("title")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -122,6 +90,7 @@ export function createWebSiteColumns({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const statusCode = row.getValue("statusCode") as number
|
||||
if (!statusCode) return "-"
|
||||
@@ -148,17 +117,7 @@ export function createWebSiteColumns({
|
||||
minSize: 150,
|
||||
cell: ({ row }) => {
|
||||
const tech = row.getValue("tech") as string[]
|
||||
if (!tech || tech.length === 0) return <span className="text-sm text-muted-foreground">-</span>
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{tech.map((technology, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{technology}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
return <ExpandableTagList items={tech} maxVisible={3} variant="outline" />
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -186,7 +145,7 @@ export function createWebSiteColumns({
|
||||
minSize: 100,
|
||||
maxSize: 300,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("location")} maxLength="location" />
|
||||
<ExpandableCell value={row.getValue("location")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -199,7 +158,7 @@ export function createWebSiteColumns({
|
||||
minSize: 80,
|
||||
maxSize: 200,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("webserver")} maxLength="webServer" />
|
||||
<ExpandableCell value={row.getValue("webserver")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -212,7 +171,7 @@ export function createWebSiteColumns({
|
||||
minSize: 80,
|
||||
maxSize: 200,
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.getValue("contentType")} maxLength="contentType" />
|
||||
<ExpandableCell value={row.getValue("contentType")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -224,7 +183,7 @@ export function createWebSiteColumns({
|
||||
size: 350,
|
||||
minSize: 250,
|
||||
cell: ({ row }) => (
|
||||
<BodyPreviewCell value={row.getValue("bodyPreview")} />
|
||||
<ExpandableCell value={row.getValue("bodyPreview")} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -236,6 +195,7 @@ export function createWebSiteColumns({
|
||||
size: 80,
|
||||
minSize: 60,
|
||||
maxSize: 100,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const vhost = row.getValue("vhost") as boolean | null
|
||||
if (vhost === null) return "-"
|
||||
@@ -255,6 +215,7 @@ export function createWebSiteColumns({
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
maxSize: 200,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as string
|
||||
return <div className="text-sm">{createdAt ? formatDate(createdAt) : "-"}</div>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { IconPlus } from "@tabler/icons-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { UnifiedDataTable } from "@/components/ui/data-table"
|
||||
import type { FilterField } from "@/components/common/smart-filter-input"
|
||||
import type { WebSite } from "@/types/website.types"
|
||||
@@ -84,14 +82,6 @@ export function WebSitesDataTable({
|
||||
})
|
||||
}
|
||||
|
||||
// 自定义工具栏右侧按钮
|
||||
const toolbarRightContent = onBulkAdd ? (
|
||||
<Button onClick={onBulkAdd} size="sm" variant="outline">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
批量添加
|
||||
</Button>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<UnifiedDataTable
|
||||
data={data}
|
||||
@@ -115,12 +105,13 @@ export function WebSitesDataTable({
|
||||
onBulkDelete={onBulkDelete}
|
||||
bulkDeleteLabel="Delete"
|
||||
showAddButton={false}
|
||||
// 批量添加按钮
|
||||
onBulkAdd={onBulkAdd}
|
||||
bulkAddLabel="批量添加"
|
||||
// 下载
|
||||
downloadOptions={downloadOptions.length > 0 ? downloadOptions : undefined}
|
||||
// 空状态
|
||||
emptyMessage="暂无数据"
|
||||
// 自定义工具栏按钮
|
||||
toolbarRight={toolbarRightContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,9 +105,15 @@ export interface UnifiedDataTableProps<TData> {
|
||||
|
||||
// 添加操作
|
||||
onAddNew?: () => void
|
||||
onAddHover?: () => void
|
||||
addButtonLabel?: string
|
||||
showAddButton?: boolean
|
||||
|
||||
// 批量添加操作
|
||||
onBulkAdd?: () => void
|
||||
bulkAddLabel?: string
|
||||
showBulkAdd?: boolean
|
||||
|
||||
// 下载操作
|
||||
downloadOptions?: DownloadOption[]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user