feat: refactor data asset center for enhanced search and analytics

Refactor homepage for focused search and data display; streamline data platform; enhance user and tag management; focus AI assistant on data analysis and report generation.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-25 06:42:34 +00:00
parent ecd8a48863
commit 4eed69520c
40 changed files with 6853 additions and 6111 deletions

View File

@@ -1,41 +0,0 @@
"use client"
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
const data = [
{ name: "用户价值", value: 25 },
{ name: "人口属性", value: 18 },
{ name: "兴趣爱好", value: 22 },
{ name: "流失风险", value: 12 },
{ name: "地理位置", value: 15 },
{ name: "活跃时间", value: 10 },
{ name: "消费能力", value: 14 },
{ name: "渠道来源", value: 12 },
]
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884d8", "#82ca9d", "#ffc658", "#8dd1e1"]
export function TagCategoryChart() {
return (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
)
}

View File

@@ -1,149 +0,0 @@
"use client"
import { useEffect, useRef } from "react"
import * as d3 from "d3"
interface Node {
id: string
name: string
category: string
value: number
}
interface Link {
source: string
target: string
value: number
}
const mockData = {
nodes: [
{ id: "1", name: "高价值用户", category: "用户价值", value: 20 },
{ id: "2", name: "90后", category: "人口属性", value: 15 },
{ id: "3", name: "游戏爱好者", category: "兴趣爱好", value: 18 },
{ id: "4", name: "流失风险高", category: "流失风险", value: 12 },
{ id: "5", name: "北京地区", category: "地理位置", value: 10 },
{ id: "6", name: "周末活跃", category: "活跃时间", value: 14 },
{ id: "7", name: "高消费", category: "消费能力", value: 16 },
{ id: "8", name: "App渠道", category: "渠道来源", value: 8 },
],
links: [
{ source: "1", target: "7", value: 5 },
{ source: "2", target: "3", value: 8 },
{ source: "2", target: "6", value: 3 },
{ source: "3", target: "7", value: 6 },
{ source: "4", target: "6", value: 4 },
{ source: "5", target: "8", value: 2 },
{ source: "6", target: "3", value: 7 },
{ source: "7", target: "8", value: 3 },
{ source: "1", target: "3", value: 5 },
{ source: "2", target: "5", value: 4 },
{ source: "4", target: "1", value: 6 },
],
}
const categoryColors = {
: "#0088FE",
: "#00C49F",
: "#FFBB28",
: "#FF8042",
: "#8884d8",
: "#82ca9d",
: "#ffc658",
: "#8dd1e1",
}
export function TagRelationshipGraph() {
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!svgRef.current) return
const svg = d3.select(svgRef.current)
svg.selectAll("*").remove()
const width = svgRef.current.clientWidth
const height = svgRef.current.clientHeight
// 创建力导向图
const simulation = d3
.forceSimulation(mockData.nodes as d3.SimulationNodeDatum[])
.force(
"link",
d3
.forceLink(mockData.links)
.id((d: any) => d.id)
.distance(100),
)
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2))
// 绘制连线
const link = svg
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(mockData.links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value))
// 绘制节点
const node = svg
.append("g")
.selectAll("circle")
.data(mockData.nodes)
.join("circle")
.attr("r", (d) => d.value * 0.8)
.attr("fill", (d) => categoryColors[d.category as keyof typeof categoryColors])
.call(d3.drag<SVGCircleElement, Node>().on("start", dragstarted).on("drag", dragged).on("end", dragended) as any)
// 添加标签
const text = svg
.append("g")
.selectAll("text")
.data(mockData.nodes)
.join("text")
.text((d) => d.name)
.attr("font-size", 10)
.attr("dx", 15)
.attr("dy", 4)
// 更新位置
simulation.on("tick", () => {
link
.attr("x1", (d: any) => d.source.x)
.attr("y1", (d: any) => d.source.y)
.attr("x2", (d: any) => d.target.x)
.attr("y2", (d: any) => d.target.y)
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
text.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
})
// 拖拽函数
function dragstarted(event: any, d: any) {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
}
function dragged(event: any, d: any) {
d.fx = event.x
d.fy = event.y
}
function dragended(event: any, d: any) {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
}
return () => {
simulation.stop()
}
}, [])
return <svg ref={svgRef} width="100%" height="100%" />
}

