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" + /> +
+
+ {/* 选素材 */}
选择海报
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/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 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/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index b05b981f..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, @@ -150,13 +153,20 @@ class PosterWeChatMiniProgram extends Controller } + if(isset($sceneConf['tips'])) { + $sTip = $sceneConf['tips']; + } else { + $sTip = ''; + } + unset($task['sceneConf']); + $task['sTip'] = $sTip; $data = [ 'id' => $task['id'], 'name' => $task['name'], 'poster' => ['sUrl' => $posterUrl], - 'sTip' => '', + 'task' => $task, ]; 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; } } 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/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/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/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..ff8c8d5e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/detail/index.module.scss @@ -0,0 +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 new file mode 100644 index 00000000..058c320a --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/detail/index.tsx @@ -0,0 +1,384 @@ +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 = () => { + 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/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..20bf7f92 --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/form/index.module.scss @@ -0,0 +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 new file mode 100644 index 00000000..dfb5a27e --- /dev/null +++ b/nkebao/src/pages/workspace/auto-group/form/index.tsx @@ -0,0 +1,245 @@ +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 = () => { + 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="请输入群名称模板" + /> + + +