From dcb95b4d022d7622b7c3a93a165b423478b23328 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 21 Jul 2025 15:08:12 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/api/config/route.php | 3 --- Server/application/cunkebao/config/route.php | 8 +++++-- .../plan/PostExternalApiV1Controller.php | 3 ++- .../Adapters/ChuKeBao/Adapter.php | 22 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Server/application/api/config/route.php b/Server/application/api/config/route.php index f6fecbfa..79d3bc47 100644 --- a/Server/application/api/config/route.php +++ b/Server/application/api/config/route.php @@ -97,9 +97,6 @@ Route::group('v1', function () { Route::get('autoCreate', 'app\api\controller\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √ }); - Route::group('scenarios', function () { - Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index'); - }); }); }); \ No newline at end of file diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 69290e14..430c9516 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -118,8 +118,13 @@ Route::group('v1/', function () { -Route::group('v1/frontend', function () { +Route::group('v1/api/scenarios', function () { + Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index'); +}); + +//小程序 +Route::group('v1/frontend', function () { Route::group('business/poster', function () { Route::post('getone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPosterTaskData'); Route::post('decryptphone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPhoneNumber'); @@ -129,5 +134,4 @@ Route::group('v1/frontend', function () { - return []; \ No newline at end of file diff --git a/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php index 1d4c6a5a..10c91780 100644 --- a/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php @@ -96,7 +96,8 @@ class PostExternalApiV1Controller extends Controller if (!$trafficPool) { $trafficPoolId =Db::name('traffic_pool')->insertGetId([ 'identifier' => $identifier, - 'mobile' => $params['phone'] + 'mobile' => !empty($params['phone']) ? $params['phone'] : '', + 'createTime' => time() ]); }else{ $trafficPoolId = $trafficPool['id']; diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 495b7ae7..bc18298c 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -191,7 +191,6 @@ class Adapter implements WeChatServiceInterface } //筛选出设备在线微信在线 $wechatIdAccountIdMap = $this->getWeChatIdsAccountIdsMapByDeviceIds($task_info['reqConf']['device']); - if (empty($wechatIdAccountIdMap)) { continue; } @@ -213,11 +212,9 @@ class Adapter implements WeChatServiceInterface continue; } - - // 判断24h内加的好友数量,friend_task 先固定10个人 getLast24hAddedFriendsCount $last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId); - if ($last24hAddedFriendsCount >= 10) { + if ($last24hAddedFriendsCount >= 20) { continue; } @@ -233,14 +230,15 @@ class Adapter implements WeChatServiceInterface $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 break; } - Db::name('task_customer') + $res = Db::name('task_customer') ->where('id', $task['id']) ->update([ 'status' => $friendAddTaskCreated ? 1 : 3, 'fail_reason' => $friendAddTaskCreated ? '' : '已经是好友了', 'processed_wechat_ids' => $task['processed_wechat_ids'], - 'updated_at' => time() - ]); // ~~不用管,回头再添加再判断即可~~ + 'updateTime' => time() + ]); + // ~~不用管,回头再添加再判断即可~~ // 失败一定是另一个进程/定时器在检查的 } @@ -254,9 +252,9 @@ class Adapter implements WeChatServiceInterface $tasks = Db::name('task_customer') ->whereIn('status', [1,2]) - ->where('updated_at', '>=', (time() - 86400 * 3)) + ->where('updateTime', '>=', (time() - 86400 * 3)) ->limit(50) - ->order('updated_at DESC') + ->order('updateTime DESC') ->select(); if (empty($tasks)) { @@ -296,7 +294,7 @@ class Adapter implements WeChatServiceInterface Db::name('task_customer') ->where('id', $task['id']) - ->update(['status' => 4, 'updated_at' => time()]); + ->update(['status' => 4, 'updateTime' => time()]); $wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']); $msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf']; @@ -316,7 +314,7 @@ class Adapter implements WeChatServiceInterface if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 1) { Db::name('task_customer') ->where('id', $task['id']) - ->update(['status' => 2, 'updated_at' => time()]); + ->update(['status' => 2, 'updateTime' => time()]); break; } @@ -324,7 +322,7 @@ class Adapter implements WeChatServiceInterface if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 2) { Db::name('task_customer') ->where('id', $task['id']) - ->update(['status' => 3, 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', 'updated_at' => time()]); + ->update(['status' => 3, 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', 'updateTime' => time()]); break; } } From 368e34cbdc77d1d54626be4942013c948a8738c8 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 21 Jul 2025 15:21:19 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=8F=90=E7=A4=BA=E8=AF=AD=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index b05b981f..e471356f 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -150,13 +150,18 @@ class PosterWeChatMiniProgram extends Controller } + if(isset($sceneConf['tips'])) { + $sTip = $sceneConf['tips']; + } else { + $sTip = ''; + } $data = [ 'id' => $task['id'], 'name' => $task['name'], 'poster' => ['sUrl' => $posterUrl], - 'sTip' => '', + 'sTip' => $sTip, ]; From 1cc04366fcba09efce9bb2f17dbf13bef9832132 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: Mon, 21 Jul 2025 15:32:21 +0800 Subject: [PATCH 03/13] =?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=A2=9E=E5=8A=A0tips=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/src/pages/scenarios/new/page.tsx | 2 ++ .../scenarios/new/steps/BasicSettings.tsx | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Cunkebao/src/pages/scenarios/new/page.tsx b/Cunkebao/src/pages/scenarios/new/page.tsx index ddbe3578..59b7e284 100644 --- a/Cunkebao/src/pages/scenarios/new/page.tsx +++ b/Cunkebao/src/pages/scenarios/new/page.tsx @@ -98,6 +98,7 @@ export default function NewPlan() { sceneId: Number(detail.scenario) || 1, remarkFormat: detail.remarkFormat ?? "", addFriendInterval: detail.addFriendInterval ?? 1, + tips: detail.tips ?? "", })); } } else { @@ -178,6 +179,7 @@ export default function NewPlan() { case 1: return ( void; onNext?: () => void; @@ -89,6 +90,7 @@ const generatePosterMaterials = (): Material[] => { }; export function BasicSettings({ + isEdit, formData, onChange, onNext, @@ -123,6 +125,7 @@ export function BasicSettings({ // 自定义标签相关状态 const [customTagInput, setCustomTagInput] = useState(""); const [customTags, setCustomTags] = useState(formData.customTags || []); + const [tips, setTips] = useState(formData.tips || ""); const [selectedScenarioTags, setSelectedScenarioTags] = useState( formData.scenarioTags || [] ); @@ -188,7 +191,11 @@ export function BasicSettings({ const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); const sceneItem = sceneList.find((v) => formData.scenario === v.id); onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); - }, [sceneList]); + }, [isEdit]); + + useEffect(() => { + setTips(formData.tips || ""); + }, [formData.tips]); // 选中场景 const handleScenarioSelect = (sceneId: number) => { @@ -494,6 +501,22 @@ export function BasicSettings({ + {/* 输入获客成功提示 */} +
+
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + className="w-full" + /> +
+
+ {/* 选素材 */}
选择海报
From 63c64fdd1ef150851047422d1dff33c911d9774b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 21 Jul 2025 16:03:04 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index e471356f..da954377 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -126,7 +126,10 @@ class PosterWeChatMiniProgram extends Controller // todo 获取海报获客任务的任务/海报数据 -- 表还没设计好,不急 ck_customer_acquisition_task public function getPosterTaskData() { $id = request()->param('id'); - $task = Db::name('customer_acquisition_task')->where(['id' => $id,'deleteTime' => 0])->find(); + $task = Db::name('customer_acquisition_task') + ->where(['id' => $id,'deleteTime' => 0]) + ->field('id,name,sceneConf,status') + ->find(); if (!$task) { return json([ 'code' => 400, @@ -156,12 +159,14 @@ class PosterWeChatMiniProgram extends Controller $sTip = ''; } + unset($task['sceneConf']); + $task['sTip'] = $sTip; $data = [ 'id' => $task['id'], 'name' => $task['name'], 'poster' => ['sUrl' => $posterUrl], - 'sTip' => $sTip, + 'task' => $task, ]; From 44e4c54df7b488daa71c8c09c5c2055fc056b2f2 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: Mon, 21 Jul 2025 16:43:59 +0800 Subject: [PATCH 05/13] =?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=BF=81=E7=A7=BB=E7=A4=BE=E7=BE=A4=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/groupPush.ts | 73 ++++ .../src/pages/workspace/group-push/Edit.tsx | 250 +++++++++++ .../group-push/GroupPush.module.scss | 100 +++++ .../pages/workspace/group-push/GroupPush.tsx | 403 +++++++++++++++++- .../workspace/group-push/detail/index.tsx | 258 +++++++++++ .../form/components/BasicSettings.tsx | 237 ++++++++++ .../form/components/ContentSelector.tsx | 247 +++++++++++ .../form/components/GroupSelector.tsx | 245 +++++++++++ .../form/components/StepIndicator.tsx | 24 ++ .../pages/workspace/group-push/form/index.tsx | 224 ++++++++++ nkebao/src/pages/workspace/group-push/new.tsx | 8 - nkebao/src/router/module/workspace.tsx | 13 +- 12 files changed, 2058 insertions(+), 24 deletions(-) create mode 100644 nkebao/src/api/groupPush.ts create mode 100644 nkebao/src/pages/workspace/group-push/Edit.tsx create mode 100644 nkebao/src/pages/workspace/group-push/GroupPush.module.scss create mode 100644 nkebao/src/pages/workspace/group-push/detail/index.tsx create mode 100644 nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx create mode 100644 nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx create mode 100644 nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx create mode 100644 nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx create mode 100644 nkebao/src/pages/workspace/group-push/form/index.tsx delete mode 100644 nkebao/src/pages/workspace/group-push/new.tsx diff --git a/nkebao/src/api/groupPush.ts b/nkebao/src/api/groupPush.ts new file mode 100644 index 00000000..00ab3620 --- /dev/null +++ b/nkebao/src/api/groupPush.ts @@ -0,0 +1,73 @@ +import request from "./request"; + +export interface GroupPushTask { + id: string; + name: string; + status: number; // 1: 运行中, 2: 已暂停 + deviceCount: number; + targetGroups: string[]; + pushCount: number; + successCount: number; + lastPushTime: string; + createTime: string; + creator: string; + pushInterval: number; + maxPushPerDay: number; + timeRange: { start: string; end: string }; + messageType: "text" | "image" | "video" | "link"; + messageContent: string; + targetTags: string[]; + pushMode: "immediate" | "scheduled"; + scheduledTime?: string; +} + +interface ApiResponse { + code: number; + message: string; + data: T; +} + +export async function fetchGroupPushTasks(): Promise { + const response = await request("/v1/workbench/list", { type: 3 }, "GET"); + if (Array.isArray(response)) return response; + if (response && Array.isArray(response.data)) return response.data; + return []; +} + +export async function deleteGroupPushTask(id: string): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`, {}, "DELETE"); +} + +export async function toggleGroupPushTask( + id: string, + status: string +): Promise { + return request( + `/v1/workspace/group-push/tasks/${id}/toggle`, + { status }, + "POST" + ); +} + +export async function copyGroupPushTask(id: string): Promise { + return request(`/v1/workspace/group-push/tasks/${id}/copy`, {}, "POST"); +} + +export async function createGroupPushTask( + taskData: Partial +): Promise { + return request("/v1/workspace/group-push/tasks", taskData, "POST"); +} + +export async function updateGroupPushTask( + id: string, + taskData: Partial +): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT"); +} + +export async function getGroupPushTaskDetail( + id: string +): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`); +} diff --git a/nkebao/src/pages/workspace/group-push/Edit.tsx b/nkebao/src/pages/workspace/group-push/Edit.tsx new file mode 100644 index 00000000..0625950f --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/Edit.tsx @@ -0,0 +1,250 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Button, Spin, message } from "antd"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import StepIndicator from "./form/components/StepIndicator"; +import BasicSettings from "./form/components/BasicSettings"; +import GroupSelector from "./form/components/GroupSelector"; +import ContentSelector from "./form/components/ContentSelector"; +import { + getGroupPushTaskDetail, + updateGroupPushTask, + GroupPushTask, +} from "@/api/groupPush"; + +const steps = [ + { id: 1, title: "步骤 1", subtitle: "基础设置" }, + { id: 2, title: "步骤 2", subtitle: "选择社群" }, + { id: 3, title: "步骤 3", subtitle: "选择内容库" }, + { id: 4, title: "步骤 4", subtitle: "京东联盟" }, +]; + +const EditGroupPush: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [formData, setFormData] = useState(null); + + useEffect(() => { + if (!id) return; + setLoading(true); + getGroupPushTaskDetail(id) + .then((res) => { + const task = res.data || res; + setFormData({ + name: task.name, + pushTimeStart: task.timeRange?.start || "06:00", + pushTimeEnd: task.timeRange?.end || "23:59", + dailyPushCount: task.maxPushPerDay, + pushOrder: task.pushOrder || "latest", + isLoopPush: task.isLoopPush || false, + isImmediatePush: task.pushMode === "immediate", + isEnabled: task.isEnabled || false, + groups: (task.targetGroups || []).map( + (name: string, idx: number) => ({ + id: String(idx + 1), + name, + avatar: "", + serviceAccount: { id: "", name: "", avatar: "" }, + }) + ), + contentLibraries: (task.contentLibraries || []).map( + (name: string, idx: number) => ({ + id: String(idx + 1), + name, + targets: [], + }) + ), + }); + }) + .finally(() => setLoading(false)); + }, [id]); + + const handleBasicSettingsNext = (values: any) => { + setFormData((prev: any) => ({ ...prev, ...values })); + setCurrentStep(2); + }; + const handleGroupsChange = (groups: any[]) => { + setFormData((prev: any) => ({ ...prev, groups })); + }; + const handleLibrariesChange = (contentLibraries: any[]) => { + setFormData((prev: any) => ({ ...prev, contentLibraries })); + }; + const handleSave = async () => { + if (!formData.name.trim()) { + message.error("请输入任务名称"); + return; + } + if (!formData.groups || formData.groups.length === 0) { + message.error("请选择至少一个社群"); + return; + } + if (!formData.contentLibraries || formData.contentLibraries.length === 0) { + message.error("请选择至少一个内容库"); + return; + } + setSaving(true); + try { + const apiData = { + name: formData.name, + timeRange: { + start: formData.pushTimeStart, + end: formData.pushTimeEnd, + }, + maxPushPerDay: formData.dailyPushCount, + pushOrder: formData.pushOrder, + isLoopPush: formData.isLoopPush, + isImmediatePush: formData.isImmediatePush, + isEnabled: formData.isEnabled, + targetGroups: formData.groups.map((g: any) => g.name), + contentLibraries: formData.contentLibraries.map((c: any) => c.name), + pushMode: formData.isImmediatePush + ? ("immediate" as const) + : ("scheduled" as const), + messageType: "text" as const, + messageContent: "", + targetTags: [], + pushInterval: 60, + }; + const response = await updateGroupPushTask(id!, apiData); + if (response.code === 200) { + message.success("保存成功"); + navigate("/workspace/group-push"); + } else { + message.error("保存失败,请稍后重试"); + } + } catch (error) { + message.error("保存失败,请稍后重试"); + } finally { + setSaving(false); + } + }; + const handleCancel = () => { + navigate("/workspace/group-push"); + }; + + if (loading || !formData) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 编辑群发推送 +
+ } + footer={} + > +
+ +
+ + ); + } + + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 编辑群发推送 + + } + footer={} + > +
+ +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={() => setCurrentStep(3)} + onSave={handleSave} + onCancel={handleCancel} + loading={saving} + /> + )} + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={() => setCurrentStep(4)} + onSave={handleSave} + onCancel={handleCancel} + loading={saving} + /> + )} + {currentStep === 4 && ( +
+ 京东联盟设置(此步骤为占位,实际功能待开发) +
+ + + +
+
+ )} +
+
+
+ ); +}; + +export default EditGroupPush; diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.module.scss b/nkebao/src/pages/workspace/group-push/GroupPush.module.scss new file mode 100644 index 00000000..3ea9f972 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/GroupPush.module.scss @@ -0,0 +1,100 @@ + + +.searchBar { + display: flex; + align-items: center; + gap: 8px; + padding: 16px 0 8px 0; +} + +.taskList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.emptyCard { + text-align: center; + padding: 48px 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.taskCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + padding: 20px 16px 12px 16px; +} + +.taskHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.taskTitle { + display: flex; + align-items: center; + font-size: 16px; + font-weight: 600; +} + +.taskActions { + display: flex; + align-items: center; + gap: 8px; +} + +.taskInfoGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px 16px; + font-size: 13px; + color: #666; + margin-bottom: 12px; +} + +.progressBlock { + margin-bottom: 12px; +} + +.progressLabel { + font-size: 13px; + color: #888; + margin-bottom: 4px; +} + +.taskFooter { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: #888; + border-top: 1px dashed #eee; + padding-top: 8px; + margin-top: 8px; +} + +.expandedPanel { + margin-top: 16px; + padding-top: 16px; + border-top: 1px dashed #eee; +} + +.expandedGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +@media (max-width: 600px) { + .taskCard { + padding: 12px 6px 8px 6px; + } + .expandedGrid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.tsx b/nkebao/src/pages/workspace/group-push/GroupPush.tsx index 382dab52..38e36029 100644 --- a/nkebao/src/pages/workspace/group-push/GroupPush.tsx +++ b/nkebao/src/pages/workspace/group-push/GroupPush.tsx @@ -1,10 +1,393 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const GroupPush: React.FC = () => { - return ( - - ); -}; - -export default GroupPush; +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { + PlusOutlined, + SearchOutlined, + ReloadOutlined, + MoreOutlined, + ClockCircleOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + CopyOutlined, + DownOutlined, + UpOutlined, + SettingOutlined, + CalendarOutlined, + TeamOutlined, + MessageOutlined, + SendOutlined, +} from "@ant-design/icons"; +import { + Card, + Button, + Input, + Badge, + Switch, + Progress, + Dropdown, + Menu, +} from "antd"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchGroupPushTasks, + deleteGroupPushTask, + toggleGroupPushTask, + copyGroupPushTask, + GroupPushTask, +} from "@/api/groupPush"; +import styles from "./GroupPush.module.scss"; + +const { Search } = Input; + +const GroupPush: React.FC = () => { + const navigate = useNavigate(); + const [expandedTaskId, setExpandedTaskId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchTasks = async () => { + setLoading(true); + try { + const list = await fetchGroupPushTasks(); + setTasks(list); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTasks(); + }, []); + + const toggleExpand = (taskId: string) => { + setExpandedTaskId(expandedTaskId === taskId ? null : taskId); + }; + + const handleDelete = async (taskId: string) => { + if (!window.confirm("确定要删除该任务吗?")) return; + await deleteGroupPushTask(taskId); + fetchTasks(); + }; + + const handleEdit = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}/edit`); + }; + + const handleView = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}`); + }; + + const handleCopy = async (taskId: string) => { + await copyGroupPushTask(taskId); + fetchTasks(); + }; + + const toggleTaskStatus = async (taskId: string) => { + const task = tasks.find((t) => t.id === taskId); + if (!task) return; + const newStatus = task.status === 1 ? 2 : 1; + await toggleGroupPushTask(taskId, String(newStatus)); + fetchTasks(); + }; + + const handleCreateNew = () => { + navigate("/workspace/group-push/new"); + }; + + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const getStatusColor = (status: number) => { + switch (status) { + case 1: + return "green"; + case 2: + return "gray"; + default: + return "gray"; + } + }; + + const getStatusText = (status: number) => { + switch (status) { + case 1: + return "进行中"; + case 2: + return "已暂停"; + default: + return "未知"; + } + }; + + const getMessageTypeText = (type: string) => { + switch (type) { + case "text": + return "文字"; + case "image": + return "图片"; + case "video": + return "视频"; + case "link": + return "链接"; + default: + return "未知"; + } + }; + + const getSuccessRate = (pushCount: number, successCount: number) => { + if (pushCount === 0) return 0; + return Math.round((successCount / pushCount) * 100); + }; + + return ( + +
+ 群消息推送 + +
+ + } + footer={} + > +
+
+ setSearchTerm(e.target.value)} + enterButton={} + style={{ maxWidth: 320 }} + /> +
+
+ {filteredTasks.length === 0 ? ( + + +
+ 暂无推送任务 +
+
+ 创建您的第一个群消息推送任务 +
+ +
+ ) : ( + filteredTasks.map((task) => ( + +
+
+ {task.name} + +
+
+ toggleTaskStatus(task.id)} + /> + + } + onClick={() => handleView(task.id)} + > + 查看 + + } + onClick={() => handleEdit(task.id)} + > + 编辑 + + } + onClick={() => handleCopy(task.id)} + > + 复制 + + } + onClick={() => handleDelete(task.id)} + danger + > + 删除 + + + } + trigger={["click"]} + > +
+
+
+
执行设备:{task.deviceCount} 个
+
目标群组:{task.targetGroups.length} 个
+
+ 推送成功:{task.successCount}/{task.pushCount} +
+
创建人:{task.creator}
+
+
+
推送成功率
+ +
+
+
+ 上次推送:{task.lastPushTime} +
+
+ 创建时间:{task.createTime} +
+
+ {expandedTaskId === task.id && ( +
+
+
+ 基本设置 +
推送间隔:{task.pushInterval} 秒
+
每日最大推送数:{task.maxPushPerDay} 条
+
+ 执行时间段:{task.timeRange.start} -{" "} + {task.timeRange.end} +
+
+ 推送模式: + {task.pushMode === "immediate" + ? "立即推送" + : "定时推送"} +
+ {task.scheduledTime && ( +
定时时间:{task.scheduledTime}
+ )} +
+
+ 目标群组 +
+ {task.targetGroups.map((group) => ( + + ))} +
+
+
+ 消息内容 +
+ 消息类型:{getMessageTypeText(task.messageType)} +
+
+ {task.messageContent} +
+
+
+ 执行进度 +
+ 今日已推送:{task.pushCount} / {task.maxPushPerDay} +
+ + {task.targetTags.length > 0 && ( +
+
目标标签:
+
+ {task.targetTags.map((tag) => ( + + ))} +
+
+ )} +
+
+
+ )} +
+ )) + )} +
+
+
+ ); +}; + +export default GroupPush; diff --git a/nkebao/src/pages/workspace/group-push/detail/index.tsx b/nkebao/src/pages/workspace/group-push/detail/index.tsx new file mode 100644 index 00000000..be665447 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/detail/index.tsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Card, Badge, Button, Progress, Spin } from "antd"; +import { + ArrowLeftOutlined, + SettingOutlined, + TeamOutlined, + MessageOutlined, + CalendarOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { getGroupPushTaskDetail, GroupPushTask } from "@/api/groupPush"; +import styles from "./GroupPush.module.scss"; + +const Detail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [task, setTask] = useState(null); + + useEffect(() => { + if (!id) return; + setLoading(true); + getGroupPushTaskDetail(id) + .then((res) => { + setTask(res.data || res); // 兼容两种返回格式 + }) + .finally(() => setLoading(false)); + }, [id]); + + if (loading) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ +
+
+ ); + } + if (!task) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ 未找到该任务 +
+
+ ); + } + + const getStatusColor = (status: number) => { + switch (status) { + case 1: + return "green"; + case 2: + return "gray"; + default: + return "gray"; + } + }; + const getStatusText = (status: number) => { + switch (status) { + case 1: + return "进行中"; + case 2: + return "已暂停"; + default: + return "未知"; + } + }; + const getMessageTypeText = (type: string) => { + switch (type) { + case "text": + return "文字"; + case "image": + return "图片"; + case "video": + return "视频"; + case "link": + return "链接"; + default: + return "未知"; + } + }; + const getSuccessRate = (pushCount: number, successCount: number) => { + if (pushCount === 0) return 0; + return Math.round((successCount / pushCount) * 100); + }; + + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ +
+
+ {task.name} + +
+
+
+
执行设备:{task.deviceCount} 个
+
目标群组:{task.targetGroups.length} 个
+
+ 推送成功:{task.successCount}/{task.pushCount} +
+
创建人:{task.creator}
+
+
+
推送成功率
+ +
+
+
+ 上次推送:{task.lastPushTime} +
+
创建时间:{task.createTime}
+
+
+
+
+ 基本设置 +
推送间隔:{task.pushInterval} 秒
+
每日最大推送数:{task.maxPushPerDay} 条
+
+ 执行时间段:{task.timeRange.start} - {task.timeRange.end} +
+
+ 推送模式: + {task.pushMode === "immediate" ? "立即推送" : "定时推送"} +
+ {task.scheduledTime && ( +
定时时间:{task.scheduledTime}
+ )} +
+
+ 目标群组 +
+ {task.targetGroups.map((group) => ( + + ))} +
+
+
+ 消息内容 +
消息类型:{getMessageTypeText(task.messageType)}
+
+ {task.messageContent} +
+
+
+ 执行进度 +
+ 今日已推送:{task.pushCount} / {task.maxPushPerDay} +
+ + {task.targetTags.length > 0 && ( +
+
目标标签:
+
+ {task.targetTags.map((tag) => ( + + ))} +
+
+ )} +
+
+
+
+
+
+ ); +}; + +export default Detail; diff --git a/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx new file mode 100644 index 00000000..a7fb8610 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx @@ -0,0 +1,237 @@ +import React, { useState } from "react"; +import { Input, Button, Card, Switch } from "antd"; +import { MinusOutlined, PlusOutlined } from "@ant-design/icons"; + +interface BasicSettingsProps { + defaultValues?: { + name: string; + pushTimeStart: string; + pushTimeEnd: string; + dailyPushCount: number; + pushOrder: "earliest" | "latest"; + isLoopPush: boolean; + isImmediatePush: boolean; + isEnabled: boolean; + }; + onNext: (values: any) => void; + onSave: (values: any) => void; + onCancel: () => void; + loading?: boolean; +} + +const BasicSettings: React.FC = ({ + defaultValues = { + name: "", + pushTimeStart: "06:00", + pushTimeEnd: "23:59", + dailyPushCount: 20, + pushOrder: "latest", + isLoopPush: false, + isImmediatePush: false, + isEnabled: false, + }, + onNext, + onSave, + onCancel, + loading = false, +}) => { + const [values, setValues] = useState(defaultValues); + + const handleChange = (field: string, value: any) => { + setValues((prev) => ({ ...prev, [field]: value })); + }; + + const handleCountChange = (increment: boolean) => { + setValues((prev) => ({ + ...prev, + dailyPushCount: increment + ? prev.dailyPushCount + 1 + : Math.max(1, prev.dailyPushCount - 1), + })); + }; + + return ( +
+ +
+ {/* 任务名称 */} +
+ *任务名称: + handleChange("name", e.target.value)} + placeholder="请输入任务名称" + style={{ marginTop: 4 }} + /> +
+ {/* 允许推送的时间段 */} +
+ 允许推送的时间段: +
+ handleChange("pushTimeStart", e.target.value)} + style={{ width: 120 }} + /> + + handleChange("pushTimeEnd", e.target.value)} + style={{ width: 120 }} + /> +
+
+ {/* 每日推送 */} +
+ 每日推送: +
+
+
+ {/* 推送顺序 */} +
+ 推送顺序: + + + + +
+ {/* 是否循环推送 */} +
+ + * + 是否循环推送: + + handleChange("isLoopPush", checked)} + disabled={loading} + /> +
+ {/* 是否立即推送 */} +
+ + * + 是否立即推送: + + handleChange("isImmediatePush", checked)} + disabled={loading} + /> +
+ {values.isImmediatePush && ( +
+ 如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群 +
+ )} + {/* 是否启用 */} +
+ + *是否启用: + + handleChange("isEnabled", checked)} + disabled={loading} + /> +
+
+
+
+ + + +
+
+ ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx new file mode 100644 index 00000000..863d5b5b --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx @@ -0,0 +1,247 @@ +import React, { useState } from "react"; +import { Button, Card, Input, Checkbox, Avatar } from "antd"; +import { FileTextOutlined, SearchOutlined } from "@ant-design/icons"; + +interface ContentLibrary { + id: string; + name: string; + targets: Array<{ + id: string; + avatar: string; + }>; +} + +interface ContentSelectorProps { + selectedLibraries: ContentLibrary[]; + onLibrariesChange: (libraries: ContentLibrary[]) => void; + onPrevious: () => void; + onNext: () => void; + onSave: () => void; + onCancel: () => void; + loading?: boolean; +} + +const mockLibraries: ContentLibrary[] = [ + { + id: "1", + name: "产品推广内容库", + targets: [ + { id: "1", avatar: "https://via.placeholder.com/32" }, + { id: "2", avatar: "https://via.placeholder.com/32" }, + { id: "3", avatar: "https://via.placeholder.com/32" }, + ], + }, + { + id: "2", + name: "活动宣传内容库", + targets: [ + { id: "4", avatar: "https://via.placeholder.com/32" }, + { id: "5", avatar: "https://via.placeholder.com/32" }, + ], + }, + { + id: "3", + name: "客户服务内容库", + targets: [ + { id: "6", avatar: "https://via.placeholder.com/32" }, + { id: "7", avatar: "https://via.placeholder.com/32" }, + { id: "8", avatar: "https://via.placeholder.com/32" }, + { id: "9", avatar: "https://via.placeholder.com/32" }, + ], + }, + { + id: "4", + name: "节日问候内容库", + targets: [ + { id: "10", avatar: "https://via.placeholder.com/32" }, + { id: "11", avatar: "https://via.placeholder.com/32" }, + ], + }, + { + id: "5", + name: "新品发布内容库", + targets: [ + { id: "12", avatar: "https://via.placeholder.com/32" }, + { id: "13", avatar: "https://via.placeholder.com/32" }, + { id: "14", avatar: "https://via.placeholder.com/32" }, + ], + }, +]; + +const ContentSelector: React.FC = ({ + selectedLibraries, + onLibrariesChange, + onPrevious, + onNext, + onSave, + onCancel, + loading = false, +}) => { + const [searchTerm, setSearchTerm] = useState(""); + const [libraries] = useState(mockLibraries); + + const filteredLibraries = libraries.filter((library) => + library.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => { + if (checked) { + onLibrariesChange([...selectedLibraries, library]); + } else { + onLibrariesChange(selectedLibraries.filter((l) => l.id !== library.id)); + } + }; + + const handleSelectAll = () => { + if (selectedLibraries.length === filteredLibraries.length) { + onLibrariesChange([]); + } else { + onLibrariesChange(filteredLibraries); + } + }; + + const isLibrarySelected = (libraryId: string) => { + return selectedLibraries.some((library) => library.id === libraryId); + }; + + return ( +
+ +
+
+ 搜索内容库: + } + placeholder="搜索内容库名称" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + disabled={loading} + style={{ marginTop: 4 }} + /> +
+
+ 0 + } + onChange={handleSelectAll} + disabled={loading} + > + 全选 ({selectedLibraries.length}/{filteredLibraries.length}) + +
+
+ {filteredLibraries.map((library) => ( +
+ + handleLibraryToggle(library, e.target.checked) + } + disabled={loading} + style={{ marginRight: 8 }} + /> + } + size={40} + style={{ + marginRight: 8, + background: "#e6f7ff", + color: "#1890ff", + }} + /> +
+
{library.name}
+
+ 包含 {library.targets.length} 条内容 +
+
+
+ {library.targets.slice(0, 3).map((target) => ( + + ))} + {library.targets.length > 3 && ( +
+ +{library.targets.length - 3} +
+ )} +
+
+ ))} + {filteredLibraries.length === 0 && ( +
+ + 没有找到匹配的内容库 +
+ )} +
+
+
+
+ + + + +
+
+ ); +}; + +export default ContentSelector; diff --git a/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx b/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx new file mode 100644 index 00000000..aed8b2bd --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx @@ -0,0 +1,245 @@ +import React, { useState } from "react"; +import { Button, Card, Input, Checkbox, Avatar } from "antd"; +import { TeamOutlined, SearchOutlined } from "@ant-design/icons"; + +interface WechatGroup { + id: string; + name: string; + avatar: string; + serviceAccount: { + id: string; + name: string; + avatar: string; + }; +} + +interface GroupSelectorProps { + selectedGroups: WechatGroup[]; + onGroupsChange: (groups: WechatGroup[]) => void; + onPrevious: () => void; + onNext: () => void; + onSave: () => void; + onCancel: () => void; + loading?: boolean; +} + +const mockGroups: WechatGroup[] = [ + { + id: "1", + name: "VIP客户群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "1", + name: "客服小美", + avatar: "https://via.placeholder.com/32", + }, + }, + { + id: "2", + name: "潜在客户群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "1", + name: "客服小美", + avatar: "https://via.placeholder.com/32", + }, + }, + { + id: "3", + name: "活动群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "2", + name: "推广专员", + avatar: "https://via.placeholder.com/32", + }, + }, + { + id: "4", + name: "推广群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "2", + name: "推广专员", + avatar: "https://via.placeholder.com/32", + }, + }, + { + id: "5", + name: "新客户群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "3", + name: "销售小王", + avatar: "https://via.placeholder.com/32", + }, + }, + { + id: "6", + name: "体验群", + avatar: "https://via.placeholder.com/40", + serviceAccount: { + id: "3", + name: "销售小王", + avatar: "https://via.placeholder.com/32", + }, + }, +]; + +const GroupSelector: React.FC = ({ + selectedGroups, + onGroupsChange, + onPrevious, + onNext, + onSave, + onCancel, + loading = false, +}) => { + const [searchTerm, setSearchTerm] = useState(""); + const [groups] = useState(mockGroups); + + const filteredGroups = groups.filter( + (group) => + group.name.toLowerCase().includes(searchTerm.toLowerCase()) || + group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleGroupToggle = (group: WechatGroup, checked: boolean) => { + if (checked) { + onGroupsChange([...selectedGroups, group]); + } else { + onGroupsChange(selectedGroups.filter((g) => g.id !== group.id)); + } + }; + + const handleSelectAll = () => { + if (selectedGroups.length === filteredGroups.length) { + onGroupsChange([]); + } else { + onGroupsChange(filteredGroups); + } + }; + + const isGroupSelected = (groupId: string) => { + return selectedGroups.some((group) => group.id === groupId); + }; + + return ( +
+ +
+
+ 搜索群组: + } + placeholder="搜索群组名称或客服名称" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + disabled={loading} + style={{ marginTop: 4 }} + /> +
+
+ 0 + } + onChange={handleSelectAll} + disabled={loading} + > + 全选 ({selectedGroups.length}/{filteredGroups.length}) + +
+
+ {filteredGroups.map((group) => ( +
+ handleGroupToggle(group, e.target.checked)} + disabled={loading} + style={{ marginRight: 8 }} + /> + } + style={{ marginRight: 8 }} + /> +
+
{group.name}
+
+ + {group.serviceAccount.name} +
+
+
+ ))} + {filteredGroups.length === 0 && ( +
+ + 没有找到匹配的群组 +
+ )} +
+
+
+
+ + + + +
+
+ ); +}; + +export default GroupSelector; diff --git a/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx new file mode 100644 index 00000000..dc293d2e --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Steps } from "antd"; + +interface StepIndicatorProps { + currentStep: number; + steps: { id: number; title: string; subtitle: string }[]; +} + +const StepIndicator: React.FC = ({ + currentStep, + steps, +}) => { + return ( +
+ + {steps.map((step) => ( + + ))} + +
+ ); +}; + +export default StepIndicator; diff --git a/nkebao/src/pages/workspace/group-push/form/index.tsx b/nkebao/src/pages/workspace/group-push/form/index.tsx new file mode 100644 index 00000000..4728be4f --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/index.tsx @@ -0,0 +1,224 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "antd"; +import { createGroupPushTask } from "@/api/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"; +import ContentSelector from "./components/ContentSelector"; + +interface WechatGroup { + id: string; + name: string; + avatar: string; + serviceAccount: { + id: string; + name: string; + avatar: string; + }; +} + +interface ContentLibrary { + id: string; + name: string; + targets: Array<{ + id: string; + avatar: string; + }>; +} + +interface FormData { + name: string; + pushTimeStart: string; + pushTimeEnd: string; + dailyPushCount: number; + pushOrder: "earliest" | "latest"; + isLoopPush: boolean; + isImmediatePush: boolean; + isEnabled: boolean; + groups: WechatGroup[]; + contentLibraries: ContentLibrary[]; +} + +const steps = [ + { id: 1, title: "步骤 1", subtitle: "基础设置" }, + { id: 2, title: "步骤 2", subtitle: "选择社群" }, + { id: 3, title: "步骤 3", subtitle: "选择内容库" }, + { id: 4, title: "步骤 4", subtitle: "京东联盟" }, +]; + +const NewGroupPush: React.FC = () => { + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + name: "", + pushTimeStart: "06:00", + pushTimeEnd: "23:59", + dailyPushCount: 20, + pushOrder: "latest", + isLoopPush: false, + isImmediatePush: false, + isEnabled: false, + groups: [], + contentLibraries: [], + }); + + const handleBasicSettingsNext = (values: Partial) => { + setFormData((prev) => ({ ...prev, ...values })); + setCurrentStep(2); + }; + + const handleGroupsChange = (groups: WechatGroup[]) => { + setFormData((prev) => ({ ...prev, groups })); + }; + + const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => { + setFormData((prev) => ({ ...prev, contentLibraries })); + }; + + const handleSave = async () => { + if (!formData.name.trim()) { + window.alert("请输入任务名称"); + return; + } + if (formData.groups.length === 0) { + window.alert("请选择至少一个社群"); + return; + } + if (formData.contentLibraries.length === 0) { + window.alert("请选择至少一个内容库"); + return; + } + setLoading(true); + try { + const apiData = { + name: formData.name, + timeRange: { + start: formData.pushTimeStart, + end: formData.pushTimeEnd, + }, + maxPushPerDay: formData.dailyPushCount, + pushOrder: formData.pushOrder, + isLoopPush: formData.isLoopPush, + isImmediatePush: formData.isImmediatePush, + isEnabled: formData.isEnabled, + targetGroups: formData.groups.map((g) => g.name), + contentLibraries: formData.contentLibraries.map((c) => c.name), + pushMode: formData.isImmediatePush + ? ("immediate" as const) + : ("scheduled" as const), + messageType: "text" as const, + messageContent: "", + targetTags: [], + pushInterval: 60, + }; + const response = await createGroupPushTask(apiData); + if (response.code === 200) { + window.alert("保存成功"); + navigate("/workspace/group-push"); + } else { + window.alert("保存失败,请稍后重试"); + } + } catch (error) { + window.alert("保存失败,请稍后重试"); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + navigate("/workspace/group-push"); + }; + + return ( + + 新建社群推送任务 + + } + footer={} + > +
+ +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={() => setCurrentStep(3)} + onSave={handleSave} + onCancel={handleCancel} + loading={loading} + /> + )} + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={() => setCurrentStep(4)} + onSave={handleSave} + onCancel={handleCancel} + loading={loading} + /> + )} + {currentStep === 4 && ( +
+ 京东联盟设置(此步骤为占位,实际功能待开发) +
+ + + +
+
+ )} +
+
+
+ ); +}; + +export default NewGroupPush; diff --git a/nkebao/src/pages/workspace/group-push/new.tsx b/nkebao/src/pages/workspace/group-push/new.tsx deleted file mode 100644 index 7147f70c..00000000 --- a/nkebao/src/pages/workspace/group-push/new.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const NewGroupPush: React.FC = () => { - return ; -}; - -export default NewGroupPush; diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index b7ccc7c2..d6917e04 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -5,7 +5,8 @@ import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; import AutoGroup from "@/pages/workspace/auto-group/AutoGroup"; import AutoGroupDetail from "@/pages/workspace/auto-group/Detail"; import GroupPush from "@/pages/workspace/group-push/GroupPush"; -import NewGroupPush from "@/pages/workspace/group-push/new"; +import FormGroupPush from "@/pages/workspace/group-push/form"; +import DetailGroupPush from "@/pages/workspace/group-push/detail"; import MomentsSync from "@/pages/workspace/moments-sync/MomentsSync"; import MomentsSyncDetail from "@/pages/workspace/moments-sync/Detail"; import NewMomentsSync from "@/pages/workspace/moments-sync/new"; @@ -60,18 +61,18 @@ const workspaceRoutes = [ auth: true, }, { - path: "/workspace/group-push/new", - element: , + path: "/workspace/group-push/:id", + element: , auth: true, }, { - path: "/workspace/group-push/:id", - element: , + path: "/workspace/group-push/new", + element: , auth: true, }, { path: "/workspace/group-push/:id/edit", - element: , + element: , auth: true, }, // 朋友圈同步 From 4948f9a0979e26a9c3933b7e8fc74a9db562dd54 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 21 Jul 2025 16:56:54 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E6=B5=81=E9=87=8F=E6=B1=A0=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/TrafficController.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 Server/application/cunkebao/controller/TrafficController.php diff --git a/Server/application/cunkebao/controller/TrafficController.php b/Server/application/cunkebao/controller/TrafficController.php new file mode 100644 index 00000000..c00154de --- /dev/null +++ b/Server/application/cunkebao/controller/TrafficController.php @@ -0,0 +1,121 @@ +request->param('page',1); + $pageSize = $this->request->param('pageSize',10); + $device = $this->request->param('device',''); + $packageId = $this->request->param('packageId',''); // 流量池id + $userValue = $this->request->param('userValue',''); // 1高价值客户 2中价值客户 3低价值客户 + $addStatus = $this->request->param('addStatus',''); // 1待添加 2已添加 3添加失败 4重复用户 + $keyword = $this->request->param('keyword',''); + + $companyId = $this->request->userInfo['companyId']; + + // 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 419430449转账 436207665红包 + + $where = []; + + // 添加筛选条件 + if (!empty($device)) { + $where['d.id'] = $device; + } + + if (!empty($packageId)) { + $where['tp.id'] = $packageId; + } + + if (!empty($userValue)) { + $where['tp.userValue'] = $userValue; + } + + if (!empty($addStatus)) { + $where['tp.addStatus'] = $addStatus; + } + + + // 构建查询 - 通过traffic_pool的identifier关联wechat_account的wechatId、alias或phone + $query = Db::name('traffic_pool')->alias('tp') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'LEFT') + ->field('tp.id, tp.identifier,tp.createTime, tp.updateTime, + wa.wechatId, wa.alias, wa.phone, wa.nickname, wa.avatar') + ->where($where); + + // 关键词搜索 - 支持通过wechat_friendship的identifier关联wechat_account的wechatId、alias或phone + if (!empty($keyword)) { + $query->where(function($q) use ($keyword) { + $q->where('tp.identifier', 'like', '%' . $keyword . '%') + ->whereOr('wa.wechatId', 'like', '%' . $keyword . '%') + ->whereOr('wa.alias', 'like', '%' . $keyword . '%') + ->whereOr('wa.phone', 'like', '%' . $keyword . '%') + ->whereOr('wa.nickname', 'like', '%' . $keyword . '%'); + }); + } + + // 获取总数 + $total = $query->count(); + + + // 分页查询 + $list = $query->order('tp.createTime desc') + ->group('tp.identifier') + ->order('tp.id desc') + ->page($page, $pageSize) + ->select(); + + + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } + + + /** + * 用户旅程 + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getUserJourney() + { + $page = $this->request->param('page',1); + $pageSize = $this->request->param('pageSize',10); + $userId = $this->request->param('userId',''); + if(empty($userId)){ + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + + $query = Db::name('user_portrait') + ->field('id,type,trafficPoolId,remark,count,createTime,updateTime') + ->where(['trafficPoolId' => $userId]); + + $total = $query->count(); + + $list = $query->order('createTime desc') + ->page($page,$pageSize) + ->select(); + + + foreach ($list as $k=>$v){ + $list[$k]['createTime'] = date('Y-m-d H:i:s',$v['createTime']); + $list[$k]['updateTime'] = date('Y-m-d H:i:s',$v['updateTime']); + } + return json_encode(['code' => 200,'data'=>['list' => $list,'total'=>$total],'获取成功']); + } + + +} \ No newline at end of file From c8dda56bc908912285128d6f1c177d9a306f6b3d 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: Mon, 21 Jul 2025 16:57:02 +0800 Subject: [PATCH 07/13] =?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=B6=88=E6=81=AF=E6=8E=A8=E9=80=81=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/workspace/group-push/Edit.tsx | 250 ------------------ .../index.module.scss} | 0 .../workspace/group-push/detail/index.tsx | 2 +- .../group-push/list/index.module.scss | 107 ++++++++ .../{GroupPush.tsx => list/index.tsx} | 37 ++- nkebao/src/router/module/workspace.tsx | 2 +- 6 files changed, 125 insertions(+), 273 deletions(-) delete mode 100644 nkebao/src/pages/workspace/group-push/Edit.tsx rename nkebao/src/pages/workspace/group-push/{GroupPush.module.scss => detail/index.module.scss} (100%) create mode 100644 nkebao/src/pages/workspace/group-push/list/index.module.scss rename nkebao/src/pages/workspace/group-push/{GroupPush.tsx => list/index.tsx} (94%) diff --git a/nkebao/src/pages/workspace/group-push/Edit.tsx b/nkebao/src/pages/workspace/group-push/Edit.tsx deleted file mode 100644 index 0625950f..00000000 --- a/nkebao/src/pages/workspace/group-push/Edit.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { Button, Spin, message } from "antd"; -import { ArrowLeftOutlined } from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; -import StepIndicator from "./form/components/StepIndicator"; -import BasicSettings from "./form/components/BasicSettings"; -import GroupSelector from "./form/components/GroupSelector"; -import ContentSelector from "./form/components/ContentSelector"; -import { - getGroupPushTaskDetail, - updateGroupPushTask, - GroupPushTask, -} from "@/api/groupPush"; - -const steps = [ - { id: 1, title: "步骤 1", subtitle: "基础设置" }, - { id: 2, title: "步骤 2", subtitle: "选择社群" }, - { id: 3, title: "步骤 3", subtitle: "选择内容库" }, - { id: 4, title: "步骤 4", subtitle: "京东联盟" }, -]; - -const EditGroupPush: React.FC = () => { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const [currentStep, setCurrentStep] = useState(1); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [formData, setFormData] = useState(null); - - useEffect(() => { - if (!id) return; - setLoading(true); - getGroupPushTaskDetail(id) - .then((res) => { - const task = res.data || res; - setFormData({ - name: task.name, - pushTimeStart: task.timeRange?.start || "06:00", - pushTimeEnd: task.timeRange?.end || "23:59", - dailyPushCount: task.maxPushPerDay, - pushOrder: task.pushOrder || "latest", - isLoopPush: task.isLoopPush || false, - isImmediatePush: task.pushMode === "immediate", - isEnabled: task.isEnabled || false, - groups: (task.targetGroups || []).map( - (name: string, idx: number) => ({ - id: String(idx + 1), - name, - avatar: "", - serviceAccount: { id: "", name: "", avatar: "" }, - }) - ), - contentLibraries: (task.contentLibraries || []).map( - (name: string, idx: number) => ({ - id: String(idx + 1), - name, - targets: [], - }) - ), - }); - }) - .finally(() => setLoading(false)); - }, [id]); - - const handleBasicSettingsNext = (values: any) => { - setFormData((prev: any) => ({ ...prev, ...values })); - setCurrentStep(2); - }; - const handleGroupsChange = (groups: any[]) => { - setFormData((prev: any) => ({ ...prev, groups })); - }; - const handleLibrariesChange = (contentLibraries: any[]) => { - setFormData((prev: any) => ({ ...prev, contentLibraries })); - }; - const handleSave = async () => { - if (!formData.name.trim()) { - message.error("请输入任务名称"); - return; - } - if (!formData.groups || formData.groups.length === 0) { - message.error("请选择至少一个社群"); - return; - } - if (!formData.contentLibraries || formData.contentLibraries.length === 0) { - message.error("请选择至少一个内容库"); - return; - } - setSaving(true); - try { - const apiData = { - name: formData.name, - timeRange: { - start: formData.pushTimeStart, - end: formData.pushTimeEnd, - }, - maxPushPerDay: formData.dailyPushCount, - pushOrder: formData.pushOrder, - isLoopPush: formData.isLoopPush, - isImmediatePush: formData.isImmediatePush, - isEnabled: formData.isEnabled, - targetGroups: formData.groups.map((g: any) => g.name), - contentLibraries: formData.contentLibraries.map((c: any) => c.name), - pushMode: formData.isImmediatePush - ? ("immediate" as const) - : ("scheduled" as const), - messageType: "text" as const, - messageContent: "", - targetTags: [], - pushInterval: 60, - }; - const response = await updateGroupPushTask(id!, apiData); - if (response.code === 200) { - message.success("保存成功"); - navigate("/workspace/group-push"); - } else { - message.error("保存失败,请稍后重试"); - } - } catch (error) { - message.error("保存失败,请稍后重试"); - } finally { - setSaving(false); - } - }; - const handleCancel = () => { - navigate("/workspace/group-push"); - }; - - if (loading || !formData) { - return ( - - navigate(-1)} - style={{ marginRight: 12, cursor: "pointer" }} - /> - 编辑群发推送 - - } - footer={} - > -
- -
-
- ); - } - - return ( - - navigate(-1)} - style={{ marginRight: 12, cursor: "pointer" }} - /> - 编辑群发推送 - - } - footer={} - > -
- -
- {currentStep === 1 && ( - - )} - {currentStep === 2 && ( - setCurrentStep(1)} - onNext={() => setCurrentStep(3)} - onSave={handleSave} - onCancel={handleCancel} - loading={saving} - /> - )} - {currentStep === 3 && ( - setCurrentStep(2)} - onNext={() => setCurrentStep(4)} - onSave={handleSave} - onCancel={handleCancel} - loading={saving} - /> - )} - {currentStep === 4 && ( -
- 京东联盟设置(此步骤为占位,实际功能待开发) -
- - - -
-
- )} -
-
-
- ); -}; - -export default EditGroupPush; diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.module.scss b/nkebao/src/pages/workspace/group-push/detail/index.module.scss similarity index 100% rename from nkebao/src/pages/workspace/group-push/GroupPush.module.scss rename to nkebao/src/pages/workspace/group-push/detail/index.module.scss diff --git a/nkebao/src/pages/workspace/group-push/detail/index.tsx b/nkebao/src/pages/workspace/group-push/detail/index.tsx index be665447..5e316ff7 100644 --- a/nkebao/src/pages/workspace/group-push/detail/index.tsx +++ b/nkebao/src/pages/workspace/group-push/detail/index.tsx @@ -11,7 +11,7 @@ import { import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import { getGroupPushTaskDetail, GroupPushTask } from "@/api/groupPush"; -import styles from "./GroupPush.module.scss"; +import styles from "./index.module.scss"; const Detail: React.FC = () => { const { id } = useParams<{ id: string }>(); diff --git a/nkebao/src/pages/workspace/group-push/list/index.module.scss b/nkebao/src/pages/workspace/group-push/list/index.module.scss new file mode 100644 index 00000000..e86bd6f9 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/list/index.module.scss @@ -0,0 +1,107 @@ + + +.searchBar { + display: flex; + gap: 8px; + padding: 16px; +} + +.refresh-btn { + height: 38px; + width: 40px; + padding: 0; + border-radius: 8px; +} + +.taskList { + display: flex; + flex-direction: column; + gap: 16px; + padding: 0 16px; +} + +.emptyCard { + text-align: center; + padding: 48px 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.taskCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + padding: 20px 16px 12px 16px; +} + +.taskHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.taskTitle { + display: flex; + align-items: center; + font-size: 16px; + font-weight: 600; +} + +.taskActions { + display: flex; + align-items: center; + gap: 8px; +} + +.taskInfoGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px 16px; + font-size: 13px; + color: #666; + margin-bottom: 12px; +} + +.progressBlock { + margin-bottom: 12px; +} + +.progressLabel { + font-size: 13px; + color: #888; + margin-bottom: 4px; +} + +.taskFooter { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: #888; + border-top: 1px dashed #eee; + padding-top: 8px; + margin-top: 8px; +} + +.expandedPanel { + margin-top: 16px; + padding-top: 16px; + border-top: 1px dashed #eee; +} + +.expandedGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +@media (max-width: 600px) { + .taskCard { + padding: 12px 6px 8px 6px; + } + .expandedGrid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.tsx b/nkebao/src/pages/workspace/group-push/list/index.tsx similarity index 94% rename from nkebao/src/pages/workspace/group-push/GroupPush.tsx rename to nkebao/src/pages/workspace/group-push/list/index.tsx index 38e36029..264c1dcb 100644 --- a/nkebao/src/pages/workspace/group-push/GroupPush.tsx +++ b/nkebao/src/pages/workspace/group-push/list/index.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { NavBar } from "antd-mobile"; import { PlusOutlined, SearchOutlined, @@ -37,7 +38,7 @@ import { copyGroupPushTask, GroupPushTask, } from "@/api/groupPush"; -import styles from "./GroupPush.module.scss"; +import styles from "./index.module.scss"; const { Search } = Input; @@ -146,37 +147,31 @@ const GroupPush: React.FC = () => { return ( -
- 群消息推送 - -
- + + 群消息推送 + } footer={} >
- setSearchTerm(e.target.value)} - enterButton={} - style={{ maxWidth: 320 }} + prefix={} + allowClear + size="large" /> +
{filteredTasks.length === 0 ? ( diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index d6917e04..ff66e46a 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -4,7 +4,7 @@ import NewAutoLike from "@/pages/workspace/auto-like/NewAutoLike"; import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; import AutoGroup from "@/pages/workspace/auto-group/AutoGroup"; import AutoGroupDetail from "@/pages/workspace/auto-group/Detail"; -import GroupPush from "@/pages/workspace/group-push/GroupPush"; +import GroupPush from "@/pages/workspace/group-push/list"; import FormGroupPush from "@/pages/workspace/group-push/form"; import DetailGroupPush from "@/pages/workspace/group-push/detail"; import MomentsSync from "@/pages/workspace/moments-sync/MomentsSync"; From 340bd3e2f578e7f543eaaf02e839dea9b8face31 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: Mon, 21 Jul 2025 16:59:55 +0800 Subject: [PATCH 08/13] =?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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/workspace/auto-group/AutoGroup.tsx | 10 - .../src/pages/workspace/auto-group/Detail.tsx | 8 - .../pages/workspace/auto-group/detail/api.ts | 6 + .../auto-group/detail/index.module.scss | 3 + .../workspace/auto-group/detail/index.tsx | 7 + .../pages/workspace/auto-group/form/api.ts | 11 + .../auto-group/form/index.module.scss | 3 + .../pages/workspace/auto-group/form/index.tsx | 7 + .../pages/workspace/auto-group/list/api.ts | 8 + .../auto-group/list/index.module.scss | 3 + .../pages/workspace/auto-group/list/index.tsx | 378 ++++++++++++++++++ nkebao/src/router/module/workspace.tsx | 19 +- 12 files changed, 441 insertions(+), 22 deletions(-) delete mode 100644 nkebao/src/pages/workspace/auto-group/AutoGroup.tsx delete mode 100644 nkebao/src/pages/workspace/auto-group/Detail.tsx create mode 100644 nkebao/src/pages/workspace/auto-group/detail/api.ts create mode 100644 nkebao/src/pages/workspace/auto-group/detail/index.module.scss create mode 100644 nkebao/src/pages/workspace/auto-group/detail/index.tsx create mode 100644 nkebao/src/pages/workspace/auto-group/form/api.ts create mode 100644 nkebao/src/pages/workspace/auto-group/form/index.module.scss create mode 100644 nkebao/src/pages/workspace/auto-group/form/index.tsx create mode 100644 nkebao/src/pages/workspace/auto-group/list/api.ts create mode 100644 nkebao/src/pages/workspace/auto-group/list/index.module.scss create mode 100644 nkebao/src/pages/workspace/auto-group/list/index.tsx diff --git a/nkebao/src/pages/workspace/auto-group/AutoGroup.tsx b/nkebao/src/pages/workspace/auto-group/AutoGroup.tsx deleted file mode 100644 index 935827d6..00000000 --- a/nkebao/src/pages/workspace/auto-group/AutoGroup.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const AutoGroup: React.FC = () => { - return ( - - ); -}; - -export default AutoGroup; diff --git a/nkebao/src/pages/workspace/auto-group/Detail.tsx b/nkebao/src/pages/workspace/auto-group/Detail.tsx deleted file mode 100644 index db34f334..00000000 --- a/nkebao/src/pages/workspace/auto-group/Detail.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const AutoGroupDetail: React.FC = () => { - return ; -}; - -export default AutoGroupDetail; diff --git a/nkebao/src/pages/workspace/auto-group/detail/api.ts b/nkebao/src/pages/workspace/auto-group/detail/api.ts new file mode 100644 index 00000000..ff38e95f --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/detail/api.ts @@ -0,0 +1,6 @@ +import request from "@/api/request"; + +// 获取自动建群任务详情 +export function getAutoGroupDetail(id: string) { + return request(`/api/auto-group/detail/${id}`); +} diff --git a/nkebao/src/pages/workspace/auto-group/detail/index.module.scss b/nkebao/src/pages/workspace/auto-group/detail/index.module.scss new file mode 100644 index 00000000..93c42e85 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/detail/index.module.scss @@ -0,0 +1,3 @@ +.autoGroupDetail { + // 这里写详情页样式 +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-group/detail/index.tsx b/nkebao/src/pages/workspace/auto-group/detail/index.tsx new file mode 100644 index 00000000..00f574dd --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/detail/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const AutoGroupDetail: React.FC = () => { + return
自动建群详情页
; +}; + +export default AutoGroupDetail; diff --git a/nkebao/src/pages/workspace/auto-group/form/api.ts b/nkebao/src/pages/workspace/auto-group/form/api.ts new file mode 100644 index 00000000..4ab01f32 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/form/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 新建自动建群任务 +export function createAutoGroup(data: any) { + return request("/api/auto-group/create", data, "POST"); +} + +// 编辑自动建群任务 +export function updateAutoGroup(id: string, data: any) { + return request(`/api/auto-group/update/${id}`, data, "POST"); +} diff --git a/nkebao/src/pages/workspace/auto-group/form/index.module.scss b/nkebao/src/pages/workspace/auto-group/form/index.module.scss new file mode 100644 index 00000000..19f9a662 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/form/index.module.scss @@ -0,0 +1,3 @@ +.autoGroupForm { + // 这里写新建/编辑页样式 +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-group/form/index.tsx b/nkebao/src/pages/workspace/auto-group/form/index.tsx new file mode 100644 index 00000000..3a8d8bd1 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/form/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const AutoGroupForm: React.FC = () => { + return
自动建群新建/编辑页
; +}; + +export default AutoGroupForm; diff --git a/nkebao/src/pages/workspace/auto-group/list/api.ts b/nkebao/src/pages/workspace/auto-group/list/api.ts new file mode 100644 index 00000000..073a6242 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/list/api.ts @@ -0,0 +1,8 @@ +import request from "@/api/request"; + +// 获取自动建群任务列表 +export function getAutoGroupList(params?: any) { + return request("/api/auto-group/list", params, "GET"); +} + +// 其他相关API可按需添加 diff --git a/nkebao/src/pages/workspace/auto-group/list/index.module.scss b/nkebao/src/pages/workspace/auto-group/list/index.module.scss new file mode 100644 index 00000000..02104154 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/list/index.module.scss @@ -0,0 +1,3 @@ +.autoGroupList { + // 这里写列表页样式 +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-group/list/index.tsx b/nkebao/src/pages/workspace/auto-group/list/index.tsx new file mode 100644 index 00000000..ec7b02eb --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/list/index.tsx @@ -0,0 +1,378 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Button, + Card, + Input, + Switch, + ProgressBar, + Popover, + Toast, +} from "antd-mobile"; +import { + MoreOutline, + AddCircleOutline, + SearchOutline, + FilterOutline, + UserAddOutline, + ClockCircleOutline, + TeamOutline, + CalendarOutline, +} from "antd-mobile-icons"; + +import { ReloadOutlined, SettingOutlined } from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import style from "./index.module.scss"; + +interface GroupTask { + id: string; + name: string; + status: "running" | "paused" | "completed"; + deviceCount: number; + targetFriends: number; + createdGroups: number; + lastCreateTime: string; + createTime: string; + creator: string; + createInterval: number; + maxGroupsPerDay: number; + timeRange: { start: string; end: string }; + groupSize: { min: number; max: number }; + targetTags: string[]; + groupNameTemplate: string; + groupDescription: string; +} + +const mockTasks: GroupTask[] = [ + { + id: "1", + name: "VIP客户建群", + deviceCount: 2, + targetFriends: 156, + createdGroups: 12, + lastCreateTime: "2025-02-06 13:12:35", + createTime: "2024-11-20 19:04:14", + creator: "admin", + status: "running", + createInterval: 300, + maxGroupsPerDay: 20, + timeRange: { start: "09:00", end: "21:00" }, + groupSize: { min: 20, max: 50 }, + targetTags: ["VIP客户", "高价值"], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "VIP客户专属交流群,提供优质服务", + }, + { + id: "2", + name: "产品推广建群", + deviceCount: 1, + targetFriends: 89, + createdGroups: 8, + lastCreateTime: "2024-03-04 14:09:35", + createTime: "2024-03-04 14:29:04", + creator: "manager", + status: "paused", + createInterval: 600, + maxGroupsPerDay: 10, + timeRange: { start: "10:00", end: "20:00" }, + groupSize: { min: 15, max: 30 }, + targetTags: ["潜在客户", "中意向"], + groupNameTemplate: "产品推广群{序号}", + groupDescription: "产品推广交流群,了解最新产品信息", + }, +]; + +const getStatusColor = (status: string) => { + switch (status) { + case "running": + return style.statusRunning; + case "paused": + return style.statusPaused; + case "completed": + return style.statusCompleted; + default: + return style.statusPaused; + } +}; + +const getStatusText = (status: string) => { + switch (status) { + case "running": + return "进行中"; + case "paused": + return "已暂停"; + case "completed": + return "已完成"; + default: + return "未知"; + } +}; + +const AutoGroupList: React.FC = () => { + const navigate = useNavigate(); + const [expandedTaskId, setExpandedTaskId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [tasks, setTasks] = useState(mockTasks); + + const toggleExpand = (taskId: string) => { + setExpandedTaskId(expandedTaskId === taskId ? null : taskId); + }; + + const handleDelete = (taskId: string) => { + const taskToDelete = tasks.find((task) => task.id === taskId); + if (!taskToDelete) return; + window.confirm(`确定要删除"${taskToDelete.name}"吗?`) && + setTasks(tasks.filter((task) => task.id !== taskId)); + Toast.show({ content: "删除成功" }); + }; + + const handleEdit = (taskId: string) => { + navigate(`/workspace/auto-group/${taskId}/edit`); + }; + + const handleView = (taskId: string) => { + navigate(`/workspace/auto-group/${taskId}`); + }; + + const handleCopy = (taskId: string) => { + const taskToCopy = tasks.find((task) => task.id === taskId); + if (taskToCopy) { + const newTask = { + ...taskToCopy, + id: `${Date.now()}`, + name: `${taskToCopy.name} (复制)`, + createTime: new Date().toISOString().replace("T", " ").substring(0, 19), + }; + setTasks([...tasks, newTask]); + Toast.show({ content: "复制成功" }); + } + }; + + const toggleTaskStatus = (taskId: string) => { + setTasks((prev) => + prev.map((task) => + task.id === taskId + ? { + ...task, + status: task.status === "running" ? "paused" : "running", + } + : task + ) + ); + Toast.show({ content: "状态已切换" }); + }; + + const handleCreateNew = () => { + navigate("/workspace/auto-group/new"); + }; + + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + +
自动建群
+ +
+ } + footer={} + > +
+
+ setSearchTerm(val)} + prefix={} + clearable + /> + + +
+
+ {filteredTasks.length === 0 ? ( + + +
暂无建群任务
+
创建您的第一个自动建群任务
+ +
+ ) : ( + filteredTasks.map((task) => ( + +
+
{task.name}
+ + {getStatusText(task.status)} + + toggleTaskStatus(task.id)} + disabled={task.status === "completed"} + style={{ marginLeft: 8 }} + /> + +
handleView(task.id)} + > + 查看 +
+
handleEdit(task.id)} + > + 编辑 +
+
handleCopy(task.id)} + > + 复制 +
+
handleDelete(task.id)} + > + 删除 +
+
+ } + trigger="click" + > + + +
+
+
+
执行设备
+
{task.deviceCount} 个
+
+
+
目标好友
+
+ {task.targetFriends} 个 +
+
+
+
已建群
+
+ {task.createdGroups} 个 +
+
+
+
创建人
+
{task.creator}
+
+
+
+
+ + 上次建群:{task.lastCreateTime} +
+
+ 创建时间:{task.createTime} + +
+
+ {expandedTaskId === task.id && ( +
+
+
+
+ {" "} + 基本设置 +
+
+ 建群间隔:{task.createInterval} 秒 +
+
+ 每日最大建群数:{task.maxGroupsPerDay} 个 +
+
+ 执行时间段:{task.timeRange.start} -{" "} + {task.timeRange.end} +
+
+ 群组规模:{task.groupSize.min}-{task.groupSize.max} 人 +
+
+
+
+ 目标人群 +
+
+ {task.targetTags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ 群组设置 +
+
+ 群名称模板:{task.groupNameTemplate} +
+
+ 群描述:{task.groupDescription} +
+
+
+
+ {" "} + 执行进度 +
+
+ 今日已建群:{task.createdGroups} /{" "} + {task.maxGroupsPerDay} +
+ +
+
+
+ )} + + )) + )} +
+
+
+ ); +}; + +export default AutoGroupList; diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index b7ccc7c2..f1230d2e 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -2,8 +2,9 @@ import Workspace from "@/pages/workspace/main"; import AutoLike from "@/pages/workspace/auto-like/AutoLike"; import NewAutoLike from "@/pages/workspace/auto-like/NewAutoLike"; import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; -import AutoGroup from "@/pages/workspace/auto-group/AutoGroup"; -import AutoGroupDetail from "@/pages/workspace/auto-group/Detail"; +import AutoGroupList from "@/pages/workspace/auto-group/list"; +import AutoGroupDetail from "@/pages/workspace/auto-group/detail"; +import AutoGroupForm from "@/pages/workspace/auto-group/form"; import GroupPush from "@/pages/workspace/group-push/GroupPush"; import NewGroupPush from "@/pages/workspace/group-push/new"; import MomentsSync from "@/pages/workspace/moments-sync/MomentsSync"; @@ -42,10 +43,15 @@ const workspaceRoutes = [ element: , auth: true, }, - // 自动分组 + // 自动建群 { path: "/workspace/auto-group", - element: , + element: , + auth: true, + }, + { + path: "/workspace/auto-group/new", + element: , auth: true, }, { @@ -53,6 +59,11 @@ const workspaceRoutes = [ element: , auth: true, }, + { + path: "/workspace/auto-group/:id/edit", + element: , + auth: true, + }, // 群发推送 { path: "/workspace/group-push", From 5f9ffadc372e00c8c0548c1df8f55a78270dbf25 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: Mon, 21 Jul 2025 17:47:22 +0800 Subject: [PATCH 09/13] =?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=87=AA=E5=8A=A8=E7=82=B9=E8=B5=9E=E5=88=97=E8=A1=A8?= =?UTF-8?q?=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/src/api/autoLike.ts | 173 +++++++ .../pages/workspace/auto-like/AutoLike.tsx | 38 -- .../workspace/auto-like/AutoLikeDetail.tsx | 8 - .../workspace/auto-like/AutoLikeListPC.scss | 1 + .../workspace/auto-like/AutoLikeListPC.tsx | 1 + .../pages/workspace/auto-like/NewAutoLike.tsx | 30 -- .../src/pages/workspace/auto-like/list/api.ts | 63 +++ .../auto-like/list/index.module.scss | 342 ++++++++++++++ .../pages/workspace/auto-like/list/index.tsx | 410 ++++++++++++++++ .../pages/workspace/auto-like/new/index.tsx | 437 ++++++++++++++++++ .../workspace/auto-like/new/new.module.scss | 301 ++++++++++++ .../workspace/auto-like/record/index.tsx | 297 ++++++++++++ .../auto-like/record/record.module.scss | 351 ++++++++++++++ nkebao/src/router/module/workspace.tsx | 10 +- nkebao/src/types/auto-like.ts | 119 +++++ 15 files changed, 2500 insertions(+), 81 deletions(-) create mode 100644 nkebao/src/api/autoLike.ts delete mode 100644 nkebao/src/pages/workspace/auto-like/AutoLike.tsx delete mode 100644 nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss create mode 100644 nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx delete mode 100644 nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/list/api.ts create mode 100644 nkebao/src/pages/workspace/auto-like/list/index.module.scss create mode 100644 nkebao/src/pages/workspace/auto-like/list/index.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/new/index.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/new/new.module.scss create mode 100644 nkebao/src/pages/workspace/auto-like/record/index.tsx create mode 100644 nkebao/src/pages/workspace/auto-like/record/record.module.scss create mode 100644 nkebao/src/types/auto-like.ts diff --git a/nkebao/src/api/autoLike.ts b/nkebao/src/api/autoLike.ts new file mode 100644 index 00000000..8ee3f4ba --- /dev/null +++ b/nkebao/src/api/autoLike.ts @@ -0,0 +1,173 @@ +import { request } from "./request"; +import { + LikeTask, + CreateLikeTaskData, + UpdateLikeTaskData, + LikeRecord, + ApiResponse, + PaginatedResponse, +} from "@/types/auto-like"; + +// 获取自动点赞任务列表 +export async function fetchAutoLikeTasks(): Promise { + try { + const res = await request>>({ + url: "/v1/workbench/list", + method: "GET", + params: { + type: 1, + page: 1, + limit: 100, + }, + }); + + if (res.code === 200 && res.data) { + return res.data.list || []; + } + return []; + } catch (error) { + console.error("获取自动点赞任务失败:", error); + return []; + } +} + +// 获取单个任务详情 +export async function fetchAutoLikeTaskDetail( + id: string +): Promise { + try { + console.log(`Fetching task detail for id: ${id}`); + const res = await request({ + url: "/v1/workbench/detail", + method: "GET", + params: { id }, + }); + console.log("Task detail API response:", res); + + if (res.code === 200) { + if (res.data) { + if (typeof res.data === "object") { + return res.data; + } else { + console.error( + "Task detail API response data is not an object:", + res.data + ); + return null; + } + } else { + console.error("Task detail API response missing data field:", res); + return null; + } + } + + console.error("Task detail API error:", res.msg || "Unknown error"); + return null; + } catch (error) { + console.error("获取任务详情失败:", error); + return null; + } +} + +// 创建自动点赞任务 +export async function createAutoLikeTask( + data: CreateLikeTaskData +): Promise { + return request({ + url: "/v1/workbench/create", + method: "POST", + data: { + ...data, + type: 1, // 自动点赞类型 + }, + }); +} + +// 更新自动点赞任务 +export async function updateAutoLikeTask( + data: UpdateLikeTaskData +): Promise { + return request({ + url: "/v1/workbench/update", + method: "POST", + data: { + ...data, + type: 1, // 自动点赞类型 + }, + }); +} + +// 删除自动点赞任务 +export async function deleteAutoLikeTask(id: string): Promise { + return request({ + url: "/v1/workbench/delete", + method: "DELETE", + params: { id }, + }); +} + +// 切换任务状态 +export async function toggleAutoLikeTask( + id: string, + status: string +): Promise { + return request({ + url: "/v1/workbench/update-status", + method: "POST", + data: { id, status }, + }); +} + +// 复制自动点赞任务 +export async function copyAutoLikeTask(id: string): Promise { + return request({ + url: "/v1/workbench/copy", + method: "POST", + data: { id }, + }); +} + +// 获取点赞记录 +export async function fetchLikeRecords( + workbenchId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise> { + try { + const params: any = { + workbenchId, + page: page.toString(), + limit: limit.toString(), + }; + + if (keyword) { + params.keyword = keyword; + } + + const res = await request>>({ + url: "/v1/workbench/records", + method: "GET", + params, + }); + + if (res.code === 200 && res.data) { + return res.data; + } + + return { + list: [], + total: 0, + page: 1, + limit: 20, + }; + } catch (error) { + console.error("获取点赞记录失败:", error); + return { + list: [], + total: 0, + page: 1, + limit: 20, + }; + } +} diff --git a/nkebao/src/pages/workspace/auto-like/AutoLike.tsx b/nkebao/src/pages/workspace/auto-like/AutoLike.tsx deleted file mode 100644 index cc452a1c..00000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLike.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { NavBar, Button } from "antd-mobile"; -import { PlusOutlined } from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; - -const AutoLike: React.FC = () => { - return ( - window.history.back()} - left={ -
- 自动点赞 -
- } - right={ - - } - /> - } - footer={} - > -
-

