新增跟進提醒和待辦事項模態框功能:在聊天窗口中添加相應的狀態管理和事件處理,提升用戶互動體驗。

This commit is contained in:
超级老白兔
2025-09-26 16:13:02 +08:00
parent 31217c6fd7
commit 7699397bda
5 changed files with 1038 additions and 7 deletions

View File

@@ -0,0 +1,224 @@
// 跟进提醒模态框样式
.followupModal {
:global(.ant-modal-header) {
border-bottom: none;
padding: 16px 20px 0 20px;
}
:global(.ant-modal-body) {
padding: 0 20px 20px 20px;
max-height: 60vh;
overflow: hidden;
}
:global(.ant-modal-close) {
top: 12px;
right: 12px;
}
}
.modalHeader {
.modalTitle {
font-size: 16px;
font-weight: 600;
color: #262626;
margin: 0;
}
.modalSubtitle {
font-size: 12px;
color: #8c8c8c;
margin: 2px 0 0 0;
}
}
.modalContent {
display: flex;
flex-direction: column;
height: 100%;
.addReminderSection {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #f0f0f0;
flex-shrink: 0;
.reminderForm {
.formRow {
display: flex;
gap: 12px;
margin-bottom: 12px;
.formItem {
flex: 1;
margin-bottom: 0;
:global(.ant-form-item-label) {
padding-bottom: 4px;
label {
font-size: 14px;
font-weight: 500;
color: #262626;
}
}
}
}
.selectInput,
.dateInput,
.contentInput {
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
}
&:focus,
&:focus-within {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
.contentInput {
resize: none;
}
.addButton {
height: 36px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
background: #1890ff;
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
&:hover {
background: #40a9ff;
border-color: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.3);
}
.anticon {
margin-right: 4px;
}
}
}
}
.remindersList {
flex: 1;
overflow-y: auto;
max-height: 200px;
border: 1px solid #f0f0f0;
border-radius: 6px;
background: #fff;
:global(.ant-list) {
padding: 0;
}
:global(.ant-list-item) {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.reminderItem {
padding: 10px;
.reminderContent {
width: 100%;
.reminderHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.typeTag {
font-size: 11px;
padding: 1px 6px;
border-radius: 10px;
font-weight: 500;
.anticon {
margin-right: 3px;
}
}
.recipient {
font-size: 12px;
color: #595959;
font-weight: 500;
}
}
.reminderBody {
margin-bottom: 6px;
.reminderText {
font-size: 13px;
color: #262626;
line-height: 1.4;
}
}
.reminderFooter {
.clockIcon {
color: #8c8c8c;
font-size: 11px;
}
.scheduledTime {
font-size: 11px;
color: #8c8c8c;
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.followupModal {
:global(.ant-modal) {
margin: 0;
max-width: 100vw;
top: 0;
padding-bottom: 0;
}
:global(.ant-modal-body) {
padding: 0 16px 16px 16px;
}
}
.modalContent {
.addReminderSection {
padding: 12px;
margin-bottom: 12px;
.reminderForm {
.formRow {
flex-direction: column;
gap: 8px;
}
}
}
.remindersList {
max-height: 150px;
}
}
}

View File

@@ -0,0 +1,251 @@
import React, { useState } from "react";
import {
Modal,
Form,
Select,
DatePicker,
Input,
Button,
List,
Tag,
Space,
Typography,
} from "antd";
import {
PlusOutlined,
ClockCircleOutlined,
PhoneOutlined,
MessageOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
const { Option } = Select;
const { TextArea } = Input;
const { Text } = Typography;
interface FollowupReminder {
id: string;
type: "电话" | "消息";
status: "待处理" | "已完成" | "已取消";
content: string;
scheduledTime: string;
recipient: string;
}
interface FollowupReminderModalProps {
visible: boolean;
onClose: () => void;
recipientName?: string;
}
const FollowupReminderModal: React.FC<FollowupReminderModalProps> = ({
visible,
onClose,
recipientName = "客户",
}) => {
const [form] = Form.useForm();
const [reminders, setReminders] = useState<FollowupReminder[]>([
{
id: "1",
type: "电话",
status: "待处理",
content: "周三14点回访",
scheduledTime: "2024/3/6 14:00:00",
recipient: "李先生",
},
{
id: "2",
type: "消息",
status: "待处理",
content: "发送产品演示视频",
scheduledTime: "2024/3/7 09:00:00",
recipient: "张总",
},
{
id: "3",
type: "消息",
status: "待处理",
content: "发送产品演示视频",
scheduledTime: "2024/3/7 09:00:00",
recipient: "张总",
},
{
id: "4",
type: "消息",
status: "待处理",
content: "发送产品演示视频",
scheduledTime: "2024/3/7 09:00:00",
recipient: "张总",
},
]);
// 跟进方式选项
const followupMethods = [
{ value: "电话回访", label: "电话回访" },
{ value: "微信消息", label: "微信消息" },
{ value: "邮件", label: "邮件" },
{ value: "短信", label: "短信" },
];
// 处理添加提醒
const handleAddReminder = async () => {
try {
const values = await form.validateFields();
const newReminder: FollowupReminder = {
id: Date.now().toString(),
type: values.method === "电话回访" ? "电话" : "消息",
status: "待处理",
content: values.content,
scheduledTime: values.dateTime.format("YYYY/M/D HH:mm:ss"),
recipient: recipientName,
};
setReminders([...reminders, newReminder]);
form.resetFields();
} catch (error) {
console.error("表单验证失败:", error);
}
};
// 获取状态标签颜色
const getStatusColor = (status: string) => {
switch (status) {
case "待处理":
return "warning";
case "已完成":
return "success";
case "已取消":
return "default";
default:
return "default";
}
};
// 获取类型图标
const getTypeIcon = (type: string) => {
return type === "电话" ? <PhoneOutlined /> : <MessageOutlined />;
};
return (
<Modal
title={
<div className={styles.modalHeader}>
<div className={styles.modalTitle}></div>
<div className={styles.modalSubtitle}></div>
</div>
}
open={visible}
onCancel={onClose}
footer={null}
width={480}
className={styles.followupModal}
>
<div className={styles.modalContent}>
{/* 添加新提醒区域 */}
<div className={styles.addReminderSection}>
<Form form={form} layout="vertical" className={styles.reminderForm}>
<div className={styles.formRow}>
<Form.Item
name="method"
label="跟进方式"
rules={[{ required: true, message: "请选择跟进方式" }]}
className={styles.formItem}
>
<Select placeholder="电话回访" className={styles.selectInput}>
{followupMethods.map(method => (
<Option key={method.value} value={method.value}>
{method.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="dateTime"
label="提醒时间"
rules={[{ required: true, message: "请选择提醒时间" }]}
className={styles.formItem}
>
<DatePicker
showTime
format="YYYY/M/D HH:mm"
placeholder="年/月/日 --:--"
className={styles.dateInput}
/>
</Form.Item>
</div>
<Form.Item
name="content"
label="提醒内容"
rules={[{ required: true, message: "请输入提醒内容" }]}
>
<TextArea
placeholder="提醒内容..."
rows={3}
className={styles.contentInput}
/>
</Form.Item>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddReminder}
className={styles.addButton}
block
>
</Button>
</Form>
</div>
{/* 现有提醒列表 */}
<div className={styles.remindersList}>
<List
dataSource={reminders}
renderItem={reminder => (
<List.Item className={styles.reminderItem}>
<div className={styles.reminderContent}>
<div className={styles.reminderHeader}>
<Space>
<Tag
icon={getTypeIcon(reminder.type)}
color="blue"
className={styles.typeTag}
>
{reminder.type}
</Tag>
<Tag color={getStatusColor(reminder.status)}>
{reminder.status}
</Tag>
</Space>
<Text className={styles.recipient}>
{reminder.recipient}
</Text>
</div>
<div className={styles.reminderBody}>
<Text className={styles.reminderText}>
{reminder.content}
</Text>
</div>
<div className={styles.reminderFooter}>
<Space>
<ClockCircleOutlined className={styles.clockIcon} />
<Text className={styles.scheduledTime}>
{reminder.scheduledTime}
</Text>
</Space>
</div>
</div>
</List.Item>
)}
/>
</div>
</div>
</Modal>
);
};
export default FollowupReminderModal;

View File

@@ -0,0 +1,266 @@
// 待办事项模态框样式
.todoModal {
:global(.ant-modal-header) {
border-bottom: none;
padding: 16px 20px 0 20px;
}
:global(.ant-modal-body) {
padding: 0 20px 20px 20px;
max-height: 60vh;
overflow: hidden;
}
:global(.ant-modal-close) {
top: 12px;
right: 12px;
}
}
.modalHeader {
.modalTitle {
font-size: 16px;
font-weight: 600;
color: #262626;
margin: 0;
}
.modalSubtitle {
font-size: 12px;
color: #8c8c8c;
margin: 2px 0 0 0;
}
}
.modalContent {
display: flex;
flex-direction: column;
height: 100%;
.addTaskSection {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
border: 1px solid #f0f0f0;
flex-shrink: 0;
.taskForm {
.titleInput,
.descriptionInput,
.prioritySelect,
.dateInput {
border: 1px solid #d9d9d9;
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
}
&:focus,
&:focus-within {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
.descriptionInput {
resize: none;
}
.formRow {
display: flex;
gap: 12px;
margin-bottom: 12px;
.formItem {
flex: 1;
margin-bottom: 0;
:global(.ant-form-item-label) {
padding-bottom: 4px;
label {
font-size: 14px;
font-weight: 500;
color: #262626;
}
}
}
}
.addButton {
height: 36px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
background: #1890ff;
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
&:hover {
background: #40a9ff;
border-color: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.3);
}
.anticon {
margin-right: 4px;
}
}
}
}
.todoList {
flex: 1;
overflow-y: auto;
max-height: 200px;
border: 1px solid #f0f0f0;
border-radius: 6px;
background: #fff;
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
:global(.ant-list) {
padding: 0;
}
:global(.ant-list-item) {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.todoItem {
padding: 10px;
.todoContent {
width: 100%;
.todoHeader {
display: flex;
align-items: center;
margin-bottom: 6px;
.todoCheckbox {
margin-right: 8px;
:global(.ant-checkbox-inner) {
width: 16px;
height: 16px;
}
}
.todoTitle {
font-size: 14px;
color: #262626;
font-weight: 500;
flex: 1;
&.completed {
text-decoration: line-through;
color: #8c8c8c;
}
}
}
.todoDescription {
margin-bottom: 8px;
.descriptionText {
font-size: 12px;
color: #595959;
line-height: 1.4;
}
}
.todoFooter {
.clientInfo {
font-size: 11px;
color: #8c8c8c;
}
.priorityTag {
font-size: 10px;
padding: 1px 6px;
border-radius: 8px;
font-weight: 500;
}
.dueDate {
.calendarIcon {
color: #8c8c8c;
font-size: 11px;
}
.dueDateText {
font-size: 11px;
color: #8c8c8c;
}
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.todoModal {
:global(.ant-modal) {
margin: 0;
max-width: 100vw;
top: 0;
padding-bottom: 0;
}
:global(.ant-modal-body) {
padding: 0 16px 16px 16px;
}
}
.modalContent {
.addTaskSection {
padding: 12px;
margin-bottom: 12px;
.taskForm {
.formRow {
flex-direction: column;
gap: 8px;
}
}
}
.todoList {
max-height: 150px;
// 移动端滚动条样式
&::-webkit-scrollbar {
width: 4px;
}
}
}
}

View File

@@ -0,0 +1,251 @@
import React, { useState } from "react";
import {
Modal,
Form,
Input,
Select,
DatePicker,
Button,
List,
Checkbox,
Tag,
Space,
Typography,
} from "antd";
import { PlusOutlined, CalendarOutlined } from "@ant-design/icons";
import styles from "./index.module.scss";
const { Option } = Select;
const { TextArea } = Input;
const { Text } = Typography;
interface TodoItem {
id: string;
title: string;
description?: string;
client?: string;
priority: "高" | "中" | "低";
dueDate: string;
completed: boolean;
}
interface TodoListModalProps {
visible: boolean;
onClose: () => void;
clientName?: string;
}
const TodoListModal: React.FC<TodoListModalProps> = ({
visible,
onClose,
clientName = "客户",
}) => {
const [form] = Form.useForm();
const [todos, setTodos] = useState<TodoItem[]>([
{
id: "1",
title: "整理客户需求文档",
description: "汇总本周客户反馈的功能需求",
client: "李先生",
priority: "高",
dueDate: "03/08 18:00",
completed: false,
},
{
id: "2",
title: "准备产品演示PPT",
description: "针对大客户的定制化演示材料",
client: "张总",
priority: "中",
dueDate: "03/09 16:00",
completed: false,
},
{
id: "3",
title: "准备产品演示PPT",
description: "针对大客户的定制化演示材料",
client: "张总",
priority: "中",
dueDate: "03/09 16:00",
completed: false,
},
]);
// 优先级选项
const priorityOptions = [
{ value: "高", label: "高优先级", color: "orange" },
{ value: "中", label: "中优先级", color: "blue" },
{ value: "低", label: "低优先级", color: "green" },
];
// 处理添加任务
const handleAddTask = async () => {
try {
const values = await form.validateFields();
const newTodo: TodoItem = {
id: Date.now().toString(),
title: values.title,
description: values.description,
client: clientName,
priority: values.priority,
dueDate: values.dueDate.format("MM/DD HH:mm"),
completed: false,
};
setTodos([...todos, newTodo]);
form.resetFields();
} catch (error) {
console.error("表单验证失败:", error);
}
};
// 处理任务完成状态切换
const handleToggleComplete = (id: string) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
};
// 获取优先级标签颜色
const getPriorityColor = (priority: string) => {
const option = priorityOptions.find(opt => opt.value === priority);
return option?.color || "default";
};
return (
<Modal
title={
<div className={styles.modalHeader}>
<div className={styles.modalTitle}></div>
<div className={styles.modalSubtitle}></div>
</div>
}
open={visible}
onCancel={onClose}
footer={null}
width={480}
className={styles.todoModal}
>
<div className={styles.modalContent}>
{/* 添加新任务区域 */}
<div className={styles.addTaskSection}>
<Form form={form} layout="vertical" className={styles.taskForm}>
<Form.Item
name="title"
rules={[{ required: true, message: "请输入任务标题" }]}
>
<Input placeholder="任务标题..." className={styles.titleInput} />
</Form.Item>
<Form.Item name="description">
<TextArea
placeholder="任务描述 (可选)..."
rows={2}
className={styles.descriptionInput}
/>
</Form.Item>
<div className={styles.formRow}>
<Form.Item
name="priority"
rules={[{ required: true, message: "请选择优先级" }]}
className={styles.formItem}
>
<Select
placeholder="中优先级"
className={styles.prioritySelect}
>
{priorityOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="dueDate"
rules={[{ required: true, message: "请选择截止时间" }]}
className={styles.formItem}
>
<DatePicker
showTime
format="MM/DD HH:mm"
placeholder="年/月/日 --:--"
className={styles.dateInput}
/>
</Form.Item>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddTask}
className={styles.addButton}
block
>
</Button>
</Form>
</div>
{/* 任务列表 */}
<div className={styles.todoList}>
<List
dataSource={todos}
renderItem={todo => (
<List.Item className={styles.todoItem}>
<div className={styles.todoContent}>
<div className={styles.todoHeader}>
<Checkbox
checked={todo.completed}
onChange={() => handleToggleComplete(todo.id)}
className={styles.todoCheckbox}
/>
<Text
className={`${styles.todoTitle} ${todo.completed ? styles.completed : ""}`}
>
{todo.title}
</Text>
</div>
{todo.description && (
<div className={styles.todoDescription}>
<Text className={styles.descriptionText}>
{todo.description}
</Text>
</div>
)}
<div className={styles.todoFooter}>
<Space>
<Text className={styles.clientInfo}>
:{todo.client}
</Text>
<Tag
color={getPriorityColor(todo.priority)}
className={styles.priorityTag}
>
{todo.priority}
</Tag>
<Space className={styles.dueDate}>
<CalendarOutlined className={styles.calendarIcon} />
<Text className={styles.dueDateText}>
{todo.dueDate}
</Text>
</Space>
</Space>
</div>
</div>
</List.Item>
)}
/>
</div>
</div>
</Modal>
);
};
export default TodoListModal;

View File

@@ -14,6 +14,8 @@ import styles from "./ChatWindow.module.scss";
import ProfileCard from "./components/ProfileCard";
import MessageEnter from "./components/MessageEnter";
import MessageRecord from "./components/MessageRecord";
import FollowupReminderModal from "./components/FollowupReminderModal";
import TodoListModal from "./components/TodoListModal";
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
import { useWeChatStore } from "@/store/module/weChat/weChat";
const { Header, Content } = Layout;
@@ -22,6 +24,12 @@ interface ChatWindowProps {
contract: ContractData | weChatGroup;
}
const typeOptions = [
{ value: 0, label: "人工接待" },
{ value: 1, label: "AI辅助" },
{ value: 2, label: "AI接管" },
];
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
const updateAiQuoteMessageContent = useWeChatStore(
state => state.updateAiQuoteMessageContent,
@@ -30,15 +38,28 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
state => state.aiQuoteMessageContent,
);
const [showProfile, setShowProfile] = useState(true);
const [followupModalVisible, setFollowupModalVisible] = useState(false);
const [todoModalVisible, setTodoModalVisible] = useState(false);
const onToggleProfile = () => {
setShowProfile(!showProfile);
};
const typeOptions = [
{ value: 0, label: "人工接待" },
{ value: 1, label: "AI辅助" },
{ value: 2, label: "AI接管" },
];
const handleFollowupClick = () => {
setFollowupModalVisible(true);
};
const handleFollowupModalClose = () => {
setFollowupModalVisible(false);
};
const handleTodoClick = () => {
setTodoModalVisible(true);
};
const handleTodoModalClose = () => {
setTodoModalVisible(false);
};
const [currentConfig, setCurrentConfig] = useState(
typeOptions.find(option => option.value === aiQuoteMessageContent),
@@ -115,8 +136,12 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
</Space>
</Header>
<div className={styles.extend}>
<Button icon={<BellOutlined />}></Button>
<Button icon={<CheckSquareOutlined />}></Button>
<Button icon={<BellOutlined />} onClick={handleFollowupClick}>
</Button>
<Button icon={<CheckSquareOutlined />} onClick={handleTodoClick}>
</Button>
</div>
{/* 聊天内容 */}
@@ -130,6 +155,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
{/* 右侧个人资料卡片 */}
{showProfile && <ProfileCard contract={contract} />}
{/* 跟进提醒模态框 */}
<FollowupReminderModal
visible={followupModalVisible}
onClose={handleFollowupModalClose}
recipientName={contract.nickname || contract.name}
/>
{/* 待办事项模态框 */}
<TodoListModal
visible={todoModalVisible}
onClose={handleTodoModalClose}
clientName={contract.nickname || contract.name}
/>
</Layout>
);
};