View File

@@ -1,49 +0,0 @@
"use client"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
const data = [
{
name: "营销系统",
使用标签数: 85,
},
{
name: "推荐系统",
使用标签数: 72,
},
{
name: "客服系统",
使用标签数: 45,
},
{
name: "风控系统",
使用标签数: 38,
},
{
name: "内容系统",
使用标签数: 65,
},
]
export function TagUsageStats() {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="使用标签数" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -1,286 +0,0 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Trash2, Plus, Save } from "lucide-react"
interface RuleCondition {
field: string
operator: string
value: string
}
interface TagRule {
id: string
name: string
description: string
conditions: RuleCondition[]
actions: {
addTags: string[]
removeTags: string[]
}
enabled: boolean
}
interface RuleEditorProps {
rule?: TagRule
onSave: (rule: TagRule) => void
onCancel: () => void
}
export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
const [formData, setFormData] = useState<TagRule>(
rule || {
id: "",
name: "",
description: "",
conditions: [{ field: "", operator: "", value: "" }],
actions: {
addTags: [],
removeTags: [],
},
enabled: true,
},
)
const [newAddTag, setNewAddTag] = useState("")
const [newRemoveTag, setNewRemoveTag] = useState("")
const fieldOptions = [
{ value: "imei", label: "IMEI" },
{ value: "phone", label: "手机号" },
{ value: "device_brand", label: "设备品牌" },
{ value: "device_model", label: "设备型号" },
{ value: "os_version", label: "系统版本" },
{ value: "app_version", label: "应用版本" },
{ value: "location", label: "地理位置" },
{ value: "user_behavior", label: "用户行为" },
{ value: "consumption_amount", label: "消费金额" },
{ value: "activity_frequency", label: "活跃频率" },
]
const operatorOptions = [
{ value: "equals", label: "等于" },
{ value: "not_equals", label: "不等于" },
{ value: "contains", label: "包含" },
{ value: "not_contains", label: "不包含" },
{ value: "starts_with", label: "开始于" },
{ value: "ends_with", label: "结束于" },
{ value: "greater_than", label: "大于" },
{ value: "less_than", label: "小于" },
{ value: "in_range", label: "在范围内" },
{ value: "regex", label: "正则匹配" },
]
const addCondition = () => {
setFormData({
...formData,
conditions: [...formData.conditions, { field: "", operator: "", value: "" }],
})
}
const removeCondition = (index: number) => {
setFormData({
...formData,
conditions: formData.conditions.filter((_, i) => i !== index),
})
}
const updateCondition = (index: number, field: keyof RuleCondition, value: string) => {
const newConditions = [...formData.conditions]
newConditions[index] = { ...newConditions[index], [field]: value }
setFormData({ ...formData, conditions: newConditions })
}
const addTag = (type: "addTags" | "removeTags") => {
const tagValue = type === "addTags" ? newAddTag : newRemoveTag
if (tagValue.trim()) {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: [...formData.actions[type], tagValue.trim()],
},
})
if (type === "addTags") {
setNewAddTag("")
} else {
setNewRemoveTag("")
}
}
}
const removeTag = (type: "addTags" | "removeTags", index: number) => {
setFormData({
...formData,
actions: {
...formData.actions,
[type]: formData.actions[type].filter((_, i) => i !== index),
},
})
}
const handleSave = () => {
if (formData.name && formData.conditions.length > 0) {
onSave(formData)
}
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="rule-name"></Label>
<Input
id="rule-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="输入规则名称"
/>
</div>
<div>
<Label htmlFor="rule-description"></Label>
<Input
id="rule-description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="输入规则描述"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{formData.conditions.map((condition, index) => (
<div key={index} className="flex items-center space-x-2 p-3 border rounded-lg">
<Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fieldOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={condition.operator} onValueChange={(value) => updateCondition(index, "operator", value)}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="选择操作符" />
</SelectTrigger>
<SelectContent>
{operatorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={condition.value}
onChange={(e) => updateCondition(index, "value", e.target.value)}
placeholder="输入值"
className="flex-1"
/>
<Button
variant="outline"
size="icon"
onClick={() => removeCondition(index)}
disabled={formData.conditions.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={addCondition}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newAddTag}
onChange={(e) => setNewAddTag(e.target.value)}
placeholder="输入要添加的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("addTags")}
/>
<Button onClick={() => addTag("addTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.addTags.map((tag, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("addTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
<div>
<Label></Label>
<div className="flex space-x-2 mt-2">
<Input
value={newRemoveTag}
onChange={(e) => setNewRemoveTag(e.target.value)}
placeholder="输入要移除的标签"
onKeyPress={(e) => e.key === "Enter" && addTag("removeTags")}
/>
<Button onClick={() => addTag("removeTags")}></Button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.actions.removeTags.map((tag, index) => (
<Badge key={index} variant="destructive" className="flex items-center gap-1">
{tag}
<button onClick={() => removeTag("removeTags", index)}>
<Trash2 className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
)
}

View File

@@ -1,258 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Filter, Calendar, FileText } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
interface ExecutionRecord {
id: string
ruleName: string
executionTime: string
duration: string
status: "success" | "failed" | "running"
affectedUsers: number
executedBy: string
}
const mockExecutionHistory: ExecutionRecord[] = [
{
id: "exec-001",
ruleName: "高价值用户识别",
executionTime: "2023-07-21 15:30:45",
duration: "45秒",
status: "success",
affectedUsers: 1250,
executedBy: "系统自动",
},
{
id: "exec-002",
ruleName: "游戏爱好者标记",
executionTime: "2023-07-21 14:45:12",
duration: "38秒",
status: "success",
affectedUsers: 2840,
executedBy: "系统自动",
},
{
id: "exec-003",
ruleName: "流失风险预警",
executionTime: "2023-07-21 13:20:33",
duration: "52秒",
status: "success",
affectedUsers: 890,
executedBy: "系统自动",
},
{
id: "exec-004",
ruleName: "高价值用户识别",
executionTime: "2023-07-20 15:30:18",
duration: "47秒",
status: "success",
affectedUsers: 1235,
executedBy: "系统自动",
},
{
id: "exec-005",
ruleName: "周末活跃用户",
executionTime: "2023-07-20 12:15:42",
duration: "1分15秒",
status: "success",
affectedUsers: 3520,
executedBy: "admin",
},
{
id: "exec-006",
ruleName: "潜在高转化用户",
executionTime: "2023-07-19 16:45:30",
duration: "2分08秒",
status: "failed",
affectedUsers: 0,
executedBy: "admin",
},
{
id: "exec-007",
ruleName: "高价值用户识别",
executionTime: "2023-07-19 15:30:22",
duration: "46秒",
status: "success",
affectedUsers: 1228,
executedBy: "系统自动",
},
]
export function RuleExecutionHistory() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedRecord, setSelectedRecord] = useState<ExecutionRecord | null>(null)
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const filteredHistory = mockExecutionHistory.filter(
(record) =>
record.ruleName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.executedBy.toLowerCase().includes(searchQuery.toLowerCase()),
)
const getStatusBadge = (status: ExecutionRecord["status"]) => {
switch (status) {
case "success":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "failed":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "running":
return <Badge className="bg-blue-100 text-blue-800"></Badge>
}
}
const handleViewDetails = (record: ExecutionRecord) => {
setSelectedRecord(record)
setIsDetailsOpen(true)
}
return (
<>
<Card>
<CardContent className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium"></h3>
<div className="flex space-x-2">
<div className="relative w-64">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索规则名称或执行人..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<Calendar className="h-4 w-4" />
</Button>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredHistory.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-mono text-xs">{record.id}</TableCell>
<TableCell>{record.ruleName}</TableCell>
<TableCell>{record.executionTime}</TableCell>
<TableCell>{record.duration}</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell>{record.affectedUsers.toLocaleString()}</TableCell>
<TableCell>{record.executedBy}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={() => handleViewDetails(record)}>
<FileText className="h-4 w-4 mr-2" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* 执行详情对话框 */}
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedRecord && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-500">ID</p>
<p className="font-mono">{selectedRecord.id}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.ruleName}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.executionTime}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.duration}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{getStatusBadge(selectedRecord.status)}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.affectedUsers.toLocaleString()}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p>{selectedRecord.executedBy}</p>
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-2"></p>
<div className="bg-gray-50 p-4 rounded-md font-mono text-xs h-40 overflow-y-auto">
{selectedRecord.status === "success" ? (
<>
<p>
[INFO] {selectedRecord.executionTime} - "{selectedRecord.ruleName}"
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>
[INFO] {selectedRecord.executionTime} - {selectedRecord.affectedUsers}{" "}
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>
[INFO] {selectedRecord.executionTime} - {selectedRecord.duration}
</p>
</>
) : (
<>
<p>
[INFO] {selectedRecord.executionTime} - "{selectedRecord.ruleName}"
</p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[INFO] {selectedRecord.executionTime} - </p>
<p>[ERROR] {selectedRecord.executionTime} - 查询执行失败: 数据库连接超时</p>
<p>
[ERROR] {selectedRecord.executionTime} - {selectedRecord.duration}
</p>
</>
)}
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -1,307 +0,0 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { CalendarIcon, Plus, Tag, X } from "lucide-react"
import { format } from "date-fns"
import { cn } from "@/lib/utils"
export function TaskCreation() {
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [newTag, setNewTag] = useState("")
const [date, setDate] = useState<Date>()
const addTag = () => {
if (newTag && !selectedTags.includes(newTag)) {
setSelectedTags([...selectedTags, newTag])
setNewTag("")
}
}
const removeTag = (tag: string) => {
setSelectedTags(selectedTags.filter((t) => t !== tag))
}
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="basic" className="space-y-4">
<TabsList>
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="target"></TabsTrigger>
<TabsTrigger value="action"></TabsTrigger>
<TabsTrigger value="schedule"></TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="task-name"></Label>
<Input id="task-name" placeholder="输入任务名称" />
</div>
<div className="space-y-2">
<Label htmlFor="task-description"></Label>
<Textarea id="task-description" placeholder="输入任务描述" />
</div>
<div className="space-y-2">
<Label htmlFor="platform"></Label>
<Select>
<SelectTrigger id="platform">
<SelectValue placeholder="选择目标平台" />
</SelectTrigger>
<SelectContent>
<SelectItem value="jd"></SelectItem>
<SelectItem value="taobao"></SelectItem>
<SelectItem value="dangdang"></SelectItem>
<SelectItem value="xiaohongshu"></SelectItem>
<SelectItem value="zhihu"></SelectItem>
<SelectItem value="bilibili"></SelectItem>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority"></Label>
<Select defaultValue="medium">
<SelectTrigger id="priority">
<SelectValue placeholder="选择优先级" />
</SelectTrigger>
<SelectContent>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</TabsContent>
<TabsContent value="target" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2 mb-2">
{selectedTags.map((tag) => (
<Badge key={tag} variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
{tag}
<button onClick={() => removeTag(tag)} className="ml-1">
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex space-x-2">
<Input
placeholder="输入标签名称"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTag()}
/>
<Button type="button" size="sm" onClick={addTag}>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="user-segment"></Label>
<Select>
<SelectTrigger id="user-segment">
<SelectValue placeholder="选择用户分群" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="new"></SelectItem>
<SelectItem value="high-value"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="user-count"></Label>
<Input id="user-count" type="number" placeholder="输入目标用户数量" />
</div>
<div className="space-y-2">
<Label htmlFor="sampling-method"></Label>
<Select defaultValue="random">
<SelectTrigger id="sampling-method">
<SelectValue placeholder="选择抽样方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="random"></SelectItem>
<SelectItem value="stratified"></SelectItem>
<SelectItem value="systematic"></SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</TabsContent>
<TabsContent value="action" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="action-type"></Label>
<Select>
<SelectTrigger id="action-type">
<SelectValue placeholder="选择动作类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="login"></SelectItem>
<SelectItem value="purchase"></SelectItem>
<SelectItem value="browse"></SelectItem>
<SelectItem value="search"></SelectItem>
<SelectItem value="content"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="action-params"></Label>
<Textarea id="action-params" placeholder="输入动作参数JSON格式" />
</div>
<div className="space-y-2">
<Label htmlFor="success-criteria"></Label>
<Select>
<SelectTrigger id="success-criteria">
<SelectValue placeholder="选择成功标准" />
</SelectTrigger>
<SelectContent>
<SelectItem value="login-success"></SelectItem>
<SelectItem value="purchase-complete"></SelectItem>
<SelectItem value="browse-time"></SelectItem>
<SelectItem value="search-count"></SelectItem>
<SelectItem value="content-interaction"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Input placeholder="输入新标签名称" />
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="标签分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="behavior"></SelectItem>
<SelectItem value="preference"></SelectItem>
<SelectItem value="value"></SelectItem>
<SelectItem value="lifecycle"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
<Button type="button" size="sm">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Switch id="auto-apply" />
<Label htmlFor="auto-apply"></Label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="schedule" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="execution-mode"></Label>
<Select defaultValue="immediate">
<SelectTrigger id="execution-mode">
<SelectValue placeholder="选择执行模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate"></SelectItem>
<SelectItem value="scheduled"></SelectItem>
<SelectItem value="recurring"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-full justify-start text-left font-normal", !date && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : "选择日期"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="execution-time"></Label>
<Input id="execution-time" type="time" />
</div>
<div className="space-y-2">
<Label htmlFor="recurrence-pattern"></Label>
<Select disabled={true}>
<SelectTrigger id="recurrence-pattern">
<SelectValue placeholder="选择重复模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
<SelectItem value="monthly"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Switch id="notify-completion" />
<Label htmlFor="notify-completion"></Label>
</div>
</div>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2 mt-6">
<Button variant="outline"></Button>
<Button></Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,273 +0,0 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Pause, AlertCircle, CheckCircle, RefreshCw, Tag, User } from "lucide-react"
export function TaskExecution() {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium"></h3>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Pause className="mr-2 h-4 w-4" />
</Button>
<Button size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center">
<Badge className="bg-blue-100 text-blue-800 mr-2"></Badge>
<span className="text-sm text-muted-foreground">预计剩余时间: 45分钟</span>
</div>
<span className="text-sm font-medium">65%</span>
</div>
<Progress value={65} className="h-2" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<User className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,500</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">8,125</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Tag className="h-5 w-5 text-purple-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">5,840</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<AlertCircle className="h-5 w-5 text-red-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">120</span>
</div>
</div>
</div>
<Tabs defaultValue="logs" className="space-y-4">
<TabsList>
<TabsTrigger value="logs"></TabsTrigger>
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="errors"></TabsTrigger>
</TabsList>
<TabsContent value="logs" className="space-y-4">
<div className="bg-black text-green-400 p-4 rounded-md font-mono text-sm h-60 overflow-y-auto">
<div>[2023-07-15 14:30:15] </div>
<div>[2023-07-15 14:30:16] ...</div>
<div>[2023-07-15 14:30:20] 12,500 </div>
<div>[2023-07-15 14:30:25] #1 (1000 users)</div>
<div>[2023-07-15 14:35:10] #1 处理完成: 成功 950, 50</div>
<div>[2023-07-15 14:35:15] #2 (1000 users)</div>
<div>[2023-07-15 14:40:05] #2 处理完成: 成功 980, 20</div>
<div>[2023-07-15 14:40:10] #3 (1000 users)</div>
<div>[2023-07-15 14:45:00] #3 处理完成: 成功 990, 10</div>
<div>[2023-07-15 14:45:05] #4 (1000 users)</div>
<div>[2023-07-15 14:50:00] #4 处理完成: 成功 995, 5</div>
<div>[2023-07-15 14:50:05] #5 (1000 users)</div>
<div>[2023-07-15 14:55:00] #5 处理完成: 成功 985, 15</div>
<div>[2023-07-15 14:55:05] #6 (1000 users)</div>
<div>[2023-07-15 15:00:00] #6 处理完成: 成功 975, 25</div>
<div>[2023-07-15 15:00:05] #7 (1000 users)</div>
<div>[2023-07-15 15:05:00] #7 处理完成: 成功 990, 10</div>
<div>[2023-07-15 15:05:05] #8 (1000 users)</div>
<div>[2023-07-15 15:10:00] #8 处理完成: 成功 975, 25</div>
<div>[2023-07-15 15:10:05] 8000 , 4500 </div>
<div>[2023-07-15 15:10:10] #9 (1000 users)</div>
</div>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>user_12345</TableCell>
<TableCell>2023-07-15 14:32:15</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, 30</TableCell>
</TableRow>
<TableRow>
<TableCell>user_12346</TableCell>
<TableCell>2023-07-15 14:32:18</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12347</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>
<Badge className="bg-red-100 text-red-800"></Badge>
</TableCell>
<TableCell>-</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12348</TableCell>
<TableCell>2023-07-15 14:32:25</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12349</TableCell>
<TableCell>2023-07-15 14:32:30</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>, </TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="errors" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>err_001</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>user_12347</TableCell>
<TableCell></TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>err_002</TableCell>
<TableCell>2023-07-15 14:33:15</TableCell>
<TableCell>user_12356</TableCell>
<TableCell></TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>err_003</TableCell>
<TableCell>2023-07-15 14:35:05</TableCell>
<TableCell>user_12378</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell>err_004</TableCell>
<TableCell>2023-07-15 14:38:30</TableCell>
<TableCell>user_12390</TableCell>
<TableCell></TableCell>
<TableCell>访</TableCell>
</TableRow>
<TableRow>
<TableCell>err_005</TableCell>
<TableCell>2023-07-15 14:42:10</TableCell>
<TableCell>user_12405</TableCell>
<TableCell>API限流</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -1,420 +0,0 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from "recharts"
import { Download, FileDown, Tag, User, CheckCircle, Clock } from "lucide-react"
export function TaskResults() {
// 模拟数据
const tagDistributionData = [
{ name: "京东活跃用户", value: 5840 },
{ name: "电商偏好", value: 4250 },
{ name: "高消费用户", value: 1850 },
{ name: "品质生活", value: 2100 },
{ name: "数码爱好者", value: 1450 },
]
const userActionData = [
{ name: "登录成功", value: 8125 },
{ name: "浏览商品", value: 6540 },
{ name: "加入购物车", value: 3250 },
{ name: "完成购买", value: 1850 },
{ name: "评价商品", value: 980 },
]
const timeDistributionData = [
{ name: "0-5分钟", count: 2450 },
{ name: "5-10分钟", count: 3650 },
{ name: "10-20分钟", count: 2850 },
{ name: "20-30分钟", count: 1950 },
{ name: "30+分钟", count: 1600 },
]
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884D8"]
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<Button variant="outline" size="sm">
<FileDown className="mr-2 h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-lg font-medium"></h3>
<div className="flex items-center space-x-2 mt-1">
<Badge className="bg-green-100 text-green-800"></Badge>
<span className="text-sm text-muted-foreground">完成时间: 2023-07-15 16:15:30</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<User className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,500</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">12,380</span>
<span className="text-sm text-muted-foreground ml-2">99.0%</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Tag className="h-5 w-5 text-purple-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">8,450</span>
<span className="text-sm text-muted-foreground ml-2">67.6%</span>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-orange-500" />
<span className="text-sm font-medium"></span>
</div>
<div className="mt-2">
<span className="text-2xl font-bold">1h 45m</span>
</div>
</div>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="tags"></TabsTrigger>
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={tagDistributionData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{tagDistributionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={userActionData}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-4"></h4>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={timeDistributionData}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</TabsContent>
<TabsContent value="tags" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>5,840</TableCell>
<TableCell>46.7%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:30:25</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>4,250</TableCell>
<TableCell>34.0%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:35:10</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,850</TableCell>
<TableCell>14.8%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:40:30</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2,100</TableCell>
<TableCell>16.8%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:45:15</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,450</TableCell>
<TableCell>11.6%</TableCell>
<TableCell></TableCell>
<TableCell>2023-07-15 15:50:05</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>3,250</TableCell>
<TableCell>26.0%</TableCell>
<TableCell>25</TableCell>
<TableCell>12.5</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>4,850</TableCell>
<TableCell>38.8%</TableCell>
<TableCell>15</TableCell>
<TableCell>8.2</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">访</TableCell>
<TableCell>2,450</TableCell>
<TableCell>19.6%</TableCell>
<TableCell>3</TableCell>
<TableCell>2.1</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,830</TableCell>
<TableCell>14.6%</TableCell>
<TableCell>1</TableCell>
<TableCell>0.5</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>120</TableCell>
<TableCell>1.0%</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="details" className="space-y-4">
<div className="flex justify-end mb-4">
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>user_12345</TableCell>
<TableCell>2023-07-15 14:32:15</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>18</TableCell>
<TableCell>9</TableCell>
<TableCell>, 30</TableCell>
</TableRow>
<TableRow>
<TableCell>user_12346</TableCell>
<TableCell>2023-07-15 14:32:18</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>25</TableCell>
<TableCell>15</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12347</TableCell>
<TableCell>2023-07-15 14:32:20</TableCell>
<TableCell>
<Badge className="bg-red-100 text-red-800"></Badge>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12348</TableCell>
<TableCell>2023-07-15 14:32:25</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>32</TableCell>
<TableCell>12</TableCell>
<TableCell>, </TableCell>
</TableRow>
<TableRow>
<TableCell>user_12349</TableCell>
<TableCell>2023-07-15 14:32:30</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Tag className="h-3 w-3" />
</Badge>
</div>
</TableCell>
<TableCell>28</TableCell>
<TableCell>18</TableCell>
<TableCell>, </TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,74 @@
"use client"
import * as React from "react"
import { HelpCircle } from "lucide-react"
import { cn } from "@/lib/utils"
interface TooltipHelpProps {
content: string
className?: string
}
export function TooltipHelp({ content, className = "" }: TooltipHelpProps) {
const [isVisible, setIsVisible] = React.useState(false)
const [position, setPosition] = React.useState({ top: 0, left: 0 })
const tooltipRef = React.useRef<HTMLDivElement>(null)
const timeoutRef = React.useRef<NodeJS.Timeout>()
const handleMouseEnter = (e: React.MouseEvent) => {
timeoutRef.current = setTimeout(() => {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const tooltipRect = tooltipRef.current?.getBoundingClientRect()
if (tooltipRect) {
const top = rect.top - tooltipRect.height - 8
const left = rect.left + (rect.width - tooltipRect.width) / 2
setPosition({ top, left })
setIsVisible(true)
}
}, 200)
}
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsVisible(false)
}
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
return (
<>
<HelpCircle
className={cn("h-4 w-4 text-gray-400 hover:text-gray-600 cursor-help", className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
{isVisible && (
<div
ref={tooltipRef}
className="fixed z-50 px-3 py-2 text-xs text-white bg-gray-900 rounded-md shadow-lg max-w-xs"
style={{
top: position.top,
left: position.left,
transition: "opacity 150ms ease-in-out",
}}
>
{content}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
</div>
)}
</>
)
}
// 为了兼容性,导出一个简单的 TooltipProvider
export const TooltipProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>