From a3bc324943662d5703c3ada7eeedd477c9faf8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Wed, 23 Jul 2025 11:33:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=AE=A1=E5=88=92=E6=9E=84=E5=BB=BA90%?= =?UTF-8?q?=E5=BD=93=E5=8A=A1=E4=B9=8B=E6=80=A5=EF=BC=8C=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E5=B0=81=E8=A3=85=E4=B8=80=E4=B8=8B=E6=AD=A5=E9=AA=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- nkebao/src/api/common.ts | 33 + .../src/pages/scenarios/plan/new/index.api.ts | 53 + .../scenarios/plan/new/index.module.scss | 0 nkebao/src/pages/scenarios/plan/new/index.tsx | 220 ++-- .../src/pages/scenarios/plan/new/page.api.ts | 20 - .../pages/scenarios/plan/new/page.module.scss | 39 - .../plan/new/steps/BasicSettings.module.scss | 63 - .../plan/new/steps/BasicSettings.tsx | 1067 +++++++++++++---- .../steps/FriendRequestSettings.module.scss | 56 - .../plan/new/steps/FriendRequestSettings.tsx | 372 ++++-- .../new/steps/MessageSettings.module.scss | 64 - .../plan/new/steps/MessageSettings.tsx | 743 +++++++++--- 13 files changed, 1891 insertions(+), 841 deletions(-) create mode 100644 nkebao/src/api/common.ts create mode 100644 nkebao/src/pages/scenarios/plan/new/index.api.ts create mode 100644 nkebao/src/pages/scenarios/plan/new/index.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/page.api.ts delete mode 100644 nkebao/src/pages/scenarios/plan/new/page.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/MessageSettings.module.scss diff --git a/nkebao/.env.development b/nkebao/.env.development index f37023e8..fde60295 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,5 +1,5 @@ # 基础环境变量示例 VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/api/common.ts b/nkebao/src/api/common.ts new file mode 100644 index 00000000..9ab445cc --- /dev/null +++ b/nkebao/src/api/common.ts @@ -0,0 +1,33 @@ +import request from "./request"; +/** + * 通用文件上传方法(支持图片、文件) + * @param {File} file - 要上传的文件对象 + * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 + * @returns {Promise} - 上传成功后返回文件url + */ +export async function uploadFile( + file: File, + uploadUrl: string = "/v1/attachment/upload" +): Promise { + try { + // 创建 FormData 对象用于文件上传 + const formData = new FormData(); + formData.append("file", file); + + // 使用 request 方法上传文件,设置正确的 Content-Type + const res = await request(uploadUrl, formData, "POST", { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + // 检查响应结果 + if (res?.code === 200 && res?.data?.url) { + return res.data.url; + } else { + throw new Error(res?.msg || "文件上传失败"); + } + } catch (e: any) { + throw new Error(e?.message || "文件上传失败"); + } +} diff --git a/nkebao/src/pages/scenarios/plan/new/index.api.ts b/nkebao/src/pages/scenarios/plan/new/index.api.ts new file mode 100644 index 00000000..a4e06ebc --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/new/index.api.ts @@ -0,0 +1,53 @@ +import request from "@/api/request"; +// 获取场景类型列表 +export function getScenarioTypes() { + return request("/v1/scenarios/types", undefined, "GET"); +} + +// 创建计划 +export function createPlan(data: any) { + return request("/v1/scenarios/plans", data, "POST"); +} + +// 更新计划 +export function updatePlan(planId: string, data: any) { + return request(`/v1/scenarios/plans/${planId}`, data, "PUT"); +} + +// 获取计划详情 +export function getPlanDetail(planId: string) { + return request(`/v1/scenarios/plans/${planId}`, undefined, "GET"); +} + +// PlanDetail 类型定义(可根据实际接口返回结构补充字段) +export interface PlanDetail { + name: string; + scenario: number; + posters: any[]; + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; + // 其它字段可扩展 + [key: string]: any; +} + +// 兼容旧代码的接口命名 +export function getPlanScenes() { + return getScenarioTypes(); +} +export function createScenarioPlan(data: any) { + return createPlan(data); +} +export function fetchPlanDetail(planId: string) { + return getPlanDetail(planId); +} +export function updateScenarioPlan(planId: string, data: any) { + return updatePlan(planId, data); +} diff --git a/nkebao/src/pages/scenarios/plan/new/index.module.scss b/nkebao/src/pages/scenarios/plan/new/index.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/nkebao/src/pages/scenarios/plan/new/index.tsx b/nkebao/src/pages/scenarios/plan/new/index.tsx index 5d166527..e8739760 100644 --- a/nkebao/src/pages/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/scenarios/plan/new/index.tsx @@ -1,21 +1,20 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile"; -import { ArrowLeftOutlined } from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; - +import { LeftOutlined } from "@ant-design/icons"; +import { Button, Steps, message } from "antd"; import BasicSettings from "./steps/BasicSettings"; import FriendRequestSettings from "./steps/FriendRequestSettings"; import MessageSettings from "./steps/MessageSettings"; +import Layout from "@/components/Layout/Layout"; import { - getScenarioTypes, - createPlan, - updatePlan, - getPlanDetail, -} from "./page.api"; -import style from "./page.module.scss"; + getPlanScenes, + createScenarioPlan, + fetchPlanDetail, + PlanDetail, + updateScenarioPlan, +} from "./index.api"; -// 步骤定义 +// 步骤定义 - 只保留三个步骤 const steps = [ { id: 1, title: "步骤一", subtitle: "基础设置" }, { id: 2, title: "步骤二", subtitle: "好友申请设置" }, @@ -26,7 +25,7 @@ const steps = [ interface FormData { name: string; scenario: number; - posters: any[]; + posters: any[]; // 后续可替换为具体Poster类型 device: string[]; remarkType: string; greeting: string; @@ -39,8 +38,8 @@ interface FormData { addFriendInterval: number; } -const NewPlan: React.FC = () => { - const navigate = useNavigate(); +export default function NewPlan() { + const router = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [formData, setFormData] = useState({ name: "", @@ -64,57 +63,53 @@ const NewPlan: React.FC = () => { planId: string; }>(); const [isEdit, setIsEdit] = useState(false); - const [saving, setSaving] = useState(false); - useEffect(() => { loadData(); }, []); const loadData = async () => { setSceneLoading(true); - try { - // 获取场景类型 - const res = await getScenarioTypes(); - if (res?.data) { - setSceneList(res.data); - } - - if (planId) { - setIsEdit(true); - // 获取计划详情 - const detailRes = await getPlanDetail(planId); - if (detailRes.code === 200 && detailRes.data) { - const detail = detailRes.data; - setFormData((prev) => ({ - ...prev, - name: detail.name ?? "", - scenario: Number(detail.scenario) || 1, - posters: detail.posters ?? [], - device: detail.device ?? [], - remarkType: detail.remarkType ?? "phone", - greeting: detail.greeting ?? "", - addInterval: detail.addInterval ?? 1, - startTime: detail.startTime ?? "09:00", - endTime: detail.endTime ?? "18:00", - enabled: detail.enabled ?? true, - sceneId: Number(detail.scenario) || 1, - remarkFormat: detail.remarkFormat ?? "", - addFriendInterval: detail.addFriendInterval ?? 1, - })); - } - } else if (scenarioId) { + //获取场景类型 + getPlanScenes() + .then((data) => { + setSceneList(data || []); + }) + .catch((err) => { + message.error(err.message || "获取场景类型失败"); + }) + .finally(() => setSceneLoading(false)); + if (planId) { + setIsEdit(true); + //获取计划详情 + try { + const detail = await fetchPlanDetail(planId); setFormData((prev) => ({ ...prev, - scenario: Number(scenarioId) || 1, + name: detail.name ?? "", + scenario: Number(detail.scenario) || 1, + posters: detail.posters ?? [], + device: detail.device ?? [], + remarkType: detail.remarkType ?? "phone", + greeting: detail.greeting ?? "", + addInterval: detail.addInterval ?? 1, + startTime: detail.startTime ?? "09:00", + endTime: detail.endTime ?? "18:00", + enabled: detail.enabled ?? true, + sceneId: Number(detail.scenario) || 1, + remarkFormat: detail.remarkFormat ?? "", + addFriendInterval: detail.addFriendInterval ?? 1, + tips: detail.tips ?? "", + })); + } catch (err) { + message.error(err.message || "获取计划详情失败"); + } + } else { + if (scenarioId) { + setFormData((prev) => ({ + ...prev, + ...{ scenario: Number(scenarioId) || 1 }, })); } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setSceneLoading(false); } }; @@ -125,52 +120,35 @@ const NewPlan: React.FC = () => { // 处理保存 const handleSave = async () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - setSaving(true); try { let result; if (isEdit && planId) { - // 编辑 + // 编辑:拼接后端需要的完整参数 const editData = { ...formData, id: Number(planId), planId: Number(planId), + // 兼容后端需要的字段 + // 你可以根据实际需要补充其它字段 }; - result = await updatePlan(planId, editData); + result = await updateScenarioPlan(planId, editData); } else { // 新建 - result = await createPlan(formData); - } - - if (result.code === 200) { - Toast.show({ - content: isEdit ? "计划已更新" : "获客计划已创建", - position: "top", - }); - const sceneItem = sceneList.find((v) => formData.scenario === v.id); - navigate( - `/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}` - ); - } else { - Toast.show({ - content: result.msg || "操作失败", - position: "top", - }); + result = await createScenarioPlan(formData); } + message.success(isEdit ? "计划已更新" : "获客计划已创建"); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + router(`/scenarios/list/${formData.sceneId}/${sceneItem.name}`); } catch (error) { - Toast.show({ - content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试", - position: "top", - }); - } finally { - setSaving(false); + message.error( + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : isEdit + ? "更新计划失败,请重试" + : "创建计划失败,请重试" + ); } }; @@ -194,6 +172,7 @@ const NewPlan: React.FC = () => { case 1: return ( { ); default: @@ -225,50 +203,25 @@ const NewPlan: React.FC = () => { } }; - if (sceneLoading) { - return ( - -
- {isEdit ? "编辑计划" : "新建计划"} -
- - } - > -
- -
加载数据中...
-
-
- ); - } - return ( - - navigate(-1)} +
+
+
+
- } - > - - {isEdit ? "编辑计划" : "新建计划"} - - - - {/* 步骤指示器 */} -
+
+
+
- {steps.map((step) => ( + {steps.map((step, idx) => ( { } > -
- {/* 步骤内容 */} -
{renderStepContent()}
-
+
{renderStepContent()}
); -}; - -export default NewPlan; +} diff --git a/nkebao/src/pages/scenarios/plan/new/page.api.ts b/nkebao/src/pages/scenarios/plan/new/page.api.ts deleted file mode 100644 index 9d1411bf..00000000 --- a/nkebao/src/pages/scenarios/plan/new/page.api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import request from "@/api/request"; -// 获取场景类型列表 -export function getScenarioTypes() { - return request("/api/scenarios/types", undefined, "GET"); -} - -// 创建计划 -export function createPlan(data: any) { - return request("/api/scenarios/plans", data, "POST"); -} - -// 更新计划 -export function updatePlan(planId: string, data: any) { - return request(`/api/scenarios/plans/${planId}`, data, "PUT"); -} - -// 获取计划详情 -export function getPlanDetail(planId: string) { - return request(`/api/scenarios/plans/${planId}`, undefined, "GET"); -} diff --git a/nkebao/src/pages/scenarios/plan/new/page.module.scss b/nkebao/src/pages/scenarios/plan/new/page.module.scss deleted file mode 100644 index 0d6784fd..00000000 --- a/nkebao/src/pages/scenarios/plan/new/page.module.scss +++ /dev/null @@ -1,39 +0,0 @@ -.new-plan-page { -} - -.nav-title { - font-size: 18px; - font-weight: 600; - color: #333; -} - -.back-btn { - height: 32px; - width: 32px; - padding: 0; - border-radius: 50%; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 60vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.steps-container { - background: #ffffff; - margin-bottom: 12px; -} - -.step-content { - flex: 1; - padding: 0 16px; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss deleted file mode 100644 index 5720527d..00000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.basic-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.form-item { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - .adm-form-item-label { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 8px; - } - - .adm-input { - border-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } -} - -.time-input { - width: 120px; - border-radius: 8px; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 40vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.actions { - padding: 20px 0; -} - -.next-btn { - width: 100%; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx index ef9bc167..1f989532 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,209 +1,858 @@ -import React, { useState, useEffect } from "react"; -import { - Form, - Input, - Selector, - Button, - SpinLoading, - Toast, - Card, - Space, -} from "antd-mobile"; -// import { getDevices, getPosters } from "./step.api"; -import style from "./BasicSettings.module.scss"; - -interface BasicSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - sceneList: any[]; - sceneLoading: boolean; -} - -const BasicSettings: React.FC = ({ - formData, - onChange, - onNext, - sceneList, - sceneLoading, -}) => { - const [devices, setDevices] = useState([]); - const [posters, setPosters] = useState([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - loadData(); - }, []); - - const loadData = async () => { - setLoading(true); - try { - // // 获取设备列表 - // const devicesRes = await getDevices(); - // if (devicesRes?.data) { - // setDevices(devicesRes.data); - // } - // // 获取海报列表 - // const postersRes = await getPosters(); - // if (postersRes?.data) { - // setPosters(postersRes.data); - // } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setLoading(false); - } - }; - - const handleNext = () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - if (!formData.scenario) { - Toast.show({ - content: "请选择场景类型", - position: "top", - }); - return; - } - - if (formData.device.length === 0) { - Toast.show({ - content: "请选择设备", - position: "top", - }); - return; - } - - onNext(); - }; - - if (loading || sceneLoading) { - return ( -
- -
加载数据中...
-
- ); - } - - return ( -
- -
- {/* 计划名称 */} - - onChange({ name: value })} - clearable - /> - - - {/* 场景类型 */} - - ({ - label: scene.name, - value: scene.id, - }))} - value={[formData.scenario]} - onChange={(value) => { - const selectedScene = sceneList.find( - (scene) => scene.id === value[0] - ); - onChange({ - scenario: value[0], - sceneId: value[0], - name: selectedScene?.name || "", - }); - }} - /> - - - {/* 选择设备 */} - - ({ - label: device.name, - value: device.id, - }))} - value={formData.device} - onChange={(value) => onChange({ device: value })} - multiple - /> - - - {/* 选择海报 */} - - ({ - label: poster.name, - value: poster.id, - }))} - value={formData.posters} - onChange={(value) => onChange({ posters: value })} - multiple - /> - - - {/* 工作时间 */} - - - onChange({ startTime: value })} - className={style["time-input"]} - /> - - onChange({ endTime: value })} - className={style["time-input"]} - /> - - - - {/* 添加间隔 */} - - - onChange({ addInterval: Number(value) || 1 }) - } - min={1} - max={60} - /> - -
-
- - {/* 操作按钮 */} -
- -
-
- ); -}; - -export default BasicSettings; +import React, { useState, useEffect, useRef } from "react"; +import { + Form, + Input, + Button, + Tag, + Switch, + // Upload, + Modal, + Alert, + Row, + Col, + message, +} from "antd"; +import { + PlusOutlined, + EyeOutlined, + CloseOutlined, + DownloadOutlined, + UploadOutlined, + CheckOutlined, +} from "@ant-design/icons"; +import { uploadFile } from "@/api/common"; + +interface BasicSettingsProps { + isEdit: boolean; + formData: any; + onChange: (data: any) => void; + onNext?: () => void; + sceneList: any[]; + sceneLoading: boolean; +} + +interface Account { + id: string; + nickname: string; + avatar: string; +} + +interface Material { + id: string; + name: string; + type: string; + preview: string; +} + +const posterTemplates = [ + { + id: "poster-1", + name: "点击领取", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", + }, + { + id: "poster-2", + name: "点击合作", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", + }, + { + id: "poster-3", + name: "点击咨询", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", + }, + { + id: "poster-4", + name: "点击签到", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", + }, + { + id: "poster-5", + name: "点击了解", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", + }, + { + id: "poster-6", + name: "点击报名", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", + }, +]; + +const generateRandomAccounts = (count: number): Account[] => { + return Array.from({ length: count }, (_, index) => ({ + id: `account-${index + 1}`, + nickname: `账号-${Math.random().toString(36).substring(2, 7)}`, + avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`, + })); +}; + +const generatePosterMaterials = (): Material[] => { + return posterTemplates.map((template) => ({ + id: template.id, + name: template.name, + type: "poster", + preview: template.preview, + })); +}; + +const BasicSettings: React.FC = ({ + isEdit, + formData, + onChange, + onNext, + sceneList, + sceneLoading, +}) => { + const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); + const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false); + const [accounts] = useState(generateRandomAccounts(50)); + const [materials] = useState(generatePosterMaterials()); + const [selectedAccounts, setSelectedAccounts] = useState( + formData.accounts?.length > 0 ? formData.accounts : [] + ); + const [selectedMaterials, setSelectedMaterials] = useState( + formData.materials?.length > 0 ? formData.materials : [] + ); + // showAllScenarios 默认为 true + const [showAllScenarios, setShowAllScenarios] = useState(true); + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + const [importedTags, setImportedTags] = useState< + Array<{ + phone: string; + wechat: string; + source?: string; + orderAmount?: number; + orderDate?: string; + }> + >(formData.importedTags || []); + + // 自定义标签相关状态 + const [customTagInput, setCustomTagInput] = useState(""); + const [customTags, setCustomTags] = useState(formData.customTags || []); + const [tips, setTips] = useState(formData.tips || ""); + const [selectedScenarioTags, setSelectedScenarioTags] = useState( + formData.scenarioTags || [] + ); + + // 电话获客相关状态 + const [phoneSettings, setPhoneSettings] = useState({ + autoAdd: formData.phoneSettings?.autoAdd ?? true, + speechToText: formData.phoneSettings?.speechToText ?? true, + questionExtraction: formData.phoneSettings?.questionExtraction ?? true, + }); + + // 群设置相关状态 + const [weixinqunName, setWeixinqunName] = useState( + formData.weixinqunName || "" + ); + const [weixinqunNotice, setWeixinqunNotice] = useState( + formData.weixinqunNotice || "" + ); + + // 新增:自定义海报相关状态 + const [customPosters, setCustomPosters] = useState([]); + const [previewUrl, setPreviewUrl] = useState(null); + + // 新增:用于文件选择的ref + const uploadInputRef = useRef(null); + const uploadOrderInputRef = useRef(null); + + // 更新电话获客设置 + const handlePhoneSettingsUpdate = () => { + onChange({ ...formData, phoneSettings }); + setIsPhoneSettingsOpen(false); + }; + + // 处理标签选择 + const handleTagToggle = (tagId: string) => { + const newTags = selectedScenarioTags.includes(tagId) + ? selectedScenarioTags.filter((id: string) => id !== tagId) + : [...selectedScenarioTags, tagId]; + + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 处理通话类型选择 + const handleCallTypeChange = (type: string) => { + // setPhoneCallType(type) // This line was removed as per the edit hint. + onChange({ ...formData, phoneCallType: type }); + }; + + // 初始化时,如果没有选择场景,默认选择海报获客 + useEffect(() => { + if (!formData.scenario) { + onChange({ ...formData, scenario: "haibao" }); + } + + // 检查是否已经有上传的订单文件 + if (formData.orderFileUploaded) { + setOrderUploaded(true); + } + }, [formData, onChange]); + + useEffect(() => { + const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); + }, [isEdit]); + + useEffect(() => { + setTips(formData.tips || ""); + }, [formData.tips]); + + // 选中场景 + const handleScenarioSelect = (sceneId: number) => { + onChange({ ...formData, scenario: sceneId }); + }; + + // 选中/取消标签 + const handleScenarioTagToggle = (tag: string) => { + const newTags = selectedScenarioTags.includes(tag) + ? selectedScenarioTags.filter((t: string) => t !== tag) + : [...selectedScenarioTags, tag]; + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 添加自定义标签 + const handleAddCustomTag = () => { + if (!customTagInput.trim()) return; + const newTag = { + id: `custom-${Date.now()}`, + name: customTagInput.trim(), + }; + const updatedCustomTags = [...customTags, newTag]; + setCustomTags(updatedCustomTags); + setCustomTagInput(""); + onChange({ ...formData, customTags: updatedCustomTags }); + }; + + // 删除自定义标签 + const handleRemoveCustomTag = (tagId: string) => { + const updatedCustomTags = customTags.filter((tag: any) => tag.id !== tagId); + setCustomTags(updatedCustomTags); + onChange({ ...formData, customTags: updatedCustomTags }); + // 同时从选中标签中移除 + const updatedSelectedTags = selectedScenarioTags.filter( + (t: string) => t !== tagId + ); + setSelectedScenarioTags(updatedSelectedTags); + onChange({ + ...formData, + scenarioTags: updatedSelectedTags, + customTags: updatedCustomTags, + }); + }; + + // 新增:自定义上传图片 + const handleCustomPosterUpload = (urls: string[]) => { + if (urls && urls.length > 0) { + const newPoster: Material = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: urls[0], + }; + setCustomPosters((prev) => [...prev, newPoster]); + } + }; + + // 新增:删除自定义海报 + const handleRemoveCustomPoster = (id: string) => { + setCustomPosters((prev) => prev.filter((p) => p.id !== id)); + // 如果选中则取消选中 + if (selectedMaterials.some((m) => m.id === id)) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } + }; + + // 修改:选中/取消选中海报 + const handleMaterialSelect = (material: Material) => { + const isSelected = selectedMaterials.some((m) => m.id === material.id); + if (isSelected) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } else { + setSelectedMaterials([material]); + onChange({ ...formData, materials: [material] }); + } + }; + + // 移除已选素材 + const handleRemoveMaterial = (id: string) => { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + }; + + // 新增:全屏预览 + const handlePreviewImage = (url: string) => { + setPreviewUrl(url); + setIsPreviewOpen(true); + }; + + // 账号多选切换 + const handleAccountToggle = (account: Account) => { + const isSelected = selectedAccounts.some( + (a: Account) => a.id === account.id + ); + let newSelected; + if (isSelected) { + newSelected = selectedAccounts.filter( + (a: Account) => a.id !== account.id + ); + } else { + newSelected = [...selectedAccounts, account]; + } + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 移除已选账号 + const handleRemoveAccount = (id: string) => { + const newSelected = selectedAccounts.filter((a: Account) => a.id !== id); + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 处理文件导入 + const handleFileImport = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const rows = content.split("\n").filter((row) => row.trim()); + const tags = rows.slice(1).map((row) => { + const [phone, wechat, source, orderAmount, orderDate] = + row.split(","); + return { + phone: phone?.trim(), + wechat: wechat?.trim(), + source: source?.trim(), + orderAmount: orderAmount ? Number(orderAmount) : undefined, + orderDate: orderDate?.trim(), + }; + }); + setImportedTags(tags); + onChange({ ...formData, importedTags: tags }); + } catch (error) { + // 可用 toast 提示 + } + }; + reader.readAsText(file); + } + }; + + // 下载模板 + const handleDownloadTemplate = () => { + const template = + "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"; + const blob = new Blob([template], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "订单导入模板.csv"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + // 修改订单表格上传逻辑,使用 uploadFile 公共方法 + const [orderUploaded, setOrderUploaded] = useState(false); + + const handleOrderFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (file) { + try { + await uploadFile(file); // 默认接口即可 + setOrderUploaded(true); + onChange({ ...formData, orderFileUploaded: true }); + // 可用 toast 或其它方式提示成功 + // alert('上传成功'); + } catch (err) { + // 可用 toast 或其它方式提示失败 + // alert('上传失败'); + } + event.target.value = ""; + } + }; + + // 账号弹窗关闭时清理搜索等状态 + const handleAccountDialogClose = () => { + setIsAccountDialogOpen(false); + // 可在此清理账号搜索等临时状态 + }; + // 素材弹窗关闭时清理搜索等状态 + const handleMaterialDialogClose = () => { + setIsMaterialDialogOpen(false); + // 可在此清理素材搜索等临时状态 + }; + // 订单导入弹窗关闭时清理文件输入等状态 + const handleImportDialogClose = () => { + setIsImportDialogOpen(false); + // 可在此清理文件输入等临时状态 + }; + // 电话获客弹窗关闭 + const handlePhoneSettingsDialogClose = () => { + setIsPhoneSettingsOpen(false); + }; + // 图片预览关闭 + const handleImagePreviewClose = () => { + setIsPreviewOpen(false); + }; + + // 当前选中的场景对象 + const currentScene = sceneList.find((s) => s.id === formData.scenario); + //打开订单 + const openOrder = + formData.scenario !== 2 ? { display: "none" } : { display: "block" }; + + const openPoster = + formData.scenario !== 1 ? { display: "none" } : { display: "block" }; + + return ( +
+ {/* 场景选择区块 */} + {sceneLoading ? ( +
加载中...
+ ) : ( + + {sceneList.map((scene) => ( + + + + ))} + + )} + + {/* 计划名称输入区 */} +
计划名称
+
+ + onChange({ ...formData, name: String(e.target.value) }) + } + placeholder="请输入计划名称" + /> +
+ +
获客标签(可多选)
+ {/* 标签选择区块 */} + {formData.scenario && ( +
+ {(currentScene?.scenarioTags || []).map((tag: string) => ( + handleScenarioTagToggle(tag)} + style={{ marginBottom: 4 }} + > + {tag} + + ))} + {/* 自定义标签 */} + {customTags.map((tag: any) => ( + handleScenarioTagToggle(tag.id)} + style={{ marginBottom: 4 }} + closable + onClose={() => handleRemoveCustomTag(tag.id)} + > + {tag.name} + + ))} +
+ )} + {/* 自定义标签输入区 */} +
+
+ setCustomTagInput(e.target.value)} + placeholder="添加自定义标签" + className="w-full" + /> +
+
+ +
+
+ + {/* 输入获客成功提示 */} +
+
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + className="w-full" + /> +
+
+ + {/* 选素材 */} +
+
选择海报
+
+ {[...materials, ...customPosters].map((material) => { + const isSelected = selectedMaterials.some( + (m) => m.id === material.id + ); + const isCustom = material.id.startsWith("custom-"); + return ( +
handleMaterialSelect(material)} + > + {/* 预览按钮:自定义海报在左上,内置海报在右上 */} + + {/* 删除自定义海报按钮 */} + {isCustom && ( + + )} + {material.name} +
+ {material.name} +
+
+ ); + })} + {/* 添加海报卡片 */} +
uploadInputRef.current?.click()} + > + + + + 添加海报 + { + const file = e.target.files?.[0]; + if (file) { + // 直接上传 + try { + const url = await uploadFile(file); + const newPoster = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: url, + }; + setCustomPosters((prev) => [...prev, newPoster]); + } catch (err) { + // 可加toast提示 + } + e.target.value = ""; + } + }} + /> +
+
+ {/* 全屏图片预览 */} + { + setIsPreviewOpen(false); + setPreviewUrl(null); + }} + footer={null} + width={800} + > + {previewUrl && ( + Preview + )} + +
+ {/* 订单导入区块优化 */} +
+
订单表格上传
+
+ + +
+
+ 支持 CSV、Excel 格式,上传后将文件保存到服务器 +
+
+ {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} + {formData.scenario === 5 && ( +
+
+
+ 电话获客设置 +
+
+
+ 自动加好友 + + setPhoneSettings((s) => ({ ...s, autoAdd: v })) + } + /> +
+
+ 语音转文字 + + setPhoneSettings((s) => ({ ...s, speechToText: v })) + } + /> +
+
+ 问题提取 + + setPhoneSettings((s) => ({ ...s, questionExtraction: v })) + } + /> +
+
+
+
+ )} + {/* 微信群设置区块,仅在选择微信群场景时显示 */} + {formData.scenario === 7 && ( +
+
+ onChange({ ...formData, weixinqunName })} + /> +
+
+ onChange({ ...formData, weixinqunNotice })} + /> +
+
+ )} + +
+ 是否启用 + onChange({ ...formData, enabled: value })} + /> +
+ +
+ ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss deleted file mode 100644 index 67047ef1..00000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss +++ /dev/null @@ -1,56 +0,0 @@ -.friend-request-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.form-item { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - .adm-form-item-label { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 8px; - } - - .adm-input { - border-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } - - .adm-text-area { - border-radius: 8px; - } -} - -.actions { - padding: 20px 0; -} - -.prev-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} - -.next-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx index fcb0bb63..f76e0fb3 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -1,113 +1,259 @@ -import React from "react"; -import { - Form, - Input, - Selector, - Button, - Card, - Space, - TextArea, -} from "antd-mobile"; -import style from "./FriendRequestSettings.module.scss"; - -interface FriendRequestSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - onPrev: () => void; -} - -const FriendRequestSettings: React.FC = ({ - formData, - onChange, - onNext, - onPrev, -}) => { - const remarkTypeOptions = [ - { label: "手机号", value: "phone" }, - { label: "微信号", value: "wechat" }, - { label: "QQ号", value: "qq" }, - { label: "自定义", value: "custom" }, - ]; - - const handleNext = () => { - if (!formData.greeting.trim()) { - // 可以添加验证逻辑 - } - onNext(); - }; - - return ( -
- -
- {/* 备注类型 */} - - onChange({ remarkType: value[0] })} - /> - - - {/* 备注格式 */} - {formData.remarkType === "custom" && ( - - onChange({ remarkFormat: value })} - clearable - /> - - )} - - {/* 打招呼消息 */} - -