存客宝 React

This commit is contained in:
柳清爽
2025-03-29 16:50:39 +08:00
parent caea0b4b99
commit 7e7c199996
388 changed files with 53282 additions and 2076 deletions

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

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

View 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">
&lt;code&gt;{apiGuide.endpoints[0].url}&lt;/code&gt;
</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>
)
}

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

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