重构PushTask组件以支持用于消息管理的脚本组。引入新的状态变量来处理脚本消息和组,更新消息发送逻辑,并增强用于添加和管理脚本组的UI。清理样式,改善消息输入区的用户体验。

This commit is contained in:
超级老白兔
2025-11-11 16:20:46 +08:00
parent bb72038e2b
commit 87d1e1c44f
12 changed files with 1526 additions and 302 deletions

View File

@@ -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;
}
}

View File

@@ -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%;
}
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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%;
}
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -19,3 +19,9 @@ export interface ContactItem {
city?: string;
extendFields?: Record<string, any>;
}
export interface ScriptGroup {
id: string;
name: string;
messages: string[];
}