diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index 86344416..c3fd82fd 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './contexts/AuthContext'; import { WechatAccountProvider } from './contexts/WechatAccountContext'; -import { ToastProvider } from './components/ui/toast'; import ProtectedRoute from './components/ProtectedRoute'; import LayoutWrapper from './components/LayoutWrapper'; import { initInterceptors } from './api'; @@ -27,7 +26,7 @@ import AIAssistant from './pages/workspace/ai-assistant/AIAssistant'; import TrafficDistribution from './pages/workspace/traffic-distribution/TrafficDistribution'; import TrafficDistributionDetail from './pages/workspace/traffic-distribution/Detail'; import Scenarios from './pages/scenarios/Scenarios'; -import NewPlan from './pages/scenarios/NewPlan'; +import NewPlan from './pages/scenarios/new/page'; import ScenarioDetail from './pages/scenarios/ScenarioDetail'; import Profile from './pages/profile/Profile'; import Plans from './pages/plans/Plans'; @@ -52,7 +51,6 @@ function App() { - @@ -101,7 +99,6 @@ function App() { - diff --git a/nkebao/src/api/scenarios.ts b/nkebao/src/api/scenarios.ts index f64ac2b2..79d49e55 100644 --- a/nkebao/src/api/scenarios.ts +++ b/nkebao/src/api/scenarios.ts @@ -237,4 +237,6 @@ export const transformSceneItem = (item: SceneItem): Channel => { growth: growthPercent } }; -}; \ No newline at end of file +}; + +export const getPlanScenes = () => get('/v1/plan/scenes'); \ No newline at end of file diff --git a/nkebao/src/components/ui/alert.tsx b/nkebao/src/components/ui/alert.tsx new file mode 100644 index 00000000..3787d88a --- /dev/null +++ b/nkebao/src/components/ui/alert.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => ( + children ? ( +
+ {children} +
+ ) : null +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } \ No newline at end of file diff --git a/nkebao/src/components/ui/dialog.tsx b/nkebao/src/components/ui/dialog.tsx index bebe71a4..5238163b 100644 --- a/nkebao/src/components/ui/dialog.tsx +++ b/nkebao/src/components/ui/dialog.tsx @@ -1,100 +1,97 @@ -import React, { useEffect } from 'react'; +"use client" -interface DialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - children: React.ReactNode; -} +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" -export function Dialog({ open, onOpenChange, children }: DialogProps) { - useEffect(() => { - if (open) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } +import { cn } from "@/utils" - return () => { - document.body.style.overflow = 'unset'; - }; - }, [open]); +const Dialog = DialogPrimitive.Root - if (!open) return null; +const DialogTrigger = DialogPrimitive.Trigger - return ( -
-
onOpenChange(false)} - /> -
- {children} -
-
- ); -} +const DialogPortal = DialogPrimitive.Portal -interface DialogContentProps { - children: React.ReactNode; - className?: string; -} +const DialogClose = DialogPrimitive.Close -export function DialogContent({ children, className = '' }: DialogContentProps) { - return ( -
+const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} -
- ); -} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName -interface DialogHeaderProps { - children: React.ReactNode; - className?: string; -} +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" -export function DialogHeader({ children, className = '' }: DialogHeaderProps) { - return ( -
- {children} -
- ); -} +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" -interface DialogTitleProps { - children: React.ReactNode; - className?: string; -} +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName -export function DialogTitle({ children, className = '' }: DialogTitleProps) { - return ( -

- {children} -

- ); -} +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName -interface DialogDescriptionProps { - children: React.ReactNode; - className?: string; +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, } - -export function DialogDescription({ children, className = '' }: DialogDescriptionProps) { - return ( -

- {children} -

- ); -} - -interface DialogFooterProps { - children: React.ReactNode; - className?: string; -} - -export function DialogFooter({ children, className = '' }: DialogFooterProps) { - return ( -
- {children} -
- ); -} \ No newline at end of file diff --git a/nkebao/src/components/ui/table.tsx b/nkebao/src/components/ui/table.tsx new file mode 100644 index 00000000..2ec86710 --- /dev/null +++ b/nkebao/src/components/ui/table.tsx @@ -0,0 +1,69 @@ +import * as React from "react" + +import { cn } from "@/utils" + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +) +Table.displayName = "Table" + +const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => , +) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( + tr]:last:border-b-0", className)} {...props} /> + ), +) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ), +) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef>( + ({ 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.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 - /> -
- -
- -
-
- - onChange({ startTime: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
- - onChange({ endTime: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-
-
-
- -
- -
-
- ); - }; - - // 好友申请设置步骤 - 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 = () => ( -
-
-

消息设置

- -
-
- -