重构PushTask组件以支持用于消息管理的脚本组。引入新的状态变量来处理脚本消息和组,更新消息发送逻辑,并增强用于添加和管理脚本组的UI。清理样式,改善消息输入区的用户体验。
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
.chatFooter {
|
||||
background: #f7f7f7;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: #e6e6e6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
min-width: 60px;
|
||||
font-size: 13px;
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
|
||||
&:hover {
|
||||
background: #06ad56;
|
||||
border-color: #06ad56;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #059748;
|
||||
border-color: #059748;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #b3b3b3;
|
||||
border-color: #b3b3b3;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hintButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inputToolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
.stepContent {
|
||||
.stepHeader {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
|
||||
.leftColumn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 400px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
border: 2px dashed #52c41a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f6ffed;
|
||||
|
||||
.previewTitle {
|
||||
font-size: 14px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.messageBubble {
|
||||
min-height: 60px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
.currentEditingLabel {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.savedScriptGroups {
|
||||
.scriptGroupTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scriptGroupItem {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
|
||||
.scriptGroupHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.scriptGroupLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
:global(.ant-radio) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.scriptGroupName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.actionButton {
|
||||
padding: 4px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupContent {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageInputArea {
|
||||
.messageInput {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.attachmentButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aiRewriteSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageHint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.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: 1200px) {
|
||||
.step3Content {
|
||||
.rightColumn {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.step3Content {
|
||||
flex-direction: column;
|
||||
|
||||
.leftColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import ContentSelection from "@/components/ContentSelection";
|
||||
import { ContentItem } from "@/components/ContentSelection/data";
|
||||
import InputMessage from "./InputMessage/InputMessage";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface StepSendMessageProps {
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import ContentSelection from "@/components/ContentSelection";
|
||||
import type { ContentItem } from "@/components/ContentSelection/data";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface ContentLibrarySelectorProps {
|
||||
selectedContentLibraries: ContentItem[];
|
||||
onSelectedContentLibrariesChange: (selectedItems: ContentItem[]) => void;
|
||||
}
|
||||
|
||||
const ContentLibrarySelector: React.FC<ContentLibrarySelectorProps> = ({
|
||||
selectedContentLibraries,
|
||||
onSelectedContentLibrariesChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.contentLibrarySelector}>
|
||||
<div className={styles.contentLibraryHeader}>
|
||||
<div className={styles.contentLibraryTitle}>内容库选择</div>
|
||||
<div className={styles.contentLibraryHint}>
|
||||
选择内容库可快速引用现有话术
|
||||
</div>
|
||||
</div>
|
||||
<ContentSelection
|
||||
selectedOptions={selectedContentLibraries}
|
||||
onSelect={onSelectedContentLibrariesChange}
|
||||
onConfirm={onSelectedContentLibrariesChange}
|
||||
placeholder="请选择内容库"
|
||||
selectedListMaxHeight={200}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentLibrarySelector;
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Input, message as antdMessage } from "antd";
|
||||
import { FolderOutlined, PictureOutlined } from "@ant-design/icons";
|
||||
|
||||
import { EmojiPicker } from "@/components/EmojiSeclection";
|
||||
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
|
||||
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
|
||||
import AudioRecorder from "@/components/Upload/AudioRecorder";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
type FileTypeValue = 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
interface InputMessageProps {
|
||||
defaultValue?: string;
|
||||
onContentChange?: (value: string) => void;
|
||||
onSend?: (value: string) => void;
|
||||
clearOnSend?: boolean;
|
||||
placeholder?: string;
|
||||
hint?: React.ReactNode;
|
||||
}
|
||||
|
||||
const FileType: Record<string, FileTypeValue> = {
|
||||
TEXT: 1,
|
||||
IMAGE: 2,
|
||||
VIDEO: 3,
|
||||
AUDIO: 4,
|
||||
FILE: 5,
|
||||
};
|
||||
|
||||
const getMsgTypeByFileFormat = (filePath: string): number => {
|
||||
const extension = filePath.toLowerCase().split(".").pop() || "";
|
||||
const imageFormats = [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
"svg",
|
||||
"ico",
|
||||
];
|
||||
if (imageFormats.includes(extension)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const videoFormats = [
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"mkv",
|
||||
"webm",
|
||||
"3gp",
|
||||
"rmvb",
|
||||
];
|
||||
if (videoFormats.includes(extension)) {
|
||||
return 43;
|
||||
}
|
||||
|
||||
return 49;
|
||||
};
|
||||
|
||||
const InputMessage: React.FC<InputMessageProps> = ({
|
||||
defaultValue = "",
|
||||
onContentChange,
|
||||
onSend,
|
||||
clearOnSend = false,
|
||||
placeholder = "输入消息...",
|
||||
hint,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue !== inputValue) {
|
||||
setInputValue(defaultValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValue]);
|
||||
|
||||
useEffect(() => {
|
||||
onContentChange?.(inputValue);
|
||||
}, [inputValue, onContentChange]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
const content = inputValue.trim();
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
onSend?.(content);
|
||||
if (clearOnSend) {
|
||||
setInputValue("");
|
||||
}
|
||||
antdMessage.success("已添加消息内容");
|
||||
}, [clearOnSend, inputValue, onSend]);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget;
|
||||
const { selectionStart, selectionEnd, value } = target;
|
||||
const nextValue =
|
||||
value.slice(0, selectionStart) + "\n" + value.slice(selectionEnd);
|
||||
setInputValue(nextValue);
|
||||
requestAnimationFrame(() => {
|
||||
const cursorPosition = selectionStart + 1;
|
||||
target.selectionStart = cursorPosition;
|
||||
target.selectionEnd = cursorPosition;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
},
|
||||
[handleSend],
|
||||
);
|
||||
|
||||
const handleEmojiSelect = useCallback((emoji: EmojiInfo) => {
|
||||
setInputValue(prev => prev + `[${emoji.name}]`);
|
||||
}, []);
|
||||
|
||||
const handleFileUploaded = useCallback(
|
||||
(
|
||||
filePath: { url: string; name: string; durationMs?: number },
|
||||
fileType: FileTypeValue,
|
||||
) => {
|
||||
let msgType = 1;
|
||||
let content: string | Record<string, unknown> = filePath.url;
|
||||
if ([FileType.TEXT].includes(fileType)) {
|
||||
msgType = getMsgTypeByFileFormat(filePath.url);
|
||||
} else if ([FileType.IMAGE].includes(fileType)) {
|
||||
msgType = 3;
|
||||
} else if ([FileType.AUDIO].includes(fileType)) {
|
||||
msgType = 34;
|
||||
content = JSON.stringify({
|
||||
url: filePath.url,
|
||||
durationMs: filePath.durationMs,
|
||||
});
|
||||
} else if ([FileType.FILE].includes(fileType)) {
|
||||
msgType = getMsgTypeByFileFormat(filePath.url);
|
||||
if (msgType === 49) {
|
||||
content = JSON.stringify({
|
||||
type: "file",
|
||||
title: filePath.name,
|
||||
url: filePath.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("模拟上传内容: ", {
|
||||
msgType,
|
||||
content,
|
||||
});
|
||||
antdMessage.success("附件上传成功,可在推送时使用");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAudioUploaded = useCallback(
|
||||
(audioData: { name: string; url: string; durationMs?: number }) => {
|
||||
handleFileUploaded(
|
||||
{
|
||||
name: audioData.name,
|
||||
url: audioData.url,
|
||||
durationMs: audioData.durationMs,
|
||||
},
|
||||
FileType.AUDIO,
|
||||
);
|
||||
},
|
||||
[handleFileUploaded],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.chatFooter}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputToolbar}>
|
||||
<div className={styles.leftTool}>
|
||||
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
|
||||
|
||||
<SimpleFileUpload
|
||||
onFileUploaded={fileInfo =>
|
||||
handleFileUploaded(fileInfo, FileType.IMAGE)
|
||||
}
|
||||
maxSize={10}
|
||||
type={1}
|
||||
slot={
|
||||
<Button
|
||||
className={styles.toolbarButton}
|
||||
type="text"
|
||||
icon={<PictureOutlined />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SimpleFileUpload
|
||||
onFileUploaded={fileInfo =>
|
||||
handleFileUploaded(fileInfo, FileType.FILE)
|
||||
}
|
||||
maxSize={20}
|
||||
type={4}
|
||||
slot={
|
||||
<Button
|
||||
className={styles.toolbarButton}
|
||||
type="text"
|
||||
icon={<FolderOutlined />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AudioRecorder
|
||||
onAudioUploaded={handleAudioUploaded}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputArea}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextArea
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder={placeholder}
|
||||
className={styles.messageInput}
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</div>
|
||||
{hint && <div className={styles.inputHint}>{hint}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputMessage;
|
||||
@@ -0,0 +1,143 @@
|
||||
.chatFooter {
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: #e6e6e6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
min-width: 60px;
|
||||
font-size: 13px;
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
|
||||
&:hover {
|
||||
background: #06ad56;
|
||||
border-color: #06ad56;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #059748;
|
||||
border-color: #059748;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #b3b3b3;
|
||||
border-color: #b3b3b3;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hintButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inputToolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
.stepContent {
|
||||
.stepHeader {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
|
||||
.leftColumn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 400px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.previewHeaderTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
border: 2px dashed #52c41a;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
|
||||
.messageBubble {
|
||||
min-height: 100px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptNameInput {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.savedScriptGroups {
|
||||
.contentLibrarySelector {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.contentLibraryHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.contentLibraryTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.contentLibraryHint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.scriptGroupHeaderRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scriptGroupTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scriptGroupHint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.scriptGroupList {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emptyGroup {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
background: #fff;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.scriptGroupItem {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
|
||||
.scriptGroupHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.scriptGroupLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
:global(.ant-checkbox) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.scriptGroupInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scriptGroupName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.actionButton {
|
||||
padding: 4px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupContent {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageInputArea {
|
||||
.messageInput {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.attachmentButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aiRewriteSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
|
||||
.aiRewriteToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aiRewriteLabel {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.aiRewriteInput {
|
||||
max-width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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: 1200px) {
|
||||
.step3Content {
|
||||
.rightColumn {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.step3Content {
|
||||
flex-direction: column;
|
||||
|
||||
.leftColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Button, Input, Select, Slider, Switch } from "antd";
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Select,
|
||||
Slider,
|
||||
Switch,
|
||||
message as antdMessage,
|
||||
} from "antd";
|
||||
import { CopyOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import type { CheckboxChangeEvent } from "antd/es/checkbox";
|
||||
|
||||
import styles from "../../index.module.scss";
|
||||
import { ContactItem } from "../../types";
|
||||
import ContentSelection from "@/components/ContentSelection";
|
||||
import { ContentItem } from "@/components/ContentSelection/data";
|
||||
import styles from "./index.module.scss";
|
||||
import { ContactItem, ScriptGroup } from "../../types";
|
||||
import InputMessage from "./InputMessage/InputMessage";
|
||||
import ContentLibrarySelector from "./ContentLibrarySelector";
|
||||
import type { ContentItem } from "@/components/ContentSelection/data";
|
||||
|
||||
interface StepSendMessageProps {
|
||||
selectedAccounts: any[];
|
||||
@@ -24,6 +35,16 @@ interface StepSendMessageProps {
|
||||
onAiRewriteToggle: (value: boolean) => void;
|
||||
aiPrompt: string;
|
||||
onAiPromptChange: (value: string) => void;
|
||||
currentScriptMessages: string[];
|
||||
onCurrentScriptMessagesChange: (messages: string[]) => void;
|
||||
currentScriptName: string;
|
||||
onCurrentScriptNameChange: (value: string) => void;
|
||||
savedScriptGroups: ScriptGroup[];
|
||||
onSavedScriptGroupsChange: (groups: ScriptGroup[]) => void;
|
||||
selectedScriptGroupIds: string[];
|
||||
onSelectedScriptGroupIdsChange: (ids: string[]) => void;
|
||||
selectedContentLibraries: ContentItem[];
|
||||
onSelectedContentLibrariesChange: (items: ContentItem[]) => void;
|
||||
}
|
||||
|
||||
const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
@@ -42,77 +63,268 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
onAiRewriteToggle,
|
||||
aiPrompt,
|
||||
onAiPromptChange,
|
||||
currentScriptMessages,
|
||||
onCurrentScriptMessagesChange,
|
||||
currentScriptName,
|
||||
onCurrentScriptNameChange,
|
||||
savedScriptGroups,
|
||||
onSavedScriptGroupsChange,
|
||||
selectedScriptGroupIds,
|
||||
onSelectedScriptGroupIdsChange,
|
||||
selectedContentLibraries,
|
||||
onSelectedContentLibrariesChange,
|
||||
}) => {
|
||||
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
|
||||
ContentItem[]
|
||||
>([]);
|
||||
const handleAddMessage = useCallback(
|
||||
(content?: string, showSuccess?: boolean) => {
|
||||
const finalContent = (content ?? messageContent).trim();
|
||||
if (!finalContent) {
|
||||
antdMessage.warning("请输入消息内容");
|
||||
return;
|
||||
}
|
||||
onCurrentScriptMessagesChange([...currentScriptMessages, finalContent]);
|
||||
onMessageContentChange("");
|
||||
if (showSuccess) {
|
||||
antdMessage.success("已添加消息内容");
|
||||
}
|
||||
},
|
||||
[
|
||||
currentScriptMessages,
|
||||
messageContent,
|
||||
onCurrentScriptMessagesChange,
|
||||
onMessageContentChange,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRemoveMessage = useCallback(
|
||||
(index: number) => {
|
||||
const next = currentScriptMessages.filter((_, idx) => idx !== index);
|
||||
onCurrentScriptMessagesChange(next);
|
||||
},
|
||||
[currentScriptMessages, onCurrentScriptMessagesChange],
|
||||
);
|
||||
|
||||
const handleSaveScriptGroup = useCallback(() => {
|
||||
if (currentScriptMessages.length === 0) {
|
||||
antdMessage.warning("请先添加消息内容");
|
||||
return;
|
||||
}
|
||||
const groupName =
|
||||
currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`;
|
||||
const newGroup: ScriptGroup = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: groupName,
|
||||
messages: currentScriptMessages,
|
||||
};
|
||||
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
|
||||
onCurrentScriptMessagesChange([]);
|
||||
onCurrentScriptNameChange("");
|
||||
onMessageContentChange("");
|
||||
antdMessage.success("已保存为话术组");
|
||||
}, [
|
||||
currentScriptMessages,
|
||||
currentScriptName,
|
||||
onCurrentScriptMessagesChange,
|
||||
onCurrentScriptNameChange,
|
||||
onMessageContentChange,
|
||||
onSavedScriptGroupsChange,
|
||||
savedScriptGroups,
|
||||
]);
|
||||
|
||||
const handleApplyGroup = useCallback(
|
||||
(group: ScriptGroup) => {
|
||||
onCurrentScriptMessagesChange(group.messages);
|
||||
onCurrentScriptNameChange(group.name);
|
||||
onMessageContentChange("");
|
||||
antdMessage.success("已加载话术组");
|
||||
},
|
||||
[
|
||||
onCurrentScriptMessagesChange,
|
||||
onCurrentScriptNameChange,
|
||||
onMessageContentChange,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDeleteGroup = useCallback(
|
||||
(groupId: string) => {
|
||||
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("已删除话术组");
|
||||
},
|
||||
[
|
||||
onSavedScriptGroupsChange,
|
||||
savedScriptGroups,
|
||||
onSelectedScriptGroupIdsChange,
|
||||
selectedScriptGroupIds,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(groupId: string) => (event: CheckboxChangeEvent) => {
|
||||
const checked = event.target.checked;
|
||||
if (checked) {
|
||||
if (!selectedScriptGroupIds.includes(groupId)) {
|
||||
onSelectedScriptGroupIdsChange([...selectedScriptGroupIds, groupId]);
|
||||
}
|
||||
} else {
|
||||
onSelectedScriptGroupIdsChange(
|
||||
selectedScriptGroupIds.filter(id => id !== groupId),
|
||||
);
|
||||
}
|
||||
},
|
||||
[onSelectedScriptGroupIdsChange, selectedScriptGroupIds],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.stepContent}>
|
||||
<div className={styles.step3Content}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.previewHeader}>
|
||||
<div className={styles.previewHeaderTitle}>模拟推送内容</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleSaveScriptGroup}
|
||||
disabled={currentScriptMessages.length === 0}
|
||||
>
|
||||
保存为话术组
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.messagePreview}>
|
||||
<div className={styles.previewTitle}>模拟推送内容</div>
|
||||
<div className={styles.messageBubble}>
|
||||
<div className={styles.currentEditingLabel}>当前编辑话术</div>
|
||||
<div className={styles.messageText}>
|
||||
{messageContent || "开始添加消息内容..."}
|
||||
</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 className={styles.savedScriptGroups}>
|
||||
<div className={styles.scriptGroupTitle}>已保存话术组</div>
|
||||
<ContentSelection
|
||||
selectedOptions={selectedContentLibraries}
|
||||
onSelect={setSelectedContentLibraries}
|
||||
placeholder="选择话术内容"
|
||||
showSelectedList
|
||||
selectedListMaxHeight={220}
|
||||
{/* 内容库选择组件 */}
|
||||
<ContentLibrarySelector
|
||||
selectedContentLibraries={selectedContentLibraries}
|
||||
onSelectedContentLibrariesChange={
|
||||
onSelectedContentLibrariesChange
|
||||
}
|
||||
/>
|
||||
<div className={styles.scriptGroupHeaderRow}>
|
||||
<div className={styles.scriptGroupTitle}>
|
||||
已保存话术组 ({savedScriptGroups.length})
|
||||
</div>
|
||||
<div className={styles.scriptGroupHint}>勾选后将随机均分推送</div>
|
||||
</div>
|
||||
<div className={styles.scriptGroupList}>
|
||||
{savedScriptGroups.length === 0 ? (
|
||||
<div className={styles.emptyGroup}>暂无已保存话术组</div>
|
||||
) : (
|
||||
savedScriptGroups.map((group, index) => (
|
||||
<div className={styles.scriptGroupItem} key={group.id}>
|
||||
<div className={styles.scriptGroupHeader}>
|
||||
<div className={styles.scriptGroupLeft}>
|
||||
<Checkbox
|
||||
checked={selectedScriptGroupIds.includes(group.id)}
|
||||
onChange={handleSelectChange(group.id)}
|
||||
/>
|
||||
<div className={styles.scriptGroupInfo}>
|
||||
<div className={styles.scriptGroupName}>
|
||||
{group.name || `话术组${index + 1}`}
|
||||
</div>
|
||||
<div className={styles.messageCount}>
|
||||
{group.messages.length}条消息
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.scriptGroupActions}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
className={styles.actionButton}
|
||||
onClick={() => handleApplyGroup(group)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
className={styles.actionButton}
|
||||
onClick={() => handleDeleteGroup(group.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.scriptGroupContent}>
|
||||
{group.messages[0]}
|
||||
{group.messages.length > 1 && " ..."}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.messageInputArea}>
|
||||
<Input.TextArea
|
||||
className={styles.messageInput}
|
||||
<InputMessage
|
||||
defaultValue={messageContent}
|
||||
onContentChange={onMessageContentChange}
|
||||
onSend={value => handleAddMessage(value)}
|
||||
clearOnSend
|
||||
placeholder="请输入内容"
|
||||
value={messageContent}
|
||||
onChange={e => onMessageContentChange(e.target.value)}
|
||||
rows={4}
|
||||
onKeyDown={e => {
|
||||
if (e.ctrlKey && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onMessageContentChange(`${messageContent}\n`);
|
||||
}
|
||||
}}
|
||||
hint={`按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`}
|
||||
/>
|
||||
<div className={styles.attachmentButtons}>
|
||||
<Button type="text" icon="😊" />
|
||||
<Button type="text" icon="🖼️" />
|
||||
<Button type="text" icon="📎" />
|
||||
<Button type="text" icon="🔗" />
|
||||
<Button type="text" icon="⭐" />
|
||||
</div>
|
||||
<div className={styles.aiRewriteSection}>
|
||||
<Switch checked={aiRewriteEnabled} onChange={onAiRewriteToggle} />
|
||||
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
|
||||
<div className={styles.aiRewriteToggle}>
|
||||
<Switch
|
||||
checked={aiRewriteEnabled}
|
||||
onChange={onAiRewriteToggle}
|
||||
/>
|
||||
<span className={styles.aiRewriteLabel}>AI智能话术改写</span>
|
||||
</div>
|
||||
{aiRewriteEnabled && (
|
||||
<Input
|
||||
placeholder="输入改写提示词"
|
||||
value={aiPrompt}
|
||||
onChange={e => onAiPromptChange(e.target.value)}
|
||||
style={{ marginLeft: 12, width: 200 }}
|
||||
onChange={event => onAiPromptChange(event.target.value)}
|
||||
className={styles.aiRewriteInput}
|
||||
/>
|
||||
)}
|
||||
<Button type="primary" style={{ marginLeft: 12 }}>
|
||||
+ 添加
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddMessage(undefined, true)}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.messageHint}>
|
||||
按住CTRL+ENTER换行,已选择{selectedContentLibraries.length}
|
||||
个话术组,已选择{selectedContacts.length}
|
||||
个进行推送
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -170,7 +382,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
|
||||
<li>
|
||||
推送{targetLabel}: {selectedContacts.length}个
|
||||
</li>
|
||||
<li>话术组数: {selectedContentLibraries.length}个</li>
|
||||
<li>话术组数: {savedScriptGroups.length}个</li>
|
||||
<li>随机推送: 否</li>
|
||||
<li>预计耗时: ~1分钟</li>
|
||||
</ul>
|
||||
|
||||
@@ -349,233 +349,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
|
||||
// 左侧栏
|
||||
.leftColumn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
// 右侧栏
|
||||
.rightColumn {
|
||||
width: 400px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
border: 2px dashed #52c41a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f6ffed;
|
||||
|
||||
.previewTitle {
|
||||
font-size: 14px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.messageBubble {
|
||||
min-height: 60px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
.currentEditingLabel {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 已保存话术组
|
||||
.savedScriptGroups {
|
||||
.scriptGroupTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.scriptGroupItem {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
|
||||
.scriptGroupHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.scriptGroupLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
:global(.ant-radio) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.scriptGroupName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.actionButton {
|
||||
padding: 4px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scriptGroupContent {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageInputArea {
|
||||
.messageInput {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.attachmentButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aiRewriteSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messageHint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filterModal {
|
||||
:global(.ant-modal-body) {
|
||||
padding-bottom: 12px;
|
||||
@@ -695,12 +468,6 @@
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
.rightColumn {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,18 +502,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.step3Content {
|
||||
flex-direction: column;
|
||||
|
||||
.leftColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightColumn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -14,8 +14,9 @@ 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 } from "./types";
|
||||
import { ContactItem, PushType, ScriptGroup } from "./types";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import type { ContentItem } from "@/components/ContentSelection/data";
|
||||
|
||||
const CreatePushTask: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -31,7 +32,18 @@ const CreatePushTask: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
|
||||
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
|
||||
const [messageContent, setMessageContent] = useState("");
|
||||
const [messageDraft, setMessageDraft] = useState("");
|
||||
const [currentScriptMessages, setCurrentScriptMessages] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [currentScriptName, setCurrentScriptName] = useState("");
|
||||
const [savedScriptGroups, setSavedScriptGroups] = useState<ScriptGroup[]>([]);
|
||||
const [selectedScriptGroupIds, setSelectedScriptGroupIds] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
|
||||
ContentItem[]
|
||||
>([]);
|
||||
const [friendInterval, setFriendInterval] = useState(10);
|
||||
const [messageInterval, setMessageInterval] = useState(1);
|
||||
const [selectedTag, setSelectedTag] = useState<string>("");
|
||||
@@ -120,8 +132,11 @@ const CreatePushTask: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (!messageContent.trim()) {
|
||||
message.warning("请输入消息内容");
|
||||
const selectedGroups = savedScriptGroups.filter(group =>
|
||||
selectedScriptGroupIds.includes(group.id),
|
||||
);
|
||||
if (currentScriptMessages.length === 0 && selectedGroups.length === 0) {
|
||||
message.warning("请先添加话术内容或选择话术组");
|
||||
return;
|
||||
}
|
||||
// TODO: 实现发送逻辑
|
||||
@@ -129,12 +144,17 @@ const CreatePushTask: React.FC = () => {
|
||||
pushType: validPushType,
|
||||
accounts: selectedAccounts,
|
||||
contacts: selectedContacts,
|
||||
messageContent,
|
||||
currentScript: {
|
||||
name: currentScriptName,
|
||||
messages: currentScriptMessages,
|
||||
},
|
||||
selectedScriptGroups: selectedGroups,
|
||||
friendInterval,
|
||||
messageInterval,
|
||||
selectedTag,
|
||||
aiRewriteEnabled,
|
||||
aiPrompt,
|
||||
selectedContentLibraries,
|
||||
});
|
||||
message.success("推送任务已创建");
|
||||
navigate("/pc/powerCenter/message-push-assistant");
|
||||
@@ -253,8 +273,18 @@ const CreatePushTask: React.FC = () => {
|
||||
selectedAccounts={selectedAccounts}
|
||||
selectedContacts={selectedContacts}
|
||||
targetLabel={step2Title}
|
||||
messageContent={messageContent}
|
||||
onMessageContentChange={setMessageContent}
|
||||
messageContent={messageDraft}
|
||||
onMessageContentChange={setMessageDraft}
|
||||
currentScriptMessages={currentScriptMessages}
|
||||
onCurrentScriptMessagesChange={setCurrentScriptMessages}
|
||||
currentScriptName={currentScriptName}
|
||||
onCurrentScriptNameChange={setCurrentScriptName}
|
||||
savedScriptGroups={savedScriptGroups}
|
||||
onSavedScriptGroupsChange={setSavedScriptGroups}
|
||||
selectedScriptGroupIds={selectedScriptGroupIds}
|
||||
onSelectedScriptGroupIdsChange={setSelectedScriptGroupIds}
|
||||
selectedContentLibraries={selectedContentLibraries}
|
||||
onSelectedContentLibrariesChange={setSelectedContentLibraries}
|
||||
friendInterval={friendInterval}
|
||||
onFriendIntervalChange={setFriendInterval}
|
||||
messageInterval={messageInterval}
|
||||
|
||||
@@ -19,3 +19,9 @@ export interface ContactItem {
|
||||
city?: string;
|
||||
extendFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ScriptGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user