移除冗余文件并重构内容管理模块:删除技术栈和兼容性说明文档,移除多个模态框组件,整合管理组件为统一结构,优化代码组织,提升可维护性。

This commit is contained in:
超级老白兔
2025-09-28 14:52:05 +08:00
parent a73481808c
commit 3b063ff64c
20 changed files with 639 additions and 978 deletions

View File

@@ -1,177 +0,0 @@
# 存客宝项目 - 浏览器兼容性说明
## 🎯 **兼容性目标**
本项目已配置为支持以下浏览器版本:
- **Chrome**: 50+
- **Firefox**: 50+
- **Safari**: 10+
- **Edge**: 12+
- **Internet Explorer**: 11+ (部分功能受限)
- **Android**: 4.4+ (特别优化Android 7)
- **iOS**: 9+
## 🔧 **兼容性配置**
### 1. **Polyfill 支持**
项目已集成以下 polyfill 来确保低版本浏览器兼容性:
- **core-js**: ES6+ 特性支持
- **regenerator-runtime**: async/await 支持
- **whatwg-fetch**: fetch API 支持
- **Android专用polyfill**: 针对Android 7等低版本系统优化
### 2. **构建配置**
- 使用 **terser** 进行代码压缩
- 配置了 **browserslist** 目标浏览器
- 添加了兼容性检测组件
- 特别针对Android设备优化
### 3. **特性支持**
项目通过 polyfill 支持以下 ES6+ 特性:
- ✅ Promise
- ✅ fetch API
- ✅ Array.from, Array.find, Array.includes, Array.findIndex
- ✅ Object.assign, Object.entries, Object.values, Object.keys
- ✅ String.includes, String.startsWith, String.endsWith
- ✅ Map, Set, WeakMap, WeakSet
- ✅ Symbol
- ✅ requestAnimationFrame
- ✅ IntersectionObserver
- ✅ ResizeObserver
- ✅ URLSearchParams
- ✅ AbortController
## 🚀 **使用方法**
### 开发环境
```bash
pnpm dev
```
### 生产构建
```bash
pnpm build
```
### 预览构建结果
```bash
pnpm preview
```
## 📱 **Android 特别优化**
### **Android 7 兼容性**
Android 7 (API 24) 系统对ES6+特性支持不完整,项目已特别优化:
#### **问题解决:**
- ✅ Array.prototype.includes 方法缺失
- ✅ String.prototype.includes 方法缺失
- ✅ Object.assign 方法缺失
- ✅ Array.from 方法缺失
- ✅ requestAnimationFrame 缺失
- ✅ IntersectionObserver 缺失
- ✅ URLSearchParams 缺失
#### **解决方案:**
- 使用自定义polyfill补充缺失方法
- 提供降级实现确保功能可用
- 自动检测Android版本并启用相应polyfill
### **Android WebView 优化**
- 针对系统WebView进行特别优化
- 支持微信、QQ等内置浏览器
- 提供降级方案确保基本功能可用
## ⚠️ **注意事项**
1. **Android 7 支持**
- 已启用兼容模式,基本功能可用
- 建议升级到Android 8+或使用最新版Chrome
- 部分高级特性可能受限
2. **Android 6 及以下**
- 支持有限,建议升级系统
- 使用最新版Chrome浏览器
- 部分功能可能不可用
3. **移动端兼容性**
- iOS Safari 10+
- Android Chrome 50+
- 微信内置浏览器 (部分功能受限)
- QQ内置浏览器 (部分功能受限)
4. **性能考虑**
- polyfill 会增加包体积
- 现代浏览器会自动忽略不需要的 polyfill
- Android设备上会有额外的兼容性检测
## 🔍 **兼容性检测**
项目包含自动兼容性检测功能:
### **通用检测**
- 在低版本浏览器中会显示警告提示
- 控制台会输出兼容性信息
- 建议用户升级浏览器
### **Android专用检测**
- 自动检测Android系统版本
- 检测Chrome和WebView版本
- 识别微信、QQ等内置浏览器
- 提供针对性的解决方案建议
## 📝 **更新日志**
### v3.0.0
- ✅ 添加 ES5 兼容性支持
- ✅ 集成 core-js polyfill
- ✅ 添加兼容性检测组件
- ✅ 优化构建配置
-**新增Android 7专用polyfill**
-**新增Android兼容性检测**
-**优化移动端体验**
## 🛠️ **故障排除**
如果遇到兼容性问题:
1. **Android设备问题**
- 检查Android系统版本
- 确认Chrome浏览器版本
- 查看控制台错误信息
- 尝试使用系统浏览器而非内置浏览器
2. **通用问题**
- 检查浏览器版本是否在支持范围内
- 查看控制台是否有错误信息
- 确认 polyfill 是否正确加载
- 尝试清除浏览器缓存
3. **性能问题**
- 在低版本设备上可能加载较慢
- 建议使用WiFi网络
- 关闭不必要的浏览器扩展
## 📞 **技术支持**
如有兼容性问题,请联系开发团队。
### **特别说明**
本项目已针对Android 7等低版本系统进行了特别优化通过代码弥补了系统内核对ES6+特性支持不完整的问题。虽然不能完全替代系统升级但可以确保应用在低版本Android设备上正常运行。

