From a3a2662eb173792fd55dbc326e71858e1482ec82 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 22 Jul 2025 11:04:02 +0800 Subject: [PATCH 01/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E4=B8=8D=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index bc18298c..ba111c73 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -787,6 +787,7 @@ class Adapter implements WeChatServiceInterface FROM s2_wechat_friend f LEFT JOIN s2_wechat_account a on a.id = f.wechatAccountId LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY f.id DESC LIMIT ?, ? ON DUPLICATE KEY UPDATE id=VALUES(id), 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 02/28] =?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 0519ecba..00000000 --- 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 0519ecba..00000000 --- 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 00000000..7c52c583 --- /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 b0a13181..b530e45d 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 adb3aa67..52c63781 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 00000000..3e709ffa --- /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 4d52913a..eb47b581 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 34df010769820acd474d72ca6e04d4a05fc03a21 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:57:24 +0800 Subject: [PATCH 03/28] =?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=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/DeviceSelection/api.ts | 10 ++++++++++ nkebao/src/components/DeviceSelection/index.tsx | 16 ++++++---------- .../src/components/DeviceSelectionDialog/api.ts | 10 ++++++++++ .../components/DeviceSelectionDialog/index.tsx | 16 ++++++---------- nkebao/src/components/FriendSelection/api.ts | 11 +++++++++++ nkebao/src/components/FriendSelection/index.tsx | 4 ++-- nkebao/src/components/GroupSelection/api.ts | 10 ++++++++++ nkebao/src/components/GroupSelection/index.tsx | 4 ++-- nkebao/src/vite-env.d.ts | 10 ++++++++++ 9 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 nkebao/src/components/DeviceSelection/api.ts create mode 100644 nkebao/src/components/DeviceSelectionDialog/api.ts create mode 100644 nkebao/src/components/FriendSelection/api.ts create mode 100644 nkebao/src/components/GroupSelection/api.ts diff --git a/nkebao/src/components/DeviceSelection/api.ts b/nkebao/src/components/DeviceSelection/api.ts new file mode 100644 index 00000000..90572df2 --- /dev/null +++ b/nkebao/src/components/DeviceSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取设备列表 +export function getDeviceList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/device/list", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 67d5072c..673a8c28 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getDeviceList } from "./api"; import style from "./module.scss"; // 设备选择项接口 @@ -37,15 +37,11 @@ export default function DeviceSelection({ const fetchDevices = async (keyword: string = "") => { setLoading(true); try { - const res = await request( - "/v1/device/list", - { - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }, - "GET" - ); + const res = await getDeviceList({ + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }); if (res && Array.isArray(res.list)) { setDevices( diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts new file mode 100644 index 00000000..90572df2 --- /dev/null +++ b/nkebao/src/components/DeviceSelectionDialog/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取设备列表 +export function getDeviceList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/device/list", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx index 4e741e15..5e24cad9 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getDeviceList } from "./api"; import style from "./module.scss"; interface Device { @@ -36,15 +36,11 @@ export function DeviceSelectionDialog({ const fetchDevices = useCallback(async (keyword: string = "") => { setLoading(true); try { - const response = await request( - "/v1/device/list", - { - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }, - "GET" - ); + const response = await getDeviceList({ + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }); if (response && Array.isArray(response.list)) { // 转换服务端数据格式为组件需要的格式 diff --git a/nkebao/src/components/FriendSelection/api.ts b/nkebao/src/components/FriendSelection/api.ts new file mode 100644 index 00000000..f94ab93b --- /dev/null +++ b/nkebao/src/components/FriendSelection/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 获取好友列表 +export function getFriendList(params: { + page: number; + limit: number; + deviceIds?: string; + keyword?: string; +}) { + return request("/v1/friend", params, "GET"); +} diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index 43f37ce5..4bdbd12d 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -6,7 +6,7 @@ import { RightOutlined, } from "@ant-design/icons"; import { Input, Button, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getFriendList } from "./api"; import style from "./module.scss"; // 微信好友接口类型 @@ -97,7 +97,7 @@ export default function FriendSelection({ params.deviceIds = deviceIds.join(","); } - const res = await request("/v1/friend", params, "GET"); + const res = await getFriendList(params); if (res && Array.isArray(res.list)) { setFriends( diff --git a/nkebao/src/components/GroupSelection/api.ts b/nkebao/src/components/GroupSelection/api.ts new file mode 100644 index 00000000..af1ea70c --- /dev/null +++ b/nkebao/src/components/GroupSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取群组列表 +export function getGroupList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/chatroom", params, "GET"); +} diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx index 5e871e86..e2d3b4fe 100644 --- a/nkebao/src/components/GroupSelection/index.tsx +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -6,7 +6,7 @@ import { RightOutlined, } from "@ant-design/icons"; import { Input, Button, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getGroupList } from "./api"; import style from "./module.scss"; // 群组接口类型 @@ -81,7 +81,7 @@ export default function GroupSelection({ params.keyword = keyword.trim(); } - const res = await request("/v1/chatroom", params, "GET"); + const res = await getGroupList(params); if (res && Array.isArray(res.list)) { setGroups( diff --git a/nkebao/src/vite-env.d.ts b/nkebao/src/vite-env.d.ts index 7d0ff9ef..422e41c2 100644 --- a/nkebao/src/vite-env.d.ts +++ b/nkebao/src/vite-env.d.ts @@ -1 +1,11 @@ /// + +declare module "*.scss" { + const content: { [className: string]: string }; + export default content; +} + +declare module "*.css" { + const content: { [className: string]: string }; + export default content; +} From 3a879c5ce2f02cb876ad49c2fe7eaf4f39e44022 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 22 Jul 2025 14:10:10 +0800 Subject: [PATCH 04/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E4=B8=8D=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/command/SyncWechatDataToCkbTask.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Server/application/command/SyncWechatDataToCkbTask.php b/Server/application/command/SyncWechatDataToCkbTask.php index d7842981..9980a630 100644 --- a/Server/application/command/SyncWechatDataToCkbTask.php +++ b/Server/application/command/SyncWechatDataToCkbTask.php @@ -51,6 +51,7 @@ class SyncWechatDataToCkbTask extends Command $this->syncWechatDeviceLoginLog($ChuKeBaoAdapter); $this->syncWechatDevice($ChuKeBaoAdapter); $this->syncWechatCustomer($ChuKeBaoAdapter); + $this->syncWechatFriendToTrafficPoolBatch($ChuKeBaoAdapter); $output->writeln("同步任务 sync_wechat_to_ckb 已结束"); return true; @@ -90,4 +91,9 @@ class SyncWechatDataToCkbTask extends Command { return $ChuKeBaoAdapter->syncWechatCustomer(); } + + protected function syncWechatFriendToTrafficPoolBatch(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatFriendToTrafficPoolBatch(); + } } \ No newline at end of file From f33bdf42e252131f3e08f8aec73403d94c8b7241 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 14:37:57 +0800 Subject: [PATCH 05/28] =?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=8A=9F=E8=83=BD=E3=80=81=E5=92=8C=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/request.ts | 44 +++++----- nkebao/src/components/DeviceSelection/api.ts | 2 +- .../{module.scss => index.module.scss} | 2 +- .../src/components/DeviceSelection/index.tsx | 36 ++++++--- .../components/DeviceSelectionDialog/api.ts | 2 +- .../{module.scss => index.module.scss} | 7 +- .../DeviceSelectionDialog/index.tsx | 20 ++--- nkebao/src/components/FriendSelection/api.ts | 2 +- .../{module.scss => index.module.scss} | 2 +- .../src/components/FriendSelection/index.tsx | 36 ++++++--- .../{module.scss => index.module.scss} | 2 +- .../src/components/GroupSelection/index.tsx | 38 ++++++--- nkebao/src/components/SelectionTest.tsx | 80 +++++++++++++++++++ nkebao/src/router/module/other.tsx | 6 ++ 14 files changed, 205 insertions(+), 74 deletions(-) rename nkebao/src/components/DeviceSelection/{module.scss => index.module.scss} (93%) rename nkebao/src/components/DeviceSelectionDialog/{module.scss => index.module.scss} (91%) rename nkebao/src/components/FriendSelection/{module.scss => index.module.scss} (93%) rename nkebao/src/components/GroupSelection/{module.scss => index.module.scss} (93%) create mode 100644 nkebao/src/components/SelectionTest.tsx diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 2c5372bf..b52caa9c 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,22 +1,27 @@ -import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios'; -import { Toast } from 'antd-mobile'; +import axios, { + AxiosInstance, + AxiosRequestConfig, + Method, + AxiosResponse, +} from "axios"; +import { Toast } from "antd-mobile"; const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); const instance: AxiosInstance = axios.create({ - baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api', + baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", timeout: 10000, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); -instance.interceptors.request.use(config => { - const token = localStorage.getItem('token'); +instance.interceptors.request.use((config) => { + const token = localStorage.getItem("token"); if (token) { config.headers = config.headers || {}; - config.headers['Authorization'] = `Bearer ${token}`; + config.headers["Authorization"] = `Bearer ${token}`; } return config; }); @@ -27,20 +32,20 @@ instance.interceptors.response.use( if (code === 200 || success) { return res.data.data ?? res.data; } - Toast.show({ content: msg || '接口错误', position: 'top' }); + Toast.show({ content: msg || "接口错误", position: "top" }); if (code === 401) { - localStorage.removeItem('token'); + localStorage.removeItem("token"); const currentPath = window.location.pathname + window.location.search; - if (currentPath === '/login') { - window.location.href = '/login'; + if (currentPath === "/login") { + window.location.href = "/login"; } else { window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; } } - return Promise.reject(msg || '接口错误'); + return Promise.reject(msg || "接口错误"); }, - err => { - Toast.show({ content: err.message || '网络异常', position: 'top' }); + (err) => { + Toast.show({ content: err.message || "网络异常", position: "top" }); return Promise.reject(err); } ); @@ -48,17 +53,18 @@ instance.interceptors.response.use( export function request( url: string, data?: any, - method: Method = 'GET', + method: Method = "GET", config?: AxiosRequestConfig, debounceGap?: number ): Promise { - const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP; + const gap = + typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; const key = `${method}_${url}_${JSON.stringify(data)}`; const now = Date.now(); const last = debounceMap.get(key) || 0; if (gap > 0 && now - last < gap) { - Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); - return Promise.reject('请求过于频繁,请稍后再试'); + // Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); + return Promise.reject("请求过于频繁,请稍后再试"); } debounceMap.set(key, now); @@ -67,7 +73,7 @@ export function request( method, ...config, }; - if (method.toUpperCase() === 'GET') { + if (method.toUpperCase() === "GET") { axiosConfig.params = data; } else { axiosConfig.data = data; diff --git a/nkebao/src/components/DeviceSelection/api.ts b/nkebao/src/components/DeviceSelection/api.ts index 90572df2..1f28ce04 100644 --- a/nkebao/src/components/DeviceSelection/api.ts +++ b/nkebao/src/components/DeviceSelection/api.ts @@ -6,5 +6,5 @@ export function getDeviceList(params: { limit: number; keyword?: string; }) { - return request("/v1/device/list", params, "GET"); + return request("/v1/devices", params, "GET"); } diff --git a/nkebao/src/components/DeviceSelection/module.scss b/nkebao/src/components/DeviceSelection/index.module.scss similarity index 93% rename from nkebao/src/components/DeviceSelection/module.scss rename to nkebao/src/components/DeviceSelection/index.module.scss index 34510d81..89a396c9 100644 --- a/nkebao/src/components/DeviceSelection/module.scss +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -22,7 +22,7 @@ .popupContainer { display: flex; flex-direction: column; - height: 100%; + height: 100vh; background: #fff; } .popupHeader { diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 673a8c28..b4a63ac7 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; import { getDeviceList } from "./api"; -import style from "./module.scss"; +import style from "./index.module.scss"; // 设备选择项接口 interface DeviceSelectionItem { @@ -56,7 +57,6 @@ export default function DeviceSelection({ } } catch (error) { console.error("获取设备列表失败:", error); - Toast.show({ content: "获取设备列表失败", position: "top" }); } finally { setLoading(false); } @@ -106,13 +106,13 @@ export default function DeviceSelection({ <> {/* 输入框 */}
- } + allowClear + size="large" />
@@ -121,7 +121,7 @@ export default function DeviceSelection({ visible={popupVisible} onMaskClick={() => setPopupVisible(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -129,12 +129,13 @@ export default function DeviceSelection({
- setSearchQuery(val)} - className={style.popupSearchInput} + onChange={(e) => setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" />
+
{loading ? ( diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts index 90572df2..1f28ce04 100644 --- a/nkebao/src/components/DeviceSelectionDialog/api.ts +++ b/nkebao/src/components/DeviceSelectionDialog/api.ts @@ -6,5 +6,5 @@ export function getDeviceList(params: { limit: number; keyword?: string; }) { - return request("/v1/device/list", params, "GET"); + return request("/v1/devices", params, "GET"); } diff --git a/nkebao/src/components/DeviceSelectionDialog/module.scss b/nkebao/src/components/DeviceSelectionDialog/index.module.scss similarity index 91% rename from nkebao/src/components/DeviceSelectionDialog/module.scss rename to nkebao/src/components/DeviceSelectionDialog/index.module.scss index 2a9cd2de..bfde5d72 100644 --- a/nkebao/src/components/DeviceSelectionDialog/module.scss +++ b/nkebao/src/components/DeviceSelectionDialog/index.module.scss @@ -1,7 +1,7 @@ .popupContainer { display: flex; flex-direction: column; - height: 100%; + height: 100vh; background: #fff; } .popupHeader { @@ -49,11 +49,6 @@ padding: 0 12px; background: #fff; } -.refreshBtn { - min-width: 40px; - height: 40px; - border-radius: 8px; -} .loadingIcon { animation: spin 1s linear infinite; font-size: 16px; diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx index 5e24cad9..1c9ba4ae 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useCallback } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; import { getDeviceList } from "./api"; -import style from "./module.scss"; +import style from "./index.module.scss"; interface Device { id: string; @@ -106,7 +107,7 @@ export function DeviceSelectionDialog({ visible={open} onMaskClick={() => onOpenChange(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -114,12 +115,13 @@ export function DeviceSelectionDialog({
- setSearchQuery(val)} - className={style.popupSearchInput} + onChange={(e) => setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" />
+ +
+
+ FriendSelection + + +
+
+ GroupSelection + + +
+ +
+
已选设备ID: {selectedDevices.join(", ")}
+
已选好友ID: {selectedFriends.join(", ")}
+
已选群组ID: {selectedGroups.join(", ")}
+
+
+ ); +} diff --git a/nkebao/src/router/module/other.tsx b/nkebao/src/router/module/other.tsx index a458b7dc..f222072c 100644 --- a/nkebao/src/router/module/other.tsx +++ b/nkebao/src/router/module/other.tsx @@ -3,6 +3,7 @@ import Plans from "@/pages/plans/Plans"; import PlanDetail from "@/pages/plans/PlanDetail"; import Orders from "@/pages/orders/Orders"; import ContactImport from "@/pages/contact-import/ContactImport"; +import SelectionTest from "@/components/SelectionTest"; const otherRoutes = [ { @@ -35,6 +36,11 @@ const otherRoutes = [ element: , auth: true, }, + { + path: "/selection-test", + element: , + auth: false, + }, ]; export default otherRoutes; From 40c53c2cfe6fa34093da5a9eb9fded0ac0711272 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 14:49:39 +0800 Subject: [PATCH 06/28] =?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=81=9A=E5=A5=BD=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSelection/index.module.scss | 32 +++++ .../src/components/DeviceSelection/index.tsx | 80 +++++++---- .../DeviceSelectionDialog/index.module.scss | 32 +++++ .../DeviceSelectionDialog/index.tsx | 136 ++++++++++++------ 4 files changed, 209 insertions(+), 71 deletions(-) diff --git a/nkebao/src/components/DeviceSelection/index.module.scss b/nkebao/src/components/DeviceSelection/index.module.scss index 89a396c9..d457fa24 100644 --- a/nkebao/src/components/DeviceSelection/index.module.scss +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -154,3 +154,35 @@ 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/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index b4a63ac7..b59e8104 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -33,17 +33,19 @@ export default function DeviceSelection({ const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); // 新增 + const [total, setTotal] = useState(0); // 新增 + const pageSize = 20; // 每页条数 - // 获取设备列表,支持keyword - const fetchDevices = async (keyword: string = "") => { + // 获取设备列表,支持keyword和分页 + const fetchDevices = async (keyword: string = "", page: number = 1) => { setLoading(true); try { const res = await getDeviceList({ - page: 1, - limit: 100, + page, + limit: pageSize, keyword: keyword.trim() || undefined, }); - if (res && Array.isArray(res.list)) { setDevices( res.list.map((d: any) => ({ @@ -54,30 +56,41 @@ export default function DeviceSelection({ status: d.alive === 1 ? "online" : "offline", })) ); + setTotal(res.total || 0); } } catch (error) { console.error("获取设备列表失败:", error); + Toast.show({ content: "获取设备列表失败", position: "top" }); } finally { setLoading(false); } }; - // 打开弹窗时获取设备列表 + // 打开弹窗时获取第一页 const openPopup = () => { setSearchQuery(""); + setCurrentPage(1); setPopupVisible(true); - fetchDevices(""); + fetchDevices("", 1); }; // 搜索防抖 useEffect(() => { if (!popupVisible) return; const timer = setTimeout(() => { - fetchDevices(searchQuery); + setCurrentPage(1); + fetchDevices(searchQuery, 1); }, 500); return () => clearTimeout(timer); }, [searchQuery, popupVisible]); + // 翻页时重新请求 + useEffect(() => { + if (!popupVisible) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + // 过滤设备(只保留状态过滤) const filteredDevices = devices.filter((device) => { const matchesStatus = @@ -87,6 +100,8 @@ export default function DeviceSelection({ return matchesStatus; }); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + // 处理设备选择 const handleDeviceToggle = (deviceId: string) => { if (selectedDevices.includes(deviceId)) { @@ -129,13 +144,12 @@ export default function DeviceSelection({
+ setSearchQuery(e.target.value)} - prefix={} - allowClear - size="large" + onChange={(val) => setSearchQuery(val)} + className={style.popupSearchInput} />
-
{loading ? ( @@ -198,6 +199,35 @@ export default function DeviceSelection({
)}
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
已选择 {selectedDevices.length} 个设备 diff --git a/nkebao/src/components/DeviceSelectionDialog/index.module.scss b/nkebao/src/components/DeviceSelectionDialog/index.module.scss index bfde5d72..e0961f10 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.module.scss +++ b/nkebao/src/components/DeviceSelectionDialog/index.module.scss @@ -163,3 +163,35 @@ 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 index 1c9ba4ae..588d774a 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -32,47 +32,53 @@ export function DeviceSelectionDialog({ 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 = "") => { - setLoading(true); - try { - const response = await getDeviceList({ - page: 1, - limit: 100, - 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, // 这个字段需要从其他API获取 - nickname: serverDevice.nickname || "", - }) - ); - setDevices(convertedDevices); + // 获取设备列表,支持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); } - } catch (error) { - console.error("获取设备列表失败:", error); - Toast.show({ - content: "获取设备列表失败,请检查网络连接", - position: "top", - }); - } finally { - setLoading(false); - } - }, []); + }, + [] + ); - // 打开弹窗时获取设备列表 + // 打开弹窗时获取第一页 useEffect(() => { if (open) { - fetchDevices(""); + setCurrentPage(1); + fetchDevices("", 1); } }, [open, fetchDevices]); @@ -80,11 +86,19 @@ export function DeviceSelectionDialog({ useEffect(() => { if (!open) return; const timer = setTimeout(() => { - fetchDevices(searchQuery); + 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 = @@ -102,6 +116,8 @@ export function DeviceSelectionDialog({ } }; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + return (
+ setSearchQuery(e.target.value)} - prefix={} - allowClear - size="large" + onChange={(val) => setSearchQuery(val)} + className={style.popupSearchInput} />
IMEI: {device.imei}
-
微信号: {device.wxid || "-"}
+
微信号: {device.wxid || "-"}
昵称: {device.nickname || "-"}
{device.usedInPlans > 0 && ( @@ -194,6 +209,35 @@ export function DeviceSelectionDialog({
)}
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
已选择 {selectedDevices.length} 个设备 From 3bd7290846274370c9bad26233924863e9de403b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 22 Jul 2025 15:35:46 +0800 Subject: [PATCH 07/28] =?UTF-8?q?[]=20=E4=BF=AE=E5=A4=8D=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E6=9D=A5=E6=BA=90=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=B8=8D=E6=9B=B4=E6=96=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/SyncWechatDataToCkbTask.php | 13 ++++ .../Adapters/ChuKeBao/Adapter.php | 66 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/Server/application/command/SyncWechatDataToCkbTask.php b/Server/application/command/SyncWechatDataToCkbTask.php index 9980a630..4e1bc482 100644 --- a/Server/application/command/SyncWechatDataToCkbTask.php +++ b/Server/application/command/SyncWechatDataToCkbTask.php @@ -52,6 +52,8 @@ class SyncWechatDataToCkbTask extends Command $this->syncWechatDevice($ChuKeBaoAdapter); $this->syncWechatCustomer($ChuKeBaoAdapter); $this->syncWechatFriendToTrafficPoolBatch($ChuKeBaoAdapter); + $this->syncTrafficSourceUser($ChuKeBaoAdapter); + $this->syncTrafficSourceGroup($ChuKeBaoAdapter); $output->writeln("同步任务 sync_wechat_to_ckb 已结束"); return true; @@ -96,4 +98,15 @@ class SyncWechatDataToCkbTask extends Command { return $ChuKeBaoAdapter->syncWechatFriendToTrafficPoolBatch(); } + protected function syncTrafficSourceUser(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncTrafficSourceUser(); + } + + protected function syncTrafficSourceGroup(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncTrafficSourceGroup(); + } + + } \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index ba111c73..8da320aa 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1225,4 +1225,70 @@ class Adapter implements WeChatServiceInterface return false; } } + + public function syncTrafficSourceUser() + { + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`) + SELECT + f.wechatId identifier, + c.departmentId companyId, + f.ownerNickname fromd, + f.ownerWechatId sourceId + FROM + s2_wechat_friend f + LEFT JOIN s2_wechat_account a ON f.wechatAccountId = a.id + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY f.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + companyId=VALUES(companyId), + sourceId=VALUES(sourceId)"; + + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + public function syncTrafficSourceGroup() + { + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`) + SELECT + m.wechatId identifier, + c.departmentId companyId, + r.nickname fromd, + f.ownerWechatId sourceId + FROM + s2_wechat_chatroom_member m + JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId + LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY m.id DESC + GROUP BY m.wechatId + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + companyId=VALUES(companyId), + sourceId=VALUES(sourceId)"; + + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } } From 95c90fb1c91ae4a086e8e92e6df124ca7afbf202 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 15:46:16 +0800 Subject: [PATCH 08/28] =?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=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/devices.ts | 44 +++ nkebao/src/api/request.ts | 83 +++-- nkebao/src/pages/devices/DeviceDetail.tsx | 363 ++++++++++++++++++- nkebao/src/pages/devices/Devices.tsx | 411 +++++++++++++++++++++- nkebao/src/types/device.ts | 67 ++++ 5 files changed, 923 insertions(+), 45 deletions(-) create mode 100644 nkebao/src/api/devices.ts create mode 100644 nkebao/src/types/device.ts diff --git a/nkebao/src/api/devices.ts b/nkebao/src/api/devices.ts new file mode 100644 index 00000000..b39c7683 --- /dev/null +++ b/nkebao/src/api/devices.ts @@ -0,0 +1,44 @@ +import request from "./request"; + +// 获取设备列表 +export const fetchDeviceList = (params: { + page?: number; + limit?: number; + keyword?: string; +}) => request("/v1/devices", params, "GET"); + +// 获取设备详情 +export const fetchDeviceDetail = (id: string | number) => + request(`/v1/devices/${id}`); + +// 获取设备关联微信账号 +export const fetchDeviceRelatedAccounts = (id: string | number) => + request(`/v1/wechats/related-device/${id}`); + +// 获取设备操作日志 +export const fetchDeviceHandleLogs = ( + id: string | number, + page = 1, + limit = 10 +) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET"); + +// 更新设备任务配置 +export const updateDeviceTaskConfig = (config: { + deviceId: string | number; + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; +}) => request("/v1/devices/task-config", config, "POST"); + +// 删除设备 +export const deleteDevice = (id: number) => + request(`/v1/devices/${id}`, undefined, "DELETE"); + +// 获取设备二维码 +export const fetchDeviceQRCode = (accountId: string) => + request("/v1/api/device/add", { accountId }, "POST"); + +// 通过IMEI添加设备 +export const addDeviceByImei = (imei: string, name: string) => + request("/v1/api/device/add-by-imei", { imei, name }, "POST"); diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 2c5372bf..b24074ad 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,22 +1,35 @@ -import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios'; -import { Toast } from 'antd-mobile'; +import axios, { + AxiosInstance, + AxiosRequestConfig, + Method, + AxiosResponse, +} from "axios"; +import { Toast } from "antd-mobile"; const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); +// 死循环请求拦截配置 +const FAIL_LIMIT = 3; +const BLOCK_TIME = 30 * 1000; // 30秒 +const failMap = new Map< + string, + { count: number; lastFail: number; blockedUntil?: number } +>(); + const instance: AxiosInstance = axios.create({ - baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api', + baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", timeout: 10000, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); -instance.interceptors.request.use(config => { - const token = localStorage.getItem('token'); +instance.interceptors.request.use((config) => { + const token = localStorage.getItem("token"); if (token) { config.headers = config.headers || {}; - config.headers['Authorization'] = `Bearer ${token}`; + config.headers["Authorization"] = `Bearer ${token}`; } return config; }); @@ -27,20 +40,20 @@ instance.interceptors.response.use( if (code === 200 || success) { return res.data.data ?? res.data; } - Toast.show({ content: msg || '接口错误', position: 'top' }); + Toast.show({ content: msg || "接口错误", position: "top" }); if (code === 401) { - localStorage.removeItem('token'); + localStorage.removeItem("token"); const currentPath = window.location.pathname + window.location.search; - if (currentPath === '/login') { - window.location.href = '/login'; + if (currentPath === "/login") { + window.location.href = "/login"; } else { window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; } } - return Promise.reject(msg || '接口错误'); + return Promise.reject(msg || "接口错误"); }, - err => { - Toast.show({ content: err.message || '网络异常', position: 'top' }); + (err) => { + Toast.show({ content: err.message || "网络异常", position: "top" }); return Promise.reject(err); } ); @@ -48,17 +61,26 @@ instance.interceptors.response.use( export function request( url: string, data?: any, - method: Method = 'GET', + method: Method = "GET", config?: AxiosRequestConfig, debounceGap?: number ): Promise { - const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP; + const gap = + typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; const key = `${method}_${url}_${JSON.stringify(data)}`; const now = Date.now(); const last = debounceMap.get(key) || 0; + + // 死循环拦截:如果被block,直接拒绝 + const failInfo = failMap.get(key); + if (failInfo && failInfo.blockedUntil && now < failInfo.blockedUntil) { + Toast.show({ content: "请求失败过多,请稍后再试", position: "top" }); + return Promise.reject("请求失败过多,请稍后再试"); + } + if (gap > 0 && now - last < gap) { - Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); - return Promise.reject('请求过于频繁,请稍后再试'); + Toast.show({ content: "请求过于频繁,请稍后再试", position: "top" }); + return Promise.reject("请求过于频繁,请稍后再试"); } debounceMap.set(key, now); @@ -67,12 +89,33 @@ export function request( method, ...config, }; - if (method.toUpperCase() === 'GET') { + if (method.toUpperCase() === "GET") { axiosConfig.params = data; } else { axiosConfig.data = data; } - return instance(axiosConfig); + return instance(axiosConfig) + .then((res) => { + // 成功则清除失败计数 + failMap.delete(key); + return res; + }) + .catch((err) => { + debounceMap.delete(key); + // 失败计数 + const fail = failMap.get(key) || { count: 0, lastFail: 0 }; + const newCount = now - fail.lastFail < BLOCK_TIME ? fail.count + 1 : 1; + if (newCount >= FAIL_LIMIT) { + failMap.set(key, { + count: newCount, + lastFail: now, + blockedUntil: now + BLOCK_TIME, + }); + } else { + failMap.set(key, { count: newCount, lastFail: now }); + } + throw err; + }); } export default request; diff --git a/nkebao/src/pages/devices/DeviceDetail.tsx b/nkebao/src/pages/devices/DeviceDetail.tsx index 69876d6e..a67f8883 100644 --- a/nkebao/src/pages/devices/DeviceDetail.tsx +++ b/nkebao/src/pages/devices/DeviceDetail.tsx @@ -1,28 +1,369 @@ -import React from "react"; -import { NavBar } from "antd-mobile"; +import React, { useEffect, useState, useCallback, useRef } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile"; +import { SettingOutlined, RedoOutlined } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceDetail, + fetchDeviceRelatedAccounts, + fetchDeviceHandleLogs, + updateDeviceTaskConfig, +} from "@/api/devices"; +import type { Device, WechatAccount, HandleLog } from "@/types/device"; const DeviceDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [device, setDevice] = useState(null); + const [tab, setTab] = useState("info"); + const [accounts, setAccounts] = useState([]); + const [accountsLoading, setAccountsLoading] = useState(false); + const [logs, setLogs] = useState([]); + const [logsLoading, setLogsLoading] = useState(false); + const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>( + {} + ); + + // 获取设备详情 + const loadDetail = useCallback(async () => { + if (!id) return; + setLoading(true); + try { + const res = await fetchDeviceDetail(id); + setDevice(res); + } catch (e: any) { + Toast.show({ content: e.message || "获取设备详情失败", position: "top" }); + } finally { + setLoading(false); + } + }, [id]); + + // 获取关联账号 + const loadAccounts = useCallback(async () => { + if (!id) return; + setAccountsLoading(true); + try { + const res = await fetchDeviceRelatedAccounts(id); + setAccounts(Array.isArray(res.accounts) ? res.accounts : []); + } catch (e: any) { + Toast.show({ content: e.message || "获取关联账号失败", position: "top" }); + } finally { + setAccountsLoading(false); + } + }, [id]); + + // 获取操作日志 + const loadLogs = useCallback(async () => { + if (!id) return; + setLogsLoading(true); + try { + const res = await fetchDeviceHandleLogs(id, 1, 20); + setLogs(Array.isArray(res.list) ? res.list : []); + } catch (e: any) { + Toast.show({ content: e.message || "获取操作日志失败", position: "top" }); + } finally { + setLogsLoading(false); + } + }, [id]); + + useEffect(() => { + loadDetail(); + // eslint-disable-next-line + }, [id]); + + useEffect(() => { + if (tab === "accounts") loadAccounts(); + if (tab === "logs") loadLogs(); + // eslint-disable-next-line + }, [tab]); + + // 功能开关 + const handleFeatureChange = async ( + feature: keyof Device["features"], + checked: boolean + ) => { + if (!id) return; + setFeatureSaving((prev) => ({ ...prev, [feature]: true })); + try { + await updateDeviceTaskConfig({ deviceId: id, [feature]: checked }); + setDevice((prev) => + prev + ? { + ...prev, + features: { ...prev.features, [feature]: checked }, + } + : prev + ); + Toast.show({ + content: `${getFeatureName(feature)}已${checked ? "开启" : "关闭"}`, + }); + } catch (e: any) { + Toast.show({ content: e.message || "设置失败", position: "top" }); + } finally { + setFeatureSaving((prev) => ({ ...prev, [feature]: false })); + } + }; + + const getFeatureName = (feature: string) => { + const map: Record = { + autoAddFriend: "自动加好友", + autoReply: "自动回复", + momentsSync: "朋友圈同步", + aiChat: "AI会话", + }; + return map[feature] || feature; + }; + return ( navigate(-1)} style={{ background: "#fff" }} - onBack={() => window.history.back()} + right={ + + } > -
+ 设备详情 -
+ } - footer={} + loading={loading} > -
-

设备详情页面

-

此页面正在开发中...

-
+ {!device ? ( +
+ +
正在加载设备信息...
+
+ ) : ( +
+ {/* 基本信息卡片 */} +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} +
+
+ {/* 标签页 */} + + + + + + {/* 功能开关 */} + {tab === "info" && ( +
+ {["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map( + (f) => ( +
+
+
{getFeatureName(f)}
+
+ + handleFeatureChange( + f as keyof Device["features"], + checked + ) + } + /> +
+ ) + )} +
+ )} + {/* 关联账号 */} + {tab === "accounts" && ( +
+ {accountsLoading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
+ 暂无关联微信账号 +
+ ) : ( +
+ {accounts.map((acc) => ( +
+ {acc.nickname} +
+
{acc.nickname}
+
+ 微信号: {acc.wechatId} +
+
+ 好友数: {acc.totalFriend} +
+
+ 最后活跃: {acc.lastActive} +
+
+ + {acc.wechatAliveText} + +
+ ))} +
+ )} +
+ +
+
+ )} + {/* 操作日志 */} + {tab === "logs" && ( +
+ {logsLoading ? ( +
+ +
+ ) : logs.length === 0 ? ( +
+ 暂无操作日志 +
+ ) : ( +
+ {logs.map((log) => ( +
+
{log.content}
+
+ 操作人: {log.username} · {log.createTime} +
+
+ ))} +
+ )} +
+ +
+
+ )} +
+ )}
); }; diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 66895d54..bb7f7f95 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,35 +1,418 @@ -import React from "react"; -import { NavBar, Button } from "antd-mobile"; -import { AddOutline } from "antd-mobile-icons"; +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; +import { Button, Input, Pagination } from "antd"; +import { AddOutline, DeleteOutline } from "antd-mobile-icons"; +import { + ReloadOutlined, + SearchOutlined, + QrcodeOutlined, +} from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceList, + fetchDeviceQRCode, + addDeviceByImei, + deleteDevice, +} from "@/api/devices"; +import type { Device } from "@/types/device"; const Devices: React.FC = () => { + // 设备列表相关 + const [devices, setDevices] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState<"all" | "online" | "offline">("all"); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [total, setTotal] = useState(0); + const [selected, setSelected] = useState<(string | number)[]>([]); + const observerRef = useRef(null); + const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 + + // 添加设备弹窗 + const [addVisible, setAddVisible] = useState(false); + const [addTab, setAddTab] = useState("scan"); + const [qrLoading, setQrLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [imei, setImei] = useState(""); + const [name, setName] = useState(""); + const [addLoading, setAddLoading] = useState(false); + + // 删除弹窗 + const [delVisible, setDelVisible] = useState(false); + const [delLoading, setDelLoading] = useState(false); + + // 加载设备列表 + const loadDevices = useCallback( + async (reset = false) => { + if (loading) return; + setLoading(true); + try { + const params: any = { page: reset ? 1 : page, limit: 20 }; + if (search) params.keyword = search; + const res = await fetchDeviceList(params); + const list = Array.isArray(res.list) ? res.list : []; + setDevices((prev) => (reset ? list : [...prev, ...list])); + setTotal(res.total || 0); + setHasMore(list.length === 20); + if (reset) setPage(1); + } catch (e) { + Toast.show({ content: "获取设备列表失败", position: "top" }); + setHasMore(false); // 请求失败后不再继续请求 + } finally { + setLoading(false); + } + }, + [loading, search, page] + ); + + // 首次加载和搜索 + useEffect(() => { + loadDevices(true); + // eslint-disable-next-line + }, [search]); + + // 无限滚动 + useEffect(() => { + if (!hasMore || loading) return; + const observer = new window.IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + setPage((p) => p + 1); + } + }, + { threshold: 0.5 } + ); + if (observerRef.current) observer.observe(observerRef.current); + return () => observer.disconnect(); + }, [hasMore, loading]); + + // 分页加载 + useEffect(() => { + if (page === 1) return; + loadDevices(); + // eslint-disable-next-line + }, [page]); + + // 状态筛选 + const filtered = devices.filter((d) => { + if (status === "all") return true; + if (status === "online") return d.status === "online" || d.alive === 1; + if (status === "offline") return d.status === "offline" || d.alive === 0; + return true; + }); + + // 获取二维码 + const handleGetQr = async () => { + setQrLoading(true); + setQrCode(null); + try { + const accountId = localStorage.getItem("s2_accountId") || ""; + if (!accountId) throw new Error("未获取到用户信息"); + const res = await fetchDeviceQRCode(accountId); + setQrCode(res.qrCode); + } catch (e: any) { + Toast.show({ content: e.message || "获取二维码失败", position: "top" }); + } finally { + setQrLoading(false); + } + }; + + // 手动添加设备 + const handleAddDevice = async () => { + if (!imei.trim() || !name.trim()) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + setAddLoading(true); + try { + await addDeviceByImei(imei, name); + Toast.show({ content: "添加成功", position: "top" }); + setAddVisible(false); + setImei(""); + setName(""); + loadDevices(true); + } catch (e: any) { + Toast.show({ content: e.message || "添加失败", position: "top" }); + } finally { + setAddLoading(false); + } + }; + + // 删除设备 + const handleDelete = async () => { + if (!selected.length) return; + setDelLoading(true); + try { + for (const id of selected) { + await deleteDevice(Number(id)); + } + Toast.show({ content: `删除成功`, position: "top" }); + setDelVisible(false); + setSelected([]); + loadDevices(true); + } catch (e: any) { + Toast.show({ content: e.message || "删除失败", position: "top" }); + } finally { + setDelLoading(false); + } + }; + + // 跳转详情 + const goDetail = (id: string | number) => { + window.location.href = `/devices/${id}`; + }; + + // 分页切换 + const handlePageChange = (p: number) => { + setPage(p); + loadDevices(true); + }; + return ( - 设备管理 -
- } right={ - } - /> + > + + 设备管理 + + } - footer={} + footer={} + loading={loading && devices.length === 0} > -
-

设备管理页面

-

此页面正在开发中...

+
+ {/* 搜索栏 */} +
+ setSearch(e.target.value)} + prefix={} + allowClear + style={{ flex: 1 }} + /> + +
+ {/* 筛选和删除 */} +
+ setStatus(k as any)} + style={{ flex: 1 }} + > + + + + + +
+ {/* 设备列表 */} +
+ {filtered.map((device) => ( +
goDetail(device.id!)} + > + { + e.stopPropagation(); + setSelected((prev) => + e.target.checked + ? [...prev, device.id!] + : prev.filter((id) => id !== device.id) + ); + }} + onClick={(e) => e.stopPropagation()} + style={{ marginRight: 12 }} + /> +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ + {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} + +
+ ))} + {/* 分页组件 */} + {usePagination && ( +
+ +
+ )} + {/* 无限滚动提示(仅在不分页时显示) */} + {!usePagination && ( +
+ {loading && } + {!hasMore && devices.length > 0 && "没有更多设备了"} + {!hasMore && devices.length === 0 && "暂无设备"} +
+ )} +
+ {/* 添加设备弹窗 */} + setAddVisible(false)} + bodyStyle={{ + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + minHeight: 320, + }} + > +
+ + + + + {addTab === "scan" && ( +
+ + {qrCode && ( +
+ 二维码 +
+ 请用手机扫码添加设备 +
+
+ )} +
+ )} + {addTab === "manual" && ( +
+ setName(e.target.value)} + allowClear + /> + setImei(e.target.value)} + allowClear + /> + +
+ )} +
+
+ {/* 删除确认弹窗 */} + setDelVisible(false)} + closeOnAction + closeOnMaskClick + actions={[ + { + key: "confirm", + text: "确认删除", + danger: true, + loading: delLoading, + }, + { key: "cancel", text: "取消" }, + ]} + /> ); }; diff --git a/nkebao/src/types/device.ts b/nkebao/src/types/device.ts new file mode 100644 index 00000000..629bde55 --- /dev/null +++ b/nkebao/src/types/device.ts @@ -0,0 +1,67 @@ +export type DeviceStatus = "online" | "offline" | "busy" | "error"; + +export interface Device { + id: number | string; + imei: string; + memo?: string; + wechatId?: string; + totalFriend?: number; + alive?: number; + status?: DeviceStatus; + nickname?: string; + battery?: number; + lastActive?: string; + features?: { + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; + }; +} + +export interface DeviceListResponse { + list: Device[]; + total: number; + page: number; + limit: number; +} + +export interface DeviceDetailResponse { + id: number | string; + imei: string; + memo?: string; + wechatId?: string; + alive?: number; + totalFriend?: number; + nickname?: string; + battery?: number; + lastActive?: string; + features?: { + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; + }; +} + +export interface WechatAccount { + id: string; + avatar: string; + nickname: string; + wechatId: string; + gender: number; + status: number; + statusText: string; + wechatAlive: number; + wechatAliveText: string; + addFriendStatus: number; + totalFriend: number; + lastActive: string; +} + +export interface HandleLog { + id: string | number; + content: string; + username: string; + createTime: string; +} From 8bb26032bde81ed1e097ab477035f1b7380a62d9 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 15:47:06 +0800 Subject: [PATCH 09/28] =?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=8E=AF=E5=A2=83=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nkebao/.env.development b/nkebao/.env.development index fe189d22..b60a89c6 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://yishi.com VITE_APP_TITLE=Nkebao Base From ac9aae920082ff5011aae12828b8318d8f6f76cc 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:02:16 +0800 Subject: [PATCH 10/28] =?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=E6=A0=B7=E5=BC=8F=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- nkebao/src/pages/devices/Devices.tsx | 150 +++++++++++++++------------ 2 files changed, 85 insertions(+), 67 deletions(-) diff --git a/nkebao/.env.development b/nkebao/.env.development index b60a89c6..da6a111b 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 -VITE_API_BASE_URL=http://yishi.com +VITE_API_BASE_URL=http://www.yishi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index bb7f7f95..2692cad9 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState, useCallback } from "react"; import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; import { Button, Input, Pagination } from "antd"; +import { useNavigate } from "react-router-dom"; import { AddOutline, DeleteOutline } from "antd-mobile-icons"; import { ReloadOutlined, SearchOutlined, QrcodeOutlined, + ArrowLeftOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; @@ -43,6 +45,7 @@ const Devices: React.FC = () => { const [delVisible, setDelVisible] = useState(false); const [delLoading, setDelLoading] = useState(false); + const navigate = useNavigate(); // 加载设备列表 const loadDevices = useCallback( async (reset = false) => { @@ -173,64 +176,90 @@ const Devices: React.FC = () => { return ( setAddVisible(true)} - > - - 添加设备 - - } - > - - 设备管理 - - + <> + + navigate(-1)} + /> +
+ } + style={{ background: "#fff" }} + right={ + + } + > + + 设备管理 + + + +
+ {/* 搜索栏 */} +
+ setSearch(e.target.value)} + prefix={} + allowClear + style={{ flex: 1 }} + /> + +
+ {/* 筛选和删除 */} +
+ setStatus(k as any)} + style={{ flex: 1 }} + > + + + + + +
+
+ + } + footer={ +
+ +
} - footer={} loading={loading && devices.length === 0} >
- {/* 搜索栏 */} -
- setSearch(e.target.value)} - prefix={} - allowClear - style={{ flex: 1 }} - /> - -
- {/* 筛选和删除 */} -
- setStatus(k as any)} - style={{ flex: 1 }} - > - - - - - -
{/* 设备列表 */}
{filtered.map((device) => ( @@ -294,18 +323,7 @@ const Devices: React.FC = () => {
))} - {/* 分页组件 */} - {usePagination && ( -
- -
- )} + {/* 无限滚动提示(仅在不分页时显示) */} {!usePagination && (
Date: Tue, 22 Jul 2025 16:03:09 +0800 Subject: [PATCH 11/28] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Adapters/ChuKeBao/Adapter.php | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 8da320aa..f4e62737 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1228,12 +1228,13 @@ class Adapter implements WeChatServiceInterface public function syncTrafficSourceUser() { - $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`) + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`) SELECT f.wechatId identifier, c.departmentId companyId, f.ownerNickname fromd, - f.ownerWechatId sourceId + f.ownerWechatId sourceId, + f.createTime createTime FROM s2_wechat_friend f LEFT JOIN s2_wechat_account a ON f.wechatAccountId = a.id @@ -1260,19 +1261,20 @@ class Adapter implements WeChatServiceInterface public function syncTrafficSourceGroup() { - $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`) - SELECT - m.wechatId identifier, - c.departmentId companyId, - r.nickname fromd, - f.ownerWechatId sourceId - FROM - s2_wechat_chatroom_member m - JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId - LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId - LEFT JOIN s2_company_account c on c.id = a.deviceAccountId - ORDER BY m.id DESC - GROUP BY m.wechatId + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`) + SELECT + m.wechatId identifier, + c.departmentId companyId, + r.nickname fromd, + m.chatroomId sourceId, + m.createTime createTime + FROM + s2_wechat_chatroom_member m + JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId + LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY m.id DESC + GROUP BY m.wechatId LIMIT ?, ? ON DUPLICATE KEY UPDATE identifier=VALUES(identifier), From 3f574b973921afd06103abe91e81850204a42026 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 22 Jul 2025 16:17:02 +0800 Subject: [PATCH 12/28] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index f4e62737..ffe44918 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1273,8 +1273,8 @@ class Adapter implements WeChatServiceInterface JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId LEFT JOIN s2_company_account c on c.id = a.deviceAccountId - ORDER BY m.id DESC GROUP BY m.wechatId + ORDER BY m.id DESC LIMIT ?, ? ON DUPLICATE KEY UPDATE identifier=VALUES(identifier), From 47f85ee35a3df84db4633137c02b262d9c96fb31 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:24:30 +0800 Subject: [PATCH 13/28] =?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=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/pages/devices/Devices.tsx | 43 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 2692cad9..11e11dd6 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState, useCallback } from "react"; import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; -import { Button, Input, Pagination } from "antd"; +import { Button, Input, Pagination, Checkbox } from "antd"; import { useNavigate } from "react-router-dom"; import { AddOutline, DeleteOutline } from "antd-mobile-icons"; import { @@ -191,11 +191,11 @@ const Devices: React.FC = () => { right={ } > @@ -223,7 +223,7 @@ const Devices: React.FC = () => {
{/* 筛选和删除 */} -
+
setStatus(k as any)} @@ -233,15 +233,18 @@ const Devices: React.FC = () => { - +
+ +
@@ -279,8 +282,7 @@ const Devices: React.FC = () => { }} onClick={() => goDetail(device.id!)} > - { e.stopPropagation(); @@ -297,13 +299,13 @@ const Devices: React.FC = () => {
{device.memo || "未命名设备"}
-
+
IMEI: {device.imei}
-
+
微信号: {device.wechatId || "未绑定"}
-
+
好友数: {device.totalFriend ?? "-"}
@@ -359,8 +361,7 @@ const Devices: React.FC = () => { {addTab === "scan" && (
- - {currentPage} / {totalPages} - - -
-
-
-
- 已选择 {selectedDevices.length} 个设备 -
-
- - -
-
-
- - - ); -} +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"; +import { DeleteOutlined } from "@ant-design/icons"; + +// 设备选择项接口 +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +// 组件属性接口 +interface DeviceSelectionProps { + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + placeholder?: string; + className?: string; + mode?: "input" | "dialog"; // 新增,默认input + open?: boolean; // 仅mode=dialog时生效 + onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效 + selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 + showInput?: boolean; // 新增 + showSelectedList?: boolean; // 新增 +} + +const PAGE_SIZE = 20; + +const DeviceSelection: React.FC = ({ + selectedDevices, + onSelect, + placeholder = "选择设备", + className = "", + mode = "input", + open, + onOpenChange, + selectedListMaxHeight = 300, // 默认300 + showInput = true, + showSelectedList = true, +}) => { + // 弹窗控制 + const [popupVisible, setPopupVisible] = useState(false); + const isDialog = mode === "dialog"; + const realVisible = isDialog ? !!open : popupVisible; + const setRealVisible = (v: boolean) => { + if (isDialog && onOpenChange) onOpenChange(v); + if (!isDialog) setPopupVisible(v); + }; + + // 设备数据 + const [devices, setDevices] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const res = await getDeviceList({ + page, + limit: PAGE_SIZE, + keyword: keyword.trim() || undefined, + }); + if (res && Array.isArray(res.list)) { + setDevices( + res.list.map((d: any) => ({ + id: d.id?.toString() || "", + name: d.memo || d.imei || "", + imei: d.imei || "", + wechatId: d.wechatId || "", + status: d.alive === 1 ? "online" : "offline", + wxid: d.wechatId || "", + nickname: d.nickname || "", + usedInPlans: d.usedInPlans || 0, + })) + ); + setTotal(res.total || 0); + } + } catch (error) { + console.error("获取设备列表失败:", error); + } finally { + setLoading(false); + } + }, + [] + ); + + // 打开弹窗时获取第一页 + const openPopup = () => { + setSearchQuery(""); + setCurrentPage(1); + setRealVisible(true); + fetchDevices("", 1); + }; + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchDevices(searchQuery, 1); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, realVisible, fetchDevices]); + + // 翻页时重新请求 + useEffect(() => { + if (!realVisible) 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 totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedDevices.length === 0) return ""; + return `已选择 ${selectedDevices.length} 个设备`; + }; + + // 获取已选设备详细信息 + const selectedDeviceObjs = selectedDevices + .map((id) => devices.find((d) => d.id === id)) + .filter(Boolean) as DeviceSelectionItem[]; + + // 删除已选设备 + const handleRemoveDevice = (id: string) => { + onSelect(selectedDevices.filter((d) => d !== id)); + }; + + // 弹窗内容 + const popupContent = ( +
+
+
选择设备
+
+
+
+ + setSearchQuery(val)} + className={style.popupSearchInput} + /> +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+ ); + + return ( + <> + {/* mode=input 显示输入框,mode=dialog不显示 */} + {mode === "input" && showInput && ( +
+ } + allowClear + size="large" + /> +
+ )} + {/* 已选设备列表窗口 */} + {mode === "input" && + showSelectedList && + selectedDeviceObjs.length > 0 && ( +
+ {selectedDeviceObjs.map((device) => ( +
+
+ {device.name} +
+
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + {popupContent} + + + ); +}; + +export default DeviceSelection; diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx new file mode 100644 index 00000000..8f77eabc --- /dev/null +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -0,0 +1,197 @@ +import React from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button, Checkbox, Popup } from "antd-mobile"; +import style from "./index.module.scss"; + +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +interface SelectionPopupProps { + visible: boolean; + onClose: () => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + devices: DeviceSelectionItem[]; + loading: boolean; + searchQuery: string; + setSearchQuery: (v: string) => void; + statusFilter: string; + setStatusFilter: (v: string) => void; + onRefresh: () => void; + filteredDevices: DeviceSelectionItem[]; + total: number; + currentPage: number; + totalPages: number; + setCurrentPage: (v: number) => void; + onCancel: () => void; + onConfirm: () => void; +} + +const SelectionPopup: React.FC = ({ + visible, + onClose, + selectedDevices, + onSelect, + devices, + loading, + searchQuery, + setSearchQuery, + statusFilter, + setStatusFilter, + onRefresh, + filteredDevices, + total, + currentPage, + totalPages, + setCurrentPage, + onCancel, + onConfirm, +}) => { + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + {}} + position="bottom" + bodyStyle={{ height: "100vh" }} + closeOnMaskClick={false} + > +
+
+
选择设备
+
+
+
+ + +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +}; + +export default SelectionPopup; From fceb787924c717f3e6404908e50d995360877b46 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:47:03 +0800 Subject: [PATCH 15/28] =?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=E9=80=89=E6=8B=A9=E7=BB=84=E4=BB=B6=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/DeviceSelection/index.tsx | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 126d43fc..a62625d2 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -30,6 +30,7 @@ interface DeviceSelectionProps { selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 showInput?: boolean; // 新增 showSelectedList?: boolean; // 新增 + readonly?: boolean; // 新增 } const PAGE_SIZE = 20; @@ -45,6 +46,7 @@ const DeviceSelection: React.FC = ({ selectedListMaxHeight = 300, // 默认300 showInput = true, showSelectedList = true, + readonly = false, }) => { // 弹窗控制 const [popupVisible, setPopupVisible] = useState(false); @@ -99,6 +101,7 @@ const DeviceSelection: React.FC = ({ // 打开弹窗时获取第一页 const openPopup = () => { + if (readonly) return; setSearchQuery(""); setCurrentPage(1); setRealVisible(true); @@ -155,6 +158,7 @@ const DeviceSelection: React.FC = ({ // 删除已选设备 const handleRemoveDevice = (id: string) => { + if (readonly) return; onSelect(selectedDevices.filter((d) => d !== id)); }; @@ -289,8 +293,13 @@ const DeviceSelection: React.FC = ({ value={getDisplayText()} onClick={openPopup} prefix={} - allowClear + allowClear={!readonly} size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } />
)} @@ -332,30 +341,32 @@ const DeviceSelection: React.FC = ({ > {device.name}
-
))}
)} {/* 弹窗 */} setRealVisible(false)} position="bottom" bodyStyle={{ height: "100vh" }} 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 16/28] =?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 f9a70a18..e70fc26a 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 1b8da372..922ddd08 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 52c63781..89b7a217 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 ab663bd3..85282d91 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 a66e35b77bb9c751914a3c393eec0898000da8c4 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 17:19:12 +0800 Subject: [PATCH 17/28] =?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=E8=AE=BE=E5=A4=87=E8=AF=A6=E6=83=85=E6=90=9E=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 1 + nkebao/src/pages/devices/Devices.tsx | 870 +++++++++++++-------------- nkebao/src/utils/common.ts | 37 ++ 3 files changed, 469 insertions(+), 439 deletions(-) create mode 100644 nkebao/src/utils/common.ts diff --git a/nkebao/.env.development b/nkebao/.env.development index da6a111b..f37023e8 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=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 11e11dd6..950c02ce 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,439 +1,431 @@ -import React, { useEffect, useRef, useState, useCallback } from "react"; -import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; -import { Button, Input, Pagination, Checkbox } from "antd"; -import { useNavigate } from "react-router-dom"; -import { AddOutline, DeleteOutline } from "antd-mobile-icons"; -import { - ReloadOutlined, - SearchOutlined, - QrcodeOutlined, - ArrowLeftOutlined, -} from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; -import { - fetchDeviceList, - fetchDeviceQRCode, - addDeviceByImei, - deleteDevice, -} from "@/api/devices"; -import type { Device } from "@/types/device"; - -const Devices: React.FC = () => { - // 设备列表相关 - const [devices, setDevices] = useState([]); - const [loading, setLoading] = useState(false); - const [search, setSearch] = useState(""); - const [status, setStatus] = useState<"all" | "online" | "offline">("all"); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [total, setTotal] = useState(0); - const [selected, setSelected] = useState<(string | number)[]>([]); - const observerRef = useRef(null); - const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 - - // 添加设备弹窗 - const [addVisible, setAddVisible] = useState(false); - const [addTab, setAddTab] = useState("scan"); - const [qrLoading, setQrLoading] = useState(false); - const [qrCode, setQrCode] = useState(null); - const [imei, setImei] = useState(""); - const [name, setName] = useState(""); - const [addLoading, setAddLoading] = useState(false); - - // 删除弹窗 - const [delVisible, setDelVisible] = useState(false); - const [delLoading, setDelLoading] = useState(false); - - const navigate = useNavigate(); - // 加载设备列表 - const loadDevices = useCallback( - async (reset = false) => { - if (loading) return; - setLoading(true); - try { - const params: any = { page: reset ? 1 : page, limit: 20 }; - if (search) params.keyword = search; - const res = await fetchDeviceList(params); - const list = Array.isArray(res.list) ? res.list : []; - setDevices((prev) => (reset ? list : [...prev, ...list])); - setTotal(res.total || 0); - setHasMore(list.length === 20); - if (reset) setPage(1); - } catch (e) { - Toast.show({ content: "获取设备列表失败", position: "top" }); - setHasMore(false); // 请求失败后不再继续请求 - } finally { - setLoading(false); - } - }, - [loading, search, page] - ); - - // 首次加载和搜索 - useEffect(() => { - loadDevices(true); - // eslint-disable-next-line - }, [search]); - - // 无限滚动 - useEffect(() => { - if (!hasMore || loading) return; - const observer = new window.IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !loading) { - setPage((p) => p + 1); - } - }, - { threshold: 0.5 } - ); - if (observerRef.current) observer.observe(observerRef.current); - return () => observer.disconnect(); - }, [hasMore, loading]); - - // 分页加载 - useEffect(() => { - if (page === 1) return; - loadDevices(); - // eslint-disable-next-line - }, [page]); - - // 状态筛选 - const filtered = devices.filter((d) => { - if (status === "all") return true; - if (status === "online") return d.status === "online" || d.alive === 1; - if (status === "offline") return d.status === "offline" || d.alive === 0; - return true; - }); - - // 获取二维码 - const handleGetQr = async () => { - setQrLoading(true); - setQrCode(null); - try { - const accountId = localStorage.getItem("s2_accountId") || ""; - if (!accountId) throw new Error("未获取到用户信息"); - const res = await fetchDeviceQRCode(accountId); - setQrCode(res.qrCode); - } catch (e: any) { - Toast.show({ content: e.message || "获取二维码失败", position: "top" }); - } finally { - setQrLoading(false); - } - }; - - // 手动添加设备 - const handleAddDevice = async () => { - if (!imei.trim() || !name.trim()) { - Toast.show({ content: "请填写完整信息", position: "top" }); - return; - } - setAddLoading(true); - try { - await addDeviceByImei(imei, name); - Toast.show({ content: "添加成功", position: "top" }); - setAddVisible(false); - setImei(""); - setName(""); - loadDevices(true); - } catch (e: any) { - Toast.show({ content: e.message || "添加失败", position: "top" }); - } finally { - setAddLoading(false); - } - }; - - // 删除设备 - const handleDelete = async () => { - if (!selected.length) return; - setDelLoading(true); - try { - for (const id of selected) { - await deleteDevice(Number(id)); - } - Toast.show({ content: `删除成功`, position: "top" }); - setDelVisible(false); - setSelected([]); - loadDevices(true); - } catch (e: any) { - Toast.show({ content: e.message || "删除失败", position: "top" }); - } finally { - setDelLoading(false); - } - }; - - // 跳转详情 - const goDetail = (id: string | number) => { - window.location.href = `/devices/${id}`; - }; - - // 分页切换 - const handlePageChange = (p: number) => { - setPage(p); - loadDevices(true); - }; - - return ( - - - navigate(-1)} - /> -
- } - style={{ background: "#fff" }} - right={ - - } - > - - 设备管理 - - - -
- {/* 搜索栏 */} -
- setSearch(e.target.value)} - prefix={} - allowClear - style={{ flex: 1 }} - /> - -
- {/* 筛选和删除 */} -
- setStatus(k as any)} - style={{ flex: 1 }} - > - - - - -
- -
-
-
- - } - footer={ -
- -
- } - loading={loading && devices.length === 0} - > -
- {/* 设备列表 */} -
- {filtered.map((device) => ( -
goDetail(device.id!)} - > - { - e.stopPropagation(); - setSelected((prev) => - e.target.checked - ? [...prev, device.id!] - : prev.filter((id) => id !== device.id) - ); - }} - onClick={(e) => e.stopPropagation()} - style={{ marginRight: 12 }} - /> -
-
- {device.memo || "未命名设备"} -
-
- IMEI: {device.imei} -
-
- 微信号: {device.wechatId || "未绑定"} -
-
- 好友数: {device.totalFriend ?? "-"} -
-
- - {device.status === "online" || device.alive === 1 - ? "在线" - : "离线"} - -
- ))} - - {/* 无限滚动提示(仅在不分页时显示) */} - {!usePagination && ( -
- {loading && } - {!hasMore && devices.length > 0 && "没有更多设备了"} - {!hasMore && devices.length === 0 && "暂无设备"} -
- )} -
-
- {/* 添加设备弹窗 */} - setAddVisible(false)} - bodyStyle={{ - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - minHeight: 320, - }} - > -
- - - - - {addTab === "scan" && ( -
- - {qrCode && ( -
- 二维码 -
- 请用手机扫码添加设备 -
-
- )} -
- )} - {addTab === "manual" && ( -
- setName(e.target.value)} - allowClear - /> - setImei(e.target.value)} - allowClear - /> - -
- )} -
-
- {/* 删除确认弹窗 */} - setDelVisible(false)} - closeOnAction - closeOnMaskClick - actions={[ - { - key: "confirm", - text: "确认删除", - danger: true, - // loading: delLoading, // antd-mobile Dialog.Action 不支持 loading 属性,去掉 - }, - { key: "cancel", text: "取消" }, - ]} - /> - - ); -}; - -export default Devices; +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; +import { Button, Input, Pagination, Checkbox } from "antd"; +import { useNavigate } from "react-router-dom"; +import { AddOutline, DeleteOutline } from "antd-mobile-icons"; +import { + ReloadOutlined, + SearchOutlined, + QrcodeOutlined, + ArrowLeftOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceList, + fetchDeviceQRCode, + addDeviceByImei, + deleteDevice, +} from "@/api/devices"; +import type { Device } from "@/types/device"; +import { comfirm } from "@/utils/common"; + +const Devices: React.FC = () => { + // 设备列表相关 + const [devices, setDevices] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState<"all" | "online" | "offline">("all"); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [total, setTotal] = useState(0); + const [selected, setSelected] = useState<(string | number)[]>([]); + const observerRef = useRef(null); + const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 + + // 添加设备弹窗 + const [addVisible, setAddVisible] = useState(false); + const [addTab, setAddTab] = useState("scan"); + const [qrLoading, setQrLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [imei, setImei] = useState(""); + const [name, setName] = useState(""); + const [addLoading, setAddLoading] = useState(false); + + // 删除弹窗 + const [delVisible, setDelVisible] = useState(false); + const [delLoading, setDelLoading] = useState(false); + + const navigate = useNavigate(); + // 加载设备列表 + const loadDevices = useCallback( + async (reset = false) => { + if (loading) return; + setLoading(true); + try { + const params: any = { page: reset ? 1 : page, limit: 20 }; + if (search) params.keyword = search; + const res = await fetchDeviceList(params); + const list = Array.isArray(res.list) ? res.list : []; + setDevices((prev) => (reset ? list : [...prev, ...list])); + setTotal(res.total || 0); + setHasMore(list.length === 20); + if (reset) setPage(1); + } catch (e) { + Toast.show({ content: "获取设备列表失败", position: "top" }); + setHasMore(false); // 请求失败后不再继续请求 + } finally { + setLoading(false); + } + }, + [loading, search, page] + ); + + // 首次加载和搜索 + useEffect(() => { + loadDevices(true); + // eslint-disable-next-line + }, [search]); + + // 无限滚动 + useEffect(() => { + if (!hasMore || loading) return; + const observer = new window.IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + setPage((p) => p + 1); + } + }, + { threshold: 0.5 } + ); + if (observerRef.current) observer.observe(observerRef.current); + return () => observer.disconnect(); + }, [hasMore, loading]); + + // 分页加载 + useEffect(() => { + if (page === 1) return; + loadDevices(); + // eslint-disable-next-line + }, [page]); + + // 状态筛选 + const filtered = devices.filter((d) => { + if (status === "all") return true; + if (status === "online") return d.status === "online" || d.alive === 1; + if (status === "offline") return d.status === "offline" || d.alive === 0; + return true; + }); + + // 获取二维码 + const handleGetQr = async () => { + setQrLoading(true); + setQrCode(null); + try { + const accountId = localStorage.getItem("s2_accountId") || ""; + if (!accountId) throw new Error("未获取到用户信息"); + const res = await fetchDeviceQRCode(accountId); + setQrCode(res.qrCode); + } catch (e: any) { + Toast.show({ content: e.message || "获取二维码失败", position: "top" }); + } finally { + setQrLoading(false); + } + }; + + // 手动添加设备 + const handleAddDevice = async () => { + if (!imei.trim() || !name.trim()) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + setAddLoading(true); + try { + await addDeviceByImei(imei, name); + Toast.show({ content: "添加成功", position: "top" }); + setAddVisible(false); + setImei(""); + setName(""); + loadDevices(true); + } catch (e: any) { + Toast.show({ content: e.message || "添加失败", position: "top" }); + } finally { + setAddLoading(false); + } + }; + + // 删除设备 + const handleDelete = async () => { + setDelLoading(true); + try { + for (const id of selected) { + await deleteDevice(Number(id)); + } + Toast.show({ content: `删除成功`, position: "top" }); + setSelected([]); + loadDevices(true); + } catch (e: any) { + if (e) Toast.show({ content: e.message || "删除失败", position: "top" }); + } finally { + setDelLoading(false); + } + }; + + // 删除按钮点击 + const handleDeleteClick = async () => { + try { + await comfirm( + `将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`, + { title: "确认删除", confirmText: "确认删除", cancelText: "取消" } + ); + handleDelete(); + } catch { + // 用户取消,无需处理 + } + }; + + // 跳转详情 + const goDetail = (id: string | number) => { + window.location.href = `/devices/${id}`; + }; + + // 分页切换 + const handlePageChange = (p: number) => { + setPage(p); + loadDevices(true); + }; + + return ( + + + navigate(-1)} + /> +
+ } + style={{ background: "#fff" }} + right={ + + } + > + + 设备管理 + + + +
+ {/* 搜索栏 */} +
+ setSearch(e.target.value)} + prefix={} + allowClear + style={{ flex: 1 }} + /> + +
+ {/* 筛选和删除 */} +
+ setStatus(k as any)} + style={{ flex: 1 }} + > + + + + +
+ +
+
+
+ + } + footer={ +
+ +
+ } + loading={loading && devices.length === 0} + > +
+ {/* 设备列表 */} +
+ {filtered.map((device) => ( +
goDetail(device.id!)} + > + { + e.stopPropagation(); + setSelected((prev) => + e.target.checked + ? [...prev, device.id!] + : prev.filter((id) => id !== device.id) + ); + }} + onClick={(e) => e.stopPropagation()} + style={{ marginRight: 12 }} + /> +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ + {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} + +
+ ))} + + {/* 无限滚动提示(仅在不分页时显示) */} + {!usePagination && ( +
+ {loading && } + {!hasMore && devices.length > 0 && "没有更多设备了"} + {!hasMore && devices.length === 0 && "暂无设备"} +
+ )} +
+
+ {/* 添加设备弹窗 */} + setAddVisible(false)} + bodyStyle={{ + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + minHeight: 320, + }} + > +
+ + + + + {addTab === "scan" && ( +
+ + {qrCode && ( +
+ 二维码 +
+ 请用手机扫码添加设备 +
+
+ )} +
+ )} + {addTab === "manual" && ( +
+ setName(e.target.value)} + allowClear + /> + setImei(e.target.value)} + allowClear + /> + +
+ )} +
+
+ + ); +}; + +export default Devices; diff --git a/nkebao/src/utils/common.ts b/nkebao/src/utils/common.ts new file mode 100644 index 00000000..60cbf840 --- /dev/null +++ b/nkebao/src/utils/common.ts @@ -0,0 +1,37 @@ +import { Modal } from "antd-mobile"; + +/** + * 通用js调用弹窗,Promise风格 + * @param content 弹窗内容 + * @param config 配置项(title, cancelText, confirmText) + * @returns Promise + */ +export const comfirm = ( + content: string, + config?: { + title?: string; + cancelText?: string; + confirmText?: string; + } +): Promise => { + return new Promise((resolve, reject) => { + Modal.show({ + title: config?.title || "提示", + content, + closeOnAction: true, + actions: [ + { + key: "cancel", + text: config?.cancelText || "取消", + onClick: () => reject(), + }, + { + key: "confirm", + text: config?.confirmText || "确认", + danger: true, + onClick: () => resolve(), + }, + ], + }); + }); +}; From 17e4cd26fe3254a46edfc6a1f90c3d32f2c8250d 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 17:58:14 +0800 Subject: [PATCH 18/28] =?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=E9=A6=96=E9=A1=B5=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/pages/home/index.module.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nkebao/src/pages/home/index.module.scss b/nkebao/src/pages/home/index.module.scss index 5ddda9a0..ac10533a 100644 --- a/nkebao/src/pages/home/index.module.scss +++ b/nkebao/src/pages/home/index.module.scss @@ -1,11 +1,8 @@ .home-page { padding: 12px; - background: #f8f6f3; - min-height: 100vh; } .content-wrapper { - padding: 12px; display: flex; flex-direction: column; gap: 12px; @@ -58,7 +55,6 @@ display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; - margin-bottom: 16px; } .stat-card { @@ -159,7 +155,6 @@ background: white; border-radius: 12px; padding: 16px; - margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); transition: all 0.3s ease; From 3987bfa551d21cd08a57a1ab0161f33cf63213b9 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 18:36:07 +0800 Subject: [PATCH 19/28] =?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=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DeviceSelectionDialog/api.ts | 10 - .../DeviceSelectionDialog/index.module.scss | 197 ------------- .../DeviceSelectionDialog/index.tsx | 258 ------------------ .../FriendSelection/index.module.scss | 12 +- .../src/components/FriendSelection/index.tsx | 212 +++++++++----- .../GroupSelection/index.module.scss | 17 +- .../src/components/GroupSelection/index.tsx | 226 ++++++++++----- nkebao/src/components/SelectionTest.tsx | 14 +- 8 files changed, 311 insertions(+), 635 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/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts deleted file mode 100644 index 1f28ce04..00000000 --- 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 e0961f10..00000000 --- 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 588d774a..00000000 --- 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/FriendSelection/index.module.scss b/nkebao/src/components/FriendSelection/index.module.scss index 0c82db96..663353e8 100644 --- a/nkebao/src/components/FriendSelection/index.module.scss +++ b/nkebao/src/components/FriendSelection/index.module.scss @@ -205,13 +205,21 @@ } .popupFooter { - border-top: 1px solid #f0f0f0; - padding: 16px; 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; +} .cancelBtn { padding: 0 24px; border-radius: 24px; diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index d67aa0ff..13090569 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -1,11 +1,7 @@ import React, { useState, useEffect } from "react"; -import { - SearchOutlined, - CloseOutlined, - LeftOutlined, - RightOutlined, -} from "@ant-design/icons"; -import { Input, Button, Popup, Toast } from "antd-mobile"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Popup, Toast } from "antd-mobile"; +import { Button, Input } from "antd"; import { getFriendList } from "./api"; import style from "./index.module.scss"; @@ -29,6 +25,10 @@ interface FriendSelectionProps { className?: string; visible?: boolean; // 新增 onVisibleChange?: (visible: boolean) => void; // 新增 + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; } export default function FriendSelection({ @@ -41,6 +41,10 @@ export default function FriendSelection({ className = "", visible, onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, }: FriendSelectionProps) { const [popupVisible, setPopupVisible] = useState(false); const [friends, setFriends] = useState([]); @@ -59,6 +63,7 @@ export default function FriendSelection({ // 打开弹窗并请求第一页好友 const openPopup = () => { + if (readonly) return; setCurrentPage(1); setSearchQuery(""); setRealVisible(true); @@ -152,6 +157,17 @@ export default function FriendSelection({ return `已选择 ${selectedFriends.length} 个好友`; }; + // 获取已选好友详细信息 + const selectedFriendObjs = selectedFriends + .map((id) => friends.find((f) => f.id === id)) + .filter(Boolean) as WechatFriend[]; + + // 删除已选好友 + const handleRemoveFriend = (id: string) => { + if (readonly) return; + onSelect(selectedFriends.filter((f) => f !== id)); + }; + const handleConfirm = () => { setPopupVisible(false); }; @@ -166,35 +182,85 @@ export default function FriendSelection({ return ( <> {/* 输入框 */} -
- - - - - - -
- - {/* 微信好友选择弹窗 */} + {showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选好友列表窗口 */} + {showSelectedList && selectedFriendObjs.length > 0 && ( +
+ {selectedFriendObjs.map((friend) => ( +
+
+ {friend.nickname || friend.wechatId || friend.id} +
+ {!readonly && ( +
+ ))} +
+ )} + {/* 弹窗 */} setRealVisible(false)} position="bottom" bodyStyle={{ height: "100vh" }} @@ -206,23 +272,33 @@ export default function FriendSelection({ setSearchQuery(val)} - className={style.searchInput} + onChange={(e) => setSearchQuery(e.target.value)} + disabled={readonly} + prefix={} + allowClear + size="large" /> - - {searchQuery && ( + {searchQuery && !readonly && ( + style={{ + color: "#ff4d4f", + border: "none", + background: "none", + minWidth: 24, + height: 24, + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + /> )}
-
{loading ? (
@@ -234,7 +310,7 @@ export default function FriendSelection({
-
- 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 7c67617a102497d967764d7a723771a52da0c268 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 09:35:01 +0800 Subject: [PATCH 22/28] =?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=AF=BC=E8=88=AA=E6=A0=8F=E6=A0=B7=E5=BC=8F=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/workspace/group-push/form/index.tsx | 20 +++++++++++++------ .../pages/workspace/group-push/list/index.tsx | 17 ++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/nkebao/src/pages/workspace/group-push/form/index.tsx b/nkebao/src/pages/workspace/group-push/form/index.tsx index 8a57544b..578937c8 100644 --- a/nkebao/src/pages/workspace/group-push/form/index.tsx +++ b/nkebao/src/pages/workspace/group-push/form/index.tsx @@ -2,10 +2,9 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "antd-mobile"; import { NavBar } from "antd-mobile"; -import { LeftOutline } from "antd-mobile-icons"; +import { ArrowLeftOutlined } from "@ant-design/icons"; import { createGroupPushTask } from "@/pages/workspace/group-push/detail/groupPush"; import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; import StepIndicator from "./components/StepIndicator"; import BasicSettings from "./components/BasicSettings"; import GroupSelector from "./components/GroupSelector"; @@ -35,6 +34,7 @@ const NewGroupPush: React.FC = () => { groups: [], contentLibraries: [], }); + const [isEditMode, setIsEditMode] = useState(false); const handleBasicSettingsNext = (values: Partial) => { setFormData((prev) => ({ ...prev, ...values })); @@ -107,14 +107,22 @@ const NewGroupPush: React.FC = () => { navigate(-1)} + back={null} style={{ background: "#fff" }} - right={null} + left={ +
+ navigate(-1)} + /> +
+ } > - 群消息推送 + + {isEditMode ? "编辑任务" : "新建任务"} + } - footer={} >
diff --git a/nkebao/src/pages/workspace/group-push/list/index.tsx b/nkebao/src/pages/workspace/group-push/list/index.tsx index 6eaa35f1..cfb56fd1 100644 --- a/nkebao/src/pages/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/workspace/group-push/list/index.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { NavBar } from "antd-mobile"; -import { LeftOutline } from "antd-mobile-icons"; import { + ArrowLeftOutlined, PlusOutlined, SearchOutlined, ReloadOutlined, @@ -151,11 +151,11 @@ const GroupPush: React.FC = () => { - - navigate(-1)} fontSize={24} /> - - 群消息推送 +
+ navigate(-1)} + />
} style={{ background: "#fff" }} @@ -168,9 +168,10 @@ const GroupPush: React.FC = () => { 创建任务 } - >
+ > + 群消息推送 + } - footer={} >
From a3bc324943662d5703c3ada7eeedd477c9faf8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Wed, 23 Jul 2025 11:33:01 +0800 Subject: [PATCH 23/28] =?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=9C=BA=E6=99=AF=E8=AE=A1=E5=88=92=E6=9E=84=E5=BB=BA90%?= =?UTF-8?q?=E5=BD=93=E5=8A=A1=E4=B9=8B=E6=80=A5=EF=BC=8C=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E5=B0=81=E8=A3=85=E4=B8=80=E4=B8=8B=E6=AD=A5=E9=AA=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- nkebao/src/api/common.ts | 33 + .../src/pages/scenarios/plan/new/index.api.ts | 53 + .../scenarios/plan/new/index.module.scss | 0 nkebao/src/pages/scenarios/plan/new/index.tsx | 220 ++-- .../src/pages/scenarios/plan/new/page.api.ts | 20 - .../pages/scenarios/plan/new/page.module.scss | 39 - .../plan/new/steps/BasicSettings.module.scss | 63 - .../plan/new/steps/BasicSettings.tsx | 1067 +++++++++++++---- .../steps/FriendRequestSettings.module.scss | 56 - .../plan/new/steps/FriendRequestSettings.tsx | 372 ++++-- .../new/steps/MessageSettings.module.scss | 64 - .../plan/new/steps/MessageSettings.tsx | 743 +++++++++--- 13 files changed, 1891 insertions(+), 841 deletions(-) create mode 100644 nkebao/src/api/common.ts create mode 100644 nkebao/src/pages/scenarios/plan/new/index.api.ts create mode 100644 nkebao/src/pages/scenarios/plan/new/index.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/page.api.ts delete mode 100644 nkebao/src/pages/scenarios/plan/new/page.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss delete mode 100644 nkebao/src/pages/scenarios/plan/new/steps/MessageSettings.module.scss diff --git a/nkebao/.env.development b/nkebao/.env.development index f37023e8..fde60295 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,5 +1,5 @@ # 基础环境变量示例 VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/api/common.ts b/nkebao/src/api/common.ts new file mode 100644 index 00000000..9ab445cc --- /dev/null +++ b/nkebao/src/api/common.ts @@ -0,0 +1,33 @@ +import request from "./request"; +/** + * 通用文件上传方法(支持图片、文件) + * @param {File} file - 要上传的文件对象 + * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 + * @returns {Promise} - 上传成功后返回文件url + */ +export async function uploadFile( + file: File, + uploadUrl: string = "/v1/attachment/upload" +): Promise { + try { + // 创建 FormData 对象用于文件上传 + const formData = new FormData(); + formData.append("file", file); + + // 使用 request 方法上传文件,设置正确的 Content-Type + const res = await request(uploadUrl, formData, "POST", { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + // 检查响应结果 + if (res?.code === 200 && res?.data?.url) { + return res.data.url; + } else { + throw new Error(res?.msg || "文件上传失败"); + } + } catch (e: any) { + throw new Error(e?.message || "文件上传失败"); + } +} diff --git a/nkebao/src/pages/scenarios/plan/new/index.api.ts b/nkebao/src/pages/scenarios/plan/new/index.api.ts new file mode 100644 index 00000000..a4e06ebc --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/new/index.api.ts @@ -0,0 +1,53 @@ +import request from "@/api/request"; +// 获取场景类型列表 +export function getScenarioTypes() { + return request("/v1/scenarios/types", undefined, "GET"); +} + +// 创建计划 +export function createPlan(data: any) { + return request("/v1/scenarios/plans", data, "POST"); +} + +// 更新计划 +export function updatePlan(planId: string, data: any) { + return request(`/v1/scenarios/plans/${planId}`, data, "PUT"); +} + +// 获取计划详情 +export function getPlanDetail(planId: string) { + return request(`/v1/scenarios/plans/${planId}`, undefined, "GET"); +} + +// PlanDetail 类型定义(可根据实际接口返回结构补充字段) +export interface PlanDetail { + name: string; + scenario: number; + posters: any[]; + device: string[]; + remarkType: string; + greeting: string; + addInterval: number; + startTime: string; + endTime: string; + enabled: boolean; + sceneId: string | number; + remarkFormat: string; + addFriendInterval: number; + // 其它字段可扩展 + [key: string]: any; +} + +// 兼容旧代码的接口命名 +export function getPlanScenes() { + return getScenarioTypes(); +} +export function createScenarioPlan(data: any) { + return createPlan(data); +} +export function fetchPlanDetail(planId: string) { + return getPlanDetail(planId); +} +export function updateScenarioPlan(planId: string, data: any) { + return updatePlan(planId, data); +} diff --git a/nkebao/src/pages/scenarios/plan/new/index.module.scss b/nkebao/src/pages/scenarios/plan/new/index.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/nkebao/src/pages/scenarios/plan/new/index.tsx b/nkebao/src/pages/scenarios/plan/new/index.tsx index 5d166527..e8739760 100644 --- a/nkebao/src/pages/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/scenarios/plan/new/index.tsx @@ -1,21 +1,20 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile"; -import { ArrowLeftOutlined } from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; - +import { LeftOutlined } from "@ant-design/icons"; +import { Button, Steps, message } from "antd"; import BasicSettings from "./steps/BasicSettings"; import FriendRequestSettings from "./steps/FriendRequestSettings"; import MessageSettings from "./steps/MessageSettings"; +import Layout from "@/components/Layout/Layout"; import { - getScenarioTypes, - createPlan, - updatePlan, - getPlanDetail, -} from "./page.api"; -import style from "./page.module.scss"; + getPlanScenes, + createScenarioPlan, + fetchPlanDetail, + PlanDetail, + updateScenarioPlan, +} from "./index.api"; -// 步骤定义 +// 步骤定义 - 只保留三个步骤 const steps = [ { id: 1, title: "步骤一", subtitle: "基础设置" }, { id: 2, title: "步骤二", subtitle: "好友申请设置" }, @@ -26,7 +25,7 @@ const steps = [ interface FormData { name: string; scenario: number; - posters: any[]; + posters: any[]; // 后续可替换为具体Poster类型 device: string[]; remarkType: string; greeting: string; @@ -39,8 +38,8 @@ interface FormData { addFriendInterval: number; } -const NewPlan: React.FC = () => { - const navigate = useNavigate(); +export default function NewPlan() { + const router = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [formData, setFormData] = useState({ name: "", @@ -64,57 +63,53 @@ const NewPlan: React.FC = () => { planId: string; }>(); const [isEdit, setIsEdit] = useState(false); - const [saving, setSaving] = useState(false); - useEffect(() => { loadData(); }, []); const loadData = async () => { setSceneLoading(true); - try { - // 获取场景类型 - const res = await getScenarioTypes(); - if (res?.data) { - setSceneList(res.data); - } - - if (planId) { - setIsEdit(true); - // 获取计划详情 - const detailRes = await getPlanDetail(planId); - if (detailRes.code === 200 && detailRes.data) { - const detail = detailRes.data; - setFormData((prev) => ({ - ...prev, - name: detail.name ?? "", - scenario: Number(detail.scenario) || 1, - posters: detail.posters ?? [], - device: detail.device ?? [], - remarkType: detail.remarkType ?? "phone", - greeting: detail.greeting ?? "", - addInterval: detail.addInterval ?? 1, - startTime: detail.startTime ?? "09:00", - endTime: detail.endTime ?? "18:00", - enabled: detail.enabled ?? true, - sceneId: Number(detail.scenario) || 1, - remarkFormat: detail.remarkFormat ?? "", - addFriendInterval: detail.addFriendInterval ?? 1, - })); - } - } else if (scenarioId) { + //获取场景类型 + getPlanScenes() + .then((data) => { + setSceneList(data || []); + }) + .catch((err) => { + message.error(err.message || "获取场景类型失败"); + }) + .finally(() => setSceneLoading(false)); + if (planId) { + setIsEdit(true); + //获取计划详情 + try { + const detail = await fetchPlanDetail(planId); setFormData((prev) => ({ ...prev, - scenario: Number(scenarioId) || 1, + name: detail.name ?? "", + scenario: Number(detail.scenario) || 1, + posters: detail.posters ?? [], + device: detail.device ?? [], + remarkType: detail.remarkType ?? "phone", + greeting: detail.greeting ?? "", + addInterval: detail.addInterval ?? 1, + startTime: detail.startTime ?? "09:00", + endTime: detail.endTime ?? "18:00", + enabled: detail.enabled ?? true, + sceneId: Number(detail.scenario) || 1, + remarkFormat: detail.remarkFormat ?? "", + addFriendInterval: detail.addFriendInterval ?? 1, + tips: detail.tips ?? "", + })); + } catch (err) { + message.error(err.message || "获取计划详情失败"); + } + } else { + if (scenarioId) { + setFormData((prev) => ({ + ...prev, + ...{ scenario: Number(scenarioId) || 1 }, })); } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setSceneLoading(false); } }; @@ -125,52 +120,35 @@ const NewPlan: React.FC = () => { // 处理保存 const handleSave = async () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - setSaving(true); try { let result; if (isEdit && planId) { - // 编辑 + // 编辑:拼接后端需要的完整参数 const editData = { ...formData, id: Number(planId), planId: Number(planId), + // 兼容后端需要的字段 + // 你可以根据实际需要补充其它字段 }; - result = await updatePlan(planId, editData); + result = await updateScenarioPlan(planId, editData); } else { // 新建 - result = await createPlan(formData); - } - - if (result.code === 200) { - Toast.show({ - content: isEdit ? "计划已更新" : "获客计划已创建", - position: "top", - }); - const sceneItem = sceneList.find((v) => formData.scenario === v.id); - navigate( - `/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}` - ); - } else { - Toast.show({ - content: result.msg || "操作失败", - position: "top", - }); + result = await createScenarioPlan(formData); } + message.success(isEdit ? "计划已更新" : "获客计划已创建"); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + router(`/scenarios/list/${formData.sceneId}/${sceneItem.name}`); } catch (error) { - Toast.show({ - content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试", - position: "top", - }); - } finally { - setSaving(false); + message.error( + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : isEdit + ? "更新计划失败,请重试" + : "创建计划失败,请重试" + ); } }; @@ -194,6 +172,7 @@ const NewPlan: React.FC = () => { case 1: return ( { ); default: @@ -225,50 +203,25 @@ const NewPlan: React.FC = () => { } }; - if (sceneLoading) { - return ( - -
- {isEdit ? "编辑计划" : "新建计划"} -
- - } - > -
- -
加载数据中...
-
-
- ); - } - return ( - - navigate(-1)} +
+
+
+
- } - > - - {isEdit ? "编辑计划" : "新建计划"} - - - - {/* 步骤指示器 */} -
+
+
+
- {steps.map((step) => ( + {steps.map((step, idx) => ( { } > -
- {/* 步骤内容 */} -
{renderStepContent()}
-
+
{renderStepContent()}
); -}; - -export default NewPlan; +} diff --git a/nkebao/src/pages/scenarios/plan/new/page.api.ts b/nkebao/src/pages/scenarios/plan/new/page.api.ts deleted file mode 100644 index 9d1411bf..00000000 --- a/nkebao/src/pages/scenarios/plan/new/page.api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import request from "@/api/request"; -// 获取场景类型列表 -export function getScenarioTypes() { - return request("/api/scenarios/types", undefined, "GET"); -} - -// 创建计划 -export function createPlan(data: any) { - return request("/api/scenarios/plans", data, "POST"); -} - -// 更新计划 -export function updatePlan(planId: string, data: any) { - return request(`/api/scenarios/plans/${planId}`, data, "PUT"); -} - -// 获取计划详情 -export function getPlanDetail(planId: string) { - return request(`/api/scenarios/plans/${planId}`, undefined, "GET"); -} diff --git a/nkebao/src/pages/scenarios/plan/new/page.module.scss b/nkebao/src/pages/scenarios/plan/new/page.module.scss deleted file mode 100644 index 0d6784fd..00000000 --- a/nkebao/src/pages/scenarios/plan/new/page.module.scss +++ /dev/null @@ -1,39 +0,0 @@ -.new-plan-page { -} - -.nav-title { - font-size: 18px; - font-weight: 600; - color: #333; -} - -.back-btn { - height: 32px; - width: 32px; - padding: 0; - border-radius: 50%; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 60vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.steps-container { - background: #ffffff; - margin-bottom: 12px; -} - -.step-content { - flex: 1; - padding: 0 16px; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss deleted file mode 100644 index 5720527d..00000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.basic-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.form-item { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - .adm-form-item-label { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 8px; - } - - .adm-input { - border-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } -} - -.time-input { - width: 120px; - border-radius: 8px; -} - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 40vh; - gap: 16px; -} - -.loading-text { - color: #666; - font-size: 14px; -} - -.actions { - padding: 20px 0; -} - -.next-btn { - width: 100%; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx index ef9bc167..1f989532 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,209 +1,858 @@ -import React, { useState, useEffect } from "react"; -import { - Form, - Input, - Selector, - Button, - SpinLoading, - Toast, - Card, - Space, -} from "antd-mobile"; -// import { getDevices, getPosters } from "./step.api"; -import style from "./BasicSettings.module.scss"; - -interface BasicSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - sceneList: any[]; - sceneLoading: boolean; -} - -const BasicSettings: React.FC = ({ - formData, - onChange, - onNext, - sceneList, - sceneLoading, -}) => { - const [devices, setDevices] = useState([]); - const [posters, setPosters] = useState([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - loadData(); - }, []); - - const loadData = async () => { - setLoading(true); - try { - // // 获取设备列表 - // const devicesRes = await getDevices(); - // if (devicesRes?.data) { - // setDevices(devicesRes.data); - // } - // // 获取海报列表 - // const postersRes = await getPosters(); - // if (postersRes?.data) { - // setPosters(postersRes.data); - // } - } catch (error) { - Toast.show({ - content: "加载数据失败", - position: "top", - }); - } finally { - setLoading(false); - } - }; - - const handleNext = () => { - if (!formData.name.trim()) { - Toast.show({ - content: "请输入计划名称", - position: "top", - }); - return; - } - - if (!formData.scenario) { - Toast.show({ - content: "请选择场景类型", - position: "top", - }); - return; - } - - if (formData.device.length === 0) { - Toast.show({ - content: "请选择设备", - position: "top", - }); - return; - } - - onNext(); - }; - - if (loading || sceneLoading) { - return ( -
- -
加载数据中...
-
- ); - } - - return ( -
- -
- {/* 计划名称 */} - - onChange({ name: value })} - clearable - /> - - - {/* 场景类型 */} - - ({ - label: scene.name, - value: scene.id, - }))} - value={[formData.scenario]} - onChange={(value) => { - const selectedScene = sceneList.find( - (scene) => scene.id === value[0] - ); - onChange({ - scenario: value[0], - sceneId: value[0], - name: selectedScene?.name || "", - }); - }} - /> - - - {/* 选择设备 */} - - ({ - label: device.name, - value: device.id, - }))} - value={formData.device} - onChange={(value) => onChange({ device: value })} - multiple - /> - - - {/* 选择海报 */} - - ({ - label: poster.name, - value: poster.id, - }))} - value={formData.posters} - onChange={(value) => onChange({ posters: value })} - multiple - /> - - - {/* 工作时间 */} - - - onChange({ startTime: value })} - className={style["time-input"]} - /> - - onChange({ endTime: value })} - className={style["time-input"]} - /> - - - - {/* 添加间隔 */} - - - onChange({ addInterval: Number(value) || 1 }) - } - min={1} - max={60} - /> - - -
- - {/* 操作按钮 */} -
- -
-
- ); -}; - -export default BasicSettings; +import React, { useState, useEffect, useRef } from "react"; +import { + Form, + Input, + Button, + Tag, + Switch, + // Upload, + Modal, + Alert, + Row, + Col, + message, +} from "antd"; +import { + PlusOutlined, + EyeOutlined, + CloseOutlined, + DownloadOutlined, + UploadOutlined, + CheckOutlined, +} from "@ant-design/icons"; +import { uploadFile } from "@/api/common"; + +interface BasicSettingsProps { + isEdit: boolean; + formData: any; + onChange: (data: any) => void; + onNext?: () => void; + sceneList: any[]; + sceneLoading: boolean; +} + +interface Account { + id: string; + nickname: string; + avatar: string; +} + +interface Material { + id: string; + name: string; + type: string; + preview: string; +} + +const posterTemplates = [ + { + id: "poster-1", + name: "点击领取", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", + }, + { + id: "poster-2", + name: "点击合作", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", + }, + { + id: "poster-3", + name: "点击咨询", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", + }, + { + id: "poster-4", + name: "点击签到", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", + }, + { + id: "poster-5", + name: "点击了解", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", + }, + { + id: "poster-6", + name: "点击报名", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", + }, +]; + +const generateRandomAccounts = (count: number): Account[] => { + return Array.from({ length: count }, (_, index) => ({ + id: `account-${index + 1}`, + nickname: `账号-${Math.random().toString(36).substring(2, 7)}`, + avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`, + })); +}; + +const generatePosterMaterials = (): Material[] => { + return posterTemplates.map((template) => ({ + id: template.id, + name: template.name, + type: "poster", + preview: template.preview, + })); +}; + +const BasicSettings: React.FC = ({ + isEdit, + formData, + onChange, + onNext, + sceneList, + sceneLoading, +}) => { + const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); + const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false); + const [accounts] = useState(generateRandomAccounts(50)); + const [materials] = useState(generatePosterMaterials()); + const [selectedAccounts, setSelectedAccounts] = useState( + formData.accounts?.length > 0 ? formData.accounts : [] + ); + const [selectedMaterials, setSelectedMaterials] = useState( + formData.materials?.length > 0 ? formData.materials : [] + ); + // showAllScenarios 默认为 true + const [showAllScenarios, setShowAllScenarios] = useState(true); + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + const [importedTags, setImportedTags] = useState< + Array<{ + phone: string; + wechat: string; + source?: string; + orderAmount?: number; + orderDate?: string; + }> + >(formData.importedTags || []); + + // 自定义标签相关状态 + const [customTagInput, setCustomTagInput] = useState(""); + const [customTags, setCustomTags] = useState(formData.customTags || []); + const [tips, setTips] = useState(formData.tips || ""); + const [selectedScenarioTags, setSelectedScenarioTags] = useState( + formData.scenarioTags || [] + ); + + // 电话获客相关状态 + const [phoneSettings, setPhoneSettings] = useState({ + autoAdd: formData.phoneSettings?.autoAdd ?? true, + speechToText: formData.phoneSettings?.speechToText ?? true, + questionExtraction: formData.phoneSettings?.questionExtraction ?? true, + }); + + // 群设置相关状态 + const [weixinqunName, setWeixinqunName] = useState( + formData.weixinqunName || "" + ); + const [weixinqunNotice, setWeixinqunNotice] = useState( + formData.weixinqunNotice || "" + ); + + // 新增:自定义海报相关状态 + const [customPosters, setCustomPosters] = useState([]); + const [previewUrl, setPreviewUrl] = useState(null); + + // 新增:用于文件选择的ref + const uploadInputRef = useRef(null); + const uploadOrderInputRef = useRef(null); + + // 更新电话获客设置 + const handlePhoneSettingsUpdate = () => { + onChange({ ...formData, phoneSettings }); + setIsPhoneSettingsOpen(false); + }; + + // 处理标签选择 + const handleTagToggle = (tagId: string) => { + const newTags = selectedScenarioTags.includes(tagId) + ? selectedScenarioTags.filter((id: string) => id !== tagId) + : [...selectedScenarioTags, tagId]; + + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 处理通话类型选择 + const handleCallTypeChange = (type: string) => { + // setPhoneCallType(type) // This line was removed as per the edit hint. + onChange({ ...formData, phoneCallType: type }); + }; + + // 初始化时,如果没有选择场景,默认选择海报获客 + useEffect(() => { + if (!formData.scenario) { + onChange({ ...formData, scenario: "haibao" }); + } + + // 检查是否已经有上传的订单文件 + if (formData.orderFileUploaded) { + setOrderUploaded(true); + } + }, [formData, onChange]); + + useEffect(() => { + const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); + const sceneItem = sceneList.find((v) => formData.scenario === v.id); + onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); + }, [isEdit]); + + useEffect(() => { + setTips(formData.tips || ""); + }, [formData.tips]); + + // 选中场景 + const handleScenarioSelect = (sceneId: number) => { + onChange({ ...formData, scenario: sceneId }); + }; + + // 选中/取消标签 + const handleScenarioTagToggle = (tag: string) => { + const newTags = selectedScenarioTags.includes(tag) + ? selectedScenarioTags.filter((t: string) => t !== tag) + : [...selectedScenarioTags, tag]; + setSelectedScenarioTags(newTags); + onChange({ ...formData, scenarioTags: newTags }); + }; + + // 添加自定义标签 + const handleAddCustomTag = () => { + if (!customTagInput.trim()) return; + const newTag = { + id: `custom-${Date.now()}`, + name: customTagInput.trim(), + }; + const updatedCustomTags = [...customTags, newTag]; + setCustomTags(updatedCustomTags); + setCustomTagInput(""); + onChange({ ...formData, customTags: updatedCustomTags }); + }; + + // 删除自定义标签 + const handleRemoveCustomTag = (tagId: string) => { + const updatedCustomTags = customTags.filter((tag: any) => tag.id !== tagId); + setCustomTags(updatedCustomTags); + onChange({ ...formData, customTags: updatedCustomTags }); + // 同时从选中标签中移除 + const updatedSelectedTags = selectedScenarioTags.filter( + (t: string) => t !== tagId + ); + setSelectedScenarioTags(updatedSelectedTags); + onChange({ + ...formData, + scenarioTags: updatedSelectedTags, + customTags: updatedCustomTags, + }); + }; + + // 新增:自定义上传图片 + const handleCustomPosterUpload = (urls: string[]) => { + if (urls && urls.length > 0) { + const newPoster: Material = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: urls[0], + }; + setCustomPosters((prev) => [...prev, newPoster]); + } + }; + + // 新增:删除自定义海报 + const handleRemoveCustomPoster = (id: string) => { + setCustomPosters((prev) => prev.filter((p) => p.id !== id)); + // 如果选中则取消选中 + if (selectedMaterials.some((m) => m.id === id)) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } + }; + + // 修改:选中/取消选中海报 + const handleMaterialSelect = (material: Material) => { + const isSelected = selectedMaterials.some((m) => m.id === material.id); + if (isSelected) { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + } else { + setSelectedMaterials([material]); + onChange({ ...formData, materials: [material] }); + } + }; + + // 移除已选素材 + const handleRemoveMaterial = (id: string) => { + setSelectedMaterials([]); + onChange({ ...formData, materials: [] }); + }; + + // 新增:全屏预览 + const handlePreviewImage = (url: string) => { + setPreviewUrl(url); + setIsPreviewOpen(true); + }; + + // 账号多选切换 + const handleAccountToggle = (account: Account) => { + const isSelected = selectedAccounts.some( + (a: Account) => a.id === account.id + ); + let newSelected; + if (isSelected) { + newSelected = selectedAccounts.filter( + (a: Account) => a.id !== account.id + ); + } else { + newSelected = [...selectedAccounts, account]; + } + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 移除已选账号 + const handleRemoveAccount = (id: string) => { + const newSelected = selectedAccounts.filter((a: Account) => a.id !== id); + setSelectedAccounts(newSelected); + onChange({ ...formData, accounts: newSelected }); + }; + + // 处理文件导入 + const handleFileImport = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const rows = content.split("\n").filter((row) => row.trim()); + const tags = rows.slice(1).map((row) => { + const [phone, wechat, source, orderAmount, orderDate] = + row.split(","); + return { + phone: phone?.trim(), + wechat: wechat?.trim(), + source: source?.trim(), + orderAmount: orderAmount ? Number(orderAmount) : undefined, + orderDate: orderDate?.trim(), + }; + }); + setImportedTags(tags); + onChange({ ...formData, importedTags: tags }); + } catch (error) { + // 可用 toast 提示 + } + }; + reader.readAsText(file); + } + }; + + // 下载模板 + const handleDownloadTemplate = () => { + const template = + "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"; + const blob = new Blob([template], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "订单导入模板.csv"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + // 修改订单表格上传逻辑,使用 uploadFile 公共方法 + const [orderUploaded, setOrderUploaded] = useState(false); + + const handleOrderFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (file) { + try { + await uploadFile(file); // 默认接口即可 + setOrderUploaded(true); + onChange({ ...formData, orderFileUploaded: true }); + // 可用 toast 或其它方式提示成功 + // alert('上传成功'); + } catch (err) { + // 可用 toast 或其它方式提示失败 + // alert('上传失败'); + } + event.target.value = ""; + } + }; + + // 账号弹窗关闭时清理搜索等状态 + const handleAccountDialogClose = () => { + setIsAccountDialogOpen(false); + // 可在此清理账号搜索等临时状态 + }; + // 素材弹窗关闭时清理搜索等状态 + const handleMaterialDialogClose = () => { + setIsMaterialDialogOpen(false); + // 可在此清理素材搜索等临时状态 + }; + // 订单导入弹窗关闭时清理文件输入等状态 + const handleImportDialogClose = () => { + setIsImportDialogOpen(false); + // 可在此清理文件输入等临时状态 + }; + // 电话获客弹窗关闭 + const handlePhoneSettingsDialogClose = () => { + setIsPhoneSettingsOpen(false); + }; + // 图片预览关闭 + const handleImagePreviewClose = () => { + setIsPreviewOpen(false); + }; + + // 当前选中的场景对象 + const currentScene = sceneList.find((s) => s.id === formData.scenario); + //打开订单 + const openOrder = + formData.scenario !== 2 ? { display: "none" } : { display: "block" }; + + const openPoster = + formData.scenario !== 1 ? { display: "none" } : { display: "block" }; + + return ( +
+ {/* 场景选择区块 */} + {sceneLoading ? ( +
加载中...
+ ) : ( + + {sceneList.map((scene) => ( +
+ + + ))} + + )} + + {/* 计划名称输入区 */} +
计划名称
+
+ + onChange({ ...formData, name: String(e.target.value) }) + } + placeholder="请输入计划名称" + /> +
+ +
获客标签(可多选)
+ {/* 标签选择区块 */} + {formData.scenario && ( +
+ {(currentScene?.scenarioTags || []).map((tag: string) => ( + handleScenarioTagToggle(tag)} + style={{ marginBottom: 4 }} + > + {tag} + + ))} + {/* 自定义标签 */} + {customTags.map((tag: any) => ( + handleScenarioTagToggle(tag.id)} + style={{ marginBottom: 4 }} + closable + onClose={() => handleRemoveCustomTag(tag.id)} + > + {tag.name} + + ))} +
+ )} + {/* 自定义标签输入区 */} +
+
+ setCustomTagInput(e.target.value)} + placeholder="添加自定义标签" + className="w-full" + /> +
+
+ +
+
+ + {/* 输入获客成功提示 */} +
+
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + className="w-full" + /> +
+
+ + {/* 选素材 */} +
+
选择海报
+
+ {[...materials, ...customPosters].map((material) => { + const isSelected = selectedMaterials.some( + (m) => m.id === material.id + ); + const isCustom = material.id.startsWith("custom-"); + return ( +
handleMaterialSelect(material)} + > + {/* 预览按钮:自定义海报在左上,内置海报在右上 */} + + {/* 删除自定义海报按钮 */} + {isCustom && ( + + )} + {material.name} +
+ {material.name} +
+
+ ); + })} + {/* 添加海报卡片 */} +
uploadInputRef.current?.click()} + > + + + + 添加海报 + { + const file = e.target.files?.[0]; + if (file) { + // 直接上传 + try { + const url = await uploadFile(file); + const newPoster = { + id: `custom-${Date.now()}`, + name: "自定义海报", + type: "poster", + preview: url, + }; + setCustomPosters((prev) => [...prev, newPoster]); + } catch (err) { + // 可加toast提示 + } + e.target.value = ""; + } + }} + /> +
+
+ {/* 全屏图片预览 */} + { + setIsPreviewOpen(false); + setPreviewUrl(null); + }} + footer={null} + width={800} + > + {previewUrl && ( + Preview + )} + +
+ {/* 订单导入区块优化 */} +
+
订单表格上传
+
+ + +
+
+ 支持 CSV、Excel 格式,上传后将文件保存到服务器 +
+
+ {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} + {formData.scenario === 5 && ( +
+
+
+ 电话获客设置 +
+
+
+ 自动加好友 + + setPhoneSettings((s) => ({ ...s, autoAdd: v })) + } + /> +
+
+ 语音转文字 + + setPhoneSettings((s) => ({ ...s, speechToText: v })) + } + /> +
+
+ 问题提取 + + setPhoneSettings((s) => ({ ...s, questionExtraction: v })) + } + /> +
+
+
+
+ )} + {/* 微信群设置区块,仅在选择微信群场景时显示 */} + {formData.scenario === 7 && ( +
+
+ onChange({ ...formData, weixinqunName })} + /> +
+
+ onChange({ ...formData, weixinqunNotice })} + /> +
+
+ )} + +
+ 是否启用 + onChange({ ...formData, enabled: value })} + /> +
+ + + ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss deleted file mode 100644 index 67047ef1..00000000 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.module.scss +++ /dev/null @@ -1,56 +0,0 @@ -.friend-request-settings { - padding: 16px 0; -} - -.form-card { - margin-bottom: 20px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.form-item { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - .adm-form-item-label { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 8px; - } - - .adm-input { - border-radius: 8px; - } - - .adm-selector { - border-radius: 8px; - } - - .adm-text-area { - border-radius: 8px; - } -} - -.actions { - padding: 20px 0; -} - -.prev-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} - -.next-btn { - flex: 1; - height: 48px; - border-radius: 24px; - font-size: 16px; - font-weight: 500; -} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx index fcb0bb63..f76e0fb3 100644 --- a/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx +++ b/nkebao/src/pages/scenarios/plan/new/steps/FriendRequestSettings.tsx @@ -1,113 +1,259 @@ -import React from "react"; -import { - Form, - Input, - Selector, - Button, - Card, - Space, - TextArea, -} from "antd-mobile"; -import style from "./FriendRequestSettings.module.scss"; - -interface FriendRequestSettingsProps { - formData: any; - onChange: (data: any) => void; - onNext: () => void; - onPrev: () => void; -} - -const FriendRequestSettings: React.FC = ({ - formData, - onChange, - onNext, - onPrev, -}) => { - const remarkTypeOptions = [ - { label: "手机号", value: "phone" }, - { label: "微信号", value: "wechat" }, - { label: "QQ号", value: "qq" }, - { label: "自定义", value: "custom" }, - ]; - - const handleNext = () => { - if (!formData.greeting.trim()) { - // 可以添加验证逻辑 - } - onNext(); - }; - - return ( -
- -
- {/* 备注类型 */} - - onChange({ remarkType: value[0] })} - /> - - - {/* 备注格式 */} - {formData.remarkType === "custom" && ( - - onChange({ remarkFormat: value })} - clearable - /> - - )} - - {/* 打招呼消息 */} - -