From 7e848b20817bb69cae9aaa97531532ecbe360b7b 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: Tue, 22 Jul 2025 13:55:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=AD=98=E4=B8=80=E6=B3=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/auto-like/AutoLikeListPC.scss | 1 - .../workspace/auto-like/AutoLikeListPC.tsx | 1 - .../workspace/auto-like/new/NewAutoLike.tsx | 552 ++++++++++++++++++ .../pages/workspace/auto-like/new/index.tsx | 318 ++++++---- .../workspace/auto-like/new/new.module.scss | 439 +++++++------- .../auto-like/record/AutoLikeDetail.tsx | 281 +++++++++ .../workspace/auto-like/record/index.tsx | 14 +- 7 files changed, 1238 insertions(+), 368 deletions(-) delete mode 100644 nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss delete mode 100644 nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss deleted file mode 100644 index 0519ecba6..000000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx deleted file mode 100644 index 0519ecba6..000000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx b/nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx new file mode 100644 index 000000000..7c52c5835 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/new/NewAutoLike.tsx @@ -0,0 +1,552 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { ChevronLeft,Plus, Minus, Check, X, Tag as TagIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { createAutoLikeTask, updateAutoLikeTask, fetchAutoLikeTaskDetail } from '@/api/autoLike'; +import { ContentType } from '@/types/auto-like'; +import { useToast } from '@/components/ui/toast'; +import Layout from '@/components/Layout'; +import DeviceSelection from '@/components/DeviceSelection'; +import FriendSelection from '@/components/FriendSelection'; + + + + + + + +// 修改CreateLikeTaskData接口,确保friends字段不是可选的 +interface CreateLikeTaskDataLocal { + name: string; + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; + targetTags: string[]; +} + +export default function NewAutoLike() { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEditMode = !!id; + const { toast } = useToast(); + const [currentStep, setCurrentStep] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(isEditMode); + const [formData, setFormData] = useState({ + name: '', + interval: 5, + maxLikes: 200, + startTime: '08:00', + endTime: '22:00', + contentTypes: ['text', 'image', 'video'], + devices: [], + friends: [], // 确保初始化为空数组而不是undefined + targetTags: [], + friendMaxLikes: 10, + enableFriendTags: false, + friendTags: '', + }); + // 新增自动开启的独立状态 + const [autoEnabled, setAutoEnabled] = useState(false); + + // 如果是编辑模式,获取任务详情 + useEffect(() => { + if (isEditMode && id) { + fetchTaskDetail(); + } + }, [id, isEditMode]); + + // 获取任务详情 + const fetchTaskDetail = async () => { + try { + const taskDetail = await fetchAutoLikeTaskDetail(id!); + console.log('Task detail response:', taskDetail); // 添加日志用于调试 + + if (taskDetail) { + // 使用类型断言处理可能的字段名称差异 + const taskAny = taskDetail as any; + // 处理可能的嵌套结构 + const config = taskAny.config || taskAny; + + setFormData({ + name: taskDetail.name || '', + interval: config.likeInterval || config.interval || 5, + maxLikes: config.maxLikesPerDay || config.maxLikes || 200, + startTime: config.timeRange?.start || config.startTime || '08:00', + endTime: config.timeRange?.end || config.endTime || '22:00', + contentTypes: config.contentTypes || ['text', 'image', 'video'], + devices: config.devices || [], + friends: config.friends || [], + targetTags: config.targetTags || [], + friendMaxLikes: config.friendMaxLikes || 10, + enableFriendTags: config.enableFriendTags || false, + friendTags: config.friendTags || '', + }); + + // 处理状态字段,使用双等号允许类型自动转换 + const status = taskAny.status; + setAutoEnabled(status === 1 || status === 'running'); + } else { + toast({ + title: '获取任务详情失败', + description: '无法找到该任务', + variant: 'destructive', + }); + navigate('/workspace/auto-like'); + } + } catch (error) { + console.error('获取任务详情出错:', error); // 添加错误日志 + toast({ + title: '获取任务详情失败', + description: '请检查网络连接后重试', + variant: 'destructive', + }); + navigate('/workspace/auto-like'); + } finally { + setIsLoading(false); + } + }; + + + + const handleUpdateFormData = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const handleNext = () => { + setCurrentStep((prev) => Math.min(prev + 1, 3)); + // 滚动到顶部 + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + const handlePrev = () => { + setCurrentStep((prev) => Math.max(prev - 1, 1)); + // 滚动到顶部 + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + const handleComplete = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + try { + // 转换为API需要的格式 + const apiFormData = { + ...formData, + // 如果API需要其他转换,可以在这里添加 + }; + + let response; + if (isEditMode) { + // 编辑模式,调用更新API + response = await updateAutoLikeTask({ + ...apiFormData, + id: id! + }); + } else { + // 新建模式,调用创建API + response = await createAutoLikeTask(apiFormData); + } + + if (response.code === 200) { + toast({ + title: isEditMode ? '更新成功' : '创建成功', + description: isEditMode ? '自动点赞任务已更新' : '自动点赞任务已创建并开始执行', + }); + navigate('/workspace/auto-like'); + } else { + toast({ + title: isEditMode ? '更新失败' : '创建失败', + description: response.msg || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + toast({ + title: isEditMode ? '更新失败' : '创建失败', + description: '请检查网络连接后重试', + variant: 'destructive', + }); + } finally { + setIsSubmitting(false); + } + }; + + const header = ( +
+
+ +

{isEditMode ? '编辑自动点赞' : '新建自动点赞'}

+
+ +
+ ); + + if (isLoading) { + return ( + +
+
+
+

加载中...

+
+
+
+ ); + } + + return ( + +
+
+ +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( +
+ handleUpdateFormData({ devices })} + placeholder="选择设备" + /> + +
+ + +
+
+ )} + + {currentStep === 3 && ( +
+ handleUpdateFormData({ friends })} + deviceIds={formData.devices} + placeholder="选择微信好友" + /> + +
+ + +
+
+ )} +
+
+
+
+ ); +} + +// 步骤指示器组件 +interface StepIndicatorProps { + currentStep: number; +} + +function StepIndicator({ currentStep }: StepIndicatorProps) { + const steps = [ + { title: '基础设置', description: '设置点赞规则' }, + { title: '设备选择', description: '选择执行设备' }, + { title: '人群选择', description: '选择目标人群' }, + ]; + + return ( +
+
+
+ {steps.map((step, index) => ( +
+
+ {index < currentStep ? : index + 1} +
+
+
+ {step.title} +
+
{step.description}
+
+
+ ))} +
+
+
+
+
+
+ ); +} + +// 基础设置组件 +interface BasicSettingsProps { + formData: CreateLikeTaskDataLocal; + onChange: (data: Partial) => void; + onNext: () => void; + autoEnabled: boolean; + setAutoEnabled: (v: boolean) => void; +} + +function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled }: BasicSettingsProps) { + const handleContentTypeChange = (type: ContentType) => { + const currentTypes = [...formData.contentTypes]; + if (currentTypes.includes(type)) { + onChange({ contentTypes: currentTypes.filter((t) => t !== type) }); + } else { + onChange({ contentTypes: [...currentTypes, type] }); + } + }; + + const incrementInterval = () => { + onChange({ interval: Math.min(formData.interval + 5, 60) }); + }; + + const decrementInterval = () => { + onChange({ interval: Math.max(formData.interval - 5, 5) }); + }; + + const incrementMaxLikes = () => { + onChange({ maxLikes: Math.min(formData.maxLikes + 10, 500) }); + }; + + const decrementMaxLikes = () => { + onChange({ maxLikes: Math.max(formData.maxLikes - 10, 10) }); + }; + + return ( +
+
+ + onChange({ name: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+ +
+ +
+ +
+ onChange({ interval: Number.parseInt(e.target.value) || 5 })} + className="h-12 rounded-none border-x-0 border-gray-200 text-center" + /> +
+ 秒 +
+
+ +
+

设置两次点赞之间的最小时间间隔

+
+ +
+ +
+ +
+ onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })} + className="h-12 rounded-none border-x-0 border-gray-200 text-center" + /> +
+ 次/天 +
+
+ +
+

设置每天最多点赞的次数

