Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
@@ -23,12 +23,16 @@ import {
|
|||||||
SendOutlined,
|
SendOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import styles from "./PushTaskModal.module.scss";
|
import styles from "./PushTaskModal.module.scss";
|
||||||
import {
|
import { useCustomerStore } from "@/store/module/weChat/customer";
|
||||||
useCustomerStore,
|
|
||||||
} from "@/store/module/weChat/customer";
|
|
||||||
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
|
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
|
||||||
|
|
||||||
export type PushType = "friend-message" | "group-message" | "group-announcement";
|
const DEFAULT_FRIEND_INTERVAL: [number, number] = [3, 10];
|
||||||
|
const DEFAULT_MESSAGE_INTERVAL: [number, number] = [1, 3];
|
||||||
|
|
||||||
|
export type PushType =
|
||||||
|
| "friend-message"
|
||||||
|
| "group-message"
|
||||||
|
| "group-announcement";
|
||||||
|
|
||||||
interface PushTaskModalProps {
|
interface PushTaskModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -67,8 +71,12 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
|
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
|
||||||
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
|
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
|
||||||
const [messageContent, setMessageContent] = useState("");
|
const [messageContent, setMessageContent] = useState("");
|
||||||
const [friendInterval, setFriendInterval] = useState(10);
|
const [friendInterval, setFriendInterval] = useState<[number, number]>([
|
||||||
const [messageInterval, setMessageInterval] = useState(1);
|
...DEFAULT_FRIEND_INTERVAL,
|
||||||
|
]);
|
||||||
|
const [messageInterval, setMessageInterval] = useState<[number, number]>([
|
||||||
|
...DEFAULT_MESSAGE_INTERVAL,
|
||||||
|
]);
|
||||||
const [selectedTag, setSelectedTag] = useState<string>("");
|
const [selectedTag, setSelectedTag] = useState<string>("");
|
||||||
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
|
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
|
||||||
const [aiPrompt, setAiPrompt] = useState("");
|
const [aiPrompt, setAiPrompt] = useState("");
|
||||||
@@ -120,8 +128,8 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
setSelectedAccounts([]);
|
setSelectedAccounts([]);
|
||||||
setSelectedContacts([]);
|
setSelectedContacts([]);
|
||||||
setMessageContent("");
|
setMessageContent("");
|
||||||
setFriendInterval(10);
|
setFriendInterval([...DEFAULT_FRIEND_INTERVAL]);
|
||||||
setMessageInterval(1);
|
setMessageInterval([...DEFAULT_MESSAGE_INTERVAL]);
|
||||||
setSelectedTag("");
|
setSelectedTag("");
|
||||||
setAiRewriteEnabled(false);
|
setAiRewriteEnabled(false);
|
||||||
setAiPrompt("");
|
setAiPrompt("");
|
||||||
@@ -270,7 +278,9 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
setCurrentStep(2);
|
setCurrentStep(2);
|
||||||
} else if (currentStep === 2) {
|
} else if (currentStep === 2) {
|
||||||
if (selectedContacts.length === 0) {
|
if (selectedContacts.length === 0) {
|
||||||
message.warning(`请至少选择一个${pushType === "friend-message" ? "好友" : "群"}`);
|
message.warning(
|
||||||
|
`请至少选择一个${pushType === "friend-message" ? "好友" : "群"}`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentStep(3);
|
setCurrentStep(3);
|
||||||
@@ -343,7 +353,9 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
<div className={styles.accountCards}>
|
<div className={styles.accountCards}>
|
||||||
{filteredAccounts.length > 0 ? (
|
{filteredAccounts.length > 0 ? (
|
||||||
filteredAccounts.map(account => {
|
filteredAccounts.map(account => {
|
||||||
const isSelected = selectedAccounts.some(a => a.id === account.id);
|
const isSelected = selectedAccounts.some(
|
||||||
|
a => a.id === account.id,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={account.id}
|
key={account.id}
|
||||||
@@ -355,7 +367,8 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
size={48}
|
size={48}
|
||||||
style={{ backgroundColor: "#1890ff" }}
|
style={{ backgroundColor: "#1890ff" }}
|
||||||
>
|
>
|
||||||
{!account.avatar && (account.nickname || account.name || "").charAt(0)}
|
{!account.avatar &&
|
||||||
|
(account.nickname || account.name || "").charAt(0)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className={styles.cardName}>
|
<div className={styles.cardName}>
|
||||||
{account.nickname || account.name || "未知"}
|
{account.nickname || account.name || "未知"}
|
||||||
@@ -570,10 +583,7 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
<Button type="text" icon="⭐" />
|
<Button type="text" icon="⭐" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.aiRewriteSection}>
|
<div className={styles.aiRewriteSection}>
|
||||||
<Switch
|
<Switch checked={aiRewriteEnabled} onChange={setAiRewriteEnabled} />
|
||||||
checked={aiRewriteEnabled}
|
|
||||||
onChange={setAiRewriteEnabled}
|
|
||||||
/>
|
|
||||||
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
|
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
|
||||||
{aiRewriteEnabled && (
|
{aiRewriteEnabled && (
|
||||||
<Input
|
<Input
|
||||||
@@ -598,13 +608,16 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
<div className={styles.settingControl}>
|
<div className={styles.settingControl}>
|
||||||
<span>间隔时间(秒)</span>
|
<span>间隔时间(秒)</span>
|
||||||
<Slider
|
<Slider
|
||||||
min={10}
|
range
|
||||||
max={20}
|
min={1}
|
||||||
|
max={60}
|
||||||
value={friendInterval}
|
value={friendInterval}
|
||||||
onChange={setFriendInterval}
|
onChange={value => setFriendInterval(value as [number, number])}
|
||||||
style={{ flex: 1, margin: "0 16px" }}
|
style={{ flex: 1, margin: "0 16px" }}
|
||||||
/>
|
/>
|
||||||
<span>{friendInterval} - 20</span>
|
<span>
|
||||||
|
{friendInterval[0]} - {friendInterval[1]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.settingItem}>
|
<div className={styles.settingItem}>
|
||||||
@@ -612,13 +625,18 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
|||||||
<div className={styles.settingControl}>
|
<div className={styles.settingControl}>
|
||||||
<span>间隔时间(秒)</span>
|
<span>间隔时间(秒)</span>
|
||||||
<Slider
|
<Slider
|
||||||
|
range
|
||||||
min={1}
|
min={1}
|
||||||
max={12}
|
max={60}
|
||||||
value={messageInterval}
|
value={messageInterval}
|
||||||
onChange={setMessageInterval}
|
onChange={value =>
|
||||||
|
setMessageInterval(value as [number, number])
|
||||||
|
}
|
||||||
style={{ flex: 1, margin: "0 16px" }}
|
style={{ flex: 1, margin: "0 16px" }}
|
||||||
/>
|
/>
|
||||||
<span>{messageInterval} - 12</span>
|
<span>
|
||||||
|
{messageInterval[0]} - {messageInterval[1]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.settingItem}>
|
<div className={styles.settingItem}>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 获取客服列表
|
||||||
|
export function queryWorkbenchCreate(params) {
|
||||||
|
return request("/v1/workbench/create", params, "POST");
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
|
|||||||
|
|
||||||
import styles from "../../index.module.scss";
|
import styles from "../../index.module.scss";
|
||||||
import { ContactItem, PushType } from "../../types";
|
import { ContactItem, PushType } from "../../types";
|
||||||
|
import PoolSelection from "@/components/PoolSelection";
|
||||||
|
import type { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||||
|
|
||||||
interface ContactFilterValues {
|
interface ContactFilterValues {
|
||||||
includeTags: string[];
|
includeTags: string[];
|
||||||
@@ -61,6 +63,8 @@ interface StepSelectContactsProps {
|
|||||||
selectedAccounts: any[];
|
selectedAccounts: any[];
|
||||||
selectedContacts: ContactItem[];
|
selectedContacts: ContactItem[];
|
||||||
onChange: (contacts: ContactItem[]) => void;
|
onChange: (contacts: ContactItem[]) => void;
|
||||||
|
selectedTrafficPools: PoolSelectionItem[];
|
||||||
|
onTrafficPoolsChange: (pools: PoolSelectionItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||||
@@ -68,6 +72,8 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
|||||||
selectedAccounts,
|
selectedAccounts,
|
||||||
selectedContacts,
|
selectedContacts,
|
||||||
onChange,
|
onChange,
|
||||||
|
selectedTrafficPools,
|
||||||
|
onTrafficPoolsChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
|
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
|
||||||
const [loadingContacts, setLoadingContacts] = useState(false);
|
const [loadingContacts, setLoadingContacts] = useState(false);
|
||||||
@@ -415,6 +421,13 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
|||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<PoolSelection
|
||||||
|
selectedOptions={selectedTrafficPools}
|
||||||
|
onSelect={onTrafficPoolsChange}
|
||||||
|
placeholder="选择流量池包"
|
||||||
|
showSelectedList
|
||||||
|
selectedListMaxHeight={200}
|
||||||
|
/>
|
||||||
<div className={styles.contentBody}>
|
<div className={styles.contentBody}>
|
||||||
<div className={styles.contactList}>
|
<div className={styles.contactList}>
|
||||||
<div className={styles.listHeader}>
|
<div className={styles.listHeader}>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
// 创建内容库参数
|
||||||
|
export interface CreateContentLibraryParams {
|
||||||
|
name: string;
|
||||||
|
sourceType: number;
|
||||||
|
sourceFriends?: string[];
|
||||||
|
sourceGroups?: string[];
|
||||||
|
keywordInclude?: string[];
|
||||||
|
keywordExclude?: string[];
|
||||||
|
aiPrompt?: string;
|
||||||
|
timeEnabled?: number;
|
||||||
|
timeStart?: string;
|
||||||
|
timeEnd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建内容库
|
||||||
|
export function createContentLibrary(
|
||||||
|
params: CreateContentLibraryParams,
|
||||||
|
): Promise<any> {
|
||||||
|
return request("/v1/content/library/create", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除内容库
|
||||||
|
export function deleteContentLibrary(params: { id: number }) {
|
||||||
|
return request(`/v1/content/library/update`, params, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能话术改写
|
||||||
|
export function aiEditContent(params: { aiPrompt: string; content: string }) {
|
||||||
|
return request(`/v1/content/library/aiEditContent`, params, "GET");
|
||||||
|
}
|
||||||
@@ -254,6 +254,7 @@
|
|||||||
|
|
||||||
.aiRewriteSection {
|
.aiRewriteSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -272,6 +273,16 @@
|
|||||||
.aiRewriteInput {
|
.aiRewriteInput {
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aiRewriteActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiRewriteButton {
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
message as antdMessage,
|
message as antdMessage,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { CopyOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
CopyOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import type { CheckboxChangeEvent } from "antd/es/checkbox";
|
import type { CheckboxChangeEvent } from "antd/es/checkbox";
|
||||||
|
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
@@ -18,6 +23,12 @@ import { ContactItem, ScriptGroup } from "../../types";
|
|||||||
import InputMessage from "./InputMessage/InputMessage";
|
import InputMessage from "./InputMessage/InputMessage";
|
||||||
import ContentLibrarySelector from "./ContentLibrarySelector";
|
import ContentLibrarySelector from "./ContentLibrarySelector";
|
||||||
import type { ContentItem } from "@/components/ContentSelection/data";
|
import type { ContentItem } from "@/components/ContentSelection/data";
|
||||||
|
import {
|
||||||
|
createContentLibrary,
|
||||||
|
deleteContentLibrary,
|
||||||
|
aiEditContent,
|
||||||
|
type CreateContentLibraryParams,
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
interface StepSendMessageProps {
|
interface StepSendMessageProps {
|
||||||
selectedAccounts: any[];
|
selectedAccounts: any[];
|
||||||
@@ -25,10 +36,10 @@ interface StepSendMessageProps {
|
|||||||
targetLabel: string;
|
targetLabel: string;
|
||||||
messageContent: string;
|
messageContent: string;
|
||||||
onMessageContentChange: (value: string) => void;
|
onMessageContentChange: (value: string) => void;
|
||||||
friendInterval: number;
|
friendInterval: [number, number];
|
||||||
onFriendIntervalChange: (value: number) => void;
|
onFriendIntervalChange: (value: [number, number]) => void;
|
||||||
messageInterval: number;
|
messageInterval: [number, number];
|
||||||
onMessageIntervalChange: (value: number) => void;
|
onMessageIntervalChange: (value: [number, number]) => void;
|
||||||
selectedTag: string;
|
selectedTag: string;
|
||||||
onSelectedTagChange: (value: string) => void;
|
onSelectedTagChange: (value: string) => void;
|
||||||
aiRewriteEnabled: boolean;
|
aiRewriteEnabled: boolean;
|
||||||
@@ -74,6 +85,10 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
selectedContentLibraries,
|
selectedContentLibraries,
|
||||||
onSelectedContentLibrariesChange,
|
onSelectedContentLibrariesChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [savingScriptGroup, setSavingScriptGroup] = useState(false);
|
||||||
|
const [aiRewriting, setAiRewriting] = useState(false);
|
||||||
|
const [deletingGroupIds, setDeletingGroupIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleAddMessage = useCallback(
|
const handleAddMessage = useCallback(
|
||||||
(content?: string, showSuccess?: boolean) => {
|
(content?: string, showSuccess?: boolean) => {
|
||||||
const finalContent = (content ?? messageContent).trim();
|
const finalContent = (content ?? messageContent).trim();
|
||||||
@@ -103,24 +118,57 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
[currentScriptMessages, onCurrentScriptMessagesChange],
|
[currentScriptMessages, onCurrentScriptMessagesChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveScriptGroup = useCallback(() => {
|
const handleSaveScriptGroup = useCallback(async () => {
|
||||||
|
if (savingScriptGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentScriptMessages.length === 0) {
|
if (currentScriptMessages.length === 0) {
|
||||||
antdMessage.warning("请先添加消息内容");
|
antdMessage.warning("请先添加消息内容");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const groupName =
|
const groupName =
|
||||||
currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`;
|
currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`;
|
||||||
const newGroup: ScriptGroup = {
|
const messages = [...currentScriptMessages];
|
||||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
const params: CreateContentLibraryParams = {
|
||||||
name: groupName,
|
name: groupName,
|
||||||
messages: currentScriptMessages,
|
sourceType: 1,
|
||||||
|
keywordInclude: messages,
|
||||||
};
|
};
|
||||||
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
|
const trimmedPrompt = aiPrompt.trim();
|
||||||
onCurrentScriptMessagesChange([]);
|
if (aiRewriteEnabled && trimmedPrompt) {
|
||||||
onCurrentScriptNameChange("");
|
params.aiPrompt = trimmedPrompt;
|
||||||
onMessageContentChange("");
|
}
|
||||||
antdMessage.success("已保存为话术组");
|
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
|
||||||
|
try {
|
||||||
|
setSavingScriptGroup(true);
|
||||||
|
hideLoading = antdMessage.loading("正在保存话术组...", 0);
|
||||||
|
const response = await createContentLibrary(params);
|
||||||
|
hideLoading?.();
|
||||||
|
const responseId =
|
||||||
|
response?.id ?? response?.data?.id ?? response?.libraryId;
|
||||||
|
const newGroup: ScriptGroup = {
|
||||||
|
id:
|
||||||
|
responseId !== undefined
|
||||||
|
? String(responseId)
|
||||||
|
: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
name: groupName,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
|
||||||
|
onCurrentScriptMessagesChange([]);
|
||||||
|
onCurrentScriptNameChange("");
|
||||||
|
onMessageContentChange("");
|
||||||
|
antdMessage.success("已保存为话术组");
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading?.();
|
||||||
|
console.error("保存话术组失败:", error);
|
||||||
|
antdMessage.error("保存失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setSavingScriptGroup(false);
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
|
aiPrompt,
|
||||||
|
aiRewriteEnabled,
|
||||||
currentScriptMessages,
|
currentScriptMessages,
|
||||||
currentScriptName,
|
currentScriptName,
|
||||||
onCurrentScriptMessagesChange,
|
onCurrentScriptMessagesChange,
|
||||||
@@ -128,6 +176,100 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
onMessageContentChange,
|
onMessageContentChange,
|
||||||
onSavedScriptGroupsChange,
|
onSavedScriptGroupsChange,
|
||||||
savedScriptGroups,
|
savedScriptGroups,
|
||||||
|
savingScriptGroup,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleAiRewrite = useCallback(async () => {
|
||||||
|
if (!aiRewriteEnabled) {
|
||||||
|
antdMessage.warning("请先开启AI智能话术改写");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmedPrompt = aiPrompt.trim();
|
||||||
|
const originalContent = messageContent;
|
||||||
|
const trimmedContent = originalContent.trim();
|
||||||
|
if (!trimmedPrompt) {
|
||||||
|
antdMessage.warning("请输入改写提示词");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trimmedContent) {
|
||||||
|
antdMessage.warning("请输入需要改写的内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (aiRewriting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
|
||||||
|
try {
|
||||||
|
setAiRewriting(true);
|
||||||
|
hideLoading = antdMessage.loading("AI正在改写话术...", 0);
|
||||||
|
const response = await aiEditContent({
|
||||||
|
aiPrompt: trimmedPrompt,
|
||||||
|
content: originalContent,
|
||||||
|
});
|
||||||
|
hideLoading?.();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
onMessageContentChange(rewrittenContent.trim());
|
||||||
|
antdMessage.success("AI改写完成,请确认内容");
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading?.();
|
||||||
|
console.error("AI改写失败:", error);
|
||||||
|
antdMessage.error("AI改写失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setAiRewriting(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
aiPrompt,
|
||||||
|
aiRewriting,
|
||||||
|
aiRewriteEnabled,
|
||||||
|
messageContent,
|
||||||
|
onMessageContentChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleApplyGroup = useCallback(
|
const handleApplyGroup = useCallback(
|
||||||
@@ -145,23 +287,47 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteGroup = useCallback(
|
const handleDeleteGroup = useCallback(
|
||||||
(groupId: string) => {
|
async (groupId: string) => {
|
||||||
const nextGroups = savedScriptGroups.filter(
|
if (deletingGroupIds.includes(groupId)) {
|
||||||
group => group.id !== groupId,
|
return;
|
||||||
);
|
}
|
||||||
onSavedScriptGroupsChange(nextGroups);
|
const numericGroupId = Number(groupId);
|
||||||
if (selectedScriptGroupIds.includes(groupId)) {
|
if (Number.isNaN(numericGroupId)) {
|
||||||
const nextSelected = selectedScriptGroupIds.filter(
|
antdMessage.error("无法删除:缺少有效的内容库ID");
|
||||||
id => id !== groupId,
|
return;
|
||||||
);
|
}
|
||||||
onSelectedScriptGroupIdsChange(nextSelected);
|
let hideLoading: ReturnType<typeof antdMessage.loading> | undefined;
|
||||||
|
try {
|
||||||
|
setDeletingGroupIds(prev => [...prev, groupId]);
|
||||||
|
hideLoading = antdMessage.loading("正在删除话术组...", 0);
|
||||||
|
await deleteContentLibrary({ id: numericGroupId });
|
||||||
|
hideLoading?.();
|
||||||
|
const nextGroups = savedScriptGroups.filter(
|
||||||
|
group => group.id !== groupId,
|
||||||
|
);
|
||||||
|
onSavedScriptGroupsChange(nextGroups);
|
||||||
|
if (selectedScriptGroupIds.includes(groupId)) {
|
||||||
|
const nextSelected = selectedScriptGroupIds.filter(
|
||||||
|
id => id !== groupId,
|
||||||
|
);
|
||||||
|
onSelectedScriptGroupIdsChange(nextSelected);
|
||||||
|
}
|
||||||
|
antdMessage.success("已删除话术组");
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading?.();
|
||||||
|
console.error("删除话术组失败:", error);
|
||||||
|
antdMessage.error("删除失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setDeletingGroupIds(prev =>
|
||||||
|
prev.filter(deletingId => deletingId !== groupId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
antdMessage.success("已删除话术组");
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
deletingGroupIds,
|
||||||
onSavedScriptGroupsChange,
|
onSavedScriptGroupsChange,
|
||||||
savedScriptGroups,
|
|
||||||
onSelectedScriptGroupIdsChange,
|
onSelectedScriptGroupIdsChange,
|
||||||
|
savedScriptGroups,
|
||||||
selectedScriptGroupIds,
|
selectedScriptGroupIds,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -192,7 +358,8 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleSaveScriptGroup}
|
onClick={handleSaveScriptGroup}
|
||||||
disabled={currentScriptMessages.length === 0}
|
disabled={currentScriptMessages.length === 0 || savingScriptGroup}
|
||||||
|
loading={savingScriptGroup}
|
||||||
>
|
>
|
||||||
保存为话术组
|
保存为话术组
|
||||||
</Button>
|
</Button>
|
||||||
@@ -279,6 +446,8 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
className={styles.actionButton}
|
className={styles.actionButton}
|
||||||
onClick={() => handleDeleteGroup(group.id)}
|
onClick={() => handleDeleteGroup(group.id)}
|
||||||
|
loading={deletingGroupIds.includes(group.id)}
|
||||||
|
disabled={deletingGroupIds.includes(group.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,23 +476,37 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
checked={aiRewriteEnabled}
|
checked={aiRewriteEnabled}
|
||||||
onChange={onAiRewriteToggle}
|
onChange={onAiRewriteToggle}
|
||||||
/>
|
/>
|
||||||
<span className={styles.aiRewriteLabel}>AI智能话术改写</span>
|
<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>
|
||||||
{aiRewriteEnabled && (
|
|
||||||
<Input
|
|
||||||
placeholder="输入改写提示词"
|
|
||||||
value={aiPrompt}
|
|
||||||
onChange={event => onAiPromptChange(event.target.value)}
|
|
||||||
className={styles.aiRewriteInput}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={() => handleAddMessage(undefined, true)}
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,13 +519,18 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
<div className={styles.settingControl}>
|
<div className={styles.settingControl}>
|
||||||
<span>间隔时间(秒)</span>
|
<span>间隔时间(秒)</span>
|
||||||
<Slider
|
<Slider
|
||||||
min={10}
|
range
|
||||||
max={20}
|
min={1}
|
||||||
|
max={60}
|
||||||
value={friendInterval}
|
value={friendInterval}
|
||||||
onChange={value => onFriendIntervalChange(value as number)}
|
onChange={value =>
|
||||||
|
onFriendIntervalChange(value as [number, number])
|
||||||
|
}
|
||||||
style={{ flex: 1, margin: "0 16px" }}
|
style={{ flex: 1, margin: "0 16px" }}
|
||||||
/>
|
/>
|
||||||
<span>{friendInterval} - 20</span>
|
<span>
|
||||||
|
{friendInterval[0]} - {friendInterval[1]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.settingItem}>
|
<div className={styles.settingItem}>
|
||||||
@@ -350,13 +538,18 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
|||||||
<div className={styles.settingControl}>
|
<div className={styles.settingControl}>
|
||||||
<span>间隔时间(秒)</span>
|
<span>间隔时间(秒)</span>
|
||||||
<Slider
|
<Slider
|
||||||
|
range
|
||||||
min={1}
|
min={1}
|
||||||
max={12}
|
max={60}
|
||||||
value={messageInterval}
|
value={messageInterval}
|
||||||
onChange={value => onMessageIntervalChange(value as number)}
|
onChange={value =>
|
||||||
|
onMessageIntervalChange(value as [number, number])
|
||||||
|
}
|
||||||
style={{ flex: 1, margin: "0 16px" }}
|
style={{ flex: 1, margin: "0 16px" }}
|
||||||
/>
|
/>
|
||||||
<span>{messageInterval} - 12</span>
|
<span>
|
||||||
|
{messageInterval[0]} - {messageInterval[1]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,47 @@ import StepSendMessage from "./components/StepSendMessage";
|
|||||||
import { ContactItem, PushType, ScriptGroup } from "./types";
|
import { ContactItem, PushType, ScriptGroup } from "./types";
|
||||||
import StepIndicator from "@/components/StepIndicator";
|
import StepIndicator from "@/components/StepIndicator";
|
||||||
import type { ContentItem } from "@/components/ContentSelection/data";
|
import type { ContentItem } from "@/components/ContentSelection/data";
|
||||||
|
import type { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||||
|
import { queryWorkbenchCreate } from "./api";
|
||||||
|
|
||||||
|
const DEFAULT_FRIEND_INTERVAL: [number, number] = [3, 10];
|
||||||
|
const DEFAULT_MESSAGE_INTERVAL: [number, number] = [1, 3];
|
||||||
|
|
||||||
|
const DEFAULT_TIME_RANGE: Record<
|
||||||
|
PushType,
|
||||||
|
{ startTime: string; endTime: string }
|
||||||
|
> = {
|
||||||
|
"friend-message": { startTime: "10:00", endTime: "22:00" },
|
||||||
|
"group-message": { startTime: "09:00", endTime: "20:00" },
|
||||||
|
"group-announcement": { startTime: "08:30", endTime: "18:30" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PUSH_ORDER: Record<PushType, 1 | 2> = {
|
||||||
|
"friend-message": 1,
|
||||||
|
"group-message": 1,
|
||||||
|
"group-announcement": 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_PER_DAY: Record<PushType, number> = {
|
||||||
|
"friend-message": 150,
|
||||||
|
"group-message": 200,
|
||||||
|
"group-announcement": 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_AUTO_START: Record<PushType, 0 | 1> = {
|
||||||
|
"friend-message": 1,
|
||||||
|
"group-message": 1,
|
||||||
|
"group-announcement": 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PUSH_TYPE: Record<PushType, 0 | 1> = {
|
||||||
|
"friend-message": 0,
|
||||||
|
"group-message": 0,
|
||||||
|
"group-announcement": 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidNumber = (value: unknown): value is number =>
|
||||||
|
typeof value === "number" && Number.isFinite(value);
|
||||||
|
|
||||||
const CreatePushTask: React.FC = () => {
|
const CreatePushTask: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -44,11 +85,19 @@ const CreatePushTask: React.FC = () => {
|
|||||||
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
|
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
|
||||||
ContentItem[]
|
ContentItem[]
|
||||||
>([]);
|
>([]);
|
||||||
const [friendInterval, setFriendInterval] = useState(10);
|
const [friendInterval, setFriendInterval] = useState<[number, number]>([
|
||||||
const [messageInterval, setMessageInterval] = useState(1);
|
...DEFAULT_FRIEND_INTERVAL,
|
||||||
|
]);
|
||||||
|
const [messageInterval, setMessageInterval] = useState<[number, number]>([
|
||||||
|
...DEFAULT_MESSAGE_INTERVAL,
|
||||||
|
]);
|
||||||
const [selectedTag, setSelectedTag] = useState<string>("");
|
const [selectedTag, setSelectedTag] = useState<string>("");
|
||||||
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
|
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
|
||||||
const [aiPrompt, setAiPrompt] = useState("");
|
const [aiPrompt, setAiPrompt] = useState("");
|
||||||
|
const [creatingTask, setCreatingTask] = useState(false);
|
||||||
|
const [selectedTrafficPools, setSelectedTrafficPools] = useState<
|
||||||
|
PoolSelectionItem[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
const customerList = useCustomerStore(state => state.customerList);
|
const customerList = useCustomerStore(state => state.customerList);
|
||||||
|
|
||||||
@@ -131,33 +180,206 @@ const CreatePushTask: React.FC = () => {
|
|||||||
setSelectedAccounts([]);
|
setSelectedAccounts([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = async () => {
|
||||||
|
if (creatingTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const selectedGroups = savedScriptGroups.filter(group =>
|
const selectedGroups = savedScriptGroups.filter(group =>
|
||||||
selectedScriptGroupIds.includes(group.id),
|
selectedScriptGroupIds.includes(group.id),
|
||||||
);
|
);
|
||||||
if (currentScriptMessages.length === 0 && selectedGroups.length === 0) {
|
if (
|
||||||
message.warning("请先添加话术内容或选择话术组");
|
currentScriptMessages.length === 0 &&
|
||||||
|
selectedGroups.length === 0 &&
|
||||||
|
selectedContentLibraries.length === 0
|
||||||
|
) {
|
||||||
|
message.warning("请添加话术内容、选择话术组或内容库");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: 实现发送逻辑
|
const manualMessages = currentScriptMessages
|
||||||
console.log("发送推送", {
|
.map(item => item.trim())
|
||||||
pushType: validPushType,
|
.filter(Boolean);
|
||||||
accounts: selectedAccounts,
|
if (validPushType === "group-announcement" && manualMessages.length === 0) {
|
||||||
contacts: selectedContacts,
|
message.warning("请先填写公告内容");
|
||||||
currentScript: {
|
return;
|
||||||
name: currentScriptName,
|
}
|
||||||
messages: currentScriptMessages,
|
const toNumberId = (value: unknown) => {
|
||||||
},
|
const numeric = Number(value);
|
||||||
selectedScriptGroups: selectedGroups,
|
return Number.isFinite(numeric) && !Number.isNaN(numeric)
|
||||||
friendInterval,
|
? numeric
|
||||||
messageInterval,
|
: null;
|
||||||
selectedTag,
|
};
|
||||||
aiRewriteEnabled,
|
const contentGroupIds = Array.from(
|
||||||
aiPrompt,
|
new Set(
|
||||||
selectedContentLibraries,
|
[
|
||||||
});
|
...selectedContentLibraries
|
||||||
message.success("推送任务已创建");
|
.map(item => toNumberId(item?.id))
|
||||||
navigate("/pc/powerCenter/message-push-assistant");
|
.filter((id): id is number => id !== null),
|
||||||
|
...selectedScriptGroupIds
|
||||||
|
.map(id => toNumberId(id))
|
||||||
|
.filter((id): id is number => id !== null),
|
||||||
|
].filter((id): id is number => id !== null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
manualMessages.length === 0 &&
|
||||||
|
selectedGroups.length === 0 &&
|
||||||
|
contentGroupIds.length === 0
|
||||||
|
) {
|
||||||
|
message.warning("缺少有效的话术内容,请重新检查");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ownerWechatIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedAccounts
|
||||||
|
.map(account => toNumberId(account?.id))
|
||||||
|
.filter((id): id is number => id !== null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ownerWechatIds.length === 0) {
|
||||||
|
message.error("缺少有效的推送账号信息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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()}`;
|
||||||
|
const deviceGroupIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedAccounts
|
||||||
|
.map(account => toNumberId(account?.currentDeviceId))
|
||||||
|
.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],
|
||||||
|
targetType: validPushType === "friend-message" ? 2 : 1,
|
||||||
|
groupPushSubType: validPushType === "group-announcement" ? 2 : 1,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
maxPerDay,
|
||||||
|
pushOrder,
|
||||||
|
friendIntervalMin,
|
||||||
|
friendIntervalMax,
|
||||||
|
messageIntervalMin,
|
||||||
|
messageIntervalMax,
|
||||||
|
isRandomTemplate: selectedScriptGroupIds.length > 1 ? 1 : 0,
|
||||||
|
contentGroups: contentGroupIds,
|
||||||
|
postPushTags: normalizedPostPushTags,
|
||||||
|
ownerWechatIds,
|
||||||
|
enableAiRewrite: aiRewriteEnabled ? 1 : 0,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (validPushType === "friend-message") {
|
||||||
|
basePayload.wechatFriends = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedContacts
|
||||||
|
.map(contact => toNumberId(contact?.id))
|
||||||
|
.filter((id): id is number => id !== null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
basePayload.targetType = 2;
|
||||||
|
} else {
|
||||||
|
const groupIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
selectedContacts
|
||||||
|
.map(contact => toNumberId(contact.groupId ?? contact.id))
|
||||||
|
.filter((id): id is number => id !== null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
basePayload.wechatGroups = groupIds;
|
||||||
|
basePayload.groupPushSubType =
|
||||||
|
validPushType === "group-announcement" ? 2 : 1;
|
||||||
|
basePayload.targetType = 1;
|
||||||
|
if (validPushType === "group-announcement") {
|
||||||
|
basePayload.announcementContent = manualMessages.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hideLoading: ReturnType<typeof message.loading> | undefined;
|
||||||
|
try {
|
||||||
|
setCreatingTask(true);
|
||||||
|
hideLoading = message.loading("正在创建推送任务...", 0);
|
||||||
|
await queryWorkbenchCreate(basePayload);
|
||||||
|
hideLoading?.();
|
||||||
|
message.success("推送任务已创建");
|
||||||
|
navigate("/pc/powerCenter/message-push-assistant");
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading?.();
|
||||||
|
console.error("创建推送任务失败:", error);
|
||||||
|
const errorMessage =
|
||||||
|
(error as any)?.message ||
|
||||||
|
(error as any)?.response?.data?.message ||
|
||||||
|
"创建推送任务失败,请稍后重试";
|
||||||
|
message.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setCreatingTask(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -242,6 +464,8 @@ const CreatePushTask: React.FC = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
loading={creatingTask}
|
||||||
|
disabled={creatingTask}
|
||||||
>
|
>
|
||||||
一键发送
|
一键发送
|
||||||
</Button>
|
</Button>
|
||||||
@@ -266,6 +490,8 @@ const CreatePushTask: React.FC = () => {
|
|||||||
selectedAccounts={selectedAccounts}
|
selectedAccounts={selectedAccounts}
|
||||||
selectedContacts={selectedContacts}
|
selectedContacts={selectedContacts}
|
||||||
onChange={setSelectedContacts}
|
onChange={setSelectedContacts}
|
||||||
|
selectedTrafficPools={selectedTrafficPools}
|
||||||
|
onTrafficPoolsChange={setSelectedTrafficPools}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
帮我对接数据,以下是传参实例,三种模式都是同一界面的。
|
||||||
|
|
||||||
|
群发助手传参实例
|
||||||
|
{
|
||||||
|
"name": "群群发-新品宣传", // 任务名称
|
||||||
|
"type": 3, // 工作台类型:3=群消息推送
|
||||||
|
"autoStart": 1, // 保存后自动启动
|
||||||
|
"status": 1, // 是否启用
|
||||||
|
"pushType": 0, // 推送方式:0=定时,1=立即
|
||||||
|
"targetType": 1, // 目标类型:1=群推送
|
||||||
|
"groupPushSubType": 1, // 群推送子类型:1=群群发,2=群公告
|
||||||
|
"startTime": "09:00", // 推送起始时间
|
||||||
|
"endTime": "20:00", // 推送结束时间
|
||||||
|
"maxPerDay": 200, // 每日最大推送群数
|
||||||
|
"pushOrder": 1, // 推送顺序:1=最早优先,2=最新优先
|
||||||
|
"wechatGroups": [102, 205, 318], // 选择的微信群 ID 列表
|
||||||
|
"contentGroups": [11, 12], // 关联内容库 ID 列表
|
||||||
|
"friendIntervalMin": 10, // 群间最小间隔(秒)
|
||||||
|
"friendIntervalMax": 25, // 群间最大间隔(秒)
|
||||||
|
"messageIntervalMin": 2, // 同一群消息间最小间隔(秒)
|
||||||
|
"messageIntervalMax": 6, // 同一群消息间最大间隔(秒)
|
||||||
|
"isRandomTemplate": 1, // 是否随机选择话术模板
|
||||||
|
"postPushTags": [301, 302], // 推送完成后打的标签
|
||||||
|
ownerWechatIds:[123123,1231231] //客服id
|
||||||
|
}
|
||||||
|
|
||||||
|
//群公告传参实例
|
||||||
|
{
|
||||||
|
"name": "群公告-双11活动", // 任务名称
|
||||||
|
"type": 3, // 群消息推送
|
||||||
|
"autoStart": 0, // 不自动启动
|
||||||
|
"status": 1, // 启用
|
||||||
|
"pushType": 1, // 立即推送
|
||||||
|
"targetType": 1, // 群推送
|
||||||
|
"groupPushSubType": 2, // 群公告
|
||||||
|
"startTime": "08:30", // 开始时间
|
||||||
|
"endTime": "18:30", // 结束时间
|
||||||
|
"maxPerDay": 80, // 每日最大公告数
|
||||||
|
"pushOrder": 2, // 最新优先
|
||||||
|
"wechatGroups": [5021, 5026], // 公告目标群
|
||||||
|
"announcementContent": "…", // 公告正文
|
||||||
|
"enableAiRewrite": 1, // 启用 AI 改写
|
||||||
|
"aiRewritePrompt": "保持活泼口吻…", // AI 改写提示词
|
||||||
|
"contentGroups": [21], // 关联内容库
|
||||||
|
"friendIntervalMin": 15, // 群间最小间隔
|
||||||
|
"friendIntervalMax": 30, // 群间最大间隔
|
||||||
|
"messageIntervalMin": 3, // 消息间最小间隔
|
||||||
|
"messageIntervalMax": 9, // 消息间最大间隔
|
||||||
|
"isRandomTemplate": 0, // 不随机模板
|
||||||
|
"postPushTags": [], // 推送后标签
|
||||||
|
ownerWechatIds:[123123,1231231] //客服id
|
||||||
|
}
|
||||||
|
|
||||||
|
//好友传参实例
|
||||||
|
{
|
||||||
|
"name": "好友私聊-新客转化", // 任务名称
|
||||||
|
"type": 3, // 群消息推送
|
||||||
|
"autoStart": 1, // 自动启动
|
||||||
|
"status": 1, // 启用
|
||||||
|
"pushType": 0, // 定时推送
|
||||||
|
"targetType": 2, // 目标类型:2=好友推送
|
||||||
|
"groupPushSubType": 1, // 固定为群群发(好友推送不支持公告)
|
||||||
|
"startTime": "10:00", // 开始时间
|
||||||
|
"endTime": "22:00", // 结束时间
|
||||||
|
"maxPerDay": 150, // 每日最大推送好友数
|
||||||
|
"pushOrder": 1, // 最早优先
|
||||||
|
"wechatFriends": ["12312"], // 指定好友列表(可为空数组)
|
||||||
|
"deviceGroups": [9001, 9002], // 必选:推送设备分组 ID
|
||||||
|
"contentGroups": [41, 42], // 话术内容库
|
||||||
|
"friendIntervalMin": 12, // 好友间最小间隔
|
||||||
|
"friendIntervalMax": 28, // 好友间最大间隔
|
||||||
|
"messageIntervalMin": 4, // 消息间最小间隔
|
||||||
|
"messageIntervalMax": 10, // 消息间最大间隔
|
||||||
|
"isRandomTemplate": 1, // 随机话术
|
||||||
|
"postPushTags": [501], // 推送后标签
|
||||||
|
ownerWechatIds:[123123,1231231] //客服id
|
||||||
|
}
|
||||||
|
|
||||||
|
请求接口是 queryWorkbenchCreate
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
* 4. 提供回调机制通知组件更新
|
* 4. 提供回调机制通知组件更新
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Dexie from "dexie";
|
||||||
import { db, chatSessionService, ChatSession } from "../db";
|
import { db, chatSessionService, ChatSession } from "../db";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
|
|
||||||
@@ -243,7 +244,9 @@ export class MessageManager {
|
|||||||
"userId",
|
"userId",
|
||||||
userId,
|
userId,
|
||||||
)) as ChatSession[];
|
)) as ChatSession[];
|
||||||
const localSessionMap = new Map(localSessions.map(s => [s.id, s]));
|
const localSessionMap = new Map(
|
||||||
|
localSessions.map(session => [session.serverId, session]),
|
||||||
|
);
|
||||||
|
|
||||||
// 2. 转换服务器数据为统一格式
|
// 2. 转换服务器数据为统一格式
|
||||||
const serverSessions: ChatSession[] = [];
|
const serverSessions: ChatSession[] = [];
|
||||||
@@ -264,16 +267,18 @@ export class MessageManager {
|
|||||||
serverSessions.push(...groups);
|
serverSessions.push(...groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSessionMap = new Map(serverSessions.map(s => [s.id, s]));
|
const serverSessionMap = new Map(
|
||||||
|
serverSessions.map(session => [session.serverId, session]),
|
||||||
|
);
|
||||||
|
|
||||||
// 3. 计算差异
|
// 3. 计算差异
|
||||||
const toAdd: ChatSession[] = [];
|
const toAdd: ChatSession[] = [];
|
||||||
const toUpdate: ChatSession[] = [];
|
const toUpdate: ChatSession[] = [];
|
||||||
const toDelete: number[] = [];
|
const toDelete: string[] = [];
|
||||||
|
|
||||||
// 检查新增和更新
|
// 检查新增和更新
|
||||||
for (const serverSession of serverSessions) {
|
for (const serverSession of serverSessions) {
|
||||||
const localSession = localSessionMap.get(serverSession.id);
|
const localSession = localSessionMap.get(serverSession.serverId);
|
||||||
|
|
||||||
if (!localSession) {
|
if (!localSession) {
|
||||||
toAdd.push(serverSession);
|
toAdd.push(serverSession);
|
||||||
@@ -286,8 +291,8 @@ export class MessageManager {
|
|||||||
|
|
||||||
// 检查删除
|
// 检查删除
|
||||||
for (const localSession of localSessions) {
|
for (const localSession of localSessions) {
|
||||||
if (!serverSessionMap.has(localSession.id)) {
|
if (!serverSessionMap.has(localSession.serverId)) {
|
||||||
toDelete.push(localSession.id);
|
toDelete.push(localSession.serverId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +339,19 @@ export class MessageManager {
|
|||||||
serverId: `${session.type}_${session.id}`,
|
serverId: `${session.type}_${session.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await db.chatSessions.bulkAdd(dataToInsert);
|
try {
|
||||||
|
await db.chatSessions.bulkAdd(dataToInsert);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Dexie.BulkError) {
|
||||||
|
console.warn(
|
||||||
|
`批量新增会话时检测到重复主键,切换为 bulkPut 以覆盖更新。错误详情:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
await db.chatSessions.bulkPut(dataToInsert);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -357,14 +374,16 @@ export class MessageManager {
|
|||||||
*/
|
*/
|
||||||
private static async batchDeleteSessions(
|
private static async batchDeleteSessions(
|
||||||
userId: number,
|
userId: number,
|
||||||
sessionIds: number[],
|
serverIds: string[],
|
||||||
) {
|
) {
|
||||||
if (sessionIds.length === 0) return;
|
if (serverIds.length === 0) return;
|
||||||
|
|
||||||
|
const serverIdSet = new Set(serverIds);
|
||||||
|
|
||||||
await db.chatSessions
|
await db.chatSessions
|
||||||
.where("userId")
|
.where("userId")
|
||||||
.equals(userId)
|
.equals(userId)
|
||||||
.and(session => sessionIds.includes(session.id))
|
.and(session => serverIdSet.has(session.serverId))
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user