From abdcff813c6667e0577e01578cc1f8faa80f8b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Tue, 23 Dec 2025 15:45:15 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx index 63f5827d..c495bdda 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx @@ -59,7 +59,7 @@ export const featureCategories: FeatureCard[] = [ "标签化精准推送", "接待模式切换", ], - path: "/pc/commonConfigss", + path: "/pc/commonConfig", }, // { // id: "content-library", From 4f2b34522f5451eb4f871278f3503f14e92fb86f Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 23 Dec 2025 17:01:59 +0800 Subject: [PATCH 02/17] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/mobile/mine/content/form/index.tsx | 5 +- .../controller/ContentLibraryController.php | 4 +- Server/application/job/SyncContentJob.php | 62 ++++++++++++------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index 98527516..f758cee4 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -9,8 +9,7 @@ import GroupSelection from "@/components/GroupSelection"; import DeviceSelection from "@/components/DeviceSelection"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; -import request from "@/api/request"; -import { getContentLibraryDetail, updateContentLibrary } from "./api"; +import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -162,7 +161,7 @@ export default function ContentForm() { await updateContentLibrary({ id, ...payload }); Toast.show({ content: "保存成功", position: "top" }); } else { - await request("/v1/content/library/create", { ...payload, formType: 0 }, "POST"); + await createContentLibrary(payload); Toast.show({ content: "创建成功", position: "top" }); } navigate("/mine/content"); diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 1151a876..690d8a64 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -95,7 +95,7 @@ class ContentLibraryController extends Controller // 来源类型 'sourceType' => $sourceType, // 1=好友,2=群,3=好友和群 // 表单类型 - 'formType' => isset($param['formType']) ? intval($param['formType']) : 0, // 表单类型,默认为0 + 'formType' => isset($param['formType']) ? intval($param['formType']) : 1, // 表单类型,默认为0 // 基础信息 'status' => isset($param['status']) ? $param['status'] : 0, // 状态:0=禁用,1=启用 'userId' => $this->request->userInfo['id'], @@ -131,7 +131,7 @@ class ContentLibraryController extends Controller $limit = $this->request->param('limit', 10); $keyword = $this->request->param('keyword', ''); $sourceType = $this->request->param('sourceType', ''); // 来源类型,1=好友,2=群 - $formType = $this->request->param('formType', ''); // 表单类型筛选 + $formType = $this->request->param('formType', 0); // 表单类型筛选 $companyId = $this->request->userInfo['companyId']; $userId = $this->request->userInfo['id']; $isAdmin = !empty($this->request->userInfo['isAdmin']); diff --git a/Server/application/job/SyncContentJob.php b/Server/application/job/SyncContentJob.php index 8f2b9b4f..e5e95bc5 100644 --- a/Server/application/job/SyncContentJob.php +++ b/Server/application/job/SyncContentJob.php @@ -1,34 +1,54 @@ delete(); - - // 记录日志 - \think\Log::info('内容库同步成功:' . json_encode($data)); - - } catch (\Exception $e) { - // 如果任务执行失败,记录日志 - \think\Log::error('内容库同步失败:' . $e->getMessage()); - - // 如果任务失败次数小于3次,重新放入队列 - if ($job->attempts() < 3) { - $job->release(60); // 延迟60秒后重试 - } else { - $job->delete(); + return true; + /*$ddd= '[{"id":21909,"groupName":"私域运营招聘","sortIndex":240426546,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8074,"groupName":"存客宝新客户介绍","sortIndex":230,"parentId":0,"replyType":1,"children":[{"id":8081,"groupName":"客户系统上线准备","sortIndex":1,"parentId":8074,"replyType":1,"children":[],"replys":null}],"replys":null},{"id":10441,"groupName":"BOSS直聘","sortIndex":229,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":15111,"groupName":"点了码新客户了解","sortIndex":228,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":12732,"groupName":"测试","sortIndex":219,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8814,"groupName":"封单话术","sortIndex":176,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8216,"groupName":"私域合伙人","sortIndex":172,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":6476,"groupName":"新客户了解","sortIndex":119,"parentId":0,"replyType":1,"children":[],"replys":null}]'; + $ddd=json_decode($ddd,1);*/ + + $ddd = ReplyGroup::where('companyId',2778)->where('companyId','<>',21898)->where('isDel',0)->select()->toArray(); + + + $authorization = 'OE7kh6Dsw_0SqqH1FTAPCB2ewCQDhx7VvPw6PrsE_p9tcRKbtlFsZau8kjk2NQ829Yah90KhTh0C_35ek569uRQgM_gC0NtKzfRPDDoqMIUE5mI6AO_hm0dm-xDJqhAFYkXHCdXnJYzQZxWS5dleJCIwtQxgRuIzIbr-_G_5C-7DeLEOSt2vi1oGPleLt00QGQ1WYVYqoHYrbPGMghMQpWIbgk5qNcUCeANlLJ_s7QFC3QzArU95_YiK0HlhU81hZqr8kI_5lmdrRBoR-yNIlyhySLRCmEZYGzOxCiUHL3uFHYZA1VnLBAVbryNj5DElZjMgwA'; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + + + foreach ($ddd as $key => $value) { + $data = []; + // 发送请求获取公司账号列表 + $result = requestCurl('https://s2.siyuguanli.com:9991/api/Reply/listReply?groupId='.$value['id'], '', 'GET', $header,'json'); + $response = handleApiResponse($result); + foreach ($response as $k => $v) { + $data[] = [ + 'groupId' => $v['groupId'], + 'userId' => $value['userId'], + 'title' => $v['title'], + 'msgType' => $v['msgType'], + 'content' => $v['content'], + 'createTime' => strtotime($v['createTime']), + 'lastUpdateTime' => strtotime($v['lastUpdateTime']), + 'sortIndex' => 50 + ]; } + $Reply = new Reply(); + $Reply->insertAll($data); } + + + exit_data(11111); + + + + } } \ No newline at end of file From 302617cd817c5d596981e0d75dc33c9af7fa83e5 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 24 Dec 2025 15:27:57 +0800 Subject: [PATCH 03/17] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=8E=B7=E5=AE=A2=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=97=AE=E9=A2=98=202=E3=80=81=E4=BF=AE=E5=A4=8D=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=A4=B4=E5=83=8F=E7=A0=B4=E5=9B=BE=E9=97=AE=E9=A2=98?= =?UTF-8?q?=203=E3=80=81=E4=BF=AE=E5=A4=8D=E5=81=A5=E5=BA=B7=E5=88=86?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98=204=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=8E=B7=E5=AE=A2=E5=9C=BA=E6=99=AF=5F=E5=A5=BD?= =?UTF-8?q?=E5=8F=8B=E8=BF=81=E7=A7=BB=E7=BC=96=E8=BE=91=E4=BC=9A=E8=B7=B3?= =?UTF-8?q?=E5=88=B0=E5=85=B6=E4=BB=96=E7=B1=BB=E7=9B=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mine/wechat-accounts/detail/index.tsx | 8 +- .../pages/mobile/scenarios/plan/list/api.ts | 11 +- .../plan/list/components/PoolListModal.tsx | 143 ++++++++++++++---- .../plan/list/components/Popups.module.scss | 26 ++++ .../pages/mobile/scenarios/plan/new/index.tsx | 4 +- .../plan/new/steps/BasicSettings.tsx | 18 ++- .../scenarios/plan/new/steps/base.module.scss | 11 ++ .../cunkebao/controller/StatsController.php | 2 +- .../controller/StoreAccountController.php | 2 +- .../controller/plan/PlanSceneV1Controller.php | 6 +- .../controller/wechat/GetWechatController.php | 2 +- .../Adapters/ChuKeBao/Adapter.php | 10 +- 12 files changed, 197 insertions(+), 46 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index a985f182..54e8de40 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -692,13 +692,13 @@ const WechatAccountDetail: React.FC = () => {
{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} - 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "-"}
- {overviewData?.healthScoreAssessment?.score || 67} + {overviewData?.healthScoreAssessment?.score || 0}
SCORE
@@ -810,7 +810,7 @@ const WechatAccountDetail: React.FC = () => {
- {overviewData?.healthScoreAssessment?.score || 67} + {overviewData?.healthScoreAssessment?.score || 0}
SCORE
@@ -869,7 +869,7 @@ const WechatAccountDetail: React.FC = () => {
- {record.title || record.description || "记录"} + {record.name || record.description || "记录"} {record.statusTag && ( {record.statusTag} diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts index 26377d6c..3943a92a 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts @@ -36,6 +36,13 @@ export function getUserList(planId: string, type: number) { } //获客列表 -export function getFriendRequestTaskStats(taskId: string) { - return request(`/v1/dashboard/friendRequestTaskStats`, { taskId }, "GET"); +export function getFriendRequestTaskStats( + taskId: string, + params?: { startTime?: string; endTime?: string }, +) { + return request( + `/v1/dashboard/friendRequestTaskStats`, + { taskId, ...params }, + "GET", + ); } diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx index a084dc3c..4871d2ba 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { Popup, SpinLoading } from "antd-mobile"; -import { Button, message } from "antd"; -import { CloseOutlined } from "@ant-design/icons"; +import React, { useEffect, useState, useCallback } from "react"; +import { Popup, SpinLoading, DatePicker } from "antd-mobile"; +import { Button, message, Input } from "antd"; +import { CloseOutlined, CalendarOutlined } from "@ant-design/icons"; import style from "./Popups.module.scss"; import { getFriendRequestTaskStats } from "../api"; import LineChart2 from "@/components/LineChart2"; @@ -39,31 +39,73 @@ const PoolListModal: React.FC = ({ const [xData, setXData] = useState([]); const [yData, setYData] = useState([]); const [loading, setLoading] = useState(false); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const [showStartTimePicker, setShowStartTimePicker] = useState(false); + const [showEndTimePicker, setShowEndTimePicker] = useState(false); - // 当弹窗打开且有ruleId时,获取数据 + // 格式化日期为 YYYY-MM-DD + const formatDate = useCallback((date: Date | null): string => { + if (!date) return ""; + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + }, []); + + // 初始化默认时间(近7天) useEffect(() => { - if (visible && ruleId) { - setLoading(true); - getFriendRequestTaskStats(ruleId.toString()) - .then(res => { - console.log(res); - setXData(res.dateArray); - setYData([ - res.allNumArray, - res.errorNumArray, - res.passNumArray, - res.passRateArray, - res.successNumArray, - res.successRateArray, - ]); - setStatistics(res.totalStats); - setLoading(false); - }) - .finally(() => { - setLoading(false); - }); + if (visible) { + // 如果时间未设置,设置默认值为近7天 + if (!startTime || !endTime) { + const today = new Date(); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(today.getDate() - 7); + setStartTime(sevenDaysAgo); + setEndTime(today); + } + } else { + // 弹窗关闭时重置时间,下次打开时重新初始化 + setStartTime(null); + setEndTime(null); } - }, [visible, ruleId]); + }, [visible]); + + // 当弹窗打开或有ruleId或时间筛选变化时,获取数据 + useEffect(() => { + if (!visible || !ruleId) return; + + setLoading(true); + const params: { startTime?: string; endTime?: string } = {}; + if (startTime) { + params.startTime = formatDate(startTime); + } + if (endTime) { + params.endTime = formatDate(endTime); + } + + getFriendRequestTaskStats(ruleId.toString(), params) + .then(res => { + console.log(res); + setXData(res.dateArray); + setYData([ + res.allNumArray, + res.errorNumArray, + res.passNumArray, + res.passRateArray, + res.successNumArray, + res.successRateArray, + ]); + setStatistics(res.totalStats); + }) + .catch(error => { + console.error("获取统计数据失败:", error); + message.error("获取统计数据失败"); + }) + .finally(() => { + setLoading(false); + }); + }, [visible, ruleId, startTime, endTime, formatDate]); const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据"; return ( @@ -89,6 +131,55 @@ const PoolListModal: React.FC = ({ />
+ {/* 时间筛选 */} +
+
+ + setShowStartTimePicker(true)} + prefix={} + className={style.dateFilterInput} + /> + setShowStartTimePicker(false)} + onConfirm={val => { + setStartTime(val); + setShowStartTimePicker(false); + }} + /> +
+
+ + setShowEndTimePicker(true)} + prefix={} + className={style.dateFilterInput} + /> + setShowEndTimePicker(false)} + onConfirm={val => { + setEndTime(val); + setShowEndTimePicker(false); + }} + /> +
+
+ {/* 统计数据表格 */}
{loading ? ( diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss index cbfce7c1..dd77f7b7 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss @@ -663,6 +663,32 @@ color: #666; } +// 日期筛选样式 +.dateFilter { + display: flex; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; + background: #fff; +} + +.dateFilterItem { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.dateFilterLabel { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.dateFilterInput { + width: 100%; +} + // 统计数据弹窗样式 .statisticsContent { flex: 1; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx index ac63a6b6..c09cfbf2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -90,7 +90,7 @@ export default function NewPlan() { setFormData(prev => ({ ...prev, name: detail.name ?? "", - scenario: Number(detail.scenario) || 1, + scenario: Number(detail.sceneId || detail.scenario) || 1, scenarioTags: detail.scenarioTags ?? [], customTags: detail.customTags ?? [], customTagsOptions: detail.customTags ?? [], @@ -102,7 +102,7 @@ export default function NewPlan() { startTime: detail.startTime ?? "09:00", endTime: detail.endTime ?? "18:00", enabled: detail.enabled ?? true, - sceneId: Number(detail.scenario) || 1, + sceneId: Number(detail.sceneId || detail.scenario) || 1, remarkFormat: detail.remarkFormat ?? "", addFriendInterval: detail.addFriendInterval ?? 1, tips: detail.tips ?? "", diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index f8136303..aa3630c2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -40,6 +40,7 @@ const generatePosterMaterials = (): Material[] => { }; const BasicSettings: React.FC = ({ + isEdit, formData, onChange, sceneList, @@ -249,16 +250,27 @@ const BasicSettings: React.FC = ({ ) : (
{sceneList - .filter(scene => scene.id !== 10) + .filter(scene => { + // 编辑模式下,如果当前选中的场景 id 是 10,则显示它 + if (isEdit && formData.scenario === 10 && scene.id === 10) { + return true; + } + // 其他情况过滤掉 id 为 10 的场景 + return scene.id !== 10; + }) .map(scene => { const selected = formData.scenario === scene.id; + // 编辑模式下,如果当前场景 id 是 10,则禁用所有场景选择 + const isDisabled = isEdit && formData.scenario === 10; return ( + )} +
+ + {/* 已选群列表 */} + {selectedGroups.length > 0 && ( +
+ {selectedGroups.map(group => ( +
+ {/* 群信息 */} +
+
+ +
+
{group.name}
+
ID: {group.chatroomId || group.id}
+
+
+ {!readonly && ( + + )} +
+ + {/* 成员选择区域 */} +
+
+ 采集群内指定成员 ({group.members?.length || 0}人) +
+
+ {group.members?.map(member => ( +
+ +
{member.nickname}
+ {!readonly && ( + + )} +
+ ))} + {!readonly && ( +
handleOpenMemberSelection(group.id)} + > + + 添加 +
+ )} +
+
+
+ ))} +
+ )} + + {/* 群选择弹窗 */} + + + {/* 成员选择弹窗 */} + +
+
+
选择成员
+ +
+
+ {loadingMembers ? ( +
+
加载中...
+
+ ) : currentGroupMembers.length > 0 ? ( + currentGroupMembers.map(member => { + const isSelected = currentSelectedMembers.some(m => m.id === member.id); + return ( +
handleSelectMember(memberSelectionVisible.groupId, member)} + > + +
{member.nickname}
+ {isSelected &&
} +
+ ); + }) + ) : ( +
+
暂无成员数据
+
+ )} +
+
+
+
+ ); +}; + +export default GroupSelectionWithMembers; diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index f758cee4..b52229bb 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -6,11 +6,13 @@ import { DownOutlined } from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; +import GroupSelectionWithMembers from "@/components/GroupSelectionWithMembers"; import DeviceSelection from "@/components/DeviceSelection"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; +import { GroupWithMembers } from "@/components/GroupSelectionWithMembers"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -48,6 +50,9 @@ export default function ContentForm() { const [selectedGroupsOptions, setSelectedGroupsOptions] = useState< GroupSelectionItem[] >([]); + const [selectedGroupsWithMembers, setSelectedGroupsWithMembers] = useState< + GroupWithMembers[] + >([]); const [useAI, setUseAI] = useState(false); const [aiPrompt, setAIPrompt] = useState("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除"); const [enabled, setEnabled] = useState(true); @@ -94,7 +99,30 @@ export default function ContentForm() { setSelectedDevices(deviceOptions || []); setSelectedFriends(data.sourceFriends || []); setSelectedGroups(data.selectedGroups || []); - setSelectedGroupsOptions(data.selectedGroupsOptions || []); + // 使用 wechatGroupsOptions 作为群列表数据 + setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []); + // 处理带成员的群数据 + // groupMembersOptions 是一个对象,key是群ID(字符串),value是成员数组 + const groupMembersMap = data.groupMembersOptions || {}; + const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map( + (group: any) => { + const groupIdStr = String(group.id); + const members = groupMembersMap[groupIdStr] || []; + // 映射成员数据结构 + return { + ...group, + members: members.map((member: any) => ({ + id: String(member.id), + nickname: member.nickname || "", + wechatId: member.wechatId || "", + avatar: member.avatar || "", + gender: undefined, + role: undefined, + })), + }; + }, + ); + setSelectedGroupsWithMembers(groupsWithMembers); setSelectedFriendsOptions(data.friendsGroupsOptions || []); setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); @@ -140,7 +168,15 @@ export default function ContentForm() { devices: selectedDevices.map(d => d.id), friendsGroups: friendsGroups, wechatGroups: selectedGroups, - groupMembers: {}, + groupMembers: selectedGroupsWithMembers.reduce( + (acc, group) => { + if (group.members && group.members.length > 0) { + acc[group.id] = group.members.map(m => m.id); + } + return acc; + }, + {} as Record, + ), keywordInclude: keywordsInclude .split(/,|,|\n|\s+/) .map(s => s.trim()) @@ -180,6 +216,12 @@ export default function ContentForm() { setSelectedGroupsOptions(groups); }; + const handleGroupsWithMembersChange = (groups: GroupWithMembers[]) => { + setSelectedGroupsWithMembers(groups); + setSelectedGroups(groups.map(g => g.id.toString())); + setSelectedGroupsOptions(groups); + }; + const handleFriendsChange = (friends: FriendSelectionItem[]) => { setSelectedFriends(friends.map(f => f.id.toString())); setSelectedFriendsOptions(friends); @@ -335,9 +377,9 @@ export default function ContentForm() { /> - diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts index 3943a92a..b1f5c5f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts @@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data"; export function getPlanList(params: { sceneId: string; page: number; - pageSize: number; + limit: number; }): Promise { return request(`/v1/plan/list`, params, "GET"); } diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 367187e9..e658e26f 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -8,8 +8,9 @@ import { Popup, Card, Tag, + InfiniteScroll, } from "antd-mobile"; -import { Input, Pagination } from "antd"; +import { Input } from "antd"; import { PlusOutlined, CopyOutlined, @@ -80,7 +81,7 @@ const ScenarioList: React.FC = () => { const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [total, setTotal] = useState(0); - const pageSize = 20; + const limit = 20; // 获取计划列表数据 const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { @@ -96,7 +97,7 @@ const ScenarioList: React.FC = () => { const response = await getPlanList({ sceneId: scenarioId, page: page, - pageSize: pageSize, + limit: limit, }); if (response && response.list) { @@ -110,7 +111,7 @@ const ScenarioList: React.FC = () => { // 更新分页信息 setTotal(response.total || 0); - setHasMore(response.list.length === pageSize); + setHasMore(response.list.length === limit); setCurrentPage(page); } } catch (error) { @@ -149,10 +150,11 @@ const ScenarioList: React.FC = () => { fetchScenarioData(); }, [scenarioId]); - // 分页改变处理 - const handlePageChange = async (page: number) => { - setCurrentPage(page); - await fetchPlanList(page, false); + // 加载更多 + const handleLoadMore = async () => { + if (!hasMore || loadingMore || loadingTasks) return; + const nextPage = currentPage + 1; + await fetchPlanList(nextPage, true); }; const handleCopyPlan = async (taskId: string) => { @@ -405,18 +407,6 @@ const ScenarioList: React.FC = () => { } loading={loading} - footer={ -
- -
- } >
{/* 计划列表 */} @@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
))} + {/* 上拉加载更多 */} + + {loadingMore && ( +
+ + + 加载中... + +
+ )} + {!hasMore && filteredTasks.length > 0 && ( +
+ 没有更多了 +
+ )} +
)}
diff --git a/Server/application/chukebao/config/route.php b/Server/application/chukebao/config/route.php index b22a01b7..7143a686 100644 --- a/Server/application/chukebao/config/route.php +++ b/Server/application/chukebao/config/route.php @@ -21,6 +21,7 @@ Route::group('v1/', function () { Route::group('wechatChatroom/', function () { Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表 Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情 + Route::get('members', 'app\chukebao\controller\WechatChatroomController@getMembers'); // 获取群成员列表 Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告 }); diff --git a/Server/application/chukebao/controller/WechatChatroomController.php b/Server/application/chukebao/controller/WechatChatroomController.php index 5d521143..936c837d 100644 --- a/Server/application/chukebao/controller/WechatChatroomController.php +++ b/Server/application/chukebao/controller/WechatChatroomController.php @@ -151,6 +151,68 @@ class WechatChatroomController extends BaseController return ResponseHelper::success($detail); } + public function getMembers() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $groupId = $this->request->param('groupId', ''); + $keyword = $this->request->param('keyword', ''); + + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + // 验证群组ID必填 + if (empty($groupId)) { + return ResponseHelper::error('群组ID不能为空'); + } + + // 验证群组是否属于当前账号 + $chatroom = Db::table('s2_wechat_chatroom') + ->where(['id' => $groupId, 'isDeleted' => 0]) + ->find(); + + if (!$chatroom) { + return ResponseHelper::error('群组不存在或无权限访问'); + } + + // 获取群组的chatroomId(微信群聊ID) + $chatroomId = $chatroom['chatroomId'] ?? $chatroom['id']; + + // 如果chatroomId为空,使用id作为chatroomId + if (empty($chatroomId)) { + $chatroomId = $chatroom['id']; + } + + // 构建查询 + $query = Db::table('s2_wechat_chatroom_member') + ->where('chatroomId', $chatroomId); + + // 关键字搜索:昵称、备注、别名 + if ($keyword !== '' && $keyword !== null) { + $query->where(function ($q) use ($keyword) { + $like = '%' . $keyword . '%'; + $q->whereLike('nickname', $like) + ->whereOr('conRemark', 'like', $like) + ->whereOr('alias', 'like', $like); + }); + } + + $query->order('id desc'); + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + // 处理时间格式 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + } + unset($v); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + public function aiAnnouncement() { $userId = $this->getUserInfo('id'); diff --git a/Server/application/command.php b/Server/application/command.php index 061fb69b..0fa99a64 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -19,7 +19,7 @@ return [ 'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √ 'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √ 'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √ - 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 √ + 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 XXXXXXXX 'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表 // 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友 // 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 690d8a64..94770ac5 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -357,6 +357,7 @@ class ContentLibraryController extends Controller // 初始化选项数组 $library['friendsGroupsOptions'] = []; $library['wechatGroupsOptions'] = []; + $library['groupMembersOptions'] = []; // 批量查询好友信息 if (!empty($library['friendsGroups'])) { @@ -385,6 +386,61 @@ class ContentLibraryController extends Controller } } + // 批量查询群成员信息 + if (!empty($library['groupMembers'])) { + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是成员ID数组 + $allMemberIds = []; + $groupMembersMap = []; + + if (is_array($library['groupMembers'])) { + foreach ($library['groupMembers'] as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $allMemberIds = array_merge($allMemberIds, $memberIds); + // 保存群组ID和成员ID的映射关系 + $groupMembersMap[$groupId] = $memberIds; + } + } + } + + if (!empty($allMemberIds)) { + // 去重 + $allMemberIds = array_unique($allMemberIds); + + // 查询群成员信息 + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime') + ->whereIn('id', $allMemberIds) + ->select(); + + // 将成员数据按ID建立索引 + $membersById = []; + foreach ($members as $member) { + // 格式化时间字段 + $member['createTime'] = !empty($member['createTime']) ? date('Y-m-d H:i:s', $member['createTime']) : ''; + $member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : ''; + $membersById[$member['id']] = $member; + } + + // 按照群组ID分组返回 + $groupMembersOptions = []; + foreach ($groupMembersMap as $groupId => $memberIds) { + $groupMembersOptions[$groupId] = []; + foreach ($memberIds as $memberId) { + if (isset($membersById[$memberId])) { + $groupMembersOptions[$groupId][] = $membersById[$memberId]; + } + } + } + + $library['groupMembersOptions'] = $groupMembersOptions; + } else { + $library['groupMembersOptions'] = []; + } + } else { + $library['groupMembersOptions'] = []; + } + //获取设备信息 if (!empty($library['deviceGroups'])) { $deviceList = DeviceModel::alias('d') @@ -921,7 +977,7 @@ class ContentLibraryController extends Controller // 如果有发送者信息,也获取发送者详情 if (!empty($item['wechatId'])) { - $senderInfo = Db::name('wechat_chatroom_member') + $senderInfo = Db::table('s2_wechat_chatroom_member') ->where([ 'chatroomId' => $groupInfo['chatroomId'], 'wechatId' => $item['wechatId'] @@ -1477,8 +1533,8 @@ class ContentLibraryController extends Controller try { // 查询群组信息 - $groups = Db::name('wechat_group')->alias('g') - ->field('g.id, g.chatroomId, g.name, g.ownerWechatId') + $groups = Db::table('s2_wechat_chatroom')->alias('g') + ->field('g.id, g.chatroomId, g.nickname as name, g.wechatAccountWechatId as ownerWechatId') ->whereIn('g.id', $groupIds) ->where('g.deleteTime', 0) ->select(); @@ -1500,12 +1556,59 @@ class ContentLibraryController extends Controller ]; } + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是该群组的成员ID数组 + // 需要按群组分组处理,确保每个群组只采集该群组配置的成员 + + // 建立群组ID到成员ID数组的映射 + $groupIdToMemberIds = []; + if (is_array($groupMembers)) { + foreach ($groupMembers as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $groupIdToMemberIds[$groupId] = $memberIds; + } + } + } + if (empty($groupIdToMemberIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员ID' + ]; + } + + // 为每个群组查询成员信息,建立群组ID到成员wechatId数组的映射 + $groupIdToMemberWechatIds = []; + foreach ($groupIdToMemberIds as $groupId => $memberIds) { + // 查询该群组的成员信息,获取wechatId + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, wechatId') + ->whereIn('id', $memberIds) + ->select(); + + $wechatIds = []; + foreach ($members as $member) { + if (!empty($member['wechatId'])) { + $wechatIds[] = $member['wechatId']; + } + } + + if (!empty($wechatIds)) { + $groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds); + } + } + if (empty($groupIdToMemberWechatIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员微信ID' + ]; + } + // 从群组采集内容 $collectedData = []; $totalMessagesCount = 0; $chatroomIds = array_column($groups, 'id'); - // 获取群消息 - 支持时间范围过滤 + // 获取群消息 - 支持时间范围过滤(先不添加群成员过滤,后面按群组分别过滤) $messageWhere = [ ['wechatChatroomId', 'in', $chatroomIds], ['type', '=', 2] @@ -1516,7 +1619,7 @@ class ContentLibraryController extends Controller $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]]; } - // 查询群消息 + // 查询群消息(先查询所有消息,后面按群组和成员过滤) $groupMessages = Db::table('s2_wechat_message') ->where($messageWhere) ->order('createTime', 'desc') @@ -1532,6 +1635,34 @@ class ContentLibraryController extends Controller $groupedMessages = []; foreach ($groupMessages as $message) { $chatroomId = $message['wechatChatroomId']; + $senderWechatId = $message['senderWechatId'] ?? ''; + + // 找到对应的群组信息 + $groupInfo = null; + foreach ($groups as $group) { + if ($group['id'] == $chatroomId) { + $groupInfo = $group; + break; + } + } + + if (!$groupInfo) { + continue; + } + + // 检查该消息的发送者是否在该群组的配置成员列表中 + $groupId = $groupInfo['id']; + if (!isset($groupIdToMemberWechatIds[$groupId])) { + // 该群组没有配置成员,跳过 + continue; + } + + // 检查发送者是否在配置的成员列表中 + if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) { + // 发送者不在该群组的配置成员列表中,跳过 + continue; + } + if (!isset($groupedMessages[$chatroomId])) { $groupedMessages[$chatroomId] = [ 'count' => 0, @@ -1579,27 +1710,14 @@ class ContentLibraryController extends Controller continue; } - // 找到对应的群组信息 - $groupInfo = null; - foreach ($groups as $group) { - if ($group['id'] == $chatroomId) { - $groupInfo = $group; - break; - } - } - - if (!$groupInfo) { - continue; - } - // 如果启用了AI处理 if (!empty($library['aiEnabled']) && !empty($content)) { $contentAi = $this->aiRewrite($library, $content); - if (!empty($content)) { - $moment['contentAi'] = $contentAi; + if (!empty($contentAi)) { + $message['contentAi'] = $contentAi; } else { - $moment['contentAi'] = ''; + $message['contentAi'] = ''; } } @@ -1968,7 +2086,38 @@ class ContentLibraryController extends Controller return true; } - // 提取消息内容中的链接 + $resUrls = []; + + $content = ''; + switch ($message['msgType']) { + case 1: // 文字 + $content = $message['content']; + $contentType = 4; + break; + case 3: //图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 47: //动态图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 34: //语言 + return false; + case 43: //视频 + $resUrls[] = $message['content']; + $contentType = 3; + break; + case 42: //名片 + return false; + case 49: //文件 + $links = json_decode($message['content'],true); + return false; + default: + return false; + } + + /*// 提取消息内容中的链接 $content = $message['content'] ?? ''; $links = []; $pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; @@ -1986,6 +2135,8 @@ class ContentLibraryController extends Controller // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) $contentType = $this->determineContentType($content, $resUrls, $links); + */ + // 创建新的内容项目 $item = new ContentItem(); @@ -1993,7 +2144,7 @@ class ContentLibraryController extends Controller $item->type = 'group_message'; // 群消息类型 $item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息'; $item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE); - $item->msgId = $message['msgId'] ?? ''; // 存储msgId便于后续查询 + $item->msgId = $message['msgSvrId'] ?? ''; // 存储msgSvrId便于后续查询 $item->createTime = time(); $item->content = $content; $item->contentType = $contentType; // 设置内容类型 @@ -2011,13 +2162,17 @@ class ContentLibraryController extends Controller if (!empty($resUrls[0])) { $item->coverImage = $resUrls[0]; } + }else{ + $item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE); } // 处理链接 if (!empty($links)) { $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); + }else{ + $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); } - + $item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE); // 设置商品信息(需根据消息内容解析) $this->extractProductInfo($item, $content); diff --git a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php index 8499c6e6..d9b11a07 100644 --- a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php +++ b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php @@ -39,10 +39,10 @@ class GetChatroomListV1Controller extends BaseController $where = []; if ($this->getUserInfo('isAdmin') == 1) { - $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['gg.isDeleted', '=', 0]; $where[] = ['g.ownerWechatId', 'in', $wechatIds]; } else { - $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['gg.isDeleted', '=', 0]; $where[] = ['g.ownerWechatId', 'in', $wechatIds]; //$where[] = ['g.userId', '=', $this->getUserInfo('id')]; } @@ -55,6 +55,7 @@ class GetChatroomListV1Controller extends BaseController ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime', 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias']) ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') + ->join(['s2_wechat_chatroom' => 'gg'], 'g.id = gg.id', 'LEFT') ->where($where); $total = $data->count(); From 879396e91c16ea08ee9394650506ca62778aa69c Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 26 Dec 2025 10:42:33 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/mobile/scenarios/plan/new/index.data.ts | 1 + Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx | 9 +++++++++ .../mobile/scenarios/plan/new/steps/BasicSettings.tsx | 9 +++++++++ .../pages/mobile/workspace/auto-like/record/index.tsx | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts index 01fec441..a6e5bade 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts @@ -60,6 +60,7 @@ export const defFormData: FormData = { enabled: true, remarkFormat: "", addFriendInterval: 1, + tips: "请注意消息,稍后加你微信", posters: [], device: [], customTags: [], diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx index c09cfbf2..87b2dfab 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -122,10 +122,19 @@ export default function NewPlan() { distributionAddReward: detail.addFriendRewardAmount, })); } else { + // 新建时,如果是海报场景,设置默认获客成功提示 + const defaultTips = "请注意消息,稍后加你微信"; if (scenarioId) { setFormData(prev => ({ ...prev, ...{ scenario: Number(scenarioId) || 1 }, + tips: Number(scenarioId) === 1 ? defaultTips : prev.tips || "", + })); + } else { + // 如果没有 scenarioId,默认是海报场景(scenario === 1),设置默认提示 + setFormData(prev => ({ + ...prev, + tips: prev.scenario === 1 ? defaultTips : prev.tips || "", })); } } diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index aa3630c2..07137f67 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -101,6 +101,15 @@ const BasicSettings: React.FC = ({ setTips(formData.tips || ""); }, [formData.tips]); + // 当切换到海报场景且 tips 为空时,设置默认值 + useEffect(() => { + if (formData.scenario === 1 && !formData.tips) { + const defaultTips = "请注意消息,稍后加你微信"; + onChange({ ...formData, tips: defaultTips }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData.scenario]); + // 选中场景 const handleScenarioSelect = (sceneId: number) => { diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx index 1915fa48..579d5e9a 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/record/index.tsx @@ -230,7 +230,7 @@ export default function AutoLikeRecord() {
Date: Mon, 29 Dec 2025 10:59:32 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E9=87=91=E9=A2=9D=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/distribution/ChannelUserController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Server/application/cunkebao/controller/distribution/ChannelUserController.php b/Server/application/cunkebao/controller/distribution/ChannelUserController.php index ee6dc67f..4c27dc0f 100644 --- a/Server/application/cunkebao/controller/distribution/ChannelUserController.php +++ b/Server/application/cunkebao/controller/distribution/ChannelUserController.php @@ -235,7 +235,7 @@ class ChannelUserController extends Controller // 2. 财务统计 // 当前可提现金额 - $withdrawableAmount = $channel['withdrawableAmount'] ?? 0; + $withdrawableAmount = round(($channel['withdrawableAmount'] ?? 0) / 100, 2); // 分转元 // 已提现金额(已打款的提现申请) $withdrawnAmount = Db::name('distribution_withdrawal') @@ -245,7 +245,7 @@ class ChannelUserController extends Controller ['status', '=', DistributionWithdrawal::STATUS_PAID] ]) ->sum('amount'); - $withdrawnAmount = $withdrawnAmount ?? 0; + $withdrawnAmount = round(($withdrawnAmount ?? 0) / 100, 2); // 分转元 // 待审核金额(待审核的提现申请) $pendingReviewAmount = Db::name('distribution_withdrawal') @@ -255,7 +255,7 @@ class ChannelUserController extends Controller ['status', '=', DistributionWithdrawal::STATUS_PENDING] ]) ->sum('amount'); - $pendingReviewAmount = $pendingReviewAmount ?? 0; + $pendingReviewAmount = round(($pendingReviewAmount ?? 0) / 100, 2); // 分转元 // 总收益(所有收益记录的总和) $totalRevenue = Db::name('distribution_revenue_record') @@ -264,7 +264,7 @@ class ChannelUserController extends Controller ['channelId', '=', $channelId] ]) ->sum('amount'); - $totalRevenue = $totalRevenue ?? 0; + $totalRevenue = round(($totalRevenue ?? 0) / 100, 2); // 分转元 $financialStats = [ 'withdrawableAmount' => $withdrawableAmount, // 当前可提现金额 From 2c90f6f33ce08c8c61fd99910cc7d3d51254ce0b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 15:01:44 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=8E=B7=E5=AE=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=90=E8=B4=A6=E5=8F=B7=E5=8F=AA=E8=83=BD?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E8=87=AA=E5=B7=B1=E7=9A=84=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/GetPlanSceneListV1Controller.php | 16 ++++++++++++---- .../controller/plan/PlanSceneV1Controller.php | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php b/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php index cc8c6fbe..887bfee2 100644 --- a/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php +++ b/Server/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php @@ -34,7 +34,6 @@ class GetPlanSceneListV1Controller extends BaseController $where[] = ['scenarioTags', 'like', '%' . $params['tag'] . '%']; } - // 查询数据 $query = PlansSceneModel::where($where); @@ -193,10 +192,19 @@ class GetPlanSceneListV1Controller extends BaseController return []; } + + $where = [ + ['companyId', '=', $companyId], + ['deleteTime', '=', 0], + ['sceneId', 'in', $sceneIds], + ]; + if(!$this->getUserInfo('isAdmin')){ + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + + $rows = Db::name('customer_acquisition_task') - ->whereIn('sceneId', $sceneIds) - ->where('companyId', $companyId) - ->where('deleteTime', 0) + ->where($where) ->field('sceneId, COUNT(*) as total') ->group('sceneId') ->select(); diff --git a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php index cbba59fa..3812e565 100644 --- a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php @@ -30,7 +30,7 @@ class PlanSceneV1Controller extends BaseController 'companyId' => $this->getUserInfo('companyId'), ]; - if($this->getUserInfo('isAdmin')){ + if(!$this->getUserInfo('isAdmin')){ $where['userId'] = $this->getUserInfo('id'); } @@ -176,7 +176,7 @@ class PlanSceneV1Controller extends BaseController } // 如果是管理员,需要验证用户权限 - if ($this->getUserInfo('isAdmin')) { + if (!$this->getUserInfo('isAdmin')) { $userPlan = Db::name('customer_acquisition_task') ->where([ 'id' => $planId, From 203e8c8eaf9edc32b574fc3e5994d4fed66415de Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 15:12:57 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E6=B8=A0=E9=81=93=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../distribution/ChannelController.php | 47 +++++++++++++++++-- .../distribution/WithdrawalController.php | 28 +++++++++-- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/Server/application/cunkebao/controller/distribution/ChannelController.php b/Server/application/cunkebao/controller/distribution/ChannelController.php index ab8fa447..f9691343 100644 --- a/Server/application/cunkebao/controller/distribution/ChannelController.php +++ b/Server/application/cunkebao/controller/distribution/ChannelController.php @@ -35,6 +35,7 @@ class ChannelController extends BaseController $createType = $this->request->param('createType', DistributionChannel::CREATE_TYPE_MANUAL); // 默认为手动创建 $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); // 参数验证 if (empty($name)) { @@ -87,6 +88,7 @@ class ChannelController extends BaseController // 准备插入数据 $data = [ 'companyId' => $companyId, + 'userId' => $userId, 'name' => $name, 'code' => $code, 'phone' => $phone ?: '', @@ -122,6 +124,7 @@ class ChannelController extends BaseController 'phone' => $channel['phone'] ?: '', 'wechatId' => $channel['wechatId'] ?: '', 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转 + 'userId' => (int)($channel['userId'] ?? 0), 'createType' => $channel['createType'], 'status' => $channel['status'], 'totalCustomers' => (int)$channel['totalCustomers'], @@ -175,6 +178,11 @@ class ChannelController extends BaseController $where[] = ['companyId', '=', $companyId]; $where[] = ['deleteTime', '=', 0]; + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + // 状态筛选 if ($status !== 'all') { $where[] = ['status', '=', $status]; @@ -208,6 +216,8 @@ class ChannelController extends BaseController 'code' => $item['code'] ?? '', 'phone' => !empty($item['phone']) ? $item['phone'] : null, 'wechatId' => !empty($item['wechatId']) ? $item['wechatId'] : null, + 'companyId' => (int)($item['companyId'] ?? 0), + 'userId' => (int)($item['userId'] ?? 0), 'createType' => $item['createType'] ?? 'manual', 'status' => $item['status'] ?? 'enabled', 'totalCustomers' => (int)($item['totalCustomers'] ?? 0), @@ -394,6 +404,8 @@ class ChannelController extends BaseController 'code' => $updatedChannel['code'], 'phone' => !empty($updatedChannel['phone']) ? $updatedChannel['phone'] : null, 'wechatId' => !empty($updatedChannel['wechatId']) ? $updatedChannel['wechatId'] : null, + 'companyId' => (int)($updatedChannel['companyId'] ?? 0), + 'userId' => (int)($updatedChannel['userId'] ?? 0), 'createType' => $updatedChannel['createType'], 'status' => $updatedChannel['status'], 'totalCustomers' => (int)$updatedChannel['totalCustomers'], @@ -606,6 +618,11 @@ class ChannelController extends BaseController ['deleteTime', '=', 0] ]; + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $baseWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + // 1. 总渠道数 $totalChannels = Db::name('distribution_channel') ->where($baseWhere) @@ -667,6 +684,11 @@ class ChannelController extends BaseController ['companyId', '=', $companyId] ]; + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $baseWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + // 1. 总支出:所有已打款的提现申请金额总和(状态为paid) $totalExpenditure = Db::name('distribution_withdrawal') ->where($baseWhere) @@ -731,6 +753,11 @@ class ChannelController extends BaseController $where[] = ['companyId', '=', $companyId]; $where[] = ['deleteTime', '=', 0]; + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + // 关键词搜索(模糊匹配 name、code) if (!empty($keyword)) { $keyword = trim($keyword); @@ -753,12 +780,20 @@ class ChannelController extends BaseController $channelIds = array_column($channels, 'id'); $withdrawalStats = []; if (!empty($channelIds)) { + // 构建提现查询条件 + $withdrawalWhere = [ + ['companyId', '=', $companyId], + ['channelId', 'in', $channelIds] + ]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $withdrawalWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + // 按渠道ID和状态分组统计提现金额 $stats = Db::name('distribution_withdrawal') - ->where([ - ['companyId', '=', $companyId], - ['channelId', 'in', $channelIds] - ]) + ->where($withdrawalWhere) ->field([ 'channelId', 'status', @@ -1310,9 +1345,10 @@ class ChannelController extends BaseController // 生成渠道编码 $code = DistributionChannel::generateChannelCode(); - // 准备插入数据 + // 准备插入数据(扫码注册时 userId 为 0,因为是通过二维码注册,没有登录用户) $data = [ 'companyId' => $companyId, + 'userId' => 0, // 扫码注册时没有登录用户,userId 为 0 'name' => $name, 'code' => $code, 'phone' => $phone ?: '', @@ -1353,6 +1389,7 @@ class ChannelController extends BaseController 'phone' => $channel['phone'] ?: '', 'wechatId' => $channel['wechatId'] ?: '', 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转 + 'userId' => (int)($channel['userId'] ?? 0), 'createType' => $channel['createType'], 'status' => $channel['status'], 'totalCustomers' => (int)$channel['totalCustomers'], diff --git a/Server/application/cunkebao/controller/distribution/WithdrawalController.php b/Server/application/cunkebao/controller/distribution/WithdrawalController.php index 412e5574..37fd76eb 100644 --- a/Server/application/cunkebao/controller/distribution/WithdrawalController.php +++ b/Server/application/cunkebao/controller/distribution/WithdrawalController.php @@ -43,6 +43,11 @@ class WithdrawalController extends BaseController $where = []; $where[] = ['w.companyId', '=', $companyId]; + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['w.userId', '=', $this->getUserInfo('id')]; + } + // 状态筛选 if ($status !== 'all') { $where[] = ['w.status', '=', $status]; @@ -89,6 +94,7 @@ class WithdrawalController extends BaseController $list = $query->field([ 'w.id', 'w.channelId', + 'w.userId', 'w.amount', 'w.status', 'w.payType', @@ -123,6 +129,7 @@ class WithdrawalController extends BaseController 'channelId' => (string)$item['channelId'], 'channelName' => $item['channelName'] ?? '', 'channelCode' => $item['channelCode'] ?? '', + 'userId' => (int)($item['userId'] ?? 0), 'amount' => round($item['amount'] / 100, 2), // 分转元,保留2位小数 'status' => $item['status'] ?? DistributionWithdrawal::STATUS_PENDING, 'payType' => !empty($item['payType']) ? $item['payType'] : null, // 支付类型 @@ -168,6 +175,7 @@ class WithdrawalController extends BaseController $amount = $this->request->param('amount', 0); // 金额单位:元 $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); // 参数验证 if (empty($channelCode)) { @@ -271,6 +279,7 @@ class WithdrawalController extends BaseController $withdrawalData = [ 'companyId' => $companyId, 'channelId' => $channelId, + 'userId' => $userId, 'amount' => $amountInFen, // 存储为分 'status' => DistributionWithdrawal::STATUS_PENDING, 'applyTime' => time(), @@ -315,6 +324,7 @@ class WithdrawalController extends BaseController 'channelId' => (string)$withdrawal['channelId'], 'channelName' => $withdrawal['channelName'] ?? '', 'channelCode' => $withdrawal['channelCode'] ?? '', + 'userId' => (int)($withdrawal['userId'] ?? 0), 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数 'status' => $withdrawal['status'], 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard(创建时为null) @@ -603,17 +613,26 @@ class WithdrawalController extends BaseController ]); } + // 构建查询条件 + $where = [ + ['w.id', '=', $id], + ['w.companyId', '=', $companyId] + ]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['w.userId', '=', $this->getUserInfo('id')]; + } + // 查询申请详情(关联渠道表) $withdrawal = Db::name('distribution_withdrawal') ->alias('w') ->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left') - ->where([ - ['w.id', '=', $id], - ['w.companyId', '=', $companyId] - ]) + ->where($where) ->field([ 'w.id', 'w.channelId', + 'w.userId', 'w.amount', 'w.status', 'w.payType', @@ -641,6 +660,7 @@ class WithdrawalController extends BaseController 'channelId' => (string)$withdrawal['channelId'], 'channelName' => $withdrawal['channelName'] ?? '', 'channelCode' => $withdrawal['channelCode'] ?? '', + 'userId' => (int)($withdrawal['userId'] ?? 0), 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数 'status' => $withdrawal['status'], 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard From b609eeb0eb3050f9fa8d31f37766f0eda30f9151 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 15:18:39 +0800 Subject: [PATCH 09/17] 1 --- .../controller/device/GetDeviceListV1Controller.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Server/application/cunkebao/controller/device/GetDeviceListV1Controller.php b/Server/application/cunkebao/controller/device/GetDeviceListV1Controller.php index cef4d96f..f55310f6 100644 --- a/Server/application/cunkebao/controller/device/GetDeviceListV1Controller.php +++ b/Server/application/cunkebao/controller/device/GetDeviceListV1Controller.php @@ -167,10 +167,9 @@ class GetDeviceListV1Controller extends BaseController if ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) { $where = $this->makeWhere(); $result = $this->getDeviceList($where); - } - - else { - $where = $this->makeWhere( $this->makeDeviceIdsWhere() ); + }else { + //$where = $this->makeWhere( $this->makeDeviceIdsWhere() ); + $where = $this->makeWhere(); $result = $this->getDeviceList($where); } From d9ee7e940216b7b834a8e915ffde442efb9cb243 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 15:25:04 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E5=88=86=E9=94=80=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/distribution/ChannelController.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Server/application/cunkebao/controller/distribution/ChannelController.php b/Server/application/cunkebao/controller/distribution/ChannelController.php index f9691343..d6b4dd22 100644 --- a/Server/application/cunkebao/controller/distribution/ChannelController.php +++ b/Server/application/cunkebao/controller/distribution/ChannelController.php @@ -879,6 +879,7 @@ class ChannelController extends BaseController $type = $this->request->param('type', 'h5'); // h5 或 miniprogram $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); // 参数验证 if (!in_array($type, ['h5', 'miniprogram'])) { @@ -890,10 +891,11 @@ class ChannelController extends BaseController ]); } - // 生成临时token(只包含公司ID,有效期24小时) + // 生成临时token(包含公司ID和用户ID,有效期24小时) // 用户扫码后需要自己填写所有信息 $tokenData = [ 'companyId' => $companyId, + 'userId' => $userId, 'expireTime' => time() + 86400 // 24小时后过期 ]; $token = base64_encode(json_encode($tokenData)); @@ -1253,6 +1255,7 @@ class ChannelController extends BaseController } $companyId = $tokenData['companyId']; + $userId = isset($tokenData['userId']) ? $tokenData['userId'] : 0; // 兼容旧token,如果没有userId则默认为0 // GET请求:返回token验证成功信息(前端可以显示表单) if ($this->request->isGet()) { @@ -1345,10 +1348,10 @@ class ChannelController extends BaseController // 生成渠道编码 $code = DistributionChannel::generateChannelCode(); - // 准备插入数据(扫码注册时 userId 为 0,因为是通过二维码注册,没有登录用户) + // 准备插入数据(从token中获取userId,记录是哪个用户生成的二维码) $data = [ 'companyId' => $companyId, - 'userId' => 0, // 扫码注册时没有登录用户,userId 为 0 + 'userId' => $userId, // 从token中获取userId,记录生成二维码的用户 'name' => $name, 'code' => $code, 'phone' => $phone ?: '', From 82f9b9371ae8730db0f91514eab4d64dd14e0748 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 16:02:24 +0800 Subject: [PATCH 11/17] =?UTF-8?q?=E6=8F=90=E7=8E=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/distribution/WithdrawalController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Server/application/cunkebao/controller/distribution/WithdrawalController.php b/Server/application/cunkebao/controller/distribution/WithdrawalController.php index 37fd76eb..4cdf7fb9 100644 --- a/Server/application/cunkebao/controller/distribution/WithdrawalController.php +++ b/Server/application/cunkebao/controller/distribution/WithdrawalController.php @@ -175,7 +175,6 @@ class WithdrawalController extends BaseController $amount = $this->request->param('amount', 0); // 金额单位:元 $companyId = $this->getUserInfo('companyId'); - $userId = $this->getUserInfo('id'); // 参数验证 if (empty($channelCode)) { @@ -228,6 +227,8 @@ class WithdrawalController extends BaseController // 统一使用渠道ID变量,后续逻辑仍然基于 channelId $channelId = $channel['id']; + // 从渠道获取创建者的userId,而不是当前登录用户的userId + $userId = intval($channel['userId'] ?? 0); // 检查渠道状态 if ($channel['status'] !== 'enabled') { @@ -309,6 +310,7 @@ class WithdrawalController extends BaseController ->field([ 'w.id', 'w.channelId', + 'w.userId', 'w.amount', 'w.status', 'w.payType', From db0985504a683d56ffa748ff504428be6ff13f78 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 16:09:22 +0800 Subject: [PATCH 12/17] =?UTF-8?q?=E6=96=B0=E7=89=88=E7=BE=A4=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../distribution-management/detail/index.tsx | 10 +- .../mobile/workspace/group-create/form/api.ts | 18 + .../form/components/BasicSettings.module.scss | 694 ++++++++++++++++++ .../form/components/BasicSettings.tsx | 606 +++++++++++++++ .../DeviceSelectionStep.module.scss | 197 +++++ .../form/components/DeviceSelectionStep.tsx | 123 ++++ .../components/PoolSelectionStep.module.scss | 233 ++++++ .../form/components/PoolSelectionStep.tsx | 156 ++++ .../workspace/group-create/form/index.tsx | 284 +++++++ .../workspace/group-create/form/types.ts | 103 +++ .../workspace/group-create/新增字段说明.md | 92 +++ Cunkebao/src/router/config.ts | 2 + Cunkebao/src/router/module/workspace.tsx | 14 +- Server/application/ai/controller/DouBaoAI.php | 3 +- Server/application/cunkebao/config/route.php | 5 + .../controller/CommonFunctionsController.php | 50 ++ 16 files changed, 2582 insertions(+), 8 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/form/types.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/新增字段说明.md create mode 100644 Server/application/cunkebao/controller/CommonFunctionsController.php diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx index 1df858fc..57c93bb6 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx @@ -33,14 +33,12 @@ import type { } from "./data"; import styles from "./index.module.scss"; -// 格式化金额显示(后端返回的是分,需要转换为元) +// 格式化金额显示(后端返回的是元,直接格式化即可) const formatCurrency = (amount: number): string => { - // 将分转换为元 - const yuan = amount / 100; - if (yuan >= 10000) { - return "¥" + (yuan / 10000).toFixed(2) + "万"; + if (amount >= 10000) { + return "¥" + (amount / 10000).toFixed(2) + "万"; } - return "¥" + yuan.toFixed(2); + return "¥" + amount.toFixed(2); }; const ChannelDetailPage: React.FC = () => { diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/api.ts b/Cunkebao/src/pages/mobile/workspace/group-create/form/api.ts new file mode 100644 index 00000000..d635dd2b --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/api.ts @@ -0,0 +1,18 @@ +import request from "@/api/request"; + +// 创建自动建群任务 +export const createGroupCreate = (params: any) => + request("/v1/workbench/create", params, "POST"); + +// 更新自动建群任务 +export const updateGroupCreate = (params: any) => + request("/v1/workbench/update", params, "POST"); + +// 获取自动建群任务详情 +export const getGroupCreateDetail = (id: string) => + request("/v1/workbench/detail", { id }, "GET"); + +// 获取自动建群任务列表 +export const getGroupCreateList = (params: any) => + request("/v1/workbench/list", params, "GET"); + diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.module.scss b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.module.scss new file mode 100644 index 00000000..66550d60 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.module.scss @@ -0,0 +1,694 @@ +.container { + padding: 16px; + background: #f8fafc; + min-height: 100vh; + padding-bottom: 100px; + box-sizing: border-box; + width: 100%; + overflow-x: hidden; // 防止水平滚动 + + @media (max-width: 375px) { + padding: 12px; // 小屏幕时减小padding + } +} + +.card { + background: #fff; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 16px; + margin-bottom: 16px; + box-sizing: border-box; // 确保padding不会导致超出 + width: 100%; + overflow: hidden; // 防止内容溢出 + + @media (max-width: 375px) { + padding: 12px; // 小屏幕时减小padding + } +} + +.label { + display: block; + font-size: 14px; + font-weight: 600; + color: #1e293b; + margin-bottom: 8px; +} + +.labelRequired { + color: #ef4444; + margin-left: 2px; +} + +.infoIcon { + color: #94a3b8; + font-size: 14px; + margin-left: 4px; + cursor: help; +} + +.radioGroup { + display: flex; + gap: 24px; +} + +.input { + width: 100%; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #f8fafc; + font-size: 14px; + outline: none; + transition: all 0.2s; + box-sizing: border-box; + + &:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); + } + + &::placeholder { + color: #cbd5e1; + } +} + +// 执行智能体选择区域 +.executorSelector { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: #f8fafc; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #3b82f6; + } +} + +.executorContent { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + overflow: hidden; +} + +.executorAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: #e2e8f0; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + position: relative; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.executorInfo { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + flex: 1; +} + +.executorName { + font-size: 14px; + font-weight: 500; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.executorId { + font-size: 12px; + color: #64748b; + margin-top: 2px; +} + +.executorExpand { + color: #64748b; + font-size: 20px; + flex-shrink: 0; +} + +.statusDot { + position: absolute; + bottom: -2px; + right: -2px; + width: 12px; + height: 12px; + background: #10b981; + border: 2px solid #fff; + border-radius: 50%; +} + +// 固定微信号 +.wechatSelect { + position: relative; + margin-bottom: 12px; +} + +.wechatSelectInput { + width: 100%; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #f8fafc; + cursor: pointer; + transition: all 0.2s; + margin-bottom: 12px; + + &:hover { + border-color: #3b82f6; + } + + &:active { + background: #f1f5f9; + } + + &.disabled { + background: #f1f5f9; + color: #94a3b8; + cursor: not-allowed; + opacity: 0.6; + + &:hover { + border-color: #e2e8f0; + } + } +} + +.selectInputWrapper { + display: flex; + align-items: center; + justify-content: space-between; +} + +.selectInputPlaceholder { + font-size: 14px; + color: #1e293b; + flex: 1; + + .wechatSelectInput.disabled & { + color: #94a3b8; + } +} + +.selectInputArrow { + color: #94a3b8; + font-size: 14px; + flex-shrink: 0; + margin-left: 8px; +} + +.selectedList { + margin-top: 12px; +} + +.selectedItem { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; + margin-bottom: 8px; +} + +.selectedItemContent { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.selectedItemAvatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: #e2e8f0; + flex-shrink: 0; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.selectedItemInfo { + display: flex; + flex-direction: column; + flex: 1; +} + +.selectedItemName { + font-size: 14px; + font-weight: 500; + color: #1e293b; +} + +.selectedItemId { + font-size: 12px; + color: #64748b; + margin-top: 2px; +} + +.deleteButton { + color: #94a3b8; + cursor: pointer; + padding: 4px; + transition: color 0.2s; + + &:hover { + color: #ef4444; + } +} + +// 手动添加 +.manualAdd { + border-top: 1px solid #f1f5f9; + padding-top: 12px; + margin-top: 12px; +} + +.manualAddLabel { + font-size: 12px; + color: #64748b; + margin-bottom: 8px; +} + +.manualAddInput { + display: flex; + gap: 8px; + height: 50px; + align-items: center; +} + +.manualAddInputWrapper { + position: relative; + flex: 1; + height: 100%; + display: flex; + align-items: center; +} + +.manualAddIcon { + position: absolute; + left: 12px; + color: #94a3b8; + font-size: 18px; + pointer-events: none; + z-index: 1; +} + +.manualAddInputField { + width: 100%; + height: 100%; + padding-left: 40px; + padding-right: 12px; + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #f8fafc; + font-size: 14px; + outline: none; + transition: all 0.2s; + + &:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.1); + } + + &::placeholder { + color: #cbd5e1; + } +} + +.manualAddButton { + width: 60px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #3b82f6; + border: 1px solid #3b82f6; + border-radius: 8px; + color: #fff; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + padding: 0; + line-height: 1.2; + + &:hover { + background: #2563eb; + } + + &:active { + transform: scale(0.98); + } + + span { + line-height: 1; + } + + .buttonText2 { + margin-top: 2px; + } +} + +// 已添加的微信号(带编号) +.addedList { + margin-top: 12px; +} + +.addedItem { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 8px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.addedItemContent { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.addedItemNumber { + width: 36px; + height: 36px; + border-radius: 50%; + background: #bfdbfe; + display: flex; + align-items: center; + justify-content: center; + color: #1e40af; + font-size: 12px; + font-weight: bold; + flex-shrink: 0; +} + +.addedItemInfo { + display: flex; + flex-direction: column; + flex: 1; +} + +.addedItemName { + font-size: 14px; + font-weight: 500; + color: #1e293b; +} + +.addedItemId { + font-size: 12px; + color: #64748b; + margin-top: 2px; +} + +.addedItemStatus { + font-size: 12px; + color: #3b82f6; + margin-top: 2px; +} + +.addedCount { + font-size: 12px; + color: #64748b; + margin-top: 8px; + text-align: right; +} + +// 群管理员 +.groupAdminHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.groupAdminLabelWrapper { + display: flex; + align-items: center; + gap: 4px; +} + +.switchWrapper { + position: relative; + display: inline-block; +} + +.groupAdminHint { + font-size: 12px; + color: #64748b; + margin-top: 8px; +} + +// 分组方式 +.groupMethod { + display: flex; + flex-direction: column; + gap: 12px; + + // 覆盖antd Radio的默认样式 + :global(.ant-radio-group) { + display: flex; + flex-direction: column; + gap: 12px; + } +} + +.groupMethodItem { + display: flex; + align-items: flex-start; + cursor: pointer; +} + +.groupMethodRadio { + margin-top: 2px; +} + +.groupMethodContent { + margin-left: 12px; + flex: 1; +} + +.groupMethodTitle { + font-size: 14px; + font-weight: 500; + color: #1e293b; + margin-bottom: 4px; +} + +.groupMethodDesc { + font-size: 12px; + color: #64748b; + line-height: 1.5; +} + +// 群配置信息卡片 +.groupConfigCard { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 12px; + padding: 16px; + margin-top: 16px; +} + +.groupConfigHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.groupConfigIcon { + color: #3b82f6; + font-size: 18px; +} + +.groupConfigTitle { + font-size: 14px; + font-weight: 600; + color: #3b82f6; +} + +.groupConfigRow { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + align-items: flex-start; + } +} + +.groupConfigLabel { + font-size: 14px; + color: #1e293b; +} + +.groupConfigValue { + font-size: 14px; + font-weight: bold; + color: #3b82f6; + text-align: right; +} + +// 错误提示 +.errorTip { + background: #fff; + border: 1px solid #fca5a5; + border-radius: 12px; + padding: 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 16px; +} + +.errorIcon { + color: #ef4444; + font-size: 16px; +} + +.errorText { + font-size: 14px; + color: #ef4444; +} + +// 群人数配置 +.groupSizeConfig { + display: flex; + flex-direction: column; + gap: 16px; +} + +.groupSizeRow { + display: flex; + gap: 12px; + align-items: flex-start; + flex-wrap: wrap; // 小屏幕时允许换行 + + @media (max-width: 375px) { + flex-direction: column; // 小屏幕时垂直排列 + gap: 16px; + } +} + +.groupSizeItem { + flex: 1; + min-width: 0; // 防止flex item超出容器 + + @media (max-width: 375px) { + flex: none; // 小屏幕时取消flex,占满宽度 + width: 100%; + } +} + +.groupSizeLabel { + font-size: 14px; + font-weight: normal; + color: #1e293b; + margin-bottom: 8px; + display: block; + word-wrap: break-word; // 允许长文本换行 + overflow-wrap: break-word; + + @media (max-width: 375px) { + font-size: 13px; // 小屏幕时稍微减小字体 + } +} + +// 执行时间 +.timeRangeContainer { + display: flex; + align-items: center; + gap: 12px; + margin-top: 12px; + flex-wrap: wrap; // 小屏幕时允许换行 + + @media (max-width: 375px) { + flex-direction: column; // 小屏幕时垂直排列 + align-items: stretch; + gap: 12px; + } +} + +.timeInputWrapper { + flex: 1; + position: relative; + min-width: 0; // 防止flex item超出容器 + + @media (max-width: 375px) { + flex: none; // 小屏幕时取消flex,占满宽度 + width: 100%; + } +} + +.timeSeparator { + color: #64748b; + font-size: 14px; + flex-shrink: 0; + margin: 0 4px; + + @media (max-width: 375px) { + display: none; // 小屏幕时隐藏分隔符 + } +} + +.timeInput { + width: 100%; + padding: 10px 12px 10px 36px; // 左边留出图标空间 + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #f8fafc; + font-size: 14px; + outline: none; + transition: all 0.2s; + box-sizing: border-box; // 确保padding不会导致超出 + + &:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); + } +} + +.timeIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #94a3b8; + font-size: 16px; + pointer-events: none; + z-index: 1; +} diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.tsx new file mode 100644 index 00000000..8f2f8745 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/BasicSettings.tsx @@ -0,0 +1,606 @@ +import React, { useImperativeHandle, forwardRef, useState, useEffect } from "react"; +import { Radio, Switch } from "antd"; +import { Input } from "antd"; +import { Toast, Avatar, Popup } from "antd-mobile"; +import { ClockCircleOutlined, InfoCircleOutlined, DeleteOutlined, UserAddOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; +import FriendSelection from "@/components/FriendSelection"; +import DeviceSelection from "@/components/DeviceSelection"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { GroupCreateFormData } from "../types"; +import style from "./BasicSettings.module.scss"; + +interface BasicSettingsProps { + formData: GroupCreateFormData; + onChange: (data: Partial) => void; +} + +export interface BasicSettingsRef { + validate: () => Promise; + getValues: () => any; +} + +const BasicSettings = forwardRef( + ({ formData, onChange }, ref) => { + const [executorSelectionVisible, setExecutorSelectionVisible] = useState(false); + const [groupAdminSelectionVisible, setGroupAdminSelectionVisible] = useState(false); + const [fixedWechatIdsSelectionVisible, setFixedWechatIdsSelectionVisible] = useState(false); + const [manualWechatIdInput, setManualWechatIdInput] = useState(""); + + + // 处理执行智能体选择(单选设备) + const handleExecutorSelect = (devices: DeviceSelectionItem[]) => { + if (devices.length > 0) { + const selectedDevice = devices[0]; + // 自动设置群名称为执行智能体的名称(优先使用 nickname,其次 memo,最后 wechatId),加上"的群"后缀 + const executorName = selectedDevice.nickname || selectedDevice.memo || selectedDevice.wechatId || ""; + onChange({ + executor: selectedDevice, + executorId: selectedDevice.id, + groupName: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式 + }); + } else { + onChange({ + executor: undefined, + executorId: undefined, + }); + } + setExecutorSelectionVisible(false); + }; + + // 处理固定微信号选择(必须3个) + const handleFixedWechatIdsSelect = (friends: FriendSelectionItem[]) => { + // 检查总数是否超过3个(包括已添加的手动微信号) + const currentManualCount = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual).length; + const newSelectedCount = friends.length; + if (currentManualCount + newSelectedCount > 3) { + Toast.show({ content: "固定微信号最多只能选择3个", position: "top" }); + return; + } + // 标记为选择的(非手动添加),确保所有从选择弹窗来的都标记为非手动 + const selectedFriends = friends.map(f => ({ ...f, isManual: false })); + // 合并已添加的手动微信号和新的选择 + const manualFriends = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true); + onChange({ + fixedWechatIds: [...manualFriends, ...selectedFriends].map(f => f.id), + fixedWechatIdsOptions: [...manualFriends, ...selectedFriends], + }); + setFixedWechatIdsSelectionVisible(false); + }; + + // 打开固定微信号选择弹窗前检查是否已选择执行智能体 + const handleOpenFixedWechatIdsSelection = () => { + if (!formData.executorId) { + Toast.show({ content: "请先选择执行智能体", position: "top" }); + return; + } + setFixedWechatIdsSelectionVisible(true); + }; + + // 处理群管理员选择(单选) + const handleGroupAdminSelect = (friends: FriendSelectionItem[]) => { + if (friends.length > 0) { + onChange({ + groupAdminWechatIdOption: friends[0], + groupAdminWechatId: friends[0].id, + }); + } else { + onChange({ + groupAdminWechatIdOption: undefined, + groupAdminWechatId: undefined, + }); + } + setGroupAdminSelectionVisible(false); + }; + + // 手动添加微信号 + const handleAddManualWechatId = () => { + if (!manualWechatIdInput.trim()) { + Toast.show({ content: "请输入微信号", position: "top" }); + return; + } + const existingIds = formData.fixedWechatIdsOptions.map(f => f.wechatId.toLowerCase()); + if (existingIds.includes(manualWechatIdInput.trim().toLowerCase())) { + Toast.show({ content: "该微信号已添加", position: "top" }); + return; + } + if (formData.fixedWechatIdsOptions.length >= 3) { + Toast.show({ content: "固定微信号最多只能添加3个", position: "top" }); + return; + } + // 创建临时好友项,标记为手动添加 + const newFriend: FriendSelectionItem = { + id: Date.now(), // 临时ID + wechatId: manualWechatIdInput.trim(), + nickname: manualWechatIdInput.trim(), + avatar: "", + isManual: true, // 标记为手动添加 + }; + onChange({ + fixedWechatIds: [...formData.fixedWechatIds, newFriend.id], + fixedWechatIdsOptions: [...formData.fixedWechatIdsOptions, newFriend], + }); + setManualWechatIdInput(""); + }; + + // 移除固定微信号 + const handleRemoveFixedWechatId = (id: number) => { + onChange({ + fixedWechatIds: formData.fixedWechatIds.filter(fid => fid !== id), + fixedWechatIdsOptions: formData.fixedWechatIdsOptions.filter(f => f.id !== id), + }); + }; + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + // 验证必填字段 + if (!formData.name?.trim()) { + Toast.show({ content: "请输入计划名称", position: "top" }); + return false; + } + if (!formData.executorId) { + Toast.show({ content: "请选择执行智能体", position: "top" }); + return false; + } + // 固定微信号不是必填的,移除验证 + if (!formData.groupName?.trim()) { + Toast.show({ content: "请输入群名称", position: "top" }); + return false; + } + // 群名称长度验证(2-100个字符) + const groupNameLength = formData.groupName.trim().length; + if (groupNameLength < 2 || groupNameLength > 100) { + Toast.show({ content: "群名称长度应在2-100个字符之间", position: "top" }); + return false; + } + return true; + }, + getValues: () => { + return formData; + }, + })); + + // 区分已选择的微信号(从下拉选择)和已添加的微信号(手动输入) + // 如果 isManual 未定义,默认为 false(即选择的) + const selectedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => !f.isManual); + const manualAddedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true); + + return ( +
+ {/* 计划类型和计划名称 */} +
+
+ + onChange({ planType: e.target.value })} + className={style.radioGroup} + > + 全局计划 + 独立计划 + +
+
+ + onChange({ name: e.target.value })} + placeholder="请输入计划名称" + /> +
+
+ + {/* 执行智能体 */} +
+ +
setExecutorSelectionVisible(true)} + > + {formData.executor ? ( +
+
+ {formData.executor.avatar ? ( + {formData.executor.memo + ) : ( + 🤖 + )} +
+
+
+
+ {formData.executor.nickname || formData.executor.memo || formData.executor.wechatId} +
+
ID: {formData.executor.wechatId}
+
+
+ ) : ( +
+
+ 🤖 +
+
+
+ 请选择执行智能体 +
+
+
+ )} + +
+
+ + {/* 固定微信号 */} +
+
+ +
+ + {/* 点击选择框 */} +
+
+ + {(selectedWechatIds.length + manualAddedWechatIds.length) > 0 + ? `已选择 ${selectedWechatIds.length + manualAddedWechatIds.length} 个微信号` + : '请选择微信号'} + + +
+
+ + {/* 已选择的微信号列表 */} + {selectedWechatIds.length > 0 && ( +
+

已选择的微信号:

+ {selectedWechatIds.map(friend => ( +
+
+
+ {friend.avatar ? ( + {friend.nickname} + ) : ( +
+ {friend.nickname?.charAt(0) || "?"} +
+ )} +
+
+
{friend.nickname || friend.wechatId}
+
微信ID: {friend.wechatId}
+
+
+ +
+ ))} +
+ )} + + {/* 手动添加微信号 */} +
+

搜索不到?请输入微信号添加

+
+
+ + setManualWechatIdInput(e.target.value)} + placeholder="请输入微信号" + /> +
+ +
+
+ + {/* 已添加的微信号列表(手动输入的) */} + {manualAddedWechatIds.length > 0 && ( +
+

已添加的微信号:

+ {manualAddedWechatIds.map((friend, index) => ( +
+
+
{index + 1}
+
+
{friend.nickname || friend.wechatId}
+
微信ID: {friend.wechatId}
+
{friend.wechatId} 已发起好友申请
+
+
+ +
+ ))} +

已添加 {(selectedWechatIds.length + manualAddedWechatIds.length)}/3 个微信号

+
+ )} +
+ + {/* 群管理员 */} +
+
+
+ + +
+ onChange({ groupAdminEnabled: checked })} + /> +
+ {formData.groupAdminEnabled && ( +
{ + if ((selectedWechatIds.length + manualAddedWechatIds.length) === 0) { + Toast.show({ content: "请先选择固定微信号", position: "top" }); + return; + } + setGroupAdminSelectionVisible(true); + }} + > +
+ + {formData.groupAdminWechatIdOption + ? (formData.groupAdminWechatIdOption.nickname || formData.groupAdminWechatIdOption.wechatId) + : '请选择群管理员微信号'} + + +
+
+ )} +

