This commit is contained in:
超级老白兔
2025-09-27 16:59:30 +08:00
parent 8ff912a4a7
commit a73481808c
17 changed files with 2639 additions and 44 deletions

View File

@@ -61,14 +61,6 @@ const CommonConfig: React.FC = () => {
</div>
</>
}
footer={
<div className={styles.actions}>
<Space>
<Button onClick={() => setActiveTab("reception")}></Button>
<Button type="primary"></Button>
</Space>
</div>
}
>
<div className={styles.content}>{renderTabContent()}</div>
</Layout>

View File

@@ -0,0 +1,162 @@
import React, { useState } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import { addKeyword, type KeywordAddRequest } from "./api";
const { TextArea } = Input;
const { Option } = Select;
interface AddKeywordModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
const AddKeywordModal: React.FC<AddKeywordModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: KeywordAddRequest = {
title: values.title,
keywords: values.keywords,
content: values.content,
matchType: values.matchType,
priority: values.priority,
replyType: values.replyType,
status: values.status || "1",
};
const response = await addKeyword(data);
if (response) {
message.success("添加关键词成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "添加关键词失败");
}
} catch (error) {
console.error("添加关键词失败:", error);
message.error("添加关键词失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="添加关键词回复"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
status: "1",
matchType: "模糊匹配",
priority: "1",
replyType: "text",
}}
>
<Form.Item
name="title"
label="关键词标题"
rules={[{ required: true, message: "请输入关键词标题" }]}
>
<Input placeholder="请输入关键词标题" />
</Form.Item>
<Form.Item
name="keywords"
label="关键词"
rules={[{ required: true, message: "请输入关键词" }]}
>
<Input placeholder="请输入关键词" />
</Form.Item>
<Form.Item
name="content"
label="回复内容"
rules={[{ required: true, message: "请输入回复内容" }]}
>
<TextArea rows={4} placeholder="请输入回复内容" />
</Form.Item>
<Form.Item
name="matchType"
label="匹配类型"
rules={[{ required: true, message: "请选择匹配类型" }]}
>
<Select placeholder="请选择匹配类型">
<Option value="模糊匹配"></Option>
<Option value="精确匹配"></Option>
</Select>
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: "请选择优先级" }]}
>
<Select placeholder="请选择优先级">
<Option value="1">1</Option>
<Option value="2">2</Option>
<Option value="3">3</Option>
<Option value="4">4</Option>
<Option value="5">5</Option>
</Select>
</Form.Item>
<Form.Item
name="replyType"
label="回复类型"
rules={[{ required: true, message: "请选择回复类型" }]}
>
<Select placeholder="请选择回复类型">
<Option value="text"></Option>
<Option value="template"></Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default AddKeywordModal;

View File

@@ -0,0 +1,174 @@
import React, { useState } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import { addMaterial, type MaterialAddRequest } from "./api";
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
const { TextArea } = Input;
const { Option } = Select;
interface AddMaterialModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
const AddMaterialModal: React.FC<AddMaterialModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [materialType, setMaterialType] = useState<string>("文本");
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: MaterialAddRequest = {
title: values.title,
content: [values.content],
cover: Array.isArray(values.cover)
? values.cover[0] || ""
: values.cover || "",
status: values.status || "1",
type: values.type || "文本",
};
const response = await addMaterial(data);
if (response) {
message.success("添加素材成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "添加素材失败");
}
} catch (error) {
console.error("添加素材失败:", error);
message.error("添加素材失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
setMaterialType("文本");
onCancel();
};
return (
<Modal
title="添加素材"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ status: "1", type: "文本" }}
onValuesChange={changedValues => {
if (changedValues.type) {
setMaterialType(changedValues.type);
}
}}
>
<Form.Item
name="title"
label="素材标题"
rules={[{ required: true, message: "请输入素材标题" }]}
>
<Input placeholder="请输入素材标题" />
</Form.Item>
<Form.Item
name="type"
label="素材类型"
rules={[{ required: true, message: "请选择素材类型" }]}
>
<Select placeholder="请选择素材类型">
<Option value="文本"></Option>
<Option value="图片"></Option>
<Option value="视频"></Option>
</Select>
</Form.Item>
{materialType === "文本" && (
<Form.Item
name="content"
label="素材内容"
rules={[{ required: true, message: "请输入素材内容" }]}
>
<TextArea
rows={4}
placeholder="请输入素材内容,多个内容用换行分隔"
/>
</Form.Item>
)}
{materialType === "图片" && (
<Form.Item
name="content"
label="图片文件"
rules={[{ required: true, message: "请上传图片文件" }]}
>
<ImageUpload
count={1}
accept="image/*"
className="material-cover-upload"
/>
</Form.Item>
)}
{materialType === "视频" && (
<Form.Item
name="content"
label="视频文件"
rules={[{ required: true, message: "请上传视频文件" }]}
>
<ImageUpload
count={1}
accept="video/*"
className="material-cover-upload"
/>
</Form.Item>
)}
<Form.Item name="cover" label="封面图片">
<ImageUpload
count={1}
accept="image/*"
className="material-cover-upload"
/>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default AddMaterialModal;

View File

