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

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