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

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

View File

@@ -461,4 +461,3 @@ export default function AnalysisReportPage({ params }: { params: { id: string }
</div>
)
}

View File

@@ -280,4 +280,3 @@ export function AnalysisSettings({ formData, updateFormData, onNext, onBack }: A
</div>
)
}

View File

@@ -177,4 +177,3 @@ export function ConfirmAnalysis({ formData, updateFormData, onSubmit, onBack }:
</div>
)
}

View File

@@ -212,4 +212,3 @@ export function DeviceSelection({ formData, updateFormData, onNext }: DeviceSele
</div>
)
}

View File

@@ -40,4 +40,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
</div>
)
}

View File

@@ -162,4 +162,3 @@ export function BasicSettings({ formData, updateFormData, onNext }: BasicSetting
</div>
)
}

View File

@@ -394,4 +394,3 @@ export function TargetSelection({ formData, updateFormData, onSubmit, onBack }:
</div>
)
}

View File

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

View File

@@ -121,4 +121,3 @@ export function BasicSettings({ formData, updateFormData, onNext }: BasicSetting
</div>
)
}

View File

@@ -394,4 +394,3 @@ export function TargetSelection({ formData, updateFormData, onSubmit, onBack }:
</div>
)
}

View File

@@ -114,4 +114,3 @@ export default function CreateAnalysisPlanPage() {
</div>
)
}

View File

@@ -365,4 +365,3 @@ export default function AIAnalyzerPage() {
</div>
)
}

View File

@@ -281,4 +281,3 @@ export default function KnowledgeBasePage() {
</div>
)
}

View File

@@ -270,4 +270,3 @@ export default function AIAssistantPage() {
</div>
)
}

View File

@@ -341,4 +341,3 @@ export default function StrategyReportPage({ params }: { params: { id: string }
</div>
)
}

View File

@@ -232,4 +232,3 @@ export function ConfirmStrategy({ formData, updateFormData, onSubmit, onBack }:
</div>
)
}

View File

@@ -200,4 +200,3 @@ export function ExecutionSetup({ formData, updateFormData, onNext, onBack }: Exe
</div>
)
}

View File

@@ -55,4 +55,3 @@ export function StepIndicator({ steps, currentStep }: StepIndicatorProps) {
</div>
)
}

View File

@@ -319,4 +319,3 @@ export function StrategySelection({ formData, updateFormData, onNext, onBack }:
</div>
)
}

View File

@@ -291,4 +291,3 @@ export function TrafficPoolSelection({ formData, updateFormData, onNext }: Traff
</div>
)
}

View File

@@ -126,4 +126,3 @@ export default function NewStrategyPage() {
</div>
)
}

View File

@@ -368,4 +368,3 @@ export default function AIStrategyPage() {
</div>
)
}

View File

@@ -184,4 +184,3 @@ export function StepByStepPlanForm({ onClose }: StepByStepPlanFormProps) {
</DialogContent>
)
}

View File

@@ -82,4 +82,3 @@ export class AutoGroupService {
})
}
}

View File

@@ -126,4 +126,3 @@ export const columns: ColumnDef<Plan>[] = [
},
},
]

View File

@@ -93,4 +93,3 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
</div>
)
}

View File

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

View File

@@ -65,4 +65,3 @@ export function DeviceSelector({ selectedDevices, onChange }: DeviceSelectorProp
</div>
)
}

View File

@@ -124,4 +124,3 @@ export function GroupAssistant({ open, onOpenChange, onCreateGroup }: GroupAssis
</Dialog>
)
}

View File

@@ -124,4 +124,3 @@ export function GroupCreationProgress({ planId, onComplete }: GroupCreationProgr
</div>
)
}

View File

@@ -94,4 +94,3 @@ export function GroupPreview({ groupIndex, members, isCreating, isCompleted, onR
</Card>
)
}

View File

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

View File

@@ -282,4 +282,3 @@ export function NewPlanForm({ onClose }: NewPlanFormProps) {
</DialogContent>
)
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>
)

View File

@@ -260,4 +260,3 @@ export function LikeConfig({ initialData, onSave, onBack }: LikeConfigProps) {
</div>
)
}

View File

@@ -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>

View File

@@ -240,4 +240,3 @@ export function TimeSettings({ initialData, onSave }: TimeSettingsProps) {
</div>
)
}

View File

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

View File

@@ -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>
)

View File

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

View File

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

View File

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

View File

@@ -344,4 +344,3 @@ export function FriendSelector({ onSelectionChange, defaultSelectedFriendIds = [
</div>
)
}

View File

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

View File

@@ -227,4 +227,3 @@ export function MessageEditor({ onMessageChange, defaultValues }: MessageEditorP
</div>
)
}

View File

@@ -78,4 +78,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
</div>
)
}

View File

@@ -61,4 +61,3 @@ export default function Loading() {
</div>
)
}

View File

@@ -82,4 +82,3 @@ export default function Loading() {
</div>
)
}

View File

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

View File

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

View File

@@ -49,4 +49,3 @@ export default function Loading() {
</div>
)
}

View File

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

View File

@@ -148,4 +148,3 @@ export function AutoGroupCreator() {
</Card>
)
}

View File

@@ -208,4 +208,3 @@ export function ContentSelector({
</div>
)
}

View File

@@ -239,4 +239,3 @@ export function GroupSelector({
</div>
)
}

View File

@@ -60,4 +60,3 @@ export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
</div>
)
}

View File

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

View File

@@ -44,4 +44,3 @@ export default function Loading() {
</div>
)
}

View File

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

View File

@@ -200,4 +200,3 @@ export default function GroupSyncPage() {
</div>
)
}

View File

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

View File

@@ -36,4 +36,3 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
</div>
)
}

View File

@@ -98,4 +98,3 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
</div>
)
}

View File

@@ -37,4 +37,3 @@ export function ContentLibrarySelection({
</div>
)
}

View File

@@ -32,4 +32,3 @@ export function DeviceSelection({ selectedDevices, onChange, onNext, onPrev }: D
</div>
)
}

View File

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

View File

@@ -127,4 +127,3 @@ export default function MomentsSyncViewPage() {
</div>
)
}

View File

@@ -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>

View File

@@ -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} />
}

View File

@@ -100,4 +100,3 @@ export function ContentViewer({ tagId }: ContentViewerProps) {
</div>
)
}

View File

@@ -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>
)

View File

@@ -49,4 +49,3 @@ export function StepIndicator({
</div>
)
}

View File

@@ -153,4 +153,3 @@ export function TagEditor({ tagId, initialData }: TagEditorProps) {
</Card>
)
}

View File

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

View File

@@ -277,4 +277,3 @@ export function ContentSelector({ formData, onChange, onNext, onPrev }: ContentS
</Card>
)
}

View File

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

View File

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

View File

@@ -531,4 +531,3 @@ export default function EditRulePage({ params }: { params: { id: string } }) {
</div>
)
}

View File

@@ -417,4 +417,3 @@ export default function RuleSettingsPage() {
</div>
)
}

View File

@@ -119,4 +119,3 @@ export default function PricingPage() {
</div>
)
}

View File

@@ -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,\

View File

@@ -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>

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

@@ -162,4 +162,3 @@ export default function TrafficPricingPage() {
</div>
)
}