diff --git a/.gitignore b/.gitignore index df8964e0..fca0d36b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ Store_vue/node_modules/ Cunkebao/.specstory/ *.cursorindexingignore Server/.specstory/ +Server/thinkphp/ Store_vue/.specstory/ Store_vue/unpackage/ Store_vue/.vscode/ SuperAdmin/.specstory/ Cunkebao/dist Touchkebao/.specstory/ +Serverruntime/ diff --git a/Cunkebao/pnpm-lock.yaml b/Cunkebao/pnpm-lock.yaml index 818ed7d7..99f410fa 100644 --- a/Cunkebao/pnpm-lock.yaml +++ b/Cunkebao/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 - dexie: - specifier: ^4.2.0 - version: 4.2.0 echarts: specifier: ^5.6.0 version: 5.6.0 @@ -1070,9 +1067,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - dexie@4.2.0: - resolution: {integrity: sha512-OSeyyWOUetDy9oFWeddJgi83OnRA3hSFh3RrbltmPgqHszE9f24eUCVLI4mPg0ifsWk0lQTdnS+jyGNrPMvhDA==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3410,8 +3404,6 @@ snapshots: detect-libc@1.0.3: optional: true - dexie@4.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 diff --git a/Server/application/common.php b/Server/application/common.php index 37a12739..8785f12c 100644 --- a/Server/application/common.php +++ b/Server/application/common.php @@ -75,7 +75,18 @@ if (!function_exists('requestCurl')) { if (!function_exists('dataBuild')) { function dataBuild($array) { - return is_array($array) ? http_build_query($array) : $array; + if (!is_array($array)) { + return $array; + } + + // 处理嵌套数组 + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = json_encode($value); + } + } + + return http_build_query($array); } } @@ -550,14 +561,15 @@ if (!function_exists('exit_data')) { exit(); } } - -/** - * 调试打印变量但不终止程序 - * @return void - */ -function dump() -{ - call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args()); +if (!function_exists('dump')) { + /** + * 调试打印变量但不终止程序 + * @return void + */ + function dump() + { + call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args()); + } } if (!function_exists('artificialAllotWechatFriend')) { diff --git a/Server/application/http/middleware/Jwt.php b/Server/application/http/middleware/Jwt.php new file mode 100644 index 00000000..4f2fb7d1 --- /dev/null +++ b/Server/application/http/middleware/Jwt.php @@ -0,0 +1,49 @@ + 401, + 'msg' => '未授权访问,缺少有效的身份凭证', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + $payload = JwtUtil::verifyToken($token); + if (!$payload) { + return json([ + 'code' => 401, + 'msg' => '授权已过期或无效', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + // 将用户信息附加到请求中 + $request->userInfo = $payload; + + // 写入日志 + Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']); + + return $next($request); + } +} diff --git a/Server/composer.json b/Server/composer.json index 61d3fa7c..5bb827df 100644 --- a/Server/composer.json +++ b/Server/composer.json @@ -13,21 +13,59 @@ { "name": "liu21st", "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" } ], "require": { - "php": ">=5.4.0", - "topthink/framework": "5.0.*" + "php": ">=5.6.0", + "topthink/framework": "5.1.*", + "topthink/think-installer": "~1.0", + "topthink/think-captcha": "^2.0", + "topthink/think-helper": "^3.0", + "topthink/think-image": "^1.0", + "topthink/think-queue": "^2.0", + "topthink/think-worker": "^2.0", + "textalk/websocket": "^1.2", + "aliyuncs/oss-sdk-php": "^2.3", + "monolog/monolog": "^1.24", + "guzzlehttp/guzzle": "^6.3", + "overtrue/wechat": "~4.0", + "endroid/qr-code": "^3.5", + "phpoffice/phpspreadsheet": "^1.8", + "workerman/workerman": "^3.5", + "workerman/gateway-worker": "^3.0", + "hashids/hashids": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "lizhichao/word": "^2.0", + "adbario/php-dot-notation": "^2.2" + }, + "require-dev": { + "symfony/var-dumper": "^3.4", + "topthink/think-migration": "^2.0" }, "autoload": { "psr-4": { - "app\\": "application" - } + "app\\": "application", + "Eison\\": "extend/Eison" + }, + "files": [ + "application/common.php" + ], + "classmap": [] }, "extra": { "think-path": "thinkphp" }, "config": { - "preferred-install": "dist" - } + "preferred-install": "dist", + "allow-plugins": { + "topthink/think-installer": true, + "easywechat-composer/easywechat-composer": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/Server/config/middleware.php b/Server/config/middleware.php index 132ab645..b221b8a1 100644 --- a/Server/config/middleware.php +++ b/Server/config/middleware.php @@ -23,7 +23,8 @@ return [ // 全局中间件 'alias' => [ - 'cors' => 'app\\common\\middleware\\AllowCrossDomain' + 'cors' => 'app\\common\\middleware\\AllowCrossDomain', + 'jwt' => 'app\\http\\middleware\\Jwt' ], // 应用中间件 diff --git a/Server/extend/Eison/Utils/Helper/ArrHelper.php b/Server/extend/Eison/Utils/Helper/ArrHelper.php new file mode 100644 index 00000000..a659c82d --- /dev/null +++ b/Server/extend/Eison/Utils/Helper/ArrHelper.php @@ -0,0 +1,125 @@ + = ({ title = "触客宝" }) => { const [messageList, setMessageList] = useState([]); const [messageCount, setMessageCount] = useState(0); const [loading, setLoading] = useState(false); + const [clearingCache, setClearingCache] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useUserStore(); @@ -72,11 +74,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/weChat"); } }; - - const isWeChat = () => { - return location.pathname.startsWith("/pc/weChat"); - }; - // 定时器获取消息条数 const IntervalMessageCount = async () => { try { @@ -125,6 +122,99 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/login"); // 跳转到登录页面 }; + // 清除所有 IndexedDB 数据库 + const clearAllIndexedDB = async (): Promise => { + return new Promise((resolve, reject) => { + if (!window.indexedDB) { + resolve(); + return; + } + + // 获取所有数据库名称 + const databases: string[] = []; + const request = indexedDB.databases(); + + request + .then(dbs => { + dbs.forEach(db => { + if (db.name) { + databases.push(db.name); + } + }); + + // 删除所有数据库 + const deletePromises = databases.map(dbName => { + return new Promise((resolveDelete, rejectDelete) => { + const deleteRequest = indexedDB.deleteDatabase(dbName); + deleteRequest.onsuccess = () => { + resolveDelete(); + }; + deleteRequest.onerror = () => { + rejectDelete(new Error(`删除数据库 ${dbName} 失败`)); + }; + deleteRequest.onblocked = () => { + // 如果数据库被阻塞,等待一下再重试 + setTimeout(() => { + const retryRequest = indexedDB.deleteDatabase(dbName); + retryRequest.onsuccess = () => resolveDelete(); + retryRequest.onerror = () => + rejectDelete(new Error(`删除数据库 ${dbName} 失败`)); + }, 100); + }; + }); + }); + + Promise.all(deletePromises) + .then(() => resolve()) + .catch(reject); + }) + .catch(reject); + }); + }; + + // 处理清除缓存 + const handleClearCache = async () => { + try { + setClearingCache(true); + const hideLoading = message.loading("正在清除缓存...", 0); + + // 1. 清除所有 localStorage + try { + localStorage.clear(); + } catch (error) { + console.warn("清除 localStorage 失败:", error); + } + + // 2. 清除所有 sessionStorage + try { + sessionStorage.clear(); + } catch (error) { + console.warn("清除 sessionStorage 失败:", error); + } + + // 3. 清除所有 IndexedDB 数据库 + try { + await clearAllIndexedDB(); + } catch (error) { + console.warn("清除 IndexedDB 失败:", error); + } + + hideLoading(); + message.success("缓存清除成功"); + + // 清除成功后跳转到登录页面 + setTimeout(() => { + logout(); + navigate("/login"); + }, 500); + } catch (error) { + console.error("清除缓存失败:", error); + message.error("清除缓存失败,请稍后重试"); + } finally { + setClearingCache(false); + } + }; + // 处理消息已读 const handleReadMessage = async (messageId: number) => { try { @@ -199,6 +289,13 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/commonConfig"); }, }, + { + key: "clearCache", + icon: , + label: clearingCache ? "清除缓存中..." : "清除缓存", + onClick: handleClearCache, + disabled: clearingCache, + }, { key: "logout", icon: , diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts index 34fa4bb4..5cc9147a 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts @@ -1,6 +1,9 @@ import request from "@/api/request"; +import type { CreatePushTaskPayload } from "./types"; -// 获取客服列表 -export function queryWorkbenchCreate(params) { +// 创建推送任务 +export function queryWorkbenchCreate( + params: CreatePushTaskPayload, +): Promise { return request("/v1/workbench/create", params, "POST"); } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss new file mode 100644 index 00000000..5b3802fd --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss @@ -0,0 +1,92 @@ +.stepContent { + .step4Content { + display: flex; + flex-direction: column; + gap: 24px; + max-width: 600px; + } + + .settingsPanel { + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 20px; + background: #fafafa; + + .settingsTitle { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 16px; + } + + .settingItem { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + .settingLabel { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + + .settingControl { + display: flex; + align-items: center; + gap: 8px; + + span { + font-size: 14px; + color: #666; + min-width: 80px; + } + } + } + } + + .tagSection { + .settingLabel { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + } + + .pushPreview { + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 20px; + background: #f0f7ff; + + .previewTitle { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + + li { + font-size: 14px; + color: #666; + line-height: 1.8; + } + } + } +} + +@media (max-width: 768px) { + .stepContent { + .step4Content { + max-width: 100%; + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx new file mode 100644 index 00000000..f15f6411 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React from "react"; +import { Select, Slider } from "antd"; +import styles from "./index.module.scss"; + +interface StepPushParamsProps { + selectedAccounts: any[]; + selectedContacts: any[]; + targetLabel: string; + friendInterval: [number, number]; + onFriendIntervalChange: (value: [number, number]) => void; + messageInterval: [number, number]; + onMessageIntervalChange: (value: [number, number]) => void; + selectedTag: string; + onSelectedTagChange: (value: string) => void; + savedScriptGroups: any[]; +} + +const StepPushParams: React.FC = ({ + selectedAccounts, + selectedContacts, + targetLabel, + friendInterval, + onFriendIntervalChange, + messageInterval, + onMessageIntervalChange, + selectedTag, + onSelectedTagChange, + savedScriptGroups, +}) => { + return ( +
+
+
+
相关设置
+
+
好友间间隔
+
+ 间隔时间(秒) + + onFriendIntervalChange(value as [number, number]) + } + style={{ flex: 1, margin: "0 16px" }} + /> + + {friendInterval[0]} - {friendInterval[1]} + +
+
+
+
消息间间隔
+
+ 间隔时间(秒) + + onMessageIntervalChange(value as [number, number]) + } + style={{ flex: 1, margin: "0 16px" }} + /> + + {messageInterval[0]} - {messageInterval[1]} + +
+
+
+ +
+
完成打标签
+ +
+ +
+
推送预览
+
    +
  • 推送账号: {selectedAccounts.length}个
  • +
  • + 推送{targetLabel}: {selectedContacts.length}个 +
  • +
  • 话术组数: {savedScriptGroups.length}个
  • +
  • 随机推送: 否
  • +
  • 预计耗时: ~1分钟
  • +
+
+
+
+ ); +}; + +export default StepPushParams; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx index baadd6b4..08b8fa38 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx @@ -6,6 +6,7 @@ import { EmojiPicker } from "@/components/EmojiSeclection"; import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji"; import SimpleFileUpload from "@/components/Upload/SimpleFileUpload"; import AudioRecorder from "@/components/Upload/AudioRecorder"; +import type { MessageItem } from "../../../types"; import styles from "./index.module.scss"; @@ -17,6 +18,7 @@ interface InputMessageProps { defaultValue?: string; onContentChange?: (value: string) => void; onSend?: (value: string) => void; + onAddMessage?: (message: MessageItem) => void; // 新增:支持添加非文本消息 clearOnSend?: boolean; placeholder?: string; hint?: React.ReactNode; @@ -68,6 +70,7 @@ const InputMessage: React.FC = ({ defaultValue = "", onContentChange, onSend, + onAddMessage, clearOnSend = false, placeholder = "输入消息...", hint, @@ -169,9 +172,44 @@ const InputMessage: React.FC = ({ msgType, content, }); - antdMessage.success("附件上传成功,可在推送时使用"); + + // 如果提供了 onAddMessage 回调,则添加到消息列表 + if (onAddMessage) { + let messageItem: MessageItem; + if ([FileType.IMAGE].includes(fileType)) { + messageItem = { + type: "image", + content: filePath.url, + fileName: filePath.name, + }; + } else if ([FileType.AUDIO].includes(fileType)) { + messageItem = { + type: "audio", + content: filePath.url, + fileName: filePath.name, + durationMs: filePath.durationMs, + }; + } else if ([FileType.FILE].includes(fileType)) { + messageItem = { + type: "file", + content: filePath.url, + fileName: filePath.name, + }; + } else { + // 默认作为文本处理 + messageItem = { + type: "text", + content: filePath.url, + fileName: filePath.name, + }; + } + onAddMessage(messageItem); + antdMessage.success("已添加消息内容"); + } else { + antdMessage.success("附件上传成功,可在推送时使用"); + } }, - [], + [onAddMessage], ); const handleAudioUploaded = useCallback( diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss index 2fe9b07f..f7c7d1a4 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss @@ -19,28 +19,26 @@ .step3Content { display: flex; + flex-direction: row; gap: 24px; - align-items: flex-start; .leftColumn { flex: 1; display: flex; flex-direction: column; gap: 20px; + min-width: 0; } .rightColumn { - width: 400px; flex: 1; display: flex; flex-direction: column; gap: 20px; + flex-shrink: 0; } .previewHeader { - display: flex; - justify-content: space-between; - .previewHeaderTitle { font-size: 16px; font-weight: 600; @@ -49,78 +47,178 @@ } .messagePreview { - border: 2px dashed #52c41a; + border: 1px solid #e8e8e8; border-radius: 8px; - padding: 15px; + padding: 16px; + background: #f5f5f5; + height: 400px; + overflow-y: auto; - .messageBubble { - min-height: 100px; - background: #fff; - border-radius: 6px; - color: #666; + .messagePlaceholder { + color: #999; font-size: 14px; - line-height: 1.6; - - .currentEditingLabel { - font-size: 14px; - color: #52c41a; - font-weight: bold; - margin-bottom: 12px; - } - - .messageText { - color: #1a1a1a; - white-space: pre-wrap; - word-break: break-word; - } - - .messagePlaceholder { - color: #999; - font-size: 14px; - } - - .messageList { - display: flex; - flex-direction: column; - gap: 0; - } - - .messageItem { - display: flex; - gap: 12px; - align-items: center; - justify-content: space-between; - padding: 10px 0; - border-bottom: 1px solid #f0f0f0; - - &:last-child { - border-bottom: none; - } - - .messageText { - flex: 1; - } - - .messageAction { - color: #ff4d4f; - padding: 0; - } - } + text-align: center; + padding: 40px 20px; } - .scriptNameInput { - margin-top: 12px; + .messageList { + display: flex; + flex-direction: column; + gap: 12px; + } + + .messageBubbleWrapper { + display: flex; + flex-direction: column; + } + + .messageBubble { + display: flex; + gap: 10px; + align-items: flex-start; + + .messageAvatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; + flex-shrink: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .messageContent { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + + .messageBubbleInner { + background: #fff; + border-radius: 8px; + padding: 10px 14px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + position: relative; + max-width: 100%; + + &::before { + content: ""; + position: absolute; + left: -6px; + top: 12px; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid #fff; + } + + .messageText { + color: #1a1a1a; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.6; + font-size: 14px; + } + + .messageMedia { + display: flex; + flex-direction: column; + gap: 8px; + + .messageMediaIcon { + font-size: 24px; + color: #1890ff; + margin-bottom: 4px; + } + + .messageImage { + max-width: 100%; + max-height: 200px; + border-radius: 4px; + object-fit: contain; + background: #f5f5f5; + } + + .messageFileInfo { + display: flex; + flex-direction: column; + gap: 4px; + } + + .messageFileName { + color: #1a1a1a; + font-size: 14px; + font-weight: 500; + word-break: break-all; + } + + .messageFileSize { + color: #999; + font-size: 12px; + } + } + } + + .messageActions { + display: flex; + gap: 8px; + justify-content: flex-start; + padding-left: 4px; + opacity: 0.7; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } + + .aiRewriteButton { + color: #1890ff; + padding: 0; + font-size: 12px; + height: auto; + line-height: 1.5; + + &:hover { + color: #40a9ff; + } + } + + .messageAction { + color: #ff4d4f; + padding: 0; + font-size: 12px; + height: auto; + line-height: 1.5; + + &:hover { + color: #ff7875; + } + } + } + } } } - .savedScriptGroups { - .contentLibrarySelector { - margin-bottom: 20px; - padding: 16px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; + .pushContentHeader { + .pushContentTitle { + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; } + } + + .contentLibrarySelector { + margin-bottom: 20px; + padding: 16px; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 8px; .contentLibraryHeader { display: flex; @@ -139,7 +237,9 @@ font-size: 12px; color: #999; } + } + .savedScriptGroups { .scriptGroupHeaderRow { display: flex; align-items: center; @@ -160,7 +260,7 @@ } .scriptGroupList { - max-height: 260px; + max-height: calc(100vh - 400px); overflow-y: auto; } @@ -241,132 +341,358 @@ } } - .messageInputArea { - .messageInput { - margin-bottom: 12px; - } + .scriptNameInput { + margin-top: 0; + } - .attachmentButtons { - display: flex; - gap: 8px; - margin-bottom: 12px; - } + .createScriptGroupButton { + margin-top: 0; + } - .aiRewriteSection { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - gap: 12px; - - .aiRewriteToggle { - display: flex; - align-items: center; - gap: 8px; + // AI改写弹窗样式 + .aiRewriteModalWrap { + :global { + .ant-modal { + border-radius: 12px; + overflow: hidden; } - .aiRewriteLabel { - font-size: 14px; - color: #1a1a1a; - } - - .aiRewriteInput { - max-width: 240px; - } - - .aiRewriteActions { - display: flex; - align-items: center; - gap: 8px; - } - - .aiRewriteButton { - min-width: 96px; + .ant-modal-content { + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); } } } - .settingsPanel { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #fafafa; - - .settingsTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 16px; - } - - .settingItem { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; + .aiRewriteModal { + :global { + .ant-modal-header { + border-bottom: 1px solid #f0f0f0; + padding: 20px 24px; + background: linear-gradient(135deg, #fff 0%, #fafafa 100%); + border-radius: 12px 12px 0 0; } - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .ant-modal-body { + padding: 24px; + background: #ffffff; + + // 确保内容区域的样式能够正确应用 + // 原文消息内容区域 + [class*="aiRewriteModalOriginalText"] { + padding: 20px !important; + background: #f5f5f5 !important; + min-height: 80px !important; + } + + // 改写提示词输入框 + [class*="aiRewriteModalTextArea"] { + textarea.ant-input { + background: #f5f5f5 !important; + min-height: 80px !important; + } + } } - .settingControl { + .ant-modal-footer { + border-top: 1px solid #f0f0f0; + padding: 16px 24px; + background: #fafafa; display: flex; - align-items: center; - gap: 8px; + justify-content: flex-end; + gap: 12px; + border-radius: 0 0 12px 12px; + } - span { - font-size: 14px; - color: #666; - min-width: 80px; + .ant-modal-close { + color: #8c8c8c; + transition: color 0.3s; + top: 20px; + right: 24px; + + &:hover { + color: #1a1a1a; } } } } - .tagSection { - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .aiRewriteModalTitle { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + + .aiRewriteModalTitleIcon { + font-size: 20px; } } - .pushPreview { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #f0f7ff; + .aiRewriteModalContent { + display: flex; + flex-direction: column; + gap: 24px; + } - .previewTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .aiRewriteModalCompareSection { + display: flex; + flex-direction: column; + gap: 24px; + padding: 24px; + background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%); + border-radius: 12px; + border: 1px solid #e8e8e8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + } + + .aiRewriteModalDivider { + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + #d9d9d9 20%, + #d9d9d9 80%, + transparent 100% + ); + margin: 12px 0; + position: relative; + + &::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 1px; + background: #1890ff; } - ul { - list-style: none; - padding: 0; - margin: 0; + &::after { + content: "→"; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: #fafafa; + padding: 0 8px; + color: #1890ff; + font-size: 12px; + } + } - li { + .aiRewriteModalSection { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 0; + } + + .aiRewriteModalSectionHeader { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; + + .aiRewriteModalSectionIcon { + font-size: 18px; + line-height: 1; + } + + .aiRewriteModalLabel { + font-size: 15px; + font-weight: 600; + color: #1a1a1a; + letter-spacing: 0.3px; + } + } + + .aiRewriteModalTextArea { + :global { + textarea.ant-input { + border-radius: 8px; + border: 1px solid #d9d9d9; + transition: all 0.3s; font-size: 14px; - color: #666; - line-height: 1.8; + padding: 12px; + background: #f5f5f5 !important; + min-height: 80px !important; + + &:hover { + border-color: #40a9ff; + background: #fafafa !important; + } + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); + background: #ffffff !important; + } } } } -} -@media (max-width: 1200px) { - .step3Content { - .rightColumn { - width: 350px; + .aiRewriteModalOriginalText { + padding: 20px !important; + background: #f5f5f5 !important; + border: 1px solid #d9d9d9; + border-left: 4px solid #8c8c8c; + border-radius: 8px; + font-size: 14px; + color: #333; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; + position: relative; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); + margin-top: 4px; + transition: all 0.3s ease; + min-height: 80px !important; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + border-color: #bfbfbf; + background: #fafafa !important; + } + } + + .aiRewriteModalLoading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 20px; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + border: 1px solid #bae6fd; + border-radius: 8px; + gap: 12px; + + .aiRewriteModalLoadingIcon { + font-size: 32px; + animation: pulse 1.5s ease-in-out infinite; + } + + .aiRewriteModalLoadingText { + color: #1890ff; + font-size: 14px; + font-weight: 500; + } + } + + @keyframes pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.05); + } + } + + .aiRewriteModalResultText { + padding: 20px; + background: #f0f9ff; + border: 1px solid #91d5ff; + border-left: 4px solid #1890ff; + border-radius: 8px; + font-size: 14px; + color: #1a1a1a; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); + position: relative; + margin-top: 4px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: #bfbfbf; + border-radius: 3px; + + &:hover { + background: #999; + } + } + } + + .aiRewriteExecuteButton { + background-color: #ff6b35 !important; + border-color: #ff6b35 !important; + font-weight: 500; + height: 36px; + padding: 0 20px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2); + transition: all 0.3s ease; + + &:hover:not(:disabled) { + background-color: #ff5722 !important; + border-color: #ff5722 !important; + box-shadow: 0 4px 8px rgba(255, 107, 53, 0.3); + transform: translateY(-1px); + } + + &:active:not(:disabled) { + background-color: #e64a19 !important; + border-color: #e64a19 !important; + transform: translateY(0); + box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2); + } + + &:disabled { + background-color: #d9d9d9 !important; + border-color: #d9d9d9 !important; + color: #bfbfbf !important; + box-shadow: none; + } + } + + .confirmButton { + background-color: #07c160 !important; + border-color: #07c160 !important; + font-weight: 500; + height: 36px; + padding: 0 20px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2); + transition: all 0.3s ease; + + &:hover:not(:disabled) { + background-color: #06ad56 !important; + border-color: #06ad56 !important; + box-shadow: 0 4px 8px rgba(7, 193, 96, 0.3); + transform: translateY(-1px); + } + + &:active:not(:disabled) { + background-color: #059c4d !important; + border-color: #059c4d !important; + transform: translateY(0); + box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2); + } + + &:disabled { + background-color: #d9d9d9 !important; + border-color: #d9d9d9 !important; + color: #bfbfbf !important; + box-shadow: none; } } } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx index 991f3a2a..ab0eefe1 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx @@ -5,8 +5,7 @@ import { Button, Checkbox, Input, - Select, - Slider, + Modal, Switch, message as antdMessage, } from "antd"; @@ -15,11 +14,15 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined, + UserOutlined, + PictureOutlined, + FileOutlined, + SoundOutlined, } from "@ant-design/icons"; import type { CheckboxChangeEvent } from "antd/es/checkbox"; import styles from "./index.module.scss"; -import { ContactItem, ScriptGroup } from "../../types"; +import { ContactItem, ScriptGroup, MessageItem } from "../../types"; import InputMessage from "./InputMessage/InputMessage"; import ContentLibrarySelector from "./ContentLibrarySelector"; import type { ContentItem } from "@/components/ContentSelection/data"; @@ -36,12 +39,6 @@ interface StepSendMessageProps { targetLabel: string; messageContent: string; onMessageContentChange: (value: string) => void; - friendInterval: [number, number]; - onFriendIntervalChange: (value: [number, number]) => void; - messageInterval: [number, number]; - onMessageIntervalChange: (value: [number, number]) => void; - selectedTag: string; - onSelectedTagChange: (value: string) => void; aiRewriteEnabled: boolean; onAiRewriteToggle: (value: boolean) => void; aiPrompt: string; @@ -64,12 +61,6 @@ const StepSendMessage: React.FC = ({ targetLabel, messageContent, onMessageContentChange, - friendInterval, - onFriendIntervalChange, - messageInterval, - onMessageIntervalChange, - selectedTag, - onSelectedTagChange, aiRewriteEnabled, onAiRewriteToggle, aiPrompt, @@ -88,47 +79,111 @@ const StepSendMessage: React.FC = ({ const [savingScriptGroup, setSavingScriptGroup] = useState(false); const [aiRewriting, setAiRewriting] = useState(false); const [deletingGroupIds, setDeletingGroupIds] = useState([]); + const [aiRewriteModalVisible, setAiRewriteModalVisible] = useState(false); + const [aiRewriteModalIndex, setAiRewriteModalIndex] = useState( + null, + ); + const [aiRewriteModalPrompt, setAiRewriteModalPrompt] = useState(""); + const [aiRewritingMessage, setAiRewritingMessage] = useState(false); + const [aiRewriteResult, setAiRewriteResult] = useState(null); + + // 将 string[] 转换为 MessageItem[] + const messagesToItems = useCallback((messages: string[]): MessageItem[] => { + return messages.map(msg => { + try { + // 尝试解析为 JSON(新格式) + const parsed = JSON.parse(msg); + if (parsed && typeof parsed === "object" && "type" in parsed) { + return parsed as MessageItem; + } + } catch { + // 解析失败,作为文本消息处理 + } + // 旧格式:纯文本 + return { type: "text", content: msg }; + }); + }, []); + + // 将 MessageItem[] 转换为 string[] + const itemsToMessages = useCallback((items: MessageItem[]): string[] => { + return items.map(item => { + // 如果是纯文本消息,直接返回内容(保持向后兼容) + if (item.type === "text" && !item.fileName) { + return item.content; + } + // 其他类型序列化为 JSON + return JSON.stringify(item); + }); + }, []); + + // 内部维护的 MessageItem[] 状态 + const [messageItems, setMessageItems] = useState(() => + messagesToItems(currentScriptMessages), + ); + + // 当 currentScriptMessages 变化时,同步更新 messageItems + React.useEffect(() => { + setMessageItems(messagesToItems(currentScriptMessages)); + }, [currentScriptMessages, messagesToItems]); const handleAddMessage = useCallback( - (content?: string, showSuccess?: boolean) => { - const finalContent = (content ?? messageContent).trim(); - if (!finalContent) { - antdMessage.warning("请输入消息内容"); - return; + (content?: string | MessageItem, showSuccess?: boolean) => { + let newItem: MessageItem; + if (typeof content === "string") { + const finalContent = (content || messageContent).trim(); + if (!finalContent) { + antdMessage.warning("请输入消息内容"); + return; + } + newItem = { type: "text", content: finalContent }; + } else if (content && typeof content === "object") { + newItem = content; + } else { + const finalContent = messageContent.trim(); + if (!finalContent) { + antdMessage.warning("请输入消息内容"); + return; + } + newItem = { type: "text", content: finalContent }; } - onCurrentScriptMessagesChange([...currentScriptMessages, finalContent]); + + const newItems = [...messageItems, newItem]; + setMessageItems(newItems); + onCurrentScriptMessagesChange(itemsToMessages(newItems)); onMessageContentChange(""); if (showSuccess) { antdMessage.success("已添加消息内容"); } }, [ - currentScriptMessages, messageContent, + messageItems, onCurrentScriptMessagesChange, onMessageContentChange, + itemsToMessages, ], ); const handleRemoveMessage = useCallback( (index: number) => { - const next = currentScriptMessages.filter((_, idx) => idx !== index); - onCurrentScriptMessagesChange(next); + const next = messageItems.filter((_, idx) => idx !== index); + setMessageItems(next); + onCurrentScriptMessagesChange(itemsToMessages(next)); }, - [currentScriptMessages, onCurrentScriptMessagesChange], + [messageItems, onCurrentScriptMessagesChange, itemsToMessages], ); const handleSaveScriptGroup = useCallback(async () => { if (savingScriptGroup) { return; } - if (currentScriptMessages.length === 0) { + if (messageItems.length === 0) { antdMessage.warning("请先添加消息内容"); return; } const groupName = currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`; - const messages = [...currentScriptMessages]; + const messages = itemsToMessages(messageItems); const params: CreateContentLibraryParams = { name: groupName, sourceType: 1, @@ -155,6 +210,7 @@ const StepSendMessage: React.FC = ({ messages, }; onSavedScriptGroupsChange([...savedScriptGroups, newGroup]); + setMessageItems([]); onCurrentScriptMessagesChange([]); onCurrentScriptNameChange(""); onMessageContentChange(""); @@ -169,7 +225,7 @@ const StepSendMessage: React.FC = ({ }, [ aiPrompt, aiRewriteEnabled, - currentScriptMessages, + messageItems, currentScriptName, onCurrentScriptMessagesChange, onCurrentScriptNameChange, @@ -177,6 +233,7 @@ const StepSendMessage: React.FC = ({ onSavedScriptGroupsChange, savedScriptGroups, savingScriptGroup, + itemsToMessages, ]); const handleAiRewrite = useCallback(async () => { @@ -272,6 +329,138 @@ const StepSendMessage: React.FC = ({ onMessageContentChange, ]); + const handleOpenAiRewriteModal = useCallback((index: number) => { + setAiRewriteModalIndex(index); + setAiRewriteModalPrompt(""); + setAiRewriteModalVisible(true); + }, []); + + const handleCloseAiRewriteModal = useCallback(() => { + setAiRewriteModalVisible(false); + setAiRewriteModalIndex(null); + setAiRewriteModalPrompt(""); + setAiRewriteResult(null); + }, []); + + // 执行 AI 改写,获取结果但不立即应用 + const handleAiRewriteExecute = useCallback(async () => { + if (aiRewriteModalIndex === null) { + return; + } + const trimmedPrompt = aiRewriteModalPrompt.trim(); + if (!trimmedPrompt) { + antdMessage.warning("请输入改写提示词"); + return; + } + const messageToRewrite = messageItems[aiRewriteModalIndex]; + if (!messageToRewrite) { + antdMessage.error("消息不存在"); + return; + } + // AI改写只支持文本消息 + if (messageToRewrite.type !== "text") { + antdMessage.warning("AI改写仅支持文本消息"); + return; + } + if (aiRewritingMessage) { + return; + } + try { + setAiRewritingMessage(true); + const response = await aiEditContent({ + aiPrompt: trimmedPrompt, + content: messageToRewrite.content, + }); + const normalizedResponse = response as { + content?: string; + contentAfter?: string; + contentFront?: string; + data?: + | string + | { + content?: string; + contentAfter?: string; + contentFront?: string; + }; + result?: string; + }; + const dataField = normalizedResponse?.data; + const dataContent = + typeof dataField === "string" + ? dataField + : (dataField?.content ?? undefined); + const dataContentAfter = + typeof dataField === "string" ? undefined : dataField?.contentAfter; + const dataContentFront = + typeof dataField === "string" ? undefined : dataField?.contentFront; + + const primaryAfter = + normalizedResponse?.contentAfter ?? dataContentAfter ?? undefined; + const primaryFront = + normalizedResponse?.contentFront ?? dataContentFront ?? undefined; + + let rewrittenContent = ""; + if (typeof response === "string") { + rewrittenContent = response; + } else if (primaryAfter) { + rewrittenContent = primaryFront + ? `${primaryFront}\n${primaryAfter}` + : primaryAfter; + } else if (typeof normalizedResponse?.content === "string") { + rewrittenContent = normalizedResponse.content; + } else if (typeof dataContent === "string") { + rewrittenContent = dataContent; + } else if (typeof normalizedResponse?.result === "string") { + rewrittenContent = normalizedResponse.result; + } else if (primaryFront) { + rewrittenContent = primaryFront; + } + if (!rewrittenContent || typeof rewrittenContent !== "string") { + antdMessage.error("AI改写失败,请稍后重试"); + return; + } + setAiRewriteResult(rewrittenContent.trim()); + } catch (error) { + console.error("AI改写失败:", error); + antdMessage.error("AI改写失败,请稍后重试"); + } finally { + setAiRewritingMessage(false); + } + }, [ + aiRewriteModalIndex, + aiRewriteModalPrompt, + messageItems, + aiRewritingMessage, + ]); + + // 确认并应用 AI 改写结果 + const handleConfirmAiRewrite = useCallback(() => { + if (aiRewriteModalIndex === null || !aiRewriteResult) { + return; + } + const messageToRewrite = messageItems[aiRewriteModalIndex]; + if (!messageToRewrite) { + antdMessage.error("消息不存在"); + return; + } + const newItems = [...messageItems]; + newItems[aiRewriteModalIndex] = { + ...messageToRewrite, + content: aiRewriteResult, + }; + setMessageItems(newItems); + onCurrentScriptMessagesChange(itemsToMessages(newItems)); + handleCloseAiRewriteModal(); + antdMessage.success("AI改写完成"); + }, [ + aiRewriteModalIndex, + aiRewriteResult, + messageItems, + onCurrentScriptMessagesChange, + itemsToMessages, + handleCloseAiRewriteModal, + ]); + const handleApplyGroup = useCallback( (group: ScriptGroup) => { onCurrentScriptMessagesChange(group.messages); @@ -352,61 +541,167 @@ const StepSendMessage: React.FC = ({
+ {/* 1. 模拟推送内容 */}
模拟推送内容
+
+ + {/* 2. 消息列表 */} +
+ {messageItems.length === 0 ? ( +
+ 开始添加消息内容... +
+ ) : ( +
+ {messageItems.map((msgItem, index) => ( +
+
+
+ +
+
+
+ {msgItem.type === "text" && ( +
+ {msgItem.content} +
+ )} + {msgItem.type === "image" && ( +
+
+ +
+ {msgItem.fileName { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> + {msgItem.fileName && ( +
+ {msgItem.fileName} +
+ )} +
+ )} + {msgItem.type === "file" && ( +
+
+ +
+
+
+ {msgItem.fileName || "文件"} +
+ {msgItem.fileSize && ( +
+ {msgItem.fileSize >= 1024 * 1024 + ? `${(msgItem.fileSize / 1024 / 1024).toFixed(2)} MB` + : `${(msgItem.fileSize / 1024).toFixed(2)} KB`} +
+ )} +
+
+ )} + {msgItem.type === "audio" && ( +
+
+ +
+
+
+ {msgItem.fileName || "语音消息"} +
+ {msgItem.durationMs && ( +
+ {Math.floor(msgItem.durationMs / 1000)}秒 +
+ )} +
+
+ )} +
+
+ {msgItem.type === "text" && ( + + )} +
+
+
+
+ ))} +
+ )} +
+ + {/* 3. 消息输入组件 */} +
+ handleAddMessage(value)} + onAddMessage={message => handleAddMessage(message)} + clearOnSend + placeholder="请输入内容" + hint={`按ENTER发送,按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`} + /> +
+ + {/* 4. 话术组标题 */} +
+ onCurrentScriptNameChange(event.target.value)} + /> +
+ + {/* 5. 创建话术组按钮 */} +
-
-
-
当前编辑话术
- {currentScriptMessages.length === 0 ? ( -
- 开始添加消息内容... -
- ) : ( -
- {currentScriptMessages.map((msg, index) => ( -
-
{msg}
-
- ))} -
- )} -
-
- - onCurrentScriptNameChange(event.target.value) - } - /> -
-
+
-
- {/* 内容库选择组件 */} +
+
+
推送内容
+
+
已保存话术组 ({savedScriptGroups.length}) @@ -460,128 +755,114 @@ const StepSendMessage: React.FC = ({ )}
- -
- handleAddMessage(value)} - clearOnSend - placeholder="请输入内容" - hint={`按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`} - /> -
-
- -
AI智能话术改写
-
- {aiRewriteEnabled && ( - onAiPromptChange(event.target.value)} - className={styles.aiRewriteInput} - /> - )} -
-
- -
- - -
-
-
-
- -
-
-
相关设置
-
-
好友间间隔
-
- 间隔时间(秒) - - onFriendIntervalChange(value as [number, number]) - } - style={{ flex: 1, margin: "0 16px" }} - /> - - {friendInterval[0]} - {friendInterval[1]} - -
-
-
-
消息间间隔
-
- 间隔时间(秒) - - onMessageIntervalChange(value as [number, number]) - } - style={{ flex: 1, margin: "0 16px" }} - /> - - {messageInterval[0]} - {messageInterval[1]} - -
-
-
- -
-
完成打标签
- -
- -
-
推送预览
-
    -
  • 推送账号: {selectedAccounts.length}个
  • -
  • - 推送{targetLabel}: {selectedContacts.length}个 -
  • -
  • 话术组数: {savedScriptGroups.length}个
  • -
  • 随机推送: 否
  • -
  • 预计耗时: ~1分钟
  • -
