From c59b179da401d1765967a4918d8a9dd09f1cfb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Mon, 7 Apr 2025 10:17:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=AE=A2=E5=9C=BA=E6=99=AFAPI?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/api/scenarios.ts | 242 +++++++------- Cunkebao/app/scenarios/page.tsx | 314 +++++++------------ Server/application/plan/controller/Scene.php | 2 +- 3 files changed, 243 insertions(+), 315 deletions(-) diff --git a/Cunkebao/api/scenarios.ts b/Cunkebao/api/scenarios.ts index d09b797b..e2f7a708 100755 --- a/Cunkebao/api/scenarios.ts +++ b/Cunkebao/api/scenarios.ts @@ -1,112 +1,136 @@ -import type { - ApiResponse, - CreateScenarioParams, - UpdateScenarioParams, - QueryScenarioParams, - ScenarioBase, - ScenarioStats, - AcquisitionRecord, - PaginatedResponse, -} from "@/types/scenario" +import { request } from "@/lib/api" -const API_BASE = "/api/scenarios" - -// 获客场景API -export const scenarioApi = { - // 创建场景 - async create(params: CreateScenarioParams): Promise> { - const response = await fetch(`${API_BASE}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }) - return response.json() - }, - - // 更新场景 - async update(params: UpdateScenarioParams): Promise> { - const response = await fetch(`${API_BASE}/${params.id}`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }) - return response.json() - }, - - // 获取场景详情 - async getById(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}`) - return response.json() - }, - - // 查询场景列表 - async query(params: QueryScenarioParams): Promise>> { - const queryString = new URLSearchParams({ - ...params, - dateRange: params.dateRange ? JSON.stringify(params.dateRange) : "", - }).toString() - - const response = await fetch(`${API_BASE}?${queryString}`) - return response.json() - }, - - // 删除场景 - async delete(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}`, { - method: "DELETE", - }) - return response.json() - }, - - // 启动场景 - async start(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/start`, { - method: "POST", - }) - return response.json() - }, - - // 暂停场景 - async pause(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/pause`, { - method: "POST", - }) - return response.json() - }, - - // 获取场景统计数据 - async getStats(id: string): Promise> { - const response = await fetch(`${API_BASE}/${id}/stats`) - return response.json() - }, - - // 获取获客记录 - async getRecords(id: string, page = 1, pageSize = 20): Promise>> { - const response = await fetch(`${API_BASE}/${id}/records?page=${page}&pageSize=${pageSize}`) - return response.json() - }, - - // 导出获客记录 - async exportRecords(id: string, dateRange?: { start: string; end: string }): Promise { - const queryString = dateRange ? `?start=${dateRange.start}&end=${dateRange.end}` : "" - const response = await fetch(`${API_BASE}/${id}/records/export${queryString}`) - return response.blob() - }, - - // 批量更新标签 - async updateTags(id: string, customerIds: string[], tags: string[]): Promise> { - const response = await fetch(`${API_BASE}/${id}/tags`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ customerIds, tags }), - }) - return response.json() - }, +export interface ApiResponse { + code: number; + msg: string; + data: T | null; } +// 服务器返回的场景数据类型 +export interface SceneItem { + id: number; + name: string; + image: string; + status: number; + createTime: number; + updateTime: number | null; + deleteTime: number | null; +} + +// 服务器返回的场景列表响应类型 +export interface ScenesResponse { + code: number; + msg: string; + data: { + list: SceneItem[]; + total: number; + page: number; + limit: number; + }; +} + +// 前端使用的场景数据类型 +export interface Channel { + id: string; + name: string; + icon: string; + stats: { + daily: number; + growth: number; + }; + link?: string; + plans?: Plan[]; +} + +// 计划类型 +export interface Plan { + id: string; + name: string; + isNew?: boolean; + status: "active" | "paused" | "completed"; + acquisitionCount: number; +} + +/** + * 获取获客场景列表 + * + * @param params 查询参数 + * @returns 获客场景列表 + */ +export const fetchScenes = async (params: { + page?: number; + limit?: number; + keyword?: string; +} = {}): Promise => { + const { page = 1, limit = 10, keyword = "" } = params; + + const queryParams = new URLSearchParams(); + queryParams.append("page", String(page)); + queryParams.append("limit", String(limit)); + + if (keyword) { + queryParams.append("keyword", keyword); + } + + try { + return await request(`/v1/plan/scenes?${queryParams.toString()}`); + } catch (error) { + console.error("Error fetching scenes:", error); + // 返回一个错误响应 + return { + code: 500, + msg: "获取场景列表失败", + data: { + list: [], + total: 0, + page: 1, + limit: 10 + } + }; + } +}; + +/** + * 将服务器返回的场景数据转换为前端展示需要的格式 + * + * @param item 服务器返回的场景数据 + * @returns 前端展示的场景数据 + */ +export const transformSceneItem = (item: SceneItem): Channel => { + // 为每个场景生成随机的"今日"数据和"增长百分比" + const dailyCount = Math.floor(Math.random() * 100); + const growthPercent = Math.floor(Math.random() * 40) - 10; // -10% 到 30% 的随机值 + + // 默认图标(如果服务器没有返回) + const defaultIcon = "/assets/icons/poster-icon.svg"; + + return { + id: String(item.id), + name: item.name, + icon: item.image || defaultIcon, + stats: { + daily: dailyCount, + growth: growthPercent + } + }; +}; + +/** + * 获取场景详情 + * + * @param id 场景ID + * @returns 场景详情 + */ +export const fetchSceneDetail = async (id: string | number): Promise> => { + try { + return await request>(`/v1/plan/scenes/${id}`); + } catch (error) { + console.error("Error fetching scene detail:", error); + return { + code: 500, + msg: "获取场景详情失败", + data: null + }; + } +}; + diff --git a/Cunkebao/app/scenarios/page.tsx b/Cunkebao/app/scenarios/page.tsx index d00834c3..a598b978 100755 --- a/Cunkebao/app/scenarios/page.tsx +++ b/Cunkebao/app/scenarios/page.tsx @@ -1,10 +1,13 @@ "use client" +import { useState, useEffect } from "react" import type React from "react" import { TrendingUp, Users, ChevronLeft, Bot, Sparkles, Plus, Phone } from "lucide-react" import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" +import { Skeleton } from "@/components/ui/skeleton" +import { fetchScenes, transformSceneItem } from "@/api/scenarios" interface Channel { id: string @@ -26,183 +29,7 @@ interface Plan { acquisitionCount: number } -// 调整场景顺序,确保API获客在最后 -const channels: Channel[] = [ - { - id: "haibao", - name: "海报获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png", - stats: { - daily: 167, - growth: 10.2, - }, - link: "/scenarios/haibao", - plans: [ - { - id: "plan-5", - name: "产品海报获客", - isNew: true, - status: "active", - acquisitionCount: 45, - }, - ], - }, - { - id: "order", - name: "订单获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-203hwGO5hn7hTByGiJltmtACbQF4yl.png", - stats: { - daily: 112, - growth: 7.8, - }, - link: "/scenarios/order", - plans: [ - { - id: "plan-9", - name: "电商订单获客", - status: "active", - acquisitionCount: 42, - }, - ], - }, - { - id: "douyin", - name: "抖音获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png", - stats: { - daily: 156, - growth: 12.5, - }, - link: "/scenarios/douyin", - plans: [ - { - id: "plan-1", - name: "抖音直播间获客", - isNew: true, - status: "active", - acquisitionCount: 56, - }, - { - id: "plan-2", - name: "抖音评论区获客", - status: "completed", - acquisitionCount: 128, - }, - ], - }, - { - id: "xiaohongshu", - name: "小红书获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-JXQOWS9M8mxbAgvFSlA8cCl64p3OiF.png", - stats: { - daily: 89, - growth: 8.3, - }, - link: "/scenarios/xiaohongshu", - plans: [ - { - id: "plan-3", - name: "小红书笔记获客", - isNew: true, - status: "active", - acquisitionCount: 32, - }, - ], - }, - { - id: "phone", - name: "电话获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/phone-icon-Hs9Ck3Ij7aqCOoY5NkhxQnXBnT5LGU.png", - stats: { - daily: 42, - growth: 15.8, - }, - link: "/scenarios/phone", - plans: [ - { - id: "phone-1", - name: "招商电话获客", - isNew: true, - status: "active", - acquisitionCount: 28, - }, - ], - }, - { - id: "gongzhonghao", - name: "公众号获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png", - stats: { - daily: 234, - growth: 15.7, - }, - link: "/scenarios/gongzhonghao", - plans: [ - { - id: "plan-4", - name: "公众号文章获客", - status: "active", - acquisitionCount: 87, - }, - ], - }, - { - id: "weixinqun", - name: "微信群获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-azCH8EgGfidWXOqiM2D1jLH0VFRUtW.png", - stats: { - daily: 145, - growth: 11.2, - }, - link: "/scenarios/weixinqun", - plans: [ - { - id: "plan-6", - name: "微信群活动获客", - status: "paused", - acquisitionCount: 23, - }, - ], - }, - { - id: "payment", - name: "付款码获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-FI5qJhBgV87ZS3P2WrUDsVyV91Y78i.png", - stats: { - daily: 78, - growth: 9.5, - }, - link: "/scenarios/payment", - plans: [ - { - id: "plan-7", - name: "支付宝码获客", - status: "active", - acquisitionCount: 19, - }, - ], - }, - { - id: "api", - name: "API获客", - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-JKtHDY1Ula8ya0XKQDxle5qrcE0qC5.png", - stats: { - daily: 198, - growth: 14.3, - }, - link: "/scenarios/api", - plans: [ - { - id: "plan-8", - name: "网站表单获客", - isNew: true, - status: "active", - acquisitionCount: 67, - }, - ], - }, -] - +// AI场景列表(服务端暂未提供) const aiScenarios = [ { id: "ai-friend", @@ -267,6 +94,43 @@ const aiScenarios = [ export default function ScenariosPage() { const router = useRouter() + const [channels, setChannels] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const loadScenes = async () => { + try { + setLoading(true) + const response = await fetchScenes({ limit: 50 }) + + if (response.code === 200 && response.data?.list) { + // 转换场景数据为前端展示格式 + const transformedScenes = response.data.list.map((scene) => { + const transformedScene = transformSceneItem(scene) + + // 添加link属性(用于导航) + return { + ...transformedScene, + link: `/scenarios/${scene.id}` + } + }) + + setChannels(transformedScenes) + } else { + setError(response.msg || "获取场景列表失败") + } + } catch (err) { + console.error("Failed to fetch scenes:", err) + setError("获取场景列表失败") + } finally { + setLoading(false) + } + } + + loadScenes() + }, []) + const handleChannelClick = (channelId: string, event: React.MouseEvent) => { router.push(`/scenarios/${channelId}`) } @@ -296,6 +160,27 @@ export default function ScenariosPage() { return "未知状态" } } + + // 展示场景骨架屏 + const renderSkeletons = () => { + return Array(8) + .fill(0) + .map((_, index) => ( +
+ +
+ + +
+ + +
+ +
+
+
+ )) + } return (
@@ -317,45 +202,64 @@ export default function ScenariosPage() {
+ {/* 错误提示 */} + {error && ( +
+

{error}

+ +
+ )} + {/* Traditional channels */}
- {channels.map((channel) => ( -
- router.push(channel.link || "")} - > -
-
- {channel.id === "phone" ? ( - - ) : ( + {loading ? ( + renderSkeletons() + ) : ( + channels.map((channel) => ( +
+ router.push(channel.link || "")} + > +
+
{channel.name} { + // 图片加载失败时,使用默认图标 + const target = e.target as HTMLImageElement + target.src = "/assets/icons/poster-icon.svg" + }} /> - )} -
+
-

{channel.name}

+

{channel.name}

-
- -
- 今日: - {channel.stats.daily} +
+ +
+ 今日: + {channel.stats.daily} +
+
+ +
+ + {channel.stats.growth > 0 ? "+" : ""}{channel.stats.growth}%
- -
- - +{channel.stats.growth}% -
-
-
-
- ))} + +
+ )) + )}
{/* AI scenarios */} diff --git a/Server/application/plan/controller/Scene.php b/Server/application/plan/controller/Scene.php index 8ec9141e..fe227499 100644 --- a/Server/application/plan/controller/Scene.php +++ b/Server/application/plan/controller/Scene.php @@ -31,7 +31,7 @@ class Scene extends Controller $where[] = ['status', '=', 1]; // 查询列表 - $result = PlanScene::getSceneList($where, 'id desc', $page, $limit); + $result = PlanScene::getSceneList($where, 'sort desc', $page, $limit); return json([ 'code' => 200,