View File

@@ -1,26 +0,0 @@
## 使用技术栈
- React 18
- TypeScript
- Vite新一代前端构建工具
- axios
- sass (scss)
- React Router v6
- antd-mobile
- antd已设置基础单位为 rem配合 postcss-pxtorem
- postcss-pxtorempx 转 rem移动端适配
- ESLint + Prettier代码规范与自动格式化
- 路径别名 @ 指向 src 目录
## 关于兼容与工程化
- 自动化脚本yarn lint、yarn dev 等)
- 移动端 rem 适配html 根字体 + pxtorem
- iOS 浏览器滚动回弹兼容问题已通过全局样式处理
- 支持 VS Code 编辑器自动格式化(推荐配合 ESLint/Prettier 插件)
## 目录结构简要
- src/ 业务源码pages、api、styles、App.tsx、main.tsx 等)
- public/ 静态资源目录
- index.html 项目入口(根目录)
- vite.config.ts 构建与路径别名配置
- tsconfig.json TypeScript 配置
- .eslintrc.js 代码规范配置

View File

@@ -18,7 +18,7 @@ const IndexPage: React.FC = () => {
if (isMobile()) {
navigate("/mobile/dashboard");
} else {
navigate("/pc/dashboard");
navigate("/pc/weChat");
}
}, [navigate]);

View File

@@ -1,162 +0,0 @@
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

@@ -1,174 +0,0 @@
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

@@ -1,133 +0,0 @@
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

@@ -1,211 +0,0 @@
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