@@ -0,0 +1,133 @@
import React, { useState } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import { addSensitiveWord, type SensitiveWordAddRequest } from "./api";
const { TextArea } = Input;
const { Option } = Select;
interface AddSensitiveWordModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
const AddSensitiveWordModal: React.FC<AddSensitiveWordModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: SensitiveWordAddRequest = {
title: values.title,
keywords: values.keywords,
content: values.content,
operation: values.operation,
status: values.status || "1",
};
const response = await addSensitiveWord(data);
if (response) {
message.success("添加敏感词成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "添加敏感词失败");
}
} catch (error) {
console.error("添加敏感词失败:", error);
message.error("添加敏感词失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="添加敏感词"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ status: "1", operation: "1" }}
>
<Form.Item
name="title"
label="敏感词标题"
rules={[{ required: true, message: "请输入敏感词标题" }]}
>
<Input placeholder="请输入敏感词标题" />
</Form.Item>
<Form.Item
name="keywords"
label="关键词"
rules={[{ required: true, message: "请输入关键词" }]}
>
<Input placeholder="请输入关键词" />
</Form.Item>
<Form.Item
name="content"
label="敏感词内容"
rules={[{ required: true, message: "请输入敏感词内容" }]}
>
<TextArea rows={4} placeholder="请输入敏感词内容" />
</Form.Item>
<Form.Item
name="operation"
label="操作类型"
rules={[{ required: true, message: "请选择操作类型" }]}
>
<Select placeholder="请选择操作类型">
<Option value="0"></Option>
<Option value="1"></Option>
<Option value="2"></Option>
<Option value="3"></Option>
<Option value="4"></Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default AddSensitiveWordModal;

View File

@@ -0,0 +1,198 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
updateKeyword,
getKeywordDetails,
type KeywordUpdateRequest,
} from "./api";
const { TextArea } = Input;
const { Option } = Select;
interface EditKeywordModalProps {
visible: boolean;
keywordId: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const EditKeywordModal: React.FC<EditKeywordModalProps> = ({
visible,
keywordId,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// 获取关键词详情
const fetchKeywordDetails = async (id: string) => {
try {
const response = await getKeywordDetails(id);
if (response) {
const keyword = response;
form.setFieldsValue({
title: keyword.title,
keywords: keyword.keywords,
content: keyword.content,
matchType: keyword.matchType,
priority: keyword.priority,
replyType: keyword.replyType,
status: keyword.status,
});
}
} catch (error) {
console.error("获取关键词详情失败:", error);
message.error("获取关键词详情失败");
}
};
// 当弹窗打开且有ID时获取详情
useEffect(() => {
if (visible && keywordId) {
fetchKeywordDetails(keywordId);
}
}, [visible, keywordId]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: KeywordUpdateRequest = {
id: keywordId,
title: values.title,
keywords: values.keywords,
content: values.content,
matchType: values.matchType,
priority: values.priority,
replyType: values.replyType,
status: values.status,
};
const response = await updateKeyword(data);
if (response) {
message.success("更新关键词回复成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "更新关键词回复失败");
}
} catch (error) {
console.error("更新关键词回复失败:", error);
message.error("更新关键词回复失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="编辑关键词回复"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
status: "1",
matchType: "模糊匹配",
priority: "1",
replyType: "text",
}}
>
<Form.Item
name="title"
label="关键词标题"
rules={[{ required: true, message: "请输入关键词标题" }]}
>
<Input placeholder="请输入关键词标题" />
</Form.Item>
<Form.Item
name="keywords"
label="关键词"
rules={[{ required: true, message: "请输入关键词" }]}
>
<Input placeholder="请输入关键词" />
</Form.Item>
<Form.Item
name="content"
label="回复内容"
rules={[{ required: true, message: "请输入回复内容" }]}
>
<TextArea rows={4} placeholder="请输入回复内容" />
</Form.Item>
<Form.Item
name="matchType"
label="匹配类型"
rules={[{ required: true, message: "请选择匹配类型" }]}
>
<Select placeholder="请选择匹配类型">
<Option value="模糊匹配"></Option>
<Option value="精确匹配"></Option>
</Select>
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: "请选择优先级" }]}
>
<Select placeholder="请选择优先级">
<Option value="1">1</Option>
<Option value="2">2</Option>
<Option value="3">3</Option>
<Option value="4">4</Option>
<Option value="5">5</Option>
</Select>
</Form.Item>
<Form.Item
name="replyType"
label="回复类型"
rules={[{ required: true, message: "请选择回复类型" }]}
>
<Select placeholder="请选择回复类型">
<Option value="text"></Option>
<Option value="template"></Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default EditKeywordModal;

View File

@@ -0,0 +1,211 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
updateMaterial,
getMaterialDetails,
type MaterialUpdateRequest,
} from "./api";
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
const { TextArea } = Input;
const { Option } = Select;
interface EditMaterialModalProps {
visible: boolean;
materialId: number | null;
onCancel: () => void;
onSuccess: () => void;
}
const EditMaterialModal: React.FC<EditMaterialModalProps> = ({
visible,
materialId,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [materialType, setMaterialType] = useState<string>("文本");
// 获取素材详情
const fetchMaterialDetails = async (id: number) => {
try {
const response = await getMaterialDetails(id.toString());
if (response) {
const material = response;
form.setFieldsValue({
title: material.title,
content: Array.isArray(material.content)
? material.content.join("\n")
: material.content,
cover: material.cover ? [material.cover] : [],
status: material.status.toString(),
type: material.type || "文本",
});
setMaterialType(material.type || "文本");
}
} catch (error) {
console.error("获取素材详情失败:", error);
message.error("获取素材详情失败");
}
};
// 当弹窗打开且有ID时获取详情
useEffect(() => {
if (visible && materialId) {
fetchMaterialDetails(materialId);
}
}, [visible, materialId]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: MaterialUpdateRequest = {
id: materialId?.toString(),
title: values.title,
content: [values.content],
cover: Array.isArray(values.cover)
? values.cover[0] || ""
: values.cover || "",
status: values.status || "1",
type: values.type || "文本",
};
const response = await updateMaterial(data);
if (response) {
message.success("更新素材成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "更新素材失败");
}
} catch (error) {
console.error("更新素材失败:", error);
message.error("更新素材失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
setMaterialType("文本");
onCancel();
};
return (
<Modal
title="编辑素材"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ status: "1", type: "文本" }}
onValuesChange={changedValues => {
if (changedValues.type) {
setMaterialType(changedValues.type);
}
}}
>
<Form.Item
name="title"
label="素材标题"
rules={[{ required: true, message: "请输入素材标题" }]}
>
<Input placeholder="请输入素材标题" />
</Form.Item>
<Form.Item
name="type"
label="素材类型"
rules={[{ required: true, message: "请选择素材类型" }]}
>
<Select placeholder="请选择素材类型">
<Option value="文本"></Option>
<Option value="图片"></Option>
<Option value="视频"></Option>
</Select>
</Form.Item>
{materialType === "文本" && (
<Form.Item
name="content"
label="素材内容"
rules={[{ required: true, message: "请输入素材内容" }]}
>
<TextArea
rows={4}
placeholder="请输入素材内容,多个内容用换行分隔"
/>
</Form.Item>
)}
{materialType === "图片" && (
<Form.Item
name="content"
label="图片文件"
rules={[{ required: true, message: "请上传图片文件" }]}
>
<ImageUpload
count={1}
accept="image/*"
className="material-cover-upload"
/>
</Form.Item>
)}
{materialType === "视频" && (
<Form.Item
name="content"
label="视频文件"
rules={[{ required: true, message: "请上传视频文件" }]}
>
<ImageUpload
count={1}
accept="video/*"
className="material-cover-upload"
/>
</Form.Item>
)}
<Form.Item name="cover" label="封面图片">
<ImageUpload
count={1}
accept="image/*"
className="material-cover-upload"
/>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default EditMaterialModal;

View File

@@ -0,0 +1,167 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
updateSensitiveWord,
getSensitiveWordDetails,
type SensitiveWordUpdateRequest,
} from "./api";
const { TextArea } = Input;
const { Option } = Select;
interface EditSensitiveWordModalProps {
visible: boolean;
sensitiveWordId: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const EditSensitiveWordModal: React.FC<EditSensitiveWordModalProps> = ({
visible,
sensitiveWordId,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// 获取敏感词详情
const fetchSensitiveWordDetails = async (id: string) => {
try {
const response = await getSensitiveWordDetails(id);
if (response) {
const sensitiveWord = response;
form.setFieldsValue({
title: sensitiveWord.title,
keywords: sensitiveWord.keywords,
content: sensitiveWord.content,
operation: sensitiveWord.operation,
status: sensitiveWord.status,
});
}
} catch (error) {
console.error("获取敏感词详情失败:", error);
message.error("获取敏感词详情失败");
}
};
// 当弹窗打开且有ID时获取详情
useEffect(() => {
if (visible && sensitiveWordId) {
fetchSensitiveWordDetails(sensitiveWordId);
}
}, [visible, sensitiveWordId]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
const data: SensitiveWordUpdateRequest = {
id: sensitiveWordId,
title: values.title,
keywords: values.keywords,
content: values.content,
operation: values.operation,
status: values.status,
};
const response = await updateSensitiveWord(data);
if (response) {
message.success("更新敏感词成功");
form.resetFields();
onSuccess();
onCancel();
} else {
message.error(response?.message || "更新敏感词失败");
}
} catch (error) {
console.error("更新敏感词失败:", error);
message.error("更新敏感词失败");
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="编辑敏感词"
open={visible}
onCancel={handleCancel}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ status: "1", operation: "1" }}
>
<Form.Item
name="title"
label="敏感词标题"
rules={[{ required: true, message: "请输入敏感词标题" }]}
>
<Input placeholder="请输入敏感词标题" />
</Form.Item>
<Form.Item
name="keywords"
label="关键词"
rules={[{ required: true, message: "请输入关键词" }]}
>
<Input placeholder="请输入关键词" />
</Form.Item>
<Form.Item
name="content"
label="敏感词内容"
rules={[{ required: true, message: "请输入敏感词内容" }]}
>
<TextArea rows={4} placeholder="请输入敏感词内容" />
</Form.Item>
<Form.Item
name="operation"
label="操作类型"
rules={[{ required: true, message: "请选择操作类型" }]}
>
<Select placeholder="请选择操作类型">
<Option value="0"></Option>
<Option value="1"></Option>
<Option value="2"></Option>
<Option value="3"></Option>
<Option value="4"></Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: "请选择状态" }]}
>
<Select placeholder="请选择状态">
<Option value="1"></Option>
<Option value="0"></Option>
</Select>
</Form.Item>
<Form.Item>
<div
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
>
<Button onClick={handleCancel}></Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default EditSensitiveWordModal;

View File

@@ -0,0 +1,226 @@
import React, { useState, useEffect } from "react";
import { Button, Input, Tag, Switch, message } from "antd";
import {
SearchOutlined,
FilterOutlined,
FormOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import {
getKeywordList,
deleteKeyword,
setKeywordStatus,
type KeywordListParams,
} from "./api";
import EditKeywordModal from "./EditKeywordModal";
const { Search } = Input;
interface KeywordItem {
id: string;
title: string;
keywords: string;
content: string;
matchType: string;
priority: string;
replyType: string;
status: string;
enabled: boolean;
}
const KeywordManagement: React.FC = () => {
const [searchValue, setSearchValue] = useState<string>("");
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingKeywordId, setEditingKeywordId] = useState<string | null>(null);
// 回复类型映射
const getReplyTypeText = (replyType: string) => {
switch (replyType) {
case "text":
return "文本回复";
case "template":
return "模板回复";
default:
return "未知类型";
}
};
// 回复类型颜色
const getReplyTypeColor = (replyType: string) => {
switch (replyType) {
case "text":
return "#1890ff";
case "template":
return "#722ed1";
default:
return "#8c8c8c";
}
};
// 获取关键词列表
const fetchKeywords = async (params?: KeywordListParams) => {
try {
setLoading(true);
const response = await getKeywordList(params || {});
if (response) {
setKeywordsList(response.list || []);
} else {
setKeywordsList([]);
message.error(response?.message || "获取关键词列表失败");
}
} catch (error) {
console.error("获取关键词列表失败:", error);
setKeywordsList([]);
message.error("获取关键词列表失败");
} finally {
setLoading(false);
}
};
// 关键词管理相关函数
const handleToggleKeyword = async (id: string) => {
try {
const response = await setKeywordStatus({ id });
if (response) {
setKeywordsList(prev =>
prev.map(item =>
item.id === id ? { ...item, enabled: !item.enabled } : item,
),
);
message.success("状态更新成功");
} else {
message.error(response?.message || "状态更新失败");
}
} catch (error) {
console.error("状态更新失败:", error);
message.error("状态更新失败");
}
};
const handleEditKeyword = (id: string) => {
setEditingKeywordId(id);
setEditModalVisible(true);
};
// 编辑弹窗成功回调
const handleEditSuccess = () => {
fetchKeywords(); // 重新获取数据
};
const handleDeleteKeyword = async (id: string) => {
try {
const response = await deleteKeyword(id);
if (response) {
setKeywordsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} else {
message.error(response?.message || "删除失败");
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 搜索和筛选功能
const filteredKeywords = keywordsList.filter(item => {
if (!searchValue) return true;
return (
item.title.toLowerCase().includes(searchValue.toLowerCase()) ||
item.keywords.toLowerCase().includes(searchValue.toLowerCase()) ||
item.content.toLowerCase().includes(searchValue.toLowerCase())
);
});
// 搜索处理函数
const handleSearch = (value: string) => {
fetchKeywords({ keyword: value });
};
// 组件挂载时获取数据
useEffect(() => {
fetchKeywords();
}, []);
return (
<div className={styles.keywordContent}>
<div className={styles.searchSection}>
<Search
placeholder="搜索关键词..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
onSearch={handleSearch}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
<Button icon={<FilterOutlined />}></Button>
</div>
<div className={styles.keywordList}>
{loading ? (
<div className={styles.loading}>...</div>
) : filteredKeywords.length === 0 ? (
<div className={styles.empty}></div>
) : (
filteredKeywords.map(item => (
<div key={item.id} className={styles.keywordItem}>
<div className={styles.itemContent}>
<div className={styles.title}>{item.title}</div>
<div className={styles.tags}>
<Tag className={styles.matchTag}>{item.matchType}</Tag>
<Tag className={styles.priorityTag}>
{item.priority}
</Tag>
</div>
<div className={styles.description}>{item.content}</div>
<Tag
color={getReplyTypeColor(item.replyType)}
className={styles.replyTypeTag}
>
{getReplyTypeText(item.replyType)}
</Tag>
</div>
<div className={styles.itemActions}>
<Switch
checked={item.enabled}
onChange={() => handleToggleKeyword(item.id)}
className={styles.toggleSwitch}
/>
<Button
type="text"
size="small"
icon={<FormOutlined className={styles.editIcon} />}
onClick={() => handleEditKeyword(item.id)}
className={styles.actionBtn}
/>
<Button
type="text"
size="small"
icon={<DeleteOutlined className={styles.deleteIcon} />}
onClick={() => handleDeleteKeyword(item.id)}
className={styles.actionBtn}
/>
</div>
</div>
))
)}
</div>
{/* 编辑弹窗 */}
<EditKeywordModal
visible={editModalVisible}
keywordId={editingKeywordId}
onCancel={() => {
setEditModalVisible(false);
setEditingKeywordId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
};
export default KeywordManagement;

View File

@@ -0,0 +1,260 @@
import React, { useState, useEffect } from "react";
import { Button, Input, Card, message, Switch, Tag } from "antd";
import {
SearchOutlined,
FilterOutlined,
FormOutlined,
FileTextOutlined,
FileImageOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import {
getMaterialList,
deleteMaterial,
setMaterialStatus,
type MaterialListParams,
} from "./api";
import EditMaterialModal from "./EditMaterialModal";
const { Search } = Input;
interface MaterialItem {
id: number;
companyId: number;
userId: number;
title: string;
content: string;
cover: string;
status: number;
type: string; // 素材类型:文本、图片、视频
createTime: string;
updateTime: string;
isDel: number;
delTime: string | null;
userName: string;
}
const MaterialManagement: React.FC = () => {
const [searchValue, setSearchValue] = useState<string>("");
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
null,
);
// 获取类型图标
const getTypeIcon = (type: string) => {
switch (type) {
case "文本":
return <FileTextOutlined className={styles.typeIcon} />;
case "图片":
return <FileImageOutlined className={styles.typeIcon} />;
case "视频":
return <PlayCircleOutlined className={styles.typeIcon} />;
default:
return <FileTextOutlined className={styles.typeIcon} />;
}
};
// 获取类型颜色
const getTypeColor = (type: string) => {
switch (type) {
case "文本":
return "#fa8c16";
case "图片":
return "#52c41a";
case "视频":
return "#1890ff";
default:
return "#8c8c8c";
}
};
// 获取素材列表
const fetchMaterials = async (params?: MaterialListParams) => {
try {
setLoading(true);
const response = await getMaterialList(params || {});
if (response) {
setMaterialsList(response.list || []);
} else {
setMaterialsList([]);
message.error(response?.message || "获取素材列表失败");
}
} catch (error) {
console.error("获取素材列表失败:", error);
setMaterialsList([]);
message.error("获取素材列表失败");
} finally {
setLoading(false);
}
};
// 素材管理相关函数
const handleDeleteMaterial = async (id: number) => {
try {
const response = await deleteMaterial(id.toString());
if (response) {
setMaterialsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} else {
message.error(response?.message || "删除失败");
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 切换素材状态
const handleToggleMaterialStatus = async (id: number, checked: boolean) => {
try {
const response = await setMaterialStatus({ id: id.toString() });
if (response) {
setMaterialsList(prev =>
prev.map(item =>
item.id === id ? { ...item, status: checked ? 1 : 0 } : item,
),
);
message.success(checked ? "启用成功" : "禁用成功");
} else {
message.error(response?.message || "状态更新失败");
}
} catch (error) {
console.error("状态更新失败:", error);
message.error("状态更新失败");
}
};
// 编辑素材
const handleEditMaterial = (id: number) => {
setEditingMaterialId(id);
setEditModalVisible(true);
};
// 编辑弹窗成功回调
const handleEditSuccess = () => {
fetchMaterials(); // 重新获取数据
};
// 搜索处理函数
const handleSearch = (value: string) => {
fetchMaterials({ keyword: value });
};
// 组件挂载时获取数据
useEffect(() => {
fetchMaterials();
}, []);
return (
<div className={styles.materialContent}>
<div className={styles.searchSection}>
<Search
placeholder="搜索素材..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
onSearch={handleSearch}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
<Button icon={<FilterOutlined />}></Button>
</div>
<div className={styles.materialGrid}>
{loading ? (
<div className={styles.loading}>...</div>
) : materialsList.length === 0 ? (
<div className={styles.empty}></div>
) : (
materialsList.map(item => (
<Card
key={item.id}
className={styles.materialCard}
hoverable
onClick={() => handleEditMaterial(item.id)}
>
<div className={styles.thumbnail}>
{item.cover ? (
<div
style={{
position: "relative",
width: "100%",
height: "100%",
}}
>
<img
src={item.cover}
alt={item.title}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
<div
style={{
position: "absolute",
top: "4px",
right: "4px",
background: "rgba(0, 0, 0, 0.6)",
color: "white",
padding: "2px 6px",
borderRadius: "4px",
fontSize: "10px",
fontWeight: "500",
}}
>
{item.type}
</div>
</div>
) : (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
color: "#999",
flexDirection: "column",
}}
>
{getTypeIcon(item.type)}
<span style={{ marginTop: "8px", fontSize: "12px" }}>
{item.type}
</span>
</div>
)}
</div>
<div className={styles.cardContent}>
<div className={styles.title}>{item.title}</div>
<div className={styles.meta}>
<div>: {item.userName}</div>
<div>{item.createTime}</div>
</div>
</div>
</Card>
))
)}
</div>
{/* 编辑弹窗 */}
<EditMaterialModal
visible={editModalVisible}
materialId={editingMaterialId}
onCancel={() => {
setEditModalVisible(false);
setEditingMaterialId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
};
export default MaterialManagement;

View File

@@ -0,0 +1,231 @@
import React, { useState, useEffect } from "react";
import { Button, Input, Tag, Switch, message } from "antd";
import {
SearchOutlined,
FilterOutlined,
FormOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import {
getSensitiveWordList,
deleteSensitiveWord,
setSensitiveWordStatus,
type SensitiveWordListParams,
} from "./api";
import EditSensitiveWordModal from "./EditSensitiveWordModal";
const { Search } = Input;
interface SensitiveWordItem {
id: string;
title: string;
keywords: string;
content: string;
operation: string;
status: string;
enabled: boolean;
}
const SensitiveWordManagement: React.FC = () => {
const [searchValue, setSearchValue] = useState<string>("");
const [sensitiveWordsList, setSensitiveWordsList] = useState<
SensitiveWordItem[]
>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingSensitiveWordId, setEditingSensitiveWordId] = useState<
string | null
>(null);
const getTagColor = (tag: string) => {
switch (tag) {
case "政治":
return "#ff4d4f";
case "色情":
return "#ff4d4f";
case "暴力":
return "#ff4d4f";
default:
return "#ff4d4f";
}
};
// 操作类型映射
const getOperationText = (operation: string) => {
switch (operation) {
case "0":
return "不操作";
case "1":
return "替换";
case "2":
return "删除";
case "3":
return "警告";
case "4":
return "禁止发送";
default:
return "未知操作";
}
};
// 获取敏感词列表
const fetchSensitiveWords = async (params?: SensitiveWordListParams) => {
try {
setLoading(true);
const response = await getSensitiveWordList(params || {});
if (response) {
setSensitiveWordsList(response.list || []);
} else {
setSensitiveWordsList([]);
message.error(response?.message || "获取敏感词列表失败");
}
} catch (error) {
console.error("获取敏感词列表失败:", error);
setSensitiveWordsList([]);
message.error("获取敏感词列表失败");
} finally {
setLoading(false);
}
};
// 敏感词管理相关函数
const handleToggleSensitiveWord = async (id: string) => {
try {
const response = await setSensitiveWordStatus({ id });
if (response) {
setSensitiveWordsList(prev =>
prev.map(item =>
item.id === id ? { ...item, enabled: !item.enabled } : item,
),
);
message.success("状态更新成功");
} else {
message.error(response?.message || "状态更新失败");
}
} catch (error) {
console.error("状态更新失败:", error);
message.error("状态更新失败");
}
};
const handleEditSensitiveWord = (id: string) => {
setEditingSensitiveWordId(id);
setEditModalVisible(true);
};
// 编辑弹窗成功回调
const handleEditSuccess = () => {
fetchSensitiveWords(); // 重新获取数据
};
const handleDeleteSensitiveWord = async (id: string) => {
try {
const response = await deleteSensitiveWord(id);
if (response) {
setSensitiveWordsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} else {
message.error(response?.message || "删除失败");
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 搜索和筛选功能
const filteredSensitiveWords = sensitiveWordsList.filter(item => {
if (!searchValue) return true;
return (
item.title.toLowerCase().includes(searchValue.toLowerCase()) ||
item.keywords.toLowerCase().includes(searchValue.toLowerCase()) ||
item.content.toLowerCase().includes(searchValue.toLowerCase())
);
});
// 搜索处理函数
const handleSearch = (value: string) => {
fetchSensitiveWords({ keyword: value });
};
// 组件挂载时获取数据
useEffect(() => {
fetchSensitiveWords();
}, []);
return (
<div className={styles.sensitiveContent}>
<div className={styles.searchSection}>
<Search
placeholder="搜索敏感词..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
onSearch={handleSearch}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
<Button icon={<FilterOutlined />}></Button>
</div>
<div className={styles.sensitiveList}>
{loading ? (
<div className={styles.loading}>...</div>
) : filteredSensitiveWords.length === 0 ? (
<div className={styles.empty}></div>
) : (
filteredSensitiveWords.map(item => (
<div key={item.id} className={styles.sensitiveItem}>
<div className={styles.itemContent}>
<div className={styles.categoryName}>{item.title}</div>
<Tag
color={getTagColor(item.keywords)}
className={styles.sensitiveTag}
>
{item.keywords}
</Tag>
<div className={styles.actionText}>
{getOperationText(item.operation)}
</div>
</div>
<div className={styles.itemActions}>
<Switch
checked={item.enabled}
onChange={() => handleToggleSensitiveWord(item.id)}
className={styles.toggleSwitch}
/>
<Button
type="text"
size="small"
icon={<FormOutlined className={styles.editIcon} />}
onClick={() => handleEditSensitiveWord(item.id)}
className={styles.actionBtn}
/>
<Button
type="text"
size="small"
icon={<DeleteOutlined className={styles.deleteIcon} />}
onClick={() => handleDeleteSensitiveWord(item.id)}
className={styles.actionBtn}
/>
</div>
</div>
))
)}
</div>
{/* 编辑弹窗 */}
<EditSensitiveWordModal
visible={editModalVisible}
sensitiveWordId={editingSensitiveWordId}
onCancel={() => {
setEditModalVisible(false);
setEditingSensitiveWordId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
};
export default SensitiveWordManagement;

View File

@@ -0,0 +1,171 @@
import request from "@/api/request";
// 素材管理相关接口
export interface MaterialListParams {
keyword?: string;
limit?: string;
page?: string;
}
export interface MaterialAddRequest {
content: string[];
cover: string;
status: string;
title: string;
type: string; // 素材类型:文本、图片、视频
[property: string]: any;
}
export interface MaterialUpdateRequest extends MaterialAddRequest {
id?: string;
}
export interface MaterialSetStatusRequest {
id: string;
}
// 素材管理-列表
export function getMaterialList(params: MaterialListParams) {
return request("/v1/kefu/content/material/list", params, "GET");
}
// 素材管理-添加
export function addMaterial(data: MaterialAddRequest) {
return request("/v1/kefu/content/material/add", data, "POST");
}
// 素材管理-详情
export function getMaterialDetails(id: string) {
return request("/v1/kefu/content/material/details", { id }, "GET");
}
// 素材管理-删除
export function deleteMaterial(id: string) {
return request("/v1/kefu/content/material/del", { id }, "GET");
}
// 素材管理-更新
export function updateMaterial(data: MaterialUpdateRequest) {
return request("/v1/kefu/content/material/update", data, "POST");
}
// 素材管理-修改状态
export function setMaterialStatus(data: MaterialSetStatusRequest) {
return request("/v1/kefu/content/material/setStatus", data, "POST");
}
// 违禁词管理相关接口
export interface SensitiveWordListParams {
keyword?: string;
limit?: string;
page?: string;
}
export interface SensitiveWordAddRequest {
content: string;
keywords: string;
/**
* 操作 0不操作 1替换 2删除 3警告 4禁止发送
*/
operation: string;
status: string;
title: string;
}
export interface SensitiveWordUpdateRequest extends SensitiveWordAddRequest {
id?: string;
}
export interface SensitiveWordSetStatusRequest {
id: string;
}
// 违禁词管理-列表
export function getSensitiveWordList(params: SensitiveWordListParams) {
return request("/v1/kefu/content/sensitiveWord/list", params, "GET");
}
// 违禁词管理-添加
export function addSensitiveWord(data: SensitiveWordAddRequest) {
return request("/v1/kefu/content/sensitiveWord/add", data, "POST");
}
// 违禁词管理-详情
export function getSensitiveWordDetails(id: string) {
return request("/v1/kefu/content/sensitiveWord/details", { id }, "GET");
}
// 违禁词管理-删除
export function deleteSensitiveWord(id: string) {
return request("/v1/kefu/content/sensitiveWord/del", { id }, "GET");
}
// 违禁词管理-更新
export function updateSensitiveWord(data: SensitiveWordUpdateRequest) {
return request("/v1/kefu/content/sensitiveWord/update", data, "POST");
}
// 违禁词管理-修改状态
export function setSensitiveWordStatus(data: SensitiveWordSetStatusRequest) {
return request("/v1/kefu/content/sensitiveWord/setStatus", data, "POST");
}
// 关键词回复管理相关接口
export interface KeywordListParams {
keyword?: string;
limit?: string;
page?: string;
}
export interface KeywordAddRequest {
title: string;
keywords: string;
content: string;
matchType: string; // 匹配类型:模糊匹配、精确匹配
priority: string; // 优先级
replyType: string; // 回复类型:文本回复、模板回复
status: string;
}
export interface KeywordUpdateRequest extends KeywordAddRequest {
id?: string;
}
export interface KeywordSetStatusRequest {
id: string;
}
// 关键词回复-列表
export function getKeywordList(params: KeywordListParams) {
return request("/v1/kefu/content/keywords/list", params, "GET");
}
// 关键词回复-添加
export function addKeyword(data: KeywordAddRequest) {
return request("/v1/kefu/content/keywords/add", data, "POST");
}
// 关键词回复-详情
export function getKeywordDetails(id: string) {
return request("/v1/kefu/content/keywords/details", { id }, "GET");
}
// 关键词回复-删除
export function deleteKeyword(id: string) {
return request("/v1/kefu/content/keywords/del", { id }, "GET");
}
// 关键词回复-更新
export function updateKeyword(data: KeywordUpdateRequest) {
return request("/v1/kefu/content/keywords/update", data, "POST");
}
// 关键词回复-修改状态
export function setKeywordStatus(data: KeywordSetStatusRequest) {
return request("/v1/kefu/content/keywords/setStatus", data, "POST");
}
//获取好友接待配置
export function getFriendInjectConfig(params) {
return request("/v1/kefu/ai/friend/get", params, "GET");
}

View File

@@ -0,0 +1,3 @@
export { default as MaterialManagement } from "../MaterialManagement";
export { default as SensitiveWordManagement } from "../SensitiveWordManagement";
export { default as KeywordManagement } from "../KeywordManagement";

View File

@@ -1,31 +1,152 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #f5f5f5;
min-height: 100vh;
}
.header {
.headerActions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
:global(.ant-btn) {
height: 36px;
border-radius: 6px;
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.tabsSection {
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #e8e8e8;
background: #fff;
border-radius: 8px 8px 0 0;
padding: 16px 16px 0px 16px;
.tab {
padding: 12px 24px;
cursor: pointer;
border-bottom: 2px solid transparent;
color: #666;
font-size: 14px;
transition: all 0.3s;
user-select: none;
&:hover {
color: #1890ff;
background-color: #f5f5f5;
}
}
.tabActive {
color: #1890ff;
border-bottom-color: #1890ff;
background-color: #f0f8ff;
font-weight: 500;
}
}
.content {
background: #fff;
border-radius: 0 0 8px 8px;
padding: 24px;
min-height: 400px;
}
.materialContent {
.searchSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
:global(.ant-input-search) {
width: 300px;
}
:global(.ant-btn) {
height: 32px;
border-radius: 6px;
}
}
.materialGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.materialCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
:global(.ant-card-body) {
padding: 16px;
}
.thumbnail {
width: 100%;
height: 120px;
background: #f5f5f5;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
position: relative;
.typeIcon {
font-size: 32px;
color: #ccc;
}
}
.cardContent {
.title {
font-size: 16px;
font-weight: 500;
color: #188eee;
margin: 0 0 12px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.typeSize {
margin-bottom: 8px;
font-size: 12px;
font-weight: 500;
}
.meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #8c8c8c;
span:first-child {
color: #1890ff;
}
}
}
}
}
.placeholder {
display: flex;
align-items: center;
@@ -34,10 +155,452 @@
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
font-size: 16px;
color: #8c8c8c;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 16px;
color: #8c8c8c;
}
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 16px;
color: #8c8c8c;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
margin: 20px 0;
}
// 敏感词管理样式
.sensitiveContent {
.searchSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
:global(.ant-input-search) {
width: 300px;
}
:global(.ant-btn) {
height: 32px;
border-radius: 6px;
}
}
}
.sensitiveList {
display: flex;
flex-direction: column;
gap: 12px;
}
.sensitiveItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.itemContent {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
.categoryName {
font-size: 16px;
font-weight: 500;
color: #262626;
min-width: 60px;
}
.sensitiveTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
border: none;
min-width: 40px;
text-align: center;
}
.actionText {
font-size: 14px;
color: #666;
flex: 1;
}
}
.itemActions {
display: flex;
align-items: center;
gap: 8px;
.toggleSwitch {
:global(.ant-switch) {
background-color: #d9d9d9;
}
:global(.ant-switch-checked) {
background-color: #1890ff;
}
}
.actionBtn {
width: 28px;
height: 28px;
padding: 0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #f5f5f5;
}
.editIcon {
font-size: 14px;
color: #1890ff;
}
.deleteIcon {
font-size: 14px;
color: #ff4d4f;
}
}
}
}
}
// 关键词管理样式
.keywordContent {
.searchSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
:global(.ant-input-search) {
width: 300px;
}
:global(.ant-btn) {
height: 32px;
border-radius: 6px;
}
}
.keywordList {
display: flex;
flex-direction: column;
gap: 12px;
}
.keywordItem {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px 20px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.itemContent {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
.title {
font-size: 16px;
font-weight: 500;
color: #262626;
margin: 0;
}
.tags {
display: flex;
gap: 8px;
margin-bottom: 4px;
.matchTag,
.priorityTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
background: #f0f0f0;
color: #666;
border: none;
}
}
.description {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 8px;
}
.replyTypeTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
border: none;
align-self: flex-start;
}
}
.itemActions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
.toggleSwitch {
:global(.ant-switch) {
background-color: #d9d9d9;
}
:global(.ant-switch-checked) {
background-color: #1890ff;
}
}
.actionBtn {
width: 28px;
height: 28px;
padding: 0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #f5f5f5;
}
.editIcon {
font-size: 14px;
color: #1890ff;
}
.deleteIcon {
font-size: 14px;
color: #ff4d4f;
}
}
}
}
}
// 弹窗中的图片上传组件样式
:global(.material-cover-upload) {
.uploadContainer {
:global(.adm-image-uploader) {
.adm-image-uploader-upload-button {
width: 120px;
height: 120px;
}
.adm-image-uploader-item {
width: 120px;
height: 120px;
}
}
}
}
// 素材卡片操作按钮样式
.cardActions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 8px;
.editBtn {
color: #1890ff;
border-color: #1890ff;
&:hover {
color: #40a9ff;
border-color: #40a9ff;
}
}
}
// 素材类型图标样式
.typeIcon {
font-size: 24px;
color: #999;
}
// 状态操作区域样式
.statusActions {
margin-top: 8px;
display: flex;
justify-content: flex-end;
}
// 卡片头部样式
.cardHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
gap: 8px;
.title {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
// 类型标签样式
.typeTag {
flex-shrink: 0;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
// 响应式设计
@media (max-width: 768px) {
.container {
padding: 16px;
}
.headerActions {
flex-direction: column;
align-items: stretch;
:global(.ant-btn) {
width: 100%;
}
}
.tabs {
flex-direction: column;
padding: 0;
.tab {
border-bottom: 1px solid #e8e8e8;
border-radius: 0;
&:last-child {
border-bottom: none;
}
}
}
.materialContent {
.searchSection {
flex-direction: column;
gap: 12px;
align-items: stretch;
:global(.ant-input-search) {
width: 100%;
}
}
.materialGrid {
grid-template-columns: 1fr;
gap: 16px;
}
}
.sensitiveContent {
.searchSection {
flex-direction: column;
gap: 12px;
align-items: stretch;
:global(.ant-input-search) {
width: 100%;
}
}
.sensitiveItem {
flex-direction: column;
align-items: stretch;
gap: 12px;
.itemContent {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.categoryName {
min-width: auto;
}
}
.itemActions {
justify-content: flex-end;
}
}
}
.keywordContent {
.searchSection {
flex-direction: column;
gap: 12px;
align-items: stretch;
:global(.ant-input-search) {
width: 100%;
}
}
.keywordItem {
flex-direction: column;
align-items: stretch;
gap: 12px;
.itemContent {
.tags {
flex-wrap: wrap;
}
}
.itemActions {
justify-content: flex-end;
margin-left: 0;
}
}
}
}

View File

@@ -1,22 +1,123 @@
import React from "react";
import React, { useState } from "react";
import { Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
import MaterialManagement from "./MaterialManagement";
import SensitiveWordManagement from "./SensitiveWordManagement";
import KeywordManagement from "./KeywordManagement";
import AddMaterialModal from "./AddMaterialModal";
import AddSensitiveWordModal from "./AddSensitiveWordModal";
import AddKeywordModal from "./AddKeywordModal";
const ContentManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>("material");
const [materialModalVisible, setMaterialModalVisible] = useState(false);
const [sensitiveWordModalVisible, setSensitiveWordModalVisible] =
useState(false);
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
const tabs = [
{ key: "material", label: "素材资源库" },
{ key: "sensitive", label: "敏感词管理" },
{ key: "keyword", label: "关键词回复" },
];
// 按钮点击处理函数
const handleAddMaterial = () => {
setMaterialModalVisible(true);
};
const handleAddSensitiveWord = () => {
setSensitiveWordModalVisible(true);
};
const handleAddKeyword = () => {
setKeywordModalVisible(true);
};
// 弹窗成功回调
const handleModalSuccess = () => {
// 这里可以触发子组件重新获取数据
// 由于子组件是独立的,它们会在组件重新挂载时自动获取数据
};
const renderTabContent = () => {
switch (activeTab) {
case "material":
return <MaterialManagement />;
case "sensitive":
return <SensitiveWordManagement />;
case "keyword":
return <KeywordManagement />;
default:
return <MaterialManagement />;
}
};
return (
<div className={styles.container}>
<PowerNavigation
title="内容管理"
subtitle="素材管理、数据词汇库、关键词自动回复"
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
showBackButton={true}
backButtonText="返回功能中心"
rightContent={
<div className={styles.headerActions}>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={handleAddMaterial}
>
</Button>
<Button icon={<PlusOutlined />} onClick={handleAddKeyword}>
</Button>
<Button icon={<PlusOutlined />} onClick={handleAddSensitiveWord}>
</Button>
</div>
}
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
<div className={styles.tabsSection}>
<br />
<div className={styles.tabs}>
{tabs.map(tab => (
<div
key={tab.key}
className={`${styles.tab} ${
activeTab === tab.key ? styles.tabActive : ""
}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</div>
))}
</div>
</div>
<div className={styles.content}>{renderTabContent()}</div>
{/* 弹窗组件 */}
<AddMaterialModal
visible={materialModalVisible}
onCancel={() => setMaterialModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<AddSensitiveWordModal
visible={sensitiveWordModalVisible}
onCancel={() => setSensitiveWordModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<AddKeywordModal
visible={keywordModalVisible}
onCancel={() => setKeywordModalVisible(false)}
onSuccess={handleModalSuccess}
/>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export { default as AddMaterialModal } from "../AddMaterialModal";
export { default as AddSensitiveWordModal } from "../AddSensitiveWordModal";
export { default as AddKeywordModal } from "../AddKeywordModal";

View File

@@ -35,7 +35,7 @@ export const featureCategories: FeatureCategory[] = [
{
id: "core",
title: "核心功能",
icon: <AimOutlined />,
icon: <AimOutlined style={{ fontSize: "24px" }} />,
color: "#1890ff",
count: 2,
features: [
@@ -43,7 +43,7 @@ export const featureCategories: FeatureCategory[] = [
id: "customer-management",
title: "客户好友管理",
description: "管理客户关系,维护好友信息,提升客户满意度",
icon: <TeamOutlined />,
icon: <TeamOutlined style={{ fontSize: "24px" }} />,
color: "#1890ff",
path: "/pc/powerCenter/customer-management",
isHot: true,
@@ -52,7 +52,7 @@ export const featureCategories: FeatureCategory[] = [
id: "communication-record",
title: "沟通记录",
description: "记录和分析所有客户沟通历史,优化服务质量",
icon: <CommentOutlined />,
icon: <CommentOutlined style={{ fontSize: "24px" }} />,
color: "#52c41a",
path: "/pc/powerCenter/communication-record",
},
@@ -61,7 +61,7 @@ export const featureCategories: FeatureCategory[] = [
{
id: "ai",
title: "AI智能功能",
icon: <ThunderboltOutlined />,
icon: <ThunderboltOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
count: 2,
features: [
@@ -69,7 +69,7 @@ export const featureCategories: FeatureCategory[] = [
id: "ai-training",
title: "AI模型训练",
description: "训练专属AI模型,提升智能服务能力",
icon: <FileTextOutlined />,
icon: <FileTextOutlined style={{ fontSize: "24px" }} />,
color: "#fa8c16",
path: "/pc/powerCenter/ai-training",
isNew: true,
@@ -78,7 +78,7 @@ export const featureCategories: FeatureCategory[] = [
id: "auto-greeting",
title: "自动问候",
description: "设置智能问候规则,自动化客户接待流程",
icon: <SoundOutlined />,
icon: <SoundOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
path: "/pc/powerCenter/auto-greeting",
},
@@ -87,7 +87,7 @@ export const featureCategories: FeatureCategory[] = [
{
id: "marketing",
title: "营销管理",
icon: <RiseOutlined />,
icon: <RiseOutlined style={{ fontSize: "24px" }} />,
color: "#52c41a",
count: 1,
features: [
@@ -95,7 +95,7 @@ export const featureCategories: FeatureCategory[] = [
id: "content-management",
title: "内容管理",
description: "管理营销内容,素材库,提升内容创作效率",
icon: <EditOutlined />,
icon: <EditOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
path: "/pc/powerCenter/content-management",
},

View File

@@ -4,7 +4,7 @@ import Dashboard from "@/pages/pc/ckbox/dashboard";
import PowerCenter from "@/pages/pc/ckbox/powerCenter";
import CustomerManagement from "@/pages/pc/ckbox/powerCenter/customer-management";
import CommunicationRecord from "@/pages/pc/ckbox/powerCenter/communication-record";
import ContentManagement from "@/pages/pc/ckbox/powerCenter/content-management";
import ContentManagement from "@/pages/pc/ckbox/powerCenter/content-management/index";
import AiTraining from "@/pages/pc/ckbox/powerCenter/ai-training";
import AutoGreeting from "@/pages/pc/ckbox/powerCenter/auto-greeting";
import CommonConfig from "@/pages/pc/ckbox/commonConfig";