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()); + } + } + + +} +