场景获客界面优化及设备选择

This commit is contained in:
wong
2025-06-11 15:30:37 +08:00
parent 7cefc2b189
commit 77a2247cc3
4 changed files with 147 additions and 312 deletions

View File

@@ -1,34 +1,26 @@
"use client"
import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Search, Filter, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Search, RefreshCw, Filter } from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ImeiDisplay } from "@/components/ImeiDisplay"
interface WechatAccount {
wechatId: string
nickname: string
remainingAdds: number
maxDailyAdds: number
todayAdded: number
}
import { Checkbox } from "@/components/ui/checkbox"
import { api } from "@/lib/api"
interface Device {
id: string
imei: string
name: string
imei: string
status: "online" | "offline"
wechatAccounts: WechatAccount[]
usedInPlans: number
tags?: string[]
wechatAccounts: {
wechatId: string
nickname: string
remainingAdds: number
maxDailyAdds: number
}[]
}
interface DeviceSelectionDialogProps {
@@ -36,117 +28,61 @@ interface DeviceSelectionDialogProps {
onOpenChange: (open: boolean) => void
selectedDevices: string[]
onSelect: (deviceIds: string[]) => void
excludeUsedDevices?: boolean
}
export function DeviceSelectionDialog({
open,
onOpenChange,
selectedDevices,
onSelect,
excludeUsedDevices = false,
}: DeviceSelectionDialogProps) {
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
const [devices, setDevices] = useState<Device[]>([])
const [loading, setLoading] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [tagFilter, setTagFilter] = useState("all")
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([])
const [activeTab, setActiveTab] = useState("all")
// 初始化已选设备
useEffect(() => {
if (open) {
setSelectedDeviceIds(selectedDevices)
}
if (open) setSelectedDeviceIds(selectedDevices)
}, [open, selectedDevices])
// 模拟获取设备数据
useEffect(() => {
if (!open) return
const fetchDevices = async () => {
setLoading(true)
try {
// 模拟API请求
await new Promise((resolve) => setTimeout(resolve, 800))
// 生成模拟数据
const deviceTags = ["高性能", "稳定", "新设备", "已配置", "测试中", "备用"]
const mockDevices: Device[] = Array.from({ length: 30 }, (_, i) => {
// 随机生成1-3个标签
const tags = Array.from(
{ length: Math.floor(Math.random() * 3) + 1 },
() => deviceTags[Math.floor(Math.random() * deviceTags.length)],
)
// 确保标签唯一
const uniqueTags = Array.from(new Set(tags))
return {
id: `device-${i + 1}`,
imei: `IMEI-${Math.random().toString(36).substr(2, 9)}`,
name: `设备 ${i + 1}`,
status: Math.random() > 0.3 ? "online" : "offline",
wechatAccounts: Array.from({ length: Math.floor(Math.random() * 2) + 1 }, (_, j) => ({
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
nickname: `微信号 ${j + 1}`,
remainingAdds: Math.floor(Math.random() * 10) + 5,
maxDailyAdds: 20,
todayAdded: Math.floor(Math.random() * 15),
})),
usedInPlans: Math.floor(Math.random() * 3),
tags: uniqueTags,
}
})
setDevices(mockDevices)
} catch (error) {
console.error("Failed to fetch devices:", error)
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([])
} finally {
setLoading(false)
}
}
fetchDevices()
}, [open])
}, [open, searchQuery, statusFilter])
// 过滤设备
const filteredDevices = devices.filter((device) => {
const matchesSearch =
searchQuery === "" ||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.wechatAccounts.some(
(account) =>
account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) ||
account.nickname.toLowerCase().includes(searchQuery.toLowerCase()),
)
const matchesStatus = statusFilter === "all" || device.status === statusFilter
const matchesUsage = !excludeUsedDevices || device.usedInPlans === 0
const matchesTag = tagFilter === "all" || (device.tags && device.tags.includes(tagFilter))
const matchesTab =
activeTab === "all" ||
(activeTab === "online" && device.status === "online") ||
(activeTab === "offline" && device.status === "offline") ||
(activeTab === "unused" && device.usedInPlans === 0)
return matchesSearch && matchesStatus && matchesUsage && matchesTag && matchesTab
})
// 处理选择设备
const handleSelectDevice = (deviceId: string) => {
setSelectedDeviceIds((prev) =>
prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId],
prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId]
)
}
// 处理全选
const handleSelectAll = () => {
if (selectedDeviceIds.length === filteredDevices.length) {
setSelectedDeviceIds([])
@@ -155,189 +91,114 @@ export function DeviceSelectionDialog({
}
}
// 处理确认选择
const handleConfirm = () => {
onSelect(selectedDeviceIds)
onOpenChange(false)
}
// 获取所有标签选项
const allTags = Array.from(new Set(devices.flatMap((device) => device.tags || [])))
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
})
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
<DialogContent className="max-w-xl w-full p-0 rounded-2xl shadow-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle className="text-lg font-bold text-center py-3 border-b"></DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col">
{/* 搜索和筛选区域 */}
<div className="space-y-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索设备IMEI/备注/微信号"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{/* 分类标签页 */}
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="online">线</TabsTrigger>
<TabsTrigger value="offline">线</TabsTrigger>
<TabsTrigger value="unused">使</TabsTrigger>
</TabsList>
</Tabs>
{/* 筛选器 */}
<div className="flex space-x-2">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="online">线</SelectItem>
<SelectItem value="offline">线</SelectItem>
</SelectContent>
</Select>
{allTags.length > 0 && (
<Select value={tagFilter} onValueChange={setTagFilter}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="标签" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{allTags.map((tag) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<Button variant="outline" className="ml-auto" onClick={handleSelectAll}>
{selectedDeviceIds.length === filteredDevices.length && filteredDevices.length > 0
? "取消全选"
: "全选"}
</Button>
</div>
<div className="p-6 pt-4">
{/* 搜索和筛选 */}
<div className="flex items-center gap-2 mb-4">
<Input
placeholder="搜索设备IMEI/备注/微信号"
value={searchQuery}
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>
</div>
{/* 设备列表 */}
<ScrollArea className="flex-1">
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-2">
{loading ? (
<div className="flex items-center justify-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<div className="text-center text-gray-400 py-8">...</div>
) : filteredDevices.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{searchQuery || statusFilter !== "all" || tagFilter !== "all" || activeTab !== "all"
? "没有符合条件的设备"
: "暂无设备数据"}
</div>
<div className="text-center text-gray-400 py-8"></div>
) : (
<div className="space-y-2 pr-4">
{filteredDevices.map((device) => (
<Card
filteredDevices.map(device => {
const checked = selectedDeviceIds.includes(device.id)
const wx = device.wechatAccounts[0] || {}
return (
<label
key={device.id}
className={`p-3 hover:shadow-md transition-shadow ${
selectedDeviceIds.includes(device.id) ? "border-primary" : ""
}`}
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
`}
>
<div className="flex items-center space-x-3">
<Checkbox
checked={selectedDeviceIds.includes(device.id)}
onCheckedChange={() => handleSelectDevice(device.id)}
id={`device-${device.id}`}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<label htmlFor={`device-${device.id}`} className="font-medium truncate cursor-pointer">
{device.name || "未命名设备"}
</label>
<div
className={`px-2 py-1 rounded-full text-xs ${
device.status === "online" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{device.status === "online" ? "在线" : "离线"}
</div>
</div>
<div className="text-sm text-gray-500 flex items-center">
<span className="mr-1">IMEI:</span>
<ImeiDisplay imei={device.imei} containerWidth={160} />
</div>
{/* 微信账号信息 */}
<div className="mt-2 space-y-2">
{device.wechatAccounts.map((account) => (
<div key={account.wechatId} className="bg-gray-50 rounded-lg p-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{account.nickname}</span>
<span className="text-gray-500">{account.wechatId}</span>
</div>
<div className="mt-1">
<div className="flex items-center justify-between text-sm">
<span>{account.remainingAdds}</span>
<span className="text-sm text-gray-500">
{account.todayAdded}/{account.maxDailyAdds}
</span>
</div>
</div>
</div>
))}
</div>
{/* 标签展示 */}
{device.tags && device.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{device.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
{device.usedInPlans > 0 && (
<div className="text-sm text-orange-500 mt-2"> {device.usedInPlans} </div>
)}
</div>
<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>
</div>
</Card>
))}
</div>
<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>
)
})
)}
</ScrollArea>
</div>
<DialogFooter className="flex items-center justify-between pt-4 border-t">
<div className="text-sm">
<span className="font-medium text-primary">{selectedDeviceIds.length}</span>
</div>
<div className="space-x-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
{/* 确认按钮 */}
<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)
}}
>
</Button>
<Button onClick={handleConfirm}></Button>
</div>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
)

View File

@@ -113,8 +113,8 @@ const tagColorPoolDark = [
"bg-purple-500 text-white",
"bg-red-500 text-white",
"bg-orange-500 text-white",
"bg-yellow-400 text-white",
"bg-gray-700 text-white",
"bg-yellow-500 text-white",
"bg-gray-500 text-white",
"bg-pink-500 text-white",
];
function getTagColorIdx(tag: string) {

View File

@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
import { Alert, AlertDescription } from "@/components/ui/alert"
import { ChevronsUpDown } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
import { DeviceSelectionDialog } from "../../../components/device-selection-dialog"
interface FriendRequestSettingsProps {
formData: any
@@ -49,7 +50,9 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
const [hasWarnings, setHasWarnings] = useState(false)
const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false)
const [selectedDevices, setSelectedDevices] = useState<any[]>(formData.selectedDevices || [])
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>(
(formData.selectedDevices || []).map((d: any) => d.id || d)
)
// 获取场景标题
const getScenarioTitle = () => {
@@ -96,18 +99,11 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
onNext()
}
const toggleDeviceSelection = (device: any) => {
const isSelected = selectedDevices.some((d) => d.id === device.id)
let newSelectedDevices
if (isSelected) {
newSelectedDevices = selectedDevices.filter((d) => d.id !== device.id)
} else {
newSelectedDevices = [...selectedDevices, device]
}
setSelectedDevices(newSelectedDevices)
onChange({ ...formData, selectedDevices: newSelectedDevices })
// 设备选择回填
const handleDeviceSelect = (deviceIds: string[]) => {
setSelectedDeviceIds(deviceIds)
// 只存id或如需完整对象可自行扩展
onChange({ ...formData, selectedDevices: deviceIds })
}
return (
@@ -119,40 +115,18 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
<Button
variant="outline"
className="w-full justify-between"
onClick={() => setIsDeviceSelectorOpen(!isDeviceSelectorOpen)}
onClick={() => setIsDeviceSelectorOpen(true)}
>
{selectedDevices.length ? `已选择 ${selectedDevices.length} 个设备` : "选择设备"}
{selectedDeviceIds.length ? `已选择 ${selectedDeviceIds.length} 个设备` : "选择设备"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
{isDeviceSelectorOpen && (
<div className="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg">
<div className="p-2">
<Input placeholder="搜索设备..." className="mb-2" />
<div className="max-h-60 overflow-auto">
{mockDevices.map((device) => (
<div
key={device.id}
className="flex items-center justify-between p-2 hover:bg-gray-100 cursor-pointer"
onClick={() => toggleDeviceSelection(device)}
>
<div className="flex items-center space-x-2">
<Checkbox
checked={selectedDevices.some((d) => d.id === device.id)}
onCheckedChange={() => toggleDeviceSelection(device)}
/>
<span>{device.name}</span>
</div>
<span className={`text-xs ${device.status === "online" ? "text-green-500" : "text-gray-400"}`}>
{device.status === "online" ? "在线" : "离线"}
</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
<DeviceSelectionDialog
open={isDeviceSelectorOpen}
onOpenChange={setIsDeviceSelectorOpen}
selectedDevices={selectedDeviceIds}
onSelect={handleDeviceSelect}
/>
</div>
<div>
@@ -239,7 +213,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
</div>
{hasWarnings && (
<Alert variant="warning" className="bg-amber-50 border-amber-200">
<Alert variant="destructive" className="bg-amber-50 border-amber-200">
<AlertCircle className="h-4 w-4 text-amber-500" />
<AlertDescription></AlertDescription>
</Alert>

View File

@@ -206,7 +206,7 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de
<Button variant="outline" onClick={onBack}>
</Button>
<Button onClick={handleSubmit} disabled={selectedPools.length === 0 || isSubmitting}>
<Button onClick={handleSubmit}>
{isSubmitting ? "提交中..." : "完成"}
</Button>
</div>