@@ -7,13 +7,22 @@ export interface MaterialListParams {
page?: string;
}
// 内容项类型定义
export interface ContentItem {
type: "text" | "image" | "video" | "file" | "audio" | "link";
data: string | LinkData;
}
// 链接数据类型
export interface LinkData {
url: string;
}
export interface MaterialAddRequest {
content: string[];
cover: string;
status: string;
title: string;
type: string; // 素材类型:文本、图片、视频
[property: string]: any;
cover?: string;
status: number;
content: ContentItem[];
}
export interface MaterialUpdateRequest extends MaterialAddRequest {

View File

@@ -1,3 +1,9 @@
export { default as MaterialManagement } from "../MaterialManagement";
export { default as SensitiveWordManagement } from "../SensitiveWordManagement";
export { default as KeywordManagement } from "../KeywordManagement";
// 管理组件导出
export { default as MaterialManagement } from "./management/MaterialManagement";
export { default as SensitiveWordManagement } from "./management/SensitiveWordManagement";
export { default as KeywordManagement } from "./management/KeywordManagement";
// 模态框组件导出
export { default as MaterialModal } from "./modals/MaterialModal";
export { default as SensitiveWordModal } from "./modals/SensitiveWordModal";
export { default as KeywordModal } from "./modals/KeywordModal";

View File

@@ -6,14 +6,14 @@ import {
FormOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import styles from "../../index.module.scss";
import {
getKeywordList,
deleteKeyword,
setKeywordStatus,
type KeywordListParams,
} from "./api";
import EditKeywordModal from "./EditKeywordModal";
} from "../../api";
import KeywordModal from "../modals/KeywordModal";
const { Search } = Input;
@@ -210,8 +210,9 @@ const KeywordManagement: React.FC = () => {
</div>
{/* 编辑弹窗 */}
<EditKeywordModal
<KeywordModal
visible={editModalVisible}
mode="edit"
keywordId={editingKeywordId}
onCancel={() => {
setEditModalVisible(false);

View File

@@ -1,4 +1,9 @@
import React, { useState, useEffect } from "react";
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { Button, Input, Card, message, Switch, Tag } from "antd";
import {
SearchOutlined,
@@ -8,14 +13,14 @@ import {
FileImageOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import styles from "../../index.module.scss";
import {
getMaterialList,
deleteMaterial,
setMaterialStatus,
type MaterialListParams,
} from "./api";
import EditMaterialModal from "./EditMaterialModal";
} from "../../api";
import MaterialModal from "../modals/MaterialModal";
const { Search } = Input;
@@ -35,7 +40,7 @@ interface MaterialItem {
userName: string;
}
const MaterialManagement: React.FC = () => {
const MaterialManagement = forwardRef<any, {}>((props, ref) => {
const [searchValue, setSearchValue] = useState<string>("");
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
@@ -92,6 +97,11 @@ const MaterialManagement: React.FC = () => {
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchMaterials,
}));
// 素材管理相关函数
const handleDeleteMaterial = async (id: number) => {
try {
@@ -244,8 +254,9 @@ const MaterialManagement: React.FC = () => {
</div>
{/* 编辑弹窗 */}
<EditMaterialModal
<MaterialModal
visible={editModalVisible}
mode="edit"
materialId={editingMaterialId}
onCancel={() => {
setEditModalVisible(false);
@@ -255,6 +266,6 @@ const MaterialManagement: React.FC = () => {
/>
</div>
);
};
});
export default MaterialManagement;

View File

@@ -6,14 +6,14 @@ import {
FormOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import styles from "../../index.module.scss";
import {
getSensitiveWordList,
deleteSensitiveWord,
setSensitiveWordStatus,
type SensitiveWordListParams,
} from "./api";
import EditSensitiveWordModal from "./EditSensitiveWordModal";
} from "../../api";
import SensitiveWordModal from "../modals/SensitiveWordModal";
const { Search } = Input;
@@ -215,8 +215,9 @@ const SensitiveWordManagement: React.FC = () => {
</div>
{/* 编辑弹窗 */}
<EditSensitiveWordModal
<SensitiveWordModal
visible={editModalVisible}
mode="edit"
sensitiveWordId={editingSensitiveWordId}
onCancel={() => {
setEditModalVisible(false);

View File

@@ -0,0 +1,4 @@
// 管理组件统一导出
export { default as MaterialManagement } from "./MaterialManagement";
export { default as SensitiveWordManagement } from "./SensitiveWordManagement";
export { default as KeywordManagement } from "./KeywordManagement";

View File

@@ -0,0 +1,244 @@
import React, { useState, useEffect } from "react";
import { Button, Input, Select } from "antd";
import {
PlusOutlined,
DeleteOutlined,
FileTextOutlined,
FileImageOutlined,
PlayCircleOutlined,
FileOutlined,
SoundOutlined,
LinkOutlined,
} from "@ant-design/icons";
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
import VideoUpload from "@/components/Upload/VideoUpload";
import FileUpload from "@/components/Upload/FileUpload";
import type { ContentItem } from "../../api";
const { TextArea } = Input;
const { Option } = Select;
interface ContentManagerProps {
value?: ContentItem[];
onChange?: (content: ContentItem[]) => void;
}
const ContentManager: React.FC<ContentManagerProps> = ({
value = [],
onChange,
}) => {
const [contentItems, setContentItems] = useState<ContentItem[]>(value);
// 内容类型配置
const contentTypes = [
{ value: "text", label: "文本", icon: <FileTextOutlined /> },
{ value: "image", label: "图片", icon: <FileImageOutlined /> },
{ value: "video", label: "视频", icon: <PlayCircleOutlined /> },
{ value: "file", label: "文件", icon: <FileOutlined /> },
{ value: "audio", label: "音频", icon: <SoundOutlined /> },
{ value: "link", label: "链接", icon: <LinkOutlined /> },
];
// 同步外部value到内部state
useEffect(() => {
setContentItems(value);
}, [value]);
// 初始化时添加默认文本内容项
useEffect(() => {
if (contentItems.length === 0) {
const defaultTextItem: ContentItem = {
type: "text",
data: "",
};
setContentItems([defaultTextItem]);
onChange?.([defaultTextItem]);
}
}, [contentItems.length, onChange]);
// 更新内容项
const updateContentItems = (newItems: ContentItem[]) => {
setContentItems(newItems);
onChange?.(newItems);
};
// 添加新内容项
const handleAddItem = () => {
const newItem: ContentItem = {
type: "text",
data: "",
};
const newItems = [...contentItems, newItem];
updateContentItems(newItems);
};
// 删除内容项
const handleDeleteItem = (index: number) => {
const newItems = contentItems.filter((_, i) => i !== index);
updateContentItems(newItems);
};
// 更新内容项数据
const updateItemData = (index: number, data: any) => {
const newItems = [...contentItems];
newItems[index] = { ...newItems[index], data };
updateContentItems(newItems);
};
// 更新内容项类型
const updateItemType = (index: number, newType: string) => {
const newItems = [...contentItems];
// 根据新类型重置数据
let newData: any;
if (newType === "link") {
newData = "";
} else {
newData = "";
}
newItems[index] = {
type: newType as any,
data: newData,
};
updateContentItems(newItems);
};
// 渲染内容项
const renderContentItem = (item: ContentItem, index: number) => {
return (
<div
key={index}
style={{
border: "1px solid #d9d9d9",
borderRadius: "6px",
padding: "12px",
marginBottom: "8px",
backgroundColor: "#fafafa",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Select
value={item.type}
onChange={newType => updateItemType(index, newType)}
style={{ width: 120 }}
size="small"
>
{contentTypes.map(type => (
<Option key={type.value} value={type.value}>
{type.icon} {type.label}
</Option>
))}
</Select>
</div>
{index !== 0 && (
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteItem(index)}
/>
)}
</div>
{renderContentInput(item, index)}
</div>
);
};
// 渲染内容输入
const renderContentInput = (item: ContentItem, index: number) => {
switch (item.type) {
case "text":
return (
<TextArea
value={item.data as string}
onChange={e => updateItemData(index, e.target.value)}
placeholder="请输入文本内容"
rows={3}
/>
);
case "image":
return (
<ImageUpload
count={1}
accept="image/*"
value={item.data ? [item.data as string] : []}
onChange={urls => updateItemData(index, urls[0] || "")}
/>
);
case "video":
return (
<VideoUpload
value={item.data as string}
onChange={url => updateItemData(index, url)}
maxSize={50}
showPreview={true}
/>
);
case "file":
return (
<FileUpload
value={item.data as string}
onChange={url => updateItemData(index, url)}
maxSize={10}
showPreview={true}
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
/>
);
case "audio":
return (
<FileUpload
value={item.data as string}
onChange={url => updateItemData(index, url)}
maxSize={50}
showPreview={true}
acceptTypes={["mp3", "wav", "aac", "m4a", "ogg"]}
/>
);
case "link": {
return (
<div>
<Input
value={item.data as string}
onChange={e => updateItemData(index, e.target.value)}
placeholder="链接URL"
style={{ marginBottom: 8 }}
/>
</div>
);
}
default:
return null;
}
};
return (
<div>
{/* 内容列表 */}
{contentItems.map((item, index) => renderContentItem(item, index))}
{/* 添加内容区域 */}
<Button
type="dashed"
block
icon={<PlusOutlined />}
onClick={handleAddItem}
>
</Button>
</div>
);
};
export default ContentManager;

View File

@@ -1,23 +1,27 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
addKeyword,
updateKeyword,
getKeywordDetails,
type KeywordAddRequest,
type KeywordUpdateRequest,
} from "./api";
} from "../../api";
const { TextArea } = Input;
const { Option } = Select;
interface EditKeywordModalProps {
interface KeywordModalProps {
visible: boolean;
keywordId: string | null;
mode: "add" | "edit";
keywordId?: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const EditKeywordModal: React.FC<EditKeywordModalProps> = ({
const KeywordModal: React.FC<KeywordModalProps> = ({
visible,
mode,
keywordId,
onCancel,
onSuccess,
@@ -47,39 +51,65 @@ const EditKeywordModal: React.FC<EditKeywordModalProps> = ({
}
};
// 当弹窗打开且有ID时,获取详情
// 当弹窗打开且为编辑模式时,获取详情
useEffect(() => {
if (visible && keywordId) {
if (visible && mode === "edit" && keywordId) {
fetchKeywordDetails(keywordId);
} else if (visible && mode === "add") {
// 添加模式时重置表单
form.resetFields();
}
}, [visible, keywordId]);
}, [visible, mode, 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();
if (mode === "add") {
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 || "添加关键词失败");
}
} else {
message.error(response?.message || "更新关键词回复失败");
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("更新关键词回复失败");
console.error(`${mode === "add" ? "添加" : "更新"}关键词失败:`, error);
message.error(`${mode === "add" ? "添加" : "更新"}关键词失败`);
} finally {
setLoading(false);
}
@@ -90,9 +120,11 @@ const EditKeywordModal: React.FC<EditKeywordModalProps> = ({
onCancel();
};
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
return (
<Modal
title="编辑关键词回复"
title={title}
open={visible}
onCancel={handleCancel}
footer={null}
@@ -195,4 +227,4 @@ const EditKeywordModal: React.FC<EditKeywordModalProps> = ({
);
};
export default EditKeywordModal;
export default KeywordModal;

View File

@@ -0,0 +1,194 @@
import React, { useState, useEffect, useCallback } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
addMaterial,
updateMaterial,
getMaterialDetails,
type MaterialAddRequest,
type MaterialUpdateRequest,
type ContentItem,
} from "../../api";
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
import ContentManager from "./ContentManager";
const { Option } = Select;
interface MaterialModalProps {
visible: boolean;
mode: "add" | "edit";
materialId?: number | null;
onCancel: () => void;
onSuccess: () => void;
}
const MaterialModal: React.FC<MaterialModalProps> = ({
visible,
mode,
materialId,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [contentItems, setContentItems] = useState<ContentItem[]>([]);
// 获取素材详情
const fetchMaterialDetails = useCallback(
async (id: number) => {
try {
const response = await getMaterialDetails(id.toString());
if (response) {
const material = response;
form.setFieldsValue({
title: material.title,
cover: material.cover ? [material.cover] : [],
status: material.status,
});
// 设置内容项
setContentItems(material.content || []);
}
} catch (error) {
console.error("获取素材详情失败:", error);
message.error("获取素材详情失败");
}
},
[form],
);
// 当弹窗打开且为编辑模式时,获取详情
useEffect(() => {
if (visible && mode === "edit" && materialId) {
fetchMaterialDetails(materialId);
} else if (visible && mode === "add") {
// 添加模式时重置表单
form.resetFields();
setContentItems([]);
}
}, [visible, mode, materialId, fetchMaterialDetails, form]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
// 验证内容项
if (contentItems.length === 0) {
message.warning("请至少添加一个内容项");
return;
}
const coverValue = Array.isArray(values.cover)
? values.cover[0] || ""
: values.cover || "";
const data: MaterialAddRequest = {
title: values.title,
status: values.status || 1,
content: contentItems,
...(coverValue && { cover: coverValue }),
};
if (mode === "add") {
const response = await addMaterial(data);
if (response) {
message.success("添加素材成功");
form.resetFields();
setContentItems([]);
onSuccess();
onCancel();
} else {
message.error(response?.message || "添加素材失败");
}
} else {
const updateData: MaterialUpdateRequest = {
...data,
id: materialId?.toString(),
};
const response = await updateMaterial(updateData);
if (response) {
message.success("更新素材成功");
form.resetFields();
setContentItems([]);
onSuccess();
onCancel();
} else {
message.error(response?.message || "更新素材失败");
}
}
} catch (error) {
console.error(`${mode === "add" ? "添加" : "更新"}素材失败:`, error);
message.error(`${mode === "add" ? "添加" : "更新"}素材失败`);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
setContentItems([]);
onCancel();
};
const title = mode === "add" ? "添加素材" : "编辑素材";
return (
<Modal
title={title}
open={visible}
onCancel={handleCancel}
footer={null}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{ status: 1 }}
>
<Form.Item
name="title"
label="素材标题"
rules={[{ required: true, message: "请输入素材标题" }]}
>
<Input placeholder="请输入素材标题" />
</Form.Item>
<Form.Item label="素材内容">
<ContentManager value={contentItems} onChange={setContentItems} />
</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 MaterialModal;

View File

@@ -1,23 +1,27 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
addSensitiveWord,
updateSensitiveWord,
getSensitiveWordDetails,
type SensitiveWordAddRequest,
type SensitiveWordUpdateRequest,
} from "./api";
} from "../../api";
const { TextArea } = Input;
const { Option } = Select;
interface EditSensitiveWordModalProps {
interface SensitiveWordModalProps {
visible: boolean;
sensitiveWordId: string | null;
mode: "add" | "edit";
sensitiveWordId?: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const EditSensitiveWordModal: React.FC<EditSensitiveWordModalProps> = ({
const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
visible,
mode,
sensitiveWordId,
onCancel,
onSuccess,
@@ -45,37 +49,61 @@ const EditSensitiveWordModal: React.FC<EditSensitiveWordModalProps> = ({
}
};
// 当弹窗打开且有ID时,获取详情
// 当弹窗打开且为编辑模式时,获取详情
useEffect(() => {
if (visible && sensitiveWordId) {
if (visible && mode === "edit" && sensitiveWordId) {
fetchSensitiveWordDetails(sensitiveWordId);
} else if (visible && mode === "add") {
// 添加模式时重置表单
form.resetFields();
}
}, [visible, sensitiveWordId]);
}, [visible, mode, 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();
if (mode === "add") {
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 || "添加敏感词失败");
}
} else {
message.error(response?.message || "更新敏感词失败");
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("更新敏感词失败");
console.error(`${mode === "add" ? "添加" : "更新"}敏感词失败:`, error);
message.error(`${mode === "add" ? "添加" : "更新"}敏感词失败`);
} finally {
setLoading(false);
}
@@ -86,9 +114,11 @@ const EditSensitiveWordModal: React.FC<EditSensitiveWordModalProps> = ({
onCancel();
};
const title = mode === "add" ? "添加敏感词" : "编辑敏感词";
return (
<Modal
title="编辑敏感词"
title={title}
open={visible}
onCancel={handleCancel}
footer={null}
@@ -164,4 +194,4 @@ const EditSensitiveWordModal: React.FC<EditSensitiveWordModalProps> = ({
);
};
export default EditSensitiveWordModal;
export default SensitiveWordModal;

View File

@@ -0,0 +1,5 @@
// 模态框组件统一导出
export { default as MaterialModal } from "./MaterialModal";
export { default as SensitiveWordModal } from "./SensitiveWordModal";
export { default as KeywordModal } from "./KeywordModal";
export { default as ContentManager } from "./ContentManager";

View File

@@ -1,14 +1,16 @@
import React, { useState } from "react";
import React, { useState, useRef } 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";
import {
MaterialManagement,
SensitiveWordManagement,
KeywordManagement,
MaterialModal,
SensitiveWordModal,
KeywordModal,
} from "./components";
const ContentManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>("material");
@@ -17,6 +19,9 @@ const ContentManagement: React.FC = () => {
useState(false);
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
// 引用素材管理组件
const materialManagementRef = useRef<any>(null);
const tabs = [
{ key: "material", label: "素材资源库" },
{ key: "sensitive", label: "敏感词管理" },
@@ -38,20 +43,22 @@ const ContentManagement: React.FC = () => {
// 弹窗成功回调
const handleModalSuccess = () => {
// 这里可以触发子组件重新获取数据
// 由于子组件是独立的,它们会在组件重新挂载时自动获取数据
// 刷新素材列表
if (materialManagementRef.current?.fetchMaterials) {
materialManagementRef.current.fetchMaterials();
}
};
const renderTabContent = () => {
switch (activeTab) {
case "material":
return <MaterialManagement />;
return <MaterialManagement ref={materialManagementRef} />;
case "sensitive":
return <SensitiveWordManagement />;
case "keyword":
return <KeywordManagement />;
default:
return <MaterialManagement />;
return <MaterialManagement ref={materialManagementRef} />;
}
};
@@ -101,20 +108,23 @@ const ContentManagement: React.FC = () => {
<div className={styles.content}>{renderTabContent()}</div>
{/* 弹窗组件 */}
<AddMaterialModal
<MaterialModal
visible={materialModalVisible}
mode="add"
onCancel={() => setMaterialModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<AddSensitiveWordModal
<SensitiveWordModal
visible={sensitiveWordModalVisible}
mode="add"
onCancel={() => setSensitiveWordModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<AddKeywordModal
<KeywordModal
visible={keywordModalVisible}
mode="add"
onCancel={() => setKeywordModalVisible(false)}
onSuccess={handleModalSuccess}
/>

View File

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