开启后,所选微信号将自动成为群管理员。

+
+ + {/* 群名称 */} +
+ + onChange({ groupName: e.target.value })} + placeholder="请输入群名称" + /> +
+ + {/* 群人数配置 */} +
+ + +
+ {/* 每日最大建群数 */} +
+ + onChange({ maxGroupsPerDay: Number(e.target.value) || 0 })} + placeholder="请输入数量" + /> +
+ + {/* 群组最小人数和最大人数 */} +
+
+ + { + const value = Number(e.target.value) || 0; + onChange({ groupSizeMin: value < 3 ? 3 : value }); + }} + placeholder="如: 3" + min={3} + /> +
+
+ + { + const value = Number(e.target.value) || 0; + onChange({ groupSizeMax: value > 38 ? 38 : value }); + }} + placeholder="如: 40" + max={38} + /> +
+
+
+
+ + {/* 执行时间 */} +
+ +
+
+ onChange({ executeTime: e.target.value || "09:00" })} + /> + +
+ - +
+ onChange({ executeEndTime: e.target.value || "18:00" })} + /> + +
+
+
+ + {/* 错误提示 */} + {(selectedWechatIds.length + manualAddedWechatIds.length) !== 3 && ( +
+ + 固定微信号必须选择3个 +
+ )} + + {/* 隐藏的选择组件 */} +
+ + +
+ + {/* 群管理员选择弹窗 - 只显示固定微信号列表 */} + setGroupAdminSelectionVisible(false)} + position="bottom" + bodyStyle={{ height: "60vh" }} + > +
+
+

