重构快捷语管理模块:新增快捷回复和分组管理功能,优化数据展示和交互逻辑,提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-10-13 14:52:10 +08:00
parent 2c46cb956f
commit 57f2e0d649
2 changed files with 644 additions and 120 deletions

View File

@@ -0,0 +1,96 @@
import request from "@/api/request";
// 快捷回复项接口
export interface QuickWordsReply {
id: number;
groupId: number;
userId: number;
title: string;
msgType: number;
content: string;
createTime: string;
lastUpdateTime: string;
sortIndex: string;
updateTime: string | null;
isDel: number;
delTime: string | null;
}
// 快捷回复组接口
export interface QuickWordsItem {
id: number;
groupName: string;
sortIndex: string;
parentId: number;
replyType: string;
replys: any | null;
companyId: number;
userId: number;
replies: QuickWordsReply[];
children: QuickWordsItem[];
}
//好友接待配置
export function setFriendInjectConfig(params: any): Promise<QuickWordsItem[]> {
return request("/v1/kefu/reply/list", params, "GET");
}
export interface AddReplyRequest {
id?: string;
content?: string;
groupId?: string;
/**
* 1文本 3图片 43视频 49链接 等
*/
msgType?: string[];
/**
* 默认50
*/
sortIndex?: string;
title?: string;
[property: string]: any;
}
// 添加快捷回复
export function addReply(params: AddReplyRequest): Promise<any> {
return request("/v1/kefu/reply/addReply", params, "POST");
}
// 更新快捷回复
export function updateReply(params: AddReplyRequest): Promise<any> {
return request("/v1/kefu/reply/updateReply", params, "POST");
}
// 删除快捷回复
export function deleteReply(params: { id: string }): Promise<any> {
return request("/v1/kefu/reply/deleteReply", params, "DELETE");
}
export interface AddGroupRequest {
id?: string;
groupName?: string;
parentId?: string;
/**
* 0 公共 1私有 2部门
*/
replyType?: string[];
/**
* 默认50
*/
sortIndex?: string;
[property: string]: any;
}
// 添加快捷回复组
export function addGroup(params: AddGroupRequest): Promise<any> {
return request("/v1/kefu/reply/addGroup", params, "POST");
}
// 更新快捷回复组
export function updateGroup(params: AddGroupRequest): Promise<any> {
return request("/v1/kefu/reply/updateGroup", params, "POST");
}
// 删除快捷回复组
export function deleteGroup(params: { id: string }): Promise<any> {
return request("/v1/kefu/reply/deleteGroup", params, "DELETE");
}

View File

@@ -1,135 +1,563 @@
import React, { useMemo, useState } from "react";
import { Card, Input, Button, Space, List, Tag } from "antd";
import React, { useMemo, useState, useEffect, useCallback } from "react";
import {
Card,
Input,
Button,
Space,
Tabs,
Tree,
Modal,
Form,
Select,
message,
Tooltip,
Spin,
} from "antd";
import {
PlusOutlined,
ReloadOutlined,
EditOutlined,
DeleteOutlined,
FileTextOutlined,
PictureOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import {
QuickWordsItem,
QuickWordsReply,
setFriendInjectConfig,
addReply,
updateReply,
deleteReply,
updateGroup,
deleteGroup,
AddReplyRequest,
AddGroupRequest,
} from "./api";
export interface QuickWordItem {
id: string | number;
text?: string; // 兼容旧结构
title?: string;
content?: string;
tag?: string; // 分类/标签
usageCount?: number;
// 消息类型枚举
export enum MessageType {
TEXT = 1,
IMAGE = 3,
VIDEO = 43,
LINK = 49,
}
// 快捷语类型枚举
export enum QuickWordsType {
PERSONAL = 1, // 个人
PUBLIC = 0, // 公共
DEPARTMENT = 2, // 部门
}
export interface QuickWordsProps {
title?: string;
words: QuickWordItem[];
onInsert?: (text: string) => void;
onAdd?: (text: string) => void;
onRemove?: (id: string | number) => void;
onInsert?: (reply: QuickWordsReply) => void;
}
const QuickWords: React.FC<QuickWordsProps> = ({
title = "快捷语录",
words,
onInsert,
onRemove,
}) => {
const [keyword, setKeyword] = useState("");
const sorted = useMemo(
() =>
[...(words || [])].sort((a, b) =>
String(a.id).localeCompare(String(b.id)),
),
[words],
const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
const [activeTab, setActiveTab] = useState<QuickWordsType>(
QuickWordsType.PUBLIC,
);
const [keyword, setKeyword] = useState("");
const [loading, setLoading] = useState(false);
const [quickWordsData, setQuickWordsData] = useState<QuickWordsItem[]>([]);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
// 模态框状态
const [addModalVisible, setAddModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [groupModalVisible, setGroupModalVisible] = useState(false);
const [editingItem, setEditingItem] = useState<QuickWordsReply | null>(null);
const [editingGroup, setEditingGroup] = useState<QuickWordsItem | null>(null);
const [form] = Form.useForm();
const [groupForm] = Form.useForm();
// 获取快捷语数据
const fetchQuickWords = useCallback(async () => {
setLoading(true);
try {
const data = await setFriendInjectConfig({ replyType: activeTab });
setQuickWordsData(data || []);
} catch (error) {
message.error("获取快捷语数据失败");
} finally {
setLoading(false);
}
}, [activeTab]);
// 初始化数据
useEffect(() => {
fetchQuickWords();
}, [fetchQuickWords]);
// 获取消息类型图标
const getMessageTypeIcon = (msgType: number) => {
switch (msgType) {
case MessageType.TEXT:
return <FileTextOutlined style={{ color: "#1890ff" }} />;
case MessageType.IMAGE:
return <PictureOutlined style={{ color: "#52c41a" }} />;
case MessageType.VIDEO:
return <PlayCircleOutlined style={{ color: "#fa8c16" }} />;
default:
return <FileTextOutlined style={{ color: "#8c8c8c" }} />;
}
};
// 将数据转换为Tree组件需要的格式
const convertToTreeData = (data: QuickWordsItem[]): any[] => {
return data.map(item => ({
key: `group-${item.id}`,
title: (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<span>{item.groupName}</span>
<div style={{ display: "flex", gap: 4 }}>
<Tooltip title="编辑分组">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={e => {
e.stopPropagation();
handleEditGroup(item);
}}
/>
</Tooltip>
<Tooltip title="删除分组">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={e => {
e.stopPropagation();
handleDeleteGroup(item.id);
}}
/>
</Tooltip>
</div>
</div>
),
children: [
...item.replies.map(reply => ({
key: `reply-${reply.id}`,
title: (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{getMessageTypeIcon(reply.msgType)}
<span>{reply.title}</span>
</div>
<div style={{ display: "flex", gap: 4 }}>
<Tooltip title="使用">
<Button
type="text"
size="small"
onClick={e => {
e.stopPropagation();
onInsert?.(reply);
}}
>
使
</Button>
</Tooltip>
<Tooltip title="编辑">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={e => {
e.stopPropagation();
handleEditReply(reply);
}}
/>
</Tooltip>
<Tooltip title="删除">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={e => {
e.stopPropagation();
handleDeleteReply(reply.id);
}}
/>
</Tooltip>
</div>
</div>
),
isLeaf: true,
})),
...convertToTreeData(item.children || []),
],
}));
};
// 处理添加快捷回复
const handleAddReply = async (values: AddReplyRequest) => {
try {
await addReply({
...values,
groupId: selectedKeys[0]?.toString().replace("group-", "") || "",
replyType: [activeTab.toString()],
});
message.success("添加快捷回复成功");
setAddModalVisible(false);
form.resetFields();
fetchQuickWords();
} catch (error) {
message.error("添加快捷回复失败");
}
};
// 处理编辑快捷回复
const handleEditReply = (reply: QuickWordsReply) => {
setEditingItem(reply);
form.setFieldsValue({
title: reply.title,
content: reply.content,
msgType: [reply.msgType.toString()],
});
setEditModalVisible(true);
};
// 处理更新快捷回复
const handleUpdateReply = async (values: AddReplyRequest) => {
if (!editingItem) return;
try {
await updateReply({
...values,
id: editingItem.id.toString(),
});
message.success("更新快捷回复成功");
setEditModalVisible(false);
setEditingItem(null);
form.resetFields();
fetchQuickWords();
} catch (error) {
message.error("更新快捷回复失败");
}
};
// 处理删除快捷回复
const handleDeleteReply = async (id: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个快捷回复吗?",
onOk: async () => {
try {
await deleteReply({ id: id.toString() });
message.success("删除成功");
fetchQuickWords();
} catch (error) {
message.error("删除失败");
}
},
});
};
// 处理编辑分组
const handleEditGroup = (group: QuickWordsItem) => {
setEditingGroup(group);
groupForm.setFieldsValue({
groupName: group.groupName,
replyType: [group.replyType],
});
setGroupModalVisible(true);
};
// 处理更新分组
const handleUpdateGroup = async (values: AddGroupRequest) => {
if (!editingGroup) return;
try {
await updateGroup({
...values,
id: editingGroup.id.toString(),
});
message.success("更新分组成功");
setGroupModalVisible(false);
setEditingGroup(null);
groupForm.resetFields();
fetchQuickWords();
} catch (error) {
message.error("更新分组失败");
}
};
// 处理删除分组
const handleDeleteGroup = async (id: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个分组吗?删除后该分组下的所有快捷回复也会被删除。",
onOk: async () => {
try {
await deleteGroup({ id: id.toString() });
message.success("删除成功");
fetchQuickWords();
} catch (error) {
message.error("删除失败");
}
},
});
};
// 过滤数据
const filteredData = useMemo(() => {
if (!keyword.trim()) return quickWordsData;
const filterData = (data: QuickWordsItem[]): QuickWordsItem[] => {
return data
.map(item => ({
...item,
replies: item.replies.filter(
reply =>
reply.title.toLowerCase().includes(keyword.toLowerCase()) ||
reply.content.toLowerCase().includes(keyword.toLowerCase()),
),
children: filterData(item.children || []),
}))
.filter(
item =>
item.replies.length > 0 ||
item.children.length > 0 ||
item.groupName.toLowerCase().includes(keyword.toLowerCase()),
);
};
return filterData(quickWordsData);
}, [quickWordsData, keyword]);
const treeData = convertToTreeData(filteredData);
return (
<Card title={title} style={{ marginTop: 12 }}>
<Space direction="vertical" style={{ width: "100%" }}>
<Input.Search
placeholder="搜索快捷语录..."
allowClear
value={keyword}
onChange={e => setKeyword(e.target.value)}
onSearch={v => setKeyword(v)}
/>
<Card style={{ marginTop: 12 }}>
<Tabs
activeKey={activeTab.toString()}
onChange={key => setActiveTab(Number(key) as QuickWordsType)}
items={[
{
key: QuickWordsType.PERSONAL.toString(),
label: "个人快捷语",
},
{
key: QuickWordsType.PUBLIC.toString(),
label: "公共快捷语",
},
{
key: QuickWordsType.DEPARTMENT.toString(),
label: "部门快捷语",
},
]}
/>
<List
itemLayout="vertical"
split={false}
dataSource={sorted.filter(item => {
const text = `${item.title || ""}${item.content || ""}${item.text || ""}`;
return text.toLowerCase().includes(keyword.trim().toLowerCase());
})}
renderItem={item => {
const displayTitle = item.title || item.text || "未命名";
const displayContent = item.content || item.text || "";
return (
<List.Item
style={{
padding: "12px 8px",
border: "1px solid #f0f0f0",
borderRadius: 8,
marginBottom: 12,
background: "#fff",
<Space direction="vertical" style={{ width: "100%", marginTop: 16 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Input.Search
placeholder="输入关键字过滤"
allowClear
value={keyword}
onChange={e => setKeyword(e.target.value)}
style={{ flex: 1 }}
/>
<Tooltip title="添加快捷回复">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setAddModalVisible(true)}
/>
</Tooltip>
<Tooltip title="刷新">
<Button icon={<ReloadOutlined />} onClick={fetchQuickWords} />
</Tooltip>
</div>
<Spin spinning={loading}>
<Tree
showLine
showIcon
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onExpand={setExpandedKeys}
onSelect={setSelectedKeys}
treeData={treeData}
style={{ maxHeight: 400, overflow: "auto" }}
/>
</Spin>
</Space>
{/* 添加快捷回复模态框 */}
<Modal
title="添加快捷回复"
open={addModalVisible}
onCancel={() => {
setAddModalVisible(false);
form.resetFields();
}}
footer={null}
>
<Form form={form} layout="vertical" onFinish={handleAddReply}>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: "请输入标题" }]}
>
<Input placeholder="请输入快捷回复标题" />
</Form.Item>
<Form.Item
name="content"
label="内容"
rules={[{ required: true, message: "请输入内容" }]}
>
<Input.TextArea rows={4} placeholder="请输入快捷回复内容" />
</Form.Item>
<Form.Item
name="msgType"
label="消息类型"
rules={[{ required: true, message: "请选择消息类型" }]}
>
<Select placeholder="请选择消息类型">
<Select.Option value="1"></Select.Option>
<Select.Option value="3"></Select.Option>
<Select.Option value="43"></Select.Option>
<Select.Option value="49"></Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
setAddModalVisible(false);
form.resetFields();
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
gap: 12,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
}}
>
{item.tag && <Tag color="blue">{item.tag}</Tag>}
<span style={{ fontWeight: 600, color: "#262626" }}>
{displayTitle}
</span>
</div>
<div
style={{
color: "#8c8c8c",
fontSize: 13,
lineHeight: 1.6,
whiteSpace: "pre-wrap",
}}
>
{displayContent}
</div>
{typeof item.usageCount === "number" && (
<div
style={{ color: "#bfbfbf", fontSize: 12, marginTop: 6 }}
>
使 {item.usageCount}
</div>
)}
</div>
<div
style={{ display: "flex", alignItems: "center", gap: 8 }}
>
{onRemove && (
<Button
size="small"
danger
onClick={() => onRemove(item.id)}
>
</Button>
)}
<Button
type="primary"
size="small"
onClick={() => onInsert?.(displayContent || displayTitle)}
>
使
</Button>
</div>
</div>
</List.Item>
);
}}
/>
</Space>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 编辑快捷回复模态框 */}
<Modal
title="编辑快捷回复"
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false);
setEditingItem(null);
form.resetFields();
}}
footer={null}
>
<Form form={form} layout="vertical" onFinish={handleUpdateReply}>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: "请输入标题" }]}
>
<Input placeholder="请输入快捷回复标题" />
</Form.Item>
<Form.Item
name="content"
label="内容"
rules={[{ required: true, message: "请输入内容" }]}
>
<Input.TextArea rows={4} placeholder="请输入快捷回复内容" />
</Form.Item>
<Form.Item
name="msgType"
label="消息类型"
rules={[{ required: true, message: "请选择消息类型" }]}
>
<Select placeholder="请选择消息类型">
<Select.Option value="1"></Select.Option>
<Select.Option value="3"></Select.Option>
<Select.Option value="43"></Select.Option>
<Select.Option value="49"></Select.Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
setEditModalVisible(false);
setEditingItem(null);
form.resetFields();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 编辑分组模态框 */}
<Modal
title="编辑分组"
open={groupModalVisible}
onCancel={() => {
setGroupModalVisible(false);
setEditingGroup(null);
groupForm.resetFields();
}}
footer={null}
>
<Form form={groupForm} layout="vertical" onFinish={handleUpdateGroup}>
<Form.Item
name="groupName"
label="分组名称"
rules={[{ required: true, message: "请输入分组名称" }]}
>
<Input placeholder="请输入分组名称" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
setGroupModalVisible(false);
setEditingGroup(null);
groupForm.resetFields();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</Card>
);
};