feat: 本次提交更新内容如下
场景获客列表搞定
This commit is contained in:
@@ -461,4 +461,3 @@ export default function AnalysisReportPage({ params }: { params: { id: string }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -280,4 +280,3 @@ export function AnalysisSettings({ formData, updateFormData, onNext, onBack }: A
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -177,4 +177,3 @@ export function ConfirmAnalysis({ formData, updateFormData, onSubmit, onBack }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -212,4 +212,3 @@ export function DeviceSelection({ formData, updateFormData, onNext }: DeviceSele
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,4 +40,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -162,4 +162,3 @@ export function BasicSettings({ formData, updateFormData, onNext }: BasicSetting
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -394,4 +394,3 @@ export function TargetSelection({ formData, updateFormData, onSubmit, onBack }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"
|
||||
import { BasicSettings } from "./components/basic-settings"
|
||||
import { TargetSelection } from "./components/target-selection"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Steps, Step } from "@/app/components/ui/steps"
|
||||
import { Steps, Step } from "@/components/ui/steps"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export default function CreateAnalyzerPage() {
|
||||
@@ -100,4 +100,3 @@ export default function CreateAnalyzerPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,4 +121,3 @@ export function BasicSettings({ formData, updateFormData, onNext }: BasicSetting
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -394,4 +394,3 @@ export function TargetSelection({ formData, updateFormData, onSubmit, onBack }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,4 +114,3 @@ export default function CreateAnalysisPlanPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -365,4 +365,3 @@ export default function AIAnalyzerPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -281,4 +281,3 @@ export default function KnowledgeBasePage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -270,4 +270,3 @@ export default function AIAssistantPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -341,4 +341,3 @@ export default function StrategyReportPage({ params }: { params: { id: string }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -232,4 +232,3 @@ export function ConfirmStrategy({ formData, updateFormData, onSubmit, onBack }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -200,4 +200,3 @@ export function ExecutionSetup({ formData, updateFormData, onNext, onBack }: Exe
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,4 +55,3 @@ export function StepIndicator({ steps, currentStep }: StepIndicatorProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -319,4 +319,3 @@ export function StrategySelection({ formData, updateFormData, onNext, onBack }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -291,4 +291,3 @@ export function TrafficPoolSelection({ formData, updateFormData, onNext }: Traff
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,4 +126,3 @@ export default function NewStrategyPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -368,4 +368,3 @@ export default function AIStrategyPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -184,4 +184,3 @@ export function StepByStepPlanForm({ onClose }: StepByStepPlanFormProps) {
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,4 +82,3 @@ export class AutoGroupService {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,4 +126,3 @@ export const columns: ColumnDef<Plan>[] = [
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -93,4 +93,3 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, CheckCircle2, AlertCircle, Smartphone, Laptop } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Search, Filter, Smartphone, Loader2, CheckCircle2 } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline" | "busy"
|
||||
lastActive: string
|
||||
type: "android" | "ios"
|
||||
number?: string
|
||||
}
|
||||
|
||||
// 模拟设备数据
|
||||
const mockDevices = Array.from({ length: 20 }).map((_, i) => ({
|
||||
id: `device-${i + 1}`,
|
||||
name: `设备 ${i + 1}`,
|
||||
model: i % 3 === 0 ? "iPhone 13" : i % 3 === 1 ? "Xiaomi 12" : "Huawei P40",
|
||||
status: i % 5 === 0 ? "offline" : i % 7 === 0 ? "busy" : "online",
|
||||
account: `wxid_${100 + i}`,
|
||||
type: i % 2 === 0 ? "mobile" : "emulator",
|
||||
group: i % 3 === 0 ? "常用设备" : i % 3 === 1 ? "备用设备" : "测试设备",
|
||||
successRate: Math.floor(70 + Math.random() * 30),
|
||||
restrictCount: Math.floor(Math.random() * 5),
|
||||
}))
|
||||
const mockDevices: Device[] = [
|
||||
{
|
||||
id: "device-1",
|
||||
name: "设备1",
|
||||
status: "online",
|
||||
lastActive: "2023-05-15 14:30",
|
||||
type: "android",
|
||||
number: "13812345678",
|
||||
},
|
||||
{
|
||||
id: "device-2",
|
||||
name: "设备2",
|
||||
status: "offline",
|
||||
lastActive: "2023-05-14 09:15",
|
||||
type: "ios",
|
||||
number: "13987654321",
|
||||
},
|
||||
{
|
||||
id: "device-3",
|
||||
name: "设备3",
|
||||
status: "busy",
|
||||
lastActive: "2023-05-15 11:45",
|
||||
type: "android",
|
||||
number: "15612345678",
|
||||
},
|
||||
{
|
||||
id: "device-4",
|
||||
name: "设备4",
|
||||
status: "online",
|
||||
lastActive: "2023-05-15 13:20",
|
||||
type: "android",
|
||||
number: "18712345678",
|
||||
},
|
||||
{
|
||||
id: "device-5",
|
||||
name: "设备5",
|
||||
status: "online",
|
||||
lastActive: "2023-05-15 10:05",
|
||||
type: "ios",
|
||||
number: "13612345678",
|
||||
},
|
||||
]
|
||||
|
||||
interface DeviceSelectionProps {
|
||||
onNext: () => void
|
||||
@@ -37,75 +77,83 @@ export function DeviceSelection({
|
||||
initialSelectedDevices = [],
|
||||
onDevicesChange,
|
||||
}: DeviceSelectionProps) {
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialSelectedDevices)
|
||||
const isMobile = useMobile()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [typeFilter, setTypeFilter] = useState<string>("all")
|
||||
const [groupFilter, setGroupFilter] = useState<string>("all")
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialSelectedDevices)
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline" | "busy">("all")
|
||||
|
||||
// 使用ref来跟踪是否已经通知了父组件初始选择
|
||||
const initialNotificationRef = useRef(false)
|
||||
|
||||
const deviceGroups = Array.from(new Set(mockDevices.map((device) => device.group)))
|
||||
|
||||
const filteredDevices = mockDevices.filter((device) => {
|
||||
const matchesSearch =
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.account.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || device.status === statusFilter
|
||||
const matchesType = typeFilter === "all" || device.type === typeFilter
|
||||
const matchesGroup = groupFilter === "all" || device.group === groupFilter
|
||||
|
||||
return matchesSearch && matchesStatus && matchesType && matchesGroup
|
||||
})
|
||||
|
||||
// 只在选择变化时通知父组件,使用防抖
|
||||
useEffect(() => {
|
||||
if (!initialNotificationRef.current) {
|
||||
initialNotificationRef.current = true
|
||||
return
|
||||
const fetchDevices = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
setDevices(mockDevices)
|
||||
} catch (error) {
|
||||
console.error("获取设备失败:", error)
|
||||
setError("获取设备列表失败,请稍后重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
onDevicesChange(selectedDevices)
|
||||
}, 300)
|
||||
fetchDevices()
|
||||
}, [])
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [selectedDevices, onDevicesChange])
|
||||
useEffect(() => {
|
||||
// 只在设备选择变化时通知父组件,而不是每次渲染
|
||||
const currentSelection = JSON.stringify(selectedDevices)
|
||||
const prevSelection = JSON.stringify(initialSelectedDevices)
|
||||
|
||||
if (currentSelection !== prevSelection) {
|
||||
onDevicesChange(selectedDevices)
|
||||
}
|
||||
}, [selectedDevices, initialSelectedDevices, onDevicesChange])
|
||||
|
||||
const handleDeviceToggle = (deviceId: string) => {
|
||||
setSelectedDevices((prev) => (prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId]))
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setSelectedDevices(filteredDevices.map((device) => device.id))
|
||||
}
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setSelectedDevices([])
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "bg-green-500"
|
||||
case "offline":
|
||||
return "bg-gray-500"
|
||||
case "busy":
|
||||
return "bg-yellow-500"
|
||||
default:
|
||||
return "bg-gray-500"
|
||||
if (selectedDevices.length === filteredDevices.length) {
|
||||
setSelectedDevices([])
|
||||
} else {
|
||||
setSelectedDevices(filteredDevices.map((device) => device.id))
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const filteredDevices = devices.filter((device) => {
|
||||
const matchesSearch =
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(device.number && device.number.includes(searchQuery))
|
||||
const matchesStatus = statusFilter === "all" || device.status === statusFilter
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
const getStatusColor = (status: Device["status"]) => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "bg-green-500/10 text-green-500"
|
||||
case "offline":
|
||||
return "bg-gray-500/10 text-gray-500"
|
||||
case "busy":
|
||||
return "bg-yellow-500/10 text-yellow-500"
|
||||
default:
|
||||
return "bg-gray-500/10 text-gray-500"
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: Device["status"]) => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "在线"
|
||||
case "offline":
|
||||
return "离线"
|
||||
case "busy":
|
||||
return "繁忙"
|
||||
return "忙碌"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
@@ -116,201 +164,105 @@ export function DeviceSelection({
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-medium">选择设备</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleSelectAll} disabled={filteredDevices.length === 0}>
|
||||
{selectedDevices.length === filteredDevices.length && filteredDevices.length > 0
|
||||
? "取消全选"
|
||||
: "全选"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索设备名称或账号"
|
||||
placeholder="搜索设备名称或手机号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<SelectItem value="busy">繁忙</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="设备类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有类型</SelectItem>
|
||||
<SelectItem value="mobile">手机设备</SelectItem>
|
||||
<SelectItem value="emulator">模拟器</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={groupFilter} onValueChange={setGroupFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="设备分组" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有分组</SelectItem>
|
||||
{deviceGroups.map((group) => (
|
||||
<SelectItem key={group} value={group}>
|
||||
{group}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 <span className="font-medium text-blue-600">{selectedDevices.length}</span> 台设备
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleSelectAll}>
|
||||
全选
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleDeselectAll}>
|
||||
取消全选
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-gray-400" />
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="online">在线</option>
|
||||
<option value="offline">离线</option>
|
||||
<option value="busy">忙碌</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="list" className="w-full">
|
||||
<TabsList className="grid w-60 grid-cols-2">
|
||||
<TabsTrigger value="list">列表视图</TabsTrigger>
|
||||
<TabsTrigger value="grid">网格视图</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="list" className="mt-2">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<div className="divide-y">
|
||||
{filteredDevices.map((device) => (
|
||||
<div key={device.id} className="flex items-center justify-between p-3 hover:bg-gray-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={device.id}
|
||||
checked={selectedDevices.includes(device.id)}
|
||||
onCheckedChange={() => handleDeviceToggle(device.id)}
|
||||
disabled={device.status === "offline"}
|
||||
/>
|
||||
{device.type === "mobile" ? (
|
||||
<Smartphone className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<Laptop className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium">{device.name}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{device.model} | {device.account}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-sm">
|
||||
成功率:{" "}
|
||||
<span className={device.successRate > 90 ? "text-green-600" : "text-yellow-600"}>
|
||||
{device.successRate}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
限制次数:{" "}
|
||||
<span className={device.restrictCount === 0 ? "text-green-600" : "text-red-600"}>
|
||||
{device.restrictCount}
|
||||
</span>
|
||||
</div>
|
||||
<Badge className={`${getStatusColor(device.status)} text-white`}>
|
||||
{getStatusText(device.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="p-4 text-center text-gray-500">没有找到符合条件的设备</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="grid" className="mt-2">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||
{filteredDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className={`border rounded-md p-3 ${
|
||||
selectedDevices.includes(device.id) ? "border-blue-500 bg-blue-50" : ""
|
||||
} ${device.status === "offline" ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center">
|
||||
{device.type === "mobile" ? (
|
||||
<Smartphone className="h-4 w-4 text-gray-500 mr-1" />
|
||||
) : (
|
||||
<Laptop className="h-4 w-4 text-gray-500 mr-1" />
|
||||
)}
|
||||
<span className="font-medium truncate">{device.name}</span>
|
||||
</div>
|
||||
<Badge className={`${getStatusColor(device.status)} text-white text-xs`}>
|
||||
{getStatusText(device.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 mb-2">{device.model}</div>
|
||||
<div className="text-xs text-gray-500 mb-3">{device.account}</div>
|
||||
|
||||
<div className="flex justify-between text-xs mb-3">
|
||||
<div>
|
||||
成功率:{" "}
|
||||
<span className={device.successRate > 90 ? "text-green-600" : "text-yellow-600"}>
|
||||
{device.successRate}%
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
限制:{" "}
|
||||
<span className={device.restrictCount === 0 ? "text-green-600" : "text-red-600"}>
|
||||
{device.restrictCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={selectedDevices.includes(device.id) ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => handleDeviceToggle(device.id)}
|
||||
disabled={device.status === "offline"}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500 mr-2" />
|
||||
<span>正在加载设备列表...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : filteredDevices.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">未找到匹配的设备</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
|
||||
{filteredDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className={`border rounded-lg p-3 cursor-pointer transition-colors ${
|
||||
selectedDevices.includes(device.id)
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50/50"
|
||||
}`}
|
||||
onClick={() => handleDeviceToggle(device.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-0.5">
|
||||
{selectedDevices.includes(device.id) ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1" /> 已选择
|
||||
</>
|
||||
<CheckCircle2 className="h-5 w-5 text-blue-500" />
|
||||
) : (
|
||||
"选择设备"
|
||||
<div className="h-5 w-5 rounded-full border-2 border-gray-300" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium flex items-center">
|
||||
{device.name}
|
||||
<Badge variant="outline" className={`ml-2 ${getStatusColor(device.status)}`}>
|
||||
{getStatusText(device.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
{device.number && <div>手机号: {device.number}</div>}
|
||||
<div>最后活跃: {device.lastActive}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="col-span-full p-4 text-center text-gray-500">没有找到符合条件的设备</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Smartphone
|
||||
className={`h-5 w-5 ${device.status === "online" ? "text-green-500" : "text-gray-400"}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{selectedDevices.length === 0 && (
|
||||
<div className="flex items-center p-3 bg-yellow-50 rounded-md text-yellow-800">
|
||||
<AlertCircle className="h-4 w-4 mr-2" />
|
||||
请至少选择一台设备来执行建群任务
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 {selectedDevices.length} / {filteredDevices.length} 个设备
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -326,4 +278,3 @@ export function DeviceSelection({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,4 +65,3 @@ export function DeviceSelector({ selectedDevices, onChange }: DeviceSelectorProp
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -124,4 +124,3 @@ export function GroupAssistant({ open, onOpenChange, onCreateGroup }: GroupAssis
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -124,4 +124,3 @@ export function GroupCreationProgress({ planId, onComplete }: GroupCreationProgr
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,4 +94,3 @@ export function GroupPreview({ groupIndex, members, isCreating, isCompleted, onR
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle, Info, Plus, Minus, User, Users, X, Search } from "lucide-react"
|
||||
import { AlertCircle, Info, Plus, Minus, User, Users, X, Search, Loader2 } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -16,8 +16,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
interface WechatFriend {
|
||||
interface WechatAccount {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
@@ -25,8 +26,47 @@ interface WechatFriend {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
// 模拟从"我的"页面获取微信账号列表
|
||||
const mockWechatAccounts: WechatAccount[] = [
|
||||
{
|
||||
id: "account-1",
|
||||
nickname: "微信号1",
|
||||
wechatId: "wxid_abc123",
|
||||
avatar: "/placeholder.svg?height=40&width=40&text=1",
|
||||
tags: ["主要账号", "活跃"],
|
||||
},
|
||||
{
|
||||
id: "account-2",
|
||||
nickname: "微信号2",
|
||||
wechatId: "wxid_def456",
|
||||
avatar: "/placeholder.svg?height=40&width=40&text=2",
|
||||
tags: ["主要账号", "业务"],
|
||||
},
|
||||
{
|
||||
id: "account-3",
|
||||
nickname: "微信号3",
|
||||
wechatId: "wxid_ghi789",
|
||||
avatar: "/placeholder.svg?height=40&width=40&text=3",
|
||||
tags: ["备用账号"],
|
||||
},
|
||||
{
|
||||
id: "account-4",
|
||||
nickname: "微信号4",
|
||||
wechatId: "wxid_jkl012",
|
||||
avatar: "/placeholder.svg?height=40&width=40&text=4",
|
||||
tags: ["新账号"],
|
||||
},
|
||||
{
|
||||
id: "account-5",
|
||||
nickname: "微信号5",
|
||||
wechatId: "wxid_mno345",
|
||||
avatar: "/placeholder.svg?height=40&width=40&text=5",
|
||||
tags: ["测试账号"],
|
||||
},
|
||||
]
|
||||
|
||||
interface GroupSettingsProps {
|
||||
onNext: () => void
|
||||
onNextStep?: () => void // 修改为可选参数
|
||||
initialValues?: {
|
||||
name: string
|
||||
fixedWechatIds: string[]
|
||||
@@ -41,7 +81,8 @@ interface GroupSettingsProps {
|
||||
}) => void
|
||||
}
|
||||
|
||||
export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSettingsProps) {
|
||||
export function GroupSettings({ onNextStep, initialValues, onValuesChange }: GroupSettingsProps) {
|
||||
const isMobile = useMobile()
|
||||
const [name, setName] = useState(initialValues?.name || "新建群计划")
|
||||
const [fixedWechatIds, setFixedWechatIds] = useState<string[]>(initialValues?.fixedWechatIds || [])
|
||||
const [newWechatId, setNewWechatId] = useState("")
|
||||
@@ -51,9 +92,10 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
const [warning, setWarning] = useState<string | null>(null)
|
||||
const [friendSelectorOpen, setFriendSelectorOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [friends, setFriends] = useState<WechatFriend[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedFriends, setSelectedFriends] = useState<WechatFriend[]>([])
|
||||
const [wechatAccounts, setWechatAccounts] = useState<WechatAccount[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedFriends, setSelectedFriends] = useState<WechatAccount[]>([])
|
||||
const [autoAddingAccounts, setAutoAddingAccounts] = useState(false)
|
||||
|
||||
// 微信群人数固定为38人
|
||||
const GROUP_SIZE = 38
|
||||
@@ -62,11 +104,40 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
// 最多可选择的微信号数量
|
||||
const MAX_WECHAT_IDS = 5
|
||||
|
||||
// 只在组件挂载时执行一次初始验证
|
||||
// 组件挂载时获取微信账号并自动选择前三个
|
||||
useEffect(() => {
|
||||
validateSettings()
|
||||
fetchFriends()
|
||||
}, [])
|
||||
const fetchWechatAccounts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 模拟API调用延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 实际项目中应该从API获取微信账号列表
|
||||
// const response = await fetch('/api/wechat-accounts');
|
||||
// const data = await response.json();
|
||||
// setWechatAccounts(data);
|
||||
|
||||
// 使用模拟数据
|
||||
setWechatAccounts(mockWechatAccounts)
|
||||
|
||||
// 自动选择前三个微信账号
|
||||
const defaultAccounts = mockWechatAccounts.slice(0, 3)
|
||||
const defaultWechatIds = defaultAccounts.map((account) => account.wechatId)
|
||||
|
||||
// 如果没有初始值或初始值为空,则使用默认选择
|
||||
if (!initialValues?.fixedWechatIds || initialValues.fixedWechatIds.length === 0) {
|
||||
setFixedWechatIds(defaultWechatIds)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取微信账号失败:", error)
|
||||
setError("获取微信账号失败,请稍后重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchWechatAccounts()
|
||||
}, [initialValues?.fixedWechatIds])
|
||||
|
||||
// 当值变化时,通知父组件
|
||||
useEffect(() => {
|
||||
@@ -82,20 +153,39 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
return () => clearTimeout(timer)
|
||||
}, [name, fixedWechatIds, groupingOption, fixedGroupCount, onValuesChange])
|
||||
|
||||
const fetchFriends = async () => {
|
||||
setLoading(true)
|
||||
// 模拟从API获取好友列表
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const mockFriends = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `friend-${i}`,
|
||||
nickname: `好友${i + 1}`,
|
||||
wechatId: `wxid_${Math.random().toString(36).substring(2, 8)}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${i + 1}`,
|
||||
tags: i % 3 === 0 ? ["重要客户"] : i % 3 === 1 ? ["潜在客户", "已沟通"] : ["新客户"],
|
||||
}))
|
||||
setFriends(mockFriends)
|
||||
setLoading(false)
|
||||
}
|
||||
// 验证设置
|
||||
useEffect(() => {
|
||||
validateSettings()
|
||||
}, [name, fixedWechatIds, groupingOption, fixedGroupCount])
|
||||
|
||||
// 模拟自动添加缺失的微信账号
|
||||
useEffect(() => {
|
||||
// 检查是否需要自动添加微信账号
|
||||
const checkAndAddMissingAccounts = async () => {
|
||||
// 获取默认应该选择的前三个微信账号ID
|
||||
const defaultAccountIds = mockWechatAccounts.slice(0, 3).map((acc) => acc.wechatId)
|
||||
|
||||
// 检查是否有缺失的账号
|
||||
const missingAccountIds = defaultAccountIds.filter((id) => !fixedWechatIds.includes(id))
|
||||
|
||||
if (missingAccountIds.length > 0) {
|
||||
setAutoAddingAccounts(true)
|
||||
setWarning(`正在自动添加 ${missingAccountIds.length} 个缺失的微信账号...`)
|
||||
|
||||
// 模拟添加过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// 添加缺失的账号
|
||||
setFixedWechatIds((prev) => [...prev, ...missingAccountIds])
|
||||
setWarning(null)
|
||||
setAutoAddingAccounts(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!loading && fixedWechatIds.length < 3) {
|
||||
checkAndAddMissingAccounts()
|
||||
}
|
||||
}, [loading, fixedWechatIds])
|
||||
|
||||
const validateSettings = () => {
|
||||
setError(null)
|
||||
@@ -126,8 +216,8 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (validateSettings()) {
|
||||
onNext()
|
||||
if (validateSettings() && onNextStep) {
|
||||
onNextStep()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +291,7 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
setSelectedFriends([])
|
||||
}
|
||||
|
||||
const toggleFriendSelection = (friend: WechatFriend) => {
|
||||
const toggleFriendSelection = (friend: WechatAccount) => {
|
||||
setSelectedFriends((prev) => {
|
||||
const isSelected = prev.some((f) => f.id === friend.id)
|
||||
if (isSelected) {
|
||||
@@ -212,10 +302,10 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
})
|
||||
}
|
||||
|
||||
const filteredFriends = friends.filter(
|
||||
(friend) =>
|
||||
friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
const filteredAccounts = wechatAccounts.filter(
|
||||
(account) =>
|
||||
account.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
// 计算总人数
|
||||
@@ -244,67 +334,78 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>这些微信号将被添加到每个群中</p>
|
||||
<p>如不在好友列表中,系统将先添加为好友</p>
|
||||
<p>系统默认选择前三个微信号</p>
|
||||
<p>如不在好友列表中,系统将自动添加为好友</p>
|
||||
<p>最多可添加5个微信号</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<User className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
value={newWechatId}
|
||||
onChange={handleNewWechatIdChange}
|
||||
placeholder="请输入微信号"
|
||||
className="pl-9"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
addWechatId()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-blue-500 mr-2" />
|
||||
<span>正在加载微信账号...</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={addWechatId}
|
||||
disabled={!newWechatId.trim() || fixedWechatIds.length >= MAX_WECHAT_IDS}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={openFriendSelector}
|
||||
disabled={fixedWechatIds.length >= MAX_WECHAT_IDS}
|
||||
>
|
||||
选择好友
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{fixedWechatIds.map((id) => {
|
||||
const account = wechatAccounts.find((acc) => acc.wechatId === id)
|
||||
return (
|
||||
<Badge key={id} variant="secondary" className="px-3 py-1">
|
||||
{account ? account.nickname : id}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => removeWechatId(id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{fixedWechatIds.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{fixedWechatIds.map((id) => (
|
||||
<Badge key={id} variant="secondary" className="px-3 py-1">
|
||||
{id}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => removeWechatId(id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex space-x-2 mt-3">
|
||||
<div className="relative flex-1">
|
||||
<User className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
value={newWechatId}
|
||||
onChange={handleNewWechatIdChange}
|
||||
placeholder="请输入微信号"
|
||||
className="pl-9"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
addWechatId()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={addWechatId}
|
||||
disabled={!newWechatId.trim() || fixedWechatIds.length >= MAX_WECHAT_IDS}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={openFriendSelector}
|
||||
disabled={fixedWechatIds.length >= MAX_WECHAT_IDS}
|
||||
>
|
||||
选择微信号
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
已添加 {fixedWechatIds.length}/{MAX_WECHAT_IDS} 个微信号
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
已添加 {fixedWechatIds.length}/{MAX_WECHAT_IDS} 个微信号
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
@@ -329,7 +430,7 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
<p className="text-sm text-gray-500 mb-2">手动指定需要创建的群数量</p>
|
||||
|
||||
{groupingOption === "fixed" && (
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<div className={`flex ${isMobile ? "flex-col space-y-2" : "items-center space-x-2"} mt-2`}>
|
||||
<Label htmlFor="groupCount" className="whitespace-nowrap">
|
||||
计划创建群组数:
|
||||
</Label>
|
||||
@@ -380,6 +481,13 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{autoAddingAccounts && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-blue-600 mr-2" />
|
||||
<AlertDescription className="text-blue-700">正在自动添加默认微信号,请稍候...</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
@@ -387,7 +495,7 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{warning && (
|
||||
{warning && !autoAddingAccounts && (
|
||||
<Alert variant="warning" className="bg-yellow-50 border-yellow-200 text-yellow-800">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{warning}</AlertDescription>
|
||||
@@ -401,7 +509,7 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
disabled={fixedWechatIds.length === 0 || !!error}
|
||||
disabled={fixedWechatIds.length === 0 || !!error || loading || autoAddingAccounts}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
@@ -410,12 +518,12 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
<Dialog open={friendSelectorOpen} onOpenChange={setFriendSelectorOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择微信好友</DialogTitle>
|
||||
<DialogTitle>选择微信账号</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索好友"
|
||||
placeholder="搜索微信账号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
@@ -425,29 +533,29 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
<div className="text-center py-4">加载中...</div>
|
||||
) : filteredFriends.length === 0 ? (
|
||||
<div className="text-center py-4">未找到匹配的好友</div>
|
||||
) : filteredAccounts.length === 0 ? (
|
||||
<div className="text-center py-4">未找到匹配的微信账号</div>
|
||||
) : (
|
||||
filteredFriends.map((friend) => (
|
||||
filteredAccounts.map((account) => (
|
||||
<div
|
||||
key={friend.id}
|
||||
key={account.id}
|
||||
className="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded-lg cursor-pointer"
|
||||
onClick={() => toggleFriendSelection(friend)}
|
||||
onClick={() => toggleFriendSelection(account)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedFriends.some((f) => f.id === friend.id)}
|
||||
onCheckedChange={() => toggleFriendSelection(friend)}
|
||||
checked={selectedFriends.some((f) => f.id === account.id)}
|
||||
onCheckedChange={() => toggleFriendSelection(account)}
|
||||
/>
|
||||
<Avatar>
|
||||
<AvatarImage src={friend.avatar} />
|
||||
<AvatarFallback>{friend.nickname[0]}</AvatarFallback>
|
||||
<AvatarImage src={account.avatar || "/placeholder.svg"} />
|
||||
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{friend.nickname}</div>
|
||||
<div className="text-sm text-gray-500">{friend.wechatId}</div>
|
||||
<div className="font-medium">{account.nickname}</div>
|
||||
<div className="text-sm text-gray-500">{account.wechatId}</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{friend.tags.map((tag) => (
|
||||
{account.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
@@ -471,4 +579,3 @@ export function GroupSettings({ onNext, initialValues, onValuesChange }: GroupSe
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -282,4 +282,3 @@ export function NewPlanForm({ onClose }: NewPlanFormProps) {
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
interface Step {
|
||||
number: number
|
||||
title: string
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
currentStep: number
|
||||
steps: { title: string; description: string }[]
|
||||
onStepClick?: (step: number) => void
|
||||
steps: Step[]
|
||||
}
|
||||
|
||||
export function StepIndicator({ currentStep, steps, onStepClick }: StepIndicatorProps) {
|
||||
export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
const isMobile = useMobile()
|
||||
|
||||
return (
|
||||
<div className="w-full mb-8">
|
||||
<div className="w-full mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex flex-col items-center relative w-full">
|
||||
<div key={step.number} className="flex flex-col items-center relative w-full">
|
||||
{/* 连接线 */}
|
||||
{index > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-[2px] w-full top-4 -left-1/2 z-0",
|
||||
index <= currentStep ? "bg-blue-500" : "bg-gray-200",
|
||||
index < currentStep ? "bg-blue-500" : "bg-gray-200",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 步骤圆点 */}
|
||||
<button
|
||||
onClick={() => index < currentStep && onStepClick?.(index)}
|
||||
disabled={index > currentStep}
|
||||
<div
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center z-10 mb-2 transition-all",
|
||||
index < currentStep
|
||||
? "bg-blue-500 text-white cursor-pointer hover:bg-blue-600"
|
||||
: index === currentStep
|
||||
index + 1 < currentStep
|
||||
? "bg-blue-500 text-white"
|
||||
: index + 1 === currentStep
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 text-gray-500 cursor-not-allowed",
|
||||
: "bg-gray-200 text-gray-500",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
{step.number}
|
||||
</div>
|
||||
|
||||
{/* 步骤标题 */}
|
||||
<div className="text-sm font-medium">{step.title}</div>
|
||||
<div className="text-xs text-gray-500">{step.description}</div>
|
||||
<div className={cn("text-sm font-medium", index + 1 === currentStep ? "text-blue-500" : "text-gray-500")}>
|
||||
{step.title}
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="text-xs text-gray-500 mt-1 hidden md:block">
|
||||
{index + 1 === 1 ? "设置群名称和微信号" : index + 1 === 2 ? "选择执行设备" : "设置流量池标签"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,392 +1,262 @@
|
||||
"use client"
|
||||
|
||||
import { AlertDescription } from "@/components/ui/alert"
|
||||
|
||||
import { Alert } from "@/components/ui/alert"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Search, TagIcon, Users, Filter, AlertCircle, UserMinus } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, Users, Loader2, ExternalLink, Filter, CheckCircle2 } from 'lucide-react'
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import Link from "next/link"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
// 模拟人群标签数据
|
||||
const mockAudienceTags = [
|
||||
{ id: "tag-1", name: "高价值客户", description: "消费能力强的客户", count: 120 },
|
||||
{ id: "tag-2", name: "潜在客户", description: "有购买意向但未成交", count: 350 },
|
||||
{ id: "tag-3", name: "教师", description: "教育行业从业者", count: 85 },
|
||||
{ id: "tag-4", name: "医生", description: "医疗行业从业者", count: 64 },
|
||||
{ id: "tag-5", name: "企业白领", description: "企业中高层管理人员", count: 210 },
|
||||
{ id: "tag-6", name: "摄影爱好者", description: "对摄影有浓厚兴趣", count: 175 },
|
||||
{ id: "tag-7", name: "运动达人", description: "经常参与体育运动", count: 230 },
|
||||
{ id: "tag-8", name: "美食爱好者", description: "对美食有特别偏好", count: 320 },
|
||||
{ id: "tag-9", name: "20-30岁", description: "年龄在20-30岁之间", count: 450 },
|
||||
{ id: "tag-10", name: "30-40岁", description: "年龄在30-40岁之间", count: 380 },
|
||||
{ id: "tag-11", name: "高消费", description: "消费水平较高", count: 150 },
|
||||
{ id: "tag-12", name: "中等消费", description: "消费水平中等", count: 420 },
|
||||
]
|
||||
|
||||
// 模拟流量词数据
|
||||
const mockTrafficTags = [
|
||||
{ id: "traffic-1", name: "健身器材", description: "对健身器材有兴趣", count: 95 },
|
||||
{ id: "traffic-2", name: "减肥产品", description: "对减肥产品有需求", count: 130 },
|
||||
{ id: "traffic-3", name: "护肤品", description: "对护肤品有兴趣", count: 210 },
|
||||
{ id: "traffic-4", name: "旅游度假", description: "有旅游度假需求", count: 180 },
|
||||
{ id: "traffic-5", name: "教育培训", description: "对教育培训有需求", count: 160 },
|
||||
{ id: "traffic-6", name: "投资理财", description: "对投资理财有兴趣", count: 110 },
|
||||
{ id: "traffic-7", name: "房产购买", description: "有购房需求", count: 75 },
|
||||
{ id: "traffic-8", name: "汽车购买", description: "有购车需求", count: 90 },
|
||||
]
|
||||
|
||||
// 模拟排除标签数据
|
||||
const mockExcludeTags = [
|
||||
{ id: "exclude-1", name: "已拉群", description: "已被拉入群聊的用户", count: 320 },
|
||||
{ id: "exclude-2", name: "黑名单", description: "被标记为黑名单的用户", count: 45 },
|
||||
{ id: "exclude-3", name: "已转化", description: "已完成转化的用户", count: 180 },
|
||||
]
|
||||
|
||||
interface TagSelectionProps {
|
||||
onPrevious: () => void
|
||||
onComplete: () => void
|
||||
initialValues?: {
|
||||
audienceTags: string[]
|
||||
trafficTags: string[]
|
||||
matchLogic: "and" | "or"
|
||||
excludeTags: string[]
|
||||
}
|
||||
onValuesChange: (values: {
|
||||
audienceTags: string[]
|
||||
trafficTags: string[]
|
||||
matchLogic: "and" | "or"
|
||||
excludeTags: string[]
|
||||
}) => void
|
||||
interface TrafficPool {
|
||||
id: string
|
||||
name: string
|
||||
userCount: number
|
||||
description: string
|
||||
tags: string[]
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export function TagSelection({ onPrevious, onComplete, initialValues, onValuesChange }: TagSelectionProps) {
|
||||
const [audienceTags, setAudienceTags] = useState<string[]>(initialValues?.audienceTags || [])
|
||||
const [trafficTags, setTrafficTags] = useState<string[]>(initialValues?.trafficTags || [])
|
||||
const [matchLogic, setMatchLogic] = useState<"and" | "or">(initialValues?.matchLogic || "or")
|
||||
const [excludeTags, setExcludeTags] = useState<string[]>(initialValues?.excludeTags || ["exclude-1"])
|
||||
const [audienceSearchQuery, setAudienceSearchQuery] = useState("")
|
||||
const [trafficSearchQuery, setTrafficSearchQuery] = useState("")
|
||||
const [excludeSearchQuery, setExcludeSearchQuery] = useState("")
|
||||
const [autoTagEnabled, setAutoTagEnabled] = useState(true)
|
||||
// 模拟流量池数据
|
||||
const mockTrafficPools: TrafficPool[] = [
|
||||
{
|
||||
id: "pool-1",
|
||||
name: "高意向客户池",
|
||||
userCount: 230,
|
||||
description: "包含所有高意向潜在客户",
|
||||
tags: ["高意向", "已沟通", "潜在客户"],
|
||||
lastUpdated: "2023-05-15 14:30",
|
||||
},
|
||||
{
|
||||
id: "pool-2",
|
||||
name: "活动获客池",
|
||||
userCount: 156,
|
||||
description: "市场活动获取的客户",
|
||||
tags: ["活动", "新客户"],
|
||||
lastUpdated: "2023-05-14 09:15",
|
||||
},
|
||||
{
|
||||
id: "pool-3",
|
||||
name: "老客户池",
|
||||
userCount: 89,
|
||||
description: "已成交的老客户",
|
||||
tags: ["已成交", "老客户", "高价值"],
|
||||
lastUpdated: "2023-05-13 11:45",
|
||||
},
|
||||
{
|
||||
id: "pool-4",
|
||||
name: "待跟进客户池",
|
||||
userCount: 120,
|
||||
description: "需要跟进的客户",
|
||||
tags: ["待跟进", "中意向"],
|
||||
lastUpdated: "2023-05-12 13:20",
|
||||
},
|
||||
{
|
||||
id: "pool-5",
|
||||
name: "VIP客户池",
|
||||
userCount: 45,
|
||||
description: "高价值VIP客户",
|
||||
tags: ["VIP", "高价值", "已成交"],
|
||||
lastUpdated: "2023-05-11 10:05",
|
||||
},
|
||||
]
|
||||
|
||||
// 使用ref来跟踪是否已经通知了父组件初始选择
|
||||
const initialNotificationRef = useRef(false)
|
||||
// 使用ref来存储上一次的值,避免不必要的更新
|
||||
const prevValuesRef = useRef({ audienceTags, trafficTags, matchLogic, excludeTags })
|
||||
interface TrafficPoolSelectionProps {
|
||||
onSubmit: () => void
|
||||
onPrevious: () => void
|
||||
initialSelectedPools?: string[]
|
||||
onPoolsChange: (poolIds: string[]) => void
|
||||
selectedDevices?: string[] // 已选设备ID列表
|
||||
}
|
||||
|
||||
// 只在值变化时通知父组件,使用防抖
|
||||
export function TrafficPoolSelection({
|
||||
onSubmit,
|
||||
onPrevious,
|
||||
initialSelectedPools = [],
|
||||
onPoolsChange,
|
||||
selectedDevices = [],
|
||||
}: TrafficPoolSelectionProps) {
|
||||
const isMobile = useMobile()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>(initialSelectedPools)
|
||||
const [trafficPools, setTrafficPools] = useState<TrafficPool[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [userCountFilter, setUserCountFilter] = useState<"all" | "high" | "medium" | "low">("all")
|
||||
|
||||
// 获取流量池数据
|
||||
useEffect(() => {
|
||||
if (!initialNotificationRef.current) {
|
||||
initialNotificationRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// 检查值是否真的变化了
|
||||
const prevValues = prevValuesRef.current
|
||||
const valuesChanged =
|
||||
prevValues.audienceTags.length !== audienceTags.length ||
|
||||
prevValues.trafficTags.length !== trafficTags.length ||
|
||||
prevValues.matchLogic !== matchLogic ||
|
||||
prevValues.excludeTags.length !== excludeTags.length
|
||||
|
||||
if (!valuesChanged) return
|
||||
|
||||
// 更新ref中存储的上一次值
|
||||
prevValuesRef.current = { audienceTags, trafficTags, matchLogic, excludeTags }
|
||||
|
||||
// 使用防抖延迟通知父组件
|
||||
const timer = setTimeout(() => {
|
||||
onValuesChange({ audienceTags, trafficTags, matchLogic, excludeTags })
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [audienceTags, trafficTags, matchLogic, excludeTags, onValuesChange])
|
||||
|
||||
const filteredAudienceTags = mockAudienceTags.filter(
|
||||
(tag) =>
|
||||
tag.name.toLowerCase().includes(audienceSearchQuery.toLowerCase()) ||
|
||||
tag.description.toLowerCase().includes(audienceSearchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const filteredTrafficTags = mockTrafficTags.filter(
|
||||
(tag) =>
|
||||
tag.name.toLowerCase().includes(trafficSearchQuery.toLowerCase()) ||
|
||||
tag.description.toLowerCase().includes(trafficSearchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const filteredExcludeTags = mockExcludeTags.filter(
|
||||
(tag) =>
|
||||
tag.name.toLowerCase().includes(excludeSearchQuery.toLowerCase()) ||
|
||||
tag.description.toLowerCase().includes(excludeSearchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const handleAudienceTagToggle = (tagId: string) => {
|
||||
setAudienceTags((prev) => (prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId]))
|
||||
}
|
||||
|
||||
const handleTrafficTagToggle = (tagId: string) => {
|
||||
setTrafficTags((prev) => (prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId]))
|
||||
}
|
||||
|
||||
const handleExcludeTagToggle = (tagId: string) => {
|
||||
setExcludeTags((prev) => (prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId]))
|
||||
}
|
||||
|
||||
// 计算匹配的预估人数
|
||||
const calculateEstimatedMatches = () => {
|
||||
if (audienceTags.length === 0 && trafficTags.length === 0) return 0
|
||||
|
||||
const selectedAudienceTags = mockAudienceTags.filter((tag) => audienceTags.includes(tag.id))
|
||||
const selectedTrafficTags = mockTrafficTags.filter((tag) => trafficTags.includes(tag.id))
|
||||
const selectedExcludeTags = mockExcludeTags.filter((tag) => excludeTags.includes(tag.id))
|
||||
|
||||
let estimatedTotal = 0
|
||||
|
||||
if (audienceTags.length === 0) {
|
||||
estimatedTotal = selectedTrafficTags.reduce((sum, tag) => sum + tag.count, 0)
|
||||
} else if (trafficTags.length === 0) {
|
||||
estimatedTotal = selectedAudienceTags.reduce((sum, tag) => sum + tag.count, 0)
|
||||
} else {
|
||||
// 如果两种标签都有选择,根据匹配逻辑计算
|
||||
if (matchLogic === "and") {
|
||||
// 取交集,估算为较小集合的70%
|
||||
const minCount = Math.min(
|
||||
selectedAudienceTags.reduce((sum, tag) => sum + tag.count, 0),
|
||||
selectedTrafficTags.reduce((sum, tag) => sum + tag.count, 0),
|
||||
)
|
||||
estimatedTotal = Math.floor(minCount * 0.7)
|
||||
} else {
|
||||
// 取并集,估算为两者之和的80%(考虑重叠)
|
||||
const totalCount =
|
||||
selectedAudienceTags.reduce((sum, tag) => sum + tag.count, 0) +
|
||||
selectedTrafficTags.reduce((sum, tag) => sum + tag.count, 0)
|
||||
estimatedTotal = Math.floor(totalCount * 0.8)
|
||||
const fetchTrafficPools = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
// 如果有选择设备,可以根据设备ID筛选流量池
|
||||
if (selectedDevices.length > 0) {
|
||||
console.log("根据已选设备筛选流量池:", selectedDevices)
|
||||
// 这里应该是实际的API调用,根据设备ID获取相关流量池
|
||||
}
|
||||
setTrafficPools(mockTrafficPools)
|
||||
} catch (error) {
|
||||
console.error("获取流量池失败:", error)
|
||||
setError("获取流量池列表失败,请稍后重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 减去排除标签的人数
|
||||
const excludeCount = selectedExcludeTags.reduce((sum, tag) => sum + tag.count, 0)
|
||||
return Math.max(0, estimatedTotal - excludeCount)
|
||||
fetchTrafficPools()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(selectedDevices)])
|
||||
|
||||
// 通知父组件选择变化
|
||||
useEffect(() => {
|
||||
// 只在流量池选择实际变化时通知父组件
|
||||
const currentSelection = JSON.stringify(selectedPools)
|
||||
const initialSelection = JSON.stringify(initialSelectedPools)
|
||||
|
||||
if (currentSelection !== initialSelection) {
|
||||
onPoolsChange(selectedPools)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedPools, JSON.stringify(initialSelectedPools)])
|
||||
|
||||
const handlePoolToggle = (poolId: string) => {
|
||||
setSelectedPools((prev) =>
|
||||
prev.includes(poolId) ? prev.filter((id) => id !== poolId) : [...prev, poolId]
|
||||
)
|
||||
}
|
||||
|
||||
const estimatedMatches = calculateEstimatedMatches()
|
||||
// 根据用户数量过滤
|
||||
const getUserCountFilterValue = (pool: TrafficPool) => {
|
||||
if (pool.userCount > 150) return "high"
|
||||
if (pool.userCount > 50) return "medium"
|
||||
return "low"
|
||||
}
|
||||
|
||||
const filteredPools = trafficPools.filter((pool) => {
|
||||
const matchesSearch = pool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
pool.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
pool.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
const matchesUserCount = userCountFilter === "all" || getUserCountFilterValue(pool) === userCountFilter
|
||||
return matchesSearch && matchesUserCount
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="include" className="w-full">
|
||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||
<TabsTrigger value="include">包含条件</TabsTrigger>
|
||||
<TabsTrigger value="exclude">排除条件</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-medium">选择流量池</Label>
|
||||
<Link href="/traffic-pool" className="text-blue-500 hover:text-blue-600 text-sm flex items-center">
|
||||
前往流量池管理
|
||||
<ExternalLink className="h-4 w-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<TabsContent value="include" className="space-y-6 mt-4">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium flex items-center">
|
||||
<TagIcon className="h-4 w-4 mr-2" />
|
||||
人群标签选择
|
||||
</Label>
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertDescription className="text-blue-700">
|
||||
选择流量池后,系统将自动筛选出该流量池中的用户,以确定自动建群所针对的目标群体。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索人群标签"
|
||||
value={audienceSearchQuery}
|
||||
onChange={(e) => setAudienceSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<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>
|
||||
|
||||
<ScrollArea className="h-[200px] rounded-md border">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 p-3">
|
||||
{filteredAudienceTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className={`border rounded-md p-2 cursor-pointer hover:bg-gray-50 ${
|
||||
audienceTags.includes(tag.id) ? "border-blue-500 bg-blue-50" : ""
|
||||
}`}
|
||||
onClick={() => handleAudienceTagToggle(tag.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-medium">{tag.name}</div>
|
||||
<Checkbox
|
||||
checked={audienceTags.includes(tag.id)}
|
||||
onCheckedChange={() => handleAudienceTagToggle(tag.id)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{tag.description}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<Users className="h-3 w-3 inline mr-1" />
|
||||
{tag.count}人
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredAudienceTags.length === 0 && (
|
||||
<div className="col-span-full p-4 text-center text-gray-500">没有找到符合条件的人群标签</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium flex items-center">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
流量词选择
|
||||
</Label>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索流量词"
|
||||
value={trafficSearchQuery}
|
||||
onChange={(e) => setTrafficSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[200px] rounded-md border">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 p-3">
|
||||
{filteredTrafficTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className={`border rounded-md p-2 cursor-pointer hover:bg-gray-50 ${
|
||||
trafficTags.includes(tag.id) ? "border-blue-500 bg-blue-50" : ""
|
||||
}`}
|
||||
onClick={() => handleTrafficTagToggle(tag.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-medium">{tag.name}</div>
|
||||
<Checkbox
|
||||
checked={trafficTags.includes(tag.id)}
|
||||
onCheckedChange={() => handleTrafficTagToggle(tag.id)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{tag.description}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<Users className="h-3 w-3 inline mr-1" />
|
||||
{tag.count}人
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredTrafficTags.length === 0 && (
|
||||
<div className="col-span-full p-4 text-center text-gray-500">没有找到符合条件的流量词</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">标签匹配逻辑</Label>
|
||||
<RadioGroup value={matchLogic} onValueChange={(value) => setMatchLogic(value as "and" | "or")}>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="and" id="and" />
|
||||
<Label htmlFor="and">同时满足所有标签(AND)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="or" id="or" />
|
||||
<Label htmlFor="or">满足任一标签即可(OR)</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="exclude" className="space-y-6 mt-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base font-medium flex items-center">
|
||||
<UserMinus className="h-4 w-4 mr-2" />
|
||||
排除标签
|
||||
</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="auto-tag" checked={autoTagEnabled} onCheckedChange={setAutoTagEnabled} />
|
||||
<Label htmlFor="auto-tag" className="text-sm">
|
||||
自动为拉群成员添加标签
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索排除标签"
|
||||
value={excludeSearchQuery}
|
||||
onChange={(e) => setExcludeSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[200px] rounded-md border">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 p-3">
|
||||
{filteredExcludeTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className={`border rounded-md p-2 cursor-pointer hover:bg-gray-50 ${
|
||||
excludeTags.includes(tag.id) ? "border-red-500 bg-red-50" : ""
|
||||
}`}
|
||||
onClick={() => handleExcludeTagToggle(tag.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-medium">{tag.name}</div>
|
||||
<Checkbox
|
||||
checked={excludeTags.includes(tag.id)}
|
||||
onCheckedChange={() => handleExcludeTagToggle(tag.id)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{tag.description}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<Users className="h-3 w-3 inline mr-1" />
|
||||
{tag.count}人
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredExcludeTags.length === 0 && (
|
||||
<div className="col-span-full p-4 text-center text-gray-500">没有找到符合条件的排除标签</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<Alert className="bg-yellow-50 border-yellow-200 text-yellow-800">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
启用自动标记后,系统将为所有被拉入群的用户添加"已拉群"标签,避免重复拉人
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="p-4 bg-blue-50 rounded-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-blue-700 font-medium">预估匹配人数:</span>
|
||||
<span className="text-blue-700 font-bold">{estimatedMatches} 人</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-gray-400" />
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={userCountFilter}
|
||||
onChange={(e) => setUserCountFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">全部用户量</option>
|
||||
<option value="high">大流量池 ({'>'}150人)</option>
|
||||
<option value="medium">中流量池 (50-150人)</option>
|
||||
<option value="low">小流量池 ({'<'}50人)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{audienceTags.length === 0 && trafficTags.length === 0 && (
|
||||
<div className="flex items-center p-3 bg-yellow-50 rounded-md text-yellow-800">
|
||||
<AlertCircle className="h-4 w-4 mr-2" />
|
||||
请至少选择一个人群标签或流量词
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500 mr-2" />
|
||||
<span>正在加载流量池列表...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : filteredPools.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">未找到匹配的流量池</div>
|
||||
) : (
|
||||
<div className="space-y-3 mt-4">
|
||||
{filteredPools.map((pool) => (
|
||||
<div
|
||||
key={pool.id}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
|
||||
selectedPools.includes(pool.id)
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50/50"
|
||||
}`}
|
||||
onClick={() => handlePoolToggle(pool.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-0.5">
|
||||
{selectedPools.includes(pool.id) ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-blue-500" />
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-gray-300" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{pool.name}</div>
|
||||
<div className="text-sm text-gray-500 mt-1">{pool.description}</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Users className="h-4 w-4 text-blue-500 mr-1" />
|
||||
<span className="text-sm text-blue-600 font-medium">{pool.userCount} 人</span>
|
||||
<span className="text-xs text-gray-500 ml-3">更新于: {pool.lastUpdated}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{pool.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="outline" className="bg-blue-50 text-blue-600 border-blue-200">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 {selectedPools.length} / {filteredPools.length} 个流量池
|
||||
</div>
|
||||
{selectedPools.length > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={() => setSelectedPools([])}>
|
||||
清空选择
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -395,15 +265,10 @@ export function TagSelection({ onPrevious, onComplete, initialValues, onValuesCh
|
||||
<Button variant="outline" onClick={onPrevious}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onComplete}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
disabled={audienceTags.length === 0 && trafficTags.length === 0}
|
||||
>
|
||||
<Button onClick={onSubmit} className="bg-blue-500 hover:bg-blue-600" disabled={selectedPools.length === 0}>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { ExternalLink, Search, Tag, AlertCircle } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
|
||||
// 流量池标签类型定义
|
||||
interface TrafficTag {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
interface TrafficPoolSelectorProps {
|
||||
onTagsSelected: (tags: TrafficTag[]) => void
|
||||
selectedDevices: string[] // 已选择的设备ID列表
|
||||
}
|
||||
|
||||
export function TrafficPoolSelector({ onTagsSelected, selectedDevices }: TrafficPoolSelectorProps) {
|
||||
// 模拟流量池标签数据
|
||||
const [tags, setTags] = useState<TrafficTag[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [filteredDeviceCount, setFilteredDeviceCount] = useState<Record<string, number>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 模拟从API获取标签数据
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
// 模拟数据
|
||||
const mockTags = [
|
||||
{ id: "1", name: "意向客户", count: 120 },
|
||||
{ id: "2", name: "高净值", count: 85 },
|
||||
{ id: "3", name: "潜在客户", count: 210 },
|
||||
{ id: "4", name: "已成交", count: 65 },
|
||||
{ id: "5", name: "待跟进", count: 178 },
|
||||
{ id: "6", name: "活跃用户", count: 156 },
|
||||
{ id: "7", name: "新客户", count: 92 },
|
||||
{ id: "8", name: "老客户", count: 143 },
|
||||
{ id: "9", name: "企业客户", count: 76 },
|
||||
{ id: "10", name: "个人客户", count: 189 },
|
||||
]
|
||||
|
||||
setTags(mockTags)
|
||||
|
||||
// 模拟计算每个标签在已选设备中的匹配数量
|
||||
const deviceCounts: Record<string, number> = {}
|
||||
mockTags.forEach((tag) => {
|
||||
// 随机生成一个不超过已选设备数量的数字
|
||||
const matchCount = Math.min(Math.floor(Math.random() * selectedDevices.length), selectedDevices.length)
|
||||
deviceCounts[tag.id] = matchCount
|
||||
})
|
||||
|
||||
setFilteredDeviceCount(deviceCounts)
|
||||
} catch (error) {
|
||||
console.error("获取流量池标签失败:", error)
|
||||
setError("获取流量池标签失败,请稍后重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedDevices.length > 0) {
|
||||
fetchTags()
|
||||
}
|
||||
}, [selectedDevices])
|
||||
|
||||
// 处理标签选择
|
||||
const handleTagToggle = (tagId: string) => {
|
||||
const updatedTags = tags.map((tag) => (tag.id === tagId ? { ...tag, selected: !tag.selected } : tag))
|
||||
setTags(updatedTags)
|
||||
|
||||
// 通知父组件选中的标签
|
||||
const selectedTags = updatedTags.filter((tag) => tag.selected)
|
||||
onTagsSelected(selectedTags)
|
||||
}
|
||||
|
||||
// 筛选标签
|
||||
const filteredTags = tags.filter((tag) => tag.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
// 选中的标签数量
|
||||
const selectedCount = tags.filter((tag) => tag.selected).length
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-lg font-medium">流量池标签筛选</CardTitle>
|
||||
<Link href="/traffic-pool" className="text-sm text-blue-600 flex items-center gap-1">
|
||||
<span>前往流量池</span>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{selectedDevices.length === 0 ? (
|
||||
<Alert variant="warning" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>请先在上一步选择设备,再进行流量池标签筛选</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="搜索标签..."
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
已选择 <Badge variant="outline">{selectedCount}</Badge> 个标签
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
||||
<span className="ml-2">加载中...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<ScrollArea className="h-[300px] pr-4">
|
||||
<div className="space-y-2">
|
||||
{filteredTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className={`flex items-center justify-between p-2 border rounded-md ${tag.selected ? "bg-blue-50 border-blue-200" : "bg-white"}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`tag-${tag.id}`}
|
||||
checked={tag.selected}
|
||||
onCheckedChange={() => handleTagToggle(tag.id)}
|
||||
/>
|
||||
<label htmlFor={`tag-${tag.id}`} className="flex items-center gap-2 cursor-pointer text-sm">
|
||||
<Tag className="h-3.5 w-3.5 text-gray-500" />
|
||||
{tag.name}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tag.count}人
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
已选设备匹配: {filteredDeviceCount[tag.id] || 0}人
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredTags.length === 0 && <p className="text-center py-4 text-gray-500">未找到匹配的标签</p>}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
const selectedTags = tags.filter((tag) => tag.selected)
|
||||
onTagsSelected(selectedTags)
|
||||
}}
|
||||
>
|
||||
确认选择
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Search, Plus } from "lucide-react"
|
||||
import { Table } from "@/components/ui/table"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Avatar } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle, Check } from "lucide-react"
|
||||
|
||||
// 微信账号类型定义
|
||||
interface WechatAccount {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
status: "online" | "offline" | "busy"
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
interface WechatAccountSelectorProps {
|
||||
selectedAccounts: string[]
|
||||
onChange: (accounts: string[]) => void
|
||||
onAccountsSelected: (accounts: WechatAccount[]) => void
|
||||
}
|
||||
|
||||
export function WechatAccountSelector({ selectedAccounts, onChange }: WechatAccountSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
export function WechatAccountSelector({ onAccountsSelected }: WechatAccountSelectorProps) {
|
||||
const [accounts, setAccounts] = useState<WechatAccount[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [addingMissingAccounts, setAddingMissingAccounts] = useState(false)
|
||||
|
||||
// 从"我的"页面获取微信账号列表
|
||||
useEffect(() => {
|
||||
const fetchWechatAccounts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 这里应该是实际的API调用,现在使用模拟数据
|
||||
// const response = await fetch('/api/my/wechat-accounts');
|
||||
// const data = await response.json();
|
||||
|
||||
// 模拟数据
|
||||
const mockAccounts: WechatAccount[] = [
|
||||
{ id: "1", name: "微信号1", avatar: "/diverse-avatars.png", status: "online", isDefault: true },
|
||||
{ id: "2", name: "微信号2", avatar: "/diverse-avatars.png", status: "online", isDefault: true },
|
||||
{ id: "3", name: "微信号3", avatar: "/diverse-avatars.png", status: "offline", isDefault: true },
|
||||
{ id: "4", name: "微信号4", avatar: "/diverse-avatars.png", status: "online" },
|
||||
{ id: "5", name: "微信号5", avatar: "/diverse-avatars.png", status: "busy" },
|
||||
]
|
||||
|
||||
// 延迟模拟网络请求
|
||||
setTimeout(() => {
|
||||
setAccounts(mockAccounts)
|
||||
setLoading(false)
|
||||
|
||||
// 自动选择前三个账号
|
||||
const defaultAccounts = mockAccounts.filter((acc) => acc.isDefault).slice(0, 3)
|
||||
onAccountsSelected(defaultAccounts)
|
||||
|
||||
// 检查是否需要添加缺失的默认账号
|
||||
checkForMissingDefaultAccounts(mockAccounts)
|
||||
}, 1000)
|
||||
} catch (err) {
|
||||
setError("获取微信账号失败,请重试")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchWechatAccounts()
|
||||
}, [onAccountsSelected])
|
||||
|
||||
// 检查是否缺少默认微信号,如果缺少则自动触发添加任务
|
||||
const checkForMissingDefaultAccounts = (accounts: WechatAccount[]) => {
|
||||
const defaultAccounts = accounts.filter((acc) => acc.isDefault)
|
||||
if (defaultAccounts.length < 3) {
|
||||
setAddingMissingAccounts(true)
|
||||
|
||||
// 模拟添加任务的过程
|
||||
setTimeout(() => {
|
||||
setAddingMissingAccounts(false)
|
||||
// 这里应该有实际的添加任务逻辑
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染状态标签
|
||||
const renderStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return <Badge className="bg-green-500">在线</Badge>
|
||||
case "offline":
|
||||
return (
|
||||
<Badge variant="outline" className="text-gray-500">
|
||||
离线
|
||||
</Badge>
|
||||
)
|
||||
case "busy":
|
||||
return <Badge className="bg-yellow-500">忙碌</Badge>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-lg font-medium mb-4">自动选择微信号</h3>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-[100px]" />
|
||||
<Skeleton className="h-3 w-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
const defaultAccounts = accounts.filter((acc) => acc.isDefault).slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="mt-1.5 space-x-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)} className="h-9">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
选择客户
|
||||
</Button>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-lg font-medium mb-4">自动选择微信号</h3>
|
||||
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)} className="h-9">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
外部账号
|
||||
</Button>
|
||||
{addingMissingAccounts && (
|
||||
<Alert className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>正在自动添加缺失的默认微信号...</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择微信账号</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
{defaultAccounts.map((account) => (
|
||||
<div key={account.id} className="flex items-center justify-between p-2 border rounded-md bg-gray-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<img src={account.avatar || "/placeholder.svg"} alt={account.name} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{account.name}</p>
|
||||
{renderStatusBadge(account.status)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-blue-500 flex items-center gap-1">
|
||||
<Check className="h-3 w-3" /> 已选择
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input placeholder="请输入关键字筛选" className="pl-9" />
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>微信号</th>
|
||||
<th>在线状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center py-4 text-gray-500">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setOpen(false)} className="bg-blue-600 hover:bg-blue-700">
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-4">系统已自动选择前三个默认微信号用于自动建群操作</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
42
Cunkebao/app/workspace/auto-group/new/loading.tsx
Normal file
42
Cunkebao/app/workspace/auto-group/new/loading.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card } from "@/components/ui/card"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen p-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="flex space-x-4">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<Skeleton className="h-6 w-40 mb-4" />
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<Skeleton className="h-10 w-full mb-4" />
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-24" />
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
136
Cunkebao/app/workspace/auto-group/new/page.tsx
Normal file
136
Cunkebao/app/workspace/auto-group/new/page.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { StepIndicator } from "../components/step-indicator"
|
||||
import { GroupSettings } from "../components/group-settings"
|
||||
import { DeviceSelection } from "../components/device-selection"
|
||||
import { TrafficPoolSelection } from "../components/tag-selection"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
export default function NewAutoGroupPage() {
|
||||
const router = useRouter()
|
||||
const isMobile = useMobile()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [formData, setFormData] = useState({
|
||||
name: "新建群计划",
|
||||
fixedWechatIds: [] as string[],
|
||||
groupingOption: "all" as "all" | "fixed",
|
||||
fixedGroupCount: 5,
|
||||
devices: [] as string[],
|
||||
trafficPools: [] as string[],
|
||||
welcomeMessage: "欢迎进群",
|
||||
})
|
||||
|
||||
const steps = [
|
||||
{ number: 1, title: "群设置" },
|
||||
{ number: 2, title: "设备选择" },
|
||||
{ number: 3, title: "流量池选择" },
|
||||
]
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < steps.length) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
window.scrollTo(0, 0)
|
||||
} else {
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 这里应该是提交表单数据到API的逻辑
|
||||
console.log("提交表单数据:", formData)
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 提交成功后返回列表页
|
||||
router.push("/workspace/auto-group")
|
||||
}
|
||||
|
||||
const updateGroupSettings = (values: {
|
||||
name: string
|
||||
fixedWechatIds: string[]
|
||||
groupingOption: "all" | "fixed"
|
||||
fixedGroupCount: number
|
||||
}) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
...values,
|
||||
}))
|
||||
}
|
||||
|
||||
const updateDevices = useCallback((deviceIds: string[]) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
devices: deviceIds,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const updateTrafficPools = useCallback((poolIds: string[]) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
trafficPools: poolIds,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center p-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium ml-3">新建自动建群</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className={`p-4 ${isMobile ? "max-w-full" : "max-w-4xl mx-auto"}`}>
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
|
||||
<div className="mt-6">
|
||||
{currentStep === 1 && (
|
||||
<GroupSettings
|
||||
onNextStep={handleNext}
|
||||
initialValues={{
|
||||
name: formData.name,
|
||||
fixedWechatIds: formData.fixedWechatIds,
|
||||
groupingOption: formData.groupingOption,
|
||||
fixedGroupCount: formData.fixedGroupCount,
|
||||
}}
|
||||
onValuesChange={updateGroupSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<DeviceSelection
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
initialSelectedDevices={formData.devices}
|
||||
onDevicesChange={updateDevices}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<TrafficPoolSelection
|
||||
onSubmit={handleSubmit}
|
||||
onPrevious={handlePrevious}
|
||||
initialSelectedPools={formData.trafficPools}
|
||||
onPoolsChange={updateTrafficPools}
|
||||
selectedDevices={formData.devices}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ChevronLeft, Plus, Filter, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye } from "lucide-react"
|
||||
import { ChevronLeft, Plus, MoreVertical, Clock, Edit, Trash2, Eye, PinIcon } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Users, Settings } from "lucide-react"
|
||||
import { Users, Settings, AlertCircle } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { useMobile } from "@/hooks/use-mobile"
|
||||
|
||||
interface Plan {
|
||||
id: string
|
||||
@@ -21,9 +22,21 @@ interface Plan {
|
||||
tags: string[]
|
||||
status: "running" | "stopped" | "completed"
|
||||
lastUpdated: string
|
||||
isPinned?: boolean
|
||||
}
|
||||
|
||||
const mockPlans: Plan[] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "默认建群规则",
|
||||
groupCount: 10,
|
||||
groupSize: 38,
|
||||
totalFriends: 380,
|
||||
tags: ["默认", "自动"],
|
||||
status: "running",
|
||||
lastUpdated: "2024-02-24 10:30",
|
||||
isPinned: true,
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
name: "品牌推广群",
|
||||
@@ -48,7 +61,7 @@ const mockPlans: Plan[] = [
|
||||
|
||||
export default function AutoGroupPage() {
|
||||
const router = useRouter()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const isMobile = useMobile()
|
||||
const [plans, setPlans] = useState(mockPlans)
|
||||
|
||||
const handleDelete = (planId: string) => {
|
||||
@@ -97,6 +110,10 @@ export default function AutoGroupPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分离置顶计划和普通计划
|
||||
const pinnedPlans = plans.filter((plan) => plan.isPinned)
|
||||
const normalPlans = plans.filter((plan) => !plan.isPinned)
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
@@ -105,7 +122,7 @@ export default function AutoGroupPage() {
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">自动建群、自动进群</h1>
|
||||
<h1 className="text-lg font-medium">自动建群</h1>
|
||||
</div>
|
||||
<Link href="/workspace/auto-group/new">
|
||||
<Button>
|
||||
@@ -117,23 +134,99 @@ export default function AutoGroupPage() {
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<Card className="p-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="搜索任务名称" 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>
|
||||
</Card>
|
||||
{/* 默认规则说明 */}
|
||||
<Alert className="mb-4 bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-600" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
默认建群规则将应用于所有新扫描的设备和新添加的微信账号。请确保设置合适的默认规则。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* 置顶的默认规则 */}
|
||||
{pinnedPlans.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-medium mb-3 flex items-center">
|
||||
<PinIcon className="h-4 w-4 mr-2 text-blue-600" />
|
||||
默认建群规则
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{pinnedPlans.map((plan) => (
|
||||
<Card key={plan.id} className="p-4 overflow-hidden border-2 border-blue-200 bg-blue-50">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-blue-100 to-blue-50 rounded-bl-full -z-10"></div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{plan.name}</h3>
|
||||
<Badge variant="outline" className={getStatusColor(plan.status)}>
|
||||
{getStatusText(plan.status)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-200">
|
||||
默认
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={plan.status === "running"} onCheckedChange={() => togglePlanStatus(plan.id)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => handleView(plan.id)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(plan.id)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`grid ${isMobile ? "grid-cols-1 gap-2" : "grid-cols-2 gap-4"} mb-4`}>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
已建群数:{plan.groupCount}
|
||||
</div>
|
||||
<div className="flex items-center mt-1">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
群规模:{plan.groupSize}人/群
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>总人数:{plan.totalFriends}人</div>
|
||||
<div className="mt-1">
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{plan.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
更新时间:{plan.lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 其他建群计划 */}
|
||||
<div className="space-y-4">
|
||||
{plans.map((plan) => (
|
||||
<h2 className="text-lg font-medium mb-3">建群计划列表</h2>
|
||||
{normalPlans.map((plan) => (
|
||||
<Card key={plan.id} className="p-4 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-blue-100 to-blue-50 rounded-bl-full -z-10"></div>
|
||||
|
||||
@@ -170,7 +263,7 @@ export default function AutoGroupPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className={`grid ${isMobile ? "grid-cols-1 gap-2" : "grid-cols-2 gap-4"} mb-4`}>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
@@ -203,6 +296,10 @@ export default function AutoGroupPage() {
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{normalPlans.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">暂无建群计划,点击"新建任务"创建您的第一个建群计划</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,9 @@ interface BasicSettingsProps {
|
||||
taskName: string
|
||||
likeInterval: number
|
||||
maxLikesPerDay: number
|
||||
friendMaxLikes: number
|
||||
timeRange: { start: string; end: string }
|
||||
contentTypes: string[]
|
||||
enabled: boolean
|
||||
enableFriendTags: boolean
|
||||
friendTags: string
|
||||
}
|
||||
onChange: (data: Partial<BasicSettingsProps["formData"]>) => void
|
||||
onNext: () => void
|
||||
@@ -40,19 +37,11 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
}
|
||||
|
||||
const incrementMaxLikes = () => {
|
||||
onChange({ maxLikesPerDay: Math.min(formData.maxLikesPerDay + 100, 10000) })
|
||||
onChange({ maxLikesPerDay: Math.min(formData.maxLikesPerDay + 10, 500) })
|
||||
}
|
||||
|
||||
const decrementMaxLikes = () => {
|
||||
onChange({ maxLikesPerDay: Math.max(formData.maxLikesPerDay - 100, 10) })
|
||||
}
|
||||
|
||||
const incrementFriendMaxLikes = () => {
|
||||
onChange({ friendMaxLikes: Math.min(formData.friendMaxLikes + 1, 20) })
|
||||
}
|
||||
|
||||
const decrementFriendMaxLikes = () => {
|
||||
onChange({ friendMaxLikes: Math.max(formData.friendMaxLikes - 1, 1) })
|
||||
onChange({ maxLikesPerDay: Math.max(formData.maxLikesPerDay - 10, 10) })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -124,7 +113,7 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
id="max-likes"
|
||||
type="number"
|
||||
min={10}
|
||||
max={10000}
|
||||
max={500}
|
||||
value={formData.maxLikesPerDay}
|
||||
onChange={(e) => onChange({ maxLikesPerDay: Number.parseInt(e.target.value) || 10 })}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
@@ -143,46 +132,7 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每天最多点赞的次数,支持手动输入(最大10000次)</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="friend-max-likes">好友最大点赞数</Label>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={decrementFriendMaxLikes}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="friend-max-likes"
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={formData.friendMaxLikes}
|
||||
onChange={(e) => onChange({ friendMaxLikes: Number.parseInt(e.target.value) || 1 })}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
|
||||
次/好友
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={incrementFriendMaxLikes}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每个好友最多被点赞的次数</p>
|
||||
<p className="text-xs text-gray-500">设置每天最多点赞的次数</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -232,33 +182,6 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
<p className="text-xs text-gray-500">选择要点赞的内容类型</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 py-2 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="enable-friend-tags" className="cursor-pointer">
|
||||
启用好友标签
|
||||
</Label>
|
||||
<Switch
|
||||
id="enable-friend-tags"
|
||||
checked={formData.enableFriendTags}
|
||||
onCheckedChange={(checked) => onChange({ enableFriendTags: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.enableFriendTags && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="friend-tags">好友标签</Label>
|
||||
<Input
|
||||
id="friend-tags"
|
||||
placeholder="请输入标签"
|
||||
value={formData.friendTags}
|
||||
onChange={(e) => onChange({ friendTags: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">只给有此标签的好友点赞</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Label htmlFor="auto-enabled" className="cursor-pointer">
|
||||
自动开启
|
||||
|
||||
@@ -1,113 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { useState } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
||||
import { Search, RefreshCw } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface ServerDevice {
|
||||
id: number
|
||||
imei: string
|
||||
memo: string
|
||||
wechatId: string
|
||||
alive: number
|
||||
totalFriend: number
|
||||
}
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
interface Device {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
imei: string
|
||||
wxid: string
|
||||
status: "online" | "offline"
|
||||
totalFriend: number
|
||||
usedInPlans: number
|
||||
}
|
||||
|
||||
interface DeviceSelectionDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedDevices: number[]
|
||||
onSelect: (devices: number[]) => void
|
||||
selectedDevices: string[]
|
||||
onSelect: (devices: string[]) => void
|
||||
}
|
||||
|
||||
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [tempSelectedDevices, setTempSelectedDevices] = useState<number[]>(selectedDevices)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
fetchDevices()
|
||||
}
|
||||
}, [open, selectedDevices])
|
||||
|
||||
const fetchDevices = async () => {
|
||||
const loadingToast = showToast("正在加载设备列表...", "loading", true);
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.get<{code: number, msg: string, data: {list: ServerDevice[], total: number}}>('/v1/devices?page=1&limit=100')
|
||||
|
||||
if (response.code === 200 && response.data.list) {
|
||||
const transformedDevices: Device[] = response.data.list.map(device => ({
|
||||
id: device.id,
|
||||
name: device.memo || '设备_' + device.id,
|
||||
imei: device.imei || '',
|
||||
wxid: device.alias || device.wechatId || '',
|
||||
status: device.alive === 1 ? "online" : "offline",
|
||||
totalFriend: device.totalFriend || 0,
|
||||
nickname: device.nickname || ''
|
||||
}))
|
||||
setDevices(transformedDevices)
|
||||
} else {
|
||||
showToast(response.msg || "获取设备列表失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove();
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
const handleDeviceToggle = (deviceId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setTempSelectedDevices(prev => [...prev, deviceId])
|
||||
} else {
|
||||
setTempSelectedDevices(prev => prev.filter(id => id !== deviceId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
onSelect(tempSelectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
// 模拟设备数据
|
||||
const devices: Device[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "设备 1",
|
||||
imei: "IMEI-radz6ewal",
|
||||
wxid: "wxid_98179ujy",
|
||||
status: "offline",
|
||||
usedInPlans: 0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "设备 2",
|
||||
imei: "IMEI-i6iszi6d",
|
||||
wxid: "wxid_viqnaic8",
|
||||
status: "online",
|
||||
usedInPlans: 2,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "设备 3",
|
||||
imei: "IMEI-01z2izj97",
|
||||
wxid: "wxid_9sb23gxr",
|
||||
status: "online",
|
||||
usedInPlans: 2,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "设备 4",
|
||||
imei: "IMEI-x6o9rpcr0",
|
||||
wxid: "wxid_k0gxzbit",
|
||||
status: "online",
|
||||
usedInPlans: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const filteredDevices = devices.filter((device) => {
|
||||
const searchLower = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
(device.name || '').toLowerCase().includes(searchLower) ||
|
||||
(device.imei || '').toLowerCase().includes(searchLower) ||
|
||||
(device.wxid || '').toLowerCase().includes(searchLower)
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.wxid.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" ||
|
||||
@@ -144,50 +107,38 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 -mx-6 px-6" style={{overflowY: 'auto'}}>
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredDevices.map((device) => (
|
||||
<label
|
||||
key={device.id}
|
||||
className="flex items-center space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
style={{paddingLeft: '0px',paddingRight: '0px'}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={tempSelectedDevices.includes(device.id)}
|
||||
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>IMEI: {device.imei || '--'}</div>
|
||||
<div>微信号: {device.wxid || '--'}({device.nickname || ''})</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 -mx-6 px-6">
|
||||
<RadioGroup value={selectedDevices[0]} onValueChange={(value) => onSelect([value])}>
|
||||
{filteredDevices.map((device) => (
|
||||
<label
|
||||
key={device.id}
|
||||
className="flex items-start space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
<RadioGroupItem value={device.id} id={device.id} className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "success" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>IMEI: {device.imei}</div>
|
||||
<div>微信号: {device.wxid}</div>
|
||||
</div>
|
||||
{device.usedInPlans > 0 && (
|
||||
<div className="text-sm text-orange-500 mt-1">已用于 {device.usedInPlans} 个计划</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="mt-4 flex gap-4 -mx-6 px-6">
|
||||
<Button className="flex-1" onClick={handleConfirm}>确认</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -260,4 +260,3 @@ export function LikeConfig({ initialData, onSave, onBack }: LikeConfigProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,14 @@ export function TagSelector({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Button variant="outline" className="flex-1" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button className="flex-1" onClick={onComplete} disabled={selectedTags.length === 0}>
|
||||
完成设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -240,4 +240,3 @@ export function TimeSettings({ initialData, onSave }: TimeSettingsProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,37 +2,29 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Search, Users } from "lucide-react"
|
||||
import { ChevronLeft, Search } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { StepIndicator } from "../components/step-indicator"
|
||||
import { BasicSettings } from "../components/basic-settings"
|
||||
import { DeviceSelectionDialog } from "../components/device-selection-dialog"
|
||||
import { TagSelector } from "../components/tag-selector"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
import { WechatFriendSelector, WechatFriend } from "@/components/WechatFriendSelector"
|
||||
|
||||
export default function NewAutoLikePage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [isFriendSelectorOpen, setIsFriendSelectorOpen] = useState(false)
|
||||
const [selectedFriends, setSelectedFriends] = useState<WechatFriend[]>([])
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
likeInterval: 5, // 默认5秒
|
||||
maxLikesPerDay: 200, // 默认200次
|
||||
friendMaxLikes: 3, // 默认每个好友最多点赞3次
|
||||
timeRange: { start: "08:00", end: "22:00" },
|
||||
contentTypes: ["text", "image", "video"],
|
||||
enabled: true,
|
||||
enableFriendTags: false, // 默认不启用好友标签
|
||||
friendTags: "", // 好友标签字段
|
||||
selectedDevices: [] as number[],
|
||||
selectedDevices: [] as string[],
|
||||
selectedTags: [] as string[],
|
||||
tagOperator: "and" as "and" | "or",
|
||||
friends: [] as string[],
|
||||
})
|
||||
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
@@ -47,50 +39,15 @@ export default function NewAutoLikePage() {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1))
|
||||
}
|
||||
|
||||
const handleSelectFriends = () => {
|
||||
if (formData.selectedDevices.length === 0) {
|
||||
showToast("请先选择设备", "error")
|
||||
return
|
||||
}
|
||||
setIsFriendSelectorOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveSelectedFriends = (friends: WechatFriend[]) => {
|
||||
const ids = friends.map(f => f.id)
|
||||
setSelectedFriends(friends)
|
||||
handleUpdateFormData({ friends: ids })
|
||||
setIsFriendSelectorOpen(false)
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/create', {
|
||||
type: 1,
|
||||
name: formData.taskName,
|
||||
interval: formData.likeInterval,
|
||||
maxLikes: formData.maxLikesPerDay,
|
||||
friendMaxLikes: formData.friendMaxLikes,
|
||||
startTime: formData.timeRange.start,
|
||||
endTime: formData.timeRange.end,
|
||||
contentTypes: formData.contentTypes,
|
||||
enabled: formData.enabled,
|
||||
devices: formData.selectedDevices,
|
||||
friends: formData.friends,
|
||||
enableFriendTags: formData.enableFriendTags,
|
||||
friendTags: formData.enableFriendTags ? formData.friendTags : "",
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast(response.msg, "success");
|
||||
router.push("/workspace/auto-like");
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("创建自动点赞任务失败:", error);
|
||||
showToast(error?.message || "请检查网络连接或稍后再试", "error");
|
||||
}
|
||||
};
|
||||
console.log("Form submitted:", formData)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "自动点赞任务已创建并开始执行",
|
||||
})
|
||||
router.push("/workspace/auto-like")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FA] pb-20">
|
||||
@@ -155,41 +112,33 @@ export default function NewAutoLikePage() {
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className="px-6">
|
||||
<div className="relative">
|
||||
<Users className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择微信好友"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={handleSelectFriends}
|
||||
readOnly
|
||||
value={formData.friends.length > 0 ? `已选择 ${formData.friends.length} 个好友` : ""}
|
||||
/>
|
||||
</div>
|
||||
{formData.friends.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选好友:{formData.friends.length} 个</div>
|
||||
)}
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
<WechatFriendSelector
|
||||
open={isFriendSelectorOpen}
|
||||
onOpenChange={setIsFriendSelectorOpen}
|
||||
selectedFriends={selectedFriends}
|
||||
onSelect={handleSaveSelectedFriends}
|
||||
devices={formData.selectedDevices}
|
||||
<TagSelector
|
||||
selectedTags={formData.selectedTags}
|
||||
tagOperator={formData.tagOperator}
|
||||
onTagsChange={(tags) => handleUpdateFormData({ selectedTags: tags })}
|
||||
onOperatorChange={(operator) => handleUpdateFormData({ tagOperator: operator })}
|
||||
onBack={handlePrev}
|
||||
onComplete={handleComplete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="fixed bottom-0 left-0 right-0 h-16 bg-white border-t flex items-center justify-around px-6">
|
||||
<button className="flex flex-col items-center text-blue-600">
|
||||
<span className="text-sm mt-1">首页</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">场景获客</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">工作台</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ChevronLeft,
|
||||
Plus,
|
||||
@@ -28,191 +28,113 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface TaskConfig {
|
||||
id: number
|
||||
workbenchId: number
|
||||
interval: number
|
||||
maxLikes: number
|
||||
friendMaxLikes?: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
contentTypes: string[]
|
||||
devices: number[]
|
||||
targetGroups: string[]
|
||||
tagOperator: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
todayLikeCount?: number
|
||||
totalLikeCount?: number
|
||||
friends?: string[]
|
||||
enableFriendTags?: boolean
|
||||
friendTags?: string
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: number
|
||||
interface LikeTask {
|
||||
id: string
|
||||
name: string
|
||||
type: number
|
||||
status: number
|
||||
autoStart: number
|
||||
status: "running" | "paused"
|
||||
deviceCount: number
|
||||
targetGroup: string
|
||||
likeCount: number
|
||||
lastLikeTime: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
config: TaskConfig
|
||||
}
|
||||
|
||||
interface TaskListResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
list: Task[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
creator: string
|
||||
likeInterval: number
|
||||
maxLikesPerDay: number
|
||||
timeRange: { start: string; end: string }
|
||||
contentTypes: string[]
|
||||
targetTags: string[]
|
||||
}
|
||||
|
||||
export default function AutoLikePage() {
|
||||
const router = useRouter()
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<number | null>(null)
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchName, setSearchName] = useState("")
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const pageSize = 10
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null)
|
||||
const [tasks, setTasks] = useState<LikeTask[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "高频互动点赞",
|
||||
deviceCount: 2,
|
||||
targetGroup: "高频互动好友",
|
||||
likeCount: 156,
|
||||
lastLikeTime: "2025-02-06 13:12:35",
|
||||
createTime: "2024-11-20 19:04:14",
|
||||
creator: "admin",
|
||||
status: "running",
|
||||
likeInterval: 5,
|
||||
maxLikesPerDay: 200,
|
||||
timeRange: { start: "08:00", end: "22:00" },
|
||||
contentTypes: ["text", "image", "video"],
|
||||
targetTags: ["高频互动", "高意向", "男性"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "潜在客户点赞",
|
||||
deviceCount: 1,
|
||||
targetGroup: "潜在客户",
|
||||
likeCount: 89,
|
||||
lastLikeTime: "2024-03-04 14:09:35",
|
||||
createTime: "2024-03-04 14:29:04",
|
||||
creator: "manager",
|
||||
status: "paused",
|
||||
likeInterval: 10,
|
||||
maxLikesPerDay: 150,
|
||||
timeRange: { start: "09:00", end: "21:00" },
|
||||
contentTypes: ["image", "video"],
|
||||
targetTags: ["潜在客户", "中意向", "女性"],
|
||||
},
|
||||
])
|
||||
|
||||
const fetchTasks = async (page: number, name?: string) => {
|
||||
const loadingToast = showToast("正在加载任务列表...", "loading", true);
|
||||
try {
|
||||
setLoading(true)
|
||||
const queryParams = new URLSearchParams({
|
||||
type: '1',
|
||||
page: page.toString(),
|
||||
limit: pageSize.toString(),
|
||||
})
|
||||
if (name) {
|
||||
queryParams.append('keyword', name)
|
||||
}
|
||||
const response = await api.get<TaskListResponse>(`/v1/workbench/list?${queryParams.toString()}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
setTasks(response.data.list)
|
||||
setTotal(response.data.total)
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取任务列表失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove();
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks(currentPage, searchName)
|
||||
}, [currentPage])
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchTasks(1, searchName)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchTasks(currentPage, searchName)
|
||||
}
|
||||
|
||||
const toggleExpand = (taskId: number) => {
|
||||
const toggleExpand = (taskId: string) => {
|
||||
setExpandedTaskId(expandedTaskId === taskId ? null : taskId)
|
||||
}
|
||||
|
||||
const handleDelete = async (taskId: number) => {
|
||||
const loadingToast = showToast("正在删除任务...", "loading", true);
|
||||
try {
|
||||
const response = await api.delete<ApiResponse>(`/v1/workbench/delete?id=${taskId}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
// 删除成功后刷新列表
|
||||
loadingToast.remove();
|
||||
fetchTasks(currentPage, searchName)
|
||||
showToast(response.msg || "已成功删除点赞任务", "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除任务失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
const handleDelete = (taskId: string) => {
|
||||
setTasks(tasks.filter((task) => task.id !== taskId))
|
||||
toast({
|
||||
title: "删除成功",
|
||||
description: "已成功删除点赞任务",
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (taskId: number) => {
|
||||
const handleEdit = (taskId: string) => {
|
||||
router.push(`/workspace/auto-like/${taskId}/edit`)
|
||||
}
|
||||
|
||||
const handleView = (taskId: number) => {
|
||||
const handleView = (taskId: string) => {
|
||||
router.push(`/workspace/auto-like/${taskId}`)
|
||||
}
|
||||
|
||||
const handleCopy = async (taskId: number) => {
|
||||
const loadingToast = showToast("正在复制任务...", "loading", true);
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/copy', {
|
||||
id: taskId
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 复制成功后刷新列表
|
||||
loadingToast.remove();
|
||||
fetchTasks(currentPage, searchName)
|
||||
showToast(response.msg || "已成功复制点赞任务", "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
const handleCopy = (taskId: string) => {
|
||||
const taskToCopy = tasks.find((task) => task.id === taskId)
|
||||
if (taskToCopy) {
|
||||
const newTask = {
|
||||
...taskToCopy,
|
||||
id: `${Date.now()}`,
|
||||
name: `${taskToCopy.name} (复制)`,
|
||||
createTime: new Date().toISOString().replace("T", " ").substring(0, 19),
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("复制任务失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
setTasks([...tasks, newTask])
|
||||
toast({
|
||||
title: "复制成功",
|
||||
description: "已成功复制点赞任务",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTaskStatus = async (taskId: number, currentStatus: number) => {
|
||||
const loadingToast = showToast("正在更新任务状态...", "loading", true);
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update-status', {
|
||||
id: taskId,
|
||||
status: currentStatus === 1 ? 2 : 1
|
||||
const toggleTaskStatus = (taskId: string) => {
|
||||
setTasks(
|
||||
tasks.map((task) =>
|
||||
task.id === taskId ? { ...task, status: task.status === "running" ? "paused" : "running" } : task,
|
||||
),
|
||||
)
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
if (task) {
|
||||
toast({
|
||||
title: task.status === "running" ? "已暂停" : "已启动",
|
||||
description: `${task.name}任务${task.status === "running" ? "已暂停" : "已启动"}`,
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新本地状态
|
||||
setTasks(tasks.map(task =>
|
||||
task.id === taskId
|
||||
? { ...task, status: currentStatus === 1 ? 2 : 1 }
|
||||
: task
|
||||
))
|
||||
|
||||
const newStatus = currentStatus === 1 ? 2 : 1
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || `任务${newStatus === 1 ? "已启动" : "已暂停"}`, "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新任务状态失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,19 +162,13 @@ export default function AutoLikePage() {
|
||||
<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="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Input placeholder="搜索任务名称" className="pl-9" />
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -263,12 +179,12 @@ export default function AutoLikePage() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={task.status === 1 ? "default" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
<Badge variant={task.status === "running" ? "success" : "secondary"}>
|
||||
{task.status === "running" ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
|
||||
<Switch checked={task.status === "running"} onCheckedChange={() => toggleTaskStatus(task.id)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
@@ -297,33 +213,31 @@ export default function AutoLikePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="mb-1">执行设备:{task.config.devices.length} 个</div>
|
||||
<div className="mb-1">目标人群:{task.config.friends?.length || 0} 个</div>
|
||||
<div>更新时间:{task.updateTime}</div>
|
||||
<div>执行设备:{task.deviceCount} 个</div>
|
||||
<div>目标人群:{task.targetGroup}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="mb-1">点赞间隔:{task.config.interval} 秒</div>
|
||||
<div className="mb-1">每日上限:{task.config.maxLikes} 次</div>
|
||||
<div>创建时间:{task.createTime}</div>
|
||||
<div>已点赞:{task.likeCount} 次</div>
|
||||
<div>创建人:{task.creator}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 border-t pt-4">
|
||||
<div className="text-sm">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<ThumbsUp className="h-4 w-4 mr-2 text-blue-500" />
|
||||
<span className="text-gray-500">今日点赞:</span>
|
||||
<span className="ml-1 font-medium">{task.config.todayLikeCount || 0} 次</span>
|
||||
</div>
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
上次点赞:{task.lastLikeTime}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="flex items-center">
|
||||
<ThumbsUp className="h-4 w-4 mr-2 text-green-500" />
|
||||
<span className="text-gray-500">总点赞数:</span>
|
||||
<span className="ml-1 font-medium">{task.config.totalLikeCount || 0} 次</span>
|
||||
</div>
|
||||
<span>创建时间:{task.createTime}</span>
|
||||
<Button variant="ghost" size="sm" className="ml-2 p-0 h-6 w-6" onClick={() => toggleExpand(task.id)}>
|
||||
{expandedTaskId === task.id ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,20 +252,16 @@ export default function AutoLikePage() {
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">点赞间隔:</span>
|
||||
<span>{task.config.interval} 秒</span>
|
||||
<span>{task.likeInterval} 秒</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">每日最大点赞数:</span>
|
||||
<span>{task.config.maxLikes} 次</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">每个好友最大点赞数:</span>
|
||||
<span>{task.config.friendMaxLikes || 3} 次</span>
|
||||
<span>{task.maxLikesPerDay} 次</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">执行时间段:</span>
|
||||
<span>
|
||||
{task.config.startTime} - {task.config.endTime}
|
||||
{task.timeRange.start} - {task.timeRange.end}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,23 +274,12 @@ export default function AutoLikePage() {
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.config.targetGroups.map((tag, index) => (
|
||||
<Badge key={`${task.id}-tag-${index}-${tag}`} variant="outline" className="bg-gray-50">
|
||||
{task.targetTags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="bg-gray-50">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
匹配方式:{task.config.tagOperator === 1 ? "满足所有标签" : "满足任一标签"}
|
||||
</div>
|
||||
{task.config.enableFriendTags && task.config.friendTags && (
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium mb-1">好友标签:</div>
|
||||
<Badge variant="outline" className="bg-blue-50 border-blue-200">
|
||||
{task.config.friendTags}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -391,46 +290,40 @@ export default function AutoLikePage() {
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.config.contentTypes.map((type, index) => (
|
||||
<Badge key={`${task.id}-type-${index}-${type}`} variant="outline" className="bg-gray-50">
|
||||
{task.contentTypes.map((type) => (
|
||||
<Badge key={type} variant="outline" className="bg-gray-50">
|
||||
{type === "text" ? "文字" : type === "image" ? "图片" : "视频"}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-5 w-5 mr-2 text-gray-500" />
|
||||
<h4 className="font-medium">执行进度</h4>
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-500">今日已点赞:</span>
|
||||
<span>
|
||||
{task.likeCount} / {task.maxLikesPerDay}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(task.likeCount / task.maxLikesPerDay) * 100}
|
||||
className="h-2"
|
||||
indicatorClassName="bg-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{total > pageSize && (
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm text-gray-500">第 {currentPage} 页</span>
|
||||
<span className="text-sm text-gray-500">共 {Math.ceil(total / pageSize)} 页</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(total / pageSize), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,137 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import { use, useState, useEffect } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { StepIndicator } from "../../components/step-indicator"
|
||||
import { BasicSettings } from "../../components/basic-settings"
|
||||
import { GroupSelector } from "../../components/group-selector"
|
||||
import { ContentSelector } from "../../components/content-selector"
|
||||
import type { WechatGroup, ContentLibrary } from "@/types/group-push"
|
||||
import { MessageEditor } from "../../components/message-editor"
|
||||
import { FriendSelector } from "../../components/friend-selector"
|
||||
import { ArrowLeft, ArrowRight, Check, Loader2 } from "lucide-react"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤 2", subtitle: "选择社群" },
|
||||
{ id: 3, title: "步骤 3", subtitle: "选择内容库" },
|
||||
{ id: 4, title: "步骤 4", subtitle: "京东联盟" },
|
||||
]
|
||||
const steps = ["推送信息", "选择好友"]
|
||||
|
||||
export default function EditGroupPushPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
// 模拟数据
|
||||
const mockPushTasks = {
|
||||
"1": {
|
||||
id: "1",
|
||||
name: "618活动推广消息",
|
||||
content: {
|
||||
text: "618年中大促,全场商品5折起!限时抢购,先到先得!",
|
||||
images: ["/placeholder.svg?height=200&width=200"],
|
||||
video: null,
|
||||
link: "https://example.com/618",
|
||||
},
|
||||
selectedFriends: ["1", "3", "5"],
|
||||
pushTime: "2025-06-18 10:00:00",
|
||||
progress: 100,
|
||||
status: "已完成",
|
||||
},
|
||||
"2": {
|
||||
id: "2",
|
||||
name: "新品上市通知",
|
||||
content: {
|
||||
text: "我们的新产品已经上市,快来体验吧!",
|
||||
images: [],
|
||||
video: "/placeholder.svg?height=400&width=400",
|
||||
link: null,
|
||||
},
|
||||
selectedFriends: ["2", "4", "6", "8"],
|
||||
pushTime: "2025-03-25 09:30:00",
|
||||
progress: 75,
|
||||
status: "进行中",
|
||||
},
|
||||
}
|
||||
|
||||
export default function EditPushPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
pushTimeStart: "06:00",
|
||||
pushTimeEnd: "23:59",
|
||||
dailyPushCount: 20,
|
||||
pushOrder: "latest" as "earliest" | "latest",
|
||||
isLoopPush: false,
|
||||
isImmediatePush: false,
|
||||
isEnabled: false,
|
||||
groups: [] as WechatGroup[],
|
||||
contentLibraries: [] as ContentLibrary[],
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [taskName, setTaskName] = useState("")
|
||||
const [messageContent, setMessageContent] = useState({
|
||||
text: "",
|
||||
images: [],
|
||||
video: null,
|
||||
link: null,
|
||||
})
|
||||
const [selectedFriends, setSelectedFriends] = useState<string[]>([])
|
||||
|
||||
// 拉取详情
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await api.get(`/v1/workbench/detail?id=${id}`) as any
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
pushTimeStart: data.config?.startTime || "06:00",
|
||||
pushTimeEnd: data.config?.endTime || "23:59",
|
||||
dailyPushCount: data.config?.maxPerDay || 20,
|
||||
pushOrder: data.config?.pushOrder === 2 ? "latest" : "earliest",
|
||||
isLoopPush: data.config?.isLoop === 1,
|
||||
isImmediatePush: false, // 详情接口如有此字段可补充
|
||||
isEnabled: data.status === 1,
|
||||
groups: (data.config.groupList || []).map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: item.groupName,
|
||||
avatar: item.groupAvatar || item.avatar,
|
||||
serviceAccount: {
|
||||
id: item.ownerWechatId,
|
||||
name: item.nickName,
|
||||
avatar: "",
|
||||
},
|
||||
})),
|
||||
contentLibraries: (data.config.contentLibraryList || []).map((item: any) => ({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
sourceType: item.sourceType,
|
||||
selectedFriends: item.selectedFriends || [],
|
||||
selectedGroups: item.selectedGroups || [],
|
||||
})),
|
||||
})
|
||||
} else {
|
||||
showToast(res.msg || "获取详情失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
// 模拟加载数据
|
||||
setTimeout(() => {
|
||||
const task = mockPushTasks[params.id as keyof typeof mockPushTasks]
|
||||
if (task) {
|
||||
setTaskName(task.name)
|
||||
setMessageContent(task.content)
|
||||
setSelectedFriends(task.selectedFriends)
|
||||
}
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
}, [params.id])
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep === 0) {
|
||||
// 验证第一步
|
||||
if (!taskName.trim()) {
|
||||
toast({
|
||||
title: "请输入任务名称",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!messageContent.text && messageContent.images.length === 0 && !messageContent.video && !messageContent.link) {
|
||||
toast({
|
||||
title: "请添加至少一种消息内容",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
fetchDetail()
|
||||
// eslint-disable-next-line
|
||||
}, [id])
|
||||
|
||||
const handleBasicSettingsNext = (values: any) => {
|
||||
setFormData((prev) => ({ ...prev, ...values }))
|
||||
setCurrentStep(2)
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handleGroupsChange = (groups: WechatGroup[]) => {
|
||||
setFormData((prev) => ({ ...prev, groups }))
|
||||
const handlePrevious = () => {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}
|
||||
|
||||
const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => {
|
||||
setFormData((prev) => ({ ...prev, contentLibraries }))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const loadingToast = showToast("正在保存...", "loading", true)
|
||||
try {
|
||||
const paramsData = {
|
||||
id,
|
||||
name: formData.name,
|
||||
type: 3,
|
||||
pushType: 1,
|
||||
startTime: formData.pushTimeStart,
|
||||
endTime: formData.pushTimeEnd,
|
||||
maxPerDay: formData.dailyPushCount,
|
||||
pushOrder: formData.pushOrder === "latest" ? 2 : 1,
|
||||
isLoop: formData.isLoopPush ? 1 : 0,
|
||||
status: formData.isEnabled ? 1 : 0,
|
||||
groups: (formData.groups || []).filter(g => g && g.id).map((g: any) => g.id),
|
||||
contentLibraries: (formData.contentLibraries || []).filter(c => c && c.id).map((c: any) => c.id),
|
||||
}
|
||||
const res = await api.post("/v1/workbench/update", paramsData) as any
|
||||
loadingToast.remove()
|
||||
if (res.code === 200) {
|
||||
showToast("保存成功", "success")
|
||||
router.push("/workspace/group-push")
|
||||
} else {
|
||||
showToast(res.msg || "保存失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
loadingToast.remove()
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
const handleSubmit = () => {
|
||||
if (selectedFriends.length === 0) {
|
||||
toast({
|
||||
title: "请选择至少一个好友",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
// 模拟提交
|
||||
toast({
|
||||
title: "推送任务更新成功",
|
||||
description: `已更新推送任务 "${taskName}"`,
|
||||
})
|
||||
|
||||
// 跳转回列表页
|
||||
router.push("/workspace/group-push")
|
||||
}
|
||||
|
||||
@@ -139,7 +123,7 @@ export default function EditGroupPushPage({ params }: { params: Promise<{ id: st
|
||||
return (
|
||||
<div className="container mx-auto py-6 flex justify-center items-center min-h-[60vh]">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="animate-spin h-8 w-8 text-blue-500">⏳</span>
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
<p className="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,78 +131,62 @@ export default function EditGroupPushPage({ params }: { params: Promise<{ id: st
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-4 px-4 sm:px-6 md:py-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/workspace/group-push")} className="mr-2">
|
||||
<div className="container mx-auto py-6">
|
||||
{/* 顶部导航栏 */}
|
||||
<div className="flex items-center justify-between mb-6 relative">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/workspace/group-push")} className="mr-auto">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-bold">编辑社群推送任务</h1>
|
||||
<h1 className="text-2xl font-bold absolute left-1/2 transform -translate-x-1/2">编辑推送</h1>
|
||||
<div className="w-10"></div> {/* 占位元素,保持标题居中 */}
|
||||
</div>
|
||||
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings
|
||||
defaultValues={{
|
||||
name: formData.name,
|
||||
pushTimeStart: formData.pushTimeStart,
|
||||
pushTimeEnd: formData.pushTimeEnd,
|
||||
dailyPushCount: formData.dailyPushCount,
|
||||
pushOrder: formData.pushOrder,
|
||||
isLoopPush: formData.isLoopPush,
|
||||
isImmediatePush: formData.isImmediatePush,
|
||||
isEnabled: formData.isEnabled,
|
||||
}}
|
||||
onNext={handleBasicSettingsNext}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{currentStep === 0 ? (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="task-name">任务名称</Label>
|
||||
<Input
|
||||
id="task-name"
|
||||
placeholder="请输入任务名称"
|
||||
value={taskName}
|
||||
onChange={(e) => setTaskName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentStep === 2 && (
|
||||
<GroupSelector
|
||||
selectedGroups={formData.groups}
|
||||
onGroupsChange={handleGroupsChange}
|
||||
onPrevious={() => setCurrentStep(1)}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Label>消息内容</Label>
|
||||
<MessageEditor onMessageChange={setMessageContent} defaultValues={messageContent} />
|
||||
</div>
|
||||
|
||||
{currentStep === 3 && (
|
||||
<ContentSelector
|
||||
selectedLibraries={formData.contentLibraries}
|
||||
onLibrariesChange={handleLibrariesChange}
|
||||
onPrevious={() => setCurrentStep(2)}
|
||||
onNext={() => setCurrentStep(4)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 4 && (
|
||||
<div className="space-y-6">
|
||||
<div className="border rounded-md p-8 text-center text-gray-500">
|
||||
京东联盟设置(此步骤为占位,实际功能待开发)
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleNext}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<FriendSelector onSelectionChange={setSelectedFriends} defaultSelectedFriendIds={selectedFriends} />
|
||||
|
||||
<div className="flex space-x-2 justify-center sm:justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setCurrentStep(3)} className="flex-1 sm:flex-none">
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSave} className="flex-1 sm:flex-none">
|
||||
完成
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={handleCancel} className="flex-1 sm:flex-none">
|
||||
取消
|
||||
</Button>
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={handlePrevious}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ interface BasicSettingsProps {
|
||||
dailyPushCount: number
|
||||
pushOrder: "earliest" | "latest"
|
||||
isLoopPush: boolean
|
||||
pushTypePush: boolean
|
||||
isImmediatePush: boolean
|
||||
isEnabled: boolean
|
||||
}
|
||||
onNext: (values: any) => void
|
||||
@@ -32,7 +32,7 @@ export function BasicSettings({
|
||||
dailyPushCount: 20,
|
||||
pushOrder: "latest",
|
||||
isLoopPush: false,
|
||||
pushTypePush: false,
|
||||
isImmediatePush: false,
|
||||
isEnabled: false,
|
||||
},
|
||||
onNext,
|
||||
@@ -164,20 +164,20 @@ export function BasicSettings({
|
||||
|
||||
{/* 是否立即推送 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="pushTypePush" className="flex items-center text-sm font-medium">
|
||||
<Label htmlFor="isImmediatePush" className="flex items-center text-sm font-medium">
|
||||
<span className="text-red-500 mr-1">*</span>是否立即推送:
|
||||
</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={values.pushTypePush ? "text-gray-400" : "text-gray-900"}>否</span>
|
||||
<span className={values.isImmediatePush ? "text-gray-400" : "text-gray-900"}>否</span>
|
||||
<Switch
|
||||
id="pushTypePush"
|
||||
checked={values.pushTypePush}
|
||||
onCheckedChange={(checked) => handleChange("pushTypePush", checked)}
|
||||
id="isImmediatePush"
|
||||
checked={values.isImmediatePush}
|
||||
onCheckedChange={(checked) => handleChange("isImmediatePush", checked)}
|
||||
/>
|
||||
<span className={values.pushTypePush ? "text-gray-900" : "text-gray-400"}>是</span>
|
||||
<span className={values.isImmediatePush ? "text-gray-900" : "text-gray-400"}>是</span>
|
||||
</div>
|
||||
</div>
|
||||
{values.pushTypePush && (
|
||||
{values.isImmediatePush && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-sm text-yellow-700">
|
||||
如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群
|
||||
</div>
|
||||
@@ -216,4 +216,3 @@ export function BasicSettings({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
@@ -8,8 +8,32 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Search, Plus, Trash2 } from "lucide-react"
|
||||
import type { ContentLibrary } from "@/types/group-push"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
// 模拟数据
|
||||
const mockContentLibraries: ContentLibrary[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "测试11",
|
||||
targets: [{ id: "t1", avatar: "/placeholder.svg?height=40&width=40" }],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "测试166666",
|
||||
targets: [
|
||||
{ id: "t2", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "t3", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "t4", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "产品介绍",
|
||||
targets: [
|
||||
{ id: "t5", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "t6", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
interface ContentSelectorProps {
|
||||
selectedLibraries: ContentLibrary[]
|
||||
@@ -30,44 +54,6 @@ export function ContentSelector({
|
||||
}: ContentSelectorProps) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [libraries, setLibraries] = useState<ContentLibrary[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 拉取内容库列表
|
||||
const fetchLibraries = async (keyword = "") => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: "1",
|
||||
limit: "100",
|
||||
keyword: keyword.trim(),
|
||||
})
|
||||
const res = await api.get(`/v1/content/library/list?${params.toString()}`) as any
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
setLibraries(res.data.list)
|
||||
} else {
|
||||
setLibraries([])
|
||||
showToast(res.msg || "获取内容库失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
setLibraries([])
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗打开/搜索时拉取
|
||||
useEffect(() => {
|
||||
if (isDialogOpen) {
|
||||
fetchLibraries(searchTerm)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isDialogOpen])
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchLibraries(searchTerm)
|
||||
}
|
||||
|
||||
const handleAddLibrary = (library: ContentLibrary) => {
|
||||
if (!selectedLibraries.some((l) => l.id === library.id)) {
|
||||
@@ -80,7 +66,7 @@ export function ContentSelector({
|
||||
onLibrariesChange(selectedLibraries.filter((library) => library.id !== libraryId))
|
||||
}
|
||||
|
||||
const filteredLibraries = libraries.filter((library) =>
|
||||
const filteredLibraries = mockContentLibraries.filter((library) =>
|
||||
library.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
@@ -118,14 +104,14 @@ export function ContentSelector({
|
||||
<TableCell>{library.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex -space-x-2 flex-wrap">
|
||||
{(((library as any).sourceType === 1 ? (library as any).selectedFriends : (library as any).selectedGroups) || []).map((target: any) => (
|
||||
{library.targets.map((target) => (
|
||||
<div
|
||||
key={target.id}
|
||||
className="w-10 h-10 rounded-md overflow-hidden border-2 border-white"
|
||||
>
|
||||
<img
|
||||
src={target.avatar || "/placeholder.svg?height=40&width=40"}
|
||||
alt={target.nickname || target.name || "Target"}
|
||||
alt="Target"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -175,24 +161,17 @@ export function ContentSelector({
|
||||
<DialogTitle>选择内容库</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="搜索内容库名称"
|
||||
className="pl-8"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Button size="sm" variant="outline" onClick={handleSearch}>搜索</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-400 py-8">加载中...</div>
|
||||
) : filteredLibraries.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">暂无内容库</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -209,14 +188,11 @@ export function ContentSelector({
|
||||
<TableCell>{library.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex -space-x-2 flex-wrap">
|
||||
{(((library as any).sourceType === 1 ? (library as any).selectedFriends : (library as any).selectedGroups) || []).map((target: any) => (
|
||||
<div
|
||||
key={target.id}
|
||||
className="w-10 h-10 rounded-md overflow-hidden border-2 border-white"
|
||||
>
|
||||
{library.targets.map((target) => (
|
||||
<div key={target.id} className="w-10 h-10 rounded-md overflow-hidden border-2 border-white">
|
||||
<img
|
||||
src={target.avatar || "/placeholder.svg?height=40&width=40"}
|
||||
alt={target.nickname || target.name || "Target"}
|
||||
alt="Target"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
@@ -225,19 +201,19 @@ export function ContentSelector({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="default"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddLibrary(library)}
|
||||
disabled={selectedLibraries.some((l) => String(l.id) === String(library.id))}
|
||||
disabled={selectedLibraries.some((l) => l.id === library.id)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{selectedLibraries.some((l) => String(l.id) === String(library.id)) ? "已添加" : "添加"}
|
||||
{selectedLibraries.some((l) => l.id === library.id) ? "已选择" : "选择"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -245,4 +221,3 @@ export function ContentSelector({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -344,4 +344,3 @@ export function FriendSelector({ onSelectionChange, defaultSelectedFriendIds = [
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
@@ -8,8 +8,40 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Search, Plus, Trash2 } from "lucide-react"
|
||||
import type { WechatGroup } from "@/types/group-push"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
// 模拟数据
|
||||
const mockGroups: WechatGroup[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "快捷语",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
serviceAccount: {
|
||||
id: "sa1",
|
||||
name: "贝蒂喜品牌wxid_rtlwsjytjk1991",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "产品交流群",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
serviceAccount: {
|
||||
id: "sa1",
|
||||
name: "贝蒂喜品牌wxid_rtlwsjytjk1991",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "客户服务群",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
serviceAccount: {
|
||||
id: "sa2",
|
||||
name: "客服小助手wxid_abc123",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
interface GroupSelectorProps {
|
||||
selectedGroups: WechatGroup[]
|
||||
@@ -31,63 +63,6 @@ export function GroupSelector({
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [serviceFilter, setServiceFilter] = useState("")
|
||||
const [groups, setGroups] = useState<WechatGroup[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const pageSize = 10
|
||||
|
||||
// 拉取群列表
|
||||
const fetchGroups = async (page = 1, keyword = "") => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: pageSize.toString(),
|
||||
keyword: keyword.trim(),
|
||||
})
|
||||
const res = await api.get(`/v1/workbench/group-list?${params.toString()}`) as any
|
||||
if (res.code === 200 && Array.isArray(res.data.list)) {
|
||||
const mappedList = (res.data.list || []).map((item: any) => ({
|
||||
...item, // 保留所有原始字段,方便渲染
|
||||
id: String(item.id),
|
||||
name: item.groupName,
|
||||
avatar: item.groupAvatar,
|
||||
serviceAccount: {
|
||||
id: item.ownerWechatId,
|
||||
name: item.nickName,
|
||||
avatar: item.avatar, // 可补充
|
||||
},
|
||||
}))
|
||||
setGroups(mappedList)
|
||||
setTotal(res.data.total || mappedList.length)
|
||||
} else {
|
||||
setGroups([])
|
||||
setTotal(0)
|
||||
showToast(res.msg || "获取群列表失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
setGroups([])
|
||||
setTotal(0)
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗打开/搜索/翻页时拉取
|
||||
useEffect(() => {
|
||||
if (isDialogOpen) {
|
||||
fetchGroups(currentPage, searchTerm)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isDialogOpen, currentPage])
|
||||
|
||||
// 搜索时重置页码
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchGroups(1, searchTerm)
|
||||
}
|
||||
|
||||
const handleAddGroup = (group: WechatGroup) => {
|
||||
if (!selectedGroups.some((g) => g.id === group.id)) {
|
||||
@@ -100,10 +75,10 @@ export function GroupSelector({
|
||||
onGroupsChange(selectedGroups.filter((group) => group.id !== groupId))
|
||||
}
|
||||
|
||||
// 过滤客服(本地过滤)
|
||||
const filteredGroups = groups.filter((group) => {
|
||||
const matchesService = !serviceFilter || group.serviceAccount?.name?.includes(serviceFilter)
|
||||
return matchesService
|
||||
const filteredGroups = mockGroups.filter((group) => {
|
||||
const matchesSearch = group.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesService = !serviceFilter || group.serviceAccount.name.includes(serviceFilter)
|
||||
return matchesSearch && matchesService
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -146,14 +121,22 @@ export function GroupSelector({
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{group.name}</div>
|
||||
<div className="text-xs text-gray-500">群主:{group.serviceAccount?.name}</div>
|
||||
</div>
|
||||
<span className="text-sm truncate">{group.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs text-gray-500">群主:{group.serviceAccount?.name}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex -space-x-2 flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden border-2 border-white">
|
||||
<img
|
||||
src={group.serviceAccount.avatar || "/placeholder.svg?height=32&width=32"}
|
||||
alt={group.serviceAccount.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs truncate">{group.serviceAccount.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
@@ -208,7 +191,6 @@ export function GroupSelector({
|
||||
className="pl-8"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:w-64">
|
||||
@@ -218,73 +200,54 @@ export function GroupSelector({
|
||||
onChange={(e) => setServiceFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleSearch}>搜索</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto max-h-96">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-400 py-8">加载中...</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">暂无群聊</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">序号</TableHead>
|
||||
<TableHead>群信息</TableHead>
|
||||
<TableHead>推送客服</TableHead>
|
||||
<TableHead className="w-20">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredGroups.map((group, index) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell>{(currentPage - 1) * pageSize + index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 rounded overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={group.avatar || "/placeholder.svg?height=32&width=32"}
|
||||
alt={group.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{group.name}</div>
|
||||
<div className="text-xs text-gray-500">群主:{group.serviceAccount?.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">序号</TableHead>
|
||||
<TableHead>群信息</TableHead>
|
||||
<TableHead>归属客服</TableHead>
|
||||
<TableHead className="w-20">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredGroups.map((group, index) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 rounded overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={group.avatar || "/placeholder.svg?height=32&width=32"}
|
||||
alt={group.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs text-gray-500">群主:{group.serviceAccount?.name}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleAddGroup(group)}
|
||||
disabled={selectedGroups.some((g) => g.id === group.id)}
|
||||
>
|
||||
{selectedGroups.some((g) => g.id === group.id) ? "已添加" : "添加"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<span className="text-sm truncate">{group.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="truncate">{group.serviceAccount.name}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddGroup(group)}
|
||||
disabled={selectedGroups.some((g) => g.id === group.id)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{selectedGroups.some((g) => g.id === group.id) ? "已选择" : "选择"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{/* 分页 */}
|
||||
{total > pageSize && (
|
||||
<div className="flex justify-center items-center gap-2 mt-4">
|
||||
<Button size="sm" variant="outline" disabled={currentPage === 1} onClick={() => setCurrentPage(p => Math.max(1, p - 1))}>上一页</Button>
|
||||
<span className="text-sm text-gray-500">第 {currentPage} / {Math.ceil(total / pageSize)} 页</span>
|
||||
<Button size="sm" variant="outline" disabled={currentPage === Math.ceil(total / pageSize)} onClick={() => setCurrentPage(p => Math.min(Math.ceil(total / pageSize), p + 1))}>下一页</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -227,4 +227,3 @@ export function MessageEditor({ onMessageChange, defaultValues }: MessageEditorP
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,4 +78,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,4 +61,3 @@ export default function Loading() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,4 +82,3 @@ export default function Loading() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import { GroupSelector } from "../components/group-selector"
|
||||
import { ContentSelector } from "../components/content-selector"
|
||||
import type { WechatGroup, ContentLibrary } from "@/types/group-push"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
|
||||
@@ -50,34 +48,13 @@ export default function NewGroupPushPage() {
|
||||
setFormData((prev) => ({ ...prev, contentLibraries }))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const loadingToast = showToast("正在保存...", "loading", true)
|
||||
try {
|
||||
const params = {
|
||||
name: formData.name,
|
||||
type: 3,
|
||||
pushType: 1,
|
||||
startTime: formData.pushTimeStart,
|
||||
endTime: formData.pushTimeEnd,
|
||||
maxPerDay: formData.dailyPushCount,
|
||||
pushOrder: formData.pushOrder === "latest" ? 2 : 1,
|
||||
isLoop: formData.isLoopPush ? 1 : 0,
|
||||
status: formData.isEnabled ? 1 : 0,
|
||||
groups: formData.groups.map(g => g.id),
|
||||
contentLibraries: formData.contentLibraries.map(c => c.id),
|
||||
}
|
||||
const res = await api.post("/v1/workbench/create", params)
|
||||
loadingToast.remove()
|
||||
if (res.code === 200) {
|
||||
showToast("保存成功", "success")
|
||||
router.push("/workspace/group-push")
|
||||
} else {
|
||||
showToast(res.msg || "保存失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
loadingToast.remove()
|
||||
showToast(e?.message || "网络错误", "error")
|
||||
}
|
||||
const handleSave = () => {
|
||||
// 这里可以添加保存逻辑,例如API调用
|
||||
toast({
|
||||
title: "保存成功",
|
||||
description: `社群推送任务"${formData.name || "未命名任务"}"已保存`,
|
||||
})
|
||||
router.push("/workspace/group-push")
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -159,4 +136,3 @@ export default function NewGroupPushPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -15,124 +15,81 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { PlusCircle, MoreVertical, Edit, Trash2, ArrowLeft, Clock, Search, Filter, RefreshCw } from "lucide-react"
|
||||
import { PlusCircle, MoreVertical, Edit, Trash2, ArrowLeft, Clock, Search, RefreshCw } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface GroupPushTask {
|
||||
id: string
|
||||
name: string
|
||||
status: number
|
||||
config: {
|
||||
maxPerDay: number
|
||||
pushOrder: number
|
||||
isLoop: number
|
||||
groups: any[]
|
||||
contentLibraries: any[]
|
||||
lastPushTime: string
|
||||
createTime: string
|
||||
}
|
||||
}
|
||||
// 模拟数据
|
||||
const mockTasks = [
|
||||
{
|
||||
id: "1",
|
||||
name: "社群推送测试",
|
||||
pushTimeRange: "06:00 - 23:59",
|
||||
dailyPushCount: 20,
|
||||
pushOrder: "latest",
|
||||
isLoopPush: false,
|
||||
isEnabled: true,
|
||||
groupCount: 3,
|
||||
contentLibraryCount: 2,
|
||||
createdAt: "2025-03-15 14:30",
|
||||
lastPushTime: "2025-03-20 10:25",
|
||||
totalPushCount: 245,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "产品更新推送",
|
||||
pushTimeRange: "09:00 - 21:00",
|
||||
dailyPushCount: 15,
|
||||
pushOrder: "earliest",
|
||||
isLoopPush: true,
|
||||
isEnabled: false,
|
||||
groupCount: 5,
|
||||
contentLibraryCount: 1,
|
||||
createdAt: "2025-03-10 10:15",
|
||||
lastPushTime: "2025-03-19 16:45",
|
||||
totalPushCount: 128,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "新客户欢迎",
|
||||
pushTimeRange: "08:00 - 22:00",
|
||||
dailyPushCount: 10,
|
||||
pushOrder: "latest",
|
||||
isLoopPush: true,
|
||||
isEnabled: true,
|
||||
groupCount: 2,
|
||||
contentLibraryCount: 1,
|
||||
createdAt: "2025-03-05 09:20",
|
||||
lastPushTime: "2025-03-18 11:30",
|
||||
totalPushCount: 87,
|
||||
},
|
||||
]
|
||||
|
||||
export default function GroupPushPage() {
|
||||
const router = useRouter()
|
||||
const [tasks, setTasks] = useState<GroupPushTask[]>([])
|
||||
const [tasks, setTasks] = useState(mockTasks)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [taskToDelete, setTaskToDelete] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const pageSize = 10
|
||||
|
||||
// 拉取数据
|
||||
const fetchTasks = async (page = 1, search = "") => {
|
||||
setLoading(true)
|
||||
const loadingToast = showToast("正在加载...", "loading", true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
type: "3",
|
||||
page: page.toString(),
|
||||
limit: pageSize.toString(),
|
||||
})
|
||||
if (search) params.append("keyword", search)
|
||||
const res = await api.get(`/v1/workbench/list?${params.toString()}`) as any
|
||||
loadingToast.remove()
|
||||
if (res.code === 200) {
|
||||
setTasks(res.data.list)
|
||||
setTotal(res.data.total)
|
||||
} else {
|
||||
showToast(res.msg || "获取失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
loadingToast.remove()
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks(currentPage, searchTerm)
|
||||
// eslint-disable-next-line
|
||||
}, [currentPage])
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setTaskToDelete(id)
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
const confirmDelete = () => {
|
||||
if (taskToDelete) {
|
||||
const loadingToast = showToast("正在删除...", "loading", true)
|
||||
try {
|
||||
const res = await api.delete(`/v1/workbench/delete?id=${taskToDelete}`) as any
|
||||
loadingToast.remove()
|
||||
if (res.code === 200) {
|
||||
showToast("删除成功", "success")
|
||||
fetchTasks(currentPage, searchTerm)
|
||||
} else {
|
||||
showToast(res.msg || "删除失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
loadingToast.remove()
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
}
|
||||
setTasks(tasks.filter((task) => task.id !== taskToDelete))
|
||||
setTaskToDelete(null)
|
||||
}
|
||||
setDeleteDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (id: string, enabled: boolean) => {
|
||||
const loadingToast = showToast("正在更新状态...", "loading", true)
|
||||
try {
|
||||
const res = await api.post('/v1/workbench/update-status', {
|
||||
id,
|
||||
status: enabled ? 1 : 0
|
||||
}) as any
|
||||
loadingToast.remove()
|
||||
if (res.code === 200) {
|
||||
showToast("状态已更新", "success")
|
||||
fetchTasks(currentPage, searchTerm)
|
||||
} else {
|
||||
showToast(res.msg || "操作失败", "error")
|
||||
}
|
||||
} catch (e) {
|
||||
loadingToast.remove()
|
||||
showToast((e as any)?.message || "网络错误", "error")
|
||||
}
|
||||
const handleToggleStatus = (id: string, isEnabled: boolean) => {
|
||||
setTasks(tasks.map((task) => (task.id === id ? { ...task, isEnabled } : task)))
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchTasks(1, searchTerm)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchTasks(currentPage, searchTerm)
|
||||
}
|
||||
const filteredTasks = tasks.filter((task) => task.name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 min-h-screen pb-16">
|
||||
@@ -165,59 +122,28 @@ export default function GroupPushPage() {
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 任务列表 */}
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<Card key={index} className="p-4 animate-pulse">
|
||||
<div className="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="bg-gray-100 p-4 rounded-full mb-4">
|
||||
<Clock className="h-10 w-10 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-1">暂无社群推送任务</h3>
|
||||
<p className="text-gray-500 mb-4">点击"新建任务"按钮创建您的第一个社群推送任务</p>
|
||||
<Link href="/workspace/group-push/new">
|
||||
<Button>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 mt-2">
|
||||
{tasks.map((task) => (
|
||||
{filteredTasks.map((task) => (
|
||||
<Card key={task.id} className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={task.status === 1 ? "success" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
<Badge variant={task.isEnabled ? "success" : "secondary"}>
|
||||
{task.isEnabled ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={task.status === 1}
|
||||
checked={task.isEnabled}
|
||||
onCheckedChange={(checked) => handleToggleStatus(task.id, checked)}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
@@ -264,51 +190,42 @@ export default function GroupPushPage() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>推送群数:{task.config.groups?.length || 0} 个</div>
|
||||
<div>内容库:{task.config.contentLibraries?.length || 0} 个</div>
|
||||
<div>推送设备:{task.groupCount} 个</div>
|
||||
<div>内容库:{task.contentLibraryCount} 个</div>
|
||||
<div>推送时间:{task.pushTimeRange}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>每日推送:{task.config.maxPerDay} 条</div>
|
||||
<div>推送顺序:{task.config.pushOrder === 2 ? "按最新" : "按最早"}</div>
|
||||
<div>循环推送:{task.config.isLoop === 1 ? "是" : "否"}</div>
|
||||
<div>每日推送:{task.dailyPushCount} 条</div>
|
||||
<div>已推送:{task.totalPushCount} 条</div>
|
||||
<div>推送顺序:{task.pushOrder === "latest" ? "按最新" : "按最早"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
上次推送:{task.config.lastPushTime || "--"}
|
||||
上次推送:{task.lastPushTime}
|
||||
</div>
|
||||
<div>创建时间:{task.config.createTime || "--"}</div>
|
||||
<div>创建时间:{task.createdAt}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页 */}
|
||||
{!loading && total > pageSize && (
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm text-gray-500">第 {currentPage} 页</span>
|
||||
<span className="text-sm text-gray-500">共 {Math.ceil(total / pageSize)} 页</span>
|
||||
{/* 空状态 */}
|
||||
{filteredTasks.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="bg-gray-100 p-4 rounded-full mb-4">
|
||||
<Clock className="h-10 w-10 text-gray-400" />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(total / pageSize), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
|
||||
>
|
||||
下一页
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-1">暂无社群推送任务</h3>
|
||||
<p className="text-gray-500 mb-4">点击"新建任务"按钮创建您的第一个社群推送任务</p>
|
||||
<Link href="/workspace/group-push/new">
|
||||
<Button>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -332,4 +249,3 @@ export default function GroupPushPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,4 +49,3 @@ export default function Loading() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function AutoGroupPage() {
|
||||
<TabsContent value="history">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="text-center py-12 text-gray-500">暂无建群记录</div>
|
||||
<div className="text-center py-12 text-gray-500"><EFBFBD><EFBFBD>无建群记录</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
@@ -66,4 +66,3 @@ export default function AutoGroupPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -148,4 +148,3 @@ export function AutoGroupCreator() {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -208,4 +208,3 @@ export function ContentSelector({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -239,4 +239,3 @@ export function GroupSelector({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,4 +60,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,3 @@ export default function Loading() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { BasicSettings } from "../components/basic-settings"
|
||||
import { GroupSelector } from "../components/group-selector"
|
||||
import { ContentSelector } from "../components/content-selector"
|
||||
import type { WechatGroup, ContentLibrary } from "@/types/group-sync"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
|
||||
@@ -47,39 +46,11 @@ export default function NewGroupSyncPage() {
|
||||
setFormData((prev) => ({ ...prev, contentLibraries }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/group-sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.code === 200) {
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "群同步计划已创建",
|
||||
});
|
||||
router.push('/workspace/group-sync');
|
||||
} else {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: data.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: "网络错误,请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleSave = () => {
|
||||
// 这里可以添加保存逻辑,例如API调用
|
||||
console.log("保存表单数据:", formData)
|
||||
router.push("/workspace/group-sync")
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push("/workspace/group-sync")
|
||||
@@ -110,7 +81,7 @@ export default function NewGroupSyncPage() {
|
||||
isEnabled: formData.isEnabled,
|
||||
}}
|
||||
onNext={handleBasicSettingsNext}
|
||||
onSave={handleSubmit}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
@@ -121,7 +92,7 @@ export default function NewGroupSyncPage() {
|
||||
onGroupsChange={handleGroupsChange}
|
||||
onPrevious={() => setCurrentStep(1)}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onSave={handleSubmit}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
@@ -132,7 +103,7 @@ export default function NewGroupSyncPage() {
|
||||
onLibrariesChange={handleLibrariesChange}
|
||||
onPrevious={() => setCurrentStep(2)}
|
||||
onNext={() => setCurrentStep(4)}
|
||||
onSave={handleSubmit}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
@@ -147,7 +118,7 @@ export default function NewGroupSyncPage() {
|
||||
<Button type="button" variant="outline" onClick={() => setCurrentStep(3)}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit}>
|
||||
<Button type="button" onClick={handleSave}>
|
||||
完成
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
@@ -160,4 +131,3 @@ export default function NewGroupSyncPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -200,4 +200,3 @@ export default function GroupSyncPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, use } from "react"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Search } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { StepIndicator } from "../../components/step-indicator"
|
||||
import { BasicSettings } from "../../components/basic-settings"
|
||||
import { DeviceSelectionDialog } from "../../components/device-selection-dialog"
|
||||
import { ContentLibrarySelectionDialog } from "../../components/content-library-selection-dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
name: string
|
||||
status: number
|
||||
config: {
|
||||
startTime: string
|
||||
endTime: string
|
||||
syncCount: number
|
||||
syncInterval: number
|
||||
syncType: number
|
||||
devices: string[]
|
||||
contentLibraries: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export default function EditMomentsSyncPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = use(params)
|
||||
export default function EditMomentsSyncPage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [libraryDialogOpen, setLibraryDialogOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
taskName: "同步卡若主号",
|
||||
startTime: "06:00",
|
||||
endTime: "23:59",
|
||||
syncCount: 5,
|
||||
syncInterval: 30,
|
||||
accountType: "business" as "business" | "personal",
|
||||
accountType: "business" as const,
|
||||
enabled: true,
|
||||
selectedDevices: [] as string[],
|
||||
selectedLibraries: [] as string[],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskDetail = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await api.get<{code: number, msg: string, data: Task}>(`/v1/workbench/detail?id=${resolvedParams.id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
const taskData = response.data
|
||||
setFormData({
|
||||
taskName: taskData.name || "",
|
||||
startTime: taskData.config.startTime || "06:00",
|
||||
endTime: taskData.config.endTime || "23:59",
|
||||
syncCount: taskData.config.syncCount || 5,
|
||||
syncInterval: taskData.config.syncInterval || 30,
|
||||
accountType: taskData.config.syncType === 1 ? "business" : "personal",
|
||||
enabled: !!taskData.status,
|
||||
selectedDevices: taskData.config.devices || [],
|
||||
selectedLibraries: taskData.config.contentLibraries || [],
|
||||
})
|
||||
} else {
|
||||
showToast(response.msg || "获取任务详情失败", "error")
|
||||
router.back()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取任务详情失败:", error)
|
||||
showToast(error?.message || "获取任务详情失败", "error")
|
||||
router.back()
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchTaskDetail()
|
||||
}, [resolvedParams.id, router])
|
||||
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
@@ -92,44 +36,9 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1))
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update', {
|
||||
id: resolvedParams.id,
|
||||
type: 2,
|
||||
name: formData.taskName,
|
||||
syncInterval: formData.syncInterval,
|
||||
syncCount: formData.syncCount,
|
||||
syncType: formData.accountType === "business" ? 1 : 2,
|
||||
startTime: formData.startTime,
|
||||
endTime: formData.endTime,
|
||||
accountType: formData.accountType === "business" ? 1 : 2,
|
||||
status: formData.enabled ? 1 : 0,
|
||||
devices: formData.selectedDevices,
|
||||
contentLibraries: formData.selectedLibraries
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast(response.msg || "更新成功", "success");
|
||||
router.push("/workspace/moments-sync");
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新朋友圈同步任务失败:", error);
|
||||
showToast(error?.message || "请检查网络连接或稍后再试", "error");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FA] flex justify-center items-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const handleComplete = () => {
|
||||
console.log("Form submitted:", formData)
|
||||
router.push("/workspace/moments-sync")
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -148,11 +57,7 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings
|
||||
formData={formData}
|
||||
onChange={handleUpdateFormData}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
<BasicSettings formData={formData} onChange={handleUpdateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
@@ -164,7 +69,6 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => setDeviceDialogOpen(true)}
|
||||
readOnly
|
||||
value={formData.selectedDevices.length > 0 ? `已选择 ${formData.selectedDevices.length} 个设备` : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +83,6 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={formData.selectedDevices.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
@@ -201,19 +104,9 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择内容库"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => setLibraryDialogOpen(true)}
|
||||
readOnly
|
||||
value={formData.selectedLibraries.length > 0 ? `已选择 ${formData.selectedLibraries.length} 个内容库` : ""}
|
||||
/>
|
||||
<Input placeholder="选择内容库" className="h-12 pl-11 rounded-xl border-gray-200 text-base" />
|
||||
</div>
|
||||
|
||||
{formData.selectedLibraries.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选内容库:{formData.selectedLibraries.length} 个</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
@@ -221,26 +114,29 @@ export default function EditMomentsSyncPage({ params }: { params: Promise<{ id:
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={formData.selectedLibraries.length === 0}
|
||||
>
|
||||
保存
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ContentLibrarySelectionDialog
|
||||
open={libraryDialogOpen}
|
||||
onOpenChange={setLibraryDialogOpen}
|
||||
selectedLibraries={formData.selectedLibraries}
|
||||
onSelect={(libraries) => {
|
||||
handleUpdateFormData({ selectedLibraries: libraries })
|
||||
setLibraryDialogOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="fixed bottom-0 left-0 right-0 h-16 bg-white border-t flex items-center justify-around px-6">
|
||||
<button className="flex flex-col items-center text-blue-600">
|
||||
<span className="text-sm mt-1">首页</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">场景获客</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">工作台</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,4 +36,3 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -98,4 +98,3 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,4 +37,3 @@ export function ContentLibrarySelection({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,4 +32,3 @@ export function DeviceSelection({ selectedDevices, onChange, onNext, onPrev }: D
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,252 +2,52 @@
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, MoreVertical, Clock, Edit, Trash2, Copy, RefreshCw, FileText, MessageSquare, History } from "lucide-react"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { Card } 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 { Avatar } from "@/components/ui/avatar"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
// 定义任务详情的接口
|
||||
interface TaskDetail {
|
||||
interface SyncTask {
|
||||
id: string
|
||||
name: string
|
||||
status: "running" | "paused"
|
||||
syncType: number
|
||||
accountType: number
|
||||
deviceCount: number
|
||||
contentLib: string
|
||||
syncCount: number
|
||||
syncInterval: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
enabled: boolean
|
||||
devices: {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
}[]
|
||||
contentLibraries: {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
}[]
|
||||
lastSyncTime: string
|
||||
createTime: string
|
||||
creator: string
|
||||
}
|
||||
|
||||
// 定义同步历史的接口
|
||||
interface SyncHistory {
|
||||
id: string
|
||||
syncTime: string
|
||||
content: string
|
||||
contentType: "text" | "image" | "video"
|
||||
status: "success" | "failed"
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
// 新增朋友圈发布记录类型
|
||||
type MomentRecord = {
|
||||
id: number
|
||||
workbenchId: number
|
||||
publishTime: number
|
||||
contentType: number // 1文本 2视频 3图片
|
||||
content: string
|
||||
resUrls: string[]
|
||||
urls: string[]
|
||||
operatorName: string
|
||||
operatorAvatar: string
|
||||
}
|
||||
|
||||
export default function MomentsSyncDetailPage({ params }: { params: { id: string } }) {
|
||||
export default function ViewMomentsSyncTask({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const [taskDetail, setTaskDetail] = useState<TaskDetail | null>(null)
|
||||
const [syncHistory, setSyncHistory] = useState<SyncHistory[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false)
|
||||
const [momentRecords, setMomentRecords] = useState<MomentRecord[]>([])
|
||||
const [isMomentLoading, setIsMomentLoading] = useState(false)
|
||||
|
||||
// 获取任务详情
|
||||
const [task, setTask] = useState<SyncTask | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskDetail = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/moments-records?workbenchId=${params.id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
setTaskDetail(response.data)
|
||||
|
||||
// 获取同步历史
|
||||
if (activeTab === "history") {
|
||||
fetchSyncHistory()
|
||||
}
|
||||
} else {
|
||||
showToast(response.msg || "获取任务详情失败", "error")
|
||||
router.push("/workspace/moments-sync")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取任务详情失败:", error)
|
||||
showToast(error?.message || "获取任务详情失败", "error")
|
||||
router.push("/workspace/moments-sync")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
// Fetch task data from API
|
||||
// For now, we'll use mock data
|
||||
setTask({
|
||||
id: params.id,
|
||||
name: "同步卡若主号",
|
||||
deviceCount: 2,
|
||||
contentLib: "卡若朋友圈",
|
||||
syncCount: 307,
|
||||
lastSyncTime: "2025-02-06 13:12:35",
|
||||
createTime: "2024-11-20 19:04:14",
|
||||
creator: "karuo",
|
||||
status: "running",
|
||||
})
|
||||
}, [params.id])
|
||||
|
||||
fetchTaskDetail()
|
||||
}, [params.id, router])
|
||||
|
||||
// 获取同步历史
|
||||
const fetchSyncHistory = async () => {
|
||||
try {
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/sync/history?id=${params.id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
setSyncHistory(response.data.list || [])
|
||||
} else {
|
||||
setSyncHistory([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取同步历史失败:", error)
|
||||
setSyncHistory([])
|
||||
const toggleTaskStatus = () => {
|
||||
if (task) {
|
||||
setTask({ ...task, status: task.status === "running" ? "paused" : "running" })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取朋友圈发布记录
|
||||
type MomentsApiResponse = { code: number; msg: string; data: { list: MomentRecord[] } }
|
||||
const fetchMomentRecords = async () => {
|
||||
setIsMomentLoading(true)
|
||||
try {
|
||||
const response = await api.get<MomentsApiResponse>(`/v1/workbench/moments-records?workbenchId=${params.id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
setMomentRecords(response.data.list || [])
|
||||
} else {
|
||||
setMomentRecords([])
|
||||
}
|
||||
} catch (error) {
|
||||
setMomentRecords([])
|
||||
} finally {
|
||||
setIsMomentLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换Tab时加载数据
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value)
|
||||
if (value === "history" && syncHistory.length === 0) {
|
||||
fetchSyncHistory()
|
||||
}
|
||||
if (value === "moments" && momentRecords.length === 0) {
|
||||
fetchMomentRecords()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
const toggleTaskStatus = async () => {
|
||||
if (!taskDetail) return
|
||||
|
||||
try {
|
||||
const newStatus = taskDetail.status === "running" ? "paused" : "running"
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update/status', {
|
||||
id: params.id,
|
||||
status: newStatus === "running" ? 1 : 0
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
setTaskDetail({
|
||||
...taskDetail,
|
||||
status: newStatus
|
||||
})
|
||||
showToast(`任务已${newStatus === "running" ? "启用" : "暂停"}`, "success")
|
||||
} else {
|
||||
showToast(response.msg || "操作失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新任务状态失败:", error)
|
||||
showToast(error?.message || "更新任务状态失败", "error")
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
const handleEdit = () => {
|
||||
router.push(`/workspace/moments-sync/${params.id}/edit`)
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = () => {
|
||||
setShowDeleteAlert(true)
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/delete', {
|
||||
id: params.id
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast("删除成功", "success")
|
||||
router.push("/workspace/moments-sync")
|
||||
} else {
|
||||
showToast(response.msg || "删除失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除任务失败:", error)
|
||||
showToast(error?.message || "删除任务失败", "error")
|
||||
} finally {
|
||||
setShowDeleteAlert(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制任务
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/copy', {
|
||||
id: params.id
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast("复制成功,正在跳转到新任务", "success")
|
||||
// 假设后端返回了新任务的ID
|
||||
if (response.data?.id) {
|
||||
router.push(`/workspace/moments-sync/${response.data.id}`)
|
||||
} else {
|
||||
router.push("/workspace/moments-sync")
|
||||
}
|
||||
} else {
|
||||
showToast(response.msg || "复制失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("复制任务失败:", error)
|
||||
showToast(error?.message || "复制任务失败", "error")
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FA] flex justify-center items-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!taskDetail) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FA] flex justify-center items-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">任务不存在或已被删除</p>
|
||||
<Button onClick={() => router.push("/workspace/moments-sync")}>返回列表</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
if (!task) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -255,253 +55,53 @@ export default function MomentsSyncDetailPage({ params }: { params: { id: string
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/workspace/moments-sync")}>
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={taskDetail.status === "running"}
|
||||
onCheckedChange={toggleTaskStatus}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={confirmDelete} className="text-red-500 hover:text-red-600">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<h1 className="text-lg font-medium">查看朋友圈同步任务</h1>
|
||||
</div>
|
||||
<Button onClick={() => router.push(`/workspace/moments-sync/${task.id}/edit`)}>编辑任务</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<Card className="mb-4">
|
||||
<div className="p-4 border-b">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-xl font-semibold">{taskDetail.name}</h2>
|
||||
<Badge variant={taskDetail.status === "running" ? "success" : "secondary"}>
|
||||
{taskDetail.status === "running" ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-2xl font-bold">{task.name}</h2>
|
||||
<Badge variant={task.status === "running" ? "success" : "secondary"}>
|
||||
{task.status === "running" ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch checked={task.status === "running"} onCheckedChange={toggleTaskStatus} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">任务详情</h3>
|
||||
<div className="space-y-2">
|
||||
<p>推送设备:{task.deviceCount} 个</p>
|
||||
<p>内容库:{task.contentLib}</p>
|
||||
<p>已同步:{task.syncCount} 条</p>
|
||||
<p>创建人:{task.creator}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm text-gray-500">
|
||||
<div>创建时间:{taskDetail.createTime}</div>
|
||||
<div>创建人:{taskDetail.creator}</div>
|
||||
<div>上次同步:{taskDetail.lastSyncTime}</div>
|
||||
<div>已同步:{taskDetail.syncCount} 条</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">时间信息</h3>
|
||||
<div className="space-y-2">
|
||||
<p>创建时间:{task.createTime}</p>
|
||||
<p>上次同步:{task.lastSyncTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="mb-4">
|
||||
<TabsList className="grid grid-cols-4">
|
||||
<TabsTrigger value="overview">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="devices">设备列表</TabsTrigger>
|
||||
<TabsTrigger value="history">同步历史</TabsTrigger>
|
||||
<TabsTrigger value="moments">发布记录</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="font-medium mb-1">账号类型</div>
|
||||
<div className="text-gray-600">{taskDetail.accountType === 1 ? "业务号" : "人设号"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">同步类型</div>
|
||||
<div className="text-gray-600">{taskDetail.syncType === 1 ? "循环同步" : "实时更新"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">同步间隔</div>
|
||||
<div className="text-gray-600">{taskDetail.syncInterval} 分钟</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">每日同步数量</div>
|
||||
<div className="text-gray-600">{taskDetail.syncCount} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">允许发布时间段</div>
|
||||
<div className="text-gray-600">{taskDetail.startTime} - {taskDetail.endTime}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium mb-1">内容库</div>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{taskDetail.contentLibraries.map((lib) => (
|
||||
<Badge key={lib.id} variant="outline" className="bg-blue-50">
|
||||
{lib.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="devices" className="mt-4">
|
||||
<Card className="p-4">
|
||||
{taskDetail.devices.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无关联设备</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{taskDetail.devices.map((device) => (
|
||||
<div key={device.id} className="flex items-center py-3 first:pt-0 last:pb-0">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
{device.avatar ? (
|
||||
<img src={device.avatar} alt={device.name} />
|
||||
) : (
|
||||
<div className="bg-blue-100 text-blue-600 h-full w-full flex items-center justify-center">
|
||||
{device.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{device.name}</div>
|
||||
<div className="text-xs text-gray-500">ID: {device.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="history" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium">同步历史</h3>
|
||||
<Button variant="outline" size="sm" onClick={fetchSyncHistory}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{syncHistory.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无同步历史</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{syncHistory.map((record) => (
|
||||
<div key={record.id} className="border rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
{record.contentType === "text" && <FileText className="h-4 w-4 mr-2 text-gray-500" />}
|
||||
{record.contentType === "image" && <img className="h-4 w-4 mr-2" src="/icons/image.svg" alt="图片" />}
|
||||
{record.contentType === "video" && <img className="h-4 w-4 mr-2" src="/icons/video.svg" alt="视频" />}
|
||||
<Badge variant={record.status === "success" ? "success" : "destructive"} className="text-xs">
|
||||
{record.status === "success" ? "成功" : "失败"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{record.syncTime}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 line-clamp-2">{record.content}</div>
|
||||
{record.status === "failed" && record.errorMessage && (
|
||||
<div className="mt-2 text-xs text-red-500">
|
||||
错误信息: {record.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="moments" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-medium">朋友圈发布记录</h3>
|
||||
<Button variant="outline" size="sm" onClick={fetchMomentRecords} disabled={isMomentLoading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
{isMomentLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
) : momentRecords.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无发布记录</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{momentRecords.map((rec) => (
|
||||
<div key={rec.id} className="border rounded-lg p-3 flex gap-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
{rec.operatorAvatar ? (
|
||||
<img src={rec.operatorAvatar} alt={rec.operatorName} />
|
||||
) : (
|
||||
<div className="bg-blue-100 text-blue-600 h-full w-full flex items-center justify-center">
|
||||
{rec.operatorName?.charAt(0) || "?"}
|
||||
</div>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-sm">{rec.operatorName}</span>
|
||||
<span className="text-xs text-gray-400">{rec.publishTime ? new Date(rec.publishTime * 1000).toLocaleString() : "-"}</span>
|
||||
</div>
|
||||
<div className="mb-1 text-gray-800 text-sm">
|
||||
{rec.contentType === 1 && rec.content}
|
||||
{rec.contentType === 3 && rec.content}
|
||||
</div>
|
||||
{/* 图片展示 */}
|
||||
{rec.contentType === 3 && rec.resUrls && rec.resUrls.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap mt-1">
|
||||
{rec.resUrls.map((url, idx) => (
|
||||
<img key={idx} src={url} alt="图片" className="h-20 w-20 object-cover rounded" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 视频展示 */}
|
||||
{rec.contentType === 2 && rec.urls && rec.urls.length > 0 && (
|
||||
<div className="mt-1">
|
||||
{rec.urls.map((url, idx) => (
|
||||
<video key={idx} src={url} controls className="h-32 w-48 rounded" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-semibold mb-2">同步内容预览</h3>
|
||||
{/* Add content preview here */}
|
||||
<p className="text-gray-500">暂无内容预览</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={showDeleteAlert} onOpenChange={setShowDeleteAlert}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
删除后,此任务将无法恢复。确定要删除吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -127,4 +127,3 @@ export default function MomentsSyncViewPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Switch } from "@/components/ui/switch"
|
||||
import { Plus, Minus, Clock, HelpCircle } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useViewMode } from "@/app/components/LayoutWrapper"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
@@ -14,7 +13,6 @@ interface BasicSettingsProps {
|
||||
startTime: string
|
||||
endTime: string
|
||||
syncCount: number
|
||||
syncInterval: number
|
||||
accountType: "business" | "personal"
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -25,142 +23,139 @@ interface BasicSettingsProps {
|
||||
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
|
||||
const handleSyncCountChange = (delta: number) => {
|
||||
const newValue = Math.max(1, formData.syncCount + delta)
|
||||
onChange({ syncCount: newValue })
|
||||
}
|
||||
|
||||
const handleSyncIntervalChange = (delta: number) => {
|
||||
const newValue = Math.max(5, formData.syncInterval + delta)
|
||||
onChange({ syncInterval: newValue })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${viewMode === "desktop" ? "p-6" : "p-4"}`}>
|
||||
<div className={`grid ${viewMode === "desktop" ? "grid-cols-2 gap-8" : "grid-cols-1 gap-4"}`}>
|
||||
<div>
|
||||
<Label htmlFor="taskName" className="text-base">任务名称</Label>
|
||||
<div className="text-base font-medium mb-2">任务名称</div>
|
||||
<Input
|
||||
id="taskName"
|
||||
value={formData.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
placeholder="请输入任务名称"
|
||||
className="mt-1.5 h-12 rounded-xl"
|
||||
className="h-12 border-0 border-b border-gray-200 rounded-none focus-visible:ring-0 focus-visible:border-blue-600 px-0 text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">每日同步数量</Label>
|
||||
<div className="flex items-center space-x-4 mt-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-xl"
|
||||
onClick={() => handleSyncCountChange(-1)}
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="w-20 text-center text-base">{formData.syncCount}</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-xl"
|
||||
onClick={() => handleSyncCountChange(1)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-gray-500">条/天</span>
|
||||
<div className="text-base font-medium mb-2">允许发布时间段</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-gray-500">至</span>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">同步间隔</Label>
|
||||
<div className="flex items-center space-x-4 mt-1.5">
|
||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-xl"
|
||||
onClick={() => handleSyncIntervalChange(-5)}
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="w-20 text-center text-base">{formData.syncInterval}</div>
|
||||
<span className="w-8 text-center text-lg font-medium">{formData.syncCount}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-xl"
|
||||
onClick={() => handleSyncIntervalChange(5)}
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-gray-500">分钟</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1.5">设置每次发朋友圈的时间间隔</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">同步时间</Label>
|
||||
<div className="grid grid-cols-2 gap-4 mt-1.5">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||
className="h-12 rounded-xl"
|
||||
/>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||
className="h-12 rounded-xl"
|
||||
/>
|
||||
<span className="text-gray-500">条朋友圈</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">账号类型</Label>
|
||||
<div className="grid grid-cols-2 gap-4 mt-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={`h-12 rounded-xl border ${
|
||||
formData.accountType === "business"
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 text-gray-600"
|
||||
}`}
|
||||
onClick={() => onChange({ accountType: "business" })}
|
||||
>
|
||||
业务号
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`h-12 rounded-xl border ${
|
||||
formData.accountType === "personal"
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 text-gray-600"
|
||||
}`}
|
||||
onClick={() => onChange({ accountType: "personal" })}
|
||||
>
|
||||
人设号
|
||||
</button>
|
||||
<div className="text-base font-medium mb-2">账号类型</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "business" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "business"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
业务号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "business" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
业务号能够循环推送内容库中的内容。当内容库所有内容循环推送完毕后,若有新内容则优先推送新内容,若无新内容则继续循环推送。
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "personal" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "personal"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
人设号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "personal" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>用于实时更新同步,有新动态时进行同步,无动态则不同步。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base">是否启用</Label>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-base font-medium">是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={onNext}
|
||||
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={!formData.taskName}
|
||||
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base font-medium shadow-sm"
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { ContentSelector as CommonContentSelector } from "@/components/common/ContentSelector"
|
||||
import type { ContentLibrary } from "@/components/common/ContentSelector"
|
||||
|
||||
interface ContentSelectorProps {
|
||||
selectedLibraries: ContentLibrary[]
|
||||
onLibrariesChange: (libraries: ContentLibrary[]) => void
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function ContentSelector(props: ContentSelectorProps) {
|
||||
return <CommonContentSelector {...props} />
|
||||
}
|
||||
@@ -100,4 +100,3 @@ export function ContentViewer({ tagId }: ContentViewerProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { useState } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
||||
import { Search, RefreshCw } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface ServerDevice {
|
||||
id: number
|
||||
imei: string
|
||||
memo: string
|
||||
wechatId: string
|
||||
alive: number
|
||||
totalFriend: number
|
||||
}
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
interface Device {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
imei: string
|
||||
wxid: string
|
||||
status: "online" | "offline"
|
||||
totalFriend: number
|
||||
usedInPlans: number
|
||||
}
|
||||
|
||||
interface DeviceSelectionDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedDevices: number[]
|
||||
onSelect: (devices: number[]) => void
|
||||
selectedDevices: string[]
|
||||
onSelect: (devices: string[]) => void
|
||||
}
|
||||
|
||||
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [tempSelectedDevices, setTempSelectedDevices] = useState<number[]>(selectedDevices)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
fetchDevices()
|
||||
}
|
||||
}, [open, selectedDevices])
|
||||
|
||||
const fetchDevices = async () => {
|
||||
const loadingToast = showToast("正在加载设备列表...", "loading", true);
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.get<{code: number, msg: string, data: {list: ServerDevice[], total: number}}>('/v1/devices?page=1&limit=100')
|
||||
|
||||
if (response.code === 200 && response.data.list) {
|
||||
const transformedDevices: Device[] = response.data.list.map(device => ({
|
||||
id: device.id,
|
||||
name: device.memo || '设备_' + device.id,
|
||||
imei: device.imei || '',
|
||||
wxid: device.alias || device.wechatId || '',
|
||||
status: device.alive === 1 ? "online" : "offline",
|
||||
totalFriend: device.totalFriend || 0,
|
||||
nickname: device.nickname || ''
|
||||
}))
|
||||
setDevices(transformedDevices)
|
||||
} else {
|
||||
showToast(response.msg || "获取设备列表失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove();
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
const handleDeviceToggle = (deviceId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setTempSelectedDevices(prev => [...prev, deviceId])
|
||||
} else {
|
||||
setTempSelectedDevices(prev => prev.filter(id => id !== deviceId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
onSelect(tempSelectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
// 模拟设备数据
|
||||
const devices: Device[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "设备 1",
|
||||
imei: "IMEI-radz6ewal",
|
||||
wxid: "wxid_98179ujy",
|
||||
status: "offline",
|
||||
usedInPlans: 0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "设备 2",
|
||||
imei: "IMEI-i6iszi6d",
|
||||
wxid: "wxid_viqnaic8",
|
||||
status: "online",
|
||||
usedInPlans: 2,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "设备 3",
|
||||
imei: "IMEI-01z2izj97",
|
||||
wxid: "wxid_9sb23gxr",
|
||||
status: "online",
|
||||
usedInPlans: 2,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "设备 4",
|
||||
imei: "IMEI-x6o9rpcr0",
|
||||
wxid: "wxid_k0gxzbit",
|
||||
status: "online",
|
||||
usedInPlans: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const filteredDevices = devices.filter((device) => {
|
||||
const searchLower = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
(device.name || '').toLowerCase().includes(searchLower) ||
|
||||
(device.imei || '').toLowerCase().includes(searchLower) ||
|
||||
(device.wxid || '').toLowerCase().includes(searchLower)
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.wxid.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" ||
|
||||
@@ -144,50 +107,38 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 -mx-6 px-6" style={{overflowY: 'auto'}}>
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredDevices.map((device) => (
|
||||
<label
|
||||
key={device.id}
|
||||
className="flex items-center space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
style={{paddingLeft: '0px',paddingRight: '0px'}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={tempSelectedDevices.includes(device.id)}
|
||||
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>IMEI: {device.imei || '--'}</div>
|
||||
<div>微信号: {device.wxid || '--'}({device.nickname || ''})</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 -mx-6 px-6">
|
||||
<RadioGroup value={selectedDevices[0]} onValueChange={(value) => onSelect([value])}>
|
||||
{filteredDevices.map((device) => (
|
||||
<label
|
||||
key={device.id}
|
||||
className="flex items-start space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
<RadioGroupItem value={device.id} id={device.id} className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "success" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>IMEI: {device.imei}</div>
|
||||
<div>微信号: {device.wxid}</div>
|
||||
</div>
|
||||
{device.usedInPlans > 0 && (
|
||||
<div className="text-sm text-orange-500 mt-1">已用于 {device.usedInPlans} 个计划</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="mt-4 flex gap-4 -mx-6 px-6">
|
||||
<Button className="flex-1" onClick={handleConfirm}>确认</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -49,4 +49,3 @@ export function StepIndicator({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -153,4 +153,3 @@ export function TagEditor({ tagId, initialData }: TagEditorProps) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,34 +7,19 @@ import { Button } from "@/components/ui/button"
|
||||
import { StepIndicator } from "../components/step-indicator"
|
||||
import { BasicSettings } from "../components/basic-settings"
|
||||
import { DeviceSelectionDialog } from "../components/device-selection-dialog"
|
||||
import { ContentLibrarySelectionDialog } from "../components/content-library-selection-dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
// 定义基本设置表单数据类型,与BasicSettings组件的formData类型匹配
|
||||
interface BasicSettingsFormData {
|
||||
taskName: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
syncCount: number
|
||||
syncInterval: number
|
||||
accountType: "business" | "personal"
|
||||
enabled: boolean
|
||||
}
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
|
||||
export default function NewMomentsSyncPage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [libraryDialogOpen, setLibraryDialogOpen] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
startTime: "06:00",
|
||||
endTime: "23:59",
|
||||
syncCount: 5,
|
||||
syncInterval: 30, // 同步间隔,默认30分钟
|
||||
accountType: "business" as "business" | "personal",
|
||||
accountType: "business" as const,
|
||||
enabled: true,
|
||||
selectedDevices: [] as string[],
|
||||
selectedLibraries: [] as string[],
|
||||
@@ -44,11 +29,6 @@ export default function NewMomentsSyncPage() {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
|
||||
// 专门用于基本设置的更新函数
|
||||
const handleBasicSettingsUpdate = (data: Partial<BasicSettingsFormData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 3))
|
||||
}
|
||||
@@ -58,31 +38,13 @@ export default function NewMomentsSyncPage() {
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/create', {
|
||||
type: 2, // 朋友圈同步任务类型为2
|
||||
name: formData.taskName,
|
||||
syncInterval: formData.syncInterval,
|
||||
syncCount: formData.syncCount,
|
||||
syncType: formData.accountType === "business" ? 1 : 2, // 业务号为1,人设号为2
|
||||
startTime: formData.startTime,
|
||||
endTime: formData.endTime,
|
||||
accountType: formData.accountType === "business" ? 1 : 2,
|
||||
status: formData.enabled ? 1 : 0, // 状态:0=禁用,1=启用
|
||||
devices: formData.selectedDevices,
|
||||
contentLibraries: formData.selectedLibraries
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast(response.msg || "创建成功", "success");
|
||||
router.push("/workspace/moments-sync");
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("创建朋友圈同步任务失败:", error);
|
||||
showToast(error?.message || "请检查网络连接或稍后再试", "error");
|
||||
}
|
||||
console.log("Form submitted:", formData)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
toast({
|
||||
title: "创建成<E5BBBA><E68890><EFBFBD>",
|
||||
description: "朋友圈同步任务已创建并开始执行",
|
||||
})
|
||||
router.push("/workspace/moments-sync")
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -101,19 +63,7 @@ export default function NewMomentsSyncPage() {
|
||||
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings
|
||||
formData={{
|
||||
taskName: formData.taskName,
|
||||
startTime: formData.startTime,
|
||||
endTime: formData.endTime,
|
||||
syncCount: formData.syncCount,
|
||||
syncInterval: formData.syncInterval,
|
||||
accountType: formData.accountType,
|
||||
enabled: formData.enabled
|
||||
}}
|
||||
onChange={handleBasicSettingsUpdate}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
<BasicSettings formData={formData} onChange={handleUpdateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
@@ -125,7 +75,6 @@ export default function NewMomentsSyncPage() {
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => setDeviceDialogOpen(true)}
|
||||
readOnly
|
||||
value={formData.selectedDevices.length > 0 ? `已选择 ${formData.selectedDevices.length} 个设备` : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -140,7 +89,6 @@ export default function NewMomentsSyncPage() {
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={formData.selectedDevices.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
@@ -162,19 +110,9 @@ export default function NewMomentsSyncPage() {
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择内容库"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => setLibraryDialogOpen(true)}
|
||||
readOnly
|
||||
value={formData.selectedLibraries.length > 0 ? `已选择 ${formData.selectedLibraries.length} 个内容库` : ""}
|
||||
/>
|
||||
<Input placeholder="选择内容库" className="h-12 pl-11 rounded-xl border-gray-200 text-base" />
|
||||
</div>
|
||||
|
||||
{formData.selectedLibraries.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选内容库:{formData.selectedLibraries.length} 个</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
@@ -182,26 +120,29 @@ export default function NewMomentsSyncPage() {
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={formData.selectedLibraries.length === 0}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ContentLibrarySelectionDialog
|
||||
open={libraryDialogOpen}
|
||||
onOpenChange={setLibraryDialogOpen}
|
||||
selectedLibraries={formData.selectedLibraries}
|
||||
onSelect={(libraries) => {
|
||||
handleUpdateFormData({ selectedLibraries: libraries })
|
||||
setLibraryDialogOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="fixed bottom-0 left-0 right-0 h-16 bg-white border-t flex items-center justify-around px-6">
|
||||
<button className="flex flex-col items-center text-blue-600">
|
||||
<span className="text-sm mt-1">首页</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">场景获客</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">工作台</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center text-gray-400">
|
||||
<span className="text-sm mt-1">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -277,4 +277,3 @@ export function ContentSelector({ formData, onChange, onNext, onPrev }: ContentS
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ChevronLeft, Plus, Filter, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye, Copy } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { ChevronLeft, Plus, Filter, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -10,196 +10,66 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
import { DeviceSelectionDialog } from "../auto-like/components/device-selection-dialog"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
interface SyncTask {
|
||||
id: string
|
||||
name: string
|
||||
status: number // 修改为数字类型:1-运行中,0-暂停
|
||||
status: "running" | "paused"
|
||||
deviceCount: number
|
||||
contentLib: string
|
||||
syncCount: number
|
||||
lastSyncTime: string
|
||||
createTime: string
|
||||
creator: string
|
||||
config: {
|
||||
devices: string[]
|
||||
contentLibraryNames: string[]
|
||||
}
|
||||
creatorName: string
|
||||
}
|
||||
|
||||
export default function MomentsSyncPage() {
|
||||
const router = useRouter()
|
||||
const [tasks, setTasks] = useState<SyncTask[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false)
|
||||
const [taskToDelete, setTaskToDelete] = useState<string | null>(null)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [selectedDevices, setSelectedDevices] = useState<number[]>([])
|
||||
const [tasks, setTasks] = useState<SyncTask[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "同步卡若主号",
|
||||
deviceCount: 2,
|
||||
contentLib: "卡若朋友圈",
|
||||
syncCount: 307,
|
||||
lastSyncTime: "2025-02-06 13:12:35",
|
||||
createTime: "2024-11-20 19:04:14",
|
||||
creator: "karuo",
|
||||
status: "running",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "暗黑4业务",
|
||||
deviceCount: 1,
|
||||
contentLib: "暗黑4代练",
|
||||
syncCount: 622,
|
||||
lastSyncTime: "2024-03-04 14:09:35",
|
||||
createTime: "2024-03-04 14:29:04",
|
||||
creator: "lkdie",
|
||||
status: "paused",
|
||||
},
|
||||
])
|
||||
|
||||
// 获取任务列表
|
||||
const fetchTasks = async () => {
|
||||
const loadingToast = showToast("正在加载任务列表...", "loading", true);
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/list?type=2&page=${currentPage}&pageSize=${pageSize}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
setTasks(response.data.list || [])
|
||||
setTotal(response.data.total || 0)
|
||||
} else {
|
||||
showToast(response.msg || "获取任务列表失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取朋友圈同步任务列表失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove();
|
||||
setIsLoading(false)
|
||||
}
|
||||
const handleDelete = (taskId: string) => {
|
||||
setTasks(tasks.filter((task) => task.id !== taskId))
|
||||
}
|
||||
|
||||
// 组件加载时获取任务列表
|
||||
useEffect(() => {
|
||||
fetchTasks()
|
||||
}, [currentPage, pageSize])
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
// 处理每页条数变化
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size)
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
}
|
||||
|
||||
// 搜索任务
|
||||
const handleSearch = () => {
|
||||
fetchTasks()
|
||||
}
|
||||
|
||||
// 切换任务状态
|
||||
const toggleTaskStatus = async (taskId: string, currentStatus: number) => {
|
||||
const loadingToast = showToast("正在更新任务状态...", "loading", true);
|
||||
try {
|
||||
const newStatus = currentStatus === 1 ? 0 : 1
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update-status', {
|
||||
id: taskId,
|
||||
status: newStatus
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
setTasks(
|
||||
tasks.map((task) =>
|
||||
task.id === taskId ? { ...task, status: newStatus } : task
|
||||
)
|
||||
)
|
||||
loadingToast.remove();
|
||||
showToast(`任务已${newStatus === 1 ? "启用" : "暂停"}`, "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "操作失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新任务状态失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "更新任务状态失败", "error")
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = (taskId: string) => {
|
||||
setTaskToDelete(taskId)
|
||||
setShowDeleteAlert(true)
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
const handleDelete = async () => {
|
||||
if (!taskToDelete) return
|
||||
|
||||
const loadingToast = showToast("正在删除任务...", "loading", true);
|
||||
try {
|
||||
const response = await api.delete<ApiResponse>(`/v1/workbench/delete?id=${taskToDelete}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
setTasks(tasks.filter((task) => task.id !== taskToDelete))
|
||||
loadingToast.remove();
|
||||
showToast("删除成功", "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "删除失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除任务失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "删除任务失败", "error")
|
||||
} finally {
|
||||
setTaskToDelete(null)
|
||||
setShowDeleteAlert(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
const handleEdit = (taskId: string) => {
|
||||
router.push(`/workspace/moments-sync/${taskId}/edit`)
|
||||
}
|
||||
|
||||
// 查看任务详情
|
||||
const handleView = (taskId: string) => {
|
||||
router.push(`/workspace/moments-sync/${taskId}`)
|
||||
}
|
||||
|
||||
// 复制任务
|
||||
const handleCopy = async (taskId: string) => {
|
||||
const loadingToast = showToast("正在复制任务...", "loading", true);
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/copy', {
|
||||
id: taskId
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
loadingToast.remove();
|
||||
showToast("复制成功", "success")
|
||||
fetchTasks() // 重新获取列表
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "复制失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("复制任务失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "复制任务失败", "error")
|
||||
}
|
||||
const toggleTaskStatus = (taskId: string) => {
|
||||
setTasks(
|
||||
tasks.map((task) =>
|
||||
task.id === taskId ? { ...task, status: task.status === "running" ? "paused" : "running" } : task,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理设备选择
|
||||
const handleDeviceSelect = (devices: number[]) => {
|
||||
setSelectedDevices(devices)
|
||||
}
|
||||
|
||||
// 过滤任务
|
||||
const filteredTasks = tasks.filter(
|
||||
(task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
@@ -224,165 +94,75 @@ export default function MomentsSyncPage() {
|
||||
<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="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Input placeholder="搜索任务名称" className="pl-9" />
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={fetchTasks}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin mb-4" />
|
||||
<p className="text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : filteredTasks.length === 0 ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="text-gray-500 mb-4">暂无数据</p>
|
||||
<Link href="/workspace/moments-sync/new">
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.map((task) => (
|
||||
<Card key={task.id} className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={task.status === 1 ? "success" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={task.status === 1}
|
||||
onCheckedChange={() => toggleTaskStatus(task.id, task.status)}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => handleView(task.id)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => confirmDelete(task.id)}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{tasks.map((task) => (
|
||||
<Card key={task.id} className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={task.status === "running" ? "success" : "secondary"}>
|
||||
{task.status === "running" ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={task.status === "running"} onCheckedChange={() => toggleTaskStatus(task.id)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => handleView(task.id)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>推送设备:{task.config.devices.length || 0} 个</div>
|
||||
<div className="truncate">内容库:{task.config.contentLibraryNames?.join('、') || '--'}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>已同步:{task.syncCount || 0} 条</div>
|
||||
<div>创建人:{task.creatorName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>推送设备:{task.deviceCount} 个</div>
|
||||
<div>内容库:{task.contentLib}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>已同步:{task.syncCount} 条</div>
|
||||
<div>创建人:{task.creator}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
上次同步:{task.lastSyncTime || '--'}
|
||||
</div>
|
||||
<div>创建时间:{task.createTime}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
<div className="flex justify-center items-center mt-4 space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<span className="text-sm">
|
||||
第 {currentPage} 页 共 {Math.ceil(total / pageSize)} 页
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DeviceSelectionDialog
|
||||
open={deviceDialogOpen}
|
||||
onOpenChange={setDeviceDialogOpen}
|
||||
selectedDevices={selectedDevices}
|
||||
onSelect={handleDeviceSelect}
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
上次同步:{task.lastSyncTime}
|
||||
</div>
|
||||
<div>创建时间:{task.createTime}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={showDeleteAlert} onOpenChange={setShowDeleteAlert}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
删除后,此任务将无法恢复。确定要删除吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,198 +1,194 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { ThumbsUp, MessageSquare, Send, Users, Share2, Brain, BarChart2, LineChart, Clock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ThumbsUp, Clock, Send, Users, Share2, MessageSquare, BarChart3, Target, TrendingUp } from "lucide-react"
|
||||
|
||||
// 功能项数据类型
|
||||
interface WorkspaceFunction {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
path: string
|
||||
isNew?: boolean
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
// 常用功能数据
|
||||
const commonFunctions: WorkspaceFunction[] = [
|
||||
{
|
||||
id: "auto-like",
|
||||
title: "自动点赞",
|
||||
description: "智能自动点赞互动",
|
||||
icon: <ThumbsUp className="w-6 h-6" />,
|
||||
path: "/workspace/auto-like",
|
||||
isNew: true,
|
||||
color: "text-red-600",
|
||||
bgColor: "bg-red-50",
|
||||
},
|
||||
{
|
||||
id: "moments-sync",
|
||||
title: "朋友圈同步",
|
||||
description: "自动同步朋友圈内容",
|
||||
icon: <Clock className="w-6 h-6" />,
|
||||
path: "/workspace/moments-sync",
|
||||
color: "text-purple-600",
|
||||
bgColor: "bg-purple-50",
|
||||
},
|
||||
{
|
||||
id: "group-push",
|
||||
title: "群消息推送",
|
||||
description: "智能群发助手",
|
||||
icon: <Send className="w-6 h-6" />,
|
||||
path: "/workspace/group-push",
|
||||
color: "text-orange-600",
|
||||
bgColor: "bg-orange-50",
|
||||
},
|
||||
{
|
||||
id: "auto-group",
|
||||
title: "自动建群",
|
||||
description: "智能拉好友建群",
|
||||
icon: <Users className="w-6 h-6" />,
|
||||
path: "/workspace/auto-group",
|
||||
color: "text-green-600",
|
||||
bgColor: "bg-green-50",
|
||||
},
|
||||
{
|
||||
id: "traffic-distribution",
|
||||
title: "流量分发",
|
||||
description: "管理流量分发和分配",
|
||||
icon: <Share2 className="w-6 h-6" />,
|
||||
path: "/workspace/traffic-distribution",
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50",
|
||||
},
|
||||
{
|
||||
id: "ai-assistant",
|
||||
title: "AI对话助手",
|
||||
description: "智能回复,提高互动质量",
|
||||
icon: <MessageSquare className="w-6 h-6" />,
|
||||
path: "/workspace/ai-assistant",
|
||||
isNew: true,
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50",
|
||||
},
|
||||
]
|
||||
|
||||
// AI智能助手功能数据
|
||||
const aiFunctions: WorkspaceFunction[] = [
|
||||
{
|
||||
id: "ai-analyzer",
|
||||
title: "AI数据分析",
|
||||
description: "智能分析客户行为特征",
|
||||
icon: <BarChart3 className="w-6 h-6" />,
|
||||
path: "/workspace/ai-analyzer",
|
||||
isNew: true,
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50",
|
||||
},
|
||||
{
|
||||
id: "ai-strategy",
|
||||
title: "AI策略优化",
|
||||
description: "智能优化获客策略",
|
||||
icon: <Target className="w-6 h-6" />,
|
||||
path: "/workspace/ai-strategy",
|
||||
isNew: true,
|
||||
color: "text-cyan-600",
|
||||
bgColor: "bg-cyan-50",
|
||||
},
|
||||
{
|
||||
id: "ai-prediction",
|
||||
title: "AI销售预测",
|
||||
description: "智能预测销售趋势",
|
||||
icon: <TrendingUp className="w-6 h-6" />,
|
||||
path: "/workspace/ai-prediction",
|
||||
color: "text-yellow-600",
|
||||
bgColor: "bg-yellow-50",
|
||||
},
|
||||
]
|
||||
|
||||
export default function WorkspacePage() {
|
||||
// 模拟任务数据
|
||||
const taskStats = {
|
||||
total: 42,
|
||||
inProgress: 12,
|
||||
completed: 30,
|
||||
todayTasks: 12,
|
||||
activityRate: 98,
|
||||
const router = useRouter()
|
||||
const [accessStats, setAccessStats] = useState<Record<string, number>>({})
|
||||
|
||||
// 记录功能访问
|
||||
const recordAccess = async (functionId: string) => {
|
||||
try {
|
||||
// 这里可以调用API记录访问统计
|
||||
setAccessStats((prev) => ({
|
||||
...prev,
|
||||
[functionId]: (prev[functionId] || 0) + 1,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("记录访问失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 常用功能 - 保持原有排列
|
||||
const commonFeatures = [
|
||||
{
|
||||
id: "auto-like",
|
||||
name: "自动点赞",
|
||||
description: "智能自动点赞互动",
|
||||
icon: <ThumbsUp className="h-5 w-5 text-red-500" />,
|
||||
path: "/workspace/auto-like",
|
||||
bgColor: "bg-red-100",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "moments-sync",
|
||||
name: "朋友圈同步",
|
||||
description: "自动同步朋友圈内容",
|
||||
icon: <Clock className="h-5 w-5 text-purple-500" />,
|
||||
path: "/workspace/moments-sync",
|
||||
bgColor: "bg-purple-100",
|
||||
},
|
||||
{
|
||||
id: "group-push",
|
||||
name: "群消息推送",
|
||||
description: "智能群发助手",
|
||||
icon: <Send className="h-5 w-5 text-orange-500" />,
|
||||
path: "/workspace/group-push",
|
||||
bgColor: "bg-orange-100",
|
||||
},
|
||||
{
|
||||
id: "auto-group",
|
||||
name: "自动建群",
|
||||
description: "智能拉好友建群",
|
||||
icon: <Users className="h-5 w-5 text-green-500" />,
|
||||
path: "/workspace/auto-group",
|
||||
bgColor: "bg-green-100",
|
||||
},
|
||||
{
|
||||
id: "traffic-distribution",
|
||||
name: "流量分发",
|
||||
description: "管理流量分发和分配",
|
||||
icon: <Share2 className="h-5 w-5 text-blue-500" />,
|
||||
path: "/workspace/traffic-distribution",
|
||||
bgColor: "bg-blue-100",
|
||||
},
|
||||
{
|
||||
id: "ai-assistant",
|
||||
name: "AI对话助手",
|
||||
description: "智能回复,提高互动质量",
|
||||
icon: <MessageSquare className="h-5 w-5 text-blue-500" />,
|
||||
path: "/workspace/ai-assistant",
|
||||
bgColor: "bg-blue-100",
|
||||
isNew: true,
|
||||
},
|
||||
]
|
||||
// 处理功能点击
|
||||
const handleFunctionClick = (func: WorkspaceFunction) => {
|
||||
recordAccess(func.id)
|
||||
router.push(func.path)
|
||||
}
|
||||
|
||||
// AI智能助手
|
||||
const aiFeatures = [
|
||||
{
|
||||
id: "ai-analyzer",
|
||||
name: "AI数据分析",
|
||||
description: "智能分析客户行为特征",
|
||||
icon: <BarChart2 className="h-5 w-5 text-indigo-500" />,
|
||||
path: "/workspace/ai-analyzer",
|
||||
bgColor: "bg-indigo-100",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-strategy",
|
||||
name: "AI策略优化",
|
||||
description: "智能优化获客策略",
|
||||
icon: <Brain className="h-5 w-5 text-cyan-500" />,
|
||||
path: "/workspace/ai-strategy",
|
||||
bgColor: "bg-cyan-100",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-forecast",
|
||||
name: "AI销售预测",
|
||||
description: "智能预测销售趋势",
|
||||
icon: <LineChart className="h-5 w-5 text-amber-500" />,
|
||||
path: "/workspace/ai-forecast",
|
||||
bgColor: "bg-amber-100",
|
||||
},
|
||||
]
|
||||
// 功能卡片组件
|
||||
const FunctionCard = ({ func }: { func: WorkspaceFunction }) => (
|
||||
<Card
|
||||
className="bg-white shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer border-0"
|
||||
onClick={() => handleFunctionClick(func)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={`p-3 rounded-xl ${func.bgColor}`}>
|
||||
<div className={func.color}>{func.icon}</div>
|
||||
</div>
|
||||
{func.isNew && <Badge className="bg-blue-500 text-white text-xs px-2 py-1 rounded-full">New</Badge>}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 mb-1">{func.title}</h3>
|
||||
<p className="text-sm text-gray-500 leading-relaxed">{func.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-4 bg-gray-50 pb-16">
|
||||
<div className="max-w-md mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-4">工作台</h1>
|
||||
|
||||
{/* 任务统计卡片 */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-sm text-gray-500">总任务数</div>
|
||||
<div className="text-3xl font-bold text-blue-500 mt-1">{taskStats.total}</div>
|
||||
<Progress value={(taskStats.inProgress / taskStats.total) * 100} className="h-2 mt-2 bg-blue-100" />
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
进行中: {taskStats.inProgress} / 已完成: {taskStats.completed}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-sm text-gray-500">今日任务</div>
|
||||
<div className="text-3xl font-bold text-green-500 mt-1">{taskStats.todayTasks}</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-green-500 mr-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12H7L10 19L14 5L17 12H21"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm">活跃度 {taskStats.activityRate}%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex-1 pb-16 bg-gray-50 min-h-screen">
|
||||
{/* 顶部标题 */}
|
||||
<header className="bg-white border-b">
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900">工作台</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
{/* 常用功能 */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-medium mb-3">常用功能</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{commonFeatures.map((feature) => (
|
||||
<Link href={feature.path} key={feature.id}>
|
||||
<Card className="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${feature.bgColor} flex items-center justify-center mb-3`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-medium">{feature.name}</div>
|
||||
{feature.isNew && (
|
||||
<Badge className="ml-2 bg-blue-100 text-blue-600 hover:bg-blue-100 border-0">New</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{feature.description}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">常用功能</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{commonFunctions.map((func) => (
|
||||
<FunctionCard key={func.id} func={func} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI智能助手 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-medium mb-3">AI 智能助手</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{aiFeatures.map((feature) => (
|
||||
<Link href={feature.path} key={feature.id}>
|
||||
<Card className="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${feature.bgColor} flex items-center justify-center mb-3`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-medium">{feature.name}</div>
|
||||
{feature.isNew && (
|
||||
<Badge className="ml-2 bg-blue-100 text-blue-600 hover:bg-blue-100 border-0">New</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{feature.description}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">AI智能助手</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{aiFunctions.map((func) => (
|
||||
<FunctionCard key={func.id} func={func} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -531,4 +531,3 @@ export default function EditRulePage({ params }: { params: { id: string } }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -417,4 +417,3 @@ export default function RuleSettingsPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -119,4 +119,3 @@ export default function PricingPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,271 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, use } from "react"
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Plus, Users, Database, Settings } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import StepIndicator from "../../new/components/step-indicator"
|
||||
import BasicInfoStep from "../../new/components/basic-info-step"
|
||||
import TargetSettingsStep from "../../new/components/target-settings-step"
|
||||
import TrafficPoolStep from "../../new/components/traffic-pool-step"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
|
||||
|
||||
interface BasicInfoData {
|
||||
name: string
|
||||
distributeType: number
|
||||
maxPerDay: number
|
||||
timeType: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
account?: string[]
|
||||
accounts?: string[]
|
||||
// 模拟数据
|
||||
const mockDistributionRule = {
|
||||
id: "1",
|
||||
name: "抖音直播引流计划",
|
||||
description: "从抖音直播间获取的潜在客户流量分发",
|
||||
status: "active",
|
||||
dailyDistributionLimit: 85,
|
||||
deviceIds: ["dev1", "dev2", "dev3"],
|
||||
trafficPoolIds: ["pool1", "pool2"],
|
||||
distributionStrategy: "even",
|
||||
autoAdjust: true,
|
||||
}
|
||||
|
||||
interface TargetSettingsData extends Omit<FormData['targetSettings'], 'account'> {
|
||||
accounts?: string[]
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
basicInfo: {
|
||||
name: string
|
||||
source?: string
|
||||
sourceIcon?: string
|
||||
description?: string
|
||||
distributeType: number
|
||||
maxPerDay: number
|
||||
timeType: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
targetSettings: {
|
||||
targetGroups: string[]
|
||||
devices: string[]
|
||||
account?: string[]
|
||||
}
|
||||
trafficPool: {
|
||||
poolIds: string[]
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export default function EditTrafficDistributionPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
export default function EditTrafficDistributionPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
basicInfo: {
|
||||
const [isDeviceDialogOpen, setIsDeviceDialogOpen] = useState(false)
|
||||
const [isPoolDialogOpen, setIsPoolDialogOpen] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
distributeType: 1,
|
||||
maxPerDay: 100,
|
||||
timeType: 2,
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
source: "",
|
||||
sourceIcon: "",
|
||||
description: "",
|
||||
},
|
||||
targetSettings: {
|
||||
targetGroups: [],
|
||||
devices: [],
|
||||
},
|
||||
trafficPool: {
|
||||
poolIds: [],
|
||||
},
|
||||
status: "active",
|
||||
dailyDistributionLimit: 100,
|
||||
deviceIds: [] as string[],
|
||||
trafficPoolIds: [] as string[],
|
||||
distributionStrategy: "even", // even, weighted, priority
|
||||
autoAdjust: true,
|
||||
})
|
||||
const [devices, setDevices] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setDevices(formData.targetSettings.devices || [])
|
||||
}, [formData.targetSettings.devices])
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: "基本信息", icon: <Plus className="h-6 w-6" /> },
|
||||
{ id: 2, title: "目标设置", icon: <Users className="h-6 w-6" /> },
|
||||
{ id: 3, title: "流量池选择", icon: <Database className="h-6 w-6" /> },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟API请求获取计划详情
|
||||
const fetchData = async () => {
|
||||
if (!id) {
|
||||
showToast("任务ID无效", "error")
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
const loadingToast = showToast("正在加载分发规则...", "loading", true)
|
||||
try {
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/detail?id=${id}`)
|
||||
if (response.code === 200 && response.data) {
|
||||
const data = response.data
|
||||
// 实际项目中应从API获取数据
|
||||
// const response = await fetch(`/api/traffic-distribution/${params.id}`)
|
||||
// const data = await response.json()
|
||||
// setFormData(data)
|
||||
|
||||
// 使用模拟数据
|
||||
setTimeout(() => {
|
||||
setFormData({
|
||||
basicInfo: {
|
||||
name: data.name || "",
|
||||
distributeType: data.config?.distributeType || 1,
|
||||
maxPerDay: data.config?.maxPerDay || 100,
|
||||
timeType: data.config?.timeType || 2,
|
||||
startTime: data.config?.startTime || "08:00",
|
||||
endTime: data.config?.endTime || "22:00",
|
||||
source: data.source || "",
|
||||
sourceIcon: data.sourceIcon || "",
|
||||
description: data.description || "",
|
||||
},
|
||||
targetSettings: {
|
||||
targetGroups: data.config?.targetGroups || [],
|
||||
devices: (data.config?.devices || []).map(String),
|
||||
account: (data.config?.account || []).map(String),
|
||||
},
|
||||
trafficPool: {
|
||||
poolIds: (data.config?.pools || []).map(String),
|
||||
},
|
||||
...mockDistributionRule,
|
||||
id: params.id,
|
||||
})
|
||||
} else {
|
||||
showToast(response.msg || "获取分发规则失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error("获取分发规则失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove()
|
||||
toast({
|
||||
title: "加载失败",
|
||||
description: "无法加载分发规则详情",
|
||||
variant: "destructive",
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [id])
|
||||
}, [params.id, toast])
|
||||
|
||||
const handleBasicInfoNext = (data: BasicInfoData) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
basicInfo: {
|
||||
name: data.name,
|
||||
distributeType: data.distributeType,
|
||||
maxPerDay: data.maxPerDay,
|
||||
timeType: data.timeType,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
source: prev.basicInfo.source,
|
||||
sourceIcon: prev.basicInfo.sourceIcon,
|
||||
description: prev.basicInfo.description,
|
||||
},
|
||||
targetSettings: {
|
||||
...prev.targetSettings,
|
||||
account: data.account || data.accounts
|
||||
}
|
||||
}))
|
||||
setCurrentStep(1)
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleTargetSettingsNext = (data: TargetSettingsData) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
targetSettings: {
|
||||
...data,
|
||||
account: data.accounts || prev.targetSettings.account
|
||||
}
|
||||
}))
|
||||
setDevices(data.devices || [])
|
||||
setCurrentStep(2)
|
||||
const handleSwitchChange = (checked: boolean, name: string) => {
|
||||
setFormData((prev) => ({ ...prev, [name]: checked }))
|
||||
}
|
||||
|
||||
const handleTargetSettingsBack = () => {
|
||||
setCurrentStep(0)
|
||||
const handleSelectChange = (value: string, name: string) => {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleTrafficPoolBack = () => {
|
||||
setCurrentStep(1)
|
||||
}
|
||||
|
||||
const handleSubmit = async (data: FormData["trafficPool"]) => {
|
||||
const finalData = {
|
||||
...formData,
|
||||
trafficPool: data,
|
||||
}
|
||||
const loadingToast = showToast("正在保存分发计划...", "loading", true)
|
||||
try {
|
||||
const response = await api.post<ApiResponse>("/v1/workbench/update", {
|
||||
id: id,
|
||||
type: 5,
|
||||
name: finalData.basicInfo.name,
|
||||
source: finalData.basicInfo.source,
|
||||
sourceIcon: finalData.basicInfo.sourceIcon,
|
||||
description: finalData.basicInfo.description,
|
||||
distributeType: finalData.basicInfo.distributeType,
|
||||
maxPerDay: finalData.basicInfo.maxPerDay,
|
||||
timeType: finalData.basicInfo.timeType,
|
||||
startTime: finalData.basicInfo.startTime,
|
||||
endTime: finalData.basicInfo.endTime,
|
||||
targetGroups: finalData.targetSettings.targetGroups,
|
||||
devices: finalData.targetSettings.devices,
|
||||
account: finalData.targetSettings.account,
|
||||
pools: finalData.trafficPool.poolIds,
|
||||
enabled: true,
|
||||
})
|
||||
if (response.code === 200) {
|
||||
loadingToast.remove()
|
||||
showToast(response.msg || "保存成功", "success")
|
||||
router.push("/workspace/traffic-distribution")
|
||||
} else {
|
||||
loadingToast.remove()
|
||||
showToast(response.msg || "保存失败,请稍后重试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("保存分发计划失败:", error)
|
||||
loadingToast.remove()
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-20 text-gray-400">加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-md mx-auto pb-20">
|
||||
<div className="sticky top-0 bg-white z-10 pb-2">
|
||||
<div className="flex items-center py-4 border-b">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-bold">编辑流量分发</h1>
|
||||
|
||||
</div>
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{currentStep === 0 && (
|
||||
<BasicInfoStep
|
||||
onNext={handleBasicInfoNext}
|
||||
initialData={{
|
||||
...formData.basicInfo,
|
||||
accounts: formData.targetSettings.account,
|
||||
account: formData.targetSettings.account
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 1 && (
|
||||
<TargetSettingsStep
|
||||
onNext={handleTargetSettingsNext}
|
||||
onBack={handleTargetSettingsBack}
|
||||
initialData={{
|
||||
...formData.targetSettings,
|
||||
devices,
|
||||
accounts: formData.targetSettings.account
|
||||
}}
|
||||
setDevices={setDevices}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<TrafficPoolStep onSubmit={handleSubmit} onBack={handleTrafficPoolBack} initialData={formData.trafficPool} devices={devices} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const handleDeviceSelection = (selectedDevices: string[]) => {
|
||||
setFormData((prev) => ({ ...prev,\
|
||||
|
||||
@@ -41,7 +41,6 @@ interface DistributionPlan {
|
||||
}
|
||||
|
||||
export default function DistributionPlanDetailPage({ params }: { params: { id: string } }) {
|
||||
const { id } = params
|
||||
const router = useRouter()
|
||||
const [plan, setPlan] = useState<DistributionPlan | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -50,7 +49,7 @@ export default function DistributionPlanDetailPage({ params }: { params: { id: s
|
||||
// 模拟API请求
|
||||
setTimeout(() => {
|
||||
setPlan({
|
||||
id: id,
|
||||
id: params.id,
|
||||
name: "抖音直播引流计划",
|
||||
status: "active",
|
||||
source: "douyin",
|
||||
@@ -84,7 +83,7 @@ export default function DistributionPlanDetailPage({ params }: { params: { id: s
|
||||
})
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
}, [id])
|
||||
}, [params.id])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -102,7 +101,7 @@ export default function DistributionPlanDetailPage({ params }: { params: { id: s
|
||||
<div className="flex-1 bg-gray-50 min-h-screen p-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-medium text-gray-700">未找到计划</h2>
|
||||
<p className="text-gray-500 mt-2">无法找到ID为 {id} 的分发计划</p>
|
||||
<p className="text-gray-500 mt-2">无法找到ID为 {params.id} 的分发计划</p>
|
||||
<Button className="mt-4" onClick={() => router.push("/workspace/traffic-distribution")}>
|
||||
返回列表
|
||||
</Button>
|
||||
@@ -121,7 +120,7 @@ export default function DistributionPlanDetailPage({ params }: { params: { id: s
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">计划详情</h1>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => router.push(`/workspace/traffic-distribution/${id}/edit`)}>
|
||||
<Button variant="outline" onClick={() => router.push(`/workspace/traffic-distribution/${params.id}/edit`)}>
|
||||
编辑计划
|
||||
</Button>
|
||||
</div>
|
||||
@@ -157,31 +156,35 @@ export default function DistributionPlanDetailPage({ params }: { params: { id: s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据区域:上3下2布局,单元格加分隔线 */}
|
||||
<div className="grid grid-cols-3 bg-white rounded-t-lg overflow-hidden border-t border-l border-r mt-6">
|
||||
<div className="flex flex-col items-center justify-center py-4 border-r border-gray-200">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">{plan.dailyAverage}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">日均分发人数</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-4 border-r border-gray-200">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">{plan.deviceCount}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">分发设备</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-4">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">{plan.poolCount}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">流量池</div>
|
||||
<div className="grid grid-cols-5 gap-2 mt-6 bg-gray-50 rounded-lg p-2">
|
||||
<div className="p-2 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">日均分发人数</div>
|
||||
<div className="text-lg font-semibold">{plan.dailyAverage}</div>
|
||||
</div>
|
||||
<div className="p-2 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">分发设备</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<Users className="h-4 w-4 mr-1 text-blue-500" />
|
||||
{plan.deviceCount}
|
||||
</div>
|
||||
{/* 横向分隔线 */}
|
||||
<div className="border-t border-gray-200 mx-auto w-full" style={{height: 0}} />
|
||||
<div className="grid grid-cols-2 bg-white rounded-b-lg overflow-hidden border-b border-l border-r">
|
||||
<div className="flex flex-col items-center justify-center py-4 border-r border-gray-200">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">{plan.dailyAverage}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">日均分发量</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-4">
|
||||
<div className="text-2xl font-bold text-gray-900 mb-1">{plan.totalUsers}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">总流量池数量</div>
|
||||
<div className="p-2 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">流量池</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<Database className="h-4 w-4 mr-1 text-green-500" />
|
||||
{plan.poolCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">日均分发量</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<TrendingUp className="h-4 w-4 mr-1 text-amber-500" />
|
||||
{plan.dailyAverage}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">总流量池数量</div>
|
||||
<div className="text-lg font-semibold">{plan.totalUsers}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,86 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { format } from "date-fns"
|
||||
import { Search, Users } from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
import { api } from "@/lib/api"
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com'
|
||||
|
||||
interface BasicInfoStepProps {
|
||||
onNext: (data: any) => void
|
||||
initialData?: {
|
||||
name?: string
|
||||
distributeType?: string | number
|
||||
maxPerDay?: number
|
||||
timeType?: string | number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
accounts?: string[]
|
||||
account?: string[]
|
||||
}
|
||||
initialData?: any
|
||||
}
|
||||
|
||||
export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoStepProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: initialData.name ?? `流量分发 ${format(new Date(), "yyyyMMdd HHmm")}`,
|
||||
distributeType: String(initialData.distributeType ?? "1"),
|
||||
maxPerDay: initialData.maxPerDay ?? 200,
|
||||
timeType: String(initialData.timeType ?? "2"),
|
||||
startTime: initialData.startTime ?? "09:00",
|
||||
endTime: initialData.endTime ?? "18:00",
|
||||
name: initialData.name || `流量分发 ${format(new Date(), "yyyyMMdd HHmm")}`,
|
||||
distributionMethod: initialData.distributionMethod || "equal",
|
||||
dailyLimit: initialData.dailyLimit || 50,
|
||||
timeRestriction: initialData.timeRestriction || "custom",
|
||||
startTime: initialData.startTime || "09:00",
|
||||
endTime: initialData.endTime || "18:00",
|
||||
})
|
||||
|
||||
// 账号选择相关状态
|
||||
const [accountDialogOpen, setAccountDialogOpen] = useState(false)
|
||||
const [accountList, setAccountList] = useState<any[]>([])
|
||||
const [selectedAccountIds, setSelectedAccountIds] = useState<string[]>(
|
||||
(initialData.account || initialData.accounts || []).map(String)
|
||||
)
|
||||
const [accountPage, setAccountPage] = useState(1)
|
||||
const [accountTotal, setAccountTotal] = useState(0)
|
||||
const [accountLoading, setAccountLoading] = useState(false)
|
||||
|
||||
// API配置弹窗状态
|
||||
const [apiDialogOpen, setApiDialogOpen] = useState(false)
|
||||
const [apiKey] = useState("naxf1-82h2f-vdwcm-rrhpm-q9hd1") // 这里可以从后端获取或生成
|
||||
const [apiUrl] = useState(`${API_BASE_URL}/v1/plan/api/scenariosz`)
|
||||
|
||||
// 拉取账号列表
|
||||
useEffect(() => {
|
||||
setAccountLoading(true)
|
||||
api.get(`/v1/workbench/account-list?page=${accountPage}&size=10`).then((res: any) => {
|
||||
setAccountList(res.data?.list || [])
|
||||
setAccountTotal(res.data?.total || 0)
|
||||
}).finally(() => setAccountLoading(false))
|
||||
}, [accountPage])
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
onNext({
|
||||
name: formData.name,
|
||||
distributeType: Number(formData.distributeType),
|
||||
maxPerDay: formData.maxPerDay,
|
||||
timeType: Number(formData.timeType),
|
||||
startTime: formData.timeType == "2" ? formData.startTime : "09:00",
|
||||
endTime: formData.timeType == "2" ? formData.endTime : "21:00",
|
||||
account: selectedAccountIds,
|
||||
accounts: selectedAccountIds,
|
||||
})
|
||||
}
|
||||
|
||||
// 账号弹窗确认
|
||||
const handleAccountDialogConfirm = () => {
|
||||
setAccountDialogOpen(false)
|
||||
onNext(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -104,16 +52,28 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
|
||||
<div className="space-y-2">
|
||||
<Label>分配方式</Label>
|
||||
<RadioGroup
|
||||
value={String(formData.distributeType)}
|
||||
onValueChange={(value) => handleChange("distributeType", value)}
|
||||
value={formData.distributionMethod}
|
||||
onValueChange={(value) => handleChange("distributionMethod", value)}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="1" id="equal" />
|
||||
<RadioGroupItem value="equal" id="equal" />
|
||||
<Label htmlFor="equal" className="cursor-pointer">
|
||||
均分配 <span className="text-gray-500 text-sm">(流量将均分配给所有客服)</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="priority" id="priority" />
|
||||
<Label htmlFor="priority" className="cursor-pointer">
|
||||
优先级分配 <span className="text-gray-500 text-sm">(按客服优先级顺序分配)</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="ratio" id="ratio" />
|
||||
<Label htmlFor="ratio" className="cursor-pointer">
|
||||
比例分配 <span className="text-gray-500 text-sm">(按设定比例分配流量)</span>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
@@ -123,41 +83,41 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>每日最大分配量</span>
|
||||
<span className="font-medium">{formData.maxPerDay} 人/天</span>
|
||||
<span className="font-medium">{formData.dailyLimit} 人/天</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[formData.maxPerDay]}
|
||||
value={[formData.dailyLimit]}
|
||||
min={1}
|
||||
max={1000}
|
||||
max={200}
|
||||
step={1}
|
||||
onValueChange={(value) => handleChange("maxPerDay", value[0])}
|
||||
onValueChange={(value) => handleChange("dailyLimit", value[0])}
|
||||
className="py-4"
|
||||
/>
|
||||
<p className="text-sm text-gray-500">限制每天最多分配的流量数量(1-1000)</p>
|
||||
<p className="text-sm text-gray-500">限制每天最多分配的流量数量</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<Label>时间限制</Label>
|
||||
<RadioGroup
|
||||
value={String(formData.timeType)}
|
||||
onValueChange={(value) => handleChange("timeType", value)}
|
||||
value={formData.timeRestriction}
|
||||
onValueChange={(value) => handleChange("timeRestriction", value)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="1" id="allDay" />
|
||||
<RadioGroupItem value="allDay" id="allDay" />
|
||||
<Label htmlFor="allDay" className="cursor-pointer">
|
||||
全天分配
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="2" id="custom" />
|
||||
<RadioGroupItem value="custom" id="custom" />
|
||||
<Label htmlFor="custom" className="cursor-pointer">
|
||||
自定义时间段
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{formData.timeType == "2" && (
|
||||
{formData.timeRestriction === "custom" && (
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div>
|
||||
<Label htmlFor="startTime" className="mb-2 block">
|
||||
@@ -189,91 +149,13 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账号选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label>选择账号 <span className="text-red-500 ml-1">*</span></Label>
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择账号"
|
||||
value={selectedAccountIds.length > 0 ? `已选择${selectedAccountIds.length}个账号` : ''}
|
||||
readOnly
|
||||
className="pl-10 cursor-pointer"
|
||||
onClick={() => setAccountDialogOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Users className="w-4 h-4 text-blue-500" />
|
||||
<span>已选账号:</span>
|
||||
{selectedAccountIds.length === 0 ? (
|
||||
<span className="text-gray-400">未选择</span>
|
||||
) : (
|
||||
<span className="bg-blue-50 text-blue-600 rounded px-2 py-0.5 font-semibold">{selectedAccountIds.length} 个</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
<Button onClick={handleSubmit} disabled={selectedAccountIds.length === 0} className="px-8">
|
||||
<Button onClick={handleSubmit} className="px-8">
|
||||
下一步 →
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 账号选择弹窗 */}
|
||||
<Dialog open={accountDialogOpen} onOpenChange={setAccountDialogOpen}>
|
||||
<DialogContent className="max-w-xl w-full p-0 rounded-2xl shadow-2xl max-h-[80vh]">
|
||||
<DialogTitle className="text-lg font-bold text-center py-3 border-b">选择账号</DialogTitle>
|
||||
<div className="p-6 pt-4">
|
||||
{/* 账号列表 */}
|
||||
<div className="max-h-[500px] overflow-y-auto space-y-2">
|
||||
{accountLoading ? (
|
||||
<div className="text-center text-gray-400 py-8">加载中...</div>
|
||||
) : accountList.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">暂无账号</div>
|
||||
) : (
|
||||
accountList.map(account => (
|
||||
<label
|
||||
key={account.id}
|
||||
className={`
|
||||
flex items-center gap-3 p-4 rounded-xl border
|
||||
${selectedAccountIds.includes(String(account.id)) ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white"}
|
||||
hover:border-blue-400 transition-colors cursor-pointer
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-500 scale-110"
|
||||
checked={selectedAccountIds.includes(String(account.id))}
|
||||
onChange={() => {
|
||||
setSelectedAccountIds(prev =>
|
||||
prev.includes(String(account.id))
|
||||
? prev.filter(id => id !== String(account.id))
|
||||
: [...prev, String(account.id)]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-base">{account.realName || account.nickname}</div>
|
||||
<div className="text-xs text-gray-500">账号: {account.userName || '--'}</div>
|
||||
</div>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{/* 确认按钮 */}
|
||||
<div className="flex justify-center mt-8">
|
||||
<Button
|
||||
className="w-4/5 py-3 rounded-full text-base font-bold shadow-md"
|
||||
onClick={handleAccountDialogConfirm}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Avatar } from "@/components/ui/avatar"
|
||||
import { Search, Smartphone } from "lucide-react"
|
||||
import { Search } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { api } from "@/lib/api"
|
||||
import { DeviceSelectionDialog } from "@/app/components/device-selection-dialog"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
@@ -19,165 +16,165 @@ interface Device {
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
interface CustomerService {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline"
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
interface TargetSettingsStepProps {
|
||||
onNext: (data: any) => void
|
||||
onBack: () => void
|
||||
initialData?: any
|
||||
}
|
||||
|
||||
export default function TargetSettingsStep({ onNext, onBack, initialData = {}, setDevices }: TargetSettingsStepProps & { setDevices: (ids: string[]) => void }) {
|
||||
const [deviceList, setDeviceList] = useState<any[]>([])
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([])
|
||||
const [search, setSearch] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
export default function TargetSettingsStep({ onNext, onBack, initialData = {} }: TargetSettingsStepProps) {
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialData.selectedDevices || [])
|
||||
const [selectedCustomerServices, setSelectedCustomerServices] = useState<string[]>(
|
||||
initialData.selectedCustomerServices || [],
|
||||
)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
|
||||
// 每次 initialData.devices 变化时,同步 selectedDeviceIds
|
||||
useEffect(() => {
|
||||
const ids = Array.isArray(initialData.devices) ? initialData.devices.map(String) : [];
|
||||
setSelectedDeviceIds(ids);
|
||||
}, [initialData.devices])
|
||||
// 模拟设备数据
|
||||
const devices: Device[] = [
|
||||
{ id: "1", name: "设备 1", status: "online" },
|
||||
{ id: "2", name: "设备 2", status: "online" },
|
||||
{ id: "3", name: "设备 3", status: "offline" },
|
||||
{ id: "4", name: "设备 4", status: "online" },
|
||||
{ id: "5", name: "设备 5", status: "offline" },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
api.get('/v1/devices?page=1&limit=100').then((res: any) => {
|
||||
setDeviceList(res.data?.list || [])
|
||||
}).finally(() => setLoading(false))
|
||||
}, [])
|
||||
// 模拟客服数据
|
||||
const customerServices: CustomerService[] = [
|
||||
{ id: "1", name: "客服 A", status: "online" },
|
||||
{ id: "2", name: "客服 B", status: "online" },
|
||||
{ id: "3", name: "客服 C", status: "offline" },
|
||||
{ id: "4", name: "客服 D", status: "online" },
|
||||
]
|
||||
|
||||
const filteredDevices = deviceList.filter(device => {
|
||||
const matchesSearch =
|
||||
search === "" ||
|
||||
(device.memo || device.nickname || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(device.imei || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(device.wechatId || "").toLowerCase().includes(search.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || (statusFilter === "online" ? device.alive === 1 : device.alive !== 1)
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
const filteredDevices = devices.filter((device) => device.name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
|
||||
const handleSubmit = () => {
|
||||
onNext({ devices: selectedDeviceIds })
|
||||
const filteredCustomerServices = customerServices.filter((cs) =>
|
||||
cs.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
const toggleDevice = (id: string) => {
|
||||
setSelectedDevices((prev) => (prev.includes(id) ? prev.filter((deviceId) => deviceId !== id) : [...prev, id]))
|
||||
}
|
||||
|
||||
// 弹窗内确认选择
|
||||
const handleDialogConfirm = () => {
|
||||
if (typeof setDevices === 'function') {
|
||||
setDevices(selectedDeviceIds)
|
||||
}
|
||||
setDeviceDialogOpen(false)
|
||||
const toggleCustomerService = (id: string) => {
|
||||
setSelectedCustomerServices((prev) => (prev.includes(id) ? prev.filter((csId) => csId !== id) : [...prev, id]))
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
onNext({
|
||||
selectedDevices,
|
||||
selectedCustomerServices,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6 shadow-sm">
|
||||
<h2 className="text-xl font-bold mb-6">目标设置</h2>
|
||||
{/* 设备选择 */}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<Input
|
||||
placeholder="选择设备"
|
||||
value={selectedDeviceIds.length > 0 ? `已选择${selectedDeviceIds.length}个设备` : ''}
|
||||
readOnly
|
||||
className="pl-10 cursor-pointer"
|
||||
onClick={() => setDeviceDialogOpen(true)}
|
||||
placeholder="搜索设备或客服"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 已选设备展示优化 */}
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Smartphone className="w-4 h-4 text-green-500" />
|
||||
<span>已选设备:</span>
|
||||
{selectedDeviceIds.length === 0 ? (
|
||||
<span className="text-gray-400">未选择</span>
|
||||
) : (
|
||||
<span className="bg-green-50 text-green-600 rounded px-2 py-0.5 font-semibold">{selectedDeviceIds.length} 个</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>← 上一步</Button>
|
||||
<Button onClick={handleSubmit} disabled={selectedDeviceIds.length === 0} className="px-8 font-bold shadow-md">下一步 →</Button>
|
||||
</div>
|
||||
{/* 设备选择弹窗 */}
|
||||
<Dialog open={deviceDialogOpen} onOpenChange={setDeviceDialogOpen}>
|
||||
<DialogContent className="max-w-xl w-full p-0 rounded-2xl shadow-2xl max-h-[80vh]">
|
||||
<DialogTitle className="text-lg font-bold text-center py-3 border-b">选择设备</DialogTitle>
|
||||
<div className="p-6 pt-4">
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Input
|
||||
placeholder="搜索设备IMEI/备注/微信号"
|
||||
value={search}
|
||||
onChange={e => setSearch(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)}
|
||||
|
||||
<Tabs defaultValue="devices" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger value="devices">设备选择</TabsTrigger>
|
||||
<TabsTrigger value="customerService">客服选择</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="devices" className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{filteredDevices.map((device) => (
|
||||
<Card
|
||||
key={device.id}
|
||||
className={`cursor-pointer border ${selectedDevices.includes(device.id) ? "border-blue-500" : "border-gray-200"}`}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="online">在线</option>
|
||||
<option value="offline">离线</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* 设备列表 */}
|
||||
<div className="max-h-[500px] overflow-y-auto space-y-2">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-400 py-8">加载中...</div>
|
||||
) : filteredDevices.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">暂无设备</div>
|
||||
) : (
|
||||
filteredDevices.map(device => (
|
||||
<label
|
||||
key={device.id}
|
||||
className={`
|
||||
flex items-center gap-3 p-4 rounded-xl border
|
||||
${selectedDeviceIds.includes(String(device.id)) ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white"}
|
||||
hover:border-blue-400 transition-colors cursor-pointer
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-500 scale-110"
|
||||
checked={selectedDeviceIds.includes(String(device.id))}
|
||||
onChange={() => {
|
||||
setSelectedDeviceIds(prev =>
|
||||
prev.includes(String(device.id))
|
||||
? prev.filter(id => id !== String(device.id))
|
||||
: [...prev, String(device.id)]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-base">{device.memo || device.nickname || device.name}</div>
|
||||
<div className="text-xs text-gray-500">IMEI: {device.imei}</div>
|
||||
<div className="text-xs text-gray-400">微信号: {device.wechatId || '--'}({device.nickname || '--'})</div>
|
||||
<CardContent className="p-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<div
|
||||
className={`w-full h-full flex items-center justify-center ${device.status === "online" ? "bg-green-100" : "bg-gray-100"}`}
|
||||
>
|
||||
<span className={`text-sm ${device.status === "online" ? "text-green-600" : "text-gray-600"}`}>
|
||||
{device.name.substring(0, 1)}
|
||||
</span>
|
||||
</div>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{device.name}</p>
|
||||
<p className={`text-xs ${device.status === "online" ? "text-green-600" : "text-gray-500"}`}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-xs font-medium">
|
||||
<span className={`w-2 h-2 rounded-full ${device.alive === 1 ? 'bg-green-500' : 'bg-gray-300'}`}></span>
|
||||
<span className={device.alive === 1 ? 'text-green-600' : 'text-gray-400'}>
|
||||
{device.alive === 1 ? '在线' : '离线'}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{/* 确认按钮 */}
|
||||
<div className="flex justify-center mt-8">
|
||||
<Button
|
||||
className="w-4/5 py-3 rounded-full text-base font-bold shadow-md"
|
||||
onClick={handleDialogConfirm}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selectedDevices.includes(device.id)}
|
||||
onCheckedChange={() => toggleDevice(device.id)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="customerService" className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{filteredCustomerServices.map((cs) => (
|
||||
<Card
|
||||
key={cs.id}
|
||||
className={`cursor-pointer border ${selectedCustomerServices.includes(cs.id) ? "border-blue-500" : "border-gray-200"}`}
|
||||
>
|
||||
<CardContent className="p-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<div
|
||||
className={`w-full h-full flex items-center justify-center ${cs.status === "online" ? "bg-green-100" : "bg-gray-100"}`}
|
||||
>
|
||||
<span className={`text-sm ${cs.status === "online" ? "text-green-600" : "text-gray-600"}`}>
|
||||
{cs.name.substring(0, 1)}
|
||||
</span>
|
||||
</div>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{cs.name}</p>
|
||||
<p className={`text-xs ${cs.status === "online" ? "text-green-600" : "text-gray-500"}`}>
|
||||
{cs.status === "online" ? "在线" : "离线"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selectedCustomerServices.includes(cs.id)}
|
||||
onCheckedChange={() => toggleCustomerService(cs.id)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-8 flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
← 上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={selectedDevices.length === 0 && selectedCustomerServices.length === 0}>
|
||||
下一步 →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Search } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Database } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
interface TrafficPool {
|
||||
id: string
|
||||
@@ -22,82 +19,43 @@ interface TrafficPoolStepProps {
|
||||
onSubmit: (data: any) => void
|
||||
onBack: () => void
|
||||
initialData?: any
|
||||
devices?: string[]
|
||||
}
|
||||
|
||||
export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, devices = [] }: TrafficPoolStepProps) {
|
||||
export default function TrafficPoolStep({ onSubmit, onBack, initialData = {} }: TrafficPoolStepProps) {
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>(initialData.selectedPools || [])
|
||||
const [searchInput, setSearchInput] = useState("")
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [deviceLabels, setDeviceLabels] = useState<{ label: string; count: number }[]>([])
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const pageSize = 10
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const filteredPools = deviceLabels.filter(
|
||||
|
||||
// 模拟流量池数据
|
||||
const trafficPools: TrafficPool[] = [
|
||||
{ id: "1", name: "新客流量池", count: 1250, description: "新获取的客户流量" },
|
||||
{ id: "2", name: "高意向流量池", count: 850, description: "有购买意向的客户" },
|
||||
{ id: "3", name: "复购流量池", count: 620, description: "已购买过产品的客户" },
|
||||
{ id: "4", name: "活跃流量池", count: 1580, description: "近期活跃的客户" },
|
||||
{ id: "5", name: "沉睡流量池", count: 2300, description: "长期未活跃的客户" },
|
||||
]
|
||||
|
||||
const filteredPools = trafficPools.filter(
|
||||
(pool) =>
|
||||
pool.label && pool.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
pool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
pool.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const pagedPools = filteredPools.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
||||
|
||||
// 监听 devices、currentPage、searchTerm 变化,请求标签(后端分页+搜索)
|
||||
useEffect(() => {
|
||||
if (!devices || devices.length === 0) {
|
||||
setDeviceLabels([])
|
||||
setTotal(0)
|
||||
return
|
||||
}
|
||||
const fetchLabels = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = devices.join(",")
|
||||
const res = await api.get<{ code: number; msg: string; data: { list: { label: string; count: number }[]; total: number } }>(`/v1/workbench/device-labels?deviceIds=${params}&page=${currentPage}&pageSize=${pageSize}&keyword=${encodeURIComponent(searchTerm)}`)
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
setDeviceLabels(res.data.list)
|
||||
setTotal(res.data.total || 0)
|
||||
} else {
|
||||
setDeviceLabels([])
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (e) {
|
||||
setDeviceLabels([])
|
||||
setTotal(0)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchLabels()
|
||||
}, [devices, currentPage, searchTerm])
|
||||
|
||||
// 搜索时重置分页并触发搜索
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
setSearchTerm(searchInput)
|
||||
}
|
||||
|
||||
// label 到描述的映射
|
||||
const poolDescMap: Record<string, string> = {
|
||||
"新客流量池": "新获取的客户流量",
|
||||
"高意向流量池": "有购买意向的客户",
|
||||
"复购流量池": "已购买过产品的客户",
|
||||
"活跃流量池": "近期活跃的客户",
|
||||
"沉睡流量池": "长期未活跃的客户",
|
||||
}
|
||||
|
||||
const togglePool = (label: string) => {
|
||||
setSelectedPools((prev) =>
|
||||
prev.includes(label) ? prev.filter((id) => id !== label) : [...prev, label]
|
||||
)
|
||||
const togglePool = (id: string) => {
|
||||
setSelectedPools((prev) => (prev.includes(id) ? prev.filter((poolId) => poolId !== id) : [...prev, id]))
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
onSubmit({ poolIds: selectedPools })
|
||||
// 这里可以添加实际的提交逻辑
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)) // 模拟API请求
|
||||
|
||||
onSubmit({
|
||||
selectedPools,
|
||||
// 可以添加其他需要提交的数据
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error)
|
||||
} finally {
|
||||
@@ -105,108 +63,57 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de
|
||||
}
|
||||
}
|
||||
|
||||
// 每次弹窗打开时重置分页
|
||||
useEffect(() => { if (dialogOpen) setCurrentPage(1) }, [dialogOpen])
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6 shadow-sm">
|
||||
<h2 className="text-xl font-bold mb-6">流量池选择</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<Input
|
||||
placeholder="选择流量池"
|
||||
value={selectedPools.join(", ")}
|
||||
readOnly
|
||||
className="pl-10 cursor-pointer"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
placeholder="搜索流量池"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-xl w-full p-0 rounded-2xl shadow-2xl max-h-[80vh]">
|
||||
<DialogTitle className="text-lg font-bold text-center py-3 border-b">选择流量池</DialogTitle>
|
||||
<div className="p-6 pt-4">
|
||||
{/* 搜索栏 */}
|
||||
<div className="relative mb-4 flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
className="pl-10 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} className="px-4">搜索</Button>
|
||||
</div>
|
||||
{/* 流量池列表 */}
|
||||
<div className="overflow-y-auto max-h-[400px] space-y-3">
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-400 py-8">加载中...</div>
|
||||
) : filteredPools.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">暂无流量池</div>
|
||||
) : (
|
||||
filteredPools.map((pool) => (
|
||||
<div
|
||||
key={pool.label}
|
||||
className={
|
||||
`flex items-center justify-between rounded-xl shadow-sm border transition-colors duration-150 cursor-pointer
|
||||
${selectedPools.includes(pool.label) ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white"}
|
||||
hover:border-blue-400`
|
||||
}
|
||||
onClick={() => togglePool(pool.label)}
|
||||
>
|
||||
<div className="flex items-center space-x-3 p-4 flex-1">
|
||||
|
||||
<div className="space-y-3 mt-4">
|
||||
{filteredPools.map((pool) => (
|
||||
<Card
|
||||
key={pool.id}
|
||||
className={`cursor-pointer border ${selectedPools.includes(pool.id) ? "border-blue-500" : "border-gray-200"}`}
|
||||
onClick={() => togglePool(pool.id)}
|
||||
>
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-base">{pool.label}</p>
|
||||
<p className="text-sm text-gray-500">{poolDescMap[pool.label] || ""}</p>
|
||||
<p className="font-medium">{pool.name}</p>
|
||||
<p className="text-sm text-gray-500">{pool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 mr-4">{pool.count} 人</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-500 scale-125 mr-6"
|
||||
checked={selectedPools.includes(pool.label)}
|
||||
onChange={e => {
|
||||
e.stopPropagation();
|
||||
togglePool(pool.label);
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-sm text-gray-500">{pool.count} 人</span>
|
||||
<Checkbox
|
||||
checked={selectedPools.includes(pool.id)}
|
||||
onCheckedChange={() => togglePool(pool.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{/* 分页按钮 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-6">
|
||||
<Button size="sm" variant="outline" disabled={currentPage === 1} onClick={() => setCurrentPage(p => Math.max(1, p - 1))}>上一页</Button>
|
||||
<span className="text-sm text-gray-500">第 {currentPage} / {totalPages} 页</span>
|
||||
<Button size="sm" variant="outline" disabled={currentPage === totalPages} onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}>下一页</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* 确认按钮 */}
|
||||
<div className="flex justify-center mt-8">
|
||||
<Button
|
||||
className="w-4/5 py-3 rounded-full text-base font-bold shadow-md"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
disabled={selectedPools.length === 0}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="mt-8 flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
← 上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
<Button onClick={handleSubmit} disabled={selectedPools.length === 0 || isSubmitting}>
|
||||
{isSubmitting ? "提交中..." : "完成"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,66 +9,16 @@ import StepIndicator from "./components/step-indicator"
|
||||
import BasicInfoStep from "./components/basic-info-step"
|
||||
import TargetSettingsStep from "./components/target-settings-step"
|
||||
import TrafficPoolStep from "./components/traffic-pool-step"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface FormData {
|
||||
basicInfo: {
|
||||
name: string
|
||||
source?: string
|
||||
sourceIcon?: string
|
||||
description?: string
|
||||
distributeType: number
|
||||
maxPerDay: number
|
||||
timeType: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
accounts?: string[]
|
||||
}
|
||||
targetSettings: {
|
||||
targetGroups: string[]
|
||||
targets: string[]
|
||||
devices?: string[]
|
||||
accounts?: string[]
|
||||
}
|
||||
trafficPool: {
|
||||
deviceIds: number[]
|
||||
poolIds: number[]
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: any
|
||||
}
|
||||
|
||||
export default function NewTrafficDistribution() {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
basicInfo: {
|
||||
name: "",
|
||||
distributeType: 1,
|
||||
maxPerDay: 100,
|
||||
timeType: 2,
|
||||
startTime: "08:00",
|
||||
endTime: "22:00",
|
||||
source: "",
|
||||
sourceIcon: "",
|
||||
description: "",
|
||||
},
|
||||
targetSettings: {
|
||||
targetGroups: [],
|
||||
targets: [],
|
||||
},
|
||||
trafficPool: {
|
||||
deviceIds: [],
|
||||
poolIds: [],
|
||||
},
|
||||
const [formData, setFormData] = useState({
|
||||
basicInfo: {},
|
||||
targetSettings: {},
|
||||
trafficPool: {},
|
||||
})
|
||||
const [devices, setDevices] = useState<string[]>([])
|
||||
|
||||
const steps = [
|
||||
{ id: 1, title: "基本信息", icon: <Plus className="h-6 w-6" /> },
|
||||
@@ -76,27 +26,13 @@ export default function NewTrafficDistribution() {
|
||||
{ id: 3, title: "流量池选择", icon: <Database className="h-6 w-6" /> },
|
||||
]
|
||||
|
||||
const handleBasicInfoNext = (data: FormData["basicInfo"]) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
basicInfo: data,
|
||||
targetSettings: {
|
||||
...prev.targetSettings,
|
||||
accounts: data.accounts
|
||||
}
|
||||
}))
|
||||
const handleBasicInfoNext = (data: any) => {
|
||||
setFormData((prev) => ({ ...prev, basicInfo: data }))
|
||||
setCurrentStep(1)
|
||||
}
|
||||
|
||||
const handleTargetSettingsNext = (data: FormData["targetSettings"]) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
targetSettings: {
|
||||
...data,
|
||||
accounts: prev.targetSettings.accounts
|
||||
}
|
||||
}))
|
||||
setDevices(data.devices || [])
|
||||
const handleTargetSettingsNext = (data: any) => {
|
||||
setFormData((prev) => ({ ...prev, targetSettings: data }))
|
||||
setCurrentStep(2)
|
||||
}
|
||||
|
||||
@@ -108,44 +44,30 @@ export default function NewTrafficDistribution() {
|
||||
setCurrentStep(1)
|
||||
}
|
||||
|
||||
const handleSubmit = async (data: FormData["trafficPool"]) => {
|
||||
const handleSubmit = async (data: any) => {
|
||||
const finalData = {
|
||||
...formData,
|
||||
trafficPool: data,
|
||||
}
|
||||
|
||||
const loadingToast = showToast("正在创建分发计划...", "loading", true);
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/create', {
|
||||
type: 5, // 5表示流量分发任务
|
||||
name: finalData.basicInfo.name,
|
||||
source: finalData.basicInfo.source,
|
||||
sourceIcon: finalData.basicInfo.sourceIcon,
|
||||
description: finalData.basicInfo.description,
|
||||
distributeType: finalData.basicInfo.distributeType,
|
||||
maxPerDay: finalData.basicInfo.maxPerDay,
|
||||
timeType: finalData.basicInfo.timeType,
|
||||
startTime: finalData.basicInfo.startTime,
|
||||
endTime: finalData.basicInfo.endTime,
|
||||
targetGroups: finalData.targetSettings.targetGroups,
|
||||
devices: finalData.targetSettings.devices,
|
||||
account: finalData.targetSettings.accounts,
|
||||
pools: finalData.trafficPool.poolIds,
|
||||
enabled: true, // 默认启用
|
||||
// 这里可以添加实际的API调用
|
||||
console.log("提交的数据:", finalData)
|
||||
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "流量分发规则已成功创建",
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "创建成功", "success")
|
||||
router.push("/workspace/traffic-distribution")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "创建失败,请稍后重试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("创建分发计划失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
// 跳转到列表页
|
||||
router.push("/workspace/traffic-distribution")
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error)
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +79,9 @@ export default function NewTrafficDistribution() {
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-bold">新建流量分发</h1>
|
||||
<Button variant="ghost" size="icon" className="ml-auto">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
@@ -170,17 +95,11 @@ export default function NewTrafficDistribution() {
|
||||
onNext={handleTargetSettingsNext}
|
||||
onBack={handleTargetSettingsBack}
|
||||
initialData={formData.targetSettings}
|
||||
setDevices={setDevices}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<TrafficPoolStep
|
||||
onSubmit={handleSubmit}
|
||||
onBack={handleTrafficPoolBack}
|
||||
initialData={formData.trafficPool}
|
||||
devices={devices}
|
||||
/>
|
||||
<TrafficPoolStep onSubmit={handleSubmit} onBack={handleTrafficPoolBack} initialData={formData.trafficPool} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
ChevronLeft,
|
||||
@@ -15,140 +15,85 @@ import {
|
||||
Users,
|
||||
Database,
|
||||
Clock,
|
||||
Search,
|
||||
Filter,
|
||||
RefreshCw,
|
||||
} from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import Link from "next/link"
|
||||
import BottomNav from "@/app/components/BottomNav"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
interface DistributionPlan {
|
||||
id: string | number
|
||||
companyId?: number
|
||||
id: string
|
||||
name: string
|
||||
type?: number
|
||||
status: number // 1: 进行中, 0: 已暂停
|
||||
autoStart?: number
|
||||
userId?: number
|
||||
status: "active" | "paused"
|
||||
source: string
|
||||
sourceIcon: string
|
||||
targetGroups: string[]
|
||||
totalUsers: number
|
||||
dailyAverage: number
|
||||
deviceCount: number
|
||||
poolCount: number
|
||||
lastUpdated: string
|
||||
createTime: string
|
||||
updateTime?: string
|
||||
config: {
|
||||
id?: number
|
||||
workbenchId?: number
|
||||
distributeType?: number
|
||||
maxPerDay?: number
|
||||
timeType?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
account?: string[]
|
||||
devices: string[]
|
||||
pools: string[]
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
lastUpdated?: string
|
||||
total: {
|
||||
dailyAverage: number
|
||||
totalAccounts: number
|
||||
deviceCount: number
|
||||
poolCount: number
|
||||
totalUsers: number
|
||||
}
|
||||
}
|
||||
creatorName?: string
|
||||
auto_like?: any
|
||||
moments_sync?: any
|
||||
group_push?: any
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
list: DistributionPlan[]
|
||||
total: number
|
||||
}
|
||||
creator: string
|
||||
}
|
||||
|
||||
export default function TrafficDistributionPage() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [plans, setPlans] = useState<DistributionPlan[]>([])
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const pageSize = 10
|
||||
const [plans, setPlans] = useState<DistributionPlan[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "抖音直播引流计划",
|
||||
status: "active",
|
||||
source: "douyin",
|
||||
sourceIcon: "🎬",
|
||||
targetGroups: ["新客户", "潜在客户"],
|
||||
totalUsers: 1250,
|
||||
dailyAverage: 85,
|
||||
deviceCount: 3,
|
||||
poolCount: 2,
|
||||
lastUpdated: "2024-03-18 10:30:00",
|
||||
createTime: "2024-03-10 08:30:00",
|
||||
creator: "admin",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "小红书种草计划",
|
||||
status: "active",
|
||||
source: "xiaohongshu",
|
||||
sourceIcon: "📱",
|
||||
targetGroups: ["女性用户", "美妆爱好者"],
|
||||
totalUsers: 980,
|
||||
dailyAverage: 65,
|
||||
deviceCount: 2,
|
||||
poolCount: 1,
|
||||
lastUpdated: "2024-03-17 14:20:00",
|
||||
createTime: "2024-03-12 09:15:00",
|
||||
creator: "marketing",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "微信社群活动",
|
||||
status: "paused",
|
||||
source: "wechat",
|
||||
sourceIcon: "💬",
|
||||
targetGroups: ["老客户", "会员"],
|
||||
totalUsers: 2340,
|
||||
dailyAverage: 0,
|
||||
deviceCount: 5,
|
||||
poolCount: 3,
|
||||
lastUpdated: "2024-03-15 09:45:00",
|
||||
createTime: "2024-02-28 11:20:00",
|
||||
creator: "social",
|
||||
},
|
||||
])
|
||||
|
||||
// 加载分发计划数据
|
||||
const fetchPlans = async (page: number, searchTerm?: string) => {
|
||||
const loadingToast = showToast("正在加载分发计划...", "loading", true);
|
||||
try {
|
||||
setLoading(true)
|
||||
const queryParams = new URLSearchParams({
|
||||
type: "5",
|
||||
page: page.toString(),
|
||||
limit: pageSize.toString()
|
||||
})
|
||||
|
||||
if (searchTerm) {
|
||||
queryParams.append('keyword', searchTerm)
|
||||
}
|
||||
|
||||
const response = await api.get<ApiResponse>(`/v1/workbench/list?${queryParams.toString()}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
setPlans(response.data.list)
|
||||
setTotal(response.data.total)
|
||||
} else {
|
||||
showToast(response.msg || "获取分发计划失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取分发计划失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
loadingToast.remove();
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
// 直接使用plans而不是filteredPlans
|
||||
const plansList = plans
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans(currentPage, searchTerm)
|
||||
}, [currentPage])
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchPlans(1, searchTerm)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchPlans(currentPage, searchTerm)
|
||||
}
|
||||
|
||||
const handleDelete = async (planId: string) => {
|
||||
const loadingToast = showToast("正在删除计划...", "loading", true);
|
||||
try {
|
||||
const response = await api.delete<ApiResponse>(`/v1/workbench/delete?id=${planId}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
loadingToast.remove();
|
||||
fetchPlans(currentPage, searchTerm)
|
||||
showToast(response.msg || "已成功删除分发计划", "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除计划失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
const handleDelete = (planId: string) => {
|
||||
setPlans(plans.filter((plan) => plan.id !== planId))
|
||||
}
|
||||
|
||||
const handleEdit = (planId: string) => {
|
||||
@@ -159,32 +104,12 @@ export default function TrafficDistributionPage() {
|
||||
router.push(`/workspace/traffic-distribution/${planId}`)
|
||||
}
|
||||
|
||||
const togglePlanStatus = async (planId: string, currentStatus: number) => {
|
||||
const loadingToast = showToast("正在更新计划状态...", "loading", true);
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update-status', {
|
||||
id: planId,
|
||||
status: currentStatus === 1 ? 0 : 1
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
setPlans(plans.map(plan =>
|
||||
plan.id === planId
|
||||
? { ...plan, status: currentStatus === 1 ? 0 : 1 }
|
||||
: plan
|
||||
))
|
||||
const newStatus = currentStatus === 1 ? 0 : 1
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || `计划${newStatus === 1 ? "已启动" : "已暂停"}`, "success")
|
||||
} else {
|
||||
loadingToast.remove();
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新计划状态失败:", error)
|
||||
loadingToast.remove();
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
const togglePlanStatus = (planId: string) => {
|
||||
setPlans(
|
||||
plans.map((plan) =>
|
||||
plan.id === planId ? { ...plan, status: plan.status === "active" ? "paused" : "active" } : plan,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -207,40 +132,7 @@ export default function TrafficDistributionPage() {
|
||||
</header>
|
||||
|
||||
<div className="p-4">
|
||||
<Card className="p-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="搜索计划名称"
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<Card key={index} className="p-4 animate-pulse">
|
||||
<div className="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : plans.length === 0 ? (
|
||||
{plansList.length === 0 ? (
|
||||
<div className="text-center py-12 bg-white rounded-lg border mt-4">
|
||||
<div className="text-gray-500">暂无数据</div>
|
||||
<Button
|
||||
@@ -253,127 +145,110 @@ export default function TrafficDistributionPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 mt-2">
|
||||
{plans.map((plan) => (
|
||||
{plansList.map((plan) => (
|
||||
<Card key={plan.id} className="overflow-hidden">
|
||||
{/* 卡片头部:全部元素一行排列,间距紧凑 */}
|
||||
{/* 卡片头部 */}
|
||||
<div className="p-4 bg-white border-b flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3 w-full">
|
||||
<span className="font-medium text-base truncate max-w-[40%]">{plan.name}</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ml-2 ${plan.status === 1 ? "bg-blue-500 text-white" : "bg-gray-300 text-gray-600"}`}>{plan.status === 1 ? "进行中" : "已暂停"}</span>
|
||||
<Switch
|
||||
checked={plan.status === 1}
|
||||
onCheckedChange={() => togglePlanStatus(plan.id.toString(), Number(plan.status))}
|
||||
className="ml-2"
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{/* <DropdownMenuItem onClick={() => handleView(plan.id.toString())}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
查看详情
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem onClick={() => handleEdit(plan.id.toString())}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
编辑计划
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => togglePlanStatus(plan.id.toString(), Number(plan.status))}>
|
||||
{plan.status === 1 ? (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
暂停计划
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
启动计划
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(plan.id.toString())} className="text-red-600">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除计划
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="text-2xl">{plan.sourceIcon}</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-lg">{plan.name}</h3>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<Badge variant={plan.status === "active" ? "success" : "secondary"}>
|
||||
{plan.status === "active" ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
{plan.targetGroups.map((group, index) => (
|
||||
<Badge key={index} variant="outline">
|
||||
{group}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleView(plan.id)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(plan.id)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
编辑计划
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => togglePlanStatus(plan.id)}>
|
||||
{plan.status === "active" ? (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
暂停计划
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
启动计划
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(plan.id)} className="text-red-600">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除计划
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* 卡片内容 - 上3下2布局,图标在文字左侧 */}
|
||||
<div className="bg-white">
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="p-3 text-center border-r border-gray-200">
|
||||
<div className="text-lg font-semibold">{plan.config.total.totalAccounts}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">分发账号</div>
|
||||
</div>
|
||||
<div className="p-3 text-center border-r border-gray-200">
|
||||
<div className="text-lg font-semibold">{plan.config.total.deviceCount}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">分发设备</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-semibold">{plan.config.total.poolCount}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">流量池</div>
|
||||
{/* 卡片内容 - 参考场景获客的标签样式 */}
|
||||
<div className="grid grid-cols-5 divide-x bg-white">
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">日均分发人数</div>
|
||||
<div className="text-lg font-semibold">{plan.dailyAverage}</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">分发设备</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<Users className="h-4 w-4 mr-1 text-blue-500" />
|
||||
{plan.deviceCount}
|
||||
</div>
|
||||
</div>
|
||||
{/* 横向分隔线 */}
|
||||
<div className="border-t border-gray-200 mx-auto w-full" style={{height: 0}} />
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="p-3 text-center border-r border-gray-200">
|
||||
<div className="text-lg font-semibold">{plan.config.total.dailyAverage}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">日均分发量</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">流量池</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<Database className="h-4 w-4 mr-1 text-green-500" />
|
||||
{plan.poolCount}
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-semibold">{plan.config.total.totalAccounts}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">总流量池数量</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">日均分发量</div>
|
||||
<div className="text-lg font-semibold flex items-center justify-center">
|
||||
<TrendingUp className="h-4 w-4 mr-1 text-amber-500" />
|
||||
{plan.dailyAverage}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">总流量池数量</div>
|
||||
<div className="text-lg font-semibold">{plan.totalUsers}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="p-3 bg-gray-50 text-sm text-gray-500 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-1" />
|
||||
<span>上次执行: {plan.config.lastUpdated}</span>
|
||||
<span>上次执行: {plan.lastUpdated.split(" ")[0]}</span>
|
||||
</div>
|
||||
<div>创建人: {plan.creatorName}</div>
|
||||
<div>创建人: {plan.creator}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页 */}
|
||||
{!loading && total > pageSize && (
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm text-gray-500">第 {currentPage} 页</span>
|
||||
<span className="text-sm text-gray-500">共 {Math.ceil(total / pageSize)} 页</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(total / pageSize), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -162,4 +162,3 @@ export default function TrafficPricingPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user