feat: 本次提交更新内容如下

场景获客列表搞定
This commit is contained in:
笔记本里的永平
2025-07-07 17:08:27 +08:00
parent 6543da9167
commit 5ff15472f5
352 changed files with 24040 additions and 18575 deletions

View File

@@ -0,0 +1,311 @@
"use client"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { BarChart3, TrendingUp, TrendingDown, Users, Star, Target, DollarSign, Percent } from "lucide-react"
interface AnalyticsData {
totalUsers: number
highValueUsers: number
mediumValueUsers: number
lowValueUsers: number
duplicateUsers: number
pendingUsers: number
addedUsers: number
failedUsers: number
avgSpent: number
conversionRate: number
addSuccessRate: number
duplicateRate: number
dailyGrowth: number
weeklyGrowth: number
monthlyGrowth: number
}
interface AnalyticsDashboardProps {
data: AnalyticsData
}
export function AnalyticsDashboard({ data }: AnalyticsDashboardProps) {
// 计算百分比
const highValuePercentage = (data.highValueUsers / data.totalUsers) * 100
const mediumValuePercentage = (data.mediumValueUsers / data.totalUsers) * 100
const lowValuePercentage = (data.lowValueUsers / data.totalUsers) * 100
// 格式化数字
const formatNumber = (num: number) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + "k"
}
return num.toString()
}
// 格式化百分比
const formatPercentage = (num: number) => {
return num.toFixed(1) + "%"
}
// 获取趋势图标和颜色
const getTrendIcon = (value: number) => {
if (value > 0) {
return <TrendingUp className="h-4 w-4 text-green-500" />
} else if (value < 0) {
return <TrendingDown className="h-4 w-4 text-red-500" />
}
return <div className="h-4 w-4" />
}
const getTrendColor = (value: number) => {
if (value > 0) return "text-green-600"
if (value < 0) return "text-red-600"
return "text-gray-600"
}
return (
<div className="space-y-4">
{/* 核心指标卡片 */}
<div className="grid grid-cols-2 gap-3">
{/* 总用户数 */}
<Card className="p-3">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-blue-600">{formatNumber(data.totalUsers)}</div>
<div className="text-xs text-gray-500"></div>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
<div className="flex items-center mt-2 text-xs">
{getTrendIcon(data.dailyGrowth)}
<span className={getTrendColor(data.dailyGrowth)}>
{data.dailyGrowth > 0 ? "+" : ""}
{data.dailyGrowth}%
</span>
</div>
</Card>
{/* 高价值用户 */}
<Card className="p-3">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-red-600">{formatNumber(data.highValueUsers)}</div>
<div className="text-xs text-gray-500"></div>
</div>
<Star className="h-8 w-8 text-red-500" />
</div>
<div className="flex items-center mt-2 text-xs">
<span className="text-gray-600"> {formatPercentage(highValuePercentage)}</span>
</div>
</Card>
{/* 平均消费 */}
<Card className="p-3">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-green-600">¥{data.avgSpent}</div>
<div className="text-xs text-gray-500"></div>
</div>
<DollarSign className="h-8 w-8 text-green-500" />
</div>
<div className="flex items-center mt-2 text-xs">
{getTrendIcon(data.weeklyGrowth)}
<span className={getTrendColor(data.weeklyGrowth)}>
{data.weeklyGrowth > 0 ? "+" : ""}
{data.weeklyGrowth}%
</span>
</div>
</Card>
{/* 转化率 */}
<Card className="p-3">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-purple-600">{formatPercentage(data.conversionRate)}</div>
<div className="text-xs text-gray-500"></div>
</div>
<Target className="h-8 w-8 text-purple-500" />
</div>
<div className="flex items-center mt-2 text-xs">
{getTrendIcon(data.monthlyGrowth)}
<span className={getTrendColor(data.monthlyGrowth)}>
{data.monthlyGrowth > 0 ? "+" : ""}
{data.monthlyGrowth}%
</span>
</div>
</Card>
</div>
{/* 用户价值分布 */}
<Card className="p-4">
<div className="flex items-center mb-3">
<BarChart3 className="h-5 w-5 mr-2 text-gray-600" />
<h3 className="text-sm font-medium"></h3>
</div>
<div className="space-y-3">
{/* 高价值用户 */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded mr-2"></div>
<span></span>
</div>
<span className="font-medium">
{data.highValueUsers} ({formatPercentage(highValuePercentage)})
</span>
</div>
<Progress value={highValuePercentage} className="h-2" />
</div>
{/* 中价值用户 */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<div className="flex items-center">
<div className="w-3 h-3 bg-blue-500 rounded mr-2"></div>
<span></span>
</div>
<span className="font-medium">
{data.mediumValueUsers} ({formatPercentage(mediumValuePercentage)})
</span>
</div>
<Progress value={mediumValuePercentage} className="h-2" />
</div>
{/* 低价值用户 */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<div className="flex items-center">
<div className="w-3 h-3 bg-gray-500 rounded mr-2"></div>
<span></span>
</div>
<span className="font-medium">
{data.lowValueUsers} ({formatPercentage(lowValuePercentage)})
</span>
</div>
<Progress value={lowValuePercentage} className="h-2" />
</div>
</div>
</Card>
{/* 添加效率分析 */}
<Card className="p-4">
<div className="flex items-center mb-3">
<Target className="h-5 w-5 mr-2 text-gray-600" />
<h3 className="text-sm font-medium"></h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600"></span>
<span className="font-medium text-green-600">{formatPercentage(data.addSuccessRate)}</span>
</div>
<Progress value={data.addSuccessRate} className="h-2" />
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600"></span>
<span className="font-medium text-orange-600">{formatPercentage(data.duplicateRate)}</span>
</div>
<Progress value={data.duplicateRate} className="h-2" />
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t">
<div className="text-center">
<div className="text-lg font-bold text-green-600">{data.addedUsers}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-yellow-600">{data.pendingUsers}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-red-600">{data.failedUsers}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</Card>
{/* 数据质量指标 */}
<Card className="p-4">
<div className="flex items-center mb-3">
<Percent className="h-5 w-5 mr-2 text-gray-600" />
<h3 className="text-sm font-medium"></h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="bg-orange-50 text-orange-800">
{data.duplicateUsers}
</Badge>
<span className="text-sm font-medium">{formatPercentage(data.duplicateRate)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="bg-green-50 text-green-800">
</Badge>
<span className="text-sm font-medium">98.5%</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600"></span>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="bg-blue-50 text-blue-800">
</Badge>
<span className="text-sm font-medium">85.2%</span>
</div>
</div>
</div>
</Card>
{/* 增长趋势 */}
<Card className="p-4">
<div className="flex items-center mb-3">
<TrendingUp className="h-5 w-5 mr-2 text-gray-600" />
<h3 className="text-sm font-medium"></h3>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-gray-50 rounded-md">
<div className="flex items-center justify-center mb-1">
{getTrendIcon(data.dailyGrowth)}
<span className={`text-sm font-medium ml-1 ${getTrendColor(data.dailyGrowth)}`}>
{data.dailyGrowth > 0 ? "+" : ""}
{formatPercentage(data.dailyGrowth)}
</span>
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-md">
<div className="flex items-center justify-center mb-1">
{getTrendIcon(data.weeklyGrowth)}
<span className={`text-sm font-medium ml-1 ${getTrendColor(data.weeklyGrowth)}`}>
{data.weeklyGrowth > 0 ? "+" : ""}
{formatPercentage(data.weeklyGrowth)}
</span>
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-md">
<div className="flex items-center justify-center mb-1">
{getTrendIcon(data.monthlyGrowth)}
<span className={`text-sm font-medium ml-1 ${getTrendColor(data.monthlyGrowth)}`}>
{data.monthlyGrowth > 0 ? "+" : ""}
{formatPercentage(data.monthlyGrowth)}
</span>
</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</Card>
</div>
)
}

View File

@@ -51,4 +51,3 @@ export function DateRangePicker({ className, value, onChange }: DateRangePickerP
</div>
)
}

View File

@@ -0,0 +1,309 @@
"use client"
import { useState, useCallback } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useToast } from "@/components/ui/use-toast"
import { Search, Smartphone, Wifi, WifiOff, Clock, Battery, MapPin, Users, X } from "lucide-react"
interface Device {
id: string
name: string
status: "online" | "offline" | "busy"
battery: number
location: string
wechatAccounts: number
dailyAddLimit: number
todayAdded: number
lastActiveTime: string
model: string
version: string
}
interface MultiDeviceSelectorProps {
devices: Device[]
selectedDevices: string[]
onDeviceSelect: (deviceIds: string[]) => void
open: boolean
onOpenChange: (open: boolean) => void
}
export function MultiDeviceSelector({
devices,
selectedDevices,
onDeviceSelect,
open,
onOpenChange,
}: MultiDeviceSelectorProps) {
const { toast } = useToast()
const [searchQuery, setSearchQuery] = useState("")
const [tempSelectedDevices, setTempSelectedDevices] = useState<string[]>(selectedDevices)
// 过滤设备
const filteredDevices = devices.filter(
(device) =>
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.location.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.model.toLowerCase().includes(searchQuery.toLowerCase()),
)
// 处理设备选择
const handleDeviceToggle = useCallback((deviceId: string, checked: boolean) => {
setTempSelectedDevices((prev) => (checked ? [...prev, deviceId] : prev.filter((id) => id !== deviceId)))
}, [])
// 处理全选
const handleSelectAll = useCallback(
(checked: boolean) => {
if (checked) {
setTempSelectedDevices(filteredDevices.map((device) => device.id))
} else {
setTempSelectedDevices([])
}
},
[filteredDevices],
)
// 处理确认选择
const handleConfirm = () => {
onDeviceSelect(tempSelectedDevices)
onOpenChange(false)
toast({
title: "设备选择成功",
description: `已选择 ${tempSelectedDevices.length} 个设备`,
})
}
// 处理取消
const handleCancel = () => {
setTempSelectedDevices(selectedDevices)
onOpenChange(false)
}
// 获取设备状态图标
const getStatusIcon = (status: string) => {
switch (status) {
case "online":
return <Wifi className="h-4 w-4 text-green-500" />
case "offline":
return <WifiOff className="h-4 w-4 text-gray-500" />
case "busy":
return <Clock className="h-4 w-4 text-yellow-500" />
default:
return <WifiOff className="h-4 w-4 text-gray-500" />
}
}
// 获取设备状态文本
const getStatusText = (status: string) => {
switch (status) {
case "online":
return "在线"
case "offline":
return "离线"
case "busy":
return "忙碌"
default:
return "未知"
}
}
// 获取设备状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "bg-green-100 text-green-800"
case "offline":
return "bg-gray-100 text-gray-800"
case "busy":
return "bg-yellow-100 text-yellow-800"
default:
return "bg-gray-100 text-gray-800"
}
}
// 获取电池颜色
const getBatteryColor = (battery: number) => {
if (battery > 50) return "text-green-600"
if (battery > 20) return "text-yellow-600"
return "text-red-600"
}
// 格式化时间
const formatTime = (timeString: string) => {
const date = new Date(timeString)
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(date)
}
// 设备卡片组件
const DeviceCard = ({ device }: { device: Device }) => {
const isSelected = tempSelectedDevices.includes(device.id)
return (
<Card
className={`p-4 cursor-pointer transition-all ${isSelected ? "border-blue-500 bg-blue-50" : "hover:shadow-md"}`}
onClick={() => handleDeviceToggle(device.id, !isSelected)}
>
<div className="flex items-start space-x-3">
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1 min-w-0">
{/* 设备名称和状态 */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<Smartphone className="h-4 w-4 text-gray-600" />
<span className="font-medium">{device.name}</span>
</div>
<div className="flex items-center space-x-1">
{getStatusIcon(device.status)}
<Badge className={getStatusColor(device.status)} variant="outline">
{getStatusText(device.status)}
</Badge>
</div>
</div>
{/* 设备信息 */}
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600 mb-2">
<div className="flex items-center">
<Battery className={`h-3 w-3 mr-1 ${getBatteryColor(device.battery)}`} />
<span> {device.battery}%</span>
</div>
<div className="flex items-center">
<MapPin className="h-3 w-3 mr-1" />
<span>{device.location}</span>
</div>
<div className="flex items-center">
<Users className="h-3 w-3 mr-1" />
<span>{device.wechatAccounts} </span>
</div>
<div className="flex items-center">
<Clock className="h-3 w-3 mr-1" />
<span>{formatTime(device.lastActiveTime)}</span>
</div>
</div>
{/* 设备型号和版本 */}
<div className="text-xs text-gray-500 mb-2">
{device.model} {device.version}
</div>
{/* 添加限制信息 */}
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500">
: {device.todayAdded}/{device.dailyAddLimit}
</span>
<div className="w-16 bg-gray-200 rounded-full h-1">
<div
className="bg-blue-500 h-1 rounded-full"
style={{ width: `${(device.todayAdded / device.dailyAddLimit) * 100}%` }}
/>
</div>
</div>
</div>
</div>
</Card>
)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* 搜索栏 */}
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索设备名称、位置或型号"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* 操作栏 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
checked={
filteredDevices.length > 0 &&
filteredDevices.every((device) => tempSelectedDevices.includes(device.id))
}
onCheckedChange={handleSelectAll}
id="select-all-devices"
/>
<label htmlFor="select-all-devices" className="text-sm font-medium">
</label>
</div>
<div className="text-sm text-gray-500">
{tempSelectedDevices.length} / {filteredDevices.length}
</div>
</div>
{/* 已选择设备标签 */}
{tempSelectedDevices.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-blue-50 rounded-md">
<span className="text-sm font-medium text-blue-800">:</span>
{tempSelectedDevices.map((deviceId) => {
const device = devices.find((d) => d.id === deviceId)
return device ? (
<Badge
key={deviceId}
variant="outline"
className="bg-white cursor-pointer"
onClick={() => handleDeviceToggle(deviceId, false)}
>
{device.name}
<X className="h-3 w-3 ml-1" />
</Badge>
) : null
})}
</div>
)}
{/* 设备列表 */}
<ScrollArea className="h-[400px]">
<div className="grid gap-3">
{filteredDevices.map((device) => (
<DeviceCard key={device.id} device={device} />
))}
{filteredDevices.length === 0 && (
<div className="text-center py-8 text-gray-500">
{searchQuery ? "未找到匹配的设备" : "暂无可用设备"}
</div>
)}
</div>
</ScrollArea>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
</Button>
<Button onClick={handleConfirm} disabled={tempSelectedDevices.length === 0}>
({tempSelectedDevices.length})
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,420 @@
"use client"
import { useState } from "react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useToast } from "@/components/ui/use-toast"
import { Plus, Edit, Trash2, Users, Calendar, Tag } from "lucide-react"
interface TrafficPool {
id: string
name: string
description: string
userCount: number
tags: string[]
createdAt: string
status: "active" | "inactive"
priority: "high" | "medium" | "low"
}
interface PoolManagementProps {
pools: TrafficPool[]
onPoolCreate: (pool: Omit<TrafficPool, "id" | "createdAt" | "userCount">) => void
onPoolUpdate: (id: string, pool: Partial<TrafficPool>) => void
onPoolDelete: (id: string) => void
}
export function PoolManagement({ pools, onPoolCreate, onPoolUpdate, onPoolDelete }: PoolManagementProps) {
const { toast } = useToast()
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showEditDialog, setShowEditDialog] = useState(false)
const [editingPool, setEditingPool] = useState<TrafficPool | null>(null)
const [formData, setFormData] = useState({
name: "",
description: "",
tags: "",
status: "active" as "active" | "inactive",
priority: "medium" as "high" | "medium" | "low",
})
// 重置表单
const resetForm = () => {
setFormData({
name: "",
description: "",
tags: "",
status: "active",
priority: "medium",
})
}
// 处理创建流量池
const handleCreate = () => {
if (!formData.name.trim()) {
toast({
title: "请输入流量池名称",
variant: "destructive",
})
return
}
const newPool = {
name: formData.name.trim(),
description: formData.description.trim(),
tags: formData.tags
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0),
status: formData.status,
priority: formData.priority,
}
onPoolCreate(newPool)
setShowCreateDialog(false)
resetForm()
toast({
title: "创建成功",
description: `流量池 "${newPool.name}" 已创建`,
})
}
// 处理编辑流量池
const handleEdit = (pool: TrafficPool) => {
setEditingPool(pool)
setFormData({
name: pool.name,
description: pool.description,
tags: pool.tags.join(", "),
status: pool.status,
priority: pool.priority,
})
setShowEditDialog(true)
}
// 处理更新流量池
const handleUpdate = () => {
if (!editingPool || !formData.name.trim()) {
toast({
title: "请输入流量池名称",
variant: "destructive",
})
return
}
const updatedPool = {
name: formData.name.trim(),
description: formData.description.trim(),
tags: formData.tags
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0),
status: formData.status,
priority: formData.priority,
}
onPoolUpdate(editingPool.id, updatedPool)
setShowEditDialog(false)
setEditingPool(null)
resetForm()
toast({
title: "更新成功",
description: `流量池 "${updatedPool.name}" 已更新`,
})
}
// 处理删除流量池
const handleDelete = (pool: TrafficPool) => {
if (pool.userCount > 0) {
toast({
title: "无法删除",
description: "该流量池中还有用户,请先清空用户后再删除",
variant: "destructive",
})
return
}
if (confirm(`确定要删除流量池 "${pool.name}" 吗?此操作不可恢复。`)) {
onPoolDelete(pool.id)
toast({
title: "删除成功",
description: `流量池 "${pool.name}" 已删除`,
})
}
}
// 获取优先级颜色
const getPriorityColor = (priority: string) => {
switch (priority) {
case "high":
return "bg-red-100 text-red-800"
case "medium":
return "bg-blue-100 text-blue-800"
case "low":
return "bg-gray-100 text-gray-800"
default:
return "bg-gray-100 text-gray-800"
}
}
// 获取状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case "active":
return "bg-green-100 text-green-800"
case "inactive":
return "bg-gray-100 text-gray-800"
default:
return "bg-gray-100 text-gray-800"
}
}
// 格式化日期
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date)
}
return (
<div className="space-y-4">
{/* 头部操作栏 */}
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium"></h3>
<Button onClick={() => setShowCreateDialog(true)} size="sm">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
{/* 流量池列表 */}
<div className="grid gap-4">
{pools.map((pool) => (
<Card key={pool.id} className="p-4">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1">
<h4 className="font-medium text-lg">{pool.name}</h4>
<Badge className={getStatusColor(pool.status)}>{pool.status === "active" ? "活跃" : "停用"}</Badge>
<Badge className={getPriorityColor(pool.priority)}>
{pool.priority === "high" ? "高优先级" : pool.priority === "medium" ? "中优先级" : "低优先级"}
</Badge>
</div>
<p className="text-sm text-gray-600 mb-2">{pool.description}</p>
<div className="flex flex-wrap gap-1 mb-2">
{pool.tags.map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
<Tag className="h-3 w-3 mr-1" />
{tag}
</Badge>
))}
</div>
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm" onClick={() => handleEdit(pool)}>
<Edit className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => handleDelete(pool)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
{/* 统计信息 */}
<div className="flex items-center justify-between text-sm text-gray-500 pt-3 border-t">
<div className="flex items-center space-x-4">
<div className="flex items-center">
<Users className="h-4 w-4 mr-1" />
<span>{pool.userCount} </span>
</div>
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-1" />
<span> {formatDate(pool.createdAt)}</span>
</div>
</div>
</div>
</Card>
))}
{pools.length === 0 && (
<Card className="p-8 text-center">
<div className="text-gray-500 mb-4"></div>
<Button onClick={() => setShowCreateDialog(true)} variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</Card>
)}
</div>
{/* 创建流量池对话框 */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input
placeholder="请输入流量池名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Textarea
placeholder="请输入流量池描述"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
placeholder="请输入标签,用逗号分隔"
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
/>
<div className="text-xs text-gray-500">,,</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={formData.status}
onValueChange={(value: "active" | "inactive") => setFormData({ ...formData, status: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={formData.priority}
onValueChange={(value: "high" | "medium" | "low") => setFormData({ ...formData, priority: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
</Button>
<Button onClick={handleCreate}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 编辑流量池对话框 */}
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input
placeholder="请输入流量池名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Textarea
placeholder="请输入流量池描述"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
placeholder="请输入标签,用逗号分隔"
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
/>
<div className="text-xs text-gray-500">,,</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={formData.status}
onValueChange={(value: "active" | "inactive") => setFormData({ ...formData, status: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Select
value={formData.priority}
onValueChange={(value: "high" | "medium" | "low") => setFormData({ ...formData, priority: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowEditDialog(false)}>
</Button>
<Button onClick={handleUpdate}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -30,4 +30,3 @@ export function PoolUsageChart({ deviceStats, poolLimit }: PoolUsageChartProps)
</Card>
)
}

View File

@@ -1,4 +1,3 @@
export default function Loading() {
return null
}

File diff suppressed because it is too large Load Diff