diff --git a/Cunkebao/app/scenarios/[channel]/edit/[id]/page.tsx b/Cunkebao/app/scenarios/[channel]/edit/[id]/page.tsx index 400b44183..2feca4b88 100644 --- a/Cunkebao/app/scenarios/[channel]/edit/[id]/page.tsx +++ b/Cunkebao/app/scenarios/[channel]/edit/[id]/page.tsx @@ -4,10 +4,10 @@ import { useState, useEffect } from "react" import { ChevronLeft } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" -import { BasicSettings } from "../../../new/steps/BasicSettings" -import { FriendRequestSettings } from "../../../new/steps/FriendRequestSettings" -import { MessageSettings } from "../../../new/steps/MessageSettings" -import { TagSettings } from "../../../new/steps/TagSettings" +import { BasicSettings } from "@/plans/new/steps/BasicSettings" +import { FriendRequestSettings } from "@/plans/new/steps/FriendRequestSettings" +import { MessageSettings } from "@/plans/new/steps/MessageSettings" +import { TagSettings } from "@/plans/new/steps/TagSettings" import { useRouter } from "next/navigation" import { toast } from "@/components/ui/use-toast" diff --git a/Cunkebao/app/scenarios/new/page.tsx b/Cunkebao/app/scenarios/new/page.tsx index f849d0291..024c9c502 100644 --- a/Cunkebao/app/scenarios/new/page.tsx +++ b/Cunkebao/app/scenarios/new/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { ChevronLeft, Settings } from "lucide-react" import { Button } from "@/components/ui/button" @@ -9,6 +9,7 @@ import { StepIndicator } from "@/app/components/ui-templates/step-indicator" import { BasicSettings } from "./steps/BasicSettings" import { FriendRequestSettings } from "./steps/FriendRequestSettings" import { MessageSettings } from "./steps/MessageSettings" +import { api, ApiResponse } from "@/lib/api" // 步骤定义 - 只保留三个步骤 const steps = [ @@ -22,7 +23,7 @@ export default function NewPlan() { const [currentStep, setCurrentStep] = useState(1) const [formData, setFormData] = useState({ planName: "", - scenario: "haibao", + scenario: "", posters: [], device: "", remarkType: "phone", @@ -31,9 +32,23 @@ export default function NewPlan() { startTime: "09:00", endTime: "18:00", enabled: true, - // 移除tags字段 }) + // 场景数据 + const [scenes, setScenes] = useState([]) + const [loadingScenes, setLoadingScenes] = useState(true) + + useEffect(() => { + api.get("/v1/plan/scenes") + .then(res => { + if (res.code === 200 && Array.isArray(res.data)) { + setScenes(res.data) + setFormData(prev => ({ ...prev, scenario: prev.scenario || (res.data[0]?.id || "") })) + } + }) + .finally(() => setLoadingScenes(false)) + }, []) + // 更新表单数据 const onChange = (data: any) => { setFormData((prev) => ({ ...prev, ...data })) @@ -78,7 +93,7 @@ export default function NewPlan() { const renderStepContent = () => { switch (currentStep) { case 1: - return + return case 2: return case 3: @@ -88,6 +103,10 @@ export default function NewPlan() { } } + if (loadingScenes) { + return
加载中...
+ } + return (
diff --git a/Cunkebao/app/scenarios/new/steps/BasicSettings.tsx b/Cunkebao/app/scenarios/new/steps/BasicSettings.tsx index 98657d6ea..dd900af8f 100644 --- a/Cunkebao/app/scenarios/new/steps/BasicSettings.tsx +++ b/Cunkebao/app/scenarios/new/steps/BasicSettings.tsx @@ -20,97 +20,11 @@ import { DialogDescription, } from "@/components/ui/dialog" -// 调整场景顺序,确保API获客在最后,并且前三个是最常用的场景 -const scenarios = [ - { id: "haibao", name: "海报获客", type: "material" }, - { id: "order", name: "订单获客", type: "api" }, - { id: "douyin", name: "抖音获客", type: "social" }, - { id: "xiaohongshu", name: "小红书获客", type: "social" }, - { id: "phone", name: "电话获客", type: "social" }, - { id: "gongzhonghao", name: "公众号获客", type: "social" }, - { id: "weixinqun", name: "微信群获客", type: "social" }, - { id: "payment", name: "付款码获客", type: "material" }, - { id: "api", name: "API获客", type: "api" }, // API获客放在最后 -] - -const phoneCallTags = [ - { id: "tag-1", name: "咨询", color: "bg-blue-100 text-blue-800" }, - { id: "tag-2", name: "投诉", color: "bg-red-100 text-red-800" }, - { id: "tag-3", name: "合作", color: "bg-green-100 text-green-800" }, - { id: "tag-4", name: "价格", color: "bg-orange-100 text-orange-800" }, - { id: "tag-5", name: "售后", color: "bg-purple-100 text-purple-800" }, - { id: "tag-6", name: "订单", color: "bg-yellow-100 text-yellow-800" }, - { id: "tag-7", name: "物流", color: "bg-teal-100 text-teal-800" }, -] - -// 不同场景的预设标签 -const scenarioTags = { - haibao: [ - { id: "poster-tag-1", name: "活动推广", color: "bg-blue-100 text-blue-800" }, - { id: "poster-tag-2", name: "产品宣传", color: "bg-green-100 text-green-800" }, - { id: "poster-tag-3", name: "品牌展示", color: "bg-purple-100 text-purple-800" }, - { id: "poster-tag-4", name: "优惠促销", color: "bg-red-100 text-red-800" }, - { id: "poster-tag-5", name: "新品发布", color: "bg-orange-100 text-orange-800" }, - ], - order: [ - { id: "order-tag-1", name: "新订单", color: "bg-green-100 text-green-800" }, - { id: "order-tag-2", name: "复购客户", color: "bg-blue-100 text-blue-800" }, - { id: "order-tag-3", name: "高价值订单", color: "bg-purple-100 text-purple-800" }, - { id: "order-tag-4", name: "待付款", color: "bg-yellow-100 text-yellow-800" }, - { id: "order-tag-5", name: "已完成", color: "bg-gray-100 text-gray-800" }, - ], - douyin: [ - { id: "douyin-tag-1", name: "短视频", color: "bg-pink-100 text-pink-800" }, - { id: "douyin-tag-2", name: "直播", color: "bg-red-100 text-red-800" }, - { id: "douyin-tag-3", name: "带货", color: "bg-orange-100 text-orange-800" }, - { id: "douyin-tag-4", name: "粉丝互动", color: "bg-blue-100 text-blue-800" }, - { id: "douyin-tag-5", name: "热门话题", color: "bg-purple-100 text-purple-800" }, - ], - xiaohongshu: [ - { id: "xhs-tag-1", name: "种草笔记", color: "bg-red-100 text-red-800" }, - { id: "xhs-tag-2", name: "美妆", color: "bg-pink-100 text-pink-800" }, - { id: "xhs-tag-3", name: "穿搭", color: "bg-purple-100 text-purple-800" }, - { id: "xhs-tag-4", name: "生活方式", color: "bg-green-100 text-green-800" }, - { id: "xhs-tag-5", name: "好物推荐", color: "bg-orange-100 text-orange-800" }, - ], - phone: phoneCallTags, - gongzhonghao: [ - { id: "gzh-tag-1", name: "文章推送", color: "bg-blue-100 text-blue-800" }, - { id: "gzh-tag-2", name: "活动通知", color: "bg-green-100 text-green-800" }, - { id: "gzh-tag-3", name: "产品介绍", color: "bg-purple-100 text-purple-800" }, - { id: "gzh-tag-4", name: "用户服务", color: "bg-orange-100 text-orange-800" }, - { id: "gzh-tag-5", name: "品牌故事", color: "bg-gray-100 text-gray-800" }, - ], - weixinqun: [ - { id: "wxq-tag-1", name: "群活动", color: "bg-green-100 text-green-800" }, - { id: "wxq-tag-2", name: "产品分享", color: "bg-blue-100 text-blue-800" }, - { id: "wxq-tag-3", name: "用户交流", color: "bg-purple-100 text-purple-800" }, - { id: "wxq-tag-4", name: "优惠信息", color: "bg-pink-100 text-pink-800" }, - { id: "wxq-tag-5", name: "答疑解惑", color: "bg-orange-100 text-orange-800" }, - { id: "wxq-tag-6", name: "新人欢迎", color: "bg-yellow-100 text-yellow-800" }, - { id: "wxq-tag-7", name: "群规通知", color: "bg-gray-100 text-gray-800" }, - { id: "wxq-tag-8", name: "活跃互动", color: "bg-indigo-100 text-indigo-800" }, - ], - payment: [ - { id: "pay-tag-1", name: "扫码支付", color: "bg-green-100 text-green-800" }, - { id: "pay-tag-2", name: "线下门店", color: "bg-blue-100 text-blue-800" }, - { id: "pay-tag-3", name: "活动收款", color: "bg-purple-100 text-purple-800" }, - { id: "pay-tag-4", name: "服务费用", color: "bg-orange-100 text-orange-800" }, - { id: "pay-tag-5", name: "会员充值", color: "bg-yellow-100 text-yellow-800" }, - ], - api: [ - { id: "api-tag-1", name: "系统对接", color: "bg-blue-100 text-blue-800" }, - { id: "api-tag-2", name: "数据同步", color: "bg-green-100 text-green-800" }, - { id: "api-tag-3", name: "自动化", color: "bg-purple-100 text-purple-800" }, - { id: "api-tag-4", name: "第三方平台", color: "bg-orange-100 text-orange-800" }, - { id: "api-tag-5", name: "实时推送", color: "bg-gray-100 text-gray-800" }, - ], -} - interface BasicSettingsProps { formData: any onChange: (data: any) => void onNext?: () => void + scenarios: any[] } interface Account { @@ -182,7 +96,36 @@ const generatePosterMaterials = (): Material[] => { })) } -export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) { +// 颜色池分为更浅的未选中和深色的选中 +const tagColorPoolLight = [ + "bg-blue-50 text-blue-600", + "bg-green-50 text-green-600", + "bg-purple-50 text-purple-600", + "bg-red-50 text-red-600", + "bg-orange-50 text-orange-600", + "bg-yellow-50 text-yellow-600", + "bg-gray-50 text-gray-600", + "bg-pink-50 text-pink-600", +]; +const tagColorPoolDark = [ + "bg-blue-500 text-white", + "bg-green-500 text-white", + "bg-purple-500 text-white", + "bg-red-500 text-white", + "bg-orange-500 text-white", + "bg-yellow-400 text-white", + "bg-gray-700 text-white", + "bg-pink-500 text-white", +]; +function getTagColorIdx(tag: string) { + let hash = 0; + for (let i = 0; i < tag.length; i++) { + hash = tag.charCodeAt(i) + ((hash << 5) - hash); + } + return Math.abs(hash) % tagColorPoolLight.length; +} + +export function BasicSettings({ formData, onChange, onNext, scenarios }: BasicSettingsProps) { const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false) const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false) const [isQRCodeOpen, setIsQRCodeOpen] = useState(false) @@ -225,11 +168,11 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps const [selectedPhoneTags, setSelectedPhoneTags] = useState(formData.phoneTags || []) const [phoneCallType, setPhoneCallType] = useState(formData.phoneCallType || "both") - // 处理标签选择 - const handleTagToggle = (tagId: string) => { - const newTags = selectedPhoneTags.includes(tagId) - ? selectedPhoneTags.filter((id) => id !== tagId) - : [...selectedPhoneTags, tagId] + // 处理标签选择 (现在处理的是字符串标签) + const handleTagToggle = (tag: string) => { + const newTags = selectedPhoneTags.includes(tag) + ? selectedPhoneTags.filter((t) => t !== tag) + : [...selectedPhoneTags, tag] setSelectedPhoneTags(newTags) onChange({ ...formData, phoneTags: newTags }) @@ -267,11 +210,11 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps } } - // 处理场景标签选择 - const handleScenarioTagToggle = (tagId: string) => { - const newTags = selectedScenarioTags.includes(tagId) - ? selectedScenarioTags.filter((id) => id !== tagId) - : [...selectedScenarioTags, tagId] + // 处理场景标签选择 (现在处理的是字符串标签) + const handleScenarioTagToggle = (tag: string) => { + const newTags = selectedScenarioTags.includes(tag) + ? selectedScenarioTags.filter((t) => t !== tag) + : [...selectedScenarioTags, tag] setSelectedScenarioTags(newTags) onChange({ ...formData, scenarioTags: newTags }) @@ -448,19 +391,23 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps {/* 预设标签 */}
- {(scenarioTags[formData.scenario as keyof typeof scenarioTags] || []).map((tag) => ( -
handleScenarioTagToggle(tag.id)} - > - {tag.name} -
- ))} + {(scenarios.find((s) => s.id === formData.scenario)?.scenarioTags || []).map((tag: string) => { + const idx = getTagColorIdx(tag); + const selected = selectedScenarioTags.includes(tag); + return ( +
handleScenarioTagToggle(tag)} + > + {tag} +
+ ); + })}
{/* 自定义标签 */} @@ -663,29 +610,33 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
- {/* 添加标签功能 */} + {/* 添加标签功能 - 使用从 scenarios 中获取的标签数据 */}
- {phoneCallTags.map((tag) => ( -
handleTagToggle(tag.id)} - > - {tag.name} -
- ))} + {(scenarios.find((s: any) => s.id === formData.scenario)?.scenarioTags || []).map((tag: string) => { + const idx = getTagColorIdx(tag); + const selected = selectedPhoneTags.includes(tag); + return ( +
handleTagToggle(tag)} + > + {tag} +
+ ); + })}
)} - {scenarios.find((s) => s.id === formData.scenario)?.type === "material" && ( + {scenarios.find((s: any) => s.id === formData.scenario)?.type === "material" && (
@@ -757,7 +708,7 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
)} - {scenarios.find((s) => s.id === formData.scenario)?.id === "order" && ( + {scenarios.find((s: any) => s.id === formData.scenario)?.id === "order" && (
diff --git a/Cunkebao/app/scenarios/page.tsx b/Cunkebao/app/scenarios/page.tsx index 156d1c7d7..8301c89de 100644 --- a/Cunkebao/app/scenarios/page.tsx +++ b/Cunkebao/app/scenarios/page.tsx @@ -4,87 +4,13 @@ import { Plus, TrendingUp } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" +import { useEffect, useState } from "react" +import { api, ApiResponse } from "@/lib/api" export default function ScenariosPage() { const router = useRouter() - - // 场景数据 - const scenarios = [ - { - id: "poster", - name: "海报获客", - icon: "🖼️", - count: 167, - growth: "+10.2%", - path: "/scenarios/poster", - }, - { - id: "order", - name: "订单获客", - icon: "📋", - count: 112, - growth: "+7.8%", - path: "/scenarios/order", - }, - { - id: "douyin", - name: "抖音获客", - icon: "📱", - count: 156, - growth: "+12.5%", - path: "/scenarios/douyin", - }, - { - id: "xiaohongshu", - name: "小红书获客", - icon: "📕", - count: 89, - growth: "+8.3%", - path: "/scenarios/xiaohongshu", - }, - { - id: "phone", - name: "电话获客", - icon: "📞", - count: 42, - growth: "+15.8%", - path: "/scenarios/phone", - }, - { - id: "gongzhonghao", - name: "公众号获客", - icon: "📢", - count: 234, - growth: "+15.7%", - path: "/scenarios/gongzhonghao", - }, - { - id: "weixinqun", - name: "微信群获客", - icon: "👥", - count: 145, - growth: "+11.2%", - path: "/scenarios/weixinqun", - }, - { - id: "payment", - name: "付款码获客", - icon: "💳", - count: 78, - growth: "+9.5%", - path: "/scenarios/payment", - }, - { - id: "api", - name: "API获客", - icon: "🔌", - count: 198, - growth: "+14.3%", - path: "/scenarios/api", - }, - ] - - // AI智能获客 + const [scenarios, setScenarios] = useState([]) + // AI智能获客用本地 mock 数据 const aiScenarios = [ { id: "ai-friend", @@ -114,13 +40,35 @@ export default function ScenariosPage() { path: "/scenarios/ai-conversion", }, ] + const [loading, setLoading] = useState(true) + const [error, setError] = useState("") + + useEffect(() => { + setLoading(true) + api.get("/v1/plan/scenes") + .then((res) => { + if (res.code === 200 && Array.isArray(res.data)) { + setScenarios(res.data) + } else { + setError(res.msg || "接口返回异常") + } + }) + .catch((err) => setError(err?.message || "接口请求失败")) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return
加载中...
+ } + if (error) { + return
{error}
+ } return (

场景获客

- {/*
) diff --git a/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx b/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx index 910e01464..2d25c7d09 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx @@ -158,14 +158,14 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de onClick={() => togglePool(pool.label)} >
-
- -
-
+
+ +
+

{pool.label}

{poolDescMap[pool.label] || ""}

-
-
+
+
{pool.count} 人 e.stopPropagation()} - /> + />
)) )} @@ -199,7 +199,7 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de 确认
- +
diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 8958c6162..18c433102 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -37,6 +37,7 @@ Route::group('v1/', function () { // 获客场景相关 Route::group('plan', function () { Route::get('scenes', 'app\cunkebao\controller\plan\GetPlanSceneListV1Controller@index'); + Route::get('scenes-detail', 'app\cunkebao\controller\plan\GetPlanSceneListV1Controller@detail'); Route::post('create', 'app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller@index'); Route::get('list', 'app\cunkebao\controller\Plan@getList'); diff --git a/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php b/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php index 566f43732..64df2445c 100644 --- a/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php +++ b/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php @@ -5,6 +5,7 @@ namespace app\cunkebao\controller\plan; use app\common\model\PlanScene as PlansSceneModel; use app\cunkebao\controller\BaseController; use library\ResponseHelper; +use think\Db; /** * 获客场景控制器 @@ -18,13 +19,15 @@ class GetPlanSceneListV1Controller extends BaseController */ protected function getSceneList(): array { - return PlansSceneModel::where( - [ - 'status' => PlansSceneModel::STATUS_ACTIVE - ] - ) - ->order('sort desc') - ->select()->toArray(); + $list = PlansSceneModel::where(['status' => PlansSceneModel::STATUS_ACTIVE])->order('sort desc')->select()->toArray(); + $userInfo = $this->getUserInfo(); + foreach($list as &$val){ + $val['scenarioTags'] = json_decode($val['scenarioTags'],true); + $val['count'] = 0; + $val['growth'] = "0%"; + } + unset($val); + return $list; } /** @@ -38,4 +41,27 @@ class GetPlanSceneListV1Controller extends BaseController $this->getSceneList() ); } + + /** + * 获取场景详情 + * + */ + public function detail() + { + $id = $this->request->param('id',''); + if(empty($id)){ + ResponseHelper::error('参数缺失'); + } + + $data = PlansSceneModel::where(['status' => PlansSceneModel::STATUS_ACTIVE,'id' => $id])->find(); + if(empty($data)){ + ResponseHelper::error('场景不存在'); + } + + $data['scenarioTags'] = json_decode($data['scenarioTags'],true); + return ResponseHelper::success($data); + } + + + } \ No newline at end of file