+
+ +
+ +
+
+ onChange({ startTime: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+
+ onChange({ endTime: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+
+

设置每天可以点赞的时间段

+
+ +
+ +
+ {[ + { id: 'text' as ContentType, label: '文字' }, + { id: 'image' as ContentType, label: '图片' }, + { id: 'video' as ContentType, label: '视频' }, + ].map((type) => ( +
handleContentTypeChange(type.id)} + > + {type.label} +
+ ))} +
+

选择要点赞的内容类型

+
+ +
+
+ + onChange({ enableFriendTags: checked })} + /> +
+ {formData.enableFriendTags && ( + <> +
+ + onChange({ friendTags: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +

只给有此标签的好友点赞

+
+ + )} +
+ +
+ + +
+ + +
+ ); +} + + + + + + \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx index b0a13181f..b530e45d3 100644 --- a/nkebao/src/pages/workspace/auto-like/new/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { Button, Input, Switch, message, Spin } from "antd"; import { + ArrowLeftOutlined, PlusOutlined, MinusOutlined, - ArrowLeftOutlined, + CheckOutlined, } from "@ant-design/icons"; -import { Button, Input, Switch, message, Spin } from "antd"; import { NavBar } from "antd-mobile"; import Layout from "@/components/Layout/Layout"; import { @@ -13,11 +14,7 @@ import { updateAutoLikeTask, fetchAutoLikeTaskDetail, } from "./api"; -import { - CreateLikeTaskData, - UpdateLikeTaskData, - ContentType, -} from "@/types/auto-like"; +import { ContentType } from "@/types/auto-like"; import style from "./new.module.scss"; const contentTypeLabels: Record = { @@ -27,28 +24,32 @@ const contentTypeLabels: Record = { link: "链接", }; +const steps = ["基础设置", "设备选择", "人群选择"]; + +const defaultForm = { + name: "", + interval: 5, + maxLikes: 200, + startTime: "08:00", + endTime: "22:00", + contentTypes: ["text", "image", "video"] as ContentType[], + devices: [] as string[], + friends: [] as string[], + targetTags: [] as string[], + friendMaxLikes: 10, + enableFriendTags: false, + friendTags: "", +}; + const NewAutoLike: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const isEditMode = !!id; - const [currentStep, setCurrentStep] = useState(1); + const [currentStep, setCurrentStep] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(isEditMode); const [autoEnabled, setAutoEnabled] = useState(false); - const [formData, setFormData] = useState({ - name: "", - interval: 5, - maxLikes: 200, - startTime: "08:00", - endTime: "22:00", - contentTypes: ["text", "image", "video"], - devices: [], - friends: [], - targetTags: [], - friendMaxLikes: 10, - enableFriendTags: false, - friendTags: "", - }); + const [formData, setFormData] = useState({ ...defaultForm }); useEffect(() => { if (isEditMode && id) { @@ -89,12 +90,12 @@ const NewAutoLike: React.FC = () => { } }; - const handleUpdateFormData = (data: Partial) => { + const handleUpdateFormData = (data: Partial) => { setFormData((prev) => ({ ...prev, ...data })); }; - const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 3)); - const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 1)); + const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 2)); + const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 0)); const handleComplete = async () => { if (!formData.name.trim()) { @@ -124,90 +125,104 @@ const NewAutoLike: React.FC = () => { // 步骤1:基础设置 const renderBasicSettings = () => ( -
-
- +
+
+
任务名称
handleUpdateFormData({ name: e.target.value })} - className={style["form-input"]} + className={style.input} />
-
- -
- + {formData.interval} 秒 +
+
设置两次点赞之间的最小时间间隔
-
- -
- + {formData.maxLikes} 次 +
+
设置每天最多点赞的次数
-
- -
+ +
+
点赞时间范围
+
handleUpdateFormData({ startTime: e.target.value }) } - className={style["time-input"]} + className={style.inputTime} /> - + handleUpdateFormData({ endTime: e.target.value })} - className={style["time-input"]} + className={style.inputTime} />
-
- -
- {(["text", "image", "video", "link"] as ContentType[]).map((type) => ( + +
+
点赞内容类型
+
+ {(["text", "image", "video"] as ContentType[]).map((type) => ( { const newTypes = formData.contentTypes.includes(type) @@ -221,78 +236,109 @@ const NewAutoLike: React.FC = () => { ))}
-
- - + +
+
+ 启用好友标签 + + handleUpdateFormData({ enableFriendTags: checked }) + } + className={style.switch} + /> +
+ {formData.enableFriendTags && ( +
+
好友标签
+ + handleUpdateFormData({ friendTags: e.target.value }) + } + className={style.input} + /> +
只给有此标签的好友点赞
+
+ )}
-
-
); - // 步骤2:设备选择(占位) + // 步骤2:设备选择 const renderDeviceSelection = () => ( -
-
- [设备选择组件占位] -
设备选择功能开发中...
-
- 当前已选择 {formData.devices?.length || 0} 个设备 -
+
+
+
选择设备
+ message.info("这里应弹出设备选择器")} + className={style.input} + /> + {formData.devices.length > 0 && ( +
+ 已选设备: {formData.devices.length}个 +
+ )}
-
- -
); - // 步骤3:好友设置(占位) - const renderFriendSettings = () => ( -
-
- [好友选择组件占位] -
好友设置功能开发中...
-
- 当前已选择 {formData.friends?.length || 0} 个好友 -
+ // 步骤3:人群选择 + const renderFriendSelection = () => ( +
+
+
选择微信好友
+ message.info("这里应弹出好友选择器")} + className={style.input} + /> + {formData.friends.length > 0 && ( +
+ 已选好友: {formData.friends.length}个 +
+ )}
-
- @@ -321,21 +367,39 @@ const NewAutoLike: React.FC = () => { } > -
-
- {/* 步骤器保留新项目的 */} - {/* 你可以在这里插入新项目的步骤器组件 */} -
- {currentStep === 1 && renderBasicSettings()} - {currentStep === 2 && renderDeviceSelection()} - {currentStep === 3 && renderFriendSettings()} - {isLoading && ( -
- -
- )} -
+
+
+ {steps.map((s, i) => ( +
+ + {i < currentStep ? : i + 1} + + {s} +
+ ))}
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {currentStep === 0 && renderBasicSettings()} + {currentStep === 1 && renderDeviceSelection()} + {currentStep === 2 && renderFriendSelection()} + + )}
); diff --git a/nkebao/src/pages/workspace/auto-like/new/new.module.scss b/nkebao/src/pages/workspace/auto-like/new/new.module.scss index adb3aa67c..52c637810 100644 --- a/nkebao/src/pages/workspace/auto-like/new/new.module.scss +++ b/nkebao/src/pages/workspace/auto-like/new/new.module.scss @@ -1,250 +1,227 @@ -.new-page-bg { +.formBg { + background: #f8f6f3; min-height: 100vh; - background: #f8f9fa; -} - -.nav-bar { - display: flex; - align-items: center; - height: 56px; - background: #fff; - box-shadow: 0 1px 0 #f0f0f0; - padding: 0 24px; - position: sticky; - top: 0; - z-index: 10; -} - -.nav-back-btn { - border: none; - background: none; - font-size: 20px; - color: #222; - margin-right: 8px; - box-shadow: none; - padding: 0; - min-width: 32px; - min-height: 32px; - display: flex; - align-items: center; - justify-content: center; -} - -.nav-title { - font-size: 18px; - font-weight: 600; - color: #222; - margin-left: 4px; -} - -.new-page-center { + padding: 32px 0 32px 0; display: flex; flex-direction: column; align-items: center; +} + +.formSteps { + display: flex; + justify-content: center; + margin-bottom: 32px; + gap: 32px; +} + +.formStepIndicator { + display: flex; + flex-direction: column; + align-items: center; + color: #bbb; + font-size: 13px; + font-weight: 400; + transition: color 0.2s; +} + +.formStepActive { + color: #188eee; + font-weight: 600; +} + +.formStepDone { + color: #19c37d; +} + +.formStepNum { + width: 28px; + height: 28px; + border-radius: 50%; + background: #e5e7eb; + color: #888; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + margin-bottom: 4px; +} + +.formStepActive .formStepNum { + background: #188eee; + color: #fff; +} + +.formStepDone .formStepNum { + background: #19c37d; + color: #fff; +} + +.formStep { + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + padding: 32px 24px 24px 24px; + width: 100%; + max-width: 420px; + margin: 0 auto 24px auto; +} + +.formItem { + margin-bottom: 24px; +} + +.formLabel { + font-size: 15px; + color: #222; + font-weight: 500; + margin-bottom: 10px; +} + +.input { + height: 44px; + border-radius: 8px; + font-size: 15px; +} + +.timeRow { + display: flex; + align-items: center; +} + +.inputTime { + width: 90px; + height: 40px; + border-radius: 8px; + font-size: 15px; +} + +.timeTo { + margin: 0 8px; + color: #888; +} + +.counterRow { + display: flex; + align-items: center; + gap: 0; +} + +.counterBtn { + width: 40px; + height: 40px; + border-radius: 8px; + background: #fff; + border: 1px solid #e5e7eb; + font-size: 16px; + color: #188eee; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border 0.2s; +} + +.counterBtn:hover { + border: 1px solid #188eee; +} + +.counterValue { + width: 48px; + text-align: center; + font-size: 18px; + font-weight: 600; + color: #222; +} + +.counterTip { + font-size: 12px; + color: #aaa; + margin-top: 4px; +} + +.contentTypes { + display: flex; + gap: 8px; +} + +.contentTypeTag { + padding: 8px 16px; + border-radius: 6px; + background: #f5f5f5; + color: #666; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.contentTypeTag:hover { + background: #e5e7eb; +} + +.contentTypeTagActive { + padding: 8px 16px; + border-radius: 6px; + background: #e6f7ff; + color: #188eee; + border: 1px solid #91d5ff; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.switchRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.switchLabel { + font-size: 15px; + color: #222; + font-weight: 500; +} + +.switch { + margin-top: 0; +} + +.selectedTip { + font-size: 13px; + color: #888; + margin-top: 8px; +} + +.formStepBtnRow { + display: flex; + justify-content: flex-end; + gap: 12px; margin-top: 32px; } -.form-card { - background: #fff; - border-radius: 18px; - box-shadow: 0 2px 16px rgba(0,0,0,0.06); - padding: 36px 32px 32px 32px; - min-width: 340px; - max-width: 420px; - width: 100%; -} - -.form-section { - width: 100%; -} - -.form-item { - margin-bottom: 28px; - display: flex; - flex-direction: column; -} - -.form-label { - font-size: 15px; - font-weight: 500; - color: #333; - margin-bottom: 8px; -} - -.form-input { - border-radius: 10px !important; +.prevBtn { height: 44px; + border-radius: 8px; font-size: 15px; - padding-left: 14px; - background: #f8f9fa; - border: 1px solid #e5e6eb; - transition: border 0.2s; + min-width: 100px; } -.form-input:focus { - border-color: #1890ff; - background: #fff; +.nextBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; } -.stepper-group { - display: flex; - align-items: center; - gap: 12px; +.completeBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; } -.stepper-btn { - border-radius: 8px !important; - width: 36px; - height: 36px; - font-size: 18px; - background: #f5f6fa; - border: 1px solid #e5e6eb; - color: #222; +.formLoading { + min-height: 200px; display: flex; align-items: center; justify-content: center; - transition: border 0.2s; -} - -.stepper-btn:hover { - border-color: #1890ff; - color: #1890ff; -} - -.stepper-value { - font-size: 15px; - font-weight: 600; - color: #333; - min-width: 60px; - text-align: center; -} - -.time-range { - display: flex; - align-items: center; - gap: 10px; -} - -.time-input { - border-radius: 10px !important; - height: 44px; - font-size: 15px; - padding-left: 14px; - background: #f8f9fa; - border: 1px solid #e5e6eb; - width: 120px; -} - -.time-separator { - font-size: 15px; - color: #888; - font-weight: 500; -} - -.content-types { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.content-type-tag { - border-radius: 8px; - background: #f5f6fa; - color: #666; - font-size: 14px; - padding: 6px 18px; - cursor: pointer; - border: 1px solid #e5e6eb; - transition: all 0.2s; -} - -.content-type-tag-active { - border-radius: 8px; - background: #e6f4ff; - color: #1890ff; - font-size: 14px; - padding: 6px 18px; - cursor: pointer; - border: 1px solid #1890ff; - font-weight: 600; - transition: all 0.2s; -} - -.form-actions { - display: flex; - gap: 16px; - margin-top: 12px; -} - -.main-btn { - border-radius: 10px !important; - height: 44px; - font-size: 16px; - font-weight: 600; - background: #1890ff; - border: none; - box-shadow: 0 2px 8px rgba(24,144,255,0.08); - transition: background 0.2s; -} - -.main-btn:hover { - background: #1677ff; -} - -.secondary-btn { - border-radius: 10px !important; - height: 44px; - font-size: 16px; - font-weight: 600; - background: #fff; - border: 1.5px solid #e5e6eb; - color: #222; - transition: border 0.2s; -} - -.secondary-btn:hover { - border-color: #1890ff; - color: #1890ff; -} - -.placeholder-content { - text-align: center; - color: #888; - padding: 40px 0 24px 0; -} - -.placeholder-icon { - font-size: 32px; - color: #d9d9d9; - margin-bottom: 12px; - display: block; -} - -.placeholder-text { - font-size: 16px; - color: #333; - margin-bottom: 6px; -} - -.placeholder-subtext { - font-size: 14px; - color: #999; -} - -.loading { - display: flex; - align-items: center; - justify-content: center; - min-height: 120px; -} - -@media (max-width: 600px) { - .form-card { - min-width: 0; - max-width: 100vw; - padding: 18px 6px 18px 6px; - } - .new-page-center { - margin-top: 12px; - } } \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx b/nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx new file mode 100644 index 000000000..3e709ffa2 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/record/AutoLikeDetail.tsx @@ -0,0 +1,281 @@ +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { + ThumbsUp, + RefreshCw, + Search, +} from 'lucide-react'; +import { Card, } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Avatar } from '@/components/ui/avatar'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Separator } from '@/components/ui/separator'; +import Layout from '@/components/Layout'; +import PageHeader from '@/components/PageHeader'; +import { useToast } from '@/components/ui/toast'; +import '@/components/Layout.css'; +import { + fetchLikeRecords, + LikeRecord, +} from '@/api/autoLike'; + +// 格式化日期 +const formatDate = (dateString: string) => { + try { + const date = new Date(dateString); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + return dateString; + } +}; + +export default function AutoLikeDetail() { + const { id } = useParams<{ id: string }>(); + const { toast } = useToast(); + const [records, setRecords] = useState([]); + const [recordsLoading, setRecordsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + const pageSize = 10; + + useEffect(() => { + if (!id) return; + setRecordsLoading(true); + fetchLikeRecords(id, 1, pageSize) + .then(response => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(1); + }) + .catch(() => { + toast({ + title: '获取点赞记录失败', + description: '请稍后重试', + variant: 'destructive', + }); + }) + .finally(() => setRecordsLoading(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleSearch = () => { + setCurrentPage(1); + fetchLikeRecords(id!, 1, pageSize, searchTerm) + .then(response => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(1); + }) + .catch(() => { + toast({ + title: '获取点赞记录失败', + description: '请稍后重试', + variant: 'destructive', + }); + }); + }; + + const handleRefresh = () => { + fetchLikeRecords(id!, currentPage, pageSize, searchTerm) + .then(response => { + setRecords(response.list || []); + setTotal(response.total || 0); + }) + .catch(() => { + toast({ + title: '获取点赞记录失败', + description: '请稍后重试', + variant: 'destructive', + }); + }); + }; + + const handlePageChange = (newPage: number) => { + fetchLikeRecords(id!, newPage, pageSize, searchTerm) + .then(response => { + setRecords(response.list || []); + setTotal(response.total || 0); + setCurrentPage(newPage); + }) + .catch(() => { + toast({ + title: '获取点赞记录失败', + description: '请稍后重试', + variant: 'destructive', + }); + }); + }; + + return ( + + +
+
+ + setSearchTerm(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> +
+ +
+ + } + footer={ + <> + {records.length > 0 && total > pageSize && ( +
+ + + 第 {currentPage} 页,共 {Math.ceil(total / pageSize)} 页 + + +
+ )} + + + } + > +
+
+ + {recordsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, index) => ( + +
+ +
+ + +
+
+ +
+ + +
+ + +
+
+
+ ))} +
+ ) : records.length === 0 ? ( +
+ +

暂无点赞记录

+
+ ) : ( + <> + {records.map((record) => ( +
+
+
+ + {record.friendName} + +
+
+ {record.friendName} +
+
内容发布者
+
+
+ + {formatDate(record.momentTime || record.likeTime)} + +
+ +
+ {record.content && ( +

+ {record.content} +

+ )} + {Array.isArray(record.resUrls) && record.resUrls.length > 0 && ( +
+ {record.resUrls.slice(0, 9).map((image: string, idx: number) => ( +
+ {`内容图片 +
+ ))} +
+ )} +
+
+ + {record.operatorName} + +
+ + {record.operatorName} + + 点赞了这条内容 +
+
+
+ ))} + + )} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/record/index.tsx b/nkebao/src/pages/workspace/auto-like/record/index.tsx index 4d52913a7..eb47b5818 100644 --- a/nkebao/src/pages/workspace/auto-like/record/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/record/index.tsx @@ -125,9 +125,8 @@ const AutoLikeDetail: React.FC = () => { "https://api.dicebear.com/7.x/avataaars/svg?seed=fallback" } className={style["user-avatar"]} - > - - + fallback={} + />
{record.friendName} @@ -167,9 +166,8 @@ const AutoLikeDetail: React.FC = () => { "https://api.dicebear.com/7.x/avataaars/svg?seed=operator" } className={style["operator-avatar"]} - > - - + fallback={} + />
{record.operatorName} @@ -194,7 +192,7 @@ const AutoLikeDetail: React.FC = () => { } /> } - footer={} + footer={} >
{/* 任务信息卡片 */} @@ -284,7 +282,7 @@ const AutoLikeDetail: React.FC = () => { data={records} renderItem={renderRecordItem} hasMore={hasMore} - loadMore={handleLoadMore} + onLoadMore={handleLoadMore} className={style["records-list"]} /> )} From 345be377af40453a777fb66ad7dda67256535956 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: Tue, 22 Jul 2025 16:54:08 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=AD=98=E4=B8=80=E4=B8=8B=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/workspace/auto-like/list/index.tsx | 2 +- .../pages/workspace/auto-like/new/index.tsx | 303 +++++++++++------- .../workspace/auto-like/new/new.module.scss | 97 ++++-- nkebao/src/router/module/workspace.tsx | 2 +- 4 files changed, 271 insertions(+), 133 deletions(-) diff --git a/nkebao/src/pages/workspace/auto-like/list/index.tsx b/nkebao/src/pages/workspace/auto-like/list/index.tsx index f9a70a18c..e70fc26a0 100644 --- a/nkebao/src/pages/workspace/auto-like/list/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/list/index.tsx @@ -178,7 +178,7 @@ const AutoLike: React.FC = () => { // 查看任务 const handleView = (taskId: string) => { - navigate(`/workspace/auto-like/detail/${taskId}`); + navigate(`/workspace/auto-like/record/${taskId}`); }; // 复制任务 diff --git a/nkebao/src/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx index 1b8da3728..922ddd08e 100644 --- a/nkebao/src/pages/workspace/auto-like/new/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -4,6 +4,7 @@ import { PlusOutlined, MinusOutlined, ArrowLeftOutlined, + CheckOutlined, } from "@ant-design/icons"; import { Button, Input, Switch, message, Spin } from "antd"; import { NavBar } from "antd-mobile"; @@ -15,7 +16,6 @@ import { } from "./api"; import { CreateLikeTaskData, - UpdateLikeTaskData, ContentType, } from "@/pages/workspace/auto-like/record/api"; import style from "./new.module.scss"; @@ -27,6 +27,12 @@ const contentTypeLabels: Record = { link: "链接", }; +const steps = [ + { title: "基础设置", desc: "设置点赞规则" }, + { title: "设备选择", desc: "选择执行设备" }, + { title: "好友设置", desc: "选择目标人群" }, +]; + const NewAutoLike: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); @@ -122,21 +128,57 @@ const NewAutoLike: React.FC = () => { } }; + // 步骤器 + const renderStepIndicator = () => ( +
+
+ {steps.map((s, i) => ( +
+ + {i + 1 < currentStep ? : i + 1} + + {s.title} + {s.desc} +
+ ))} +
+
+
+
+
+ ); + // 步骤1:基础设置 const renderBasicSettings = () => ( -
-
- +
+
+
任务名称
handleUpdateFormData({ name: e.target.value })} - className={style["form-input"]} + className={style.input} />
-
- -
+
+
点赞间隔
+
-
- -
+
+
每日最大点赞数
+
-
- -
+
+
点赞时间范围
+
handleUpdateFormData({ startTime: e.target.value }) } - className={style["time-input"]} + className={style.inputTime} /> - + handleUpdateFormData({ endTime: e.target.value })} - className={style["time-input"]} + className={style.inputTime} />
-
- -
- {(["text", "image", "video", "link"] as ContentType[]).map((type) => ( +
+
点赞内容类型
+
+ {(["text", "image", "video"] as ContentType[]).map((type) => ( { const newTypes = formData.contentTypes.includes(type) @@ -221,121 +263,158 @@ const NewAutoLike: React.FC = () => { ))}
-
- - +
+
+ 启用好友标签 + + handleUpdateFormData({ enableFriendTags: checked }) + } + className={style.switch} + /> +
+ {formData.enableFriendTags && ( +
+
好友标签
+ + handleUpdateFormData({ friendTags: e.target.value }) + } + className={style.input} + /> +
只给有此标签的好友点赞
+
+ )}
-
- +
+
+ 自动开启 + +
); - // 步骤2:设备选择(占位) + // 步骤2:设备选择 const renderDeviceSelection = () => ( -
-
- [设备选择组件占位] -
设备选择功能开发中...
-
- 当前已选择 {formData.devices?.length || 0} 个设备 -
-
-
- - +
+
+
选择设备
+ message.info("这里应弹出设备选择器")} + className={style.input} + /> + {formData.devices.length > 0 && ( +
+ 已选设备: {formData.devices.length}个 +
+ )}
); - // 步骤3:好友设置(占位) + // 步骤3:好友设置 const renderFriendSettings = () => ( -
-
- [好友选择组件占位] -
好友设置功能开发中...
-
- 当前已选择 {formData.friends?.length || 0} 个好友 -
+
+
+
选择微信好友
+ message.info("这里应弹出好友选择器")} + className={style.input} + /> + {formData.friends.length > 0 && ( +
+ 已选好友: {formData.friends.length}个 +
+ )}
-
-
+ ); + + // 底部按钮 + const renderFooterBtn = () => ( +
+ {currentStep > 1 && ( + + )} + {currentStep < 3 && ( + + )} + {currentStep === 3 && ( -
+ )}
); return ( - navigate(-1)} - /> -
- } - > - - {isEditMode ? "编辑自动点赞" : "新建自动点赞"} - - + <> + + navigate(-1)} + /> +
+ } + > + + {isEditMode ? "编辑自动点赞" : "新建自动点赞"} + + + {renderStepIndicator()} + } + footer={renderFooterBtn()} > -
-
- {/* 步骤器保留新项目的 */} - {/* 你可以在这里插入新项目的步骤器组件 */} -
+
+ {isLoading ? ( +
+ +
+ ) : ( + <> {currentStep === 1 && renderBasicSettings()} {currentStep === 2 && renderDeviceSelection()} {currentStep === 3 && renderFriendSettings()} - {isLoading && ( -
- -
- )} -
-
+ + )}
); diff --git a/nkebao/src/pages/workspace/auto-like/new/new.module.scss b/nkebao/src/pages/workspace/auto-like/new/new.module.scss index 52c637810..89b7a2170 100644 --- a/nkebao/src/pages/workspace/auto-like/new/new.module.scss +++ b/nkebao/src/pages/workspace/auto-like/new/new.module.scss @@ -1,20 +1,25 @@ .formBg { background: #f8f6f3; min-height: 100vh; - padding: 32px 0 32px 0; - display: flex; - flex-direction: column; - align-items: center; + padding: 0 0 80px 0; + position: relative; } -.formSteps { +.stepIndicatorWrapper { + position: sticky; + top: 0; + z-index: 20; + background: #f8f6f3; + padding: 16px 0 8px 0; +} + +.stepIndicator { display: flex; justify-content: center; - margin-bottom: 32px; gap: 32px; } -.formStepIndicator { +.stepItem { display: flex; flex-direction: column; align-items: center; @@ -22,18 +27,19 @@ font-size: 13px; font-weight: 400; transition: color 0.2s; + min-width: 80px; } -.formStepActive { +.stepActive { color: #188eee; font-weight: 600; } -.formStepDone { +.stepDone { color: #19c37d; } -.formStepNum { +.stepNum { width: 28px; height: 28px; border-radius: 50%; @@ -46,24 +52,56 @@ margin-bottom: 4px; } -.formStepActive .formStepNum { +.stepActive .stepNum { background: #188eee; color: #fff; } -.formStepDone .formStepNum { +.stepDone .stepNum { background: #19c37d; color: #fff; } -.formStep { - background: #fff; - border-radius: 10px; - box-shadow: 0 2px 8px rgba(0,0,0,0.06); - padding: 32px 24px 24px 24px; +.stepTitle { + font-size: 14px; + margin-top: 2px; + font-weight: 500; +} + +.stepDesc { + font-size: 12px; + color: #888; + margin-top: 2px; + text-align: center; +} + +.stepProgressBarBg { + position: relative; + width: 80%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + margin: 12px auto 0 auto; +} + +.stepProgressBar { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: #188eee; + border-radius: 2px; + transition: width 0.3s; +} + +.basicSection { + background: none; + border-radius: 0; + box-shadow: none; + padding: 24px 16px 0 16px; width: 100%; - max-width: 420px; - margin: 0 auto 24px auto; + max-width: 600px; + margin: 0 auto; } .formItem { @@ -224,4 +262,25 @@ display: flex; align-items: center; justify-content: center; +} + +.footerBtnBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 30; + background: #fff; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + padding: 16px 24px 24px 24px; + display: flex; + justify-content: center; + gap: 16px; +} + +.prevBtn, .nextBtn, .completeBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 120px; } \ No newline at end of file diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index ab663bd37..85282d918 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -35,7 +35,7 @@ const workspaceRoutes = [ auth: true, }, { - path: "/workspace/auto-like/:id", + path: "/workspace/auto-like/record/:id", element: , auth: true, }, From 7445840fdfc6b2332705d4dafd2c0dd231605f49 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: Tue, 22 Jul 2025 19:18:05 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=E5=AD=98=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 3 +- .../components/DeviceSelectionDialog/api.ts | 10 - .../DeviceSelectionDialog/index.module.scss | 197 ---- .../DeviceSelectionDialog/index.tsx | 258 ----- nkebao/src/components/SelectionTest.tsx | 13 - .../pages/workspace/auto-like/new/index.tsx | 921 ++++++++++-------- 6 files changed, 500 insertions(+), 902 deletions(-) delete mode 100644 nkebao/src/components/DeviceSelectionDialog/api.ts delete mode 100644 nkebao/src/components/DeviceSelectionDialog/index.module.scss delete mode 100644 nkebao/src/components/DeviceSelectionDialog/index.tsx diff --git a/nkebao/.env.development b/nkebao/.env.development index da6a111b9..05c62ec44 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,5 @@ # 基础环境变量示例 -VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=http://www.yishi.com +VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts deleted file mode 100644 index 1f28ce04e..000000000 --- a/nkebao/src/components/DeviceSelectionDialog/api.ts +++ /dev/null @@ -1,10 +0,0 @@ -import request from "@/api/request"; - -// 获取设备列表 -export function getDeviceList(params: { - page: number; - limit: number; - keyword?: string; -}) { - return request("/v1/devices", params, "GET"); -} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.module.scss b/nkebao/src/components/DeviceSelectionDialog/index.module.scss deleted file mode 100644 index e0961f10c..000000000 --- a/nkebao/src/components/DeviceSelectionDialog/index.module.scss +++ /dev/null @@ -1,197 +0,0 @@ -.popupContainer { - display: flex; - flex-direction: column; - height: 100vh; - background: #fff; -} -.popupHeader { - padding: 16px; - border-bottom: 1px solid #f0f0f0; -} -.popupTitle { - font-size: 18px; - font-weight: 600; - text-align: center; -} -.popupSearchRow { - display: flex; - align-items: center; - gap: 16px; - padding: 16px; -} -.popupSearchInputWrap { - position: relative; - flex: 1; -} -.inputIcon { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - color: #bdbdbd; - z-index: 10; - font-size: 16px; -} -.popupSearchInput { - padding-left: 36px !important; - border-radius: 12px !important; - height: 44px; - font-size: 15px; - border: 1px solid #e5e6eb !important; - background: #f8f9fa; -} -.statusSelect { - width: 128px; - height: 40px; - border-radius: 8px; - border: 1px solid #e5e6eb; - font-size: 14px; - padding: 0 12px; - background: #fff; -} -.loadingIcon { - animation: spin 1s linear infinite; - font-size: 16px; -} -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} -.deviceList { - flex: 1; - overflow-y: auto; -} -.deviceListInner { - display: flex; - flex-direction: column; - gap: 12px; - padding: 16px; -} -.deviceItem { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 16px; - border-radius: 12px; - border: 1px solid #f0f0f0; - background: #fff; - cursor: pointer; - transition: background 0.2s; - &:hover { - background: #f5f6fa; - } -} -.deviceCheckbox { - margin-top: 4px; -} -.deviceInfo { - flex: 1; -} -.deviceInfoRow { - display: flex; - align-items: center; - justify-content: space-between; -} -.deviceName { - font-weight: 500; - font-size: 16px; - color: #222; -} -.statusOnline { - padding: 4px 8px; - border-radius: 12px; - background: #52c41a; - color: #fff; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; -} -.statusOffline { - padding: 4px 8px; - border-radius: 12px; - background: #e5e6eb; - color: #888; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; -} -.deviceInfoDetail { - font-size: 13px; - color: #888; - margin-top: 4px; -} -.usedInPlans { - font-size: 13px; - color: #fa8c16; - margin-top: 4px; -} -.loadingBox { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} -.loadingText { - color: #888; - font-size: 15px; -} -.emptyBox { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} -.emptyText { - color: #888; - font-size: 15px; -} -.popupFooter { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - border-top: 1px solid #f0f0f0; - background: #fff; -} -.selectedCount { - font-size: 14px; - color: #888; -} -.footerBtnGroup { - display: flex; - gap: 12px; -} -.refreshBtn { - width: 36px; - height: 36px; -} -.paginationRow { - border-top: 1px solid #f0f0f0; - padding: 16px; - display: flex; - align-items: center; - justify-content: space-between; - background: #fff; -} -.totalCount { - font-size: 14px; - color: #888; -} -.paginationControls { - display: flex; - align-items: center; - gap: 8px; -} -.pageBtn { - padding: 0 8px; - height: 32px; - min-width: 32px; - border-radius: 16px; -} -.pageInfo { - font-size: 14px; - color: #222; - margin: 0 8px; -} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx deleted file mode 100644 index 588d774a8..000000000 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Checkbox, Popup, Toast } from "antd-mobile"; -import { Input, Button } from "antd"; -import { getDeviceList } from "./api"; -import style from "./index.module.scss"; - -interface Device { - id: string; - name: string; - imei: string; - wxid: string; - status: "online" | "offline"; - usedInPlans: number; - nickname: string; -} - -interface DeviceSelectionDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - selectedDevices: string[]; - onSelect: (devices: string[]) => void; -} - -export function DeviceSelectionDialog({ - open, - onOpenChange, - selectedDevices, - onSelect, -}: DeviceSelectionDialogProps) { - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [loading, setLoading] = useState(false); - const [devices, setDevices] = useState([]); - const [currentPage, setCurrentPage] = useState(1); // 新增 - const [total, setTotal] = useState(0); // 新增 - const pageSize = 20; // 每页条数 - - // 获取设备列表,支持keyword和分页 - const fetchDevices = useCallback( - async (keyword: string = "", page: number = 1) => { - setLoading(true); - try { - const response = await getDeviceList({ - page, - limit: pageSize, - keyword: keyword.trim() || undefined, - }); - if (response && Array.isArray(response.list)) { - const convertedDevices: Device[] = response.list.map( - (serverDevice: any) => ({ - id: serverDevice.id.toString(), - name: serverDevice.memo || `设备 ${serverDevice.id}`, - imei: serverDevice.imei, - wxid: serverDevice.wechatId || "", - status: serverDevice.alive === 1 ? "online" : "offline", - usedInPlans: 0, - nickname: serverDevice.nickname || "", - }) - ); - setDevices(convertedDevices); - setTotal(response.total || 0); - } - } catch (error) { - console.error("获取设备列表失败:", error); - Toast.show({ - content: "获取设备列表失败,请检查网络连接", - position: "top", - }); - } finally { - setLoading(false); - } - }, - [] - ); - - // 打开弹窗时获取第一页 - useEffect(() => { - if (open) { - setCurrentPage(1); - fetchDevices("", 1); - } - }, [open, fetchDevices]); - - // 搜索防抖 - useEffect(() => { - if (!open) return; - const timer = setTimeout(() => { - setCurrentPage(1); - fetchDevices(searchQuery, 1); - }, 500); - return () => clearTimeout(timer); - }, [searchQuery, open, fetchDevices]); - - // 翻页时重新请求 - useEffect(() => { - if (!open) return; - fetchDevices(searchQuery, currentPage); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - - // 过滤设备(只保留状态过滤) - const filteredDevices = devices.filter((device) => { - const matchesStatus = - statusFilter === "all" || - (statusFilter === "online" && device.status === "online") || - (statusFilter === "offline" && device.status === "offline"); - return matchesStatus; - }); - - const handleDeviceSelect = (deviceId: string) => { - if (selectedDevices.includes(deviceId)) { - onSelect(selectedDevices.filter((id) => id !== deviceId)); - } else { - onSelect([...selectedDevices, deviceId]); - } - }; - - const totalPages = Math.max(1, Math.ceil(total / pageSize)); - - return ( - onOpenChange(false)} - position="bottom" - bodyStyle={{ height: "100vh" }} - > -
-
-
选择设备
-
-
-
- - setSearchQuery(val)} - className={style.popupSearchInput} - /> -
- - -
-
- {loading ? ( -
-
加载中...
-
- ) : filteredDevices.length === 0 ? ( -
-
暂无数据
-
- ) : ( -
- {filteredDevices.map((device) => ( - - ))} -
- )} -
- {/* 分页栏 */} -
-
总计 {total} 个设备
-
- - - {currentPage} / {totalPages} - - -
-
-
-
- 已选择 {selectedDevices.length} 个设备 -
-
- - -
-
-
-
- ); -} diff --git a/nkebao/src/components/SelectionTest.tsx b/nkebao/src/components/SelectionTest.tsx index 4ee4af05e..0f256264c 100644 --- a/nkebao/src/components/SelectionTest.tsx +++ b/nkebao/src/components/SelectionTest.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import DeviceSelection from "./DeviceSelection"; -import { DeviceSelectionDialog } from "./DeviceSelectionDialog"; import FriendSelection from "./FriendSelection"; import GroupSelection from "./GroupSelection"; import { Button, Space } from "antd-mobile"; @@ -29,18 +28,6 @@ export default function SelectionTest() { onSelect={setSelectedDevices} />
-
- DeviceSelectionDialog(纯弹窗) - - -
FriendSelection
-
-
-
每日最大点赞数
-
-
-
-
-
点赞时间范围
-
- - handleUpdateFormData({ startTime: e.target.value }) - } - className={style.inputTime} - /> - - handleUpdateFormData({ endTime: e.target.value })} - className={style.inputTime} - /> -
-
-
-
点赞内容类型
-
- {(["text", "image", "video"] as ContentType[]).map((type) => ( - { - const newTypes = formData.contentTypes.includes(type) - ? formData.contentTypes.filter((t) => t !== type) - : [...formData.contentTypes, type]; - handleUpdateFormData({ contentTypes: newTypes }); - }} - > - {contentTypeLabels[type]} - - ))} -
-
-
-
- 启用好友标签 - - handleUpdateFormData({ enableFriendTags: checked }) - } - className={style.switch} - /> -
- {formData.enableFriendTags && ( -
-
好友标签
- - handleUpdateFormData({ friendTags: e.target.value }) - } - className={style.input} - /> -
只给有此标签的好友点赞
-
- )} -
-
-
- 自动开启 - -
-
-
- ); - - // 步骤2:设备选择 - const renderDeviceSelection = () => ( -
-
-
选择设备
- message.info("这里应弹出设备选择器")} - className={style.input} - /> - {formData.devices.length > 0 && ( -
- 已选设备: {formData.devices.length}个 -
- )} -
-
- ); - - // 步骤3:好友设置 - const renderFriendSettings = () => ( -
-
-
选择微信好友
- message.info("这里应弹出好友选择器")} - className={style.input} - /> - {formData.friends.length > 0 && ( -
- 已选好友: {formData.friends.length}个 -
- )} -
-
- ); - - // 底部按钮 - const renderFooterBtn = () => ( -
- {currentStep > 1 && ( - - )} - {currentStep < 3 && ( - - )} - {currentStep === 3 && ( - - )} -
- ); - - return ( - - - navigate(-1)} - /> -
- } - > - - {isEditMode ? "编辑自动点赞" : "新建自动点赞"} - - - {renderStepIndicator()} - - } - footer={renderFooterBtn()} - > -
- {isLoading ? ( -
- -
- ) : ( - <> - {currentStep === 1 && renderBasicSettings()} - {currentStep === 2 && renderDeviceSelection()} - {currentStep === 3 && renderFriendSettings()} - - )} -
- - ); -}; - -export default NewAutoLike; +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + PlusOutlined, + MinusOutlined, + ArrowLeftOutlined, + CheckOutlined, +} from "@ant-design/icons"; +import { + Button, + Input, + Switch, + Spin, + Modal, + Table, + Select, + message, +} from "antd"; +import Layout from "@/components/Layout/Layout"; +import { + createAutoLikeTask, + updateAutoLikeTask, + fetchAutoLikeTaskDetail, +} from "./api"; +import { + CreateLikeTaskData, + ContentType, +} from "@/pages/workspace/auto-like/record/api"; +import style from "./new.module.scss"; + +const contentTypeLabels: Record = { + text: "文字", + image: "图片", + video: "视频", + link: "链接", +}; + +const steps = [ + { title: "基础设置", desc: "设置点赞规则" }, + { title: "设备选择", desc: "选择执行设备" }, + { title: "好友设置", desc: "选择目标人群" }, +]; + +// 假数据(实际应从接口获取) +const mockDevices = Array.from({ length: 10 }).map((_, i) => ({ + key: String(i + 1), + name: `设备${i + 1}`, + id: `dev${i + 1}`, +})); +const mockFriends = Array.from({ length: 20 }).map((_, i) => ({ + key: String(i + 1), + name: `好友${i + 1}`, + id: `friend${i + 1}`, +})); + +const NewAutoLike: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEditMode = !!id; + const [currentStep, setCurrentStep] = useState(1); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(isEditMode); + const [autoEnabled, setAutoEnabled] = useState(false); + const [formData, setFormData] = useState({ + name: "", + interval: 5, + maxLikes: 200, + startTime: "08:00", + endTime: "22:00", + contentTypes: ["text", "image", "video"], + devices: [], + friends: [], + targetTags: [], + friendMaxLikes: 10, + enableFriendTags: false, + friendTags: "", + }); + // 设备/好友选择弹窗 + const [deviceModalOpen, setDeviceModalOpen] = useState(false); + const [friendModalOpen, setFriendModalOpen] = useState(false); + const [selectedDeviceRowKeys, setSelectedDeviceRowKeys] = useState( + [] + ); + const [selectedFriendRowKeys, setSelectedFriendRowKeys] = useState( + [] + ); + + useEffect(() => { + if (isEditMode && id) { + fetchTaskDetail(); + } + }, [id, isEditMode]); + + const fetchTaskDetail = async () => { + setIsLoading(true); + try { + const taskDetail = await fetchAutoLikeTaskDetail(id!); + if (taskDetail) { + const config = (taskDetail as any).config || taskDetail; + setFormData({ + name: taskDetail.name || "", + interval: config.likeInterval || config.interval || 5, + maxLikes: config.maxLikesPerDay || config.maxLikes || 200, + startTime: config.timeRange?.start || config.startTime || "08:00", + endTime: config.timeRange?.end || config.endTime || "22:00", + contentTypes: config.contentTypes || ["text", "image", "video"], + devices: config.devices || [], + friends: config.friends || [], + targetTags: config.targetTags || [], + friendMaxLikes: config.friendMaxLikes || 10, + enableFriendTags: config.enableFriendTags || false, + friendTags: config.friendTags || "", + }); + setAutoEnabled( + (taskDetail as any).status === 1 || + (taskDetail as any).status === "running" + ); + setSelectedDeviceRowKeys(config.devices || []); + setSelectedFriendRowKeys(config.friends || []); + } + } catch (error) { + message.error("获取任务详情失败"); + navigate("/workspace/auto-like"); + } finally { + setIsLoading(false); + } + }; + + const handleUpdateFormData = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 3)); + const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 1)); + + const handleComplete = async () => { + if (!formData.name.trim()) { + message.warning("请输入任务名称"); + return; + } + if (!formData.devices || formData.devices.length === 0) { + message.warning("请选择执行设备"); + return; + } + setIsSubmitting(true); + try { + if (isEditMode && id) { + await updateAutoLikeTask({ ...formData, id }); + message.success("更新成功"); + } else { + await createAutoLikeTask(formData); + message.success("创建成功"); + } + navigate("/workspace/auto-like"); + } catch (error) { + message.error(isEditMode ? "更新失败" : "创建失败"); + } finally { + setIsSubmitting(false); + } + }; + + // 步骤器 + const renderStepIndicator = () => ( +
+
+ {steps.map((s, i) => ( +
+ + {i + 1 < currentStep ? : i + 1} + + {s.title} + {s.desc} +
+ ))} +
+
+
+
+
+ ); + + // 步骤1:基础设置 + const renderBasicSettings = () => ( +
+
+
任务名称
+ handleUpdateFormData({ name: e.target.value })} + className={style.input} + /> +
+
+
点赞间隔
+
+
+
+
+
每日最大点赞数
+
+
+
+
+
点赞时间范围
+
+ + handleUpdateFormData({ startTime: e.target.value }) + } + className={style.inputTime} + /> + + handleUpdateFormData({ endTime: e.target.value })} + className={style.inputTime} + /> +
+
+
+
点赞内容类型
+ +
+
+
+ 启用好友标签 + + handleUpdateFormData({ enableFriendTags: checked }) + } + className={style.switch} + /> +
+ {formData.enableFriendTags && ( +
+
好友标签
+ + handleUpdateFormData({ friendTags: e.target.value }) + } + className={style.input} + /> +
只给有此标签的好友点赞
+
+ )} +
+
+
+ 自动开启 + +
+
+
+ ); + + // 步骤2:设备选择 + const renderDeviceSelection = () => ( +
+
+
选择设备
+ +
+ {formData.devices.length > 0 + ? `已选设备: ${formData.devices.length} 个` + : "未选择设备"} +
+
+ setDeviceModalOpen(false)} + onOk={() => { + handleUpdateFormData({ devices: selectedDeviceRowKeys }); + setDeviceModalOpen(false); + }} + okText="确定" + cancelText="取消" + width={520} + > + setSelectedDeviceRowKeys(keys as string[]), + }} + columns={[ + { title: "设备名称", dataIndex: "name", key: "name" }, + { title: "ID", dataIndex: "id", key: "id" }, + ]} + dataSource={mockDevices} + pagination={false} + rowKey="id" + size="small" + /> + + + ); + + // 步骤3:好友设置 + const renderFriendSettings = () => ( +
+
+
选择微信好友
+ +
+ {formData.friends.length > 0 + ? `已选好友: ${formData.friends.length} 个` + : "未选择好友"} +
+
+ setFriendModalOpen(false)} + onOk={() => { + handleUpdateFormData({ friends: selectedFriendRowKeys }); + setFriendModalOpen(false); + }} + okText="确定" + cancelText="取消" + width={520} + > +
setSelectedFriendRowKeys(keys as string[]), + }} + columns={[ + { title: "好友昵称", dataIndex: "name", key: "name" }, + { title: "ID", dataIndex: "id", key: "id" }, + ]} + dataSource={mockFriends} + pagination={false} + rowKey="id" + size="small" + /> + + + ); + + // 底部按钮 + const renderFooterBtn = () => ( +
+ {currentStep > 1 && ( + + )} + {currentStep < 3 && ( + + )} + {currentStep === 3 && ( + + )} +
+ ); + + return ( + +
+
+ {renderStepIndicator()} + + } + footer={renderFooterBtn()} + > +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {currentStep === 1 && renderBasicSettings()} + {currentStep === 2 && renderDeviceSelection()} + {currentStep === 3 && renderFriendSettings()} + + )} +
+
+ ); +}; + +export default NewAutoLike; From 8bd1b965d6645009d704ea04ed6980524d3e82a2 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:26 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E7=BE=A4=E7=BB=84=E6=8E=A8=E9=80=81=EF=BC=8C=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=90=8C=E6=AD=A5=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- .../pages/workspace/auto-like/new/index.tsx | 300 ++++----- .../workspace/auto-like/new/new.module.scss | 605 +++++++++--------- 3 files changed, 453 insertions(+), 454 deletions(-) diff --git a/nkebao/.env.development b/nkebao/.env.development index f37023e8b..fde602957 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/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx index 276cf0105..048eefd0f 100644 --- a/nkebao/src/pages/workspace/auto-like/new/index.tsx +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -6,17 +6,10 @@ import { ArrowLeftOutlined, CheckOutlined, } from "@ant-design/icons"; -import { - Button, - Input, - Switch, - Spin, - Modal, - Table, - Select, - message, -} from "antd"; +import { Button, Input, Switch, Spin, message } from "antd"; import Layout from "@/components/Layout/Layout"; +import DeviceSelection from "@/components/DeviceSelection"; +import FriendSelection from "@/components/FriendSelection"; import { createAutoLikeTask, updateAutoLikeTask, @@ -27,32 +20,20 @@ import { ContentType, } from "@/pages/workspace/auto-like/record/api"; import style from "./new.module.scss"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; const contentTypeLabels: Record = { text: "文字", image: "图片", video: "视频", - link: "链接", }; const steps = [ { title: "基础设置", desc: "设置点赞规则" }, { title: "设备选择", desc: "选择执行设备" }, - { title: "好友设置", desc: "选择目标人群" }, + { title: "人群选择", desc: "选择目标人群" }, ]; -// 假数据(实际应从接口获取) -const mockDevices = Array.from({ length: 10 }).map((_, i) => ({ - key: String(i + 1), - name: `设备${i + 1}`, - id: `dev${i + 1}`, -})); -const mockFriends = Array.from({ length: 20 }).map((_, i) => ({ - key: String(i + 1), - name: `好友${i + 1}`, - id: `friend${i + 1}`, -})); - const NewAutoLike: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); @@ -75,15 +56,6 @@ const NewAutoLike: React.FC = () => { enableFriendTags: false, friendTags: "", }); - // 设备/好友选择弹窗 - const [deviceModalOpen, setDeviceModalOpen] = useState(false); - const [friendModalOpen, setFriendModalOpen] = useState(false); - const [selectedDeviceRowKeys, setSelectedDeviceRowKeys] = useState( - [] - ); - const [selectedFriendRowKeys, setSelectedFriendRowKeys] = useState( - [] - ); useEffect(() => { if (isEditMode && id) { @@ -115,8 +87,6 @@ const NewAutoLike: React.FC = () => { (taskDetail as any).status === 1 || (taskDetail as any).status === "running" ); - setSelectedDeviceRowKeys(config.devices || []); - setSelectedFriendRowKeys(config.friends || []); } } catch (error) { message.error("获取任务详情失败"); @@ -130,8 +100,23 @@ const NewAutoLike: React.FC = () => { setFormData((prev) => ({ ...prev, ...data })); }; - const handleNext = () => setCurrentStep((prev) => Math.min(prev + 1, 3)); - const handlePrev = () => setCurrentStep((prev) => Math.max(prev - 1, 1)); + const handleNext = () => { + setCurrentStep((prev) => Math.min(prev + 1, 3)); + // 滚动到顶部 + const mainElement = document.querySelector("main"); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: "smooth" }); + } + }; + + const handlePrev = () => { + setCurrentStep((prev) => Math.max(prev - 1, 1)); + // 滚动到顶部 + const mainElement = document.querySelector("main"); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: "smooth" }); + } + }; const handleComplete = async () => { if (!formData.name.trim()) { @@ -219,7 +204,21 @@ const NewAutoLike: React.FC = () => { } className={style.counterBtn} /> - {formData.interval} 秒 +
+ + handleUpdateFormData({ + interval: Number.parseInt(e.target.value) || 1, + }) + } + className={style.counterInput} + /> + +
); @@ -330,48 +363,31 @@ const NewAutoLike: React.FC = () => { const renderDeviceSelection = () => (
-
选择设备
- -
- {formData.devices.length > 0 - ? `已选设备: ${formData.devices.length} 个` - : "未选择设备"} -
-
- setDeviceModalOpen(false)} - onOk={() => { - handleUpdateFormData({ devices: selectedDeviceRowKeys }); - setDeviceModalOpen(false); - }} - okText="确定" - cancelText="取消" - width={520} - > -
setSelectedDeviceRowKeys(keys as string[]), - }} - columns={[ - { title: "设备名称", dataIndex: "name", key: "name" }, - { title: "ID", dataIndex: "id", key: "id" }, - ]} - dataSource={mockDevices} - pagination={false} - rowKey="id" - size="small" + handleUpdateFormData({ devices })} + mode="dialog" + showInput={true} + showSelectedList={true} /> - + + + ); @@ -379,82 +395,30 @@ const NewAutoLike: React.FC = () => { const renderFriendSettings = () => (
-
选择微信好友
- -
- {formData.friends.length > 0 - ? `已选好友: ${formData.friends.length} 个` - : "未选择好友"} -
-
- setFriendModalOpen(false)} - onOk={() => { - handleUpdateFormData({ friends: selectedFriendRowKeys }); - setFriendModalOpen(false); - }} - okText="确定" - cancelText="取消" - width={520} - > -
setSelectedFriendRowKeys(keys as string[]), - }} - columns={[ - { title: "好友昵称", dataIndex: "name", key: "name" }, - { title: "ID", dataIndex: "id", key: "id" }, - ]} - dataSource={mockFriends} - pagination={false} - rowKey="id" - size="small" + handleUpdateFormData({ friends })} + deviceIds={formData.devices} /> - - - ); - - // 底部按钮 - const renderFooterBtn = () => ( -
- {currentStep > 1 && ( - - )} - {currentStep < 3 && ( - - )} - {currentStep === 3 && ( - - )} +
+ + ); @@ -476,7 +440,7 @@ const NewAutoLike: React.FC = () => { {renderStepIndicator()} } - footer={renderFooterBtn()} + footer={} >
{isLoading ? ( diff --git a/nkebao/src/pages/workspace/auto-like/new/new.module.scss b/nkebao/src/pages/workspace/auto-like/new/new.module.scss index 89b7a2170..630babd45 100644 --- a/nkebao/src/pages/workspace/auto-like/new/new.module.scss +++ b/nkebao/src/pages/workspace/auto-like/new/new.module.scss @@ -1,286 +1,321 @@ -.formBg { - background: #f8f6f3; - min-height: 100vh; - padding: 0 0 80px 0; - position: relative; -} - -.stepIndicatorWrapper { - position: sticky; - top: 0; - z-index: 20; - background: #f8f6f3; - padding: 16px 0 8px 0; -} - -.stepIndicator { - display: flex; - justify-content: center; - gap: 32px; -} - -.stepItem { - display: flex; - flex-direction: column; - align-items: center; - color: #bbb; - font-size: 13px; - font-weight: 400; - transition: color 0.2s; - min-width: 80px; -} - -.stepActive { - color: #188eee; - font-weight: 600; -} - -.stepDone { - color: #19c37d; -} - -.stepNum { - width: 28px; - height: 28px; - border-radius: 50%; - background: #e5e7eb; - color: #888; - display: flex; - align-items: center; - justify-content: center; - font-size: 15px; - margin-bottom: 4px; -} - -.stepActive .stepNum { - background: #188eee; - color: #fff; -} - -.stepDone .stepNum { - background: #19c37d; - color: #fff; -} - -.stepTitle { - font-size: 14px; - margin-top: 2px; - font-weight: 500; -} - -.stepDesc { - font-size: 12px; - color: #888; - margin-top: 2px; - text-align: center; -} - -.stepProgressBarBg { - position: relative; - width: 80%; - height: 4px; - background: #e5e7eb; - border-radius: 2px; - margin: 12px auto 0 auto; -} - -.stepProgressBar { - position: absolute; - left: 0; - top: 0; - height: 100%; - background: #188eee; - border-radius: 2px; - transition: width 0.3s; -} - -.basicSection { - background: none; - border-radius: 0; - box-shadow: none; - padding: 24px 16px 0 16px; - width: 100%; - max-width: 600px; - margin: 0 auto; -} - -.formItem { - margin-bottom: 24px; -} - -.formLabel { - font-size: 15px; - color: #222; - font-weight: 500; - margin-bottom: 10px; -} - -.input { - height: 44px; - border-radius: 8px; - font-size: 15px; -} - -.timeRow { - display: flex; - align-items: center; -} - -.inputTime { - width: 90px; - height: 40px; - border-radius: 8px; - font-size: 15px; -} - -.timeTo { - margin: 0 8px; - color: #888; -} - -.counterRow { - display: flex; - align-items: center; - gap: 0; -} - -.counterBtn { - width: 40px; - height: 40px; - border-radius: 8px; - background: #fff; - border: 1px solid #e5e7eb; - font-size: 16px; - color: #188eee; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: border 0.2s; -} - -.counterBtn:hover { - border: 1px solid #188eee; -} - -.counterValue { - width: 48px; - text-align: center; - font-size: 18px; - font-weight: 600; - color: #222; -} - -.counterTip { - font-size: 12px; - color: #aaa; - margin-top: 4px; -} - -.contentTypes { - display: flex; - gap: 8px; -} - -.contentTypeTag { - padding: 8px 16px; - border-radius: 6px; - background: #f5f5f5; - color: #666; - font-size: 14px; - cursor: pointer; - transition: all 0.2s; -} - -.contentTypeTag:hover { - background: #e5e7eb; -} - -.contentTypeTagActive { - padding: 8px 16px; - border-radius: 6px; - background: #e6f7ff; - color: #188eee; - border: 1px solid #91d5ff; - font-size: 14px; - cursor: pointer; - transition: all 0.2s; -} - -.switchRow { - display: flex; - align-items: center; - justify-content: space-between; -} - -.switchLabel { - font-size: 15px; - color: #222; - font-weight: 500; -} - -.switch { - margin-top: 0; -} - -.selectedTip { - font-size: 13px; - color: #888; - margin-top: 8px; -} - -.formStepBtnRow { - display: flex; - justify-content: flex-end; - gap: 12px; - margin-top: 32px; -} - -.prevBtn { - height: 44px; - border-radius: 8px; - font-size: 15px; - min-width: 100px; -} - -.nextBtn { - height: 44px; - border-radius: 8px; - font-size: 15px; - min-width: 100px; -} - -.completeBtn { - height: 44px; - border-radius: 8px; - font-size: 15px; - min-width: 100px; -} - -.formLoading { - min-height: 200px; - display: flex; - align-items: center; - justify-content: center; -} - -.footerBtnBar { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 30; - background: #fff; - box-shadow: 0 -2px 8px rgba(0,0,0,0.04); - padding: 16px 24px 24px 24px; - display: flex; - justify-content: center; - gap: 16px; -} - -.prevBtn, .nextBtn, .completeBtn { - height: 44px; - border-radius: 8px; - font-size: 15px; - min-width: 120px; +.formBg { + background: #f8f6f3; + min-height: 100vh; + padding: 0 0 80px 0; + position: relative; +} + +.stepIndicatorWrapper { + position: sticky; + top: 0; + z-index: 20; + background: #f8f6f3; + padding: 16px 0 8px 0; +} + +.stepIndicator { + display: flex; + justify-content: center; + gap: 32px; +} + +.stepItem { + display: flex; + flex-direction: column; + align-items: center; + color: #bbb; + font-size: 13px; + font-weight: 400; + transition: color 0.2s; + min-width: 80px; +} + +.stepActive { + color: #188eee; + font-weight: 600; +} + +.stepDone { + color: #19c37d; +} + +.stepNum { + width: 28px; + height: 28px; + border-radius: 50%; + background: #e5e7eb; + color: #888; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + margin-bottom: 4px; +} + +.stepActive .stepNum { + background: #188eee; + color: #fff; +} + +.stepDone .stepNum { + background: #19c37d; + color: #fff; +} + +.stepTitle { + font-size: 14px; + margin-top: 2px; + font-weight: 500; +} + +.stepDesc { + font-size: 12px; + color: #888; + margin-top: 2px; + text-align: center; +} + +.stepProgressBarBg { + position: relative; + width: 80%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + margin: 12px auto 0 auto; +} + +.stepProgressBar { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: #188eee; + border-radius: 2px; + transition: width 0.3s; +} + +.basicSection { + background: none; + border-radius: 0; + box-shadow: none; + padding: 24px 16px 0 16px; + width: 100%; + max-width: 600px; + margin: 0 auto; +} + +.formItem { + margin-bottom: 24px; +} + +.formLabel { + font-size: 15px; + color: #222; + font-weight: 500; + margin-bottom: 10px; +} + +.input { + height: 44px; + border-radius: 8px; + font-size: 15px; +} + +.timeRow { + display: flex; + align-items: center; +} + +.inputTime { + width: 90px; + height: 40px; + border-radius: 8px; + font-size: 15px; +} + +.timeTo { + margin: 0 8px; + color: #888; +} + +.counterRow { + display: flex; + align-items: center; + gap: 0; +} + +.counterBtn { + width: 40px; + height: 40px; + border-radius: 8px; + background: #fff; + border: 1px solid #e5e7eb; + font-size: 16px; + color: #188eee; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border 0.2s; +} + +.counterBtn:hover { + border: 1px solid #188eee; +} + +.counterValue { + width: 48px; + text-align: center; + font-size: 18px; + font-weight: 600; + color: #222; +} + +.counterInputWrapper { + position: relative; + width: 80px; + display: flex; + align-items: center; +} + +.counterInput { + width: 100%; + height: 40px; + border-radius: 0; + border: 1px solid #e5e7eb; + border-left: none; + border-right: none; + text-align: center; + font-size: 16px; + font-weight: 600; + color: #222; + padding: 0 8px; +} + +.counterUnit { + position: absolute; + right: 8px; + color: #888; + font-size: 14px; + pointer-events: none; +} + +.timeSeparator { + margin: 0 8px; + color: #888; + font-size: 14px; +} + +.counterTip { + font-size: 12px; + color: #aaa; + margin-top: 4px; +} + +.contentTypes { + display: flex; + gap: 8px; +} + +.contentTypeTag { + padding: 8px 16px; + border-radius: 6px; + background: #f5f5f5; + color: #666; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.contentTypeTag:hover { + background: #e5e7eb; +} + +.contentTypeTagActive { + padding: 8px 16px; + border-radius: 6px; + background: #e6f7ff; + color: #188eee; + border: 1px solid #91d5ff; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.switchRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.switchLabel { + font-size: 15px; + color: #222; + font-weight: 500; +} + +.switch { + margin-top: 0; +} + +.selectedTip { + font-size: 13px; + color: #888; + margin-top: 8px; +} + +.formStepBtnRow { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 32px; +} + +.prevBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; +} + +.nextBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; +} + +.completeBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 100px; +} + +.formLoading { + min-height: 200px; + display: flex; + align-items: center; + justify-content: center; +} + +.footerBtnBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 30; + background: #fff; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + padding: 16px 24px 24px 24px; + display: flex; + justify-content: center; + gap: 16px; +} + +.prevBtn, .nextBtn, .completeBtn { + height: 44px; + border-radius: 8px; + font-size: 15px; + min-width: 120px; } \ No newline at end of file