Files
cunkebao_v3/Cunkebao/app/traffic-pool/components/multi-device-selector.tsx
2025-07-07 17:08:27 +08:00

310 lines
10 KiB
TypeScript

"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>
)
}