-
+ + {/* AI改写弹窗 */} + + + AI智能改写 +
+ } + open={aiRewriteModalVisible} + onCancel={handleCloseAiRewriteModal} + width={680} + footer={[ + , + , + , + ]} + className={styles.aiRewriteModal} + wrapClassName={styles.aiRewriteModalWrap} + > +
+ {/* 原文和结果对比区域 */} +
+ {/* 原消息内容区域 */} + {aiRewriteModalIndex !== null && ( +
+
+ 📝 + + 1原消息内容 + +
+
+ {messageItems[aiRewriteModalIndex]?.type === "text" + ? messageItems[aiRewriteModalIndex].content + : "非文本消息不支持AI改写"} +
+
+ )} + + {/* Loading 状态 */} + {aiRewritingMessage && ( +
+
+
+ AI正在改写中,请稍候... +
+
+ )} + + {/* 分隔线 */} + {aiRewriteModalIndex !== null && aiRewriteResult && ( +
+ )} + + {/* 改写结果区域 */} + {aiRewriteResult && ( +
+
+ + 改写结果 +
+
+ {aiRewriteResult} +
+
+ )} +
+ + {/* 提示词输入区域 - 放在最下面 */} +
+
+ 💡 + 改写提示词 +
+ setAiRewriteModalPrompt(event.target.value)} + rows={3} + autoFocus + disabled={aiRewritingMessage} + className={styles.aiRewriteModalTextArea} + /> +
+
+
); }; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx index 5ee5b4bd..6789c6a6 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx @@ -14,7 +14,13 @@ import { getCustomerList } from "@/pages/pc/ckbox/weChat/api"; import StepSelectAccount from "./components/StepSelectAccount"; import StepSelectContacts from "./components/StepSelectContacts"; import StepSendMessage from "./components/StepSendMessage"; -import { ContactItem, PushType, ScriptGroup } from "./types"; +import StepPushParams from "./components/StepPushParams"; +import { + ContactItem, + PushType, + ScriptGroup, + CreatePushTaskPayload, +} from "./types"; import StepIndicator from "@/components/StepIndicator"; import type { ContentItem } from "@/components/ContentSelection/data"; import type { PoolSelectionItem } from "@/components/PoolSelection/data"; @@ -163,6 +169,20 @@ const CreatePushTask: React.FC = () => { return; } setCurrentStep(3); + return; + } + + if (currentStep === 3) { + // 验证推送内容 + if ( + currentScriptMessages.length === 0 && + selectedScriptGroupIds.length === 0 && + selectedContentLibraries.length === 0 + ) { + message.warning("请至少添加一条消息、选择一个话术组或内容库"); + return; + } + setCurrentStep(4); } }; @@ -184,9 +204,12 @@ const CreatePushTask: React.FC = () => { if (creatingTask) { return; } + + // ========== 1. 数据验证和准备 ========== const selectedGroups = savedScriptGroups.filter(group => selectedScriptGroupIds.includes(group.id), ); + if ( currentScriptMessages.length === 0 && selectedGroups.length === 0 && @@ -195,19 +218,27 @@ const CreatePushTask: React.FC = () => { message.warning("请添加话术内容、选择话术组或内容库"); return; } + + // 手动消息处理 const manualMessages = currentScriptMessages .map(item => item.trim()) .filter(Boolean); + if (validPushType === "group-announcement" && manualMessages.length === 0) { message.warning("请先填写公告内容"); return; } - const toNumberId = (value: unknown) => { + + // ID 转换工具函数 + const toNumberId = (value: unknown): number | null => { + if (value === null || value === undefined) return null; const numeric = Number(value); return Number.isFinite(numeric) && !Number.isNaN(numeric) ? numeric : null; }; + + // ========== 2. 内容库ID处理 ========== const contentGroupIds = Array.from( new Set( [ @@ -220,6 +251,7 @@ const CreatePushTask: React.FC = () => { ].filter((id): id is number => id !== null), ), ); + if ( manualMessages.length === 0 && selectedGroups.length === 0 && @@ -228,6 +260,8 @@ const CreatePushTask: React.FC = () => { message.warning("缺少有效的话术内容,请重新检查"); return; } + + // ========== 3. 账号ID处理 ========== const ownerWechatIds = Array.from( new Set( selectedAccounts @@ -235,47 +269,25 @@ const CreatePushTask: React.FC = () => { .filter((id): id is number => id !== null), ), ); + if (ownerWechatIds.length === 0) { message.error("缺少有效的推送账号信息"); return; } + + // ========== 4. 联系人ID处理 ========== const selectedContactIds = Array.from( new Set( selectedContacts.map(contact => contact?.id).filter(isValidNumber), ), ); + if (selectedContactIds.length === 0) { message.error("缺少有效的推送对象"); return; } - const friendIntervalMin = friendInterval[0]; - const friendIntervalMax = friendInterval[1]; - const messageIntervalMin = messageInterval[0]; - const messageIntervalMax = messageInterval[1]; - const trafficPoolIds = selectedTrafficPools - .map(pool => pool.id) - .filter( - id => id !== undefined && id !== null && String(id).trim() !== "", - ); - const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType]; - const maxPerDay = - selectedContacts.length > 0 - ? selectedContacts.length - : DEFAULT_MAX_PER_DAY[validPushType]; - const pushOrder = DEFAULT_PUSH_ORDER[validPushType]; - const normalizedPostPushTags = - selectedTag.trim().length > 0 - ? [ - toNumberId(selectedTag) !== null - ? (toNumberId(selectedTag) as number) - : selectedTag, - ] - : []; - const taskName = - currentScriptName.trim() || - selectedGroups[0]?.name || - (manualMessages[0] ? manualMessages[0].slice(0, 20) : "") || - `推送任务-${Date.now()}`; + + // ========== 5. 设备分组ID处理(好友推送必填) ========== const deviceGroupIds = Array.from( new Set( selectedAccounts @@ -283,84 +295,191 @@ const CreatePushTask: React.FC = () => { .filter((id): id is number => id !== null), ), ); + if (validPushType === "friend-message" && deviceGroupIds.length === 0) { message.error("缺少有效的推送设备分组"); return; } - const basePayload: Record = { - name: taskName, - type: 3, - autoStart: DEFAULT_AUTO_START[validPushType], - status: 1, - pushType: DEFAULT_PUSH_TYPE[validPushType], + // ========== 6. 流量池ID处理 ========== + const trafficPoolIds = selectedTrafficPools + .map(pool => { + const id = pool.id; + if (id === undefined || id === null) return null; + const strId = String(id).trim(); + return strId !== "" ? strId : null; + }) + .filter((id): id is string => id !== null); + + // ========== 7. 时间范围 ========== + const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType]; + + // ========== 8. 每日最大推送数 ========== + const maxPerDay = + selectedContacts.length > 0 + ? selectedContacts.length + : DEFAULT_MAX_PER_DAY[validPushType]; + + // ========== 9. 推送顺序 ========== + const pushOrder = DEFAULT_PUSH_ORDER[validPushType]; + + // ========== 10. 推送后标签处理 ========== + const postPushTags = + selectedTag.trim().length > 0 + ? (() => { + const tagId = toNumberId(selectedTag); + return tagId !== null ? [tagId] : []; + })() + : []; + + // ========== 11. 任务名称 ========== + const taskName = + currentScriptName.trim() || + selectedGroups[0]?.name || + (manualMessages[0] ? manualMessages[0].slice(0, 20) : "") || + `推送任务-${Date.now()}`; + + // ========== 12. 构建基础载荷 ========== + const basePayload: CreatePushTaskPayload = { + name: String(taskName).trim(), + type: 3, // 固定值:工作台类型 + autoStart: DEFAULT_AUTO_START[validPushType] ? 1 : 0, + status: 1, // 固定值:启用 + pushType: DEFAULT_PUSH_TYPE[validPushType] ? 1 : 0, targetType: validPushType === "friend-message" ? 2 : 1, groupPushSubType: validPushType === "group-announcement" ? 2 : 1, - startTime, - endTime, - maxPerDay, - pushOrder, - friendIntervalMin, - friendIntervalMax, - messageIntervalMin, - messageIntervalMax, + startTime: String(startTime), + endTime: String(endTime), + maxPerDay: Number(maxPerDay), + pushOrder: Number(pushOrder), + friendIntervalMin: Number(friendInterval[0]), + friendIntervalMax: Number(friendInterval[1]), + messageIntervalMin: Number(messageInterval[0]), + messageIntervalMax: Number(messageInterval[1]), isRandomTemplate: selectedScriptGroupIds.length > 1 ? 1 : 0, - contentGroups: contentGroupIds, - postPushTags: normalizedPostPushTags, - ownerWechatIds, - enableAiRewrite: aiRewriteEnabled ? 1 : 0, + contentGroups: contentGroupIds.length > 0 ? contentGroupIds : [], + postPushTags: postPushTags, + ownerWechatIds: ownerWechatIds, }; - if (trafficPoolIds.length > 0) { - basePayload.trafficPools = trafficPoolIds; - } - if (validPushType === "friend-message") { - basePayload.isLoop = 0; - basePayload.deviceGroups = deviceGroupIds; - } - if (manualMessages.length > 0) { - basePayload.manualMessages = manualMessages; - if (currentScriptName.trim()) { - basePayload.manualScriptName = currentScriptName.trim(); - } - } - if (selectedScriptGroupIds.length > 0) { - basePayload.selectedScriptGroupIds = selectedScriptGroupIds; - } - if (aiRewriteEnabled && aiPrompt.trim()) { - basePayload.aiRewritePrompt = aiPrompt.trim(); - } - if (selectedGroups.length > 0) { - basePayload.scriptGroups = selectedGroups.map(group => ({ - id: group.id, - name: group.name, - messages: group.messages, - })); - } + + // ========== 13. 根据推送类型添加特定字段 ========== if (validPushType === "friend-message") { + // 好友推送特有字段 + // 注意:wechatFriends 必须是字符串数组,不是数字数组 basePayload.wechatFriends = Array.from( new Set( selectedContacts - .map(contact => toNumberId(contact?.id)) - .filter((id): id is number => id !== null), + .map(contact => { + const id = toNumberId(contact?.id); + return id !== null ? String(id) : null; + }) + .filter((id): id is string => id !== null), ), ); - basePayload.targetType = 2; + basePayload.deviceGroups = deviceGroupIds; // 必填,数字数组 + basePayload.isLoop = 0; // 固定值 + basePayload.targetType = 2; // 确保是好友类型 + basePayload.groupPushSubType = 1; // 固定为群群发 } else { + // 群推送特有字段 const groupIds = Array.from( new Set( selectedContacts - .map(contact => toNumberId(contact.groupId ?? contact.id)) + .map(contact => { + // 优先使用 groupId,如果没有则使用 id + const id = contact.groupId ?? contact.id; + return toNumberId(id); + }) .filter((id): id is number => id !== null), ), ); - basePayload.wechatGroups = groupIds; + + basePayload.wechatGroups = groupIds; // 数字数组 + basePayload.targetType = 1; // 群类型 basePayload.groupPushSubType = validPushType === "group-announcement" ? 2 : 1; - basePayload.targetType = 1; + + // 群公告特有字段 if (validPushType === "group-announcement") { basePayload.announcementContent = manualMessages.join("\n"); } } + + // ========== 14. 可选字段处理 ========== + // 流量池(如果存在) + if (trafficPoolIds.length > 0) { + basePayload.trafficPools = trafficPoolIds; // 字符串数组 + } + + // 手动消息(如果存在) + if (manualMessages.length > 0) { + basePayload.manualMessages = manualMessages; + if (currentScriptName.trim()) { + basePayload.manualScriptName = String(currentScriptName.trim()); + } + } + + // 选中的话术组ID(如果存在) + if (selectedScriptGroupIds.length > 0) { + basePayload.selectedScriptGroupIds = selectedScriptGroupIds.map(id => + String(id), + ); + } + + // AI改写相关(如果启用) + if (aiRewriteEnabled) { + basePayload.enableAiRewrite = 1; + if (aiPrompt.trim()) { + basePayload.aiRewritePrompt = String(aiPrompt.trim()); + } + } else { + basePayload.enableAiRewrite = 0; + } + + // 话术组对象(如果存在) + if (selectedGroups.length > 0) { + basePayload.scriptGroups = selectedGroups.map(group => ({ + id: String(group.id), + name: String(group.name || ""), + messages: Array.isArray(group.messages) + ? group.messages.map(msg => String(msg)) + : [], + })); + } + + // ========== 15. 数据验证和提交 ========== + // 最终验证:确保必填字段存在 + if (validPushType === "friend-message") { + if ( + !Array.isArray(basePayload.deviceGroups) || + basePayload.deviceGroups.length === 0 + ) { + message.error("好友推送必须选择设备分组"); + return; + } + if ( + !Array.isArray(basePayload.wechatFriends) || + basePayload.wechatFriends.length === 0 + ) { + message.error("好友推送必须选择好友"); + return; + } + } else { + if ( + !Array.isArray(basePayload.wechatGroups) || + basePayload.wechatGroups.length === 0 + ) { + message.error("群推送必须选择群"); + return; + } + } + + // 提交前打印日志(开发环境) + if (process.env.NODE_ENV === "development") { + console.log("提交数据:", JSON.stringify(basePayload, null, 2)); + } + + // ========== 16. 提交请求 ========== let hideLoading: ReturnType | undefined; try { setCreatingTask(true); @@ -386,7 +505,7 @@ const CreatePushTask: React.FC = () => { -
+
{ onBackClick={handleClose} />
- +
+ +
} footer={ @@ -434,6 +560,12 @@ const CreatePushTask: React.FC = () => { {selectedContacts.length}个 )} + {currentStep === 4 && ( + + 推送账号: {selectedAccounts.length}个, 推送{step2Title}:{" "} + {selectedContacts.length}个 + + )}
{currentStep === 1 && ( @@ -458,6 +590,14 @@ const CreatePushTask: React.FC = () => { )} {currentStep === 3 && ( + <> + + + + )} + {currentStep === 4 && ( <>
diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts index bd5eb0f1..f51d5ed4 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts @@ -20,8 +20,67 @@ export interface ContactItem { extendFields?: Record; } +// 消息类型定义 +export type MessageType = "text" | "image" | "file" | "audio"; + +export interface MessageItem { + type: MessageType; + content: string; // 文本内容或文件URL + // 文件相关字段 + fileName?: string; // 文件名 + fileSize?: number; // 文件大小(字节) + // 语音相关字段 + durationMs?: number; // 语音时长(毫秒) +} + export interface ScriptGroup { id: string; name: string; - messages: string[]; + messages: string[]; // 保持向后兼容,但实际应该使用 MessageItem[] +} + +// 接口请求载荷类型定义 +export interface CreatePushTaskPayload { + // 基础字段 + name: string; + type: 3; // 固定值:工作台类型 + autoStart: 0 | 1; + status: 1; // 固定值:启用 + pushType: 0 | 1; // 0=定时,1=立即 + targetType: 1 | 2; // 1=群推送,2=好友推送 + groupPushSubType: 1 | 2; // 1=群群发,2=群公告 + startTime: string; // "HH:mm" 格式 + endTime: string; // "HH:mm" 格式 + maxPerDay: number; + pushOrder: 1 | 2; // 1=最早优先,2=最新优先 + friendIntervalMin: number; + friendIntervalMax: number; + messageIntervalMin: number; + messageIntervalMax: number; + isRandomTemplate: 0 | 1; + contentGroups: number[]; // 内容库ID数组 + postPushTags: number[]; // 推送后标签ID数组 + ownerWechatIds: number[]; // 客服ID数组 + + // 好友推送特有字段 + wechatFriends?: string[]; // 好友ID列表(字符串数组) + deviceGroups?: number[]; // 设备分组ID数组(好友推送时必填) + isLoop?: 0; // 固定值(好友推送时) + + // 群推送特有字段 + wechatGroups?: number[]; // 微信群ID数组 + announcementContent?: string; // 群公告内容(群公告时必填) + + // 可选字段 + trafficPools?: string[]; // 流量池ID数组(字符串数组) + manualMessages?: string[]; // 手动消息数组 + manualScriptName?: string; // 手动话术名称 + selectedScriptGroupIds?: string[]; // 选中的话术组ID数组 + enableAiRewrite?: 0 | 1; // 是否启用AI改写 + aiRewritePrompt?: string; // AI改写提示词 + scriptGroups?: Array<{ + id: string; + name: string; + messages: string[]; + }>; // 话术组对象数组 } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx index bd2545a7..1236b5a8 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -220,7 +220,7 @@ const PushHistory: React.FC = () => { render: (type: PushType) => getPushTypeTag(type), }, { - title: "推送内容", + title: "任务名称", dataIndex: "pushContent", key: "pushContent", ellipsis: true,