2025-03-29 16:50:39 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react"
|
2025-06-11 15:30:37 +08:00
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
2025-03-29 16:50:39 +08:00
|
|
|
|
import { Input } from "@/components/ui/input"
|
2025-06-11 15:30:37 +08:00
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
|
import { Search, RefreshCw, Filter } from "lucide-react"
|
2025-03-29 16:50:39 +08:00
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
2025-06-11 15:30:37 +08:00
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
|
|
|
|
import { api } from "@/lib/api"
|
2025-03-29 16:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
interface Device {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
2025-06-11 15:30:37 +08:00
|
|
|
|
imei: string
|
2025-03-29 16:50:39 +08:00
|
|
|
|
status: "online" | "offline"
|
2025-06-11 15:30:37 +08:00
|
|
|
|
wechatAccounts: {
|
|
|
|
|
|
wechatId: string
|
|
|
|
|
|
nickname: string
|
|
|
|
|
|
remainingAdds: number
|
|
|
|
|
|
maxDailyAdds: number
|
|
|
|
|
|
}[]
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface DeviceSelectionDialogProps {
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
|
selectedDevices: string[]
|
|
|
|
|
|
onSelect: (deviceIds: string[]) => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-11 15:30:37 +08:00
|
|
|
|
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
|
2025-03-29 16:50:39 +08:00
|
|
|
|
const [devices, setDevices] = useState<Device[]>([])
|
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState("all")
|
|
|
|
|
|
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-06-11 15:30:37 +08:00
|
|
|
|
if (open) setSelectedDeviceIds(selectedDevices)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}, [open, selectedDevices])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!open) return
|
|
|
|
|
|
const fetchDevices = async () => {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
2025-06-11 15:30:37 +08:00
|
|
|
|
const params = []
|
|
|
|
|
|
if (searchQuery) params.push(`keyword=${encodeURIComponent(searchQuery)}`)
|
|
|
|
|
|
if (statusFilter !== "all") params.push(`status=${statusFilter}`)
|
|
|
|
|
|
params.push("page=1", "limit=100")
|
|
|
|
|
|
const url = `/v1/devices?${params.join("&")}`
|
|
|
|
|
|
const response = await api.get<any>(url)
|
|
|
|
|
|
const list = response.data?.list || response.data?.items || []
|
|
|
|
|
|
const devices = list.map((device: any) => ({
|
|
|
|
|
|
id: device.id?.toString() || device.id,
|
|
|
|
|
|
imei: device.imei || "",
|
|
|
|
|
|
name: device.memo || device.name || `设备_${device.id}`,
|
|
|
|
|
|
status: device.alive === 1 || device.status === "online" ? "online" : "offline",
|
|
|
|
|
|
wechatAccounts: [
|
|
|
|
|
|
{
|
|
|
|
|
|
wechatId: device.wechatId || device.wxid || "",
|
|
|
|
|
|
nickname: device.nickname || "",
|
|
|
|
|
|
remainingAdds: device.remainingAdds || 0,
|
|
|
|
|
|
maxDailyAdds: device.maxDailyAdds || 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}))
|
|
|
|
|
|
setDevices(devices)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setDevices([])
|
2025-03-29 16:50:39 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchDevices()
|
2025-06-11 15:30:37 +08:00
|
|
|
|
}, [open, searchQuery, statusFilter])
|
2025-03-29 16:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
const handleSelectDevice = (deviceId: string) => {
|
|
|
|
|
|
setSelectedDeviceIds((prev) =>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId]
|
2025-03-29 16:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectAll = () => {
|
|
|
|
|
|
if (selectedDeviceIds.length === filteredDevices.length) {
|
|
|
|
|
|
setSelectedDeviceIds([])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedDeviceIds(filteredDevices.map((device) => device.id))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleConfirm = () => {
|
|
|
|
|
|
onSelect(selectedDeviceIds)
|
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-11 15:30:37 +08:00
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setSelectedDeviceIds(selectedDevices)
|
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filteredDevices = devices.filter((device) => {
|
|
|
|
|
|
const searchLower = searchQuery.toLowerCase()
|
|
|
|
|
|
const matchesSearch =
|
|
|
|
|
|
(device.name || '').toLowerCase().includes(searchLower) ||
|
|
|
|
|
|
(device.imei || '').toLowerCase().includes(searchLower) ||
|
|
|
|
|
|
(device.wechatAccounts[0]?.wechatId || '').toLowerCase().includes(searchLower)
|
|
|
|
|
|
const matchesStatus =
|
|
|
|
|
|
statusFilter === "all" ||
|
|
|
|
|
|
(statusFilter === "online" && device.status === "online") ||
|
|
|
|
|
|
(statusFilter === "offline" && device.status === "offline")
|
|
|
|
|
|
return matchesSearch && matchesStatus
|
|
|
|
|
|
})
|
2025-03-29 16:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<DialogContent className="max-w-xl w-full p-0 rounded-2xl shadow-2xl max-h-[80vh]">
|
2025-03-29 16:50:39 +08:00
|
|
|
|
<DialogHeader>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<DialogTitle className="text-lg font-bold text-center py-3 border-b">选择设备</DialogTitle>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
</DialogHeader>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<div className="p-6 pt-4">
|
|
|
|
|
|
{/* 搜索和筛选 */}
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
2025-06-17 15:56:02 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索设备IMEI/备注/微信号"
|
|
|
|
|
|
value={searchQuery}
|
2025-06-11 15:30:37 +08:00
|
|
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
|
|
|
|
className="flex-1 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="border rounded-lg px-3 py-2 text-sm bg-gray-50 focus:border-blue-500"
|
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
|
onChange={e => setStatusFilter(e.target.value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="all">全部状态</option>
|
|
|
|
|
|
<option value="online">在线</option>
|
|
|
|
|
|
<option value="offline">离线</option>
|
|
|
|
|
|
</select>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
{/* 设备列表 */}
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-2">
|
2025-03-29 16:50:39 +08:00
|
|
|
|
{loading ? (
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<div className="text-center text-gray-400 py-8">加载中...</div>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
) : filteredDevices.length === 0 ? (
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<div className="text-center text-gray-400 py-8">暂无设备</div>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
) : (
|
2025-06-11 15:30:37 +08:00
|
|
|
|
filteredDevices.map(device => {
|
|
|
|
|
|
const checked = selectedDeviceIds.includes(device.id)
|
|
|
|
|
|
const wx = device.wechatAccounts[0] || {}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<label
|
2025-03-29 16:50:39 +08:00
|
|
|
|
key={device.id}
|
2025-06-11 15:30:37 +08:00
|
|
|
|
className={`
|
|
|
|
|
|
flex items-center gap-3 p-4 rounded-xl border
|
|
|
|
|
|
${checked ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white"}
|
|
|
|
|
|
hover:border-blue-400 transition-colors cursor-pointer
|
|
|
|
|
|
`}
|
2025-03-29 16:50:39 +08:00
|
|
|
|
>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
className="accent-blue-500 scale-110"
|
|
|
|
|
|
checked={checked}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setSelectedDeviceIds(prev =>
|
|
|
|
|
|
prev.includes(device.id)
|
|
|
|
|
|
? prev.filter(id => id !== device.id)
|
|
|
|
|
|
: [...prev, device.id]
|
|
|
|
|
|
)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<div className="font-semibold text-base">{device.name}</div>
|
|
|
|
|
|
<div className="text-xs text-gray-500">IMEI: {device.imei}</div>
|
|
|
|
|
|
<div className="text-xs text-gray-400">微信号: {wx.wechatId || '--'}({wx.nickname || '--'})</div>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
</div>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
<span className="flex items-center gap-1 text-xs font-medium">
|
|
|
|
|
|
<span className={`w-2 h-2 rounded-full ${device.status === 'online' ? 'bg-green-500' : 'bg-gray-300'}`}></span>
|
|
|
|
|
|
<span className={device.status === 'online' ? 'text-green-600' : 'text-gray-400'}>
|
|
|
|
|
|
{device.status === 'online' ? '在线' : '离线'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
2025-03-29 16:50:39 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
{/* 确认按钮 */}
|
|
|
|
|
|
<div className="flex justify-center mt-8">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className="w-4/5 py-3 rounded-full text-base font-bold shadow-md"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
onSelect(selectedDeviceIds)
|
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
确认
|
2025-03-29 16:50:39 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-06-11 15:30:37 +08:00
|
|
|
|
</div>
|
2025-03-29 16:50:39 +08:00
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|