存客宝 React
This commit is contained in:
20
Cunkebao/app/api/acquisition/[planId]/orders/route.ts
Normal file
20
Cunkebao/app/api/acquisition/[planId]/orders/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import type { OrderFormData } from "@/types/acquisition"
|
||||
|
||||
export async function POST(request: Request, { params }: { params: { planId: string } }) {
|
||||
try {
|
||||
const data: OrderFormData = await request.json()
|
||||
|
||||
// 这里应该添加实际的数据库存储逻辑
|
||||
console.log("Received order:", data, "for plan:", params.planId)
|
||||
|
||||
// 模拟成功响应
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "订单已成功提交",
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json({ success: false, message: "订单提交失败" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
69
Cunkebao/app/api/auth.ts
Normal file
69
Cunkebao/app/api/auth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
// API请求工具函数
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"
|
||||
|
||||
// 带有认证的请求函数
|
||||
export async function authFetch(url: string, options: RequestInit = {}) {
|
||||
const token = localStorage.getItem("token")
|
||||
|
||||
// 合并headers
|
||||
let headers = { ...options.headers }
|
||||
|
||||
// 如果有token,添加到请求头
|
||||
if (token) {
|
||||
headers = {
|
||||
...headers,
|
||||
Token: `${token}`,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 检查token是否过期(仅当有token时)
|
||||
if (token && (data.code === 401 || data.code === 403)) {
|
||||
// 清除token
|
||||
localStorage.removeItem("token")
|
||||
|
||||
// 暂时不重定向到登录页
|
||||
// if (typeof window !== "undefined") {
|
||||
// window.location.href = "/login"
|
||||
// }
|
||||
|
||||
console.warn("登录已过期")
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("API请求错误:", error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "请求失败",
|
||||
description: error instanceof Error ? error.message : "网络错误,请稍后重试",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 不需要认证的请求函数
|
||||
export async function publicFetch(url: string, options: RequestInit = {}) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, options)
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error("API请求错误:", error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "请求失败",
|
||||
description: error instanceof Error ? error.message : "网络错误,请稍后重试",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
82
Cunkebao/app/api/devices/route.ts
Normal file
82
Cunkebao/app/api/devices/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import type {
|
||||
CreateDeviceParams,
|
||||
QueryDeviceParams,
|
||||
Device,
|
||||
ApiResponse,
|
||||
DeviceStatus,
|
||||
DeviceType,
|
||||
} from "@/types/device"
|
||||
|
||||
// 设备管理路由处理
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body: CreateDeviceParams = await request.json()
|
||||
|
||||
// TODO: 实现创建设备的具体逻辑
|
||||
const device: Device = {
|
||||
id: "generated-id",
|
||||
...body,
|
||||
status: DeviceStatus.OFFLINE,
|
||||
lastOnlineTime: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const response: ApiResponse<Device> = {
|
||||
code: 0,
|
||||
message: "创建成功",
|
||||
data: device,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: "创建失败",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const params: QueryDeviceParams = {
|
||||
keyword: searchParams.get("keyword") || undefined,
|
||||
status: (searchParams.get("status") as DeviceStatus) || undefined,
|
||||
type: (searchParams.get("type") as DeviceType) || undefined,
|
||||
tags: searchParams.get("tags") ? JSON.parse(searchParams.get("tags")!) : undefined,
|
||||
dateRange: searchParams.get("dateRange") ? JSON.parse(searchParams.get("dateRange")!) : undefined,
|
||||
page: Number(searchParams.get("page")) || 1,
|
||||
pageSize: Number(searchParams.get("pageSize")) || 20,
|
||||
}
|
||||
|
||||
// TODO: 实现查询设备列表的具体逻辑
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
message: "查询成功",
|
||||
data: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
totalPages: 0,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: "查询失败",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
231
Cunkebao/app/api/docs/scenarios/[channel]/[id]/page.tsx
Normal file
231
Cunkebao/app/api/docs/scenarios/[channel]/[id]/page.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { ChevronLeft, Copy, Check, Info } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { getApiGuideForScenario } from "@/docs/api-guide"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||
|
||||
export default function ApiDocPage({ params }: { params: { channel: string; id: string } }) {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [copiedExample, setCopiedExample] = useState<string | null>(null)
|
||||
|
||||
const apiGuide = getApiGuideForScenario(params.id, params.channel)
|
||||
|
||||
const copyToClipboard = (text: string, exampleId: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedExample(exampleId)
|
||||
|
||||
toast({
|
||||
title: "已复制代码",
|
||||
description: "代码示例已复制到剪贴板",
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setCopiedExample(null)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between h-14 px-4">
|
||||
<div className="flex items-center">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="ml-2 text-lg font-medium">{apiGuide.title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>接口说明</CardTitle>
|
||||
<CardDescription>{apiGuide.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4 text-blue-500" />
|
||||
<p className="text-sm text-gray-700">
|
||||
此接口用于将外部系统收集的客户信息直接导入到存客宝的获客计划中。您需要使用API密钥进行身份验证。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-amber-50 p-4 border border-amber-200">
|
||||
<p className="text-sm text-amber-800">
|
||||
<strong>安全提示:</strong>{" "}
|
||||
请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Accordion type="single" collapsible className="mb-6">
|
||||
{apiGuide.endpoints.map((endpoint, index) => (
|
||||
<AccordionItem key={index} value={`endpoint-${index}`}>
|
||||
<AccordionTrigger className="px-4 hover:bg-gray-50">
|
||||
<div className="flex items-center">
|
||||
<Badge className="mr-2">{endpoint.method}</Badge>
|
||||
<span className="font-mono text-sm">{endpoint.url}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-700">{endpoint.description}</p>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">请求头</h4>
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
{endpoint.headers.map((header, i) => (
|
||||
<div key={i} className="flex items-start mb-2 last:mb-0">
|
||||
<Badge variant="outline" className="mr-2 mt-0.5 font-mono">
|
||||
{header.required ? "*" : ""}
|
||||
{header.name}
|
||||
</Badge>
|
||||
<div>
|
||||
<p className="text-sm">{header.value}</p>
|
||||
<p className="text-xs text-gray-500">{header.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">请求参数</h4>
|
||||
<div className="bg-gray-50 rounded-md p-3">
|
||||
{endpoint.parameters.map((param, i) => (
|
||||
<div key={i} className="flex items-start mb-3 last:mb-0">
|
||||
<Badge variant="outline" className="mr-2 mt-0.5 font-mono">
|
||||
{param.required ? "*" : ""}
|
||||
{param.name}
|
||||
</Badge>
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
<span className="text-gray-500 font-mono">{param.type}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{param.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">响应示例</h4>
|
||||
<pre className="bg-gray-50 rounded-md p-3 text-xs overflow-auto">
|
||||
{JSON.stringify(endpoint.response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>代码示例</CardTitle>
|
||||
<CardDescription>以下是不同编程语言的接口调用示例</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue={apiGuide.examples[0].language}>
|
||||
<TabsList className="mb-4">
|
||||
{apiGuide.examples.map((example) => (
|
||||
<TabsTrigger key={example.language} value={example.language}>
|
||||
{example.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{apiGuide.examples.map((example) => (
|
||||
<TabsContent key={example.language} value={example.language}>
|
||||
<div className="relative">
|
||||
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{example.code}</pre>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => copyToClipboard(example.code, example.language)}
|
||||
>
|
||||
{copiedExample === example.language ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-medium mb-4">集成指南</h3>
|
||||
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">集简云平台集成</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm">
|
||||
<li>登录集简云平台</li>
|
||||
<li>导航至"应用集成" > "外部接口"</li>
|
||||
<li>选择"添加新接口",输入存客宝接口信息</li>
|
||||
<li>配置回调参数,将"X-API-KEY"设置为您的API密钥</li>
|
||||
<li>
|
||||
设置接口URL为:
|
||||
<code className="bg-gray-100 px-1 py-0.5 rounded">
|
||||
<code>{apiGuide.endpoints[0].url}</code>
|
||||
</code>
|
||||
</li>
|
||||
<li>映射必要字段(name, phone等)</li>
|
||||
<li>保存并启用集成</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">问题排查</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">接口认证失败</h4>
|
||||
<p className="text-sm text-gray-700">
|
||||
请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">数据格式错误</h4>
|
||||
<p className="text-sm text-gray-700">
|
||||
确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-1">请求频率限制</h4>
|
||||
<p className="text-sm text-gray-700">
|
||||
单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
74
Cunkebao/app/api/scenarios/route.ts
Normal file
74
Cunkebao/app/api/scenarios/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import type { CreateScenarioParams, QueryScenarioParams, ScenarioBase, ApiResponse } from "@/types/scenario"
|
||||
|
||||
// 获客场景路由处理
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body: CreateScenarioParams = await request.json()
|
||||
|
||||
// TODO: 实现创建场景的具体逻辑
|
||||
const scenario: ScenarioBase = {
|
||||
id: "generated-id",
|
||||
...body,
|
||||
status: "draft",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
creator: "current-user-id",
|
||||
}
|
||||
|
||||
const response: ApiResponse<ScenarioBase> = {
|
||||
code: 0,
|
||||
message: "创建成功",
|
||||
data: scenario,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: "创建失败",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const params: QueryScenarioParams = {
|
||||
type: searchParams.get("type") as any,
|
||||
status: searchParams.get("status") as any,
|
||||
keyword: searchParams.get("keyword") || undefined,
|
||||
dateRange: searchParams.get("dateRange") ? JSON.parse(searchParams.get("dateRange")!) : undefined,
|
||||
page: Number(searchParams.get("page")) || 1,
|
||||
pageSize: Number(searchParams.get("pageSize")) || 20,
|
||||
}
|
||||
|
||||
// TODO: 实现查询场景列表的具体逻辑
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
message: "查询成功",
|
||||
data: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
totalPages: 0,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: "查询失败",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
273
Cunkebao/app/api/users/route.ts
Normal file
273
Cunkebao/app/api/users/route.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import type { TrafficUser } from "@/types/traffic"
|
||||
|
||||
// 中文名字生成器数据
|
||||
const familyNames = [
|
||||
"张",
|
||||
"王",
|
||||
"李",
|
||||
"赵",
|
||||
"陈",
|
||||
"刘",
|
||||
"杨",
|
||||
"黄",
|
||||
"周",
|
||||
"吴",
|
||||
"朱",
|
||||
"孙",
|
||||
"马",
|
||||
"胡",
|
||||
"郭",
|
||||
"林",
|
||||
"何",
|
||||
"高",
|
||||
"梁",
|
||||
"郑",
|
||||
"罗",
|
||||
"宋",
|
||||
"谢",
|
||||
"唐",
|
||||
"韩",
|
||||
"曹",
|
||||
"许",
|
||||
"邓",
|
||||
"萧",
|
||||
"冯",
|
||||
]
|
||||
const givenNames1 = [
|
||||
"志",
|
||||
"建",
|
||||
"文",
|
||||
"明",
|
||||
"永",
|
||||
"春",
|
||||
"秀",
|
||||
"金",
|
||||
"水",
|
||||
"玉",
|
||||
"国",
|
||||
"立",
|
||||
"德",
|
||||
"海",
|
||||
"和",
|
||||
"荣",
|
||||
"伟",
|
||||
"新",
|
||||
"英",
|
||||
"佳",
|
||||
]
|
||||
const givenNames2 = [
|
||||
"华",
|
||||
"平",
|
||||
"军",
|
||||
"强",
|
||||
"辉",
|
||||
"敏",
|
||||
"峰",
|
||||
"磊",
|
||||
"超",
|
||||
"艳",
|
||||
"娜",
|
||||
"霞",
|
||||
"燕",
|
||||
"娟",
|
||||
"静",
|
||||
"丽",
|
||||
"涛",
|
||||
"洋",
|
||||
"勇",
|
||||
"龙",
|
||||
]
|
||||
|
||||
// 生成固定的用户数据池
|
||||
const userPool: TrafficUser[] = Array.from({ length: 1610 }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去7天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 7))
|
||||
|
||||
return {
|
||||
id: `${Date.now()}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页"][Math.floor(Math.random() * 6)],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
}
|
||||
})
|
||||
|
||||
// 计算今日新增数量
|
||||
const todayStart = new Date()
|
||||
todayStart.setHours(0, 0, 0, 0)
|
||||
const todayUsers = userPool.filter((user) => new Date(user.addTime) >= todayStart)
|
||||
|
||||
// 生成微信好友数据池
|
||||
const generateWechatFriends = (wechatId: string, count: number) => {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]
|
||||
const givenName1 = givenNames1[Math.floor(Math.random() * givenNames1.length)]
|
||||
const givenName2 = givenNames2[Math.floor(Math.random() * givenNames2.length)]
|
||||
const fullName = Math.random() > 0.5 ? familyName + givenName1 + givenName2 : familyName + givenName1
|
||||
|
||||
// 生成随机时间(在过去30天内)
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 30))
|
||||
|
||||
return {
|
||||
id: `wechat-${wechatId}-${i}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${fullName[0]}`,
|
||||
nickname: fullName,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
phone: `1${["3", "5", "7", "8", "9"][Math.floor(Math.random() * 5)]}${Array.from({ length: 9 }, () => Math.floor(Math.random() * 10)).join("")}`,
|
||||
region: [
|
||||
"广东深圳",
|
||||
"浙江杭州",
|
||||
"江苏苏州",
|
||||
"北京",
|
||||
"上海",
|
||||
"四川成都",
|
||||
"湖北武汉",
|
||||
"福建厦门",
|
||||
"山东青岛",
|
||||
"河南郑州",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
note: [
|
||||
"咨询产品价格",
|
||||
"对产品很感兴趣",
|
||||
"准备购买",
|
||||
"需要更多信息",
|
||||
"想了解优惠活动",
|
||||
"询问产品规格",
|
||||
"要求产品demo",
|
||||
"索要产品目录",
|
||||
"询问售后服务",
|
||||
"要求上门演示",
|
||||
][Math.floor(Math.random() * 10)],
|
||||
status: ["pending", "added", "failed"][Math.floor(Math.random() * 3)] as TrafficUser["status"],
|
||||
addTime: date.toISOString(),
|
||||
source: ["抖音直播", "小红书", "微信朋友圈", "视频号", "公众号", "个人主页", "微信好友"][
|
||||
Math.floor(Math.random() * 7)
|
||||
],
|
||||
assignedTo: "",
|
||||
category: ["potential", "customer", "lost"][Math.floor(Math.random() * 3)] as TrafficUser["category"],
|
||||
tags: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 微信好友数据缓存
|
||||
const wechatFriendsCache = new Map<string, TrafficUser[]>()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const page = Number.parseInt(searchParams.get("page") || "1")
|
||||
const pageSize = Number.parseInt(searchParams.get("pageSize") || "10")
|
||||
const search = searchParams.get("search") || ""
|
||||
const category = searchParams.get("category") || "all"
|
||||
const source = searchParams.get("source") || "all"
|
||||
const status = searchParams.get("status") || "all"
|
||||
const startDate = searchParams.get("startDate")
|
||||
const endDate = searchParams.get("endDate")
|
||||
const wechatSource = searchParams.get("wechatSource") || ""
|
||||
|
||||
let filteredUsers = [...userPool]
|
||||
|
||||
// 如果有微信来源参数,生成或获取微信好友数据
|
||||
if (wechatSource) {
|
||||
if (!wechatFriendsCache.has(wechatSource)) {
|
||||
// 生成150-300个随机好友
|
||||
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150
|
||||
wechatFriendsCache.set(wechatSource, generateWechatFriends(wechatSource, friendCount))
|
||||
}
|
||||
filteredUsers = wechatFriendsCache.get(wechatSource) || []
|
||||
}
|
||||
|
||||
// 应用过滤条件
|
||||
filteredUsers = filteredUsers.filter((user) => {
|
||||
const matchesSearch = search
|
||||
? user.nickname.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.wechatId.toLowerCase().includes(search.toLowerCase()) ||
|
||||
user.phone.includes(search)
|
||||
: true
|
||||
|
||||
const matchesCategory = category === "all" ? true : user.category === category
|
||||
const matchesSource = source === "all" ? true : user.source === source
|
||||
const matchesStatus = status === "all" ? true : user.status === status
|
||||
|
||||
const matchesDate =
|
||||
startDate && endDate
|
||||
? new Date(user.addTime) >= new Date(startDate) && new Date(user.addTime) <= new Date(endDate)
|
||||
: true
|
||||
|
||||
return matchesSearch && matchesCategory && matchesSource && matchesStatus && matchesDate
|
||||
})
|
||||
|
||||
// 按添加时间倒序排序
|
||||
filteredUsers.sort((a, b) => new Date(b.addTime).getTime() - new Date(a.addTime).getTime())
|
||||
|
||||
// 计算分页
|
||||
const total = filteredUsers.length
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const users = filteredUsers.slice(start, end)
|
||||
|
||||
// 计算分类统计
|
||||
const categoryStats = {
|
||||
potential: userPool.filter((user) => user.category === "potential").length,
|
||||
customer: userPool.filter((user) => user.category === "customer").length,
|
||||
lost: userPool.filter((user) => user.category === "lost").length,
|
||||
}
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
return NextResponse.json({
|
||||
users,
|
||||
pagination: {
|
||||
total,
|
||||
totalPages,
|
||||
currentPage: page,
|
||||
pageSize,
|
||||
},
|
||||
stats: {
|
||||
total: wechatSource ? filteredUsers.length : userPool.length,
|
||||
todayNew: wechatSource ? Math.floor(filteredUsers.length * 0.1) : todayUsers.length,
|
||||
categoryStats,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user