选择群管理员微信号

+ +
+
+ {(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? ( +
+ 暂无固定微信号可选 +
+ ) : ( + [...selectedWechatIds, ...manualAddedWechatIds].map(friend => { + const isSelected = formData.groupAdminWechatIdOption?.id === friend.id; + return ( +
{ + onChange({ + groupAdminWechatIdOption: friend, + groupAdminWechatId: friend.id, + }); + setGroupAdminSelectionVisible(false); + }} + style={{ + padding: "12px", + marginBottom: "8px", + borderRadius: "8px", + border: `1px solid ${isSelected ? "#3b82f6" : "#e2e8f0"}`, + background: isSelected ? "#eff6ff" : "#fff", + cursor: "pointer", + transition: "all 0.2s", + }} + onMouseEnter={(e) => { + if (!isSelected) { + e.currentTarget.style.borderColor = "#3b82f6"; + e.currentTarget.style.background = "#f8fafc"; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.borderColor = "#e2e8f0"; + e.currentTarget.style.background = "#fff"; + } + }} + > +
+
+ {friend.avatar ? ( + {friend.nickname} + ) : ( + + {friend.nickname?.charAt(0) || friend.wechatId?.charAt(0) || "?"} + + )} +
+
+
+ {friend.nickname || friend.wechatId} +
+
+ 微信ID: {friend.wechatId} +
+
+ {isSelected && ( + + )} +
+
+ ); + }) + )} +
+
+
+
+ ); + }, +); + +BasicSettings.displayName = "BasicSettings"; + +export default BasicSettings; diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.module.scss b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.module.scss new file mode 100644 index 00000000..7665a04d --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.module.scss @@ -0,0 +1,197 @@ +.container { + padding: 16px; + background: #f8fafc; + min-height: 100vh; + padding-bottom: 100px; +} + +.header { + margin-bottom: 16px; +} + +.title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 0; +} + +.deviceSelectorWrapper { + margin-top: 16px; +} + +.deviceSelector { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: #f8fafc; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #3b82f6; + } +} + +.selectedDevicesInfo { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + overflow: hidden; +} + +.selectedCountText { + font-size: 14px; + font-weight: 500; + color: #1e293b; +} + +.deviceNames { + font-size: 12px; + color: #64748b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.placeholder { + flex: 1; + color: #cbd5e1; + font-size: 14px; +} + +.expandIcon { + color: #94a3b8; + font-size: 14px; + flex-shrink: 0; + margin-left: 8px; +} + +.selectedDevicesGrid { + margin-top: 16px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.selectedDeviceCard { + position: relative; + background: #fff; + border-radius: 12px; + padding: 16px; + border: 2px solid #3b82f6; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + background: #eff6ff; +} + +.deviceCardHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.deviceCardIcon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + overflow: hidden; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + display: flex; + align-items: center; + justify-content: center; + } +} + +.deviceIconOnline { + background: #e0f2fe; + color: #3b82f6; +} + +.deviceIconOffline { + background: #f3f4f6; + color: #94a3b8; +} + +.deviceCardStatusBadge { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; +} + +.statusOnline { + background: #d1fae5; + color: #065f46; +} + +.statusOffline { + background: #f3f4f6; + color: #374151; +} + +.deviceCardName { + font-size: 16px; + font-weight: 700; + color: #1e293b; + margin: 0 0 8px 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.deviceCardInfo { + display: flex; + flex-direction: column; + gap: 4px; +} + +.deviceCardPhone { + font-size: 12px; + color: #64748b; + margin: 0; +} + +.deviceCardDeleteButton { + position: absolute; + top: 12px; + right: 12px; + width: 24px; + height: 24px; + border-radius: 4px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: rgba(239, 68, 68, 0.2); + border-color: #ef4444; + } +} + +.deviceCardDeleteIcon { + color: #ef4444; + font-size: 14px; +} diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.tsx new file mode 100644 index 00000000..461a1215 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/DeviceSelectionStep.tsx @@ -0,0 +1,123 @@ +import React, { useState, useRef, useImperativeHandle, forwardRef } from "react"; +import { DeleteOutlined } from "@ant-design/icons"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import DeviceSelection from "@/components/DeviceSelection"; +import style from "./DeviceSelectionStep.module.scss"; + +export interface DeviceSelectionStepRef { + validate: () => Promise; + getValues: () => any; +} + +interface DeviceSelectionStepProps { + selectedDevices?: DeviceSelectionItem[]; + selectedDeviceIds?: number[]; + onSelect: (devices: DeviceSelectionItem[]) => void; +} + +const DeviceSelectionStep = forwardRef( + ({ selectedDevices = [], selectedDeviceIds = [], onSelect }, ref) => { + const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false); + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + if (selectedDeviceIds.length === 0) { + return false; + } + return true; + }, + getValues: () => { + return { selectedDevices, selectedDeviceIds }; + }, + })); + + const handleDeviceSelect = (devices: DeviceSelectionItem[]) => { + onSelect(devices); + setDeviceSelectionVisible(false); + }; + + return ( +
+
+

选择设备

+
+ +
+
setDeviceSelectionVisible(true)} + > + {selectedDevices.length > 0 ? ( +
+ + 已选择 {selectedDevices.length} 个设备 + +
+ ) : ( +
+ 请选择执行设备 +
+ )} + +
+
+ + {/* 已选设备列表 */} + {selectedDevices.length > 0 && ( +
+ {selectedDevices.map((device) => ( +
+
+
+ {device.avatar ? ( + {device.memo + ) : ( + 📱 + )} +
+ + {device.status === "online" ? "在线" : "离线"} + +
+

{device.memo || device.wechatId}

+
+

{device.wechatId}

+
+
{ + e.stopPropagation(); + const newSelectedDevices = selectedDevices.filter(d => d.id !== device.id); + onSelect(newSelectedDevices); + }} + > + +
+
+ ))} +
+ )} + + {/* 设备选择弹窗 */} +
+ +
+
+ ); + } +); + +DeviceSelectionStep.displayName = "DeviceSelectionStep"; + +export default DeviceSelectionStep; diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.module.scss b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.module.scss new file mode 100644 index 00000000..e9264d6d --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.module.scss @@ -0,0 +1,233 @@ +.container { + padding: 16px; + background: #f8fafc; + min-height: 100vh; + padding-bottom: 100px; +} + +.header { + margin-bottom: 16px; +} + +.headerTop { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-size: 18px; + font-weight: 700; + color: #1e293b; + margin: 0; +} + +.manageLink { + font-size: 14px; + color: #3b82f6; + text-decoration: none; + display: flex; + align-items: center; + gap: 4px; + transition: opacity 0.2s; + + &:hover { + opacity: 0.8; + } +} + +.linkIcon { + font-size: 14px; +} + +.infoBox { + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 12px; + padding: 16px; + margin-bottom: 24px; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.infoIcon { + color: #3b82f6; + font-size: 20px; + margin-top: 2px; + flex-shrink: 0; +} + +.infoText { + font-size: 14px; + color: #1e40af; + line-height: 1.5; + margin: 0; +} + +.poolSelectorWrapper { + margin-bottom: 24px; +} + +.poolSelector { + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #3b82f6; + } +} + +.selectedPoolsInfo { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + overflow: hidden; +} + +.selectedCountText { + font-size: 14px; + font-weight: 500; + color: #1e293b; +} + +.placeholder { + flex: 1; + color: #cbd5e1; + font-size: 14px; +} + +.expandIcon { + color: #94a3b8; + font-size: 14px; + flex-shrink: 0; + margin-left: 8px; +} + +.selectedPoolsList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.selectedPoolCard { + position: relative; + background: #fff; + border-radius: 16px; + padding: 20px; + border: 1px solid transparent; + box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05); + transition: all 0.2s; + cursor: pointer; + + &:hover { + border-color: #3b82f6; + box-shadow: 0 4px 20px -2px rgba(59, 130, 246, 0.15); + } +} + +.poolCardContent { + padding-right: 32px; +} + +.poolCardHeader { + margin-bottom: 8px; +} + +.poolCardName { + font-size: 16px; + font-weight: 700; + color: #1e293b; + margin: 0; +} + +.poolCardDescription { + font-size: 14px; + color: #64748b; + margin: 0 0 12px 0; +} + +.poolCardStats { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 12px; +} + +.poolCardUsers { + display: flex; + align-items: center; + gap: 6px; + color: #3b82f6; + font-weight: 700; + font-size: 14px; +} + +.usersIcon { + font-size: 18px; +} + +.usersCount { + font-size: 14px; +} + +.poolCardDivider { + width: 1px; + height: 12px; + background: #d1d5db; +} + +.poolCardTime { + font-size: 12px; + color: #9ca3af; +} + +.poolCardTags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.poolTag { + padding: 4px 10px; + border-radius: 8px; + background: #eff6ff; + color: #3b82f6; + font-size: 12px; + font-weight: 500; + border: 1px solid #bfdbfe; +} + +.poolCardDeleteButton { + position: absolute; + top: 16px; + right: 16px; + width: 24px; + height: 24px; + border-radius: 4px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: rgba(239, 68, 68, 0.2); + border-color: #ef4444; + } +} + +.poolCardDeleteIcon { + color: #ef4444; + font-size: 14px; +} diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.tsx new file mode 100644 index 00000000..32a2eb78 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/components/PoolSelectionStep.tsx @@ -0,0 +1,156 @@ +import React, { useState, useRef, useImperativeHandle, forwardRef } from "react"; +import { DeleteOutlined } from "@ant-design/icons"; +import { PoolSelectionItem } from "@/components/PoolSelection/data"; +import PoolSelection from "@/components/PoolSelection"; +import style from "./PoolSelectionStep.module.scss"; + +export interface PoolSelectionStepRef { + validate: () => Promise; + getValues: () => any; +} + +interface PoolSelectionStepProps { + selectedPools?: PoolSelectionItem[]; + poolGroups?: string[]; + onSelect: (pools: PoolSelectionItem[], poolGroups: string[]) => void; +} + +const PoolSelectionStep = forwardRef( + ({ selectedPools = [], poolGroups = [], onSelect }, ref) => { + const [poolSelectionVisible, setPoolSelectionVisible] = useState(false); + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + if (selectedPools.length === 0) { + return false; + } + return true; + }, + getValues: () => { + return { selectedPools, poolGroups }; + }, + })); + + const handlePoolSelect = (pools: PoolSelectionItem[]) => { + const poolGroupIds = pools.map(p => p.id); + onSelect(pools, poolGroupIds); + setPoolSelectionVisible(false); + }; + + return ( +
+ + +
+ +

+ 选择流量池后,系统将自动筛选出该流量池中的用户,以确定自动建群所针对的目标群体。 +

+
+ +
+
setPoolSelectionVisible(true)} + > + {selectedPools.length > 0 ? ( +
+ + 已选择 {selectedPools.length} 个流量池 + +
+ ) : ( +
+ 请选择流量池 +
+ )} + +
+
+ + {/* 已选流量池列表 */} + {selectedPools.length > 0 && ( +
+ {selectedPools.map((pool) => ( +
+
+
+

{pool.name}

+
+ {pool.description && ( +

{pool.description}

+ )} +
+
+ 👥 + {pool.num || 0} 人 +
+ {pool.createTime && ( + <> +
+ + 更新于 {pool.createTime} + + + )} +
+ {pool.tags && pool.tags.length > 0 && ( +
+ {pool.tags.map((tag: any, index: number) => ( + + {typeof tag === 'string' ? tag : tag.name || tag.label} + + ))} +
+ )} +
+
{ + e.stopPropagation(); + const newSelectedPools = selectedPools.filter(p => p.id !== pool.id); + const newPoolGroups = newSelectedPools.map(p => p.id); + onSelect(newSelectedPools, newPoolGroups); + }} + > + +
+
+ ))} +
+ )} + + {/* 流量池选择弹窗 */} + +
+ ); + } +); + +PoolSelectionStep.displayName = "PoolSelectionStep"; + +export default PoolSelectionStep; diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/index.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/form/index.tsx new file mode 100644 index 00000000..814fba1a --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/index.tsx @@ -0,0 +1,284 @@ +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Toast } from "antd-mobile"; +import { Button } from "antd"; +import Layout from "@/components/Layout/Layout"; +import { createGroupCreate, updateGroupCreate, getGroupCreateDetail } from "./api"; +import { GroupCreateFormData } from "./types"; +import StepIndicator from "@/components/StepIndicator"; +import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings"; +import DeviceSelectionStep, { DeviceSelectionStepRef } from "./components/DeviceSelectionStep"; +import PoolSelectionStep, { PoolSelectionStepRef } from "./components/PoolSelectionStep"; +import NavCommon from "@/components/NavCommon/index"; + +const steps = [ + { id: 1, title: "1", subtitle: "群设置" }, + { id: 2, title: "2", subtitle: "设备选择" }, + { id: 3, title: "3", subtitle: "流量池选择" }, +]; + +const defaultForm: GroupCreateFormData = { + planType: 1, // 默认独立计划 + name: "新建群计划", + executorId: undefined, + executor: undefined, + selectedDevices: [], + selectedDeviceIds: [], + fixedWechatIds: [], + fixedWechatIdsOptions: [], + groupAdminEnabled: false, + groupAdminWechatId: undefined, + groupAdminWechatIdOption: undefined, + groupName: "卡若001", + groupMethod: 0, // 默认所有好友自动分组 + maxGroupsPerDay: 10, + groupSizeMin: 3, + groupSizeMax: 38, + executeType: 1, // 默认定时执行 + executeTime: "09:00", // 默认开始时间 + executeEndTime: "18:00", // 默认结束时间 + poolGroups: [], + poolGroupsOptions: [], + status: 1, // 默认启用 +}; + +const GroupCreateForm: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams(); + const isEdit = Boolean(id); + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(false); + const [dataLoaded, setDataLoaded] = useState(!isEdit); + const [formData, setFormData] = useState(defaultForm); + + // 创建子组件的ref + const basicSettingsRef = useRef(null); + const deviceSelectionStepRef = useRef(null); + const poolSelectionStepRef = useRef(null); + + useEffect(() => { + if (!id) return; + // 获取详情并回填表单 + getGroupCreateDetail(id) + .then(res => { + const updatedForm: GroupCreateFormData = { + ...defaultForm, + id: res.id, + planType: res.planType ?? 1, + name: res.name ?? "", + executorId: res.executorId, + executor: res.executor, + selectedDevices: res.selectedDevices || [], + selectedDeviceIds: res.selectedDeviceIds || [], + fixedWechatIds: res.fixedWechatIds || [], + fixedWechatIdsOptions: res.fixedWechatIdsOptions || [], + groupAdminEnabled: res.groupAdminEnabled ?? false, + groupAdminWechatId: res.groupAdminWechatId, + groupAdminWechatIdOption: res.groupAdminWechatIdOption, + groupName: res.groupName ?? "", + groupMethod: res.groupMethod ?? 0, + maxGroupsPerDay: res.maxGroupsPerDay ?? 10, + groupSizeMin: res.groupSizeMin ?? 3, + groupSizeMax: res.groupSizeMax ?? 38, + executeType: res.executeType ?? 1, + executeTime: res.executeTime ?? "09:00", + executeEndTime: res.executeEndTime ?? "18:00", + poolGroups: res.poolGroups || [], + poolGroupsOptions: res.poolGroupsOptions || [], + status: res.status ?? 1, + }; + setFormData(updatedForm); + setDataLoaded(true); + }) + .catch(err => { + Toast.show({ content: err.message || "获取详情失败" }); + setDataLoaded(true); + }); + }, [id]); + + const handleFormDataChange = (values: Partial) => { + setFormData(prev => ({ ...prev, ...values })); + }; + + const handleNext = async () => { + if (currentStep === 1) { + // 验证第一步 + const isValid = (await basicSettingsRef.current?.validate()) || false; + if (!isValid) { + return; + } + setCurrentStep(2); + // 切换到下一步时,滚动到顶部 + setTimeout(() => { + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, 100); + } else if (currentStep === 2) { + // 验证第二步 + const isValid = (await deviceSelectionStepRef.current?.validate()) || false; + if (!isValid) { + Toast.show({ content: "请至少选择一个执行设备", position: "top" }); + return; + } + setCurrentStep(3); + // 切换到下一步时,滚动到顶部 + setTimeout(() => { + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, 100); + } else if (currentStep === 3) { + // 验证第三步并保存 + await handleSave(); + } + }; + + const handleSave = async () => { + // 验证第三步 + const isValid = (await poolSelectionStepRef.current?.validate()) || false; + if (!isValid) { + Toast.show({ content: "请至少选择一个流量池", position: "top" }); + return; + } + + setLoading(true); + try { + // 构建提交数据 + const submitData = { + ...formData, + type: 4, // 自动建群任务类型(保持与旧版一致) + }; + + if (isEdit) { + await updateGroupCreate(submitData); + Toast.show({ content: "编辑成功" }); + } else { + await createGroupCreate(submitData); + Toast.show({ content: "创建成功" }); + } + // 注意:需要导航到列表页面,但目前列表页面还未创建 + // navigate("/workspace/group-create"); + navigate("/workspace"); + } catch (e: any) { + Toast.show({ content: e?.message || "提交失败" }); + } finally { + setLoading(false); + } + }; + + const renderCurrentStep = () => { + // 编辑模式下,等待数据加载完成后再渲染 + if (isEdit && !dataLoaded) { + return ( +
加载中...
+ ); + } + + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( + { + const deviceIds = devices.map(d => d.id); + handleFormDataChange({ + selectedDevices: devices, + selectedDeviceIds: deviceIds, + // 如果只有一个设备,也设置 executor 和 executorId 用于兼容 + executor: devices.length === 1 ? devices[0] : undefined, + executorId: devices.length === 1 ? devices[0].id : undefined, + }); + }} + /> + ); + case 3: + return ( + { + handleFormDataChange({ + poolGroupsOptions: pools, + poolGroups: poolGroupIds, + }); + }} + /> + ); + default: + return null; + } + }; + + const renderFooter = () => { + return ( +
+ {currentStep > 1 && ( + + )} + +
+ ); + }; + + return ( + + navigate(-1)} + /> + + + } + footer={renderFooter()} + > +
{renderCurrentStep()}
+
+ ); +}; + +export default GroupCreateForm; diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/form/types.ts b/Cunkebao/src/pages/mobile/workspace/group-create/form/types.ts new file mode 100644 index 00000000..516545f9 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/form/types.ts @@ -0,0 +1,103 @@ +import { FriendSelectionItem } from "@/components/FriendSelection/data"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { PoolSelectionItem } from "@/components/PoolSelection/data"; + +// 自动建群表单数据类型定义(新版) +export interface GroupCreateFormData { + id?: string; // 任务ID + planType: number; // 计划类型:0-全局计划,1-独立计划 + name: string; // 计划名称 + executor?: DeviceSelectionItem; // 执行智能体(执行者)- 单个设备(保留用于兼容) + executorId?: number; // 执行智能体ID(设备ID)(保留用于兼容) + selectedDevices?: DeviceSelectionItem[]; // 选中的设备列表 + selectedDeviceIds?: number[]; // 选中的设备ID列表 + fixedWechatIds: number[]; // 固定微信号ID列表(必须3个) + fixedWechatIdsOptions: FriendSelectionItem[]; // 固定微信号选项 + groupAdminEnabled: boolean; // 群管理员开关 + groupAdminWechatId?: number; // 群管理员微信号ID + groupAdminWechatIdOption?: FriendSelectionItem; // 群管理员微信号选项 + groupName: string; // 群名称 + groupMethod: number; // 分组方式:0-所有好友自动分组,1-指定群数量 + maxGroupsPerDay: number; // 每日最大建群数(当groupMethod为1时使用) + groupSizeMin: number; // 群组最小人数 + groupSizeMax: number; // 群组最大人数 + executeType: number; // 执行类型:0-立即执行,1-定时执行 + executeTime?: string; // 执行开始时间(HH:mm),默认 09:00 + executeEndTime?: string; // 执行结束时间(HH:mm),默认 18:00 + poolGroups?: string[]; // 流量池ID列表 + poolGroupsOptions?: PoolSelectionItem[]; // 流量池选项列表 + status: number; // 是否启用 (1: 启用, 0: 禁用) + [key: string]: any; +} + +// 步骤定义 +export interface StepItem { + id: number; + title: string; + subtitle: string; +} + +// 表单验证规则 +export const formValidationRules = { + name: [ + { required: true, message: "请输入计划名称" }, + { min: 2, max: 50, message: "计划名称长度应在2-50个字符之间" }, + ], + executorId: [{ required: true, message: "请选择执行智能体" }], + fixedWechatIds: [ + { required: true, message: "请选择固定微信号" }, + { + validator: (_: any, value: number[]) => { + if (!value || value.length !== 3) { + return Promise.reject(new Error("固定微信号必须选择3个")); + } + return Promise.resolve(); + }, + }, + ], + groupName: [ + { required: true, message: "请输入群名称" }, + { min: 2, max: 100, message: "群名称长度应在2-100个字符之间" }, + ], + maxGroupsPerDay: [ + { required: true, message: "请输入每日最大建群数" }, + { + type: "number", + min: 1, + max: 100, + message: "每日最大建群数应在1-100之间", + }, + ], + groupSizeMin: [ + { required: true, message: "请输入群组最小人数" }, + { type: "number", min: 1, max: 500, message: "群组最小人数应在1-500之间" }, + ], + groupSizeMax: [ + { required: true, message: "请输入群组最大人数" }, + { type: "number", min: 1, max: 500, message: "群组最大人数应在1-500之间" }, + ], + executeDate: [ + { + validator: (_: any, value: string, callback: any) => { + // 如果执行类型是定时执行,则必填 + const executeType = callback?.executeType ?? 1; + if (executeType === 1 && !value) { + return Promise.reject(new Error("请选择执行日期")); + } + return Promise.resolve(); + }, + }, + ], + executeTime: [ + { + validator: (_: any, value: string, callback: any) => { + // 如果执行类型是定时执行,则必填 + const executeType = callback?.executeType ?? 1; + if (executeType === 1 && !value) { + return Promise.reject(new Error("请选择执行时间")); + } + return Promise.resolve(); + }, + }, + ], +}; diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/新增字段说明.md b/Cunkebao/src/pages/mobile/workspace/group-create/新增字段说明.md new file mode 100644 index 00000000..b0eca09f --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/新增字段说明.md @@ -0,0 +1,92 @@ +# 自动建群(group-create)新增字段说明 + +## 新增字段列表 + +相对于旧的 auto-group 模块,新版本(group-create)新增了以下字段: + +### 1. planType (计划类型) +- **类型**: number +- **取值**: 0-全局计划, 1-独立计划 +- **默认值**: 1 (独立计划) +- **说明**: 计划类型,用于区分全局计划和独立计划 + +### 2. executor / executorId (执行智能体) +- **类型**: FriendSelectionItem / number +- **说明**: 执行智能体(执行者),单个微信号选择 +- **必填**: 是 +- **替换**: 原 `devices` 字段(原为设备选择,现改为微信号选择) + +### 3. fixedWechatIds / fixedWechatIdsOptions (固定微信号) +- **类型**: number[] / FriendSelectionItem[] +- **说明**: 固定微信号,必须选择3个 +- **必填**: 是 +- **新增**: 完全新增的字段,旧版本没有对应字段 +- **功能**: 支持搜索选择,也支持手动输入微信号添加 + +### 4. groupAdminEnabled (群管理员开关) +- **类型**: boolean +- **默认值**: false +- **说明**: 是否启用群管理员功能 +- **新增**: 完全新增的字段 + +### 5. groupAdminWechatId / groupAdminWechatIdOption (群管理员微信号) +- **类型**: number / FriendSelectionItem +- **说明**: 群管理员微信号(当 groupAdminEnabled 为 true 时选择) +- **必填**: 否(仅在 groupAdminEnabled 为 true 时选择) +- **新增**: 完全新增的字段 +- **替换**: 原 `admins` 字段(原为多个管理员,现改为单个可选的管理员) + +### 6. groupName (群名称) +- **类型**: string +- **说明**: 群名称 +- **必填**: 是 +- **替换**: 原 `groupNameTemplate` (群名称模板) +- **变更**: 从模板改为直接输入群名称 + +### 7. executeType (执行类型) +- **类型**: number +- **取值**: 0-立即执行, 1-定时执行 +- **默认值**: 1 (定时执行) +- **说明**: 执行类型 +- **新增**: 完全新增的字段 + +### 8. executeDate (执行日期) +- **类型**: string (YYYY-MM-DD) +- **说明**: 执行日期,仅在 executeType === 1 (定时执行) 时必填 +- **新增**: 完全新增的字段 + +### 9. executeTime (执行时间) +- **类型**: string (HH:mm) +- **说明**: 执行时间,仅在 executeType === 1 (定时执行) 时必填 +- **新增**: 完全新增的字段 + +## 已删除的字段 + +以下字段在新版本中不再使用: + +1. **devices / devicesOptions** - 被 executor / executorId 替换(从设备选择改为微信号选择) +2. **admins / adminsOptions** - 被 groupAdminWechatId / groupAdminWechatIdOption 替换(从多个管理员改为单个可选管理员) +3. **poolGroups / poolGroupsOptions** - 流量池选择,新版本不再需要 +4. **startTime / endTime** - 允许建群的时间段,新版本改为 executeDate + executeTime +5. **groupNameTemplate** - 被 groupName 替换(从模板改为直接输入) +6. **groupDescription** - 群描述,新版本不再需要 + +## 保留的字段 + +以下字段在新版本中继续使用: + +1. **id** - 任务ID +2. **name** - 计划名称(保持原有逻辑) +3. **maxGroupsPerDay** - 每日最大建群数(保持原有逻辑) +4. **groupSizeMin** - 群组最小人数(保持原有逻辑) +5. **groupSizeMax** - 群组最大人数(保持原有逻辑) +6. **status** - 是否启用(保持原有逻辑) +7. **type** - 任务类型(固定为4,与旧版保持一致) + +## 接口说明 + +接口路径保持与旧版一致: +- 创建: POST /v1/workbench/create +- 更新: POST /v1/workbench/update +- 详情: GET /v1/workbench/detail +- 列表: GET /v1/workbench/list diff --git a/Cunkebao/src/router/config.ts b/Cunkebao/src/router/config.ts index b6a8f394..97e07e3f 100644 --- a/Cunkebao/src/router/config.ts +++ b/Cunkebao/src/router/config.ts @@ -40,6 +40,8 @@ export const routeGroups = { "/workspace/auto-like/:id/edit", "/workspace/auto-group", "/workspace/auto-group/:id", + "/workspace/group-create/new", + "/workspace/group-create/:id", "/workspace/group-push", "/workspace/group-push/new", "/workspace/group-push/:id", diff --git a/Cunkebao/src/router/module/workspace.tsx b/Cunkebao/src/router/module/workspace.tsx index 55959dfc..b78a696a 100644 --- a/Cunkebao/src/router/module/workspace.tsx +++ b/Cunkebao/src/router/module/workspace.tsx @@ -5,6 +5,7 @@ import RecordAutoLike from "@/pages/mobile/workspace/auto-like/record"; import AutoGroupList from "@/pages/mobile/workspace/auto-group/list"; import AutoGroupDetail from "@/pages/mobile/workspace/auto-group/detail"; import AutoGroupForm from "@/pages/mobile/workspace/auto-group/form"; +import GroupCreateForm from "@/pages/mobile/workspace/group-create/form"; import GroupPush from "@/pages/mobile/workspace/group-push/list"; import FormGroupPush from "@/pages/mobile/workspace/group-push/form"; import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail"; @@ -53,7 +54,7 @@ const workspaceRoutes = [ element: , auth: true, }, - // 自动建群 + // 自动建群(旧版) { path: "/workspace/auto-group", element: , @@ -74,6 +75,17 @@ const workspaceRoutes = [ element: , auth: true, }, + // 自动建群(新版) + { + path: "/workspace/group-create/new", + element: , + auth: true, + }, + { + path: "/workspace/group-create/:id", + element: , + auth: true, + }, // 群发推送 { path: "/workspace/group-push", diff --git a/Server/application/ai/controller/DouBaoAI.php b/Server/application/ai/controller/DouBaoAI.php index 2a78e4f0..a3c942cb 100644 --- a/Server/application/ai/controller/DouBaoAI.php +++ b/Server/application/ai/controller/DouBaoAI.php @@ -37,11 +37,12 @@ class DouBaoAI extends Controller if (empty($params)){ $content = $this->request->param('content', ''); + $model = $this->request->param('model', 'doubao-seed-1-8-251215'); if(empty($content)){ return json_encode(['code' => 500, 'msg' => '提示词缺失']); } $params = [ - 'model' => 'doubao-seed-1-8-251215', + 'model' => $model, 'messages' => [ ['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'], ['role' => 'user', 'content' => $content], diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 90c6fd0d..bfd63be1 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -157,6 +157,11 @@ Route::group('v1/', function () { Route::get('userInfoStats', 'app\cunkebao\controller\StatsController@userInfoStats'); }); + // 常用功能相关 + Route::group('common-functions', function () { + Route::get('list', 'app\cunkebao\controller\CommonFunctionsController@getList'); // 获取常用功能列表 + }); + // 算力相关 Route::group('tokens', function () { Route::get('list', 'app\cunkebao\controller\TokensController@getList'); diff --git a/Server/application/cunkebao/controller/CommonFunctionsController.php b/Server/application/cunkebao/controller/CommonFunctionsController.php new file mode 100644 index 00000000..55c20713 --- /dev/null +++ b/Server/application/cunkebao/controller/CommonFunctionsController.php @@ -0,0 +1,50 @@ +getUserInfo('companyId'); + + // 从数据库查询常用功能列表 + $functions = Db::name('workbench_function') + ->where('status', 1) + ->order('sort ASC, id ASC') + ->select(); + + + // 处理数据,判断是否显示New标签(创建时间近1个月) + $oneMonthAgo = time() - 30 * 24 * 60 * 60; // 30天前的时间戳 + foreach ($functions as &$function) { + // 判断是否显示New标签:创建时间在近1个月内 + $function['isNew'] = ($function['createTime'] >= $oneMonthAgo) ? true : false; + $function['labels'] = json_decode($function['labels'],true); + } + unset($function); + + return ResponseHelper::success([ + 'list' => $functions + ]); + + } catch (\Exception $e) { + return ResponseHelper::error('获取常用功能列表失败:' . $e->getMessage()); + } + } + + +} + From b0d9481cdb9e219695824fb11ae5c9f602225a7e Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 29 Dec 2025 18:00:46 +0800 Subject: [PATCH 13/17] =?UTF-8?q?=E6=8F=90=E7=8E=B0bug=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/distribution/ChannelController.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Server/application/cunkebao/controller/distribution/ChannelController.php b/Server/application/cunkebao/controller/distribution/ChannelController.php index d6b4dd22..322a7de7 100644 --- a/Server/application/cunkebao/controller/distribution/ChannelController.php +++ b/Server/application/cunkebao/controller/distribution/ChannelController.php @@ -807,16 +807,22 @@ class ChannelController extends BaseController $cid = $stat['channelId']; if (!isset($withdrawalStats[$cid])) { $withdrawalStats[$cid] = [ - 'totalRevenue' => 0, // 所有状态的总金额 + 'totalRevenue' => 0, // 总收益(不包括驳回的) 'withdrawn' => 0, // 已打款(paid) 'pendingReview' => 0 // 待审核(pending) ]; } $amount = intval($stat['totalAmount'] ?? 0); - $withdrawalStats[$cid]['totalRevenue'] += $amount; - if ($stat['status'] === DistributionWithdrawal::STATUS_PAID) { + $status = $stat['status']; + + // totalRevenue 不包括驳回(rejected)状态的金额 + if ($status !== DistributionWithdrawal::STATUS_REJECTED) { + $withdrawalStats[$cid]['totalRevenue'] += $amount; + } + + if ($status === DistributionWithdrawal::STATUS_PAID) { $withdrawalStats[$cid]['withdrawn'] += $amount; - } elseif ($stat['status'] === DistributionWithdrawal::STATUS_PENDING) { + } elseif ($status === DistributionWithdrawal::STATUS_PENDING) { $withdrawalStats[$cid]['pendingReview'] += $amount; } } From 64358df1c1cbc6978301aca8f6c78b79d0f50b7e Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 30 Dec 2025 11:06:46 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=BA=93=E9=87=87?= =?UTF-8?q?=E9=9B=86=E6=94=AF=E6=8C=81=E9=93=BE=E6=8E=A5=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/mobile/mine/content/form/index.tsx | 11 +- .../controller/ContentLibraryController.php | 270 +++++++++--------- .../application/job/MessageFriendsListJob.php | 2 +- 3 files changed, 139 insertions(+), 144 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index b52229bb..7ad12d64 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -68,6 +68,7 @@ export default function ContentForm() { "text", "image", "video", + "link", ]); const [submitting, setSubmitting] = useState(false); const [loading, setLoading] = useState(false); @@ -126,7 +127,7 @@ export default function ContentForm() { setSelectedFriendsOptions(data.friendsGroupsOptions || []); setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); - setCatchType(data.catchType || ["text", "image", "video"]); + setCatchType(data.catchType || ["text", "image", "video", "link"]); setAIPrompt(data.aiPrompt || ""); // aiEnabled 为 AI 提示词开关,1 开启 0 关闭 if (typeof data.aiEnabled !== "undefined") { @@ -275,7 +276,7 @@ export default function ContentForm() { 来源渠道 - 微信朋友圈 + 微信
@@ -427,7 +428,7 @@ export default function ContentForm() { 采集内容类型
- {["text", "image", "video"].map(type => ( + {["text", "image", "video", "link"].map(type => (
diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 94770ac5..5a11452b 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -182,18 +182,18 @@ class ContentLibraryController extends Controller foreach ($list as $item) { $libraryIds[] = $item['id']; - + // 解析JSON字段 $item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true); $item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true); $item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true); $item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true); - + // 收集好友和群组ID if (!empty($item['sourceFriends']) && $item['sourceType'] == 1) { $friendIdsByLibrary[$item['id']] = $item['sourceFriends']; } - + if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) { $groupIdsByLibrary[$item['id']] = $item['sourceGroups']; } @@ -208,12 +208,12 @@ class ContentLibraryController extends Controller ->where('isDel', 0) ->group('libraryId') ->select(); - + foreach ($counts as $count) { $itemCounts[$count['libraryId']] = $count['count']; } } - + // 批量查询好友信息 $friendsInfoMap = []; $allFriendIds = []; @@ -222,7 +222,7 @@ class ContentLibraryController extends Controller $allFriendIds = array_merge($allFriendIds, $friendIds); } } - + if (!empty($allFriendIds)) { $allFriendIds = array_unique($allFriendIds); $friendsInfo = Db::name('wechat_friendship')->alias('wf') @@ -230,12 +230,12 @@ class ContentLibraryController extends Controller ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') ->whereIn('wf.id', $allFriendIds) ->select(); - + foreach ($friendsInfo as $friend) { $friendsInfoMap[$friend['id']] = $friend; } } - + // 批量查询群组信息 $groupsInfoMap = []; $allGroupIds = []; @@ -244,14 +244,14 @@ class ContentLibraryController extends Controller $allGroupIds = array_merge($allGroupIds, $groupIds); } } - + if (!empty($allGroupIds)) { $allGroupIds = array_unique($allGroupIds); $groupsInfo = Db::name('wechat_group')->alias('g') ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId') ->whereIn('g.id', $allGroupIds) ->select(); - + foreach ($groupsInfo as $group) { $groupsInfoMap[$group['id']] = $group; } @@ -261,10 +261,10 @@ class ContentLibraryController extends Controller foreach ($list as &$item) { // 添加创建人名称 $item['creatorName'] = $item['user']['username'] ?? ''; - + // 添加内容项数量 $item['itemCount'] = $itemCounts[$item['id']] ?? 0; - + // 处理好友信息 if (!empty($friendIdsByLibrary[$item['id']])) { $selectedFriends = []; @@ -275,7 +275,7 @@ class ContentLibraryController extends Controller } $item['selectedFriends'] = $selectedFriends; } - + // 处理群组信息 if (!empty($groupIdsByLibrary[$item['id']])) { $selectedGroups = []; @@ -286,7 +286,7 @@ class ContentLibraryController extends Controller } $item['selectedGroups'] = $selectedGroups; } - + unset($item['user']); // 移除关联数据 } @@ -311,7 +311,7 @@ class ContentLibraryController extends Controller $companyId = $this->request->userInfo['companyId']; $userId = $this->request->userInfo['id']; $isAdmin = !empty($this->request->userInfo['isAdmin']); - + if (empty($id)) { return json(['code' => 400, 'msg' => '参数错误']); } @@ -344,7 +344,7 @@ class ContentLibraryController extends Controller $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true); $library['catchType'] = json_decode($library['catchType'] ?: [], true); $library['deviceGroups'] = json_decode($library['devices'] ?: [], true); - unset($library['sourceFriends'], $library['sourceGroups'],$library['devices']); + unset($library['sourceFriends'], $library['sourceGroups'], $library['devices']); // 将时间戳转换为日期格式(精确到日) if (!empty($library['timeStart'])) { @@ -358,7 +358,7 @@ class ContentLibraryController extends Controller $library['friendsGroupsOptions'] = []; $library['wechatGroupsOptions'] = []; $library['groupMembersOptions'] = []; - + // 批量查询好友信息 if (!empty($library['friendsGroups'])) { $friendIds = $library['friendsGroups']; @@ -392,7 +392,7 @@ class ContentLibraryController extends Controller // 键是群组ID,值是成员ID数组 $allMemberIds = []; $groupMembersMap = []; - + if (is_array($library['groupMembers'])) { foreach ($library['groupMembers'] as $groupId => $memberIds) { if (is_array($memberIds) && !empty($memberIds)) { @@ -402,17 +402,17 @@ class ContentLibraryController extends Controller } } } - + if (!empty($allMemberIds)) { // 去重 $allMemberIds = array_unique($allMemberIds); - + // 查询群成员信息 $members = Db::table('s2_wechat_chatroom_member') ->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime') ->whereIn('id', $allMemberIds) ->select(); - + // 将成员数据按ID建立索引 $membersById = []; foreach ($members as $member) { @@ -421,7 +421,7 @@ class ContentLibraryController extends Controller $member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : ''; $membersById[$member['id']] = $member; } - + // 按照群组ID分组返回 $groupMembersOptions = []; foreach ($groupMembersMap as $groupId => $memberIds) { @@ -432,7 +432,7 @@ class ContentLibraryController extends Controller } } } - + $library['groupMembersOptions'] = $groupMembersOptions; } else { $library['groupMembersOptions'] = []; @@ -466,7 +466,6 @@ class ContentLibraryController extends Controller } - return json([ 'code' => 200, 'msg' => '获取成功', @@ -616,7 +615,7 @@ class ContentLibraryController extends Controller ['isDel', '=', 0] ]; - + if (!$isAdmin) { $libraryWhere[] = ['userId', '=', $userId]; } @@ -644,7 +643,7 @@ class ContentLibraryController extends Controller // 查询数据 $list = ContentItem::where($where) ->field('id,libraryId,type,title,content,contentAi,contentType,resUrls,urls,friendId,wechatId,wechatChatroomId,createTime,createMomentTime,createMessageTime,coverImage,ossUrls') - ->order('createMomentTime DESC,createTime DESC') + ->order('createTime DESC,createMomentTime DESC') ->page($page, $limit) ->select(); @@ -652,7 +651,7 @@ class ContentLibraryController extends Controller $friendIds = []; $chatroomIds = []; $wechatIds = []; - + foreach ($list as $item) { if ($item['type'] == 'moment' && !empty($item['friendId'])) { $friendIds[] = $item['friendId']; @@ -663,7 +662,7 @@ class ContentLibraryController extends Controller } } } - + // 批量查询好友信息 $friendInfoMap = []; if (!empty($friendIds)) { @@ -672,12 +671,12 @@ class ContentLibraryController extends Controller ->whereIn('id', $friendIds) ->field('id, nickname, avatar') ->select(); - + foreach ($friendInfos as $info) { $friendInfoMap[$info['id']] = $info; } } - + // 批量查询群成员信息 $memberInfoMap = []; if (!empty($wechatIds)) { @@ -686,7 +685,7 @@ class ContentLibraryController extends Controller ->whereIn('wechatId', $wechatIds) ->field('wechatId, nickname, avatar') ->select(); - + foreach ($memberInfos as $info) { $memberInfoMap[$info['wechatId']] = $info; } @@ -697,14 +696,14 @@ class ContentLibraryController extends Controller foreach ($list as &$item) { // 使用AI内容(如果有) $item['content'] = !empty($item['contentAi']) ? $item['contentAi'] : $item['content']; - + // 处理JSON字段 $item['resUrls'] = json_decode($item['resUrls'] ?: [], true); $item['urls'] = json_decode($item['urls'] ?: [], true); $item['ossUrls'] = json_decode($item['ossUrls'] ?: [], true); - if(!empty($item['ossUrls']) && count($item['ossUrls']) > 0){ - $item['resUrls'] = $item['ossUrls']; + if (!empty($item['ossUrls']) && count($item['ossUrls']) > 0) { + $item['resUrls'] = $item['ossUrls']; } @@ -722,7 +721,7 @@ class ContentLibraryController extends Controller // 设置发送者信息 $item['senderNickname'] = ''; $item['senderAvatar'] = ''; - + // 从映射表获取发送者信息 if ($item['type'] == 'moment' && !empty($item['friendId'])) { if (isset($friendInfoMap[$item['friendId']])) { @@ -738,7 +737,7 @@ class ContentLibraryController extends Controller } } - unset($item['contentAi'],$item['ossUrls']); + unset($item['contentAi'], $item['ossUrls']); } return json([ @@ -849,8 +848,8 @@ class ContentLibraryController extends Controller ['l.companyId', '=', $this->request->userInfo['companyId']] ]; - if(empty($this->request->userInfo['isAdmin'])){ - $where[] = ['l.userId', '=', $this->request->userInfo['id']]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['l.userId', '=', $this->request->userInfo['id']]; } // 查询内容项目是否存在并检查权限 @@ -860,7 +859,7 @@ class ContentLibraryController extends Controller ->find(); - if(empty($item)) { + if (empty($item)) { return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); } @@ -887,7 +886,7 @@ class ContentLibraryController extends Controller $id = $this->request->param('id', 0); $userId = $this->request->userInfo['id']; $isAdmin = !empty($this->request->userInfo['isAdmin']); - + if (empty($id)) { return json(['code' => 400, 'msg' => '参数错误']); } @@ -897,7 +896,7 @@ class ContentLibraryController extends Controller ['i.id', '=', $id], ['i.isDel', '=', 0] ]; - + // 非管理员只能查看自己的内容项 if (!$isAdmin) { $where[] = ['l.userId', '=', $userId]; @@ -951,7 +950,7 @@ class ContentLibraryController extends Controller // 初始化发送者和群组信息 $item['senderInfo'] = []; $item['groupInfo'] = []; - + // 批量获取关联信息 if ($item['type'] == 'moment' && !empty($item['friendId'])) { // 获取朋友圈发送者信息 @@ -961,7 +960,7 @@ class ContentLibraryController extends Controller ->where('wf.id', $item['friendId']) ->field('wf.id, wf.wechatId, wa.nickname, wa.avatar') ->find(); - + if ($friendInfo) { $item['senderInfo'] = $friendInfo; } @@ -971,10 +970,10 @@ class ContentLibraryController extends Controller ->where('id', $item['wechatChatroomId']) ->field('id, chatroomId, name, avatar, ownerWechatId') ->find(); - + if ($groupInfo) { $item['groupInfo'] = $groupInfo; - + // 如果有发送者信息,也获取发送者详情 if (!empty($item['wechatId'])) { $senderInfo = Db::table('s2_wechat_chatroom_member') @@ -984,7 +983,7 @@ class ContentLibraryController extends Controller ]) ->field('wechatId, nickname, avatar') ->find(); - + if ($senderInfo) { $item['senderInfo'] = $senderInfo; } @@ -1187,9 +1186,9 @@ class ContentLibraryController extends Controller ->select()->toArray(); if (empty($libraries)) { - return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'],256); + return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'], 256); } - + $successCount = 0; $failCount = 0; $results = []; @@ -1212,13 +1211,13 @@ class ContentLibraryController extends Controller foreach ($libraries as $library) { try { $processedLibraries++; - + // 根据数据来源类型执行不同的采集逻辑 $collectResult = [ 'status' => 'skipped', 'message' => '没有配置数据来源' ]; - + switch ($library['sourceType']) { case 1: // 好友类型 if (!empty($library['sourceFriends'])) { @@ -1269,7 +1268,7 @@ class ContentLibraryController extends Controller 'status' => 'error', 'message' => $e->getMessage() ]; - + // 记录错误日志 \think\facade\Log::error('内容库采集错误: ' . $e->getMessage() . ' [库ID: ' . $library['id'] . ']'); } @@ -1286,7 +1285,7 @@ class ContentLibraryController extends Controller 'skipped' => $totalLibraries - $successCount - $failCount, 'results' => $results ] - ],256); + ], 256); } /** @@ -1303,14 +1302,14 @@ class ContentLibraryController extends Controller 'message' => '没有指定要采集的好友' ]; } - + try { // 获取API配置 $toAccountId = ''; $username = Env::get('api.username2', ''); $password = Env::get('api.password2', ''); $needFetch = false; - + // 检查是否需要主动获取朋友圈 if (!empty($username) && !empty($password)) { $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); @@ -1336,10 +1335,10 @@ class ContentLibraryController extends Controller $totalMomentsCount = 0; $processedFriends = 0; $totalFriends = count($friends); - + // 获取采集类型限制 $catchTypes = $library['catchType'] ?? []; - + foreach ($friends as $friend) { $processedFriends++; @@ -1349,16 +1348,16 @@ class ContentLibraryController extends Controller // 执行切换好友命令 $automaticAssign = new AutomaticAssign(); $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $toAccountId], true); - + // 存入缓存 $friendData = $friend; $friendData['friendId'] = $friend['id']; artificialAllotWechatFriend($friendData); - + // 执行采集朋友圈命令 $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); $webSocket->getMoments(['wechatFriendId' => $friend['id'], 'wechatAccountId' => $friend['wechatAccountId']]); - + // 采集完毕切换回原账号 $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $friend['accountId']], true); } catch (\Exception $e) { @@ -1375,17 +1374,17 @@ class ContentLibraryController extends Controller ]) ->order('createTime', 'desc') ->group('snsId'); - + // 如果启用了时间限制 if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) { $query->whereBetween('createTime', [$library['timeStart'], $library['timeEnd']]); } - + // 如果指定了采集类型,进行过滤 /*if (!empty($catchTypes)) { $query->whereIn('type', $catchTypes); }*/ - + // 获取最近20条朋友圈 $moments = $query->page(1, 100)->select(); if (empty($moments)) { @@ -1400,24 +1399,24 @@ class ContentLibraryController extends Controller foreach ($moments as $moment) { // 处理关键词过滤 $content = $moment['content'] ?? ''; - + // 应用关键词过滤 if (!$this->passKeywordFilter($content, $library['keywordInclude'], $library['keywordExclude'])) { continue; } - /* // 如果启用了AI处理 - if (!empty($library['aiEnabled']) && !empty($content)) { - try { - $contentAi = $this->aiRewrite($library, $content); - if (!empty($contentAi)) { - $moment['contentAi'] = $contentAi; - } - } catch (\Exception $e) { - \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']'); - $moment['contentAi'] = ''; - } - }*/ + /* // 如果启用了AI处理 + if (!empty($library['aiEnabled']) && !empty($content)) { + try { + $contentAi = $this->aiRewrite($library, $content); + if (!empty($contentAi)) { + $moment['contentAi'] = $contentAi; + } + } catch (\Exception $e) { + \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']'); + $moment['contentAi'] = ''; + } + }*/ // 保存到内容库的content_item表 if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) { @@ -1441,7 +1440,7 @@ class ContentLibraryController extends Controller $totalMomentsCount += $friendMomentsCount; } - + // 每处理5个好友,释放一次内存 if ($processedFriends % 5 == 0 && $processedFriends < $totalFriends) { gc_collect_cycles(); @@ -1473,7 +1472,7 @@ class ContentLibraryController extends Controller ]; } } - + /** * 应用关键词过滤规则 * @param string $content 内容文本 @@ -1487,7 +1486,7 @@ class ContentLibraryController extends Controller if (empty($content)) { return false; } - + // 检查是否包含必须关键词 $includeMatch = empty($includeKeywords); if (!empty($includeKeywords)) { @@ -1512,7 +1511,7 @@ class ContentLibraryController extends Controller } } } - + return true; // 通过所有过滤条件 } @@ -1559,7 +1558,7 @@ class ContentLibraryController extends Controller // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} // 键是群组ID,值是该群组的成员ID数组 // 需要按群组分组处理,确保每个群组只采集该群组配置的成员 - + // 建立群组ID到成员ID数组的映射 $groupIdToMemberIds = []; if (is_array($groupMembers)) { @@ -1575,7 +1574,7 @@ class ContentLibraryController extends Controller 'message' => '未找到有效的群成员ID' ]; } - + // 为每个群组查询成员信息,建立群组ID到成员wechatId数组的映射 $groupIdToMemberWechatIds = []; foreach ($groupIdToMemberIds as $groupId => $memberIds) { @@ -1584,14 +1583,14 @@ class ContentLibraryController extends Controller ->field('id, wechatId') ->whereIn('id', $memberIds) ->select(); - + $wechatIds = []; foreach ($members as $member) { if (!empty($member['wechatId'])) { $wechatIds[] = $member['wechatId']; } } - + if (!empty($wechatIds)) { $groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds); } @@ -1636,7 +1635,7 @@ class ContentLibraryController extends Controller foreach ($groupMessages as $message) { $chatroomId = $message['wechatChatroomId']; $senderWechatId = $message['senderWechatId'] ?? ''; - + // 找到对应的群组信息 $groupInfo = null; foreach ($groups as $group) { @@ -1649,20 +1648,20 @@ class ContentLibraryController extends Controller if (!$groupInfo) { continue; } - + // 检查该消息的发送者是否在该群组的配置成员列表中 $groupId = $groupInfo['id']; if (!isset($groupIdToMemberWechatIds[$groupId])) { // 该群组没有配置成员,跳过 continue; } - + // 检查发送者是否在配置的成员列表中 if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) { // 发送者不在该群组的配置成员列表中,跳过 continue; } - + if (!isset($groupedMessages[$chatroomId])) { $groupedMessages[$chatroomId] = [ 'count' => 0, @@ -2025,7 +2024,7 @@ class ContentLibraryController extends Controller // 如果不存在,则创建新的内容项目 - if(empty($exists)){ + if (empty($exists)) { $exists = new ContentItem(); } @@ -2087,7 +2086,8 @@ class ContentLibraryController extends Controller } $resUrls = []; - + $links = []; + $contentType = 4; $content = ''; switch ($message['msgType']) { case 1: // 文字 @@ -2110,34 +2110,25 @@ class ContentLibraryController extends Controller break; case 42: //名片 return false; - case 49: //文件 - $links = json_decode($message['content'],true); - return false; + case 49: //文件 链接 + $link = json_decode($message['content'], true); + switch ($link['type']) { + case 'link': + $links[] = [ + 'desc' => $link['desc'], + 'image' => $link['thumbPath'], + 'url' => $link['url'], + ]; + $contentType = 2; + break; + default: + return false; + } + break; default: return false; } - /*// 提取消息内容中的链接 - $content = $message['content'] ?? ''; - $links = []; - $pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; - preg_match_all($pattern, $content, $matches); - - if (!empty($matches[0])) { - $links = $matches[0]; - } - - // 提取可能的图片URL - $resUrls = []; - if (isset($message['imageUrl']) && !empty($message['imageUrl'])) { - $resUrls[] = $message['imageUrl']; - } - - // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) - $contentType = $this->determineContentType($content, $resUrls, $links); - */ - - // 创建新的内容项目 $item = new ContentItem(); $item->libraryId = $libraryId; @@ -2162,17 +2153,18 @@ class ContentLibraryController extends Controller if (!empty($resUrls[0])) { $item->coverImage = $resUrls[0]; } - }else{ + } else { $item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE); } // 处理链接 if (!empty($links)) { $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); - }else{ + } else { $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); } $item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE); + // 设置商品信息(需根据消息内容解析) $this->extractProductInfo($item, $content); @@ -2593,7 +2585,7 @@ class ContentLibraryController extends Controller ['companyId', '=', $companyId], ['isDel', '=', 0] ]; - + if (!$isAdmin) { $libraryWhere[] = ['userId', '=', $userId]; } @@ -2623,13 +2615,13 @@ class ContentLibraryController extends Controller // 下载远程文件到临时目录 $tmpFile = tempnam(sys_get_temp_dir(), 'excel_import_') . '.' . $urlExt; $fileContent = @file_get_contents($fileUrl); - + if ($fileContent === false) { return json(['code' => 400, 'msg' => '下载远程文件失败,请检查URL是否可访问']); } - + file_put_contents($tmpFile, $fileContent); - + } elseif ($file) { // 处理上传的文件 $ext = strtolower($file->getExtension()); @@ -2654,7 +2646,7 @@ class ContentLibraryController extends Controller // 加载Excel文件 $excel = PHPExcel_IOFactory::load($tmpFile); $sheet = $excel->getActiveSheet(); - + // 获取所有图片 $images = []; try { @@ -2663,14 +2655,14 @@ class ContentLibraryController extends Controller if ($drawing instanceof \PHPExcel_Worksheet_Drawing) { $coordinates = $drawing->getCoordinates(); $imagePath = $drawing->getPath(); - + // 如果是嵌入的图片(zip://格式),提取到临时文件 if (strpos($imagePath, 'zip://') === 0) { $zipEntry = str_replace('zip://', '', $imagePath); $zipEntry = explode('#', $zipEntry); $zipFile = $zipEntry[0]; $imageEntry = isset($zipEntry[1]) ? $zipEntry[1] : ''; - + if (!empty($imageEntry)) { $zip = new \ZipArchive(); if ($zip->open($zipFile) === true) { @@ -2691,11 +2683,11 @@ class ContentLibraryController extends Controller // 处理内存中的图片 $coordinates = $drawing->getCoordinates(); $imageResource = $drawing->getImageResource(); - + if ($imageResource) { $tempImageFile = tempnam(sys_get_temp_dir(), 'excel_img_') . '.png'; $imageType = $drawing->getMimeType(); - + switch ($imageType) { case 'image/png': imagepng($imageResource, $tempImageFile); @@ -2710,7 +2702,7 @@ class ContentLibraryController extends Controller default: imagepng($imageResource, $tempImageFile); } - + $images[$coordinates] = $tempImageFile; } } @@ -2737,7 +2729,7 @@ class ContentLibraryController extends Controller try { foreach ($data as $rowIndex => $row) { $rowNum = $rowIndex + 3; // Excel行号(从3开始,因为前两行是标题和说明) - + // 跳过空行 if (empty(array_filter($row))) { continue; @@ -2752,7 +2744,7 @@ class ContentLibraryController extends Controller $content = isset($row[3]) ? trim($row[3]) : ''; $selfReply = isset($row[4]) ? trim($row[4]) : ''; $displayForm = isset($row[5]) ? trim($row[5]) : ''; - + // 如果没有朋友圈文案,跳过 if (empty($content)) { continue; @@ -2763,11 +2755,11 @@ class ContentLibraryController extends Controller for ($colIndex = 6; $colIndex <= 14; $colIndex++) { $columnLetter = $this->columnLetter($colIndex); $cellCoordinate = $columnLetter . $rowNum; - + // 检查是否有图片 if (isset($images[$cellCoordinate])) { $imagePath = $images[$cellCoordinate]; - + // 上传图片到OSS $imageExt = 'jpg'; if (file_exists($imagePath)) { @@ -2779,10 +2771,10 @@ class ContentLibraryController extends Controller } } } - + $objectName = AliyunOSS::generateObjectName('excel_img_' . $rowNum . '_' . ($colIndex - 5) . '.' . $imageExt); $uploadResult = AliyunOSS::uploadFile($imagePath, $objectName); - + if ($uploadResult['success']) { $imageUrls[] = $uploadResult['url']; } @@ -2798,7 +2790,7 @@ class ContentLibraryController extends Controller $year = $matches[1]; $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); - + // 解析时间 $hour = 0; $minute = 0; @@ -2806,11 +2798,11 @@ class ContentLibraryController extends Controller $hour = intval($timeMatches[1]); $minute = intval($timeMatches[2]); } - + $createMomentTime = strtotime("{$year}-{$month}-{$day} {$hour}:{$minute}:00"); } } - + if ($createMomentTime == 0) { $createMomentTime = time(); } @@ -2833,12 +2825,12 @@ class ContentLibraryController extends Controller $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); $item->createMomentTime = $createMomentTime; $item->createTime = time(); - + // 设置封面图片 if (!empty($imageUrls[0])) { $item->coverImage = $imageUrls[0]; } - + // 保存其他信息到contentData $contentData = [ 'date' => $date, @@ -2848,7 +2840,7 @@ class ContentLibraryController extends Controller 'selfReply' => $selfReply ]; $item->contentData = json_encode($contentData, JSON_UNESCAPED_UNICODE); - + $item->save(); $successCount++; @@ -2885,7 +2877,7 @@ class ContentLibraryController extends Controller } catch (\Exception $e) { Db::rollback(); - + // 清理临时文件 foreach ($images as $imagePath) { if (file_exists($imagePath) && strpos($imagePath, sys_get_temp_dir()) === 0) { @@ -2895,7 +2887,7 @@ class ContentLibraryController extends Controller if (file_exists($tmpFile) && strpos($tmpFile, sys_get_temp_dir()) === 0) { @unlink($tmpFile); } - + return json(['code' => 500, 'msg' => '导入失败:' . $e->getMessage()]); } diff --git a/Server/application/job/MessageFriendsListJob.php b/Server/application/job/MessageFriendsListJob.php index 459066ca..ee7c3092 100644 --- a/Server/application/job/MessageFriendsListJob.php +++ b/Server/application/job/MessageFriendsListJob.php @@ -84,7 +84,7 @@ class MessageFriendsListJob $data = $response['data']; // 判断是否有下一页 - if (!empty($data) && count($data['results']) > 0) { + if (!empty($data) && count($data) > 0) { // 有下一页,将下一页任务添加到队列 $nextPageIndex = $pageIndex + 1; $this->addNextPageToQueue($nextPageIndex, $pageSize); From 08d2a811b7155d9d82a1520a1afba0b605ed8144 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Sun, 4 Jan 2026 15:58:47 +0800 Subject: [PATCH 15/17] =?UTF-8?q?=E8=A7=A6=E5=AE=A2=E4=BF=9D=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chukebao/controller/MomentsController.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Server/application/chukebao/controller/MomentsController.php b/Server/application/chukebao/controller/MomentsController.php index fd3ae575..f0d6a4df 100644 --- a/Server/application/chukebao/controller/MomentsController.php +++ b/Server/application/chukebao/controller/MomentsController.php @@ -28,6 +28,12 @@ class MomentsController extends BaseController $labels = $this->request->param('labels', []); // 标签列表 $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s')); // 定时发布时间 $immediately = $this->request->param('immediately', false); // 是否立即发布 + + // 格式化时间字符串为统一格式 + $timingTime = $this->normalizeTimingTime($timingTime); + if ($timingTime === false) { + return ResponseHelper::error('定时发布时间格式不正确'); + } // 参数验证 if (empty($text) && empty($picUrlList) && empty($videoUrl)) { @@ -173,6 +179,12 @@ class MomentsController extends BaseController $labels = $this->request->param('labels', []); $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s')); $immediately = $this->request->param('immediately', false); + + // 格式化时间字符串为统一格式 + $timingTime = $this->normalizeTimingTime($timingTime); + if ($timingTime === false) { + return ResponseHelper::error('定时发布时间格式不正确'); + } // 读取待编辑记录 /** @var KfMoments|null $moments */ @@ -427,4 +439,66 @@ class MomentsController extends BaseController return ResponseHelper::error('删除失败:' . $e->getMessage()); } } + + /** + * 规范化时间字符串为 Y-m-d H:i:s 格式 + * 支持多种时间格式: + * - "2026年1月5日15:43:00" + * - "2026-01-05 15:43:00" + * - "2026/01/05 15:43:00" + * - 时间戳 + * @param string|int $timingTime 时间字符串或时间戳 + * @return string|false 格式化后的时间字符串,失败返回false + */ + private function normalizeTimingTime($timingTime) + { + if (empty($timingTime)) { + return date('Y-m-d H:i:s'); + } + + // 如果是时间戳 + if (is_numeric($timingTime) && strlen($timingTime) == 10) { + return date('Y-m-d H:i:s', $timingTime); + } + + // 如果是毫秒时间戳 + if (is_numeric($timingTime) && strlen($timingTime) == 13) { + return date('Y-m-d H:i:s', intval($timingTime / 1000)); + } + + // 如果已经是标准格式,直接返回 + if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timingTime)) { + return $timingTime; + } + + // 处理中文日期格式:2026年1月5日15:43:00 或 2026年01月05日15:43:00 + if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT); + $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT); + $second = str_pad($matches[6], 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}"; + } + + // 处理中文日期格式(无秒):2026年1月5日15:43 + if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT); + $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day} {$hour}:{$minute}:00"; + } + + // 尝试使用 strtotime 解析其他格式 + $timestamp = strtotime($timingTime); + if ($timestamp !== false) { + return date('Y-m-d H:i:s', $timestamp); + } + + // 如果所有方法都失败,返回 false + return false; + } } \ No newline at end of file From b4c813311b842e395d229b1fd934c380fbd93e4b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Sun, 4 Jan 2026 17:03:51 +0800 Subject: [PATCH 16/17] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=BB=BA=E7=BE=A4?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=20=20+=20=20=E8=A7=A6=E5=AE=A2=E5=AE=9D?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E6=9C=8B=E5=8F=8B=E5=9C=88=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.module.scss | 42 + .../GroupSelectionWithMembers/index.tsx | 83 +- .../pages/mobile/mine/content/form/index.tsx | 11 +- .../plan/list/components/PoolListModal.tsx | 34 +- .../components/AddChannelModal.tsx | 2 +- .../detail/group-detail.module.scss | 598 +++ .../group-create/detail/group-detail.tsx | 675 +++ .../detail/groups-list.module.scss | 275 ++ .../group-create/detail/groups-list.tsx | 290 ++ .../group-create/detail/index.module.scss | 482 ++ .../workspace/group-create/detail/index.tsx | 401 ++ .../mobile/workspace/group-create/form/api.ts | 56 + .../form/components/BasicSettings.module.scss | 6 + .../form/components/BasicSettings.tsx | 118 +- .../form/components/DeviceSelectionStep.tsx | 24 +- .../workspace/group-create/form/index.tsx | 85 +- .../workspace/group-create/form/types.ts | 50 +- .../group-create/list/index.module.scss | 303 ++ .../workspace/group-create/list/index.tsx | 344 ++ .../src/pages/mobile/workspace/main/index.tsx | 11 + .../list/components/PoolListModal.tsx | 10 +- Cunkebao/src/router/config.ts | 4 + Cunkebao/src/router/module/workspace.tsx | 24 + .../api/controller/WebSocketController.php | 4 +- .../controller/WechatChatroomController.php | 2 +- Server/application/cunkebao/config/route.php | 43 +- .../controller/ContentLibraryController.php | 2 +- .../distribution/ChannelController.php | 6 +- .../workbench/WorkbenchAutoLikeController.php | 108 + .../workbench/WorkbenchController.php | 3353 ++++++++++++++ .../workbench/WorkbenchController_new.php | 3006 +++++++++++++ .../WorkbenchGroupCreateController.php | 3895 +++++++++++++++++ .../WorkbenchGroupPushController.php} | 1072 ++++- .../workbench/WorkbenchHelperController.php | 313 ++ .../WorkbenchImportContactController.php | 69 + .../workbench/WorkbenchMomentsController.php | 125 + .../workbench/WorkbenchTrafficController.php | 309 ++ Server/application/job/SyncContentJob.php | 12 + .../job/WorkbenchGroupCreateJob.php | 521 ++- 39 files changed, 16264 insertions(+), 504 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/groups-list.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/groups-list.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/list/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchController_new.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php rename Server/application/cunkebao/controller/{WorkbenchController.php => workbench/WorkbenchGroupPushController.php} (77%) create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchHelperController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchImportContactController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchMomentsController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchTrafficController.php diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss index 344a93ee..75e5a2ec 100644 --- a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss @@ -219,6 +219,48 @@ font-size: 16px; } +.searchBox { + padding: 12px 20px; + border-bottom: 1px solid #f0f0f0; + background: #fff; + display: flex; + align-items: center; + gap: 8px; +} + +.searchInputWrapper { + flex: 1; + position: relative; + display: flex; + align-items: center; +} + +.searchInput { + flex: 1; + background: #f5f5f5; + border-radius: 8px; + padding: 8px 12px; + font-size: 14px; +} + +.clearSearchBtn { + position: absolute; + right: 8px; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + color: #999; + display: flex; + align-items: center; + justify-content: center; +} + +.searchBtn { + min-width: 60px; + height: 32px; +} + .memberList { flex: 1; overflow-y: auto; diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx index f1f73acd..ec7114d1 100644 --- a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { SearchOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { SearchOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from "@ant-design/icons"; import { Button, Input, Popup } from "antd-mobile"; import { Avatar } from "antd-mobile"; import style from "./index.module.scss"; @@ -82,6 +82,9 @@ const GroupSelectionWithMembers: React.FC = ({ const [allMembers, setAllMembers] = useState>({}); const [selectedMembers, setSelectedMembers] = useState>({}); const [loadingMembers, setLoadingMembers] = useState(false); + const [memberSearchKeyword, setMemberSearchKeyword] = useState(""); + // 存储完整成员列表(用于搜索时切换回完整列表) + const [fullMembersCache, setFullMembersCache] = useState>({}); // 处理群选择 const handleGroupSelect = (groups: GroupSelectionItem[]) => { @@ -110,24 +113,66 @@ const GroupSelectionWithMembers: React.FC = ({ const handleOpenMemberSelection = async (groupId: string) => { if (readonly) return; setMemberSelectionVisible({ visible: true, groupId }); + setMemberSearchKeyword(""); // 重置搜索关键词 - // 如果还没有加载过该群的成员列表,则加载 - if (!allMembers[groupId]) { + // 如果还没有加载过该群的成员列表,则加载所有成员(不使用搜索关键词) + if (!allMembers[groupId] && !fullMembersCache[groupId]) { setLoadingMembers(true); try { - const members = await getGroupMembers(groupId); + const members = await getGroupMembers(groupId, 1, 100, ""); setAllMembers(prev => ({ ...prev, [groupId]: members })); + setFullMembersCache(prev => ({ ...prev, [groupId]: members })); // 缓存完整列表 } catch (error) { console.error("加载群成员失败:", error); } finally { setLoadingMembers(false); } + } else if (fullMembersCache[groupId] && !allMembers[groupId]) { + // 如果有缓存但没有显示列表,恢复完整列表 + setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] })); } }; // 关闭成员选择弹窗 const handleCloseMemberSelection = () => { setMemberSelectionVisible({ visible: false, groupId: "" }); + setMemberSearchKeyword(""); // 重置搜索关键词 + }; + + // 手动触发搜索 + const handleSearchMembers = async () => { + const groupId = memberSelectionVisible.groupId; + if (!groupId) return; + + const keyword = memberSearchKeyword.trim(); + + // 如果搜索关键词为空,使用缓存的完整列表 + if (!keyword) { + if (fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) { + setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] })); + } + return; + } + + // 有搜索关键词时,调用 API 搜索 + setLoadingMembers(true); + try { + const members = await getGroupMembers(groupId, 1, 100, keyword); + setAllMembers(prev => ({ ...prev, [groupId]: members })); + } catch (error) { + console.error("搜索群成员失败:", error); + } finally { + setLoadingMembers(false); + } + }; + + // 清空搜索 + const handleClearSearch = () => { + setMemberSearchKeyword(""); + const groupId = memberSelectionVisible.groupId; + if (groupId && fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) { + setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] })); + } }; // 选择成员 @@ -328,6 +373,36 @@ const GroupSelectionWithMembers: React.FC = ({ 完成
+
+
+ setMemberSearchKeyword(val)} + onEnterPress={handleSearchMembers} + className={style.searchInput} + /> + {memberSearchKeyword && ( + + )} +
+ +
{loadingMembers ? (
diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index 7ad12d64..6de9a90c 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -99,13 +99,18 @@ export default function ContentForm() { : []); setSelectedDevices(deviceOptions || []); setSelectedFriends(data.sourceFriends || []); - setSelectedGroups(data.selectedGroups || []); // 使用 wechatGroupsOptions 作为群列表数据 - setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []); + const groupsOptions = data.wechatGroupsOptions || data.selectedGroupsOptions || []; + setSelectedGroupsOptions(groupsOptions); + // 从 groupsOptions 中提取群 ID 列表,如果没有 selectedGroups 的话 + const groupIds = data.selectedGroups && data.selectedGroups.length > 0 + ? data.selectedGroups + : groupsOptions.map((g: any) => String(g.id)); + setSelectedGroups(groupIds); // 处理带成员的群数据 // groupMembersOptions 是一个对象,key是群ID(字符串),value是成员数组 const groupMembersMap = data.groupMembersOptions || {}; - const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map( + const groupsWithMembers: GroupWithMembers[] = groupsOptions.map( (group: any) => { const groupIdStr = String(group.id); const members = groupMembersMap[groupIdStr] || []; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx index 4871d2ba..308bd4f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx @@ -75,7 +75,7 @@ const PoolListModal: React.FC = ({ useEffect(() => { if (!visible || !ruleId) return; - setLoading(true); + setLoading(true); const params: { startTime?: string; endTime?: string } = {}; if (startTime) { params.startTime = formatDate(startTime); @@ -85,26 +85,26 @@ const PoolListModal: React.FC = ({ } getFriendRequestTaskStats(ruleId.toString(), params) - .then(res => { - console.log(res); - setXData(res.dateArray); - setYData([ - res.allNumArray, - res.errorNumArray, - res.passNumArray, - res.passRateArray, - res.successNumArray, - res.successRateArray, - ]); - setStatistics(res.totalStats); + .then(res => { + console.log(res); + setXData(res.dateArray); + setYData([ + res.allNumArray, + res.errorNumArray, + res.passNumArray, + res.passRateArray, + res.successNumArray, + res.successRateArray, + ]); + setStatistics(res.totalStats); }) .catch(error => { console.error("获取统计数据失败:", error); message.error("获取统计数据失败"); - }) - .finally(() => { - setLoading(false); - }); + }) + .finally(() => { + setLoading(false); + }); }, [visible, ruleId, startTime, endTime, formatDate]); const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据"; diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx index 26ab1b35..2863e768 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx @@ -221,7 +221,7 @@ const AddChannelModal: React.FC = ({ setQrCodeData(null); setScanning(false); const timer = setTimeout(() => { - handleGenerateQRCode(); + handleGenerateQRCode(); }, 100); return () => clearTimeout(timer); } diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss new file mode 100644 index 00000000..dd4ee0d5 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss @@ -0,0 +1,598 @@ +.container { + padding: 16px; + padding-bottom: 100px; + background: #f3f4f6; + min-height: 100vh; + position: relative; +} + +.syncOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(2px); +} + +.syncContent { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + background: #fff; + padding: 32px 40px; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.syncText { + font-size: 14px; + color: #4b5563; + font-weight: 500; +} + +.loadingContainer, +.emptyContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} + +.emptyText { + font-size: 16px; + color: #64748b; + margin-bottom: 24px; +} + +.backButton { + padding: 8px 16px; + background: #3b82f6; + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; +} + +.moreButton { + padding: 8px; + margin-right: -8px; + background: none; + border: none; + cursor: pointer; + color: #6b7280; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: color 0.2s; + + &:hover { + color: #111827; + } +} + +// 群组信息卡片 +.groupInfoCard { + background: #fff; + border-radius: 16px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: center; +} + +.groupIconLarge { + width: 80px; + height: 80px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 32px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + margin: 0 auto 16px; + object-fit: cover; + background-size: cover; + background-position: center; + font-weight: 600; + letter-spacing: 1px; +} + +.groupTitle { + font-size: 20px; + font-weight: 700; + color: #111827; + text-align: center; + margin: 0 0 4px 0; +} + +.createTimeInfo { + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; + color: #6b7280; + margin-bottom: 16px; +} + +.createTimeIcon { + font-size: 16px; +} + +.actionButtons { + display: flex; + gap: 12px; + width: 100%; +} + +.actionButton { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px; + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; + border: none; + border-radius: 12px; + font-weight: 500; + font-size: 14px; + cursor: pointer; + transition: background 0.2s; + + &:active { + background: rgba(59, 130, 246, 0.2); + } +} + +.actionButtonIcon { + font-size: 18px; +} + +// 基本信息 +.basicInfoCard { + background: #fff; + border-radius: 16px; + padding: 20px; + margin-bottom: 24px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.sectionTitle { + font-size: 16px; + font-weight: 600; + color: #111827; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.sectionTitleDot { + width: 4px; + height: 16px; + background: #3b82f6; + border-radius: 2px; +} + +.basicInfoList { + display: flex; + flex-direction: column; + gap: 0; +} + +.basicInfoItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; +} + +.basicInfoLabel { + font-size: 14px; + color: #6b7280; +} + +.basicInfoValue { + font-size: 14px; + font-weight: 500; + color: #111827; + display: flex; + align-items: center; + gap: 4px; +} + +.planIcon { + font-size: 16px; + color: #3b82f6; + margin-right: 4px; +} + +.adminAvatar { + width: 20px; + height: 20px; + border-radius: 50%; + background: #fef3c7; + color: #92400e; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + margin-right: 8px; + position: relative; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.adminAvatarText { + position: absolute; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.hidden { + display: none; +} + +.basicInfoDivider { + height: 1px; + background: #f3f4f6; +} + +.noAnnouncement { + color: #9ca3af; + display: flex; + align-items: center; +} + +.chevronIcon { + font-size: 16px; + margin-left: 4px; +} + +// 群成员 +.membersCard { + background: #fff; + border-radius: 16px; + margin-bottom: 24px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + overflow: hidden; +} + +.membersHeader { + padding: 20px; + border-bottom: 1px solid #f3f4f6; + display: flex; + justify-content: space-between; + align-items: center; +} + +.memberCountBadge { + margin-left: 8px; + font-size: 12px; + font-weight: 400; + color: #6b7280; + background: #f3f4f6; + padding: 2px 8px; + border-radius: 9999px; +} + +.searchButton { + padding: 6px; + color: #6b7280; + background: none; + border: none; + cursor: pointer; + border-radius: 8px; + transition: background 0.2s; + + &:active { + background: #f3f4f6; + } +} + +.searchBox { + padding: 12px 20px; + border-bottom: 1px solid #f3f4f6; +} + +.searchInput { + width: 100%; + padding: 8px 12px; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 14px; + outline: none; + + &:focus { + border-color: #3b82f6; + } +} + +.membersList { + max-height: 320px; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} + +.memberItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #f9fafb; + transition: background 0.2s; + + &:last-child { + border-bottom: none; + } + + &:active { + background: #f9fafb; + } +} + +.memberInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.memberInfoText { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.memberNameRow { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.memberAvatarWrapper { + position: relative; +} + +.memberAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + background: #e5e7eb; +} + +.memberAvatarPlaceholder { + width: 40px; + height: 40px; + border-radius: 50%; + background: #e9d5ff; + color: #7c3aed; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; +} + +.adminBadge { + position: absolute; + bottom: -2px; + right: -2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fbbf24; + border: 2px solid #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.adminBadgeIcon { + font-size: 10px; + color: #fff; +} + +.memberInfoText { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.memberNameRow { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.memberName { + font-size: 14px; + font-weight: 500; + color: #111827; +} + +.memberWechatId { + font-size: 12px; + color: #6b7280; +} + +.quitTag { + display: inline-flex; + align-items: center; + padding: 2px 6px; + background: #fee2e2; + color: #dc2626; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.joinStatusTag { + display: inline-flex; + align-items: center; + padding: 2px 6px; + background: #dbeafe; + color: #2563eb; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.memberExtraInfo { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #9ca3af; + flex-wrap: wrap; +} + +.divider { + color: #d1d5db; +} + +.memberJoinTime { + font-size: 12px; + color: #9ca3af; +} + +.memberActions { + display: flex; + align-items: center; +} + +.adminTag { + padding: 2px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 500; + background: #fef3c7; + color: #92400e; + border: 1px solid rgba(146, 64, 14, 0.3); +} + +.removeButton { + color: #9ca3af; + background: none; + border: none; + cursor: pointer; + font-size: 20px; + transition: color 0.2s; + + &:active { + color: #ef4444; + } +} + +.emptyMembers { + padding: 40px 20px; + text-align: center; +} + +.emptyMembersText { + font-size: 14px; + color: #9ca3af; +} + +// 操作按钮 +.actionsSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.actionCard { + background: #fff; + border-radius: 12px; + padding: 16px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + cursor: pointer; + transition: background 0.2s; + + &:active { + background: #f9fafb; + } +} + +.actionCardIcon { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f3f4f6; + color: #4b5563; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: all 0.2s; +} + +.actionCard:hover .actionCardIcon { + background: #dbeafe; + color: #3b82f6; +} + +.actionCardText { + font-size: 14px; + font-weight: 500; + color: #111827; +} + +.actionCardDanger { + &:active { + background: #fef2f2; + } +} + +.actionCardIconDanger { + background: #fee2e2; + color: #ef4444; +} + +.actionCardTextDanger { + color: #dc2626; +} diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx new file mode 100644 index 00000000..c70a1d73 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx @@ -0,0 +1,675 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Toast, SpinLoading, Dialog, Input, TextArea } from "antd-mobile"; +import { + EditOutlined, + QrcodeOutlined, + SearchOutlined, + SyncOutlined, + LogoutOutlined, + StarOutlined, + CloseCircleOutlined, + ScheduleOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { getCreatedGroupDetail, syncGroupInfo, modifyGroupInfo, quitGroup } from "../form/api"; +import style from "./group-detail.module.scss"; + +interface GroupMember { + id?: string; + friendId?: number; + nickname?: string; + wechatId?: string; + avatar?: string; + isOwner?: number; // 1表示是群主,0表示不是 + isGroupAdmin?: boolean; + joinStatus?: string; // "auto" | "manual" - 入群状态 + isQuit?: number; // 0/1 - 是否已退群 + joinTime?: string; // 入群时间 + alias?: string; // 成员别名 + remark?: string; // 成员备注 + [key: string]: any; +} + +interface GroupDetail { + id: string; + name: string; + createTime?: string; + planName?: string; + groupAdmin?: { + id: string; + nickname?: string; + wechatId?: string; + avatar?: string; + }; + announcement?: string; + memberCount?: number; + members?: GroupMember[]; + [key: string]: any; +} + +const GroupDetailPage: React.FC = () => { + const { id, groupId } = useParams(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [groupDetail, setGroupDetail] = useState(null); + const [searchKeyword, setSearchKeyword] = useState(""); + const [showSearch, setShowSearch] = useState(false); + const [syncing, setSyncing] = useState(false); + const [editNameVisible, setEditNameVisible] = useState(false); + const [editNameValue, setEditNameValue] = useState(""); + const [editAnnouncementVisible, setEditAnnouncementVisible] = useState(false); + const [editAnnouncementValue, setEditAnnouncementValue] = useState(""); + + useEffect(() => { + if (!id || !groupId) return; + fetchGroupDetail(); + }, [id, groupId]); + + const fetchGroupDetail = async () => { + if (!id || !groupId) return; + setLoading(true); + try { + const res = await getCreatedGroupDetail({ + workbenchId: id, + groupId: groupId, + }); + const data = res?.data || res || {}; + + // 处理成员数据,确保有id字段 + const members = (data.members || []).map((member: any) => ({ + ...member, + id: String(member.friendId || member.id || member.wechatId || ''), + })); + + const detailData: GroupDetail = { + id: String(data.id || groupId), + name: data.groupName || data.name || "未命名群组", + createTime: data.createTime || "", + planName: data.workbenchName || data.planName || "", + groupAdmin: data.ownerWechatId || data.ownerNickname + ? { + id: String(data.ownerWechatId || ""), + nickname: data.ownerNickname, + wechatId: data.ownerWechatId, + avatar: data.ownerAvatar, + } + : undefined, + announcement: data.announce || data.announcement, // 接口返回的字段是 announce + memberCount: data.memberCount || members.length || 0, + members: members, + avatar: data.avatar || data.groupAvatar, + groupAvatar: data.groupAvatar, + }; + + setGroupDetail(detailData); + } catch (e: any) { + Toast.show({ content: e?.message || "获取群组详情失败", position: "top" }); + navigate(-1); + } finally { + setLoading(false); + } + }; + + const handleEditName = () => { + if (!groupDetail) return; + setEditNameValue(groupDetail.name); + setEditNameVisible(true); + }; + + const handleConfirmEditName = async () => { + if (!id || !groupId || !editNameValue.trim()) { + Toast.show({ content: "群名称不能为空", position: "top" }); + return; + } + + try { + await modifyGroupInfo({ + workbenchId: id, + groupId: groupId, + chatroomName: editNameValue.trim(), + }); + Toast.show({ content: "修改成功", position: "top" }); + setEditNameVisible(false); + // 刷新群详情 + fetchGroupDetail(); + } catch (e: any) { + Toast.show({ + content: e?.message || "修改群名称失败", + position: "top" + }); + } + }; + + const handleEditAnnouncement = () => { + if (!groupDetail) return; + setEditAnnouncementValue(groupDetail.announcement || ""); + setEditAnnouncementVisible(true); + }; + + const handleConfirmEditAnnouncement = async () => { + if (!id || !groupId) { + return; + } + + try { + await modifyGroupInfo({ + workbenchId: id, + groupId: groupId, + announce: editAnnouncementValue.trim() || undefined, + }); + Toast.show({ content: "修改成功", position: "top" }); + setEditAnnouncementVisible(false); + setEditAnnouncementValue(""); + // 刷新群详情 + fetchGroupDetail(); + } catch (e: any) { + Toast.show({ + content: e?.message || "修改群公告失败", + position: "top" + }); + } + }; + + const handleShowQRCode = () => { + Toast.show({ content: "群二维码功能待实现", position: "top" }); + }; + + const handleSyncGroup = async () => { + if (!id || !groupId) { + Toast.show({ content: "参数错误", position: "top" }); + return; + } + + setSyncing(true); + try { + const res = await syncGroupInfo({ + workbenchId: id, + groupId: groupId, + }); + const data = res?.data || res || {}; + + const successMessages: string[] = []; + if (data.groupInfoSynced) { + successMessages.push("群信息同步成功"); + } + if (data.memberInfoSynced) { + successMessages.push("群成员信息同步成功"); + } + + if (successMessages.length > 0) { + Toast.show({ + content: successMessages.join(","), + position: "top" + }); + // 同步成功后刷新群详情 + await fetchGroupDetail(); + } else { + Toast.show({ + content: res?.msg || "同步完成", + position: "top" + }); + } + } catch (e: any) { + Toast.show({ + content: e?.message || "同步群信息失败", + position: "top" + }); + } finally { + setSyncing(false); + } + }; + + const handleDisbandGroup = () => { + Dialog.confirm({ + content: "确定要退出该群组吗?", + confirmText: "确定", + cancelText: "取消", + onConfirm: async () => { + if (!id || !groupId) { + Toast.show({ content: "参数错误", position: "top" }); + return; + } + + try { + await quitGroup({ + workbenchId: id, + groupId: groupId, + }); + Toast.show({ content: "退出群组成功", position: "top" }); + // 退出成功后返回上一页 + navigate(-1); + } catch (e: any) { + Toast.show({ + content: e?.message || "退出群组失败", + position: "top" + }); + } + }, + }); + }; + + const handleRemoveMember = (memberId: string) => { + Dialog.confirm({ + content: "确定要移除此成员吗?", + confirmText: "确定", + cancelText: "取消", + onConfirm: () => { + Toast.show({ content: "移除成员功能待实现", position: "top" }); + }, + }); + }; + + // 过滤成员 + const filteredMembers = groupDetail?.members?.filter((member) => { + if (!searchKeyword) return true; + const keyword = searchKeyword.toLowerCase(); + return ( + member.nickname?.toLowerCase().includes(keyword) || + member.wechatId?.toLowerCase().includes(keyword) + ); + }) || []; + + // 群组图标颜色 + const iconColors = { + from: "#3b82f6", + to: "#4f46e5", + }; + + if (loading) { + return ( + navigate(-1)} />} + > +
+ +
+
+ ); + } + + if (!groupDetail) { + return ( + navigate(-1)} />} + > +
+
未找到该群组
+ +
+
+ ); + } + + return ( + navigate(-1)} + /> + } + > +
+ {/* 同步遮罩层 */} + {syncing && ( +
+
+ +
同步中...
+
+
+ )} + {/* 群组信息卡片 */} +
+ {groupDetail.avatar || groupDetail.groupAvatar ? ( + {groupDetail.name { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const fallback = target.nextElementSibling as HTMLElement; + if (fallback) { + fallback.style.display = 'flex'; + } + }} + /> + ) : null} +
+ {(groupDetail.name || '').charAt(0) || '👥'} +
+

{groupDetail.name}

+
+ + 创建于 {groupDetail.createTime || "-"} +
+
+ + {/* 群二维码功能暂时隐藏 */} + {/* */} +
+
+ + {/* 基本信息 */} +
+

+ + 基本信息 +

+
+
+ 所属计划 +
+ 📋 + {groupDetail.planName || "-"} +
+
+
+
+ 群主 +
+ {groupDetail.groupAdmin ? ( + <> + + {groupDetail.groupAdmin.avatar ? ( + {groupDetail.groupAdmin.nickname { + (e.target as HTMLImageElement).style.display = "none"; + (e.target as HTMLImageElement).nextElementSibling?.classList.remove( + style.hidden + ); + }} + /> + ) : null} + + {groupDetail.groupAdmin.nickname?.charAt(0)?.toUpperCase() || "A"} + + + {groupDetail.groupAdmin.wechatId || "-"} + + ) : ( + "-" + )} +
+
+
+
+ 群公告 +
+ {groupDetail.announcement ? ( + groupDetail.announcement + ) : ( + + 未设置 + + + )} +
+
+
+
+ + {/* 群成员 */} +
+
+

+ + 群成员 + + {groupDetail.memberCount || 0}人 + +

+ +
+ + {showSearch && ( +
+ setSearchKeyword(e.target.value)} + /> +
+ )} + +
+ {filteredMembers.length > 0 ? ( + filteredMembers.map((member) => { + const isGroupAdmin = member.isOwner === 1 || member.isGroupAdmin || member.id === groupDetail.groupAdmin?.id; + return ( +
+
+
+ {member.avatar ? ( + {member.nickname { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + ) : ( +
+ {member.nickname?.charAt(0)?.toUpperCase() || "?"} +
+ )} + {isGroupAdmin && ( +
+ +
+ )} +
+
+
+ + {member.nickname || "-"} + + {member.isQuit === 1 && ( + 已退群 + )} + {member.joinStatus && ( + + {member.joinStatus === "auto" ? "自动" : "手动"} + + )} +
+
+ {member.wechatId || "-"} +
+ {(member.alias || member.remark) && ( +
+ {member.alias && 别名:{member.alias}} + {member.alias && member.remark && |} + {member.remark && 备注:{member.remark}} +
+ )} + {member.joinTime && ( +
+ 入群时间:{member.joinTime} +
+ )} +
+
+
+ {isGroupAdmin ? ( + 群主 + ) : ( + // 移除成员功能暂时隐藏 + // + null + )} +
+
+ ); + }) + ) : ( +
+
+ {searchKeyword ? "未找到匹配的成员" : "暂无成员"} +
+
+ )} +
+
+ + {/* 操作按钮 */} +
+ + +
+
+ + {/* 修改群名称弹窗 */} + +
+ 群名称 +
+ setEditNameValue(val)} + style={{ + fontSize: "14px", + padding: "10px 12px", + border: "1px solid #e5e5e5", + borderRadius: "6px", + backgroundColor: "#fff", + }} + maxLength={30} + /> +
+ } + closeOnAction + onClose={() => { + setEditNameVisible(false); + setEditNameValue(""); + }} + actions={[ + { + key: "cancel", + text: "取消", + onClick: () => { + setEditNameVisible(false); + setEditNameValue(""); + }, + }, + { + key: "confirm", + text: "确定", + primary: true, + onClick: handleConfirmEditName, + }, + ]} + /> + + {/* 修改群公告弹窗 */} + +
+ 群公告 +
+