Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into release/friend
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
8
Cunkebao/pnpm-lock.yaml
generated
8
Cunkebao/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
49
Server/application/http/middleware/Jwt.php
Normal file
49
Server/application/http/middleware/Jwt.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\http\middleware;
|
||||
|
||||
use app\common\util\JwtUtil;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* JWT认证中间件
|
||||
*/
|
||||
class Jwt
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
* @param \think\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// 获取Token
|
||||
$token = JwtUtil::getRequestToken();
|
||||
|
||||
// 验证Token
|
||||
if (!$token) {
|
||||
return json([
|
||||
'code' => 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ return [
|
||||
|
||||
// 全局中间件
|
||||
'alias' => [
|
||||
'cors' => 'app\\common\\middleware\\AllowCrossDomain'
|
||||
'cors' => 'app\\common\\middleware\\AllowCrossDomain',
|
||||
'jwt' => 'app\\http\\middleware\\Jwt'
|
||||
],
|
||||
|
||||
// 应用中间件
|
||||
|
||||
125
Server/extend/Eison/Utils/Helper/ArrHelper.php
Normal file
125
Server/extend/Eison/Utils/Helper/ArrHelper.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Eison\Utils\Helper;
|
||||
|
||||
/**
|
||||
* 数组辅助类
|
||||
*/
|
||||
class ArrHelper
|
||||
{
|
||||
/**
|
||||
* 从数组中提取指定的键值
|
||||
*
|
||||
* @param string $keys 键名,多个用逗号分隔,支持键名映射(如:account=userName)
|
||||
* @param array $array 源数组
|
||||
* @param mixed $default 默认值,如果键不存在时返回此值
|
||||
* @return array
|
||||
*/
|
||||
public static function getValue(string $keys, array $array, $default = null): array
|
||||
{
|
||||
$result = [];
|
||||
$keyList = explode(',', $keys);
|
||||
|
||||
foreach ($keyList as $key) {
|
||||
$key = trim($key);
|
||||
|
||||
// 支持键名映射:account=userName
|
||||
if (strpos($key, '=') !== false) {
|
||||
list($sourceKey, $targetKey) = explode('=', $key, 2);
|
||||
$sourceKey = trim($sourceKey);
|
||||
$targetKey = trim($targetKey);
|
||||
|
||||
// 如果源键存在,使用源键的值;否则使用目标键的值;都不存在则使用默认值
|
||||
if (isset($array[$sourceKey])) {
|
||||
$result[$targetKey] = $array[$sourceKey];
|
||||
} elseif (isset($array[$targetKey])) {
|
||||
$result[$targetKey] = $array[$targetKey];
|
||||
} else {
|
||||
// 如果提供了默认值,使用默认值;否则不添加该键
|
||||
if ($default !== null) {
|
||||
$result[$targetKey] = $default;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通键名
|
||||
if (isset($array[$key])) {
|
||||
$result[$key] = $array[$key];
|
||||
} else {
|
||||
// 如果提供了默认值,使用默认值;否则不添加该键
|
||||
if ($default !== null) {
|
||||
$result[$key] = $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除数组中的空值(null、空字符串、空数组)
|
||||
*
|
||||
* @param array $array 源数组
|
||||
* @return array
|
||||
*/
|
||||
public static function rmValue(array $array): array
|
||||
{
|
||||
return array_filter($array, function($value) {
|
||||
if (is_array($value)) {
|
||||
return !empty($value);
|
||||
}
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 左连接两个数组
|
||||
*
|
||||
* @param array $leftArray 左数组
|
||||
* @param array $rightArray 右数组
|
||||
* @param string $key 关联键名
|
||||
* @return array
|
||||
*/
|
||||
public static function leftJoin(array $leftArray, array $rightArray, string $key): array
|
||||
{
|
||||
// 将右数组按关联键索引
|
||||
$rightIndexed = [];
|
||||
foreach ($rightArray as $item) {
|
||||
if (isset($item[$key])) {
|
||||
$rightIndexed[$item[$key]] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 左连接
|
||||
$result = [];
|
||||
foreach ($leftArray as $leftItem) {
|
||||
$leftKeyValue = $leftItem[$key] ?? null;
|
||||
if ($leftKeyValue !== null && isset($rightIndexed[$leftKeyValue])) {
|
||||
$result[] = array_merge($leftItem, $rightIndexed[$leftKeyValue]);
|
||||
} else {
|
||||
$result[] = $leftItem;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组的某一列作为键,重新组织数组
|
||||
*
|
||||
* @param string $key 作为键的列名
|
||||
* @param array $array 源数组
|
||||
* @return array
|
||||
*/
|
||||
public static function columnTokey(string $key, array $array): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($array as $item) {
|
||||
if (isset($item[$key])) {
|
||||
$result[$item[$key]] = $item;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Badge,
|
||||
Dropdown,
|
||||
Empty,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
BarChartOutlined,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
ThunderboltOutlined,
|
||||
SettingOutlined,
|
||||
CalendarOutlined,
|
||||
RetweetOutlined,
|
||||
ClearOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { noticeList, readMessage, readAll } from "./api";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
@@ -54,6 +55,7 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
const [messageList, setMessageList] = useState<MessageItem[]>([]);
|
||||
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<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
navigate("/pc/weChat");
|
||||
}
|
||||
};
|
||||
|
||||
const isWeChat = () => {
|
||||
return location.pathname.startsWith("/pc/weChat");
|
||||
};
|
||||
|
||||
// 定时器获取消息条数
|
||||
const IntervalMessageCount = async () => {
|
||||
try {
|
||||
@@ -125,6 +122,99 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
navigate("/login"); // 跳转到登录页面
|
||||
};
|
||||
|
||||
// 清除所有 IndexedDB 数据库
|
||||
const clearAllIndexedDB = async (): Promise<void> => {
|
||||
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<void>((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<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
navigate("/pc/commonConfig");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "clearCache",
|
||||
icon: <ClearOutlined style={{ fontSize: 16 }} />,
|
||||
label: clearingCache ? "清除缓存中..." : "清除缓存",
|
||||
onClick: handleClearCache,
|
||||
disabled: clearingCache,
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
icon: <LogoutOutlined style={{ fontSize: 14 }} />,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import request from "@/api/request";
|
||||
import type { CreatePushTaskPayload } from "./types";
|
||||
|
||||
// 获取客服列表
|
||||
export function queryWorkbenchCreate(params) {
|
||||
// 创建推送任务
|
||||
export function queryWorkbenchCreate(
|
||||
params: CreatePushTaskPayload,
|
||||
): Promise<any> {
|
||||
return request("/v1/workbench/create", params, "POST");
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<StepPushParamsProps> = ({
|
||||
selectedAccounts,
|
||||
selectedContacts,
|
||||
targetLabel,
|
||||
friendInterval,
|
||||
onFriendIntervalChange,
|
||||
messageInterval,
|
||||
onMessageIntervalChange,
|
||||
selectedTag,
|
||||
onSelectedTagChange,
|
||||
savedScriptGroups,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.stepContent}>
|
||||
<div className={styles.step4Content}>
|
||||
<div className={styles.settingsPanel}>
|
||||
<div className={styles.settingsTitle}>相关设置</div>
|
||||
<div className={styles.settingItem}>
|
||||
<div className={styles.settingLabel}>好友间间隔</div>
|
||||
<div className={styles.settingControl}>
|
||||
<span>间隔时间(秒)</span>
|
||||
<Slider
|
||||
range
|
||||
min={1}
|
||||
max={60}
|
||||
value={friendInterval}
|
||||
onChange={value =>
|
||||
onFriendIntervalChange(value as [number, number])
|
||||
}
|
||||
style={{ flex: 1, margin: "0 16px" }}
|
||||
/>
|
||||
<span>
|
||||
{friendInterval[0]} - {friendInterval[1]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.settingItem}>
|
||||
<div className={styles.settingLabel}>消息间间隔</div>
|
||||
<div className={styles.settingControl}>
|
||||
<span>间隔时间(秒)</span>
|
||||
<Slider
|
||||
range
|
||||
min={1}
|
||||
max={60}
|
||||
value={messageInterval}
|
||||
onChange={value =>
|
||||
onMessageIntervalChange(value as [number, number])
|
||||
}
|
||||
style={{ flex: 1, margin: "0 16px" }}
|
||||
/>
|
||||
<span>
|
||||
{messageInterval[0]} - {messageInterval[1]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tagSection}>
|
||||
<div className={styles.settingLabel}>完成打标签</div>
|
||||
<Select
|
||||
value={selectedTag}
|
||||
onChange={onSelectedTagChange}
|
||||
placeholder="选择标签"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select.Option value="potential">潜在客户</Select.Option>
|
||||
<Select.Option value="customer">客户</Select.Option>
|
||||
<Select.Option value="partner">合作伙伴</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={styles.pushPreview}>
|
||||
<div className={styles.previewTitle}>推送预览</div>
|
||||
<ul>
|
||||
<li>推送账号: {selectedAccounts.length}个</li>
|
||||
<li>
|
||||
推送{targetLabel}: {selectedContacts.length}个
|
||||
</li>
|
||||
<li>话术组数: {savedScriptGroups.length}个</li>
|
||||
<li>随机推送: 否</li>
|
||||
<li>预计耗时: ~1分钟</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepPushParams;
|
||||
@@ -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<InputMessageProps> = ({
|
||||
defaultValue = "",
|
||||
onContentChange,
|
||||
onSend,
|
||||
onAddMessage,
|
||||
clearOnSend = false,
|
||||
placeholder = "输入消息...",
|
||||
hint,
|
||||
@@ -169,9 +172,44 @@ const InputMessage: React.FC<InputMessageProps> = ({
|
||||
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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StepSendMessageProps> = ({
|
||||
targetLabel,
|
||||
messageContent,
|
||||
onMessageContentChange,
|
||||
friendInterval,
|
||||
onFriendIntervalChange,
|
||||
messageInterval,
|
||||
onMessageIntervalChange,
|
||||
selectedTag,
|
||||
onSelectedTagChange,
|
||||
aiRewriteEnabled,
|
||||
onAiRewriteToggle,
|
||||
aiPrompt,
|
||||
@@ -88,47 +79,111 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
const [savingScriptGroup, setSavingScriptGroup] = useState(false);
|
||||
const [aiRewriting, setAiRewriting] = useState(false);
|
||||
const [deletingGroupIds, setDeletingGroupIds] = useState<string[]>([]);
|
||||
const [aiRewriteModalVisible, setAiRewriteModalVisible] = useState(false);
|
||||
const [aiRewriteModalIndex, setAiRewriteModalIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [aiRewriteModalPrompt, setAiRewriteModalPrompt] = useState("");
|
||||
const [aiRewritingMessage, setAiRewritingMessage] = useState(false);
|
||||
const [aiRewriteResult, setAiRewriteResult] = useState<string | null>(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<MessageItem[]>(() =>
|
||||
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<StepSendMessageProps> = ({
|
||||
messages,
|
||||
};
|
||||
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
|
||||
setMessageItems([]);
|
||||
onCurrentScriptMessagesChange([]);
|
||||
onCurrentScriptNameChange("");
|
||||
onMessageContentChange("");
|
||||
@@ -169,7 +225,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
}, [
|
||||
aiPrompt,
|
||||
aiRewriteEnabled,
|
||||
currentScriptMessages,
|
||||
messageItems,
|
||||
currentScriptName,
|
||||
onCurrentScriptMessagesChange,
|
||||
onCurrentScriptNameChange,
|
||||
@@ -177,6 +233,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
onSavedScriptGroupsChange,
|
||||
savedScriptGroups,
|
||||
savingScriptGroup,
|
||||
itemsToMessages,
|
||||
]);
|
||||
|
||||
const handleAiRewrite = useCallback(async () => {
|
||||
@@ -272,6 +329,138 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
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<StepSendMessageProps> = ({
|
||||
<div className={styles.stepContent}>
|
||||
<div className={styles.step3Content}>
|
||||
<div className={styles.leftColumn}>
|
||||
{/* 1. 模拟推送内容 */}
|
||||
<div className={styles.previewHeader}>
|
||||
<div className={styles.previewHeaderTitle}>模拟推送内容</div>
|
||||
</div>
|
||||
|
||||
{/* 2. 消息列表 */}
|
||||
<div className={styles.messagePreview}>
|
||||
{messageItems.length === 0 ? (
|
||||
<div className={styles.messagePlaceholder}>
|
||||
开始添加消息内容...
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.messageList}>
|
||||
{messageItems.map((msgItem, index) => (
|
||||
<div className={styles.messageBubbleWrapper} key={index}>
|
||||
<div className={styles.messageBubble}>
|
||||
<div className={styles.messageAvatar}>
|
||||
<UserOutlined />
|
||||
</div>
|
||||
<div className={styles.messageContent}>
|
||||
<div className={styles.messageBubbleInner}>
|
||||
{msgItem.type === "text" && (
|
||||
<div className={styles.messageText}>
|
||||
{msgItem.content}
|
||||
</div>
|
||||
)}
|
||||
{msgItem.type === "image" && (
|
||||
<div className={styles.messageMedia}>
|
||||
<div className={styles.messageMediaIcon}>
|
||||
<PictureOutlined />
|
||||
</div>
|
||||
<img
|
||||
src={msgItem.content}
|
||||
alt={msgItem.fileName || "图片"}
|
||||
className={styles.messageImage}
|
||||
onError={e => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
{msgItem.fileName && (
|
||||
<div className={styles.messageFileName}>
|
||||
{msgItem.fileName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{msgItem.type === "file" && (
|
||||
<div className={styles.messageMedia}>
|
||||
<div className={styles.messageMediaIcon}>
|
||||
<FileOutlined />
|
||||
</div>
|
||||
<div className={styles.messageFileInfo}>
|
||||
<div className={styles.messageFileName}>
|
||||
{msgItem.fileName || "文件"}
|
||||
</div>
|
||||
{msgItem.fileSize && (
|
||||
<div className={styles.messageFileSize}>
|
||||
{msgItem.fileSize >= 1024 * 1024
|
||||
? `${(msgItem.fileSize / 1024 / 1024).toFixed(2)} MB`
|
||||
: `${(msgItem.fileSize / 1024).toFixed(2)} KB`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{msgItem.type === "audio" && (
|
||||
<div className={styles.messageMedia}>
|
||||
<div className={styles.messageMediaIcon}>
|
||||
<SoundOutlined />
|
||||
</div>
|
||||
<div className={styles.messageFileInfo}>
|
||||
<div className={styles.messageFileName}>
|
||||
{msgItem.fileName || "语音消息"}
|
||||
</div>
|
||||
{msgItem.durationMs && (
|
||||
<div className={styles.messageFileSize}>
|
||||
{Math.floor(msgItem.durationMs / 1000)}秒
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.messageActions}>
|
||||
{msgItem.type === "text" && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => handleOpenAiRewriteModal(index)}
|
||||
className={styles.aiRewriteButton}
|
||||
>
|
||||
AI改写
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveMessage(index)}
|
||||
className={styles.messageAction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3. 消息输入组件 */}
|
||||
<div className={styles.messageInputArea}>
|
||||
<InputMessage
|
||||
defaultValue={messageContent}
|
||||
onContentChange={onMessageContentChange}
|
||||
onSend={value => handleAddMessage(value)}
|
||||
onAddMessage={message => handleAddMessage(message)}
|
||||
clearOnSend
|
||||
placeholder="请输入内容"
|
||||
hint={`按ENTER发送,按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 4. 话术组标题 */}
|
||||
<div className={styles.scriptNameInput}>
|
||||
<Input
|
||||
placeholder="话术组名称(可选)"
|
||||
value={currentScriptName}
|
||||
onChange={event => onCurrentScriptNameChange(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 5. 创建话术组按钮 */}
|
||||
<div className={styles.createScriptGroupButton}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleSaveScriptGroup}
|
||||
disabled={currentScriptMessages.length === 0 || savingScriptGroup}
|
||||
loading={savingScriptGroup}
|
||||
block
|
||||
>
|
||||
保存为话术组
|
||||
创建为话术组
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.messagePreview}>
|
||||
<div className={styles.messageBubble}>
|
||||
<div className={styles.currentEditingLabel}>当前编辑话术</div>
|
||||
{currentScriptMessages.length === 0 ? (
|
||||
<div className={styles.messagePlaceholder}>
|
||||
开始添加消息内容...
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.messageList}>
|
||||
{currentScriptMessages.map((msg, index) => (
|
||||
<div className={styles.messageItem} key={index}>
|
||||
<div className={styles.messageText}>{msg}</div>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveMessage(index)}
|
||||
className={styles.messageAction}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.scriptNameInput}>
|
||||
<Input
|
||||
placeholder="话术组名称(可选)"
|
||||
value={currentScriptName}
|
||||
onChange={event =>
|
||||
onCurrentScriptNameChange(event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.savedScriptGroups}>
|
||||
{/* 内容库选择组件 */}
|
||||
<div className={styles.rightColumn}>
|
||||
<div className={styles.pushContentHeader}>
|
||||
<div className={styles.pushContentTitle}>推送内容</div>
|
||||
<ContentLibrarySelector
|
||||
selectedContentLibraries={selectedContentLibraries}
|
||||
onSelectedContentLibrariesChange={
|
||||
onSelectedContentLibrariesChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.savedScriptGroups}>
|
||||
<div className={styles.scriptGroupHeaderRow}>
|
||||
<div className={styles.scriptGroupTitle}>
|
||||
已保存话术组 ({savedScriptGroups.length})
|
||||
@@ -460,128 +755,114 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.messageInputArea}>
|
||||
<InputMessage
|
||||
defaultValue={messageContent}
|
||||
onContentChange={onMessageContentChange}
|
||||
onSend={value => handleAddMessage(value)}
|
||||
clearOnSend
|
||||
placeholder="请输入内容"
|
||||
hint={`按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`}
|
||||
/>
|
||||
<div className={styles.aiRewriteSection}>
|
||||
<div className={styles.aiRewriteToggle}>
|
||||
<Switch
|
||||
checked={aiRewriteEnabled}
|
||||
onChange={onAiRewriteToggle}
|
||||
/>
|
||||
<div className={styles.aiRewriteLabel}>AI智能话术改写</div>
|
||||
<div>
|
||||
{aiRewriteEnabled && (
|
||||
<Input
|
||||
placeholder="输入改写提示词"
|
||||
value={aiPrompt}
|
||||
onChange={event => onAiPromptChange(event.target.value)}
|
||||
className={styles.aiRewriteInput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.aiRewriteActions}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleAiRewrite}
|
||||
disabled={!aiRewriteEnabled}
|
||||
loading={aiRewriting}
|
||||
className={styles.aiRewriteButton}
|
||||
>
|
||||
AI改写
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddMessage(undefined, true)}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightColumn}>
|
||||
<div className={styles.settingsPanel}>
|
||||
<div className={styles.settingsTitle}>相关设置</div>
|
||||
<div className={styles.settingItem}>
|
||||
<div className={styles.settingLabel}>好友间间隔</div>
|
||||
<div className={styles.settingControl}>
|
||||
<span>间隔时间(秒)</span>
|
||||
<Slider
|
||||
range
|
||||
min={1}
|
||||
max={60}
|
||||
value={friendInterval}
|
||||
onChange={value =>
|
||||
onFriendIntervalChange(value as [number, number])
|
||||
}
|
||||
style={{ flex: 1, margin: "0 16px" }}
|
||||
/>
|
||||
<span>
|
||||
{friendInterval[0]} - {friendInterval[1]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.settingItem}>
|
||||
<div className={styles.settingLabel}>消息间间隔</div>
|
||||
<div className={styles.settingControl}>
|
||||
<span>间隔时间(秒)</span>
|
||||
<Slider
|
||||
range
|
||||
min={1}
|
||||
max={60}
|
||||
value={messageInterval}
|
||||
onChange={value =>
|
||||
onMessageIntervalChange(value as [number, number])
|
||||
}
|
||||
style={{ flex: 1, margin: "0 16px" }}
|
||||
/>
|
||||
<span>
|
||||
{messageInterval[0]} - {messageInterval[1]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tagSection}>
|
||||
<div className={styles.settingLabel}>完成打标签</div>
|
||||
<Select
|
||||
value={selectedTag}
|
||||
onChange={onSelectedTagChange}
|
||||
placeholder="选择标签"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select.Option value="potential">潜在客户</Select.Option>
|
||||
<Select.Option value="customer">客户</Select.Option>
|
||||
<Select.Option value="partner">合作伙伴</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={styles.pushPreview}>
|
||||
<div className={styles.previewTitle}>推送预览</div>
|
||||
<ul>
|
||||
<li>推送账号: {selectedAccounts.length}个</li>
|
||||
<li>
|
||||
推送{targetLabel}: {selectedContacts.length}个
|
||||
</li>
|
||||
<li>话术组数: {savedScriptGroups.length}个</li>
|
||||
<li>随机推送: 否</li>
|
||||
<li>预计耗时: ~1分钟</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI改写弹窗 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className={styles.aiRewriteModalTitle}>
|
||||
<span className={styles.aiRewriteModalTitleIcon}>✨</span>
|
||||
<span>AI智能改写</span>
|
||||
</div>
|
||||
}
|
||||
open={aiRewriteModalVisible}
|
||||
onCancel={handleCloseAiRewriteModal}
|
||||
width={680}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCloseAiRewriteModal}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="execute"
|
||||
type="primary"
|
||||
className={styles.aiRewriteExecuteButton}
|
||||
loading={aiRewritingMessage}
|
||||
onClick={handleAiRewriteExecute}
|
||||
disabled={!aiRewriteModalPrompt.trim()}
|
||||
>
|
||||
AI改写
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
className={styles.confirmButton}
|
||||
onClick={handleConfirmAiRewrite}
|
||||
disabled={!aiRewriteResult || aiRewritingMessage}
|
||||
>
|
||||
确认改写
|
||||
</Button>,
|
||||
]}
|
||||
className={styles.aiRewriteModal}
|
||||
wrapClassName={styles.aiRewriteModalWrap}
|
||||
>
|
||||
<div className={styles.aiRewriteModalContent}>
|
||||
{/* 原文和结果对比区域 */}
|
||||
<div className={styles.aiRewriteModalCompareSection}>
|
||||
{/* 原消息内容区域 */}
|
||||
{aiRewriteModalIndex !== null && (
|
||||
<div className={styles.aiRewriteModalSection}>
|
||||
<div className={styles.aiRewriteModalSectionHeader}>
|
||||
<span className={styles.aiRewriteModalSectionIcon}>📝</span>
|
||||
<span className={styles.aiRewriteModalLabel}>
|
||||
1原消息内容
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.aiRewriteModalOriginalText}>
|
||||
{messageItems[aiRewriteModalIndex]?.type === "text"
|
||||
? messageItems[aiRewriteModalIndex].content
|
||||
: "非文本消息不支持AI改写"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading 状态 */}
|
||||
{aiRewritingMessage && (
|
||||
<div className={styles.aiRewriteModalLoading}>
|
||||
<div className={styles.aiRewriteModalLoadingIcon}>⏳</div>
|
||||
<div className={styles.aiRewriteModalLoadingText}>
|
||||
AI正在改写中,请稍候...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分隔线 */}
|
||||
{aiRewriteModalIndex !== null && aiRewriteResult && (
|
||||
<div className={styles.aiRewriteModalDivider} />
|
||||
)}
|
||||
|
||||
{/* 改写结果区域 */}
|
||||
{aiRewriteResult && (
|
||||
<div className={styles.aiRewriteModalSection}>
|
||||
<div className={styles.aiRewriteModalSectionHeader}>
|
||||
<span className={styles.aiRewriteModalSectionIcon}>✨</span>
|
||||
<span className={styles.aiRewriteModalLabel}>改写结果</span>
|
||||
</div>
|
||||
<div className={styles.aiRewriteModalResultText}>
|
||||
{aiRewriteResult}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 提示词输入区域 - 放在最下面 */}
|
||||
<div className={styles.aiRewriteModalSection}>
|
||||
<div className={styles.aiRewriteModalSectionHeader}>
|
||||
<span className={styles.aiRewriteModalSectionIcon}>💡</span>
|
||||
<span className={styles.aiRewriteModalLabel}>改写提示词</span>
|
||||
</div>
|
||||
<Input.TextArea
|
||||
placeholder="默认提示词为: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除。"
|
||||
value={aiRewriteModalPrompt}
|
||||
onChange={event => setAiRewriteModalPrompt(event.target.value)}
|
||||
rows={3}
|
||||
autoFocus
|
||||
disabled={aiRewritingMessage}
|
||||
className={styles.aiRewriteModalTextArea}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string, any> = {
|
||||
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<typeof message.loading> | undefined;
|
||||
try {
|
||||
setCreatingTask(true);
|
||||
@@ -386,7 +505,7 @@ const CreatePushTask: React.FC = () => {
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<div style={{ padding: "20px" }}>
|
||||
<div style={{ padding: "0 20px" }}>
|
||||
<PowerNavigation
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
@@ -395,26 +514,33 @@ const CreatePushTask: React.FC = () => {
|
||||
onBackClick={handleClose}
|
||||
/>
|
||||
</div>
|
||||
<StepIndicator
|
||||
currentStep={currentStep}
|
||||
steps={[
|
||||
{
|
||||
id: 1,
|
||||
title: "选择微信",
|
||||
subtitle: "选择微信",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: `选择${step2Title}`,
|
||||
subtitle: `选择${step2Title}`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "一键群发",
|
||||
subtitle: "一键群发",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div style={{ margin: "0 20px" }}>
|
||||
<StepIndicator
|
||||
currentStep={currentStep}
|
||||
steps={[
|
||||
{
|
||||
id: 1,
|
||||
title: "选择微信",
|
||||
subtitle: "选择微信",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: `选择${step2Title}`,
|
||||
subtitle: `选择${step2Title}`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "推送内容",
|
||||
subtitle: "推送内容",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "推送参数",
|
||||
subtitle: "推送参数",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
@@ -434,6 +560,12 @@ const CreatePushTask: React.FC = () => {
|
||||
{selectedContacts.length}个
|
||||
</span>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<span>
|
||||
推送账号: {selectedAccounts.length}个, 推送{step2Title}:{" "}
|
||||
{selectedContacts.length}个
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.footerRight}>
|
||||
{currentStep === 1 && (
|
||||
@@ -458,6 +590,14 @@ const CreatePushTask: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步 >
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
@@ -511,16 +651,24 @@ const CreatePushTask: React.FC = () => {
|
||||
onSelectedScriptGroupIdsChange={setSelectedScriptGroupIds}
|
||||
selectedContentLibraries={selectedContentLibraries}
|
||||
onSelectedContentLibrariesChange={setSelectedContentLibraries}
|
||||
aiRewriteEnabled={aiRewriteEnabled}
|
||||
onAiRewriteToggle={setAiRewriteEnabled}
|
||||
aiPrompt={aiPrompt}
|
||||
onAiPromptChange={setAiPrompt}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<StepPushParams
|
||||
selectedAccounts={selectedAccounts}
|
||||
selectedContacts={selectedContacts}
|
||||
targetLabel={step2Title}
|
||||
friendInterval={friendInterval}
|
||||
onFriendIntervalChange={setFriendInterval}
|
||||
messageInterval={messageInterval}
|
||||
onMessageIntervalChange={setMessageInterval}
|
||||
selectedTag={selectedTag}
|
||||
onSelectedTagChange={setSelectedTag}
|
||||
aiRewriteEnabled={aiRewriteEnabled}
|
||||
onAiRewriteToggle={setAiRewriteEnabled}
|
||||
aiPrompt={aiPrompt}
|
||||
onAiPromptChange={setAiPrompt}
|
||||
savedScriptGroups={savedScriptGroups}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,67 @@ export interface ContactItem {
|
||||
extendFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 消息类型定义
|
||||
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[];
|
||||
}>; // 话术组对象数组
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ const PushHistory: React.FC = () => {
|
||||
render: (type: PushType) => getPushTypeTag(type),
|
||||
},
|
||||
{
|
||||
title: "推送内容",
|
||||
title: "任务名称",
|
||||
dataIndex: "pushContent",
|
||||
key: "pushContent",
|
||||
ellipsis: true,
|
||||
|
||||
Reference in New Issue
Block a user