自动点赞页面

-

此页面正在开发中...

-
-
- ); -}; - -export default AutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx b/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx deleted file mode 100644 index 08811261..00000000 --- a/nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const AutoLikeDetail: React.FC = () => { - return ; -}; - -export default AutoLikeDetail; diff --git a/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.scss @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/AutoLikeListPC.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx b/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx deleted file mode 100644 index 12bf9b7e..00000000 --- a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { NavBar } from "antd-mobile"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; - -const NewAutoLike: React.FC = () => { - return ( - window.history.back()} - > -
- 新建自动点赞 -
- - } - footer={} - > -
-

新建自动点赞页面

-

此页面正在开发中...

-
-
- ); -}; - -export default NewAutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/list/api.ts b/nkebao/src/pages/workspace/auto-like/list/api.ts new file mode 100644 index 00000000..930902b4 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/api.ts @@ -0,0 +1,63 @@ +import request from "@/api/request"; +import { + LikeTask, + CreateLikeTaskData, + UpdateLikeTaskData, + LikeRecord, + PaginatedResponse, +} from "@/types/auto-like"; + +// 获取自动点赞任务列表 +export function fetchAutoLikeTasks( + params = { type: 1, page: 1, limit: 100 } +): Promise { + return request("/v1/workbench/list", params, "GET"); +} + +// 获取单个任务详情 +export function fetchAutoLikeTaskDetail(id: string): Promise { + return request("/v1/workbench/detail", { id }, "GET"); +} + +// 创建自动点赞任务 +export function createAutoLikeTask(data: CreateLikeTaskData): Promise { + return request("/v1/workbench/create", { ...data, type: 1 }, "POST"); +} + +// 更新自动点赞任务 +export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise { + return request("/v1/workbench/update", { ...data, type: 1 }, "POST"); +} + +// 删除自动点赞任务 +export function deleteAutoLikeTask(id: string): Promise { + return request("/v1/workbench/delete", { id }, "DELETE"); +} + +// 切换任务状态 +export function toggleAutoLikeTask(id: string, status: string): Promise { + return request("/v1/workbench/update-status", { id, status }, "POST"); +} + +// 复制自动点赞任务 +export function copyAutoLikeTask(id: string): Promise { + return request("/v1/workbench/copy", { id }, "POST"); +} + +// 获取点赞记录 +export function fetchLikeRecords( + workbenchId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise> { + const params: any = { + workbenchId, + page: page.toString(), + limit: limit.toString(), + }; + if (keyword) { + params.keyword = keyword; + } + return request("/v1/workbench/records", params, "GET"); +} diff --git a/nkebao/src/pages/workspace/auto-like/list/index.module.scss b/nkebao/src/pages/workspace/auto-like/list/index.module.scss new file mode 100644 index 00000000..d70e7a45 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/index.module.scss @@ -0,0 +1,342 @@ +.header { + background: white; + border-bottom: 1px solid #e5e5e5; + position: sticky; + top: 0; + z-index: 10; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.header-title { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.search-bar { + display: flex; + gap: 12px; + align-items: center; + padding: 16px; +} + +.search-input-wrapper { + position: relative; + flex: 1; + + .ant-input { + border-radius: 8px; + height: 40px; + } +} +.refresh-btn { + height: 40px; + width: 40px; + padding: 0; + border-radius: 8px; +} + +.new-task-btn { + height: 32px; + padding: 0 12px; + border-radius: 6px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; +} + + +.task-list { + padding: 0 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.task-card { + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s; + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } +} + +.task-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.task-title-section { + display: flex; + align-items: center; + gap: 8px; +} + +.task-name { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.task-status { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + + &.active { + background: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; + } + + &.inactive { + background: #f5f5f5; + color: #666; + border: 1px solid #d9d9d9; + } +} + +.task-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; + + &:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + } + } + + input:checked + .slider { + background-color: #1890ff; + } + + input:checked + .slider:before { + transform: translateX(20px); + } +} + +.menu-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + border-radius: 4px; + color: #666; + + &:hover { + background: #f5f5f5; + } +} + +.menu-dropdown { + position: absolute; + right: 0; + top: 28px; + background: white; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 100; + min-width: 120px; + padding: 4px; + border: 1px solid #e5e5e5; +} + +.menu-item { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + border-radius: 4px; + font-size: 14px; + gap: 8px; + transition: background 0.2s; + + &:hover { + background: #f5f5f5; + } + + &.danger { + color: #ff4d4f; + + &:hover { + background: #fff2f0; + } + } +} + +.task-info { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.info-section { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.info-label { + font-size: 16px; + color: #666; +} + +.info-value { + font-size: 16px; + color: #333; + font-weight: 600; +} + +.task-stats { + display: flex; + justify-content: space-between; + font-size: 14px; + color: #666; + border-top: 1px solid #f0f0f0; + padding-top: 10px; +} + +.stats-item { + display: flex; + align-items: center; + gap: 8px; +} + +.stats-icon { + font-size: 16px; + + &.blue { + color: #1890ff; + } + + &.green { + color: #52c41a; + } +} + +.stats-label { + font-weight: 500; +} + +.stats-value { + color: #333; + font-weight: 600; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.empty-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.empty-text { + font-size: 16px; + margin-bottom: 8px; +} + +.empty-subtext { + font-size: 14px; + color: #999; +} + +// 移动端适配 +@media (max-width: 768px) { + .task-info { + grid-template-columns: 1fr; + gap: 16px; + } + + .task-stats { + gap: 12px; + align-items: flex-start; + } + + .header-content { + padding: 12px 16px; + } + + .search-section { + padding: 12px 16px; + } + + .task-list { + padding: 0 12px; + } +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/list/index.tsx b/nkebao/src/pages/workspace/auto-like/list/index.tsx new file mode 100644 index 00000000..7e5f78e9 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/list/index.tsx @@ -0,0 +1,410 @@ +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { + NavBar, + Button, + Toast, + SpinLoading, + Dialog, + Popup, + Card, + Tag, +} from "antd-mobile"; +import { Input } from "antd"; +import { + PlusOutlined, + CopyOutlined, + DeleteOutlined, + SettingOutlined, + SearchOutlined, + ReloadOutlined, + EyeOutlined, + EditOutlined, + MoreOutlined, + LikeOutlined, + ClockCircleOutlined, +} from "@ant-design/icons"; +import { LeftOutline } from "antd-mobile-icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchAutoLikeTasks, + deleteAutoLikeTask, + toggleAutoLikeTask, + copyAutoLikeTask, +} from "./api"; +import { LikeTask } from "@/types/auto-like"; +import style from "./index.module.scss"; + +// 卡片菜单组件 +interface CardMenuProps { + onView: () => void; + onEdit: () => void; + onCopy: () => void; + onDelete: () => void; +} + +const CardMenu: React.FC = ({ + onView, + onEdit, + onCopy, + onDelete, +}) => { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + if (open) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
{ + onView(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 查看 +
+
{ + onEdit(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 编辑 +
+
{ + onCopy(); + setOpen(false); + }} + className={style["menu-item"]} + > + + 复制 +
+
{ + onDelete(); + setOpen(false); + }} + className={`${style["menu-item"]} ${style["danger"]}`} + > + + 删除 +
+
+ )} +
+ ); +}; + +const AutoLike: React.FC = () => { + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(false); + + // 获取任务列表 + const fetchTasks = async () => { + setLoading(true); + try { + const Res: any = await fetchAutoLikeTasks(); + // 直接就是任务数组,无需再解包 + const mappedTasks = Res?.list?.map((task: any) => ({ + ...task, + status: task.status || 2, // 默认为关闭状态 + deviceCount: task.deviceCount || 0, + targetGroup: task.targetGroup || "全部好友", + likeInterval: task.likeInterval || 60, + maxLikesPerDay: task.maxLikesPerDay || 100, + lastLikeTime: task.lastLikeTime || "暂无", + createTime: task.createTime || "", + updateTime: task.updateTime || "", + todayLikeCount: task.todayLikeCount || 0, + totalLikeCount: task.totalLikeCount || 0, + })); + setTasks(mappedTasks); + } catch (error) { + console.error("获取自动点赞任务失败:", error); + Toast.show({ + content: "获取任务列表失败", + position: "top", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTasks(); + }, []); + + // 删除任务 + const handleDelete = async (id: string) => { + const result = await Dialog.confirm({ + content: "确定要删除这个任务吗?", + confirmText: "删除", + cancelText: "取消", + }); + + if (result) { + try { + await deleteAutoLikeTask(id); + Toast.show({ + content: "删除成功", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "删除失败", + position: "top", + }); + } + } + }; + + // 编辑任务 + const handleEdit = (taskId: string) => { + navigate(`/workspace/auto-like/edit/${taskId}`); + }; + + // 查看任务 + const handleView = (taskId: string) => { + navigate(`/workspace/auto-like/detail/${taskId}`); + }; + + // 复制任务 + const handleCopy = async (id: string) => { + try { + await copyAutoLikeTask(id); + Toast.show({ + content: "复制成功", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "复制失败", + position: "top", + }); + } + }; + + // 切换任务状态 + const toggleTaskStatus = async (id: string, status: number) => { + try { + const newStatus = status === 1 ? "2" : "1"; + await toggleAutoLikeTask(id, newStatus); + Toast.show({ + content: status === 1 ? "已暂停" : "已启动", + position: "top", + }); + fetchTasks(); // 重新获取列表 + } catch (error) { + Toast.show({ + content: "操作失败", + position: "top", + }); + } + }; + + // 创建新任务 + const handleCreateNew = () => { + navigate("/workspace/auto-like/new"); + }; + + // 过滤任务 + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + + window.history.back()} + left={ +
+ 自动点赞 +
+ } + right={ + + } + /> + {/* 搜索栏 */} +
+
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ + } + > +
+ {/* 任务列表 */} +
+ {loading ? ( +
+ +
加载中...
+
+ ) : filteredTasks.length === 0 ? ( +
+
+ +
+
暂无自动点赞任务
+
+ 点击右上角按钮创建新任务 +
+
+ ) : ( + filteredTasks.map((task) => ( + +
+
+

{task.name}

+ + {Number(task.status) === 1 ? "进行中" : "已暂停"} + +
+
+ + handleView(task.id)} + onEdit={() => handleEdit(task.id)} + onCopy={() => handleCopy(task.id)} + onDelete={() => handleDelete(task.id)} + /> +
+
+ +
+
+
+ 执行设备 + + {task.deviceCount} 个 + +
+
+ 目标人群 + + {task.targetGroup} + +
+
+ 更新时间 + + {task.updateTime} + +
+
+
+
+ 点赞间隔 + + {task.likeInterval} 秒 + +
+
+ 每日上限 + + {task.maxLikesPerDay} 次 + +
+
+ 创建时间 + + {task.createTime} + +
+
+
+ +
+
+ + 今日点赞: + + {task.lastLikeTime} + +
+
+ + 总点赞数: + + {task.totalLikeCount || 0} + +
+
+
+ )) + )} +
+
+
+ ); +}; + +export default AutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/new/index.tsx b/nkebao/src/pages/workspace/auto-like/new/index.tsx new file mode 100644 index 00000000..bd6c9d5e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/new/index.tsx @@ -0,0 +1,437 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + NavBar, + Button, + Toast, + SpinLoading, + Form, + Input, + Switch, + Stepper, + Card, + Tag, +} from "antd-mobile"; +import { Input as AntInput, TimePicker, Select } from "antd"; +import { + PlusOutlined, + MinusOutlined, + CheckOutlined, + TagOutlined, + ClockCircleOutlined, + LikeOutlined, + UserOutlined, + SettingOutlined, +} from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + createAutoLikeTask, + updateAutoLikeTask, + fetchAutoLikeTaskDetail, +} from "@/api/autoLike"; +import { + CreateLikeTaskData, + UpdateLikeTaskData, + ContentType, +} from "@/types/auto-like"; +import style from "./new.module.scss"; + +const { Option } = Select; + +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: "", + }); + + // 如果是编辑模式,获取任务详情 + useEffect(() => { + if (isEditMode && id) { + fetchTaskDetail(); + } + }, [id, isEditMode]); + + // 获取任务详情 + const fetchTaskDetail = async () => { + try { + const taskDetail = await fetchAutoLikeTaskDetail(id!); + 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"); + } + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + position: "top", + }); + } finally { + setIsLoading(false); + } + }; + + const handleUpdateFormData = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const handleNext = () => { + if (currentStep < 3) { + setCurrentStep(currentStep + 1); + } + }; + + const handlePrev = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + + const handleComplete = async () => { + if (!formData.name.trim()) { + Toast.show({ + content: "请输入任务名称", + position: "top", + }); + return; + } + + if (formData.devices.length === 0) { + Toast.show({ + content: "请选择执行设备", + position: "top", + }); + return; + } + + setIsSubmitting(true); + try { + if (isEditMode && id) { + const updateData: UpdateLikeTaskData = { + ...formData, + id, + }; + await updateAutoLikeTask(updateData); + Toast.show({ + content: "更新成功", + position: "top", + }); + } else { + await createAutoLikeTask(formData); + Toast.show({ + content: "创建成功", + position: "top", + }); + } + navigate("/workspace/auto-like"); + } catch (error) { + Toast.show({ + content: isEditMode ? "更新失败" : "创建失败", + position: "top", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleContentTypeChange = (type: ContentType) => { + const newTypes = formData.contentTypes.includes(type) + ? formData.contentTypes.filter((t) => t !== type) + : [...formData.contentTypes, type]; + handleUpdateFormData({ contentTypes: newTypes }); + }; + + const renderStepIndicator = () => ( +
+
+
= 1 ? style["active"] : ""}`} + > +
1
+
基础设置
+
+
= 2 ? style["active"] : ""}`} + > +
2
+
设备选择
+
+
= 3 ? style["active"] : ""}`} + > +
3
+
好友设置
+
+
+
+ ); + + const renderBasicSettings = () => ( +
+ +
+ + handleUpdateFormData({ name: value })} + className={style["form-input"]} + /> +
+ +
+ +
+ + + {formData.interval} 秒 + + +
+
+ +
+ +
+ + + {formData.maxLikes} 次 + + +
+
+ +
+ +
+ handleUpdateFormData({ startTime: value })} + className={style["time-input"]} + /> + + handleUpdateFormData({ endTime: value })} + className={style["time-input"]} + /> +
+
+ +
+ +
+ {(["text", "image", "video", "link"] as ContentType[]).map( + (type) => ( + handleContentTypeChange(type)} + className={style["content-type-tag"]} + > + {type === "text" && "文字"} + {type === "image" && "图片"} + {type === "video" && "视频"} + {type === "link" && "链接"} + + ) + )} +
+
+ +
+ + +
+
+
+ ); + + const renderDeviceSelection = () => ( +
+ +
+ +
设备选择功能开发中...
+
+ 当前已选择 {formData.devices.length} 个设备 +
+
+
+
+ ); + + const renderFriendSettings = () => ( +
+ +
+ +
好友设置功能开发中...
+
+ 当前已选择 {formData.friends.length} 个好友 +
+
+
+
+ ); + + if (isLoading) { + return ( + window.history.back()} + left={ +
+ {isEditMode ? "编辑任务" : "新建任务"} +
+ } + /> + } + footer={} + > +
+ +
加载中...
+
+
+ ); + } + + return ( + window.history.back()} + left={ +
+ {isEditMode ? "编辑任务" : "新建任务"} +
+ } + /> + } + footer={} + > +
+ {renderStepIndicator()} + + {currentStep === 1 && renderBasicSettings()} + {currentStep === 2 && renderDeviceSelection()} + {currentStep === 3 && renderFriendSettings()} + +
+ {currentStep > 1 && ( + + )} + + {currentStep < 3 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default NewAutoLike; diff --git a/nkebao/src/pages/workspace/auto-like/new/new.module.scss b/nkebao/src/pages/workspace/auto-like/new/new.module.scss new file mode 100644 index 00000000..5b25188e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/new/new.module.scss @@ -0,0 +1,301 @@ +.new-page { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 80px; +} + +.step-indicator { + background: white; + padding: 20px 16px; + border-bottom: 1px solid #f0f0f0; +} + +.step-list { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 20px; + left: 20px; + right: 20px; + height: 2px; + background: #e5e5e5; + z-index: 1; + } +} + +.step-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: relative; + z-index: 2; + + &.active { + .step-number { + background: #1890ff; + color: white; + border-color: #1890ff; + } + + .step-label { + color: #1890ff; + font-weight: 600; + } + } +} + +.step-number { + width: 40px; + height: 40px; + border-radius: 50%; + background: white; + border: 2px solid #e5e5e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + color: #999; + transition: all 0.3s; +} + +.step-label { + font-size: 12px; + color: #666; + text-align: center; + transition: all 0.3s; +} + +.step-content { + padding: 16px; +} + +.form-card { + background: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.form-item { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } +} + +.form-label { + display: block; + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.form-input { + width: 100%; + height: 40px; + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.stepper-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.stepper-btn { + width: 32px; + height: 32px; + padding: 0; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #d9d9d9; + background: white; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } +} + +.stepper-value { + font-size: 14px; + font-weight: 600; + color: #333; + min-width: 60px; + text-align: center; +} + +.time-range { + display: flex; + align-items: center; + gap: 12px; +} + +.time-input { + flex: 1; + height: 40px; + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.time-separator { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.content-types { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.content-type-tag { + cursor: pointer; + transition: all 0.2s; + + &:hover { + transform: translateY(-1px); + } +} + +.form-switch { + margin-left: auto; +} + +.placeholder-content { + text-align: center; + padding: 40px 20px; + color: #666; +} + +.placeholder-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.placeholder-text { + font-size: 16px; + margin-bottom: 8px; + color: #333; +} + +.placeholder-subtext { + font-size: 14px; + color: #999; +} + +.step-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 16px; + border-top: 1px solid #f0f0f0; + display: flex; + gap: 12px; + z-index: 10; +} + +.prev-btn { + flex: 1; + height: 44px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} + +.next-btn, +.complete-btn { + flex: 1; + height: 44px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +// 移动端适配 +@media (max-width: 768px) { + .step-list { + &::before { + left: 15px; + right: 15px; + } + } + + .step-number { + width: 36px; + height: 36px; + font-size: 14px; + } + + .step-label { + font-size: 11px; + } + + .form-card { + padding: 16px; + } + + .stepper-wrapper { + gap: 8px; + } + + .stepper-btn { + width: 28px; + height: 28px; + } + + .time-range { + flex-direction: column; + gap: 8px; + } + + .content-types { + gap: 6px; + } +} \ 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 new file mode 100644 index 00000000..4d52913a --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/record/index.tsx @@ -0,0 +1,297 @@ +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import { NavBar, Button, Toast, SpinLoading, Card, Avatar } from "antd-mobile"; +import { Input } from "antd"; +import InfiniteList from "@/components/InfiniteList/InfiniteList"; +import { + SearchOutlined, + ReloadOutlined, + LikeOutlined, + UserOutlined, +} from "@ant-design/icons"; + +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { fetchLikeRecords, fetchAutoLikeTaskDetail } from "@/api/autoLike"; +import { LikeRecord, LikeTask } from "@/types/auto-like"; +import style from "./record.module.scss"; + +// 格式化日期 +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; + } +}; + +const AutoLikeDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [records, setRecords] = useState([]); + const [taskDetail, setTaskDetail] = useState(null); + const [recordsLoading, setRecordsLoading] = useState(false); + const [taskLoading, setTaskLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + const [hasMore, setHasMore] = useState(true); + const pageSize = 10; + + // 获取任务详情 + const fetchTaskDetail = async () => { + if (!id) return; + setTaskLoading(true); + try { + const detail = await fetchAutoLikeTaskDetail(id); + setTaskDetail(detail); + } catch (error) { + Toast.show({ + content: "获取任务详情失败", + position: "top", + }); + } finally { + setTaskLoading(false); + } + }; + + // 获取点赞记录 + const fetchRecords = async ( + page: number = 1, + isLoadMore: boolean = false + ) => { + if (!id) return; + + if (!isLoadMore) { + setRecordsLoading(true); + } + + try { + const response = await fetchLikeRecords(id, page, pageSize, searchTerm); + const newRecords = response.list || []; + + if (isLoadMore) { + setRecords((prev) => [...prev, ...newRecords]); + } else { + setRecords(newRecords); + } + + setTotal(response.total || 0); + setCurrentPage(page); + setHasMore(newRecords.length === pageSize); + } catch (error) { + Toast.show({ + content: "获取点赞记录失败", + position: "top", + }); + } finally { + setRecordsLoading(false); + } + }; + + useEffect(() => { + fetchTaskDetail(); + fetchRecords(1, false); + }, [id]); + + const handleSearch = () => { + setCurrentPage(1); + fetchRecords(1, false); + }; + + const handleRefresh = () => { + fetchRecords(currentPage, false); + }; + + const handleLoadMore = async () => { + if (hasMore && !recordsLoading) { + await fetchRecords(currentPage + 1, true); + } + }; + + const renderRecordItem = (record: LikeRecord) => ( + +
+
+ + + +
+
+ {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} + + 点赞了这条内容 +
+
+
+ ); + + return ( + window.history.back()} + left={ +
+ 点赞记录 +
+ } + /> + } + footer={} + > +
+ {/* 任务信息卡片 */} + {taskDetail && ( +
+
+

{taskDetail.name}

+ + {Number(taskDetail.status) === 1 ? "进行中" : "已暂停"} + +
+
+
+ + 今日点赞: + + {taskDetail.todayLikeCount || 0} + +
+
+ + 总点赞数: + + {taskDetail.totalLikeCount || 0} + +
+
+
+ )} + + {/* 搜索区域 */} +
+
+
+ + setSearchTerm(e.target.value)} + onPressEnter={handleSearch} + /> +
+ + +
+
+ + {/* 记录列表 */} +
+ {recordsLoading && currentPage === 1 ? ( +
+ +
加载中...
+
+ ) : records.length === 0 ? ( +
+ +
暂无点赞记录
+
+ ) : ( + + )} +
+
+
+ ); +}; + +export default AutoLikeDetail; diff --git a/nkebao/src/pages/workspace/auto-like/record/record.module.scss b/nkebao/src/pages/workspace/auto-like/record/record.module.scss new file mode 100644 index 00000000..9bc931b6 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-like/record/record.module.scss @@ -0,0 +1,351 @@ +.detail-page { + background: #f5f5f5; + min-height: 100vh; + padding-bottom: 80px; +} + +.task-info-card { + background: white; + margin: 16px; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.task-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.task-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin: 0; +} + +.task-status { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + + &.active { + background: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; + } + + &.inactive { + background: #f5f5f5; + color: #666; + border: 1px solid #d9d9d9; + } +} + +.task-stats { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: #666; +} + +.stat-icon { + font-size: 16px; + color: #1890ff; +} + +.stat-label { + font-weight: 500; +} + +.stat-value { + color: #333; + font-weight: 600; +} + +.search-section { + padding: 0 16px 16px; +} + +.search-wrapper { + display: flex; + align-items: center; + gap: 8px; + background: white; + border-radius: 8px; + padding: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.search-input-wrapper { + position: relative; + flex: 1; +} + +.search-input { + width: 100%; + height: 36px; + padding: 0 12px 0 32px; + border: 1px solid #d9d9d9; + border-radius: 6px; + font-size: 14px; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } +} + +.search-icon { + position: absolute; + left: 8px; + top: 50%; + transform: translateY(-50%); + color: #999; + font-size: 14px; +} + +.search-btn { + height: 36px; + padding: 0 12px; + border-radius: 6px; + font-size: 14px; + white-space: nowrap; +} + +.refresh-btn { + height: 36px; + width: 36px; + padding: 0; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #d9d9d9; + background: white; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.records-section { + padding: 0 16px; +} + +.records-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.record-card { + background: white; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.record-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; +} + +.user-info { + display: flex; + align-items: center; + gap: 12px; + max-width: 65%; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.user-details { + min-width: 0; +} + +.user-name { + font-size: 14px; + font-weight: 600; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-role { + font-size: 12px; + color: #666; + margin-top: 2px; +} + +.record-time { + font-size: 12px; + color: #666; + background: #f8f9fa; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; +} + +.record-content { + margin-bottom: 12px; +} + +.content-text { + font-size: 14px; + color: #333; + line-height: 1.5; + margin-bottom: 12px; + white-space: pre-line; +} + +.content-images { + display: grid; + gap: 4px; + + &.single { + grid-template-columns: 1fr; + } + + &.double { + grid-template-columns: 1fr 1fr; + } + + &.multiple { + grid-template-columns: repeat(3, 1fr); + } +} + +.image-item { + aspect-ratio: 1; + border-radius: 6px; + overflow: hidden; +} + +.content-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.like-info { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #f8f9fa; + border-radius: 6px; +} + +.operator-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + flex-shrink: 0; +} + +.like-text { + font-size: 14px; + color: #666; + min-width: 0; +} + +.operator-name { + font-weight: 600; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + max-width: 100%; +} + +.like-action { + margin-left: 4px; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + gap: 16px; +} + +.loading-text { + color: #666; + font-size: 14px; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; +} + +.empty-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.empty-text { + font-size: 16px; + color: #999; +} + +// 移动端适配 +@media (max-width: 768px) { + .task-stats { + flex-direction: column; + gap: 12px; + } + + .search-wrapper { + flex-direction: column; + gap: 12px; + } + + .search-btn { + width: 100%; + } + + .user-info { + max-width: 60%; + } + + .content-images { + &.multiple { + grid-template-columns: repeat(2, 1fr); + } + } +} \ No newline at end of file diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index b7ccc7c2..20e199b9 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -1,7 +1,7 @@ import Workspace from "@/pages/workspace/main"; -import AutoLike from "@/pages/workspace/auto-like/AutoLike"; -import NewAutoLike from "@/pages/workspace/auto-like/NewAutoLike"; -import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; +import ListAutoLike from "@/pages/workspace/auto-like/list"; +import NewAutoLike from "@/pages/workspace/auto-like/new"; +import RecordAutoLike from "@/pages/workspace/auto-like/record"; import AutoGroup from "@/pages/workspace/auto-group/AutoGroup"; import AutoGroupDetail from "@/pages/workspace/auto-group/Detail"; import GroupPush from "@/pages/workspace/group-push/GroupPush"; @@ -24,7 +24,7 @@ const workspaceRoutes = [ // 自动点赞 { path: "/workspace/auto-like", - element: , + element: , auth: true, }, { @@ -34,7 +34,7 @@ const workspaceRoutes = [ }, { path: "/workspace/auto-like/:id", - element: , + element: , auth: true, }, { diff --git a/nkebao/src/types/auto-like.ts b/nkebao/src/types/auto-like.ts new file mode 100644 index 00000000..bace06ec --- /dev/null +++ b/nkebao/src/types/auto-like.ts @@ -0,0 +1,119 @@ +// 自动点赞任务状态 +export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 + +// 内容类型 +export type ContentType = "text" | "image" | "video" | "link"; + +// 设备信息 +export interface Device { + id: string; + name: string; + status: "online" | "offline"; + lastActive: string; +} + +// 好友信息 +export interface Friend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + tags: string[]; + region: string; + source: string; +} + +// 点赞记录 +export interface LikeRecord { + id: string; + workbenchId: string; + momentsId: string; + snsId: string; + wechatAccountId: string; + wechatFriendId: string; + likeTime: string; + content: string; + resUrls: string[]; + momentTime: string; + userName: string; + operatorName: string; + operatorAvatar: string; + friendName: string; + friendAvatar: string; +} + +// 自动点赞任务 +export interface LikeTask { + id: string; + name: string; + status: LikeTaskStatus; + deviceCount: number; + targetGroup: string; + likeCount: number; + lastLikeTime: string; + createTime: string; + creator: string; + likeInterval: number; + maxLikesPerDay: number; + timeRange: { start: string; end: string }; + contentTypes: ContentType[]; + targetTags: string[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; + todayLikeCount: number; + totalLikeCount: number; + updateTime: string; +} + +// 创建任务数据 +export interface CreateLikeTaskData { + 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 interface UpdateLikeTaskData extends CreateLikeTaskData { + id: string; +} + +// 任务配置 +export interface TaskConfig { + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; +} + +// API响应类型 +export interface ApiResponse { + code: number; + msg: string; + data: T; +} + +// 分页响应类型 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +} From 60af57317d2e4cd737e2f298f0594aef8127a63b 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: Mon, 21 Jul 2025 17:58:09 +0800 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E7=BE=A4=E6=B6=88=E6=81=AF=E6=8E=A8=E9=80=81=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E6=A0=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 --- .../form/components/StepIndicator.tsx | 27 +++++++-- .../workspace/group-push/form/index.data.ts | 32 +++++++++++ .../pages/workspace/group-push/form/index.tsx | 55 ++++--------------- .../group-push/list/index.module.scss | 6 +- .../pages/workspace/group-push/list/index.tsx | 25 ++++++++- 5 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 nkebao/src/pages/workspace/group-push/form/index.data.ts diff --git a/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx index dc293d2e..5702c77f 100644 --- a/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx +++ b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Steps } from "antd"; +import { Steps } from "antd-mobile"; interface StepIndicatorProps { currentStep: number; @@ -11,10 +11,29 @@ const StepIndicator: React.FC = ({ steps, }) => { return ( -
+
- {steps.map((step) => ( - + {steps.map((step, idx) => ( + + {step.id} +
+ } + /> ))}
diff --git a/nkebao/src/pages/workspace/group-push/form/index.data.ts b/nkebao/src/pages/workspace/group-push/form/index.data.ts new file mode 100644 index 00000000..fd965045 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/index.data.ts @@ -0,0 +1,32 @@ +export interface WechatGroup { + id: string; + name: string; + avatar: string; + serviceAccount: { + id: string; + name: string; + avatar: string; + }; +} + +export interface ContentLibrary { + id: string; + name: string; + targets: Array<{ + id: string; + avatar: string; + }>; +} + +export interface FormData { + name: string; + pushTimeStart: string; + pushTimeEnd: string; + dailyPushCount: number; + pushOrder: "earliest" | "latest"; + isLoopPush: boolean; + isImmediatePush: boolean; + isEnabled: boolean; + groups: WechatGroup[]; + contentLibraries: ContentLibrary[]; +} diff --git a/nkebao/src/pages/workspace/group-push/form/index.tsx b/nkebao/src/pages/workspace/group-push/form/index.tsx index 4728be4f..e6b4be35 100644 --- a/nkebao/src/pages/workspace/group-push/form/index.tsx +++ b/nkebao/src/pages/workspace/group-push/form/index.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Button } from "antd"; +import { Button } from "antd-mobile"; +import { NavBar } from "antd-mobile"; +import { LeftOutline } from "antd-mobile-icons"; import { createGroupPushTask } from "@/api/groupPush"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; @@ -8,39 +10,7 @@ import StepIndicator from "./components/StepIndicator"; import BasicSettings from "./components/BasicSettings"; import GroupSelector from "./components/GroupSelector"; import ContentSelector from "./components/ContentSelector"; - -interface WechatGroup { - id: string; - name: string; - avatar: string; - serviceAccount: { - id: string; - name: string; - avatar: string; - }; -} - -interface ContentLibrary { - id: string; - name: string; - targets: Array<{ - id: string; - avatar: string; - }>; -} - -interface FormData { - name: string; - pushTimeStart: string; - pushTimeEnd: string; - dailyPushCount: number; - pushOrder: "earliest" | "latest"; - isLoopPush: boolean; - isImmediatePush: boolean; - isEnabled: boolean; - groups: WechatGroup[]; - contentLibraries: ContentLibrary[]; -} +import type { WechatGroup, ContentLibrary, FormData } from "./index.data"; const steps = [ { id: 1, title: "步骤 1", subtitle: "基础设置" }, @@ -136,16 +106,13 @@ const NewGroupPush: React.FC = () => { return ( navigate(-1)} + style={{ background: "#fff" }} + right={null} > - 新建社群推送任务 - + 群消息推送 + } footer={} > @@ -206,7 +173,7 @@ const NewGroupPush: React.FC = () => { - + } + > } footer={} > From 0658292354d3e01a62c99aec482be8564f343a4c 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: Mon, 21 Jul 2025 18:18:45 +0800 Subject: [PATCH 11/13] =?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=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/pages/scenarios/list/index.tsx | 11 +- .../scenarios/plan/list/index.module.scss | 12 - .../src/pages/scenarios/plan/list/index.tsx | 6 +- nkebao/src/pages/scenarios/plan/new/index.tsx | 59 ++- .../pages/scenarios/plan/new/page.module.scss | 6 +- .../auto-group/detail/index.module.scss | 148 ++++++- .../workspace/auto-group/detail/index.tsx | 381 +++++++++++++++++- .../auto-group/form/index.module.scss | 33 +- .../pages/workspace/auto-group/form/index.tsx | 242 ++++++++++- .../auto-group/list/index.module.scss | 202 +++++++++- .../pages/workspace/auto-group/list/index.tsx | 1 - nkebao/src/styles/global.scss | 38 +- 12 files changed, 1070 insertions(+), 69 deletions(-) diff --git a/nkebao/src/pages/scenarios/list/index.tsx b/nkebao/src/pages/scenarios/list/index.tsx index 97be9b46..fe28da46 100644 --- a/nkebao/src/pages/scenarios/list/index.tsx +++ b/nkebao/src/pages/scenarios/list/index.tsx @@ -82,13 +82,12 @@ const Scene: React.FC = () => { -
场景获客
+
场景获客
@@ -113,21 +112,19 @@ const Scene: React.FC = () => { 场景获客} + left={
场景获客
} right={ } >
} - footer={} >
diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index f8b8ddb0..4df03dc4 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -2,18 +2,6 @@ padding:0 16px; } -.nav-title { - font-size: 18px; - font-weight: 600; - color: #333; -} - -.new-plan-btn { - font-size: 14px; - height: 32px; - padding: 0 12px; -} - .loading { display: flex; flex-direction: column; diff --git a/nkebao/src/pages/scenarios/plan/list/index.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx index ebeb8f46..a2cd331d 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -356,8 +356,8 @@ const ScenarioList: React.FC = () => { back={null} style={{ background: "#fff" }} left={ -
- +
+ navigate(-1)} fontSize={24} /> {scenarioName} @@ -368,7 +368,7 @@ const ScenarioList: React.FC = () => { size="small" color="primary" onClick={handleCreateNewPlan} - className={style["new-plan-btn"]} + className="new-plan-btn" > 新建计划 diff --git a/nkebao/src/pages/scenarios/plan/new/index.tsx b/nkebao/src/pages/scenarios/plan/new/index.tsx index cf52b104..4d3e1497 100644 --- a/nkebao/src/pages/scenarios/plan/new/index.tsx +++ b/nkebao/src/pages/scenarios/plan/new/index.tsx @@ -248,41 +248,36 @@ const NewPlan: React.FC = () => { return ( - {isEdit ? "编辑计划" : "新建计划"} -
- } - right={ - - } - /> + <> + + + navigate(-1)} fontSize={24} /> + + {isEdit ? "编辑计划" : "新建计划"} +
+ } + /> + + {/* 步骤指示器 */} +
+ + {steps.map((step) => ( + + ))} + +
+ } - footer={} >
- {/* 步骤指示器 */} -
- - {steps.map((step) => ( - - ))} - -
- {/* 步骤内容 */}
{renderStepContent()}
diff --git a/nkebao/src/pages/scenarios/plan/new/page.module.scss b/nkebao/src/pages/scenarios/plan/new/page.module.scss index eaff99f1..0d6784fd 100644 --- a/nkebao/src/pages/scenarios/plan/new/page.module.scss +++ b/nkebao/src/pages/scenarios/plan/new/page.module.scss @@ -1,6 +1,4 @@ .new-plan-page { - background: #f5f5f5; - min-height: 100vh; } .nav-title { @@ -31,10 +29,8 @@ } .steps-container { - background: white; - padding: 20px 16px; + background: #ffffff; margin-bottom: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .step-content { diff --git a/nkebao/src/pages/workspace/auto-group/detail/index.module.scss b/nkebao/src/pages/workspace/auto-group/detail/index.module.scss index 93c42e85..ff8c8d5e 100644 --- a/nkebao/src/pages/workspace/auto-group/detail/index.module.scss +++ b/nkebao/src/pages/workspace/auto-group/detail/index.module.scss @@ -1,3 +1,149 @@ .autoGroupDetail { - // 这里写详情页样式 + padding: 16px 0 80px 0; + background: #f7f8fa; + min-height: 100vh; +} + +.headerBar { + display: flex; + align-items: center; + height: 48px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + font-size: 18px; + font-weight: 600; + padding: 0 16px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #222; + flex: 1; + text-align: center; +} + +.infoCard { + margin-bottom: 16px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 16px; +} +.infoGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} +.infoTitle { + font-size: 14px; + font-weight: 500; + color: #1677ff; + margin-bottom: 4px; +} +.infoItem { + font-size: 13px; + color: #444; + margin-bottom: 2px; +} + +.progressSection { + margin-top: 16px; +} +.progressCard { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 16px; + margin-bottom: 16px; +} +.progressHeader { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 15px; + font-weight: 500; + margin-bottom: 8px; +} +.groupList { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 12px; +} +.groupCard { + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + border: none; + background: #fff; + padding: 12px 16px; +} +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 15px; + font-weight: 500; + margin-bottom: 8px; +} +.memberGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + margin-top: 8px; +} +.memberItem { + background: #f5f7fa; + border-radius: 8px; + padding: 4px 8px; + font-size: 13px; + color: #333; + display: flex; + align-items: center; +} +.warnText { + color: #faad14; + font-size: 13px; + margin-top: 8px; + display: flex; + align-items: center; +} +.successText { + color: #389e0d; + font-size: 13px; + margin-top: 8px; + display: flex; + align-items: center; +} +.successAlert { + color: #389e0d; + background: #f6ffed; + border-radius: 8px; + padding: 8px 0; + text-align: center; + margin-top: 12px; + font-size: 14px; +} +.emptyCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + margin-top: 32px; +} +.emptyTitle { + font-size: 16px; + color: #888; + margin: 12px 0 4px 0; +} +.emptyDesc { + font-size: 13px; + color: #bbb; + margin-bottom: 16px; } \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-group/detail/index.tsx b/nkebao/src/pages/workspace/auto-group/detail/index.tsx index 00f574dd..058c320a 100644 --- a/nkebao/src/pages/workspace/auto-group/detail/index.tsx +++ b/nkebao/src/pages/workspace/auto-group/detail/index.tsx @@ -1,7 +1,384 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + Card, + Button, + Toast, + ProgressBar, + Tag, + SpinLoading, +} from "antd-mobile"; +import { TeamOutline, LeftOutline } from "antd-mobile-icons"; +import { AlertOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import style from "./index.module.scss"; + +interface GroupMember { + id: string; + nickname: string; + wechatId: string; + tags: string[]; +} + +interface Group { + id: string; + members: GroupMember[]; +} + +interface GroupTaskDetail { + id: string; + name: string; + status: "preparing" | "creating" | "completed" | "paused"; + totalGroups: number; + currentGroupIndex: number; + groups: Group[]; + createTime: string; + lastUpdateTime: string; + creator: string; + deviceCount: number; + targetFriends: number; + groupSize: { min: number; max: number }; + timeRange: { start: string; end: string }; + targetTags: string[]; + groupNameTemplate: string; + groupDescription: string; +} + +const mockTaskDetail: GroupTaskDetail = { + id: "1", + name: "VIP客户建群", + status: "creating", + totalGroups: 5, + currentGroupIndex: 2, + groups: Array.from({ length: 5 }).map((_, index) => ({ + id: `group-${index}`, + members: Array.from({ length: Math.floor(Math.random() * 10) + 30 }).map( + (_, mIndex) => ({ + id: `member-${index}-${mIndex}`, + nickname: `用户${mIndex + 1}`, + wechatId: `wx_${mIndex}`, + tags: [`标签${(mIndex % 3) + 1}`], + }) + ), + })), + createTime: "2024-11-20 19:04:14", + lastUpdateTime: "2025-02-06 13:12:35", + creator: "admin", + deviceCount: 2, + targetFriends: 156, + groupSize: { min: 20, max: 50 }, + timeRange: { start: "09:00", end: "21:00" }, + targetTags: ["VIP客户", "高价值"], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "VIP客户专属交流群,提供优质服务", +}; + +const GroupPreview: React.FC<{ + groupIndex: number; + members: GroupMember[]; + isCreating: boolean; + isCompleted: boolean; + onRetry?: () => void; +}> = ({ groupIndex, members, isCreating, isCompleted, onRetry }) => { + const [expanded, setExpanded] = useState(false); + const targetSize = 38; + return ( + +
+
+ 群 {groupIndex + 1} + + {isCompleted ? "已完成" : isCreating ? "创建中" : "等待中"} + +
+
+ + {members.length}/{targetSize} +
+
+ {isCreating && !isCompleted && ( + + )} + {expanded ? ( + <> +
+ {members.map((member) => ( +
+ {member.nickname} + {member.tags.length > 0 && ( + + {member.tags[0]} + + )} +
+ ))} +
+ + + ) : ( + + )} + {!isCompleted && members.length < targetSize && ( +
+ + 群人数不足{targetSize}人 + {onRetry && ( + + )} +
+ )} + {isCompleted &&
群创建完成
} +
+ ); +}; + +const GroupCreationProgress: React.FC<{ + taskDetail: GroupTaskDetail; + onComplete: () => void; +}> = ({ taskDetail, onComplete }) => { + const [groups, setGroups] = useState(taskDetail.groups); + const [currentGroupIndex, setCurrentGroupIndex] = useState( + taskDetail.currentGroupIndex + ); + const [status, setStatus] = useState( + taskDetail.status + ); + + useEffect(() => { + if (status === "creating" && currentGroupIndex < groups.length) { + const timer = setTimeout(() => { + if (currentGroupIndex === groups.length - 1) { + setStatus("completed"); + onComplete(); + } else { + setCurrentGroupIndex((prev) => prev + 1); + } + }, 3000); + return () => clearTimeout(timer); + } + }, [status, currentGroupIndex, groups.length, onComplete]); + + const handleRetryGroup = (groupIndex: number) => { + setGroups((prev) => + prev.map((group, index) => { + if (index === groupIndex) { + return { + ...group, + members: [ + ...group.members, + { + id: `retry-member-${Date.now()}`, + nickname: `补充用户${group.members.length + 1}`, + wechatId: `wx_retry_${Date.now()}`, + tags: ["新加入"], + }, + ], + }; + } + return group; + }) + ); + }; + + return ( +
+ +
+
+ 建群进度 + + {status === "preparing" + ? "准备中" + : status === "creating" + ? "创建中" + : "已完成"} + +
+
+ {currentGroupIndex + 1}/{groups.length}组 +
+
+ +
+
+ {groups.map((group, index) => ( + handleRetryGroup(index)} + /> + ))} +
+ {status === "completed" && ( +
所有群组已创建完成
+ )} +
+ ); +}; const AutoGroupDetail: React.FC = () => { - return
自动建群详情页
; + const { id } = useParams(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [taskDetail, setTaskDetail] = useState(null); + + useEffect(() => { + setLoading(true); + setTimeout(() => { + setTaskDetail(mockTaskDetail); + setLoading(false); + }, 800); + }, [id]); + + const handleComplete = () => { + Toast.show({ content: "所有群组已创建完成" }); + }; + + if (loading) { + return ( + + +
建群详情
+
+ } + footer={} + loading={true} + > +
+ + ); + } + + if (!taskDetail) { + return ( + + +
建群详情
+
+ } + footer={} + > + + +
任务不存在
+
请检查任务ID是否正确
+ +
+ + ); + } + + return ( + + +
{taskDetail.name} - 建群详情
+
+ } + footer={} + > +
+ +
+
+
基本信息
+
任务名称:{taskDetail.name}
+
+ 创建时间:{taskDetail.createTime} +
+
创建人:{taskDetail.creator}
+
+ 执行设备:{taskDetail.deviceCount} 个 +
+
+
+
建群配置
+
+ 群组规模:{taskDetail.groupSize.min}-{taskDetail.groupSize.max}{" "} + 人 +
+
+ 执行时间:{taskDetail.timeRange.start} -{" "} + {taskDetail.timeRange.end} +
+
+ 目标标签:{taskDetail.targetTags.join(", ")} +
+
+ 群名称模板:{taskDetail.groupNameTemplate} +
+
+
+
+ +
+
+ ); }; export default AutoGroupDetail; diff --git a/nkebao/src/pages/workspace/auto-group/form/index.module.scss b/nkebao/src/pages/workspace/auto-group/form/index.module.scss index 19f9a662..20bf7f92 100644 --- a/nkebao/src/pages/workspace/auto-group/form/index.module.scss +++ b/nkebao/src/pages/workspace/auto-group/form/index.module.scss @@ -1,3 +1,34 @@ .autoGroupForm { - // 这里写新建/编辑页样式 + padding: 10px; + background: #f7f8fa; +} + +.headerBar { + display: flex; + align-items: center; + height: 48px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + font-size: 18px; + font-weight: 600; + padding: 0 16px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #222; + flex: 1; + text-align: center; +} + +.timeRangeRow { + display: flex; + align-items: center; + gap: 8px; +} +.groupSizeRow { + display: flex; + align-items: center; + gap: 8px; } \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-group/form/index.tsx b/nkebao/src/pages/workspace/auto-group/form/index.tsx index 3a8d8bd1..dfb5a27e 100644 --- a/nkebao/src/pages/workspace/auto-group/form/index.tsx +++ b/nkebao/src/pages/workspace/auto-group/form/index.tsx @@ -1,7 +1,245 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Form, + Input, + Button, + Toast, + Switch, + Selector, + TextArea, +} from "antd-mobile"; +import { LeftOutline } from "antd-mobile-icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import style from "./index.module.scss"; +import { createAutoGroup, updateAutoGroup } from "./api"; + +const defaultForm = { + name: "", + deviceCount: 1, + targetFriends: 0, + createInterval: 300, + maxGroupsPerDay: 10, + timeRange: { start: "09:00", end: "21:00" }, + groupSize: { min: 20, max: 50 }, + targetTags: [], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "", +}; + +const tagOptions = [ + { label: "VIP客户", value: "VIP客户" }, + { label: "高价值", value: "高价值" }, + { label: "潜在客户", value: "潜在客户" }, + { label: "中意向", value: "中意向" }, +]; const AutoGroupForm: React.FC = () => { - return
自动建群新建/编辑页
; + const navigate = useNavigate(); + const { id } = useParams(); + const isEdit = Boolean(id); + const [form, setForm] = useState(defaultForm); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isEdit) { + // 这里应请求详情接口,回填表单,演示用mock + setForm({ + ...defaultForm, + name: "VIP客户建群", + deviceCount: 2, + targetFriends: 156, + createInterval: 300, + maxGroupsPerDay: 20, + timeRange: { start: "09:00", end: "21:00" }, + groupSize: { min: 20, max: 50 }, + targetTags: ["VIP客户", "高价值"], + groupNameTemplate: "VIP客户交流群{序号}", + groupDescription: "VIP客户专属交流群,提供优质服务", + }); + } + }, [isEdit, id]); + + const handleSubmit = async () => { + setLoading(true); + try { + if (isEdit) { + await updateAutoGroup(id as string, form); + Toast.show({ content: "编辑成功" }); + } else { + await createAutoGroup(form); + Toast.show({ content: "创建成功" }); + } + navigate("/workspace/auto-group"); + } catch (e) { + Toast.show({ content: "提交失败" }); + } finally { + setLoading(false); + } + }; + + return ( + + +
+ {isEdit ? "编辑建群任务" : "新建建群任务"} +
+ + } + > +
+
+ {isEdit ? "保存修改" : "创建任务"} + + } + > + + setForm((f: any) => ({ ...f, name: val }))} + placeholder="请输入任务名称" + /> + + + + setForm((f: any) => ({ ...f, deviceCount: Number(val) })) + } + placeholder="请输入设备数量" + /> + + + + setForm((f: any) => ({ ...f, targetFriends: Number(val) })) + } + placeholder="请输入目标好友数" + /> + + + + setForm((f: any) => ({ ...f, createInterval: Number(val) })) + } + placeholder="请输入建群间隔" + /> + + + + setForm((f: any) => ({ ...f, maxGroupsPerDay: Number(val) })) + } + placeholder="请输入最大建群数" + /> + + +
+ + setForm((f: any) => ({ + ...f, + timeRange: { ...f.timeRange, start: val }, + })) + } + placeholder="开始时间" + /> + - + + setForm((f: any) => ({ + ...f, + timeRange: { ...f.timeRange, end: val }, + })) + } + placeholder="结束时间" + /> +
+
+ +
+ + setForm((f: any) => ({ + ...f, + groupSize: { ...f.groupSize, min: Number(val) }, + })) + } + placeholder="最小人数" + /> + - + + setForm((f: any) => ({ + ...f, + groupSize: { ...f.groupSize, max: Number(val) }, + })) + } + placeholder="最大人数" + /> +
+
+ + + setForm((f: any) => ({ ...f, targetTags: val })) + } + /> + + + + setForm((f: any) => ({ ...f, groupNameTemplate: val })) + } + placeholder="请输入群名称模板" + /> + + +