重构快捷语组件:引入Layout组件,优化界面结构和样式,提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-10-13 17:23:31 +08:00
parent 57f2e0d649
commit 7c36100553
3 changed files with 476 additions and 209 deletions

View File

@@ -0,0 +1,67 @@
import React from "react";
import { Modal, Form, Input, Space, Button } from "antd";
import { AddGroupRequest } from "../api";
export interface GroupModalProps {
open: boolean;
mode: "add" | "edit";
initialValues?: Partial<AddGroupRequest>;
onSubmit: (values: AddGroupRequest) => void;
onCancel: () => void;
}
const GroupModal: React.FC<GroupModalProps> = ({
open,
mode,
initialValues,
onSubmit,
onCancel,
}) => {
const [form] = Form.useForm<AddGroupRequest>();
return (
<Modal
title={mode === "add" ? "新增分组" : "编辑分组"}
open={open}
onCancel={() => {
onCancel();
form.resetFields();
}}
footer={null}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={values => onSubmit(values)}
initialValues={initialValues}
>
<Form.Item
name="groupName"
label="分组名称"
rules={[{ required: true, message: "请输入分组名称" }]}
>
<Input placeholder="请输入分组名称" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
onCancel();
form.resetFields();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default GroupModal;

View File

@@ -0,0 +1,247 @@
import React, { useMemo } from "react";
import { Modal, Form, Input, Select, Space, Button } from "antd";
import {
PictureOutlined,
VideoCameraOutlined,
LinkOutlined,
} from "@ant-design/icons";
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
// 简化版不再使用样式与解析组件
import { AddReplyRequest } from "../api";
export interface QuickReplyModalProps {
open: boolean;
mode: "add" | "edit";
initialValues?: Partial<AddReplyRequest>;
onSubmit: (values: AddReplyRequest) => void;
onCancel: () => void;
groupOptions?: { label: string; value: string }[];
defaultGroupId?: string;
}
const QuickReplyModal: React.FC<QuickReplyModalProps> = ({
open,
mode,
initialValues,
onSubmit,
onCancel,
groupOptions,
defaultGroupId,
}) => {
const [form] = Form.useForm<AddReplyRequest>();
const mergedInitialValues = useMemo(() => {
return {
groupId: defaultGroupId,
msgType: initialValues?.msgType || ["1"],
...initialValues,
} as Partial<AddReplyRequest>;
}, [initialValues, defaultGroupId]);
// 监听类型变化
const msgTypeWatch = Form.useWatch("msgType", form);
const selectedMsgType = useMemo(() => {
const value = msgTypeWatch;
const raw = Array.isArray(value) ? value[0] : value;
return Number(raw || "1");
}, [msgTypeWatch]);
// 根据文件格式判断消息类型
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 FileType = {
TEXT: 1,
IMAGE: 2,
VIDEO: 3,
AUDIO: 4,
FILE: 5,
} as const;
const handleFileUploaded = (
filePath: string | { url: string; durationMs: number },
fileType: number,
) => {
let msgType = 1;
if (([FileType.TEXT] as number[]).includes(fileType)) {
msgType = getMsgTypeByFileFormat(filePath as string);
} else if (([FileType.IMAGE] as number[]).includes(fileType)) {
msgType = 3;
} else if (([FileType.VIDEO] as number[]).includes(fileType)) {
msgType = 43;
} else if (([FileType.AUDIO] as number[]).includes(fileType)) {
msgType = 34;
} else if (([FileType.FILE] as number[]).includes(fileType)) {
msgType = 49;
}
form.setFieldsValue({
msgType: [String(msgType)],
content: ([FileType.AUDIO] as number[]).includes(fileType)
? JSON.stringify(filePath)
: (filePath as string),
});
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault();
form.submit();
}
};
// 简化后不再有预览解析
return (
<Modal
title={mode === "add" ? "添加快捷回复" : "编辑快捷回复"}
open={open}
onCancel={() => {
onCancel();
form.resetFields();
}}
footer={null}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={values => {
const normalized = {
...values,
msgType: Array.isArray(values.msgType)
? values.msgType
: [String(values.msgType)],
} as AddReplyRequest;
onSubmit(normalized);
}}
initialValues={mergedInitialValues}
>
<Space style={{ width: "100%" }} size={24}>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: "请输入标题" }]}
style={{ flex: 1 }}
>
<Input placeholder="请输入快捷语标题" allowClear />
</Form.Item>
<Form.Item
name="groupId"
label="选择分组"
style={{ width: 260 }}
rules={[{ required: true, message: "请选择分组" }]}
>
<Select
placeholder="请选择分组"
options={groupOptions}
showSearch
optionFilterProp="label"
/>
</Form.Item>
</Space>
<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
name="content"
label="内容"
rules={[{ required: true, message: "请输入/上传内容" }]}
>
{selectedMsgType === 1 && (
<Input.TextArea
rows={4}
placeholder="请输入文本内容"
value={form.getFieldValue("content")}
onChange={e => form.setFieldsValue({ content: e.target.value })}
onKeyDown={handleKeyPress}
/>
)}
{selectedMsgType === 3 && (
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.IMAGE)
}
maxSize={1}
type={1}
slot={<Button icon={<PictureOutlined />}></Button>}
/>
)}
{selectedMsgType === 43 && (
<SimpleFileUpload
onFileUploaded={filePath =>
handleFileUploaded(filePath, FileType.VIDEO)
}
maxSize={1}
type={4}
slot={<Button icon={<VideoCameraOutlined />}></Button>}
/>
)}
{selectedMsgType === 49 && (
<Input
placeholder="请输入链接地址"
prefix={<LinkOutlined />}
value={form.getFieldValue("content")}
onChange={e => form.setFieldsValue({ content: e.target.value })}
/>
)}
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
onCancel();
form.resetFields();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default QuickReplyModal;

View File

@@ -1,6 +1,5 @@
import React, { useMemo, useState, useEffect, useCallback } from "react";
import {
Card,
Input,
Button,
Space,
@@ -8,10 +7,10 @@ import {
Tree,
Modal,
Form,
Select,
message,
Tooltip,
Spin,
Dropdown,
} from "antd";
import {
PlusOutlined,
@@ -33,7 +32,12 @@ import {
deleteGroup,
AddReplyRequest,
AddGroupRequest,
addGroup,
} from "./api";
import Layout from "@/components/Layout/LayoutFiexd";
import QuickReplyModal from "./components/QuickReplyModal";
import GroupModal from "./components/GroupModal";
import { useWeChatStore } from "@/store/module/weChat/weChat";
// 消息类型枚举
export enum MessageType {
@@ -70,8 +74,12 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
const [groupModalVisible, setGroupModalVisible] = useState(false);
const [editingItem, setEditingItem] = useState<QuickWordsReply | null>(null);
const [editingGroup, setEditingGroup] = useState<QuickWordsItem | null>(null);
const [isAddingGroup, setIsAddingGroup] = useState(false);
const [form] = Form.useForm();
const [groupForm] = Form.useForm();
const updateQuoteMessageContent = useWeChatStore(
state => state.updateQuoteMessageContent,
);
// 获取快捷语数据
const fetchQuickWords = useCallback(async () => {
@@ -157,24 +165,26 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
justifyContent: "space-between",
width: "100%",
}}
onClick={e => {
e.stopPropagation();
// 将快捷语内容写入输入框(仅文本或可直接粘贴的内容)
try {
if ([MessageType.TEXT].includes(reply.msgType)) {
updateQuoteMessageContent(reply.content || "");
} else if ([MessageType.LINK].includes(reply.msgType)) {
updateQuoteMessageContent(reply.content || "");
} else {
// 非文本类型,插入标题作为占位,方便用户手动调整
updateQuoteMessageContent(reply.title || "");
}
} catch (_) {}
}}
>
<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"
@@ -211,9 +221,13 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
// 处理添加快捷回复
const handleAddReply = async (values: AddReplyRequest) => {
try {
const fallbackGroupId =
selectedKeys[0]?.toString().replace("group-", "") ||
groupOptions[0]?.value ||
"";
await addReply({
...values,
groupId: selectedKeys[0]?.toString().replace("group-", "") || "",
groupId: values.groupId || fallbackGroupId,
replyType: [activeTab.toString()],
});
message.success("添加快捷回复成功");
@@ -228,11 +242,6 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
// 处理编辑快捷回复
const handleEditReply = (reply: QuickWordsReply) => {
setEditingItem(reply);
form.setFieldsValue({
title: reply.title,
content: reply.content,
msgType: [reply.msgType.toString()],
});
setEditModalVisible(true);
};
@@ -248,7 +257,6 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
message.success("更新快捷回复成功");
setEditModalVisible(false);
setEditingItem(null);
form.resetFields();
fetchQuickWords();
} catch (error) {
message.error("更新快捷回复失败");
@@ -274,11 +282,16 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
// 处理编辑分组
const handleEditGroup = (group: QuickWordsItem) => {
setIsAddingGroup(false);
setEditingGroup(group);
groupForm.setFieldsValue({
groupName: group.groupName,
replyType: [group.replyType],
});
setGroupModalVisible(true);
};
// 打开新增分组
const handleOpenAddGroup = () => {
setIsAddingGroup(true);
setEditingGroup(null);
groupForm.resetFields();
setGroupModalVisible(true);
};
@@ -294,13 +307,31 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
message.success("更新分组成功");
setGroupModalVisible(false);
setEditingGroup(null);
groupForm.resetFields();
fetchQuickWords();
} catch (error) {
message.error("更新分组失败");
}
};
// 处理新增分组
const handleAddGroup = async (values: AddGroupRequest) => {
try {
await addGroup({
...values,
parentId: selectedKeys[0]?.toString().startsWith("group-")
? selectedKeys[0]?.toString().replace("group-", "")
: "0",
replyType: [activeTab.toString()],
});
message.success("新增分组成功");
setGroupModalVisible(false);
setIsAddingGroup(false);
fetchQuickWords();
} catch (error) {
message.error("新增分组失败");
}
};
// 处理删除分组
const handleDeleteGroup = async (id: number) => {
Modal.confirm({
@@ -346,48 +377,78 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
const treeData = convertToTreeData(filteredData);
// 供新增/编辑快捷语使用的分组下拉数据
const groupOptions = useMemo(() => {
const flat: { label: string; value: string }[] = [];
const walk = (items: QuickWordsItem[]) => {
items.forEach(it => {
flat.push({ label: it.groupName, value: it.id.toString() });
if (it.children && it.children.length) walk(it.children);
});
};
walk(quickWordsData);
return flat;
}, [quickWordsData]);
return (
<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: "部门快捷语",
},
]}
/>
<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 }}
<Layout
header={
<div style={{ padding: "0 16px" }}>
<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: "部门快捷语",
},
]}
/>
<Tooltip title="添加快捷回复">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setAddModalVisible(true)}
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Input.Search
placeholder="输入关键字过滤"
allowClear
value={keyword}
onChange={e => setKeyword(e.target.value)}
style={{ flex: 1 }}
/>
</Tooltip>
<Tooltip title="刷新">
<Button icon={<ReloadOutlined />} onClick={fetchQuickWords} />
</Tooltip>
<Dropdown
menu={{
items: [
{ key: "add-group", label: "添加新分组" },
{ key: "add-reply", label: "新增快捷语" },
{ key: "import-reply", label: "导入快捷语" },
],
onClick: ({ key }) => {
if (key === "add-group") return handleOpenAddGroup();
if (key === "add-reply") return setAddModalVisible(true);
if (key === "import-reply")
return message.info("导入快捷语功能开发中");
},
}}
placement="bottomRight"
trigger={["click"]}
>
<Tooltip title="添加">
<Button type="primary" icon={<PlusOutlined />} />
</Tooltip>
</Dropdown>
<Tooltip title="刷新">
<Button icon={<ReloadOutlined />} onClick={fetchQuickWords} />
</Tooltip>
</div>
</div>
}
>
<Space direction="vertical" style={{ width: "100%", padding: 16 }}>
<Spin spinning={loading}>
<Tree
showLine
@@ -397,168 +458,60 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
onExpand={setExpandedKeys}
onSelect={setSelectedKeys}
treeData={treeData}
style={{ maxHeight: 400, overflow: "auto" }}
/>
</Spin>
</Space>
{/* 添加快捷回复模态框 */}
<Modal
title="添加快捷回复"
<QuickReplyModal
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>
mode="add"
groupOptions={groupOptions}
defaultGroupId={
selectedKeys[0]?.toString().replace("group-", "") ||
groupOptions[0]?.value
}
onSubmit={handleAddReply}
onCancel={() => setAddModalVisible(false)}
/>
<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();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 编辑快捷回复模态框 */}
<Modal
title="编辑快捷回复"
<QuickReplyModal
open={editModalVisible}
mode="edit"
groupOptions={groupOptions}
defaultGroupId={selectedKeys[0]?.toString().replace("group-", "")}
initialValues={
editingItem
? {
title: editingItem.title,
content: editingItem.content,
msgType: [editingItem.msgType.toString()],
groupId:
editingItem.groupId?.toString?.() ||
selectedKeys[0]?.toString().replace("group-", ""),
}
: undefined
}
onSubmit={handleUpdateReply}
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="编辑分组"
<GroupModal
open={groupModalVisible}
mode={isAddingGroup ? "add" : "edit"}
initialValues={
editingGroup ? { groupName: editingGroup.groupName } : undefined
}
onSubmit={isAddingGroup ? handleAddGroup : handleUpdateGroup}
onCancel={() => {
setGroupModalVisible(false);
setEditingGroup(null);
groupForm.resetFields();
setIsAddingGroup(false);
}}
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>
/>
</Layout>
);
};