>(
+ ({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]", className)}
+ {...props}
+ />
+ ),
+)
+TableCell.displayName = "TableCell"
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell }
\ No newline at end of file
diff --git a/nkebao/src/components/ui/toast.tsx b/nkebao/src/components/ui/toast.tsx
index 6b5f7eb5..96438086 100644
--- a/nkebao/src/components/ui/toast.tsx
+++ b/nkebao/src/components/ui/toast.tsx
@@ -1,132 +1,223 @@
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { X } from 'lucide-react';
+"use client"
-interface Toast {
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/utils"
+
+export type { ToastActionElement, ToastProps };
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive: "destructive border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+// === 以下为 use-toast.ts 的内容迁移 ===
+
+const TOAST_LIMIT = 1;
+const TOAST_REMOVE_DELAY = 1000000;
+
+type ToasterToast = ToastProps & {
id: string;
- title: string;
- description?: string;
- variant?: 'default' | 'destructive';
-}
-
-interface ToastContextType {
- toast: (toast: Omit) => void;
-}
-
-const ToastContext = createContext(undefined);
-
-export const useToast = () => {
- const context = useContext(ToastContext);
- if (!context) {
- throw new Error('useToast must be used within a ToastProvider');
- }
- return context;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ action?: ToastActionElement;
};
-interface ToastProviderProps {
- children: ReactNode;
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const;
+
+let count = 0;
+function genId() {
+ count = (count + 1) % Number.MAX_VALUE;
+ return count.toString();
}
-export function ToastProvider({ children }: ToastProviderProps) {
- const [toasts, setToasts] = useState([]);
+type ActionType = typeof actionTypes;
+type Action =
+ | { type: ActionType["ADD_TOAST"]; toast: ToasterToast }
+ | { type: ActionType["UPDATE_TOAST"]; toast: Partial }
+ | { type: ActionType["DISMISS_TOAST"]; toastId?: ToasterToast["id"] }
+ | { type: ActionType["REMOVE_TOAST"]; toastId?: ToasterToast["id"] };
- const toast = (newToast: Omit) => {
- const id = Math.random().toString(36).substr(2, 9);
- const toastWithId = { ...newToast, id };
-
- setToasts(prev => [...prev, toastWithId]);
-
- // 自动移除toast
- setTimeout(() => {
- setToasts(prev => prev.filter(t => t.id !== id));
- }, 5000);
- };
-
- const removeToast = (id: string) => {
- setToasts(prev => prev.filter(t => t.id !== id));
- };
-
- return (
-
- {children}
-
- {toasts.map((toast) => (
-
-
-
-
- {toast.title}
-
- {toast.description && (
-
- {toast.description}
-
- )}
-
-
-
-
- ))}
-
-
- );
+interface State {
+ toasts: ToasterToast[];
}
-// 直接导出toast函数,用于在组件中直接使用
-export const toast = (toastData: Omit) => {
- // 这里需要确保ToastProvider已经包装了应用
- // 在实际使用中,应该通过useToast hook来调用
- console.warn('toast function called without context. Please use useToast hook instead.');
-
- // 创建一个简单的DOM toast作为fallback
- const toastElement = document.createElement('div');
- toastElement.className = `fixed top-4 right-4 z-50 max-w-sm w-full bg-white rounded-lg shadow-lg border p-4 transform transition-all duration-300 ${
- toastData.variant === 'destructive'
- ? 'border-red-200 bg-red-50'
- : 'border-gray-200'
- }`;
-
- toastElement.innerHTML = `
-
-
-
- ${toastData.title}
-
- ${toastData.description ? `
-
- ${toastData.description}
-
- ` : ''}
-
-
-
- `;
-
- document.body.appendChild(toastElement);
-
- // 自动移除
- setTimeout(() => {
- if (toastElement.parentElement) {
- toastElement.remove();
+const toastTimeouts = new Map>();
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) return;
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId);
+ dispatch({ type: "REMOVE_TOAST", toastId });
+ }, TOAST_REMOVE_DELAY);
+ toastTimeouts.set(toastId, timeout);
+};
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) };
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
+ };
+ case "DISMISS_TOAST": {
+ const { toastId } = action;
+ if (toastId) {
+ addToRemoveQueue(toastId);
+ } else {
+ state.toasts.forEach((toast) => addToRemoveQueue(toast.id));
+ }
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined ? { ...t, open: false } : t
+ ),
+ };
}
- }, 5000);
-};
\ No newline at end of file
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return { ...state, toasts: [] };
+ }
+ return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId) };
+ }
+};
+
+const listeners: Array<(state: State) => void> = [];
+let memoryState: State = { toasts: [] };
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action);
+ listeners.forEach((listener) => listener(memoryState));
+}
+
+type Toast = Omit;
+
+function toast({ ...props }: Toast) {
+ const id = genId();
+ const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id } });
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open: boolean) => {
+ if (!open) dismiss();
+ },
+ },
+ });
+ return { id, dismiss, update };
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState);
+ React.useEffect(() => {
+ listeners.push(setState);
+ return () => {
+ const index = listeners.indexOf(setState);
+ if (index > -1) listeners.splice(index, 1);
+ };
+ }, []);
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ };
+}
+
+export { useToast, toast };
\ No newline at end of file
diff --git a/nkebao/src/components/ui/use-toast.ts b/nkebao/src/components/ui/use-toast.ts
new file mode 100644
index 00000000..ae46b285
--- /dev/null
+++ b/nkebao/src/components/ui/use-toast.ts
@@ -0,0 +1,188 @@
+"use client"
+
+import * as React from "react"
+
+import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_VALUE
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t,
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
\ No newline at end of file
diff --git a/nkebao/src/pages/scenarios/NewPlan.tsx b/nkebao/src/pages/scenarios/NewPlan.tsx
deleted file mode 100644
index 45a5f3db..00000000
--- a/nkebao/src/pages/scenarios/NewPlan.tsx
+++ /dev/null
@@ -1,443 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate, useSearchParams } from 'react-router-dom';
-import { Check, Settings } from 'lucide-react';
-import PageHeader from '@/components/PageHeader';
-import { useToast } from '@/components/ui/toast';
-import Layout from '@/components/Layout';
-import '@/components/Layout.css';
-
-// 步骤定义
-const steps = [
- { id: 1, title: "步骤一", subtitle: "基础设置" },
- { id: 2, title: "步骤二", subtitle: "好友申请设置" },
- { id: 3, title: "步骤三", subtitle: "消息设置" },
-];
-
-interface ScenarioOption {
- id: string;
- name: string;
- icon: string;
- description: string;
- image: string;
-}
-
-const scenarioOptions: ScenarioOption[] = [
- {
- id: "douyin",
- name: "抖音获客",
- icon: "🎵",
- description: "通过抖音平台进行精准获客",
- image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
- },
- {
- id: "xiaohongshu",
- name: "小红书获客",
- icon: "📖",
- description: "利用小红书平台进行内容营销获客",
- image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
- },
- {
- id: "gongzhonghao",
- name: "公众号获客",
- icon: "📱",
- description: "通过微信公众号进行获客",
- image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
- },
- {
- id: "haibao",
- name: "海报获客",
- icon: "🖼️",
- description: "通过海报分享进行获客",
- image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
- },
-];
-
-interface FormData {
- planName: string;
- posters: any[];
- device: any[];
- remarkType: string;
- greeting: string;
- addInterval: number;
- startTime: string;
- endTime: string;
- enabled: boolean;
- sceneId: string;
- scenario: string;
- planNameEdited: boolean;
-}
-
-export default function NewPlan() {
- const navigate = useNavigate();
- const searchParams = useSearchParams();
- const { toast } = useToast();
- const [currentStep, setCurrentStep] = useState(1);
- const [loading, setLoading] = useState(false);
-
-
- const [formData, setFormData] = useState({
- planName: "",
- posters: [],
- device: [],
- remarkType: "default",
- greeting: "",
- addInterval: 60,
- startTime: "09:00",
- endTime: "18:00",
- enabled: true,
- sceneId: searchParams[0].get("scenario") || "",
- scenario: searchParams[0].get("scenario") || "",
- planNameEdited: false
- });
-
- // 更新表单数据
- const onChange = (data: Partial) => {
- if ('planName' in data) {
- setFormData(prev => ({ ...prev, planNameEdited: true, ...data }));
- } else {
- setFormData(prev => ({ ...prev, ...data }));
- }
- };
-
- // 处理保存
- const handleSave = async () => {
- try {
- setLoading(true);
- // 模拟API调用
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- toast({
- title: "创建成功",
- description: "获客计划已创建",
- });
- navigate("/scenarios");
- } catch (error: any) {
- toast({
- title: "创建失败",
- description: error?.message || "创建计划失败,请重试",
- variant: "destructive",
- });
- } finally {
- setLoading(false);
- }
- };
-
- // 下一步
- const handleNext = () => {
- if (currentStep === steps.length) {
- handleSave();
- } else {
- setCurrentStep((prev) => prev + 1);
- }
- };
-
- // 上一步
- const handlePrev = () => {
- setCurrentStep((prev) => Math.max(prev - 1, 1));
- };
-
- // 步骤指示器组件(方案A:简洁线性进度条风格)
- const StepIndicator = ({ steps, currentStep }: { steps: any[], currentStep: number }) => {
- const percent = ((currentStep - 1) / (steps.length - 1)) * 100;
- return (
-
- {/* 进度条 */}
-
-
-
- {/* 圆点 */}
-
- {steps.map((step, idx) => {
- const isActive = currentStep === step.id;
- const isDone = currentStep > step.id;
- return (
-
-
- {isDone ? : {step.id}}
-
-
- );
- })}
-
-
- {/* 步骤文字 */}
-
- {steps.map((step, idx) => {
- const isActive = currentStep === step.id;
- const isDone = currentStep > step.id;
- return (
-
- {step.title}
- {step.subtitle}
-
- );
- })}
-
-
- );
- };
-
- // 基础设置步骤
- const BasicSettings = () => {
- return (
-
-
- 选择获客场景
-
- {scenarioOptions.map((scenario) => (
- onChange({ sceneId: scenario.id, scenario: scenario.id })}
- >
- {formData.sceneId === scenario.id && (
-
-
-
- )}
-
- 
- {scenario.name}
- {scenario.description}
-
-
- ))}
-
-
-
-
- 计划信息
-
-
-
- onChange({ planName: e.target.value })}
- placeholder="请输入计划名称"
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- required
- />
-
-
-
-
-
-
-
-
-
- );
- };
-
- // 好友申请设置步骤
- const FriendRequestSettings = () => (
-
-
- 好友申请设置
-
-
-
-
- onChange({ addInterval: parseInt(e.target.value) || 60 })}
- min="30"
- max="3600"
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
- />
- 建议设置60-300秒,避免被限制
-
-
-
-
-
-
-
- {formData.remarkType === 'custom' && (
-
-
-
-
- )}
-
-
-
-
-
-
-
-
- );
-
- // 消息设置步骤
- const MessageSettings = () => (
-
-
- 消息设置
-
-
-
-
-
- onChange({ enabled: e.target.checked })}
- className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
- />
-
-
-
-
-
-
-
-
-
-
- );
-
- // 渲染当前步骤内容
- const renderStepContent = () => {
- switch (currentStep) {
- case 1:
- return ;
- case 2:
- return ;
- case 3:
- return ;
- default:
- return null;
- }
- };
-
- return (
-
-
-
- }
- />
- }
- >
-
-
-
-
-
- {renderStepContent()}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/nkebao/src/pages/scenarios/new/page.tsx b/nkebao/src/pages/scenarios/new/page.tsx
new file mode 100644
index 00000000..835557fa
--- /dev/null
+++ b/nkebao/src/pages/scenarios/new/page.tsx
@@ -0,0 +1,135 @@
+import { useState, useEffect } from "react"
+import { useNavigate } from "react-router-dom"
+import { ChevronLeft, Settings } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { toast } from "@/components/ui/use-toast"
+import { Steps, StepItem } from 'tdesign-mobile-react';
+import { BasicSettings } from "./steps/BasicSettings"
+import { FriendRequestSettings } from "./steps/FriendRequestSettings"
+import { MessageSettings } from "./steps/MessageSettings"
+import Layout from "@/components/Layout"
+import { getPlanScenes } from '@/api/scenarios';
+
+// 步骤定义 - 只保留三个步骤
+const steps = [
+ { id: 1, title: "步骤一", subtitle: "基础设置" },
+ { id: 2, title: "步骤二", subtitle: "好友申请设置" },
+ { id: 3, title: "步骤三", subtitle: "消息设置" },
+]
+
+export default function NewPlan() {
+ const router = useNavigate()
+ const [currentStep, setCurrentStep] = useState(1)
+ const [formData, setFormData] = useState({
+ planName: "",
+ scenario: "haibao",
+ posters: [],
+ device: "",
+ remarkType: "phone",
+ greeting: "你好,请通过",
+ addInterval: 1,
+ startTime: "09:00",
+ endTime: "18:00",
+ enabled: true,
+ // 移除tags字段
+ })
+ const [sceneList, setSceneList] = useState([]);
+ const [sceneLoading, setSceneLoading] = useState(true);
+
+ useEffect(() => {
+ setSceneLoading(true);
+ getPlanScenes()
+ .then(res => {
+ setSceneList(res?.data || []);
+ })
+ .finally(() => setSceneLoading(false));
+ }, []);
+
+ // 更新表单数据
+ const onChange = (data: any) => {
+ setFormData((prev) => ({ ...prev, ...data }))
+ }
+
+ // 处理保存
+ const handleSave = async () => {
+ try {
+ // 这里应该是实际的API调用
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ toast({
+ title: "创建成功",
+ description: "获客计划已创建",
+ })
+ router("/plans")
+ } catch (error) {
+ toast({
+ title: "创建失败",
+ description: "创建计划失败,请重试",
+ variant: "destructive",
+ })
+ }
+ }
+
+ // 下一步
+ const handleNext = () => {
+ if (currentStep === steps.length) {
+ handleSave()
+ } else {
+ setCurrentStep((prev) => prev + 1)
+ }
+ }
+
+ // 上一步
+ const handlePrev = () => {
+ setCurrentStep((prev) => Math.max(prev - 1, 1))
+ }
+
+ // 渲染当前步骤内容
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return
+ case 2:
+ return
+ case 3:
+ return
+ default:
+ return null
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ {steps.map((step) => (
+
+ ))}
+
+
+ >
+ }>
+
+
+ {renderStepContent()}
+
+
+
+ )
+}
diff --git a/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx
new file mode 100644
index 00000000..e0009efb
--- /dev/null
+++ b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx
@@ -0,0 +1,666 @@
+import type React from "react"
+import { useState, useEffect } from "react"
+import { Button, Input, Tag, Grid, Dialog, ImageViewer, Table, Switch } from 'tdesign-mobile-react';
+
+
+// 调整场景顺序,确保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
+ 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,
+ }))
+}
+
+export function BasicSettings({ formData, onChange, onNext, sceneList, sceneLoading }: BasicSettingsProps) {
+ 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 [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 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.planName) {
+ const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
+ onChange({ ...formData, planName: `海报${today}` })
+ }
+ }, [formData, onChange])
+
+ // 选中场景
+ 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 handleMaterialSelect = (material: Material) => {
+ setSelectedMaterials([material]);
+ onChange({ ...formData, materials: [material] });
+ setIsMaterialDialogOpen(false);
+ };
+
+ // 移除已选素材
+ const handleRemoveMaterial = (id: string) => {
+ setSelectedMaterials([]);
+ onChange({ ...formData, materials: [] });
+ };
+
+ // 预览图片
+ const handlePreviewImage = (url: string) => {
+ 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 displayedScenarios = showAllScenarios ? scenarios : scenarios.slice(0, 3)
+
+ // 处理文件导入
+ 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);
+ };
+
+ // 账号弹窗关闭时清理搜索等状态
+ 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);
+
+ return (
+
+ {/* 场景选择区块 */}
+ {sceneLoading ? (
+ 加载中...
+ ) : (
+
+ {sceneList.map((scene) => (
+
+ ))}
+
+ )}
+
+ {/* 计划名称输入区 */}
+ 计划名称
+
+ onChange({ ...formData, planName: 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(String(v))}
+ placeholder="添加自定义标签"
+ className="w-full"
+ />
+
+
+
+
+
+ {/* 账号选择区块 */}
+
+
+
+ {selectedAccounts.map((account: Account) => (
+ handleRemoveAccount(account.id)}
+ >
+ {account.nickname}
+
+ ))}
+
+
+
+ {/* 素材选择区块 */}
+
+
+
+ {selectedMaterials.map((material: Material) => (
+ handleRemoveMaterial(material.id)}
+ onClick={() => handlePreviewImage(material.preview)}
+ >
+ {material.name}
+
+ ))}
+
+
+ m.preview)}
+ visible={isPreviewOpen}
+ onClose={handleImagePreviewClose}
+ index={0}
+ />
+
+ {/* 订单导入区块 */}
+
+
+
+
+ {/* 电话获客设置区块,仅在选择电话获客场景时显示 */}
+ {formData.scenario === 'phone' && (
+
+
+
+
+ )}
+ {/* 微信群设置区块,仅在选择微信群场景时显示 */}
+ {formData.scenario === 'weixinqun' && (
+
+ )}
+
+
+ );
+}
diff --git a/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx
new file mode 100644
index 00000000..cbfa37cf
--- /dev/null
+++ b/nkebao/src/pages/scenarios/new/steps/FriendRequestSettings.tsx
@@ -0,0 +1,277 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Card } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { HelpCircle, MessageSquare, AlertCircle } from "lucide-react"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+import { ChevronsUpDown } from "lucide-react"
+import { Checkbox } from "@/components/ui/checkbox"
+
+interface FriendRequestSettingsProps {
+ formData: any
+ onChange: (data: any) => void
+ onNext: () => void
+ onPrev: () => void
+}
+
+// 招呼语模板
+const greetingTemplates = [
+ "你好,请通过",
+ "你好,了解XX,请通过",
+ "你好,我是XX产品的客服请通过",
+ "你好,感谢关注我们的产品",
+ "你好,很高兴为您服务",
+]
+
+// 备注类型选项
+const remarkTypes = [
+ { value: "phone", label: "手机号" },
+ { value: "nickname", label: "昵称" },
+ { value: "source", label: "来源" },
+]
+
+// 模拟设备数据
+const mockDevices = [
+ { id: "1", name: "iPhone 13 Pro", status: "online" },
+ { id: "2", name: "Xiaomi 12", status: "online" },
+ { id: "3", name: "Huawei P40", status: "offline" },
+ { id: "4", name: "OPPO Find X3", status: "online" },
+ { id: "5", name: "Samsung S21", status: "online" },
+]
+
+export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: FriendRequestSettingsProps) {
+ const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
+ const [hasWarnings, setHasWarnings] = useState(false)
+ const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false)
+ const [selectedDevices, setSelectedDevices] = useState(formData.selectedDevices || [])
+
+ // 获取场景标题
+ const getScenarioTitle = () => {
+ switch (formData.scenario) {
+ case "douyin":
+ return "抖音直播"
+ case "xiaohongshu":
+ return "小红书"
+ case "weixinqun":
+ return "微信群"
+ case "gongzhonghao":
+ return "公众号"
+ default:
+ return formData.planName || "获客计划"
+ }
+ }
+
+ // 使用useEffect设置默认值
+ useEffect(() => {
+ if (!formData.greeting) {
+ onChange({
+ ...formData,
+ greeting: "你好,请通过",
+ remarkType: "phone", // 默认选择手机号
+ remarkFormat: `手机号+${getScenarioTitle()}`, // 默认备注格式
+ addFriendInterval: 1,
+ })
+ }
+ }, [formData, formData.greeting, onChange])
+
+ // 检查是否有未完成的必填项
+ useEffect(() => {
+ const hasIncompleteFields = !formData.greeting?.trim()
+ setHasWarnings(hasIncompleteFields)
+ }, [formData])
+
+ const handleTemplateSelect = (template: string) => {
+ onChange({ ...formData, greeting: template })
+ setIsTemplateDialogOpen(false)
+ }
+
+ const handleNext = () => {
+ // 即使有警告也允许进入下一步,但会显示提示
+ onNext()
+ }
+
+ const toggleDeviceSelection = (device: any) => {
+ const isSelected = selectedDevices.some((d) => d.id === device.id)
+ let newSelectedDevices
+
+ if (isSelected) {
+ newSelectedDevices = selectedDevices.filter((d) => d.id !== device.id)
+ } else {
+ newSelectedDevices = [...selectedDevices, device]
+ }
+
+ setSelectedDevices(newSelectedDevices)
+ onChange({ ...formData, selectedDevices: newSelectedDevices })
+ }
+
+ return (
+
+
+
+
+
+
+
+ {isDeviceSelectorOpen && (
+
+
+
+
+ {mockDevices.map((device) => (
+ toggleDeviceSelection(device)}
+ >
+
+ d.id === device.id)}
+ onCheckedChange={() => toggleDeviceSelection(device)}
+ />
+ {device.name}
+
+
+ {device.status === "online" ? "在线" : "离线"}
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置添加好友时的备注格式
+ 备注格式预览:
+ {formData.remarkType === "phone" && `138****1234+${getScenarioTitle()}`}
+ {formData.remarkType === "nickname" && `小红书用户2851+${getScenarioTitle()}`}
+ {formData.remarkType === "source" && `抖音直播+${getScenarioTitle()}`}
+
+
+
+
+
+
+
+
+
+
+
+
+ onChange({ ...formData, greeting: e.target.value })}
+ placeholder="请输入招呼语"
+ className="mt-2"
+ />
+
+
+
+
+
+ onChange({ ...formData, addFriendInterval: Number(e.target.value) })}
+ className="w-32"
+ />
+ 分钟
+
+
+
+
+
+ {hasWarnings && (
+
+
+ 您有未完成的设置项,建议完善后再进入下一步。
+
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx b/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx
new file mode 100644
index 00000000..3c8fc4a9
--- /dev/null
+++ b/nkebao/src/pages/scenarios/new/steps/MessageSettings.tsx
@@ -0,0 +1,552 @@
+import { useState } from "react"
+import { Card } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ MessageSquare,
+ ImageIcon,
+ Video,
+ FileText,
+ Link2,
+ Users,
+ AppWindowIcon as Window,
+ Plus,
+ X,
+ Upload,
+ Clock,
+} from "lucide-react"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { toast } from "@/components/ui/use-toast"
+
+interface MessageContent {
+ id: string
+ type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group"
+ content: string
+ sendInterval?: number
+ intervalUnit?: "seconds" | "minutes"
+ scheduledTime?: {
+ hour: number
+ minute: number
+ second: number
+ }
+ title?: string
+ description?: string
+ address?: string
+ coverImage?: string
+ groupId?: string
+ linkUrl?: string
+}
+
+interface DayPlan {
+ day: number
+ messages: MessageContent[]
+}
+
+interface MessageSettingsProps {
+ formData: any
+ onChange: (data: any) => void
+ onNext: () => void
+ onPrev: () => void
+}
+
+// 消息类型配置
+const messageTypes = [
+ { id: "text", icon: MessageSquare, label: "文本" },
+ { id: "image", icon: ImageIcon, label: "图片" },
+ { id: "video", icon: Video, label: "视频" },
+ { id: "file", icon: FileText, label: "文件" },
+ { id: "miniprogram", icon: Window, label: "小程序" },
+ { id: "link", icon: Link2, label: "链接" },
+ { id: "group", icon: Users, label: "邀请入群" },
+]
+
+// 模拟群组数据
+const mockGroups = [
+ { id: "1", name: "产品交流群1", memberCount: 156 },
+ { id: "2", name: "产品交流群2", memberCount: 234 },
+ { id: "3", name: "产品交流群3", memberCount: 89 },
+]
+
+export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageSettingsProps) {
+ const [dayPlans, setDayPlans] = useState([
+ {
+ day: 0,
+ messages: [
+ {
+ id: "1",
+ type: "text",
+ content: "",
+ sendInterval: 5,
+ intervalUnit: "seconds", // 默认改为秒
+ },
+ ],
+ },
+ ])
+ const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false)
+ const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false)
+ const [selectedGroupId, setSelectedGroupId] = useState("")
+
+ // 添加新消息
+ const handleAddMessage = (dayIndex: number, type = "text") => {
+ const updatedPlans = [...dayPlans]
+ const newMessage: MessageContent = {
+ id: Date.now().toString(),
+ type: type as MessageContent["type"],
+ content: "",
+ }
+
+ if (dayPlans[dayIndex].day === 0) {
+ // 即时消息使用间隔设置
+ newMessage.sendInterval = 5
+ newMessage.intervalUnit = "seconds" // 默认改为秒
+ } else {
+ // 非即时消息使用具体时间设置
+ newMessage.scheduledTime = {
+ hour: 9,
+ minute: 0,
+ second: 0,
+ }
+ }
+
+ updatedPlans[dayIndex].messages.push(newMessage)
+ setDayPlans(updatedPlans)
+ onChange({ ...formData, messagePlans: updatedPlans })
+ }
+
+ // 更新消息内容
+ const handleUpdateMessage = (dayIndex: number, messageIndex: number, updates: Partial) => {
+ const updatedPlans = [...dayPlans]
+ updatedPlans[dayIndex].messages[messageIndex] = {
+ ...updatedPlans[dayIndex].messages[messageIndex],
+ ...updates,
+ }
+ setDayPlans(updatedPlans)
+ onChange({ ...formData, messagePlans: updatedPlans })
+ }
+
+ // 删除消息
+ const handleRemoveMessage = (dayIndex: number, messageIndex: number) => {
+ const updatedPlans = [...dayPlans]
+ updatedPlans[dayIndex].messages.splice(messageIndex, 1)
+ setDayPlans(updatedPlans)
+ onChange({ ...formData, messagePlans: updatedPlans })
+ }
+
+ // 切换时间单位
+ const toggleIntervalUnit = (dayIndex: number, messageIndex: number) => {
+ const message = dayPlans[dayIndex].messages[messageIndex]
+ const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes"
+ handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit })
+ }
+
+ // 添加新的天数计划
+ const handleAddDayPlan = () => {
+ const newDay = dayPlans.length
+ setDayPlans([
+ ...dayPlans,
+ {
+ day: newDay,
+ messages: [
+ {
+ id: Date.now().toString(),
+ type: "text",
+ content: "",
+ scheduledTime: {
+ hour: 9,
+ minute: 0,
+ second: 0,
+ },
+ },
+ ],
+ },
+ ])
+ setIsAddDayPlanOpen(false)
+ toast({
+ title: "添加成功",
+ description: `已添加第${newDay}天的消息计划`,
+ })
+ }
+
+ // 选择群组
+ const handleSelectGroup = (groupId: string) => {
+ setSelectedGroupId(groupId)
+ setIsGroupSelectOpen(false)
+ toast({
+ title: "选择成功",
+ description: `已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}`,
+ })
+ }
+
+ // 处理文件上传
+ const handleFileUpload = (dayIndex: number, messageIndex: number, type: "image" | "video" | "file") => {
+ // 模拟文件上传
+ toast({
+ title: "上传成功",
+ description: `${type === "image" ? "图片" : type === "video" ? "视频" : "文件"}上传成功`,
+ })
+ }
+
+ return (
+
+
+
+ 消息设置
+
+
+
+
+
+ {dayPlans.map((plan) => (
+
+ {plan.day === 0 ? "即时消息" : `第${plan.day}天`}
+
+ ))}
+
+
+ {dayPlans.map((plan, dayIndex) => (
+
+
+ {plan.messages.map((message, messageIndex) => (
+
+
+
+ {plan.day === 0 ? (
+ <>
+
+
+ handleUpdateMessage(dayIndex, messageIndex, { sendInterval: Number(e.target.value) })
+ }
+ className="w-20"
+ />
+
+ >
+ ) : (
+ <>
+
+
+
+ handleUpdateMessage(dayIndex, messageIndex, {
+ scheduledTime: {
+ ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }),
+ hour: Number(e.target.value),
+ },
+ })
+ }
+ className="w-16"
+ />
+ :
+
+ handleUpdateMessage(dayIndex, messageIndex, {
+ scheduledTime: {
+ ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }),
+ minute: Number(e.target.value),
+ },
+ })
+ }
+ className="w-16"
+ />
+ :
+
+ handleUpdateMessage(dayIndex, messageIndex, {
+ scheduledTime: {
+ ...(message.scheduledTime || { hour: 0, minute: 0, second: 0 }),
+ second: Number(e.target.value),
+ },
+ })
+ }
+ className="w-16"
+ />
+
+ >
+ )}
+
+
+
+
+
+ {messageTypes.map((type) => (
+
+ ))}
+
+
+ {message.type === "text" && (
+
+ ))}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {/* 添加天数计划弹窗 */}
+
+
+ {/* 选择群聊弹窗 */}
+
+
+ )
+}
diff --git a/nkebao/src/pages/scenarios/new/steps/TagSettings.tsx b/nkebao/src/pages/scenarios/new/steps/TagSettings.tsx
new file mode 100644
index 00000000..281672a8
--- /dev/null
+++ b/nkebao/src/pages/scenarios/new/steps/TagSettings.tsx
@@ -0,0 +1,205 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Card } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Badge } from "@/components/ui/badge"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
+import { Plus, X, Edit2, AlertCircle } from "lucide-react"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+
+interface TagSettingsProps {
+ formData: any
+ onChange: (data: any) => void
+ onNext?: () => void
+ onPrev?: () => void
+}
+
+interface Tag {
+ id: string
+ name: string
+ keywords: string[]
+}
+
+export function TagSettings({ formData, onChange, onNext, onPrev }: TagSettingsProps) {
+ const [tags, setTags] = useState(formData.tags || [])
+ const [isAddTagDialogOpen, setIsAddTagDialogOpen] = useState(false)
+ const [editingTag, setEditingTag] = useState(null)
+ const [newTagName, setNewTagName] = useState("")
+ const [newTagKeywords, setNewTagKeywords] = useState("")
+ const [hasWarnings, setHasWarnings] = useState(false)
+
+ // 当标签更新时,更新formData
+ useEffect(() => {
+ onChange({ ...formData, tags })
+ }, [tags, onChange])
+
+ // 检查是否有标签
+ useEffect(() => {
+ setHasWarnings(tags.length === 0)
+ }, [tags])
+
+ const handleAddTag = () => {
+ if (!newTagName.trim()) return
+
+ const keywordsArray = newTagKeywords
+ .split("\n")
+ .map((k) => k.trim())
+ .filter((k) => k !== "")
+
+ if (editingTag) {
+ // 编辑现有标签
+ setTags(
+ tags.map((tag) => (tag.id === editingTag.id ? { ...tag, name: newTagName, keywords: keywordsArray } : tag)),
+ )
+ } else {
+ // 添加新标签
+ setTags([
+ ...tags,
+ {
+ id: Date.now().toString(),
+ name: newTagName,
+ keywords: keywordsArray,
+ },
+ ])
+ }
+
+ // 重置表单
+ setNewTagName("")
+ setNewTagKeywords("")
+ setEditingTag(null)
+ setIsAddTagDialogOpen(false)
+ }
+
+ const handleEditTag = (tag: Tag) => {
+ setEditingTag(tag)
+ setNewTagName(tag.name)
+ setNewTagKeywords(tag.keywords.join("\n"))
+ setIsAddTagDialogOpen(true)
+ }
+
+ const handleDeleteTag = (tagId: string) => {
+ setTags(tags.filter((tag) => tag.id !== tagId))
+ }
+
+ const handleNext = () => {
+ // 确保onNext是一个函数
+ if (typeof onNext === "function") {
+ onNext()
+ }
+ }
+
+ const handlePrev = () => {
+ // 确保onPrev是一个函数
+ if (typeof onPrev === "function") {
+ onPrev()
+ }
+ }
+
+ const handleCancel = () => {
+ setNewTagName("")
+ setNewTagKeywords("")
+ setEditingTag(null)
+ setIsAddTagDialogOpen(false)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {tags.length === 0 ? (
+
+ 暂无标签,点击"添加标签"按钮来创建标签
+
+ ) : (
+
+ {tags.map((tag) => (
+
+
+ {tag.name}
+
+ {tag.keywords.length > 0 ? `关键词: ${tag.keywords.join(", ")}` : "无关键词"}
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {hasWarnings && (
+
+
+ 建议添加至少一个标签,以便更好地管理客户。
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
|