Merge branch 'yongpxu-dev' into develop

This commit is contained in:
超级老白兔
2025-09-28 16:04:44 +08:00
65 changed files with 8184 additions and 2012 deletions

View File

@@ -602,7 +602,7 @@ const ScenarioList: React.FC = () => {
<div className={style["link-label"]}></div>
<div className={style["link-input-wrapper"]}>
<Input
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`}
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`}
readOnly
className={style["link-input"]}
placeholder="小程序链接"
@@ -610,7 +610,7 @@ const ScenarioList: React.FC = () => {
<Button
size="small"
onClick={() => {
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`;
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",

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

@@ -11,6 +11,7 @@
align-items: center;
min-height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
justify-content: space-between;
.headerLeft {
display: flex;

View File

@@ -13,6 +13,7 @@ export interface PowerNavigationProps {
onBackClick?: () => void;
className?: string;
style?: React.CSSProperties;
rightContent?: React.ReactNode;
}
const PowerNavigation: React.FC<PowerNavigationProps> = ({
@@ -23,6 +24,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
onBackClick,
className,
style,
rightContent,
}) => {
const navigate = useNavigate();
@@ -57,6 +59,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
</div>
</div>
<div className={styles.headerRight}>{rightContent}</div>
</div>
);
};

View File

@@ -0,0 +1,243 @@
.videoUploadContainer {
width: 100%;
// 覆盖 antd Upload 组件的默认样式
:global {
.ant-upload {
width: 100%;
}
.ant-upload-list {
width: 100%;
}
.ant-upload-list-text {
width: 100%;
}
.ant-upload-list-text .ant-upload-list-item {
width: 100%;
}
}
.videoUploadButton {
width: 100%;
aspect-ratio: 16 / 9;
min-height: clamp(90px, 20vw, 180px);
border: 2px dashed #d9d9d9;
border-radius: 12px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
border-color: #1890ff;
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(24, 144, 255, 0.15);
}
&:active {
transform: translateY(0);
}
.uploadingContainer {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 100%;
padding: 20px;
.uploadingIcon {
font-size: clamp(24px, 4vw, 32px);
color: #1890ff;
animation: pulse 2s infinite;
}
.uploadingText {
font-size: clamp(11px, 2vw, 14px);
color: #666;
font-weight: 500;
}
.uploadProgress {
width: 100%;
max-width: 200px;
}
}
.uploadContent {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px;
text-align: center;
.uploadIcon {
font-size: clamp(50px, 6vw, 48px);
color: #1890ff;
transition: all 0.3s ease;
}
.uploadText {
.uploadTitle {
font-size: clamp(14px, 2.5vw, 16px);
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.uploadSubtitle {
font-size: clamp(10px, 1.5vw, 14px);
color: #666;
line-height: 1.4;
}
}
&:hover .uploadIcon {
transform: scale(1.1);
color: #40a9ff;
}
}
}
.videoItem {
width: 100%;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.1);
}
.videoItemContent {
display: flex;
align-items: center;
gap: 12px;
.videoIcon {
width: clamp(28px, 5vw, 40px);
height: clamp(28px, 5vw, 40px);
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: clamp(14px, 2.5vw, 18px);
flex-shrink: 0;
}
.videoInfo {
flex: 1;
min-width: 0;
.videoName {
font-size: clamp(11px, 2vw, 14px);
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.videoSize {
font-size: clamp(10px, 1.5vw, 12px);
color: #666;
}
}
.videoActions {
display: flex;
gap: 4px;
flex-shrink: 0;
.previewBtn,
.deleteBtn {
padding: 4px 8px;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background: #f5f5f5;
}
}
.previewBtn {
color: #1890ff;
&:hover {
color: #40a9ff;
background: #e6f7ff;
}
}
.deleteBtn {
color: #ff4d4f;
&:hover {
color: #ff7875;
background: #fff2f0;
}
}
}
}
.itemProgress {
margin-top: 8px;
}
}
.videoPreview {
display: flex;
justify-content: center;
align-items: center;
background: #000;
border-radius: 8px;
overflow: hidden;
video {
border-radius: 8px;
}
}
}
// 禁用状态
.videoUploadContainer.disabled {
opacity: 0.6;
pointer-events: none;
}
// 错误状态
.videoUploadContainer.error {
.videoUploadButton {
border-color: #ff4d4f;
background: #fff2f0;
}
}
// 动画效果
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}

View File

@@ -0,0 +1,385 @@
import React, { useState } from "react";
import { Upload, message, Progress, Button, Modal } from "antd";
import {
LoadingOutlined,
SoundOutlined,
DeleteOutlined,
EyeOutlined,
FileOutlined,
CloudUploadOutlined,
} from "@ant-design/icons";
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
import style from "./index.module.scss";
interface AudioUploadProps {
value?: string | string[]; // 支持单个字符串或字符串数组
onChange?: (url: string | string[]) => void; // 支持单个字符串或字符串数组
disabled?: boolean;
className?: string;
maxSize?: number; // 最大文件大小(MB)
showPreview?: boolean; // 是否显示预览
maxCount?: number; // 最大上传数量默认为1
}
const AudioUpload: React.FC<AudioUploadProps> = ({
value = "",
onChange,
disabled = false,
className,
maxSize = 50,
showPreview = true,
maxCount = 1,
}) => {
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploadProgress, setUploadProgress] = useState(0);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewUrl, setPreviewUrl] = useState("");
React.useEffect(() => {
if (value) {
// 处理单个字符串或字符串数组
const urls = Array.isArray(value) ? value : [value];
const files: UploadFile[] = urls.map((url, index) => ({
uid: `file-${index}`,
name: `audio-${index + 1}`,
status: "done",
url: url || "",
}));
setFileList(files);
} else {
setFileList([]);
}
}, [value]);
// 文件验证
const beforeUpload = (file: File) => {
const isAudio = file.type.startsWith("audio/");
if (!isAudio) {
message.error("只能上传音频文件!");
return false;
}
const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
if (!isLtMaxSize) {
message.error(`音频大小不能超过${maxSize}MB`);
return false;
}
return true;
};
// 处理文件变化
const handleChange: UploadProps["onChange"] = info => {
// 更新 fileList确保所有 URL 都是字符串
const updatedFileList = info.fileList.map(file => {
let url = "";
if (file.url) {
url = file.url;
} else if (file.response) {
// 处理响应对象
if (typeof file.response === "string") {
url = file.response;
} else if (file.response.data) {
url =
typeof file.response.data === "string"
? file.response.data
: file.response.data.url || "";
} else if (file.response.url) {
url = file.response.url;
}
}
return {
...file,
url: url,
};
});
setFileList(updatedFileList);
// 处理上传状态
if (info.file.status === "uploading") {
setLoading(true);
// 模拟上传进度
const progress = Math.min(99, Math.random() * 100);
setUploadProgress(progress);
} else if (info.file.status === "done") {
setLoading(false);
setUploadProgress(100);
// 从响应中获取上传后的URL
let uploadedUrl = "";
if (info.file.response) {
// 检查响应是否成功
if (info.file.response.code && info.file.response.code !== 200) {
message.error(info.file.response.message || "上传失败");
return;
}
if (typeof info.file.response === "string") {
uploadedUrl = info.file.response;
} else if (info.file.response.data) {
uploadedUrl =
typeof info.file.response.data === "string"
? info.file.response.data
: info.file.response.data.url || "";
} else if (info.file.response.url) {
uploadedUrl = info.file.response.url;
}
}
if (uploadedUrl) {
message.success("音频上传成功!");
if (maxCount === 1) {
// 单个音频模式
onChange?.(uploadedUrl);
} else {
// 多个音频模式
const currentUrls = Array.isArray(value)
? value
: value
? [value]
: [];
const newUrls = [...currentUrls, uploadedUrl];
onChange?.(newUrls);
}
} else {
message.error("上传失败未获取到音频URL");
}
} else if (info.file.status === "error") {
setLoading(false);
setUploadProgress(0);
message.error("上传失败,请重试");
} else if (info.file.status === "removed") {
if (maxCount === 1) {
onChange?.("");
} else {
// 多个音频模式,移除对应的音频
const currentUrls = Array.isArray(value) ? value : value ? [value] : [];
const removedIndex = info.fileList.findIndex(
f => f.uid === info.file.uid,
);
if (removedIndex !== -1) {
const newUrls = currentUrls.filter(
(_, index) => index !== removedIndex,
);
onChange?.(newUrls);
}
}
}
};
// 删除文件
const handleRemove = (file?: UploadFile) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个音频文件吗?",
okText: "确定",
cancelText: "取消",
onOk: () => {
if (maxCount === 1) {
setFileList([]);
onChange?.("");
} else if (file) {
// 多个音频模式,删除指定音频
const currentUrls = Array.isArray(value)
? value
: value
? [value]
: [];
const fileIndex = fileList.findIndex(f => f.uid === file.uid);
if (fileIndex !== -1) {
const newUrls = currentUrls.filter(
(_, index) => index !== fileIndex,
);
onChange?.(newUrls);
}
}
message.success("音频已删除");
},
});
return true;
};
// 预览音频
const handlePreview = (url: string) => {
setPreviewUrl(url);
setPreviewVisible(true);
};
// 获取文件大小显示
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
// 自定义上传按钮
const uploadButton = (
<div className={style.videoUploadButton}>
{loading ? (
<div className={style.uploadingContainer}>
<div className={style.uploadingIcon}>
<LoadingOutlined spin />
</div>
<div className={style.uploadingText}>...</div>
<Progress
percent={uploadProgress}
size="small"
showInfo={false}
strokeColor="#1890ff"
className={style.uploadProgress}
/>
</div>
) : (
<div className={style.uploadContent}>
<div className={style.uploadIcon}>
<CloudUploadOutlined />
</div>
<div className={style.uploadText}>
<div className={style.uploadTitle}>
{maxCount === 1
? "上传音频"
: `上传音频 (${fileList.length}/${maxCount})`}
</div>
<div className={style.uploadSubtitle}>
MP3WAVAAC {maxSize}MB
{maxCount > 1 && `,最多上传 ${maxCount} 个音频`}
</div>
</div>
</div>
)}
</div>
);
// 自定义文件列表项
const customItemRender = (
originNode: React.ReactElement,
file: UploadFile,
) => {
if (file.status === "uploading") {
return (
<div className={style.videoItem}>
<div className={style.videoItemContent}>
<div className={style.videoIcon}>
<SoundOutlined />
</div>
<div className={style.videoInfo}>
<div className={style.videoName}>{file.name}</div>
<div className={style.videoSize}>
{file.size ? formatFileSize(file.size) : "计算中..."}
</div>
</div>
<div className={style.videoActions}>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={() => handleRemove(file)}
className={style.deleteBtn}
/>
</div>
</div>
<Progress
percent={uploadProgress}
size="small"
strokeColor="#1890ff"
className={style.itemProgress}
/>
</div>
);
}
if (file.status === "done") {
return (
<div className={style.videoItem}>
<div className={style.videoItemContent}>
<div className={style.videoIcon}>
<SoundOutlined />
</div>
<div className={style.videoInfo}>
<div className={style.videoName}>{file.name}</div>
<div className={style.videoSize}>
{file.size ? formatFileSize(file.size) : "未知大小"}
</div>
</div>
<div className={style.videoActions}>
{showPreview && (
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={() => handlePreview(file.url || "")}
className={style.previewBtn}
/>
)}
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={() => handleRemove(file)}
className={style.deleteBtn}
/>
</div>
</div>
</div>
);
}
return originNode;
};
const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload";
return (
<div className={`${style.videoUploadContainer} ${className || ""}`}>
<Upload
name="file"
headers={{
Authorization: `Bearer ${localStorage.getItem("token")}`,
}}
action={action}
multiple={maxCount > 1}
fileList={fileList}
accept="audio/*"
listType="text"
showUploadList={{
showPreviewIcon: false,
showRemoveIcon: false,
showDownloadIcon: false,
}}
disabled={disabled || loading}
beforeUpload={beforeUpload}
onChange={handleChange}
onRemove={handleRemove}
maxCount={maxCount}
itemRender={customItemRender}
>
{fileList.length >= maxCount ? null : uploadButton}
</Upload>
{/* 音频预览模态框 */}
<Modal
title="音频预览"
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
footer={null}
width={600}
centered
>
<div className={style.videoPreview}>
<audio controls style={{ width: "100%" }} src={previewUrl}>
</audio>
</div>
</Modal>
</div>
);
};
export default AudioUpload;

View File

@@ -176,12 +176,17 @@ const FileUpload: React.FC<FileUploadProps> = ({
} else if (info.file.status === "done") {
setLoading(false);
setUploadProgress(100);
message.success("文件上传成功!");
// 从响应中获取上传后的URL
let uploadedUrl = "";
if (info.file.response) {
// 检查响应是否成功
if (info.file.response.code && info.file.response.code !== 200) {
message.error(info.file.response.message || "上传失败");
return;
}
if (typeof info.file.response === "string") {
uploadedUrl = info.file.response;
} else if (info.file.response.data) {
@@ -195,6 +200,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
}
if (uploadedUrl) {
message.success("文件上传成功!");
if (maxCount === 1) {
// 单个文件模式
onChange?.(uploadedUrl);
@@ -208,6 +214,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
const newUrls = [...currentUrls, uploadedUrl];
onChange?.(newUrls);
}
} else {
message.error("上传失败未获取到文件URL");
}
} else if (info.file.status === "error") {
setLoading(false);

View File

@@ -108,12 +108,17 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
} else if (info.file.status === "done") {
setLoading(false);
setUploadProgress(100);
message.success("视频上传成功!");
// 从响应中获取上传后的URL
let uploadedUrl = "";
if (info.file.response) {
// 检查响应是否成功
if (info.file.response.code && info.file.response.code !== 200) {
message.error(info.file.response.message || "上传失败");
return;
}
if (typeof info.file.response === "string") {
uploadedUrl = info.file.response;
} else if (info.file.response.data) {
@@ -127,6 +132,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
}
if (uploadedUrl) {
message.success("视频上传成功!");
if (maxCount === 1) {
// 单个视频模式
onChange?.(uploadedUrl);
@@ -140,6 +146,8 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
const newUrls = [...currentUrls, uploadedUrl];
onChange?.(newUrls);
}
} else {
message.error("上传失败未获取到视频URL");
}
} else if (info.file.status === "error") {
setLoading(false);

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

@@ -27,12 +27,12 @@ export function clearUnreadCount1(params) {
return request("/v1/kefu/message/readMessage", params, "GET");
}
export function clearUnreadCount2(params) {
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
return request2("/api/WechatFriend/clearUnreadCount", params, "PUT");
}
//更新配置
export function updateConfig(params) {
return request("/api/WechatFriend/updateConfig", params, "PUT");
return request2("/api/WechatFriend/updateConfig", params, "PUT");
}
//获取聊天记录-2 获取列表
export function getChatMessages(params: {
@@ -44,7 +44,7 @@ export function getChatMessages(params: {
Count: number;
olderData: boolean;
}) {
return request("/api/FriendMessage/SearchMessage", params, "GET");
return request2("/api/FriendMessage/SearchMessage", params, "GET");
}
export function getChatroomMessages(params: {
wechatAccountId: number;

View File

@@ -0,0 +1,13 @@
import request from "@/api/request";
export const getAiSettings = () => {
return request("/v1/kefu/ai/friend/get", "GET");
};
export const setAiSettings = (params: {
isUpdata: string;
packageId: string[];
type: number;
}) => {
return request("/v1/kefu/ai/friend/setAll", params, "POST");
};

View File

@@ -0,0 +1,98 @@
.container {
display: flex;
gap: 24px;
height: 100%;
.left {
flex: 1;
min-width: 0;
.tip {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 20px;
font-size: 14px;
color: #586069;
line-height: 1.5;
}
.formItem {
margin-bottom: 20px;
.label {
font-size: 14px;
font-weight: 500;
color: #24292e;
margin-bottom: 8px;
}
}
.primaryBtn {
margin-top: 20px;
height: 40px;
font-size: 14px;
font-weight: 500;
}
}
.right {
flex: 1;
min-width: 0;
:global(.ant-list-item) {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
:global(.ant-list-item-meta-title) {
font-size: 14px;
font-weight: 500;
color: #24292e;
margin-bottom: 4px;
}
:global(.ant-list-item-meta-description) {
font-size: 12px;
color: #586069;
}
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
text-align: center;
.emptyText {
font-size: 16px;
color: #999;
margin-bottom: 8px;
}
.emptyDesc {
font-size: 14px;
color: #ccc;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.container {
flex-direction: column;
gap: 16px;
.left,
.right {
flex: none;
}
}
}

View File

@@ -0,0 +1,202 @@
import React, { useState } from "react";
import { Card, Select, Button, Space, Tag, List, message, Modal } from "antd";
import { DatabaseOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import PoolSelection from "@/components/PoolSelection";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
import { setAiSettings } from "./api";
import styles from "./index.module.scss";
const { Option } = Select;
const ReceptionSettings: React.FC = () => {
const [selectedPools, setSelectedPools] = useState<PoolSelectionItem[]>([]);
const [mode, setMode] = useState<number>(0);
const [loading, setLoading] = useState(false);
const typeOptions = [
{ value: 0, label: "人工接待" },
{ value: 1, label: "AI辅助" },
{ value: 2, label: "AI接管" },
];
// 处理流量池选择
const handlePoolSelect = (pools: PoolSelectionItem[]) => {
setSelectedPools(pools);
};
// 处理接待模式选择
const handleModeChange = (value: number) => {
setMode(value);
};
// 处理批量设置
const handleBatchSet = async () => {
if (selectedPools.length === 0) {
message.warning("请先选择流量池");
return;
}
const selectedModeLabel =
typeOptions.find(opt => opt.value === mode)?.label || "人工接待";
const poolNames = selectedPools
.map(pool => pool.name || pool.nickname)
.join("、");
Modal.confirm({
title: "确认批量设置",
icon: <ExclamationCircleOutlined />,
content: (
<div>
<p></p>
<div
style={{
margin: "12px 0",
padding: "8px 12px",
background: "#f5f5f5",
borderRadius: "4px",
}}
>
<strong></strong>
{poolNames}
</div>
<div
style={{
margin: "12px 0",
padding: "8px 12px",
background: "#f5f5f5",
borderRadius: "4px",
}}
>
<strong></strong>
{selectedModeLabel}
</div>
<p style={{ color: "#ff4d4f", marginTop: "12px" }}>
{selectedPools.length}{" "}
</p>
</div>
),
okText: "确认设置",
cancelText: "取消",
okType: "primary",
onOk: async () => {
setLoading(true);
try {
const packageIds = selectedPools.map(pool => pool.id);
const params = {
isUpdata: "1", // 1表示更新0表示新增
packageId: packageIds,
type: mode,
};
const response = await setAiSettings(params);
if (response) {
message.success(
`成功为 ${selectedPools.length} 个流量池设置接待模式为"${selectedModeLabel}"`,
);
// 可以在这里刷新流量池状态列表
}
} catch (error) {
console.error("批量设置失败:", error);
message.error("批量设置失败,请重试");
} finally {
setLoading(false);
}
},
});
};
return (
<div className={styles.container}>
<div className={styles.left}>
<Card
title={
<Space size={8}>
<DatabaseOutlined />
<span></span>
</Space>
}
>
<div className={styles.tip}>
</div>
<div className={styles.formItem}>
<div className={styles.label}></div>
<PoolSelection
selectedOptions={selectedPools}
onSelect={handlePoolSelect}
placeholder="请选择流量池"
showSelectedList={true}
selectedListMaxHeight={200}
/>
</div>
<div className={styles.formItem}>
<div className={styles.label}></div>
<Select
value={mode}
onChange={handleModeChange}
style={{ width: "100%" }}
>
{typeOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</div>
<Button
type="primary"
block
className={styles.primaryBtn}
onClick={handleBatchSet}
loading={loading}
>
</Button>
</Card>
</div>
<div className={styles.right}>
<Card
title={
<Space size={8}>
<DatabaseOutlined />
<span></span>
</Space>
}
>
{selectedPools.length > 0 ? (
<List
itemLayout="horizontal"
dataSource={selectedPools}
renderItem={item => (
<List.Item>
<List.Item.Meta
title={item.name || item.nickname}
description={`${item.num || 0} 个客户`}
/>
<Tag color="blue">
{typeOptions.find(opt => opt.value === mode)?.label ||
"人工接待"}
</Tag>
</List.Item>
)}
/>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div>
)}
</Card>
</div>
</div>
);
};
export default ReceptionSettings;

View File

@@ -0,0 +1,78 @@
/* common-config page styles */
.content {
flex: 1;
overflow: hidden;
padding: 16px;
}
.tabsBar {
display: flex;
gap: 0;
border-bottom: 1px solid #e8e8e8;
margin-bottom: 24px;
padding: 0 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;
}
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
font-size: 16px;
color: #999;
background: #fafafa;
border-radius: 8px;
border: 1px dashed #d9d9d9;
}
.left,
.right {
min-height: 420px;
}
.tip {
color: #98a2b3;
margin-bottom: 16px;
}
.formItem {
margin-bottom: 16px;
}
.label {
margin-bottom: 8px;
color: #344054;
}
.primaryBtn {
height: 40px;
border-radius: 8px;
}
.actions {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #eee;
}

View File

@@ -0,0 +1,70 @@
import React, { useState } from "react";
import Layout from "@/components/Layout/LayoutFiexd";
import PowerNavigation from "@/components/PowerNavtion";
import { Button, Space } from "antd";
import ReceptionSettings from "./components/ReceptionSettings";
import styles from "./index.module.scss";
const CommonConfig: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>("reception");
const tabs = [
{ key: "reception", label: "接待设置" },
{ key: "notification", label: "通知设置" },
{ key: "system", label: "系统设置" },
{ key: "security", label: "安全设置" },
{ key: "advanced", label: "高级设置" },
];
const handleTabClick = (tabKey: string) => {
setActiveTab(tabKey);
};
const renderTabContent = () => {
switch (activeTab) {
case "reception":
return <ReceptionSettings />;
case "notification":
return <div className={styles.placeholder}>...</div>;
case "system":
return <div className={styles.placeholder}>...</div>;
case "security":
return <div className={styles.placeholder}>...</div>;
case "advanced":
return <div className={styles.placeholder}>...</div>;
default:
return <ReceptionSettings />;
}
};
return (
<Layout
header={
<>
<PowerNavigation
title="全局配置"
subtitle="系统全局设置和配置管理"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.tabsBar}>
{tabs.map(tab => (
<div
key={tab.key}
className={`${styles.tab} ${
activeTab === tab.key ? styles.tabActive : ""
}`}
onClick={() => handleTabClick(tab.key)}
>
{tab.label}
</div>
))}
</div>
</>
}
>
<div className={styles.content}>{renderTabContent()}</div>
</Layout>
);
};
export default CommonConfig;

View File

@@ -79,6 +79,7 @@
display: flex;
align-items: center;
padding: 8px 0;
gap: 26px;
.suanli {
display: flex;
align-items: center;
@@ -131,10 +132,20 @@
.userSection {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
padding: 8px 16px;
.userInfo2 {
line-height: 1;
padding-top: 5px;
font-size: 14px;
.userNickname {
margin-bottom: 4px;
}
.userAccount {
font-size: 12px;
color: #888;
}
}
}
.userNickname {

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { Layout, Drawer, Avatar, Space, Button, Badge, Dropdown } from "antd";
import {
MenuOutlined,
BarChartOutlined,
UserOutlined,
BellOutlined,
LogoutOutlined,
@@ -90,24 +90,20 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
<>
<Header className={styles.header}>
<div className={styles.headerLeft}>
{!isWeChat() ? (
<Button
type="text"
size="large"
icon={<WechatOutlined />}
onClick={handleMenuClick}
className={styles.menuButton}
/>
) : (
<Button
type="text"
size="large"
icon={<MenuOutlined />}
onClick={handleMenuClick}
className={styles.menuButton}
/>
)}
<Button
icon={<BarChartOutlined />}
type={!isWeChat() ? "primary" : "default"}
onClick={handleMenuClick}
>
</Button>
<Button
icon={<WechatOutlined />}
type={isWeChat() ? "primary" : "default"}
onClick={handleMenuClick}
>
Ai智能客服
</Button>
<span className={styles.title}>{title}</span>
</div>
@@ -124,9 +120,14 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
<BellOutlined style={{ fontSize: 20 }} />
</Badge>
</div>
<div className={styles.messageButton}>
<SettingOutlined style={{ fontSize: 20 }} />
</div>
<Button
onClick={() => {
navigate("/pc/commonConfig");
}}
icon={<SettingOutlined />}
>
</Button>
<Dropdown
menu={{ items: userMenuItems }}
placement="bottomRight"
@@ -139,6 +140,11 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
src={user?.avatar}
className={styles.avatar}
/>
<div className={styles.userInfo2}>
<div className={styles.userNickname}>{user.username}</div>
<div className={styles.userAccount}></div>
</div>
</div>
</Dropdown>
</Space>

View File

@@ -4,11 +4,7 @@ import { Outlet } from "react-router-dom";
import NavCommon from "./components/NavCommon";
const CkboxPage: React.FC = () => {
return (
<Layout
header={
<NavCommon title="AI自动聊天懂业务会引导客户不停地聊不停" />
}
>
<Layout header={<NavCommon title="触客宝" />}>
<Outlet />
</Layout>
);

View File

@@ -3,41 +3,426 @@
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 100vh;
}
// 头部样式
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
margin-bottom: 24px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
.headerLeft {
.computeBalance {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
.anticon {
color: #1890ff;
}
}
}
}
.headerRight {
.startTrainingButton {
height: 40px;
padding: 0 24px;
font-weight: 500;
}
}
}
// 内容区域
.content {
.tabs {
.ant-tabs-nav {
margin-bottom: 24px;
}
.ant-tabs-tab {
font-size: 16px;
font-weight: 500;
}
}
}
// 话术投喂样式
.utteranceFeeding {
display: flex;
gap: 24px;
min-height: 600px;
.leftPanel {
flex: 1;
max-width: 400px;
.addCard {
height: fit-content;
.cardHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
.icon {
font-size: 20px;
color: #1890ff;
}
.title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
}
.description {
color: #8c8c8c;
margin-bottom: 24px;
font-size: 14px;
}
.form {
.formItem {
margin-bottom: 20px;
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #262626;
}
.ant-input,
.ant-input-affix-wrapper {
border-radius: 6px;
}
.ant-input {
height: 40px;
}
}
.saveButton {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 6px;
}
}
}
}
.rightPanel {
flex: 2;
.libraryCard {
.cardHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #262626;
.icon {
font-size: 20px;
color: #1890ff;
}
}
.count {
color: #8c8c8c;
font-size: 14px;
}
}
.utteranceList {
.utteranceItem {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
background: #fafafa;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
.utteranceHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.utteranceTitle {
font-size: 16px;
font-weight: 600;
color: #262626;
}
}
.utteranceCategory {
margin-bottom: 12px;
}
.utteranceContent {
color: #595959;
line-height: 1.6;
margin-bottom: 16px;
font-size: 14px;
}
.utteranceFooter {
display: flex;
justify-content: space-between;
align-items: center;
.timestamps {
display: flex;
gap: 16px;
font-size: 12px;
color: #8c8c8c;
}
.actions {
display: flex;
gap: 8px;
.ant-btn {
border: none;
box-shadow: none;
&:hover {
background: #f0f0f0;
}
}
}
}
}
}
}
}
}
// 模型管理样式
.modelManagement {
.cardHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
.icon {
font-size: 20px;
color: #1890ff;
}
.title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
}
.modelList {
.modelItem {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
background: #fafafa;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
.modelInfo {
margin-bottom: 16px;
.modelName {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.name {
font-size: 16px;
font-weight: 600;
color: #262626;
}
}
.modelStatus {
display: flex;
align-items: center;
gap: 16px;
.accuracy {
font-size: 14px;
color: #595959;
}
}
}
.modelActions {
display: flex;
gap: 12px;
margin-bottom: 12px;
.ant-btn {
border-radius: 6px;
}
}
.modelTimestamps {
display: flex;
gap: 16px;
font-size: 12px;
color: #8c8c8c;
}
}
}
}
// 训练分析样式
.trainingAnalysis {
.analysisCards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
.analysisCard {
.cardTitle {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 16px;
}
.cardContent {
.statItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.statLabel {
color: #8c8c8c;
font-size: 14px;
}
.statValue {
font-weight: 600;
color: #262626;
font-size: 16px;
}
}
}
}
}
.chartCard {
.cardTitle {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 16px;
}
.chartPlaceholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
.chartIcon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
p {
color: #8c8c8c;
font-size: 16px;
margin: 0;
}
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.utteranceFeeding {
flex-direction: column;
.leftPanel {
max-width: none;
}
}
.trainingAnalysis {
.analysisCards {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 768px) {
.container {
padding: 16px;
}
.header {
flex-direction: column;
gap: 16px;
align-items: stretch;
.headerLeft,
.headerRight {
text-align: center;
}
}
.utteranceFeeding {
.leftPanel,
.rightPanel {
.addCard,
.libraryCard {
.cardHeader {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
}
}
}

View File

@@ -1,21 +1,465 @@
import React from "react";
import React, { useState } from "react";
import { Button, Input, Tabs, Card, Tag, message, Modal, Tooltip } from "antd";
import {
PlusOutlined,
PlayCircleOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
ThunderboltOutlined,
FileTextOutlined,
DatabaseOutlined,
BarChartOutlined,
SaveOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const { TextArea } = Input;
const { TabPane } = Tabs;
// 话术数据类型
interface UtteranceData {
id: string;
title: string;
category: string;
content: string;
status: "active" | "pending";
createTime: string;
updateTime: string;
}
// 模型数据类型
interface ModelData {
id: string;
name: string;
version: string;
status: "training" | "completed" | "failed";
accuracy: number;
createTime: string;
lastTraining: string;
}
// 训练分析数据类型
interface TrainingAnalysis {
totalUtterances: number;
activeUtterances: number;
pendingUtterances: number;
trainingAccuracy: number;
lastTrainingTime: string;
nextTrainingTime: string;
}
const AiTraining: React.FC = () => {
const [activeTab, setActiveTab] = useState("utterance");
const [utteranceForm, setUtteranceForm] = useState({
title: "",
category: "",
content: "",
});
const [utterances, setUtterances] = useState<UtteranceData[]>([
{
id: "1",
title: "产品介绍话术",
category: "产品介绍",
content:
"我们的AI营销系统具有智能客服、精准营销、自动化运营等核心功能能够帮助您提升客户满意度和业务效率...",
status: "active",
createTime: "2024/3/1",
updateTime: "2024/3/5",
},
{
id: "2",
title: "价格咨询回复",
category: "价格咨询",
content:
"关于价格方面,我们提供多种套餐选择,可以根据您的具体需求定制。基础版适合小型企业,专业版适合中型企业...",
status: "active",
createTime: "2024/3/2",
updateTime: "2024/3/4",
},
{
id: "3",
title: "技术支持话术",
category: "技术支持",
content:
"我们提供7x24小时技术支持包括在线客服、电话支持、远程协助等多种方式确保您的问题得到及时解决...",
status: "pending",
createTime: "2024/3/3",
updateTime: "2024/3/3",
},
]);
const [models] = useState<ModelData[]>([
{
id: "1",
name: "智能客服模型",
version: "v2.1.0",
status: "completed",
accuracy: 94.5,
createTime: "2024/2/15",
lastTraining: "2024/3/5",
},
{
id: "2",
name: "营销推荐模型",
version: "v1.8.0",
status: "training",
accuracy: 89.2,
createTime: "2024/2/20",
lastTraining: "2024/3/4",
},
]);
const [trainingAnalysis] = useState<TrainingAnalysis>({
totalUtterances: 3,
activeUtterances: 2,
pendingUtterances: 1,
trainingAccuracy: 94.5,
lastTrainingTime: "2024/3/5 14:30:00",
nextTrainingTime: "2024/3/6 09:00:00",
});
// 保存话术
const handleSaveUtterance = () => {
if (
!utteranceForm.title ||
!utteranceForm.category ||
!utteranceForm.content
) {
message.warning("请填写完整的话术信息");
return;
}
const newUtterance: UtteranceData = {
id: Date.now().toString(),
title: utteranceForm.title,
category: utteranceForm.category,
content: utteranceForm.content,
status: "pending",
createTime: new Date().toLocaleDateString("zh-CN"),
updateTime: new Date().toLocaleDateString("zh-CN"),
};
setUtterances([...utterances, newUtterance]);
setUtteranceForm({ title: "", category: "", content: "" });
message.success("话术保存成功");
};
// 删除话术
const handleDeleteUtterance = (id: string) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这条话术吗?",
onOk: () => {
setUtterances(utterances.filter(item => item.id !== id));
message.success("删除成功");
},
});
};
// 开始训练
const handleStartTraining = () => {
Modal.confirm({
title: "开始训练",
content: "确定要开始AI模型训练吗训练过程可能需要几分钟时间。",
onOk: () => {
message.success("训练已开始,请稍候...");
// 这里可以添加实际的训练逻辑
},
});
};
// 话术投喂组件
const UtteranceFeeding = () => (
<div className={styles.utteranceFeeding}>
<div className={styles.leftPanel}>
<Card className={styles.addCard}>
<div className={styles.cardHeader}>
<PlusOutlined className={styles.icon} />
<span className={styles.title}></span>
</div>
<p className={styles.description}>AI模型</p>
<div className={styles.form}>
<div className={styles.formItem}>
<label></label>
<Input
placeholder="输入话术标题..."
value={utteranceForm.title}
onChange={e =>
setUtteranceForm({ ...utteranceForm, title: e.target.value })
}
/>
</div>
<div className={styles.formItem}>
<label></label>
<Input
placeholder="如:产品介绍、价格咨询等"
value={utteranceForm.category}
onChange={e =>
setUtteranceForm({
...utteranceForm,
category: e.target.value,
})
}
/>
</div>
<div className={styles.formItem}>
<label></label>
<TextArea
placeholder="输入详细的话术内容..."
rows={6}
value={utteranceForm.content}
onChange={e =>
setUtteranceForm({
...utteranceForm,
content: e.target.value,
})
}
/>
</div>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSaveUtterance}
className={styles.saveButton}
>
</Button>
</div>
</Card>
</div>
<div className={styles.rightPanel}>
<Card className={styles.libraryCard}>
<div className={styles.cardHeader}>
<DatabaseOutlined className={styles.icon} />
<span className={styles.title}></span>
<span className={styles.count}>{utterances.length}</span>
</div>
<div className={styles.utteranceList}>
{utterances.map(utterance => (
<div key={utterance.id} className={styles.utteranceItem}>
<div className={styles.utteranceHeader}>
<span className={styles.utteranceTitle}>
{utterance.title}
</span>
<Tag color={utterance.status === "active" ? "green" : "blue"}>
{utterance.status === "active" ? "已激活" : "待处理"}
</Tag>
</div>
<div className={styles.utteranceCategory}>
<Tag color="blue">{utterance.category}</Tag>
</div>
<div className={styles.utteranceContent}>
{utterance.content}
</div>
<div className={styles.utteranceFooter}>
<div className={styles.timestamps}>
<span>: {utterance.createTime}</span>
<span>: {utterance.updateTime}</span>
</div>
<div className={styles.actions}>
<Tooltip title="查看">
<Button type="text" icon={<EyeOutlined />} size="small" />
</Tooltip>
<Tooltip title="编辑">
<Button
type="text"
icon={<EditOutlined />}
size="small"
/>
</Tooltip>
<Tooltip title="删除">
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
onClick={() => handleDeleteUtterance(utterance.id)}
/>
</Tooltip>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
</div>
);
// 模型管理组件
const ModelManagement = () => (
<div className={styles.modelManagement}>
<Card>
<div className={styles.cardHeader}>
<FileTextOutlined className={styles.icon} />
<span className={styles.title}></span>
</div>
<div className={styles.modelList}>
{models.map(model => (
<div key={model.id} className={styles.modelItem}>
<div className={styles.modelInfo}>
<div className={styles.modelName}>
<span className={styles.name}>{model.name}</span>
<Tag color="blue">{model.version}</Tag>
</div>
<div className={styles.modelStatus}>
<Tag
color={
model.status === "completed"
? "green"
: model.status === "training"
? "orange"
: "red"
}
>
{model.status === "completed"
? "已完成"
: model.status === "training"
? "训练中"
: "失败"}
</Tag>
<span className={styles.accuracy}>
: {model.accuracy}%
</span>
</div>
</div>
<div className={styles.modelActions}>
<Button type="primary" size="small">
</Button>
<Button size="small"></Button>
<Button size="small"></Button>
</div>
<div className={styles.modelTimestamps}>
<span>: {model.createTime}</span>
<span>: {model.lastTraining}</span>
</div>
</div>
))}
</div>
</Card>
</div>
);
// 训练分析组件
const TrainingAnalysis = () => (
<div className={styles.trainingAnalysis}>
<div className={styles.analysisCards}>
<Card className={styles.analysisCard}>
<div className={styles.cardTitle}></div>
<div className={styles.cardContent}>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.totalUtterances}
</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.activeUtterances}
</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.pendingUtterances}
</span>
</div>
</div>
</Card>
<Card className={styles.analysisCard}>
<div className={styles.cardTitle}></div>
<div className={styles.cardContent}>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.trainingAccuracy}%
</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.lastTrainingTime}
</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}></span>
<span className={styles.statValue}>
{trainingAnalysis.nextTrainingTime}
</span>
</div>
</div>
</Card>
</div>
<Card className={styles.chartCard}>
<div className={styles.cardTitle}></div>
<div className={styles.chartPlaceholder}>
<BarChartOutlined className={styles.chartIcon} />
<p></p>
</div>
</Card>
</div>
);
return (
<div className={styles.container}>
<PowerNavigation
title="AI模型训练"
subtitle="自定义AI模型训练打造专属智能客服助手"
subtitle="训练和优化AI模型提升智能服务质量"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>AI模型训练功能正在开发中...</p>
<div className={styles.header}>
<div className={styles.headerLeft}>
<div className={styles.computeBalance}>
<ThunderboltOutlined />
<span>算力余额: 9307.423</span>
</div>
</div>
<div className={styles.headerRight}>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={handleStartTraining}
className={styles.startTrainingButton}
>
</Button>
</div>
</div>
<div className={styles.content}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
className={styles.tabs}
>
<TabPane tab="话术投喂" key="utterance">
<UtteranceFeeding />
</TabPane>
<TabPane tab="模型管理" key="model">
<ModelManagement />
</TabPane>
<TabPane tab="训练分析" key="analysis">
<TrainingAnalysis />
</TabPane>
</Tabs>
</div>
</div>
);

View File

@@ -3,41 +3,268 @@
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 100vh;
}
// 头部区域样式
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
.headerLeft {
display: flex;
align-items: center;
gap: 16px;
.backButton {
color: #666;
font-size: 14px;
&:hover {
color: #1890ff;
}
}
.titleSection {
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 4px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
}
.headerRight {
display: flex;
align-items: center;
gap: 16px;
.activeRules {
font-size: 14px;
color: #666;
}
}
}
// 子导航栏样式
.subNav {
margin-bottom: 24px;
:global(.ant-tabs-nav) {
margin-bottom: 0;
}
:global(.ant-tabs-tab) {
font-size: 16px;
font-weight: 500;
}
}
// 主要内容区域
.mainContent {
min-height: 600px;
}
// 问候规则内容
.rulesContent {
display: flex;
gap: 24px;
min-height: 600px;
.leftPanel {
flex: 0 0 400px;
.createCard {
height: fit-content;
:global(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:global(.ant-card-head-title) {
font-size: 16px;
font-weight: 600;
color: #262626;
}
.cardSubtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0 0 20px 0;
}
.createForm {
:global(.ant-form-item) {
margin-bottom: 20px;
}
:global(.ant-form-item-label) {
font-weight: 500;
}
}
}
}
.rightPanel {
flex: 1;
.listCard {
height: fit-content;
:global(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:global(.ant-card-head-title) {
font-size: 16px;
font-weight: 600;
color: #262626;
}
.listHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-size: 14px;
color: #666;
}
.ruleList {
.ruleItem {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
&:last-child {
margin-bottom: 0;
}
.ruleHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.ruleTitle {
flex: 1;
h4 {
font-size: 16px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
.ruleTags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
.ruleActions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
:global(.ant-btn) {
padding: 4px 8px;
height: auto;
}
}
}
.ruleContent {
.ruleDescription {
font-size: 14px;
color: #666;
margin: 0 0 8px 0;
font-weight: 500;
}
.ruleText {
font-size: 14px;
color: #262626;
line-height: 1.6;
margin: 0 0 12px 0;
background: #fff;
padding: 12px;
border-radius: 6px;
border: 1px solid #e8e8e8;
}
.ruleFooter {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #8c8c8c;
}
}
}
}
}
}
}
// 其他标签页内容
.templatesContent,
.statisticsContent {
padding: 24px;
text-align: center;
color: #8c8c8c;
}
// 响应式设计
@media (max-width: 1200px) {
.rulesContent {
flex-direction: column;
.leftPanel {
flex: none;
}
}
}
@media (max-width: 768px) {
.container {
padding: 16px;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.headerRight {
width: 100%;
justify-content: space-between;
}
}
.rulesContent {
.leftPanel,
.rightPanel {
.createCard,
.listCard {
:global(.ant-card-body) {
padding: 16px;
}
}
}
}
}

View File

@@ -1,21 +1,344 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import React, { useState } from "react";
import {
Button,
Card,
Form,
Input,
Select,
Switch,
Tabs,
Tag,
Space,
Popconfirm,
message,
} from "antd";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ArrowLeftOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import styles from "./index.module.scss";
const { TextArea } = Input;
const { Option } = Select;
// 问候规则数据类型
interface GreetingRule {
id: string;
name: string;
triggerType: string;
triggerCondition: string;
content: string;
priority: number;
isActive: boolean;
usageCount: number;
createTime: string;
tags: string[];
}
const AutoGreeting: React.FC = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState("rules");
const [rules, setRules] = useState<GreetingRule[]>([
{
id: "1",
name: "产品咨询自动回复",
triggerType: "关键词",
triggerCondition: "包含:产品、价格、功能",
content:
"感谢您对我们产品的关注!我们的AI营销系统具有智能客服、精准营销、自动化运营等核心功能。详细资料我稍后发送给您,请稍等。",
priority: 3,
isActive: false,
usageCount: 234,
createTime: "2024/3/3",
tags: ["关键词", "优先级3"],
},
{
id: "2",
name: "新好友欢迎",
triggerType: "新好友",
triggerCondition: "添加好友后",
content:
"您好!欢迎添加我为好友,我是触客宝AI助手,很高兴为您服务!如有任何问题,随时可以咨询我。",
priority: 1,
isActive: true,
usageCount: 156,
createTime: "2024/3/1",
tags: ["新好友", "优先级1"],
},
{
id: "3",
name: "工作时间问候",
triggerType: "时间触发",
triggerCondition: "工作日 9:00-18:00",
content: "您好!现在是工作时间,我是触客宝AI助手,很高兴为您服务!",
priority: 2,
isActive: true,
usageCount: 89,
createTime: "2024/2/28",
tags: ["时间触发", "优先级2"],
},
]);
// 计算活跃规则数量
const activeRulesCount = rules.filter(rule => rule.isActive).length;
// 处理表单提交
const handleSubmit = (values: any) => {
const newRule: GreetingRule = {
id: Date.now().toString(),
name: values.name,
triggerType: values.triggerType,
triggerCondition: values.triggerCondition,
content: values.content,
priority: values.priority,
isActive: true,
usageCount: 0,
createTime: new Date().toLocaleDateString(),
tags: [values.triggerType, `优先级${values.priority}`],
};
setRules([...rules, newRule]);
form.resetFields();
message.success("规则创建成功!");
};
// 切换规则状态
const toggleRuleStatus = (id: string) => {
setRules(
rules.map(rule =>
rule.id === id ? { ...rule, isActive: !rule.isActive } : rule,
),
);
};
// 删除规则
const deleteRule = (id: string) => {
setRules(rules.filter(rule => rule.id !== id));
message.success("规则删除成功!");
};
// 编辑规则
const editRule = (rule: GreetingRule) => {
form.setFieldsValue({
name: rule.name,
triggerType: rule.triggerType,
triggerCondition: rule.triggerCondition,
content: rule.content,
priority: rule.priority,
});
message.info("规则已加载到编辑表单");
};
return (
<div className={styles.container}>
<PowerNavigation
title="自动打招呼"
subtitle="智能识别新好友,自动发送个性化欢迎消息"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
{/* 头部区域 */}
<div className={styles.header}>
<div className={styles.headerLeft}>
<Button
type="text"
size="large"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
className={styles.backButton}
>
</Button>
<div className={styles.titleSection}>
<h1></h1>
<p>,</p>
</div>
</div>
<div className={styles.headerRight}>
<div className={styles.activeRules}>
: {activeRulesCount}/{rules.length}
</div>
<Button type="primary" icon={<PlusOutlined />}>
+
</Button>
</div>
</div>
{/* 子导航栏 */}
<div className={styles.subNav}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: "rules",
label: "问候规则",
},
{
key: "templates",
label: "话术模板",
},
{
key: "statistics",
label: "使用统计",
},
]}
/>
</div>
{/* 主要内容区域 */}
<div className={styles.mainContent}>
{activeTab === "rules" && (
<div className={styles.rulesContent}>
{/* 左侧创建规则表单 */}
<div className={styles.leftPanel}>
<Card title="+ 创建问候规则" className={styles.createCard}>
<p className={styles.cardSubtitle}>
</p>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
className={styles.createForm}
>
<Form.Item
name="name"
label="规则名称"
rules={[{ required: true, message: "请输入规则名称" }]}
>
<Input placeholder="输入规则名称..." />
</Form.Item>
<Form.Item
name="triggerType"
label="触发条件"
rules={[{ required: true, message: "请选择触发条件" }]}
>
<Select placeholder="选择触发条件">
<Option value="新好友添加"></Option>
<Option value="关键词"></Option>
<Option value="时间触发"></Option>
<Option value="群聊"></Option>
</Select>
</Form.Item>
<Form.Item
name="triggerCondition"
label="具体条件"
rules={[{ required: true, message: "请输入具体条件" }]}
>
<Input placeholder="如:工作日 9:00-18:00 或包含关键词" />
</Form.Item>
<Form.Item
name="content"
label="问候内容"
rules={[{ required: true, message: "请输入问候内容" }]}
>
<TextArea rows={4} placeholder="输入自动问候的内容..." />
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: "请选择优先级" }]}
>
<Select placeholder="选择优先级">
<Option value={1}></Option>
<Option value={2}></Option>
<Option value={3}></Option>
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
</Form.Item>
</Form>
</Card>
</div>
{/* 右侧规则列表 */}
<div className={styles.rightPanel}>
<Card title="问候规则列表" className={styles.listCard}>
<div className={styles.listHeader}>
<span>{rules.length}</span>
</div>
<div className={styles.ruleList}>
{rules.map(rule => (
<div key={rule.id} className={styles.ruleItem}>
<div className={styles.ruleHeader}>
<div className={styles.ruleTitle}>
<h4>{rule.name}</h4>
<div className={styles.ruleTags}>
{rule.tags.map((tag, index) => (
<Tag
key={index}
color={index === 0 ? "blue" : "default"}
>
{tag}
</Tag>
))}
</div>
</div>
<div className={styles.ruleActions}>
<Switch
checked={rule.isActive}
onChange={() => toggleRuleStatus(rule.id)}
size="small"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => editRule(rule)}
/>
<Popconfirm
title="确定要删除这个规则吗?"
onConfirm={() => deleteRule(rule.id)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
icon={<DeleteOutlined />}
danger
/>
</Popconfirm>
</div>
</div>
<div className={styles.ruleContent}>
<p className={styles.ruleDescription}>
{rule.triggerCondition}
</p>
<p className={styles.ruleText}>{rule.content}</p>
<div className={styles.ruleFooter}>
<span>使:{rule.usageCount}</span>
<span>:{rule.createTime}</span>
</div>
</div>
</div>
))}
</div>
</Card>
</div>
</div>
)}
{activeTab === "templates" && (
<div className={styles.templatesContent}>
<Card>
<p>...</p>
</Card>
</div>
)}
{activeTab === "statistics" && (
<div className={styles.statisticsContent}>
<Card>
<p>使...</p>
</Card>
</div>
)}
</div>
</div>
);

View File

@@ -5,39 +5,289 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
// 导出按钮样式
.exportButton {
height: 36px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.content {
min-height: 400px;
}
.placeholder {
// 顶部搜索和筛选区域
.headerSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.searchBar {
flex: 1;
max-width: 400px;
}
.filterButtons {
display: flex;
gap: 12px;
.ant-btn {
height: 36px;
border-radius: 6px;
font-size: 14px;
}
}
// 导航栏样式
.navigationTabs {
margin-bottom: 24px;
.tabs {
.ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-tab {
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
.anticon {
margin-right: 8px;
}
}
.ant-tabs-tab-active {
color: #1890ff;
.ant-tabs-tab-btn {
color: #1890ff;
}
}
.ant-tabs-ink-bar {
background: #1890ff;
}
}
}
// 记录列表样式
.recordsList {
display: flex;
flex-direction: column;
gap: 16px;
}
.recordCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #d9d9d9;
}
.ant-card-body {
padding: 16px 20px;
}
}
.cardContent {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.cardLeft {
display: flex;
gap: 12px;
flex: 1;
}
.avatar {
width: 40px;
height: 40px;
background: #1890ff;
color: #fff;
font-size: 16px;
font-weight: 600;
flex-shrink: 0;
}
.recordInfo {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.nameAndType {
display: flex;
align-items: center;
gap: 12px;
.name {
font-size: 15px;
font-weight: 600;
color: #262626;
}
.type {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #8c8c8c;
.anticon {
font-size: 13px;
}
}
}
.statusAndTime {
display: flex;
align-items: center;
gap: 12px;
.dateTime {
font-size: 13px;
color: #8c8c8c;
}
.duration {
font-size: 13px;
color: #8c8c8c;
}
}
.directionAndSubject {
display: flex;
align-items: center;
gap: 12px;
.subject {
font-size: 13px;
color: #262626;
font-weight: 500;
}
}
.content {
font-size: 13px;
color: #595959;
line-height: 1.4;
margin: 2px 0;
}
.tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
.tag {
font-size: 11px;
background: #f5f5f5;
border: 1px solid #d9d9d9;
color: #8c8c8c;
border-radius: 3px;
padding: 1px 6px;
margin: 0;
}
}
.attachments {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 6px;
.attachmentsLabel {
font-size: 12px;
color: #8c8c8c;
font-weight: 500;
}
.attachment {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #f0f0f0;
.attachmentIcon {
font-size: 14px;
}
.attachmentName {
font-size: 12px;
color: #262626;
flex: 1;
}
.downloadIcon {
color: #8c8c8c;
cursor: pointer;
transition: color 0.3s ease;
font-size: 12px;
&:hover {
color: #1890ff;
}
}
}
}
.cardRight {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
width: 28px;
height: 28px;
.viewIcon {
font-size: 14px;
color: #8c8c8c;
margin: 0;
cursor: pointer;
transition: color 0.3s ease;
&:hover {
color: #1890ff;
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.headerSection {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.searchBar {
max-width: none;
}
.filterButtons {
justify-content: center;
}
.cardContent {
flex-direction: column;
gap: 16px;
}
.cardRight {
align-self: flex-end;
}
}

View File

@@ -1,20 +1,323 @@
import React from "react";
import React, { useState } from "react";
import { Input, Button, Tabs, Tag, Avatar, Card } from "antd";
import {
SearchOutlined,
FilterOutlined,
CalendarOutlined,
MessageOutlined,
PhoneOutlined,
VideoCameraOutlined,
MailOutlined,
EyeOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const { Search } = Input;
interface CommunicationRecord {
id: string;
avatar: string;
name: string;
type: "chat" | "call" | "video" | "email";
status: "completed" | "pending" | "cancelled";
dateTime: string;
duration?: string;
direction: "incoming" | "outgoing";
subject?: string;
content: string;
tags: string[];
attachments?: Array<{
name: string;
type: "pdf" | "xlsx" | "doc" | "other";
}>;
}
const CommunicationRecord: React.FC = () => {
const [activeTab, setActiveTab] = useState("chat");
const [searchValue, setSearchValue] = useState("");
// 模拟数据
const mockData: CommunicationRecord[] = [
{
id: "1",
avatar: "李",
name: "李先生",
type: "chat",
status: "completed",
dateTime: "2024/3/5 14:30:00",
direction: "incoming",
content: "咨询AI营销产品的详细功能和价格",
tags: ["产品咨询", "价格询问"],
},
{
id: "2",
avatar: "张",
name: "张总",
type: "call",
status: "completed",
dateTime: "2024/3/5 10:15:00",
duration: "25分钟",
direction: "outgoing",
subject: "产品演示预约",
content: "与客户确认产品演示时间,讨论具体需求",
tags: ["产品演示", "需求确认"],
},
{
id: "3",
avatar: "王",
name: "王女士",
type: "video",
status: "completed",
dateTime: "2024/3/4 16:45:00",
duration: "45分钟",
direction: "incoming",
subject: "产品功能演示",
content: "详细演示AI客服功能,客户表示很满意",
tags: ["产品演示", "功能介绍"],
attachments: [
{ name: "产品介绍.pdf", type: "pdf" },
{ name: "报价单.xlsx", type: "xlsx" },
],
},
];
const getTypeIcon = (type: string) => {
switch (type) {
case "chat":
return <MessageOutlined />;
case "call":
return <PhoneOutlined />;
case "video":
return <VideoCameraOutlined />;
case "email":
return <MailOutlined />;
default:
return <MessageOutlined />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "success";
case "pending":
return "processing";
case "cancelled":
return "error";
default:
return "default";
}
};
const getDirectionColor = (direction: string) => {
return direction === "incoming" ? "green" : "blue";
};
const getDirectionText = (direction: string) => {
return direction === "incoming" ? "来电/来信" : "去电/去信";
};
const getStatusText = (status: string) => {
switch (status) {
case "completed":
return "已完成";
case "pending":
return "进行中";
case "cancelled":
return "已取消";
default:
return "未知";
}
};
const getAttachmentIcon = (type: string) => {
switch (type) {
case "pdf":
return "📄";
case "xlsx":
return "📊";
case "doc":
return "📝";
default:
return "📎";
}
};
const filteredData = mockData.filter(
record =>
record.type === activeTab &&
(searchValue === "" ||
record.name.includes(searchValue) ||
record.content.includes(searchValue) ||
record.tags.some(tag => tag.includes(searchValue))),
);
const tabItems = [
{
key: "chat",
label: (
<span>
<MessageOutlined />
(1)
</span>
),
},
{
key: "call",
label: (
<span>
<PhoneOutlined />
(2)
</span>
),
},
{
key: "video",
label: (
<span>
<VideoCameraOutlined />
(1)
</span>
),
},
{
key: "email",
label: (
<span>
<MailOutlined />
(1)
</span>
),
},
];
// 导出记录处理函数
const handleExportRecords = () => {
console.log("导出记录功能");
// TODO: 实现导出功能
};
return (
<div className={styles.container}>
<PowerNavigation
title="沟通记录"
subtitle="完整记录客户沟通历史,支持多维度查询分析"
subtitle="查看和管理所有客户沟通记录"
showBackButton={true}
backButtonText="返回功能中心"
rightContent={
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleExportRecords}
className={styles.exportButton}
>
</Button>
}
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
{/* 顶部搜索和筛选区域 */}
<div className={styles.headerSection}>
<div className={styles.searchBar}>
<Search
placeholder="搜索客户、内容或标签..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
</div>
<div className={styles.filterButtons}>
<Button icon={<FilterOutlined />}></Button>
<Button icon={<CalendarOutlined />}></Button>
</div>
</div>
{/* 通信类型导航栏 */}
<div className={styles.navigationTabs}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
className={styles.tabs}
/>
</div>
{/* 通信记录列表 */}
<div className={styles.recordsList}>
{filteredData.map(record => (
<Card key={record.id} className={styles.recordCard}>
<div className={styles.cardContent}>
<div className={styles.cardLeft}>
<Avatar className={styles.avatar}>{record.avatar}</Avatar>
<div className={styles.recordInfo}>
<div className={styles.nameAndType}>
<span className={styles.name}>{record.name}</span>
<span className={styles.type}>
{getTypeIcon(record.type)}
{record.type === "chat"
? "聊天"
: record.type === "call"
? "通话"
: record.type === "video"
? "视频"
: "邮件"}
</span>
</div>
<div className={styles.statusAndTime}>
<Tag color={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
<span className={styles.dateTime}>{record.dateTime}</span>
{record.duration && (
<span className={styles.duration}>
:{record.duration}
</span>
)}
</div>
<div className={styles.directionAndSubject}>
<Tag color={getDirectionColor(record.direction)}>
{getDirectionText(record.direction)}
</Tag>
{record.subject && (
<span className={styles.subject}>{record.subject}</span>
)}
</div>
<div className={styles.content}>{record.content}</div>
<div className={styles.tags}>
{record.tags.map((tag, index) => (
<Tag key={index} className={styles.tag}>
{tag}
</Tag>
))}
</div>
{record.attachments && record.attachments.length > 0 && (
<div className={styles.attachments}>
<span className={styles.attachmentsLabel}>:</span>
{record.attachments.map((attachment, index) => (
<div key={index} className={styles.attachment}>
<span className={styles.attachmentIcon}>
{getAttachmentIcon(attachment.type)}
</span>
<span className={styles.attachmentName}>
{attachment.name}
</span>
<DownloadOutlined className={styles.downloadIcon} />
</div>
))}
</div>
)}
</div>
</div>
<div className={styles.cardRight}>
<EyeOutlined className={styles.viewIcon} />
</div>
</div>
</Card>
))}
</div>
</div>
</div>

View File

@@ -0,0 +1,182 @@
import request from "@/api/request";
// 素材管理相关接口
export interface MaterialListParams {
keyword?: string;
limit?: string;
page?: string;
}
// 内容项类型定义
export interface ContentItem {
type: "text" | "image" | "video" | "file" | "audio" | "link";
data: string | LinkData;
}
// 链接数据类型
export interface LinkData {
title: string;
url: string;
cover: string;
}
export interface MaterialAddRequest {
title: string;
cover?: string;
status: number;
content: ContentItem[];
}
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 }, "DELETE");
}
// 素材管理-更新
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 }, "DELETE");
}
// 违禁词管理-更新
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 }, "DELETE");
}
// 关键词回复-更新
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,9 @@
// 管理组件导出
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

@@ -0,0 +1,243 @@
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} 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 KeywordModal from "../modals/KeywordModal";
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 = forwardRef<any, Record<string, never>>(
(props, ref) => {
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);
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchKeywords,
}));
// 关键词管理相关函数
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>
{/* 编辑弹窗 */}
<KeywordModal
visible={editModalVisible}
mode="edit"
keywordId={editingKeywordId}
onCancel={() => {
setEditModalVisible(false);
setEditingKeywordId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
},
);
KeywordManagement.displayName = "KeywordManagement";
export default KeywordManagement;

View File

@@ -0,0 +1,273 @@
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { Button, Input, Card, message, Modal } from "antd";
import {
SearchOutlined,
FilterOutlined,
FormOutlined,
FileTextOutlined,
FileImageOutlined,
PlayCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "../../index.module.scss";
import {
getMaterialList,
deleteMaterial,
type MaterialListParams,
} from "../../api";
import MaterialModal from "../modals/MaterialModal";
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 = forwardRef<any, Record<string, never>>(
(props, ref) => {
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 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);
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchMaterials,
}));
// 素材管理相关函数
const handleDeleteMaterial = async (id: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个素材吗?删除后无法恢复。",
okText: "确定",
cancelText: "取消",
okType: "danger",
onOk: async () => {
try {
await deleteMaterial(id.toString());
setMaterialsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} catch (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
actions={[
<Button
key="edit"
type="text"
icon={<FormOutlined />}
onClick={e => {
e.stopPropagation();
handleEditMaterial(item.id);
}}
>
</Button>,
<Button
key="delete"
type="text"
danger
icon={<DeleteOutlined />}
onClick={e => {
e.stopPropagation();
handleDeleteMaterial(item.id);
}}
>
</Button>,
]}
>
<div
className={styles.thumbnail}
onClick={() => handleEditMaterial(item.id)}
style={{ cursor: "pointer" }}
>
{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>
{/* 编辑弹窗 */}
<MaterialModal
visible={editModalVisible}
mode="edit"
materialId={editingMaterialId}
onCancel={() => {
setEditModalVisible(false);
setEditingMaterialId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
},
);
MaterialManagement.displayName = "MaterialManagement";
export default MaterialManagement;

View File

@@ -0,0 +1,232 @@
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 SensitiveWordModal from "../modals/SensitiveWordModal";
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>
{/* 编辑弹窗 */}
<SensitiveWordModal
visible={editModalVisible}
mode="edit"
sensitiveWordId={editingSensitiveWordId}
onCancel={() => {
setEditModalVisible(false);
setEditingSensitiveWordId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
};
export default SensitiveWordManagement;

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,276 @@
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 AudioUpload from "@/components/Upload/AudioUpload";
import type { ContentItem, LinkData } 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 = { title: "", url: "", cover: "" };
} 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 => {
const videoUrl = Array.isArray(url) ? url[0] || "" : url || "";
updateItemData(index, videoUrl);
}}
maxSize={50}
showPreview={true}
/>
);
case "file":
return (
<FileUpload
value={item.data as string}
onChange={url => {
const fileUrl = Array.isArray(url) ? url[0] || "" : url || "";
updateItemData(index, fileUrl);
}}
maxSize={10}
showPreview={true}
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
/>
);
case "audio":
return (
<AudioUpload
value={item.data as string}
onChange={url => {
const audioUrl = Array.isArray(url) ? url[0] || "" : url || "";
updateItemData(index, audioUrl);
}}
maxSize={50}
showPreview={true}
/>
);
case "link": {
const linkData = (item.data as LinkData) || {
title: "",
url: "",
cover: "",
};
return (
<div>
<Input
value={linkData.title}
onChange={e =>
updateItemData(index, { ...linkData, title: e.target.value })
}
placeholder="链接标题"
style={{ marginBottom: 8 }}
/>
<Input
value={linkData.url}
onChange={e =>
updateItemData(index, { ...linkData, url: e.target.value })
}
placeholder="链接URL"
style={{ marginBottom: 8 }}
/>
<ImageUpload
count={1}
accept="image/*"
value={linkData.cover ? [linkData.cover] : []}
onChange={urls =>
updateItemData(index, { ...linkData, cover: urls[0] || "" })
}
/>
</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

@@ -0,0 +1,236 @@
import React, { useState, useEffect, useCallback } from "react";
import { Modal, Form, Input, Button, message, Select } from "antd";
import {
addKeyword,
updateKeyword,
getKeywordDetails,
type KeywordAddRequest,
type KeywordUpdateRequest,
} from "../../api";
const { TextArea } = Input;
const { Option } = Select;
interface KeywordModalProps {
visible: boolean;
mode: "add" | "edit";
keywordId?: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const KeywordModal: React.FC<KeywordModalProps> = ({
visible,
mode,
keywordId,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
// 获取关键词详情
const fetchKeywordDetails = useCallback(
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("获取关键词详情失败");
}
},
[form],
);
// 当弹窗打开时处理数据
useEffect(() => {
if (visible) {
if (mode === "edit" && keywordId) {
// 编辑模式:获取详情
fetchKeywordDetails(keywordId);
} else if (mode === "add") {
// 添加模式:重置表单
form.resetFields();
}
}
}, [visible, mode, keywordId, fetchKeywordDetails, form]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
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 {
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(`${mode === "add" ? "添加" : "更新"}关键词失败:`, error);
message.error(`${mode === "add" ? "添加" : "更新"}关键词失败`);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
return (
<Modal
title={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 KeywordModal;

View File

@@ -0,0 +1,197 @@
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) {
if (mode === "edit" && materialId) {
// 编辑模式:获取详情
fetchMaterialDetails(materialId);
} else if (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

@@ -0,0 +1,197 @@
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";
const { TextArea } = Input;
const { Option } = Select;
interface SensitiveWordModalProps {
visible: boolean;
mode: "add" | "edit";
sensitiveWordId?: string | null;
onCancel: () => void;
onSuccess: () => void;
}
const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
visible,
mode,
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("获取敏感词详情失败");
}
};
// 当弹窗打开且为编辑模式时,获取详情
useEffect(() => {
if (visible && mode === "edit" && sensitiveWordId) {
fetchSensitiveWordDetails(sensitiveWordId);
} else if (visible && mode === "add") {
// 添加模式时重置表单
form.resetFields();
}
}, [visible, mode, sensitiveWordId]);
const handleSubmit = async (values: any) => {
try {
setLoading(true);
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 {
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(`${mode === "add" ? "添加" : "更新"}敏感词失败:`, error);
message.error(`${mode === "add" ? "添加" : "更新"}敏感词失败`);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
const title = mode === "add" ? "添加敏感词" : "编辑敏感词";
return (
<Modal
title={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 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,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,144 @@
import React 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,
SensitiveWordManagement,
KeywordManagement,
MaterialModal,
SensitiveWordModal,
KeywordModal,
} from "./components";
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 materialManagementRef = useRef<any>(null);
const keywordManagementRef = useRef<any>(null);
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 = () => {
// 刷新素材列表
if (materialManagementRef.current?.fetchMaterials) {
materialManagementRef.current.fetchMaterials();
}
// 刷新关键词列表
if (keywordManagementRef.current?.fetchKeywords) {
keywordManagementRef.current.fetchKeywords();
}
};
const renderTabContent = () => {
switch (activeTab) {
case "material":
return (
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
);
case "sensitive":
return <SensitiveWordManagement />;
case "keyword":
return (
<KeywordManagement ref={keywordManagementRef} {...({} as any)} />
);
default:
return (
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
);
}
};
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>
{/* 弹窗组件 */}
<MaterialModal
visible={materialModalVisible}
mode="add"
onCancel={() => setMaterialModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<SensitiveWordModal
visible={sensitiveWordModalVisible}
mode="add"
onCancel={() => setSensitiveWordModalVisible(false)}
onSuccess={handleModalSuccess}
/>
<KeywordModal
visible={keywordModalVisible}
mode="add"
onCancel={() => setKeywordModalVisible(false)}
onSuccess={handleModalSuccess}
/>
</div>
);
};

View File

@@ -1,43 +1,355 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
.searchBar {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 0 8px 0;
}
.content {
min-height: 400px;
}
.placeholder {
// 页面头部
.header {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
.headerLeft {
.title {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
.subtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
.headerRight {
.addButton {
background: #1890ff;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background: #40a9ff;
}
}
}
}
// 搜索和筛选区域
.searchSection {
display: flex;
gap: 12px;
margin-bottom: 24px;
.searchBox {
flex: 1;
position: relative;
display: flex;
align-items: center;
.searchIcon {
position: absolute;
left: 12px;
color: #8c8c8c;
font-size: 16px;
}
.searchInput {
width: 100%;
height: 40px;
padding: 0 12px 0 40px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
&:focus {
border-color: #1890ff;
}
&::placeholder {
color: #8c8c8c;
}
}
}
.filterButton {
display: flex;
align-items: center;
gap: 6px;
height: 40px;
padding: 0 16px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
color: #262626;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
}
}
// 标签页
.tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #f0f0f0;
.tab {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
font-size: 14px;
color: #8c8c8c;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
&.activeTab {
color: #1890ff;
border-bottom-color: #1890ff;
}
}
}
// 联系人列表
.contactsList {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
}
// 联系人卡片
.contactCard {
background: white;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px;
transition: all 0.3s;
height: 100%;
display: flex;
flex-direction: column;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #d9d9d9;
}
.cardHeader {
margin-bottom: 16px;
.contactInfo {
display: flex;
align-items: flex-start;
gap: 12px;
}
.nameSection {
flex: 1;
.contactName {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #262626;
margin: 0 0 4px 0;
.starIcon {
color: #faad14;
font-size: 14px;
}
}
.roleCompany {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
// 头像样式
.avatar {
border-radius: 50%;
object-fit: cover;
border: 2px solid #f0f0f0;
}
.avatarPlaceholder {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
border: 2px solid #f0f0f0;
}
.contactDetails {
margin-bottom: 16px;
flex: 1;
.contactItem {
display: flex;
margin: 0 0 8px 0;
font-size: 14px;
.label {
color: #8c8c8c;
margin-right: 8px;
min-width: 40px;
}
}
}
.tagsSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
.tag {
background: #f5f5f5;
color: #595959;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
}
.source {
font-size: 12px;
color: #8c8c8c;
}
}
.lastContact {
font-size: 12px;
color: #8c8c8c;
margin-bottom: 12px;
}
.notes {
background: #f9f9f9;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: #595959;
margin-bottom: 16px;
line-height: 1.4;
}
.actions {
display: flex;
gap: 12px;
margin-top: auto;
.actionButton {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 32px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 12px;
color: #595959;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
&:first-child:hover {
background: #e6f7ff;
}
&:last-child:hover {
background: #f6ffed;
border-color: #52c41a;
color: #52c41a;
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.contactsList {
grid-template-columns: 1fr;
}
.contactCard {
min-height: 175px;
}
.header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
.headerRight {
width: 100%;
.addButton {
width: 100%;
}
}
}
.searchSection {
flex-direction: column;
.filterButton {
width: 100%;
justify-content: center;
}
}
.tabs {
overflow-x: auto;
white-space: nowrap;
.tab {
flex-shrink: 0;
}
}
}

View File

@@ -1,24 +1,331 @@
import React from "react";
import React, { useState, useEffect } from "react";
import PowerNavigation from "@/components/PowerNavtion";
import {
SearchOutlined,
FilterOutlined,
MessageOutlined,
PhoneOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
import { Button, Input, Row, Col, Pagination, Spin, message } from "antd";
import { getContactList } from "@/pages/pc/ckbox/weChat/api";
import { ContractData } from "@/pages/pc/ckbox/data";
import Layout from "@/components/Layout/LayoutFiexd";
// 头像组件
const Avatar: React.FC<{ name: string; avatar?: string; size?: number }> = ({
name,
avatar,
size = 40,
}) => {
const getInitials = (name: string) => {
return name.charAt(0).toUpperCase();
};
const CustomerManagement: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="客户好友管理"
subtitle="统一管理客户信息和好友关系,提升服务效率"
showBackButton={true}
backButtonText="返回功能中心"
const getAvatarColor = (name: string) => {
const colors = [
"#1890ff",
"#52c41a",
"#faad14",
"#f5222d",
"#722ed1",
"#13c2c2",
"#eb2f96",
"#fa8c16",
];
const index = name.charCodeAt(0) % colors.length;
return colors[index];
};
if (avatar) {
return (
<img
src={avatar}
alt={name}
className={styles.avatar}
style={{ width: size, height: size }}
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
);
}
return (
<div
className={styles.avatarPlaceholder}
style={{
width: size,
height: size,
backgroundColor: getAvatarColor(name),
fontSize: size * 0.4,
}}
>
{getInitials(name)}
</div>
);
};
const CustomerManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState("all");
const [searchValue, setSearchValue] = useState("");
const [contacts, setContacts] = useState<ContractData[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 12,
total: 0,
});
// 获取各分类的总数
const [tabCounts, setTabCounts] = useState({
all: 0,
customer: 0,
potential: 0,
partner: 0,
friend: 0,
});
const tabs = [
{ key: "all", label: "全部", count: tabCounts.all },
{ key: "customer", label: "客户", count: tabCounts.customer },
{ key: "potential", label: "潜在客户", count: tabCounts.potential },
{ key: "partner", label: "合作伙伴", count: tabCounts.partner },
{ key: "friend", label: "朋友", count: tabCounts.friend },
];
// 加载联系人数据
const loadContacts = async (page: number = 1, pageSize: number = 12) => {
try {
setLoading(true);
// 构建请求参数
const params: any = {
page,
limit: pageSize,
};
// 添加搜索条件
if (searchValue.trim()) {
params.keyword = searchValue;
}
// 添加分类筛选
if (activeTab === "customer") {
params.isPassed = true;
} else if (activeTab === "potential") {
params.isPassed = false;
}
// "全部"、"partner" 和 "friend" 不添加额外筛选条件
const response = await getContactList(params);
// 假设接口返回格式为 { data: Contact[], total: number, page: number, limit: number }
setContacts(response.data || response.list || []);
setPagination(prev => ({
...prev,
current: response.page || page,
pageSize: response.limit || pageSize,
total: response.total || 0,
}));
// 更新分类统计
if (page === 1) {
// 只在第一页时更新统计,避免重复请求
const allResponse = await getContactList({ page: 1, limit: 1 });
const customerResponse = await getContactList({
page: 1,
limit: 1,
isPassed: true,
});
const potentialResponse = await getContactList({
page: 1,
limit: 1,
isPassed: false,
});
setTabCounts({
all: allResponse.total || 0,
customer: customerResponse.total || 0,
potential: potentialResponse.total || 0,
partner: 0, // 可以根据业务逻辑调整
friend: 0, // 可以根据业务逻辑调整
});
}
} catch (error) {
console.error("加载联系人数据失败:", error);
message.error("加载联系人数据失败,请稍后重试");
} finally {
setLoading(false);
}
};
// 当分类或搜索条件改变时重新加载数据
useEffect(() => {
loadContacts(1, pagination.pageSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, searchValue, pagination.pageSize]);
const filteredContacts = contacts;
return (
<Layout
header={
<>
<div style={{ padding: "20px" }}>
<PowerNavigation
title="客户好友管理"
subtitle="管理客户关系,维护好友信息"
showBackButton={true}
backButtonText="返回功能中心"
rightContent={<Button></Button>}
/>
{/* 搜索和筛选 */}
<div className={styles.searchBar}>
<Input
placeholder="搜索好友姓名、公司或标签..."
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
<Button
onClick={() => loadContacts(1, pagination.pageSize)}
size="large"
className={styles["refresh-btn"]}
loading={loading}
>
<FilterOutlined />
</Button>
</div>
{/* 标签页 */}
<div className={styles.tabs}>
{tabs.map(tab => (
<button
key={tab.key}
className={`${styles.tab} ${activeTab === tab.key ? styles.activeTab : ""}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.label} ({tab.count})
</button>
))}
</div>
</div>
</>
}
footer={
<div className="pagination-wrapper">
<Pagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
showSizeChanger
showQuickJumper
showTotal={(total, range) =>
`${range[0]}-${range[1]} 条,共 ${total}`
}
onChange={(page, pageSize) => {
loadContacts(page, pageSize || pagination.pageSize);
}}
onShowSizeChange={(current, size) => {
loadContacts(1, size);
}}
pageSizeOptions={["6", "12", "24", "48"]}
/>
</div>
}
>
<div className={styles.container}>
<div className={styles.content}>
{/* 联系人卡片列表 */}
<div className={styles.contactsList}>
{loading ? (
<div style={{ textAlign: "center", padding: "50px" }}>
<Spin size="large" />
<p style={{ marginTop: "16px", color: "#666" }}>
...
</p>
</div>
) : filteredContacts.length === 0 ? (
<div style={{ textAlign: "center", padding: "50px" }}>
<p style={{ color: "#999" }}></p>
</div>
) : (
<>
<Row gutter={[16, 16]}>
{filteredContacts.map(contact => (
<Col span={8} key={contact.id || contact.serverId}>
<div className={styles.contactCard}>
<div className={styles.cardHeader}>
<div className={styles.contactInfo}>
<Avatar
name={
contact.conRemark ||
contact.nickname ||
contact.alias ||
"未知用户"
}
avatar={contact.avatar}
size={48}
/>
<div className={styles.nameSection}>
<h3 className={styles.contactName}>
{contact.conRemark ||
contact.nickname ||
contact.alias ||
"未知用户"}
</h3>
<p className={styles.roleCompany}>
{"·"} {contact.desc || "未设置公司"}
</p>
</div>
</div>
</div>
<div className={styles.contactDetails}>
<div className={styles.contactInfo}>
<p className={styles.contactItem}>
<span className={styles.label}>:</span>{" "}
{contact.phone || "未设置电话"}
</p>
<p className={styles.contactItem}>
<span className={styles.label}>:</span>{" "}
{contact.region || contact.city || "未设置地区"}
</p>
<p className={styles.contactItem}>
<span className={styles.label}>ID:</span>{" "}
{contact.wechatId}
</p>
</div>
</div>
<div className={styles.tagsSection}>
<div className={styles.tags}>
{contact?.labels?.map(
(tag: string, index: number) => (
<span key={index} className={styles.tag}>
{tag}
</span>
),
)}
</div>
</div>
<div className={styles.actions}>
<Button type="primary" block>
<MessageOutlined />
</Button>
</div>
</div>
</Col>
))}
</Row>
</>
)}
</div>
</div>
</div>
</Layout>
);
};
export default CustomerManagement;

View File

@@ -1,21 +1,15 @@
import React from "react";
import {
AimOutlined,
SendOutlined,
CalendarOutlined,
TagsOutlined,
UserOutlined,
MessageOutlined,
FileTextOutlined,
RobotOutlined,
UserAddOutlined,
AppstoreOutlined,
SoundOutlined,
ThunderboltOutlined,
RiseOutlined,
TeamOutlined,
FolderOutlined,
BarChartOutlined,
CommentOutlined,
FileTextOutlined,
SoundOutlined,
EditOutlined,
} from "@ant-design/icons";
// 数据类型定义
export interface FeatureCard {
id: string;
title: string;
@@ -24,96 +18,87 @@ export interface FeatureCard {
color: string;
path?: string;
isNew?: boolean;
isHot?: boolean;
}
export interface TabItem {
key: string;
label: string;
icon?: React.ReactNode;
active?: boolean;
export interface FeatureCategory {
id: string;
title: string;
icon: React.ReactNode;
color: string;
count: number;
features: FeatureCard[];
}
export const featureCards: FeatureCard[] = [
// 功能数据
export const featureCategories: FeatureCategory[] = [
{
id: "precision-send",
title: "精准群发",
description: "基于客户标签和行为数据进行精准群发",
icon: <AimOutlined />,
color: "#ff6b35",
path: "/pc/powerCenter/precision-send",
id: "core",
title: "核心功能",
icon: <AimOutlined style={{ fontSize: "24px" }} />,
color: "#1890ff",
count: 2,
features: [
{
id: "customer-management",
title: "客户好友管理",
description: "管理客户关系,维护好友信息,提升客户满意度",
icon: <TeamOutlined style={{ fontSize: "24px" }} />,
color: "#1890ff",
path: "/pc/powerCenter/customer-management",
isHot: true,
},
{
id: "communication-record",
title: "沟通记录",
description: "记录和分析所有客户沟通历史,优化服务质量",
icon: <CommentOutlined style={{ fontSize: "24px" }} />,
color: "#52c41a",
path: "/pc/powerCenter/communication-record",
},
],
},
{
id: "sop-send",
title: "SOP群发",
description: "使用触客宝SOP标准化流程进行批量消息发送",
icon: <SendOutlined />,
color: "#4285f4",
path: "/pc/powerCenter/sop-send",
id: "ai",
title: "AI智能功能",
icon: <ThunderboltOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
count: 2,
features: [
{
id: "ai-training",
title: "AI模型训练",
description: "训练专属AI模型,提升智能服务能力",
icon: <FileTextOutlined style={{ fontSize: "24px" }} />,
color: "#fa8c16",
path: "/pc/powerCenter/ai-training",
isNew: true,
},
{
id: "auto-greeting",
title: "自动问候",
description: "设置智能问候规则,自动化客户接待流程",
icon: <SoundOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
path: "/pc/powerCenter/auto-greeting",
},
],
},
{
id: "moments-marketing",
title: "朋友圈营销",
description: "AI智能生成朋友圈内容提升品牌曝光度",
icon: <CalendarOutlined />,
color: "#34a853",
path: "/pc/powerCenter/moments-marketing",
},
{
id: "tag-management",
title: "标签管理",
description: "智能客户标签分类,精准用户画像分析",
icon: <TagsOutlined />,
color: "#9c27b0",
path: "/pc/powerCenter/tag-management",
},
{
id: "customer-management",
title: "客户好友管理",
description: "统一管理客户信息和好友关系,提升服务效率",
icon: <UserOutlined />,
color: "#6366f1",
path: "/pc/powerCenter/customer-management",
},
{
id: "communication-record",
title: "沟通记录",
description: "完整记录客户沟通历史,支持多维度查询分析",
icon: <MessageOutlined />,
color: "#06b6d4",
path: "/pc/powerCenter/communication-record",
},
{
id: "content-management",
title: "内容管理",
description: "素材管理、数据词汇库、关键词自动回复",
icon: <FileTextOutlined />,
color: "#f59e0b",
path: "/pc/powerCenter/content-management",
},
{
id: "ai-training",
title: "AI模型训练",
description: "自定义AI模型训练打造专属智能客服助手",
icon: <RobotOutlined />,
color: "#ec4899",
path: "/pc/powerCenter/ai-training",
isNew: true,
},
{
id: "auto-greeting",
title: "自动打招呼",
description: "智能识别新好友,自动发送个性化欢迎消息",
icon: <UserAddOutlined />,
color: "#10b981",
path: "/pc/powerCenter/auto-greeting",
id: "marketing",
title: "营销管理",
icon: <RiseOutlined style={{ fontSize: "24px" }} />,
color: "#52c41a",
count: 1,
features: [
{
id: "content-management",
title: "内容管理",
description: "管理营销内容,素材库,提升内容创作效率",
icon: <EditOutlined style={{ fontSize: "24px" }} />,
color: "#722ed1",
path: "/pc/powerCenter/content-management",
},
],
},
];
export const tabItems: TabItem[] = [
{ key: "all", label: "全部功能", icon: <AppstoreOutlined /> },
{ key: "marketing", label: "营销推广", icon: <SoundOutlined /> },
{ key: "customer", label: "客户管理", icon: <TeamOutlined /> },
{ key: "ai", label: "AI智能", icon: <RobotOutlined /> },
{ key: "content", label: "内容管理", icon: <FolderOutlined /> },
{ key: "data", label: "数据分析", icon: <BarChartOutlined /> },
];

View File

@@ -1,127 +1,176 @@
.powerCenter {
padding: 24px;
background-color: #f5f5f5;
padding: 40px;
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
min-height: 100vh;
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
// 功能分类区域
.categorySection {
margin-bottom: 48px;
.headerLeft {
.title {
font-size: 28px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
.categoryHeader {
display: flex;
align-items: center;
margin-bottom: 24px;
padding: 0 8px;
.categoryIcon {
width: 48px;
height: 48px;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
.anticon {
font-size: 24px;
color: white;
}
}
.subtitle {
font-size: 14px;
color: #666;
margin: 0;
.categoryInfo {
display: flex;
align-items: center;
gap: 10px;
.categoryTitle {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
}
.categoryCount {
font-size: 12px;
color: #666;
background: #f0f0f0;
border-radius: 12px;
border: 1px solid #e0e0e0;
height: 24px;
line-height: 20px;
padding: 0 10px;
}
}
}
.headerRight {
.tabs {
.tab {
border-radius: 8px;
border: 1px solid #e0e0e0;
color: #666;
.featureCards {
.featureCard {
border-radius: 16px;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
background: white;
padding: 20px;
box-sizing: border-box;
margin-bottom: 16px;
&:hover {
color: #1890ff;
border-color: #1890ff;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.activeTab {
border-radius: 8px;
background: #1890ff;
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
.cardContent {
.cardHeader {
position: relative;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.cardIcon {
color: #ffffff;
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.anticon {
font-size: 24px;
color: white;
}
}
.badge {
background: #ff6b35;
color: white;
font-size: 11px;
font-weight: 500;
padding: 4px 8px;
border-radius: 12px;
z-index: 2;
box-shadow: 0 1px 4px rgba(255, 107, 53, 0.3);
height: 24px;
// 新功能标签样式
&[data-type="new"] {
background: #52c41a;
box-shadow: 0 1px 4px rgba(82, 196, 26, 0.3);
}
}
}
.cardInfo {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.cardTitle {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
line-height: 1.3;
}
.cardDescription {
font-size: 14px;
color: #666;
line-height: 1.5;
margin: 0 0 12px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.cardAction {
display: flex;
justify-content: space-between;
color: #979797;
font-size: 12px;
font-weight: 500;
.arrow {
font-size: 14px;
transition: transform 0.3s ease;
}
&:hover .arrow {
transform: translateX(4px);
}
}
}
}
}
}
}
.cardGrid {
.featureCard {
border-radius: 16px;
border: none;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
height: 200px;
cursor: pointer;
// 页面底部
.footer {
text-align: center;
margin-top: 60px;
padding: 24px 0;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.cardContent {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 100%;
justify-content: center;
.iconWrapper {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
position: relative;
.icon {
font-size: 28px;
color: white;
}
}
.cardInfo {
.cardTitle {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.newBadge {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 8px;
font-weight: 500;
}
}
.cardDescription {
font-size: 12px;
color: #666;
line-height: 1.4;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}
p {
font-size: 16px;
color: #666;
margin: 0;
font-weight: 500;
}
}
}
@@ -129,16 +178,65 @@
// 响应式设计
@media (max-width: 1200px) {
.powerCenter {
.header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
padding: 32px 24px;
.headerRight {
width: 100%;
.categorySection {
.categoryHeader {
.categoryIcon {
width: 44px;
height: 44px;
.tabs {
flex-wrap: wrap;
.anticon {
font-size: 22px;
}
}
.categoryInfo {
.categoryTitle {
font-size: 22px;
}
}
}
.featureCards {
.featureCard {
height: 180px;
width: 260px;
padding: 20px;
.cardContent {
.cardIcon {
width: 44px;
height: 44px;
margin: 18px auto 14px;
.anticon {
font-size: 22px;
}
}
.cardInfo {
padding: 0 14px 14px;
.cardTitle {
font-size: 15px;
margin-bottom: 6px;
}
.cardDescription {
font-size: 11px;
margin-bottom: 10px;
}
.cardAction {
font-size: 11px;
.arrow {
font-size: 12px;
}
}
}
}
}
}
}
@@ -147,91 +245,89 @@
@media (max-width: 768px) {
.powerCenter {
padding: 16px;
padding: 24px 16px;
.header {
padding: 16px;
.categorySection {
margin-bottom: 32px;
.headerLeft {
.title {
font-size: 24px;
.categoryHeader {
.categoryIcon {
width: 40px;
height: 40px;
.anticon {
font-size: 20px;
}
}
}
}
.cardGrid {
.featureCard {
height: 160px;
.cardContent {
.iconWrapper {
width: 48px;
height: 48px;
.icon {
font-size: 20px;
}
.categoryInfo {
.categoryTitle {
font-size: 20px;
}
.cardInfo {
.cardTitle {
font-size: 14px;
.categoryCount {
font-size: 12px;
padding: 3px 10px;
}
}
}
.featureCards {
.featureCard {
height: 160px;
width: 240px;
padding: 16px;
.cardContent {
.badge {
top: 10px;
right: 10px;
font-size: 10px;
padding: 3px 6px;
}
.cardDescription {
font-size: 11px;
.cardIcon {
width: 40px;
height: 40px;
margin: 16px auto 12px;
.anticon {
font-size: 20px;
}
}
.cardInfo {
padding: 0 12px 12px;
.cardTitle {
font-size: 14px;
margin-bottom: 6px;
}
.cardDescription {
font-size: 10px;
margin-bottom: 8px;
}
.cardAction {
font-size: 10px;
.arrow {
font-size: 11px;
}
}
}
}
}
}
}
}
}
// 卡片颜色主题
.featureCard {
// 精准群发 - 橙色
&[data-color="#ff6b35"] .iconWrapper {
background: linear-gradient(135deg, #ff6b35, #f7931e);
}
// SOP群发 - 蓝色
&[data-color="#4285f4"] .iconWrapper {
background: linear-gradient(135deg, #4285f4, #1a73e8);
}
// 朋友圈营销 - 绿色
&[data-color="#34a853"] .iconWrapper {
background: linear-gradient(135deg, #34a853, #137333);
}
// 标签管理 - 紫色
&[data-color="#9c27b0"] .iconWrapper {
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
}
// 客户管理 - 靛蓝
&[data-color="#6366f1"] .iconWrapper {
background: linear-gradient(135deg, #6366f1, #4f46e5);
}
// 沟通记录 - 青色
&[data-color="#06b6d4"] .iconWrapper {
background: linear-gradient(135deg, #06b6d4, #0891b2);
}
// 内容管理 - 黄色
&[data-color="#f59e0b"] .iconWrapper {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
// AI训练 - 粉色
&[data-color="#ec4899"] .iconWrapper {
background: linear-gradient(135deg, #ec4899, #db2777);
}
// 自动打招呼 - 翠绿
&[data-color="#10b981"] .iconWrapper {
background: linear-gradient(135deg, #10b981, #059669);
.footer {
margin-top: 40px;
p {
font-size: 14px;
}
}
}
}

View File

@@ -1,12 +1,10 @@
import React, { useState, useMemo } from "react";
import { Card, Row, Col, Button, Space } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import { featureCards, tabItems, FeatureCard } from "./index.data";
import styles from "./index.module.scss";
import { FeatureCard, featureCategories } from "./index.data.tsx";
import { Col, Row } from "antd";
const PowerCenter: React.FC = () => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("all");
const handleCardClick = (card: FeatureCard) => {
if (card.path) {
@@ -14,87 +12,84 @@ const PowerCenter: React.FC = () => {
}
};
const handleTabClick = (tabKey: string) => {
setActiveTab(tabKey);
};
// 根据选中的标签过滤功能卡片
const filteredCards = useMemo(() => {
if (activeTab === "all") {
return featureCards;
}
const categoryMap: { [key: string]: string[] } = {
marketing: ["precision-send", "sop-send", "moments-marketing"],
customer: ["customer-management", "tag-management"],
ai: ["ai-training", "auto-greeting"],
content: ["content-management"],
data: ["communication-record"],
};
const categoryIds = categoryMap[activeTab] || [];
return featureCards.filter(card => categoryIds.includes(card.id));
}, [activeTab]);
return (
<div className={styles.powerCenter}>
{/* 页面头部 */}
<div className={styles.header}>
<div className={styles.headerLeft}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}>AI私域营销系统 - </p>
</div>
<div className={styles.headerRight}>
<Space className={styles.tabs}>
{tabItems.map(item => (
<Button
key={item.key}
type={activeTab === item.key ? "primary" : "text"}
className={
activeTab === item.key ? styles.activeTab : styles.tab
}
onClick={() => handleTabClick(item.key)}
icon={item.icon}
{/* 功能分类展示 */}
{featureCategories.map(category => (
<div key={category.id} className={styles.categorySection}>
{/* 分类标题 */}
<div className={styles.categoryHeader}>
<div
className={styles.categoryIcon}
style={{ backgroundColor: category.color }}
>
{category.icon}
</div>
<div className={styles.categoryInfo}>
<h2 className={styles.categoryTitle}>{category.title}</h2>
<span
className={styles.categoryCount}
style={{ backgroundColor: category.color, color: "#ffffff" }}
>
{item.label}
</Button>
))}
</Space>
</div>
</div>
{category.count}
</span>
</div>
</div>
{/* 功能卡片网格 */}
<div className={styles.cardGrid}>
<Row gutter={[24, 24]}>
{filteredCards.map(card => (
<Col key={card.id} xs={24} sm={12} md={8} lg={6} xl={6}>
<Card
className={styles.featureCard}
hoverable
onClick={() => handleCardClick(card)}
bodyStyle={{ padding: "24px" }}
>
<div className={styles.cardContent}>
{/* 功能卡片 */}
<div className={styles.featureCards}>
<Row gutter={16}>
{category.features.map(card => (
<Col span={8} key={card.id}>
<div
className={styles.iconWrapper}
style={{ backgroundColor: card.color }}
key={card.id}
className={styles.featureCard}
onClick={() => handleCardClick(card)}
>
<div className={styles.icon}>{card.icon}</div>
<div className={styles.cardContent}>
<div className={styles.cardHeader}>
<div
className={styles.cardIcon}
style={{ backgroundColor: card.color }}
>
{card.icon}
</div>
{/* 热门/新功能标签 */}
{(card.isHot || card.isNew) && (
<div
className={styles.badge}
data-type={card.isNew ? "new" : "hot"}
>
{card.isHot ? "热门" : "新功能"}
</div>
)}
{/* 功能图标 */}
</div>
{/* 功能信息 */}
<div className={styles.cardInfo}>
<h3 className={styles.cardTitle}>{card.title}</h3>
<p className={styles.cardDescription}>
{card.description}
</p>
<div className={styles.cardAction}>
<span></span>
<span className={styles.arrow}></span>
</div>
</div>
</div>
</div>
<div className={styles.cardInfo}>
<h3 className={styles.cardTitle}>
{card.title}
{card.isNew && (
<span className={styles.newBadge}></span>
)}
</h3>
<p className={styles.cardDescription}>{card.description}</p>
</div>
</div>
</Card>
</Col>
))}
</Row>
</Col>
))}
</Row>
</div>
</div>
))}
{/* 页面底部 */}
<div className={styles.footer}>
<p> AI私域营销系统 - </p>
</div>
</div>
);

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const MomentsMarketing: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="朋友圈营销"
subtitle="AI智能生成朋友圈内容提升品牌曝光度"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
</div>
);
};
export default MomentsMarketing;

View File

@@ -1,215 +0,0 @@
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.stepsContainer {
padding: 24px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.steps {
max-width: 600px;
margin: 0 auto;
}
.stepContent {
margin: 15px;
}
.stepCard {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 8px;
:global(.ant-card-head) {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.ant-card-head-title {
font-weight: 600;
color: #495057;
.anticon {
margin-right: 8px;
color: #1890ff;
}
}
}
}
.ageRange {
display: flex;
align-items: center;
.ant-input-number {
width: 80px;
}
}
.estimatedCount {
margin-top: 24px;
padding: 16px;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
.anticon {
color: #1890ff;
font-size: 16px;
}
span {
font-size: 14px;
color: #262626;
strong {
color: #1890ff;
font-size: 16px;
}
}
}
.variableList {
display: flex;
flex-wrap: wrap;
gap: 8px;
.ant-tag {
margin: 0;
transition: all 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
.messagePreview {
margin-top: 24px;
h4 {
margin-bottom: 12px;
color: #262626;
font-weight: 600;
}
}
.previewContent {
padding: 16px;
background: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 6px;
min-height: 80px;
font-size: 14px;
line-height: 1.6;
color: #262626;
white-space: pre-wrap;
}
.summary {
h4 {
margin-bottom: 16px;
color: #262626;
font-weight: 600;
}
}
.summaryGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.summaryItem {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
.anticon {
color: #1890ff;
font-size: 16px;
}
span {
font-size: 14px;
color: #262626;
}
}
.stepActions {
display: flex;
justify-content: center;
padding: 24px 0;
background: #fff;
.ant-btn {
min-width: 100px;
}
}
// 响应式设计
@media (max-width: 768px) {
.container {
padding: 16px;
}
.stepsContainer {
padding: 16px;
}
.summaryGrid {
grid-template-columns: 1fr;
}
.ageRange {
flex-direction: column;
align-items: flex-start;
gap: 8px;
span {
display: none;
}
}
}
// 表单样式优化
:global {
.ant-form-item-label > label {
font-weight: 500;
color: #262626;
}
.ant-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
}
.ant-upload-list-picture-card .ant-upload-list-item {
border-radius: 6px;
}
.ant-steps-item-title {
font-weight: 500 !important;
}
}

View File

@@ -1,726 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import Layout from "@/components/Layout/LayoutFiexd";
import {
Card,
Button,
Steps,
Form,
Input,
Select,
Checkbox,
Radio,
DatePicker,
TimePicker,
InputNumber,
Upload,
Tag,
Divider,
Space,
message,
} from "antd";
import {
UserOutlined,
MessageOutlined,
SendOutlined,
PlusOutlined,
TagsOutlined,
ClockCircleOutlined,
TeamOutlined,
AimOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
import type { UploadFile } from "antd";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const { Step } = Steps;
const { TextArea } = Input;
const { Option } = Select;
interface CustomerFilter {
tags: string[];
regions: string[];
ageRange: [number, number];
gender: "all" | "male" | "female";
lastContactTime: "all" | "7days" | "30days" | "90days" | "180days";
purchaseHistory: "all" | "purchased" | "no-purchase" | "high-value";
}
interface MessageContent {
type: "text" | "image" | "mixed";
text: string;
images: UploadFile[];
variables: string[];
}
interface SendSettings {
sendMode: "immediate" | "scheduled";
scheduledTime?: string;
sendInterval: number;
maxPerDay: number;
timeRange: [string, string];
}
const PrecisionSend: React.FC = () => {
const [currentStep, setCurrentStep] = useState(0);
const [form] = Form.useForm();
const [customerFilter, setCustomerFilter] = useState<CustomerFilter>({
tags: [],
regions: [],
ageRange: [18, 65],
gender: "all",
lastContactTime: "all",
purchaseHistory: "all",
});
const [messageContent, setMessageContent] = useState<MessageContent>({
type: "text",
text: "",
images: [],
variables: [],
});
const [sendSettings, setSendSettings] = useState<SendSettings>({
sendMode: "immediate",
sendInterval: 5,
maxPerDay: 100,
timeRange: ["09:00", "18:00"],
});
const [estimatedCount, setEstimatedCount] = useState(0);
const [loading, setLoading] = useState(false);
const customerTags = [
"高价值客户",
"潜在客户",
"活跃用户",
"沉默用户",
"VIP客户",
"新客户",
"老客户",
"流失客户",
];
const regions = [
"北京",
"上海",
"广州",
"深圳",
"杭州",
"南京",
"成都",
"武汉",
"西安",
"重庆",
"天津",
"苏州",
];
const messageVariables = [
"{客户姓名}",
"{公司名称}",
"{联系人}",
"{产品名称}",
"{优惠金额}",
"{到期时间}",
"{客服电话}",
"{官网地址}",
];
const handleNext = () => {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
}
};
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = async () => {
try {
// 验证必填项
if (!messageContent.text && messageContent.images.length === 0) {
message.error("请输入消息内容或上传图片");
return;
}
if (
sendSettings.sendMode === "scheduled" &&
!sendSettings.scheduledTime
) {
message.error("请选择发送时间");
return;
}
if (estimatedCount === 0) {
message.error("没有符合条件的客户,请调整筛选条件");
return;
}
setLoading(true);
const hide = message.loading("正在创建发送任务...", 0);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000));
hide();
message.success(
`发送任务已创建成功!预计发送给 ${estimatedCount} 位客户`,
);
// 重置表单
setCurrentStep(0);
form.resetFields();
setCustomerFilter({
tags: [],
regions: [],
ageRange: [18, 65],
gender: "all",
lastContactTime: "all",
purchaseHistory: "all",
});
setMessageContent({
type: "text",
text: "",
images: [],
variables: [],
});
setSendSettings({
sendMode: "immediate",
sendInterval: 5,
maxPerDay: 100,
timeRange: ["09:00", "18:00"],
});
} catch (error) {
message.error("提交失败,请检查网络连接后重试");
console.error("Submit error:", error);
} finally {
setLoading(false);
}
};
// 计算预估客户数量
const calculateEstimatedCount = useCallback(() => {
let baseCount = 1000; // 假设基础客户数
// 根据筛选条件调整数量
if (customerFilter.tags.length > 0) {
baseCount = Math.floor(
baseCount * (0.9 - customerFilter.tags.length * 0.1),
);
}
if (customerFilter.regions.length > 0) {
baseCount = Math.floor(
baseCount * (0.95 - customerFilter.regions.length * 0.05),
);
}
if (customerFilter.gender !== "all") {
baseCount = Math.floor(baseCount * 0.5);
}
if (customerFilter.purchaseHistory !== "all") {
baseCount = Math.floor(baseCount * 0.6);
}
setEstimatedCount(Math.max(baseCount, 0));
}, [customerFilter]);
useEffect(() => {
calculateEstimatedCount();
}, [calculateEstimatedCount]);
const renderStepContent = () => {
switch (currentStep) {
case 0:
return (
<Card
title={
<>
<UserOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="客户标签" name="tags">
<Checkbox.Group
options={customerTags.map(tag => ({
label: tag,
value: tag,
}))}
value={customerFilter.tags}
onChange={values =>
setCustomerFilter({
...customerFilter,
tags: values as string[],
})
}
/>
</Form.Item>
<Form.Item label="地区筛选" name="regions">
<Select
mode="multiple"
placeholder="选择目标地区"
value={customerFilter.regions}
onChange={values =>
setCustomerFilter({ ...customerFilter, regions: values })
}
>
{regions.map(region => (
<Option key={region} value={region}>
{region}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="年龄范围" name="ageRange">
<div className={styles.ageRange}>
<InputNumber
min={18}
max={100}
value={customerFilter.ageRange[0]}
onChange={value =>
setCustomerFilter({
...customerFilter,
ageRange: [value || 18, customerFilter.ageRange[1]],
})
}
/>
<span style={{ margin: "0 8px" }}>-</span>
<InputNumber
min={18}
max={100}
value={customerFilter.ageRange[1]}
onChange={value =>
setCustomerFilter({
...customerFilter,
ageRange: [customerFilter.ageRange[0], value || 65],
})
}
/>
</div>
</Form.Item>
<Form.Item label="性别" name="gender">
<Radio.Group
value={customerFilter.gender}
onChange={e =>
setCustomerFilter({
...customerFilter,
gender: e.target.value,
})
}
>
<Radio value="all"></Radio>
<Radio value="male"></Radio>
<Radio value="female"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="最后联系时间" name="lastContactTime">
<Select
value={customerFilter.lastContactTime}
onChange={value =>
setCustomerFilter({
...customerFilter,
lastContactTime: value,
})
}
>
<Option value="all"></Option>
<Option value="7days">7</Option>
<Option value="30days">30</Option>
<Option value="90days">90</Option>
<Option value="180days">180</Option>
</Select>
</Form.Item>
<Form.Item label="购买历史" name="purchaseHistory">
<Select
value={customerFilter.purchaseHistory}
onChange={value =>
setCustomerFilter({
...customerFilter,
purchaseHistory: value,
})
}
>
<Option value="all"></Option>
<Option value="purchased"></Option>
<Option value="no-purchase"></Option>
<Option value="high-value"></Option>
</Select>
</Form.Item>
</Form>
<div className={styles.estimatedCount}>
<TeamOutlined />
<span>
<strong>{estimatedCount}</strong>
</span>
</div>
</Card>
);
case 1:
return (
<Card
title={
<>
<MessageOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="消息类型" name="messageType">
<Radio.Group
value={messageContent.type}
onChange={e =>
setMessageContent({
...messageContent,
type: e.target.value,
})
}
>
<Radio value="text"></Radio>
<Radio value="image"></Radio>
<Radio value="mixed"></Radio>
</Radio.Group>
</Form.Item>
{(messageContent.type === "text" ||
messageContent.type === "mixed") && (
<Form.Item label="消息文本" name="messageContent">
<TextArea
rows={6}
placeholder="请输入消息内容,支持变量如 {客户姓名}、{产品名称} 等"
value={messageContent.text}
onChange={e =>
setMessageContent({
...messageContent,
text: e.target.value,
})
}
showCount
maxLength={500}
/>
</Form.Item>
)}
{(messageContent.type === "image" ||
messageContent.type === "mixed") && (
<Form.Item label="上传图片" name="images">
<Upload
listType="picture-card"
fileList={messageContent.images}
onChange={({ fileList }) =>
setMessageContent({ ...messageContent, images: fileList })
}
beforeUpload={file => {
const isImage = file.type?.startsWith("image/");
if (!isImage) {
message.error("只能上传图片文件!");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error("图片大小不能超过 2MB!");
}
return false; // 阻止自动上传
}}
accept="image/*"
>
{messageContent.images.length < 9 && (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}></div>
</div>
)}
</Upload>
</Form.Item>
)}
<Form.Item label="可用变量">
<div className={styles.variableList}>
{messageVariables.map(variable => (
<Tag
key={variable}
icon={<TagsOutlined />}
color="blue"
style={{ cursor: "pointer", margin: "4px" }}
onClick={() => {
const newText = messageContent.text + variable;
setMessageContent({ ...messageContent, text: newText });
}}
>
{variable}
</Tag>
))}
</div>
</Form.Item>
<Divider />
<div className={styles.previewArea}>
<h4></h4>
<div className={styles.messagePreview}>
{messageContent.text && (
<div className={styles.textPreview}>
{messageContent.text}
</div>
)}
{messageContent.images.length > 0 && (
<div className={styles.imagePreview}>
{messageContent.images.map((image, index) => (
<img
key={index}
src={image.url || image.thumbUrl}
alt={`preview-${index}`}
style={{
width: 60,
height: 60,
objectFit: "cover",
margin: 4,
borderRadius: 4,
}}
/>
))}
</div>
)}
</div>
</div>
</Form>
</Card>
);
case 2:
return (
<Card
title={
<>
<SendOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="发送模式" name="sendMode">
<Radio.Group
value={sendSettings.sendMode}
onChange={e =>
setSendSettings({
...sendSettings,
sendMode: e.target.value,
})
}
>
<Radio value="immediate"></Radio>
<Radio value="scheduled"></Radio>
</Radio.Group>
</Form.Item>
{sendSettings.sendMode === "scheduled" && (
<Form.Item label="定时时间" name="scheduledTime">
<DatePicker
showTime
placeholder="选择发送时间"
onChange={(date, dateString) =>
setSendSettings({
...sendSettings,
scheduledTime: dateString as string,
})
}
disabledDate={current =>
current && current < dayjs().endOf("day")
}
/>
</Form.Item>
)}
<Form.Item label="发送间隔(秒)" name="sendInterval">
<InputNumber
min={1}
max={60}
value={sendSettings.sendInterval}
onChange={value =>
setSendSettings({
...sendSettings,
sendInterval: value || 5,
})
}
addonAfter="秒"
/>
</Form.Item>
<Form.Item label="每日最大发送数" name="maxPerDay">
<InputNumber
min={1}
max={1000}
value={sendSettings.maxPerDay}
onChange={value =>
setSendSettings({
...sendSettings,
maxPerDay: value || 100,
})
}
addonAfter="条"
/>
</Form.Item>
<Form.Item label="发送时间段" name="timeRange">
<TimePicker.RangePicker
format="HH:mm"
value={
sendSettings.timeRange.map(time =>
time ? dayjs(time, "HH:mm") : null,
) as any
}
onChange={(times, timeStrings) =>
setSendSettings({
...sendSettings,
timeRange: timeStrings as [string, string],
})
}
placeholder={["开始时间", "结束时间"]}
/>
</Form.Item>
<Form.Item label="高级设置">
<Space direction="vertical">
<Checkbox> ()</Checkbox>
<Checkbox> ()</Checkbox>
<Checkbox> ()</Checkbox>
</Space>
</Form.Item>
</Form>
<Divider />
<div className={styles.summary}>
<h4>
<AimOutlined style={{ marginRight: 8 }} />
</h4>
<div className={styles.summaryGrid}>
<div className={styles.summaryItem}>
<TeamOutlined />
<span>
<strong>{estimatedCount}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<ClockCircleOutlined />
<span>
<strong>
{sendSettings.sendMode === "immediate"
? "立即发送"
: `定时发送 (${sendSettings.scheduledTime || "未设置"})`}
</strong>
</span>
</div>
<div className={styles.summaryItem}>
<AimOutlined />
<span>
<strong>{sendSettings.sendInterval}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<TagsOutlined />
<span>
<strong>{sendSettings.maxPerDay}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<ClockCircleOutlined />
<span>
<strong>
{sendSettings.timeRange[0]} - {sendSettings.timeRange[1]}
</strong>
</span>
</div>
</div>
</div>
</Card>
);
default:
return null;
}
};
return (
<Layout
header={
<>
<PowerNavigation
title="精准群发1"
subtitle="基于客户标签和行为数据进行精准群发"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.stepsContainer}>
<Steps current={currentStep} className={styles.steps}>
<Step title="客户筛选" icon={<UserOutlined />} />
<Step title="消息内容" icon={<MessageOutlined />} />
<Step title="发送设置" icon={<SendOutlined />} />
</Steps>
</div>
</>
}
footer={
<div className={styles.stepActions}>
<Space>
{currentStep > 0 && (
<Button size="large" onClick={handlePrev} disabled={loading}>
</Button>
)}
{currentStep < 2 ? (
<Button
type="primary"
size="large"
onClick={() => {
// 简单验证当前步骤
if (currentStep === 0 && estimatedCount === 0) {
message.warning("请设置筛选条件以选择目标客户");
return;
}
if (
currentStep === 1 &&
!messageContent.text &&
messageContent.images.length === 0
) {
message.warning("请输入消息内容或上传图片");
return;
}
handleNext();
}}
disabled={loading}
>
</Button>
) : (
<Button
type="primary"
size="large"
onClick={handleSubmit}
loading={loading}
icon={<SendOutlined />}
>
</Button>
)}
</Space>
</div>
}
>
<div className={styles.container}>
<div className={styles.stepContent}>{renderStepContent()}</div>
</div>
</Layout>
);
};
export default PrecisionSend;

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const SopSend: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="SOP群发"
subtitle="使用触客宝SOP标准化流程进行批量消息发送"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>SOP群发功能正在开发中...</p>
</div>
</div>
</div>
);
};
export default SopSend;

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const TagManagement: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="标签管理"
subtitle="智能客户标签分类,精准用户画像分析"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
</div>
);
};
export default TagManagement;

View File

@@ -37,6 +37,11 @@ export function getContactList(params) {
export function getGroupList(params) {
return request("/v1/kefu/wechatChatroom/list", params, "GET");
}
// 分组列表
export function getLabelsListByGroup(params) {
return request("/v1/kefu/wechatGroup/list", params, "GET");
}
//==============-原接口=================
// 获取联系人列表
// export const getContactList = (params: { prevId: number; count: number }) => {
@@ -316,3 +321,65 @@ export const forwardMessage = (
export const recallMessage = (messageId: string): Promise<void> => {
return request2(`/v1/messages/${messageId}/recall`, {}, "PUT");
};
// ============== 跟进提醒相关接口 ==============
// 跟进提醒列表
export const getFollowUpList = (params: {
isProcess?: string;
isRemind?: string;
keyword?: string;
level?: string;
limit?: string;
page?: string;
friendId?: string;
}) => {
return request("/v1/kefu/followUp/list", params, "GET");
};
// 跟进提醒添加
export const addFollowUp = (params: {
description?: string;
friendId: string;
reminderTime?: string;
title?: string;
type?: string; // 0其他 1电话回访 2发送消息 3安排会议 4发送邮件
}) => {
return request("/v1/kefu/followUp/add", params, "POST");
};
// 跟进提醒处理
export const processFollowUp = (params: { ids?: string }) => {
return request("/v1/kefu/followUp/process", params, "GET");
};
// ============== 待办事项相关接口 ==============
// 待办事项列表
export const getTodoList = (params: {
isProcess?: string;
isRemind?: string;
keyword?: string;
level?: string;
limit?: string;
page?: string;
friendId?: string;
}) => {
return request("/v1/kefu/todo/list", params, "GET");
};
// 待办事项添加
export const addTodo = (params: {
description?: string;
friendId: string;
level?: string; // 0低优先级 1中优先级 2高优先级 3紧急
reminderTime?: string;
title?: string;
}) => {
return request("/v1/kefu/todo/add", params, "POST");
};
// 待办事项处理
export const processTodo = (params: { ids: string }) => {
return request("/v1/kefu/todo/process", params, "GET");
};

View File

@@ -9,6 +9,12 @@
display: flex;
flex-direction: column;
min-width: 0;
.extend {
background: #fff;
padding: 10px 16px;
display: flex;
gap: 16px;
}
}
.chatHeader {

View File

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

View File

@@ -0,0 +1,323 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Modal,
Form,
Select,
DatePicker,
Input,
Button,
List,
Tag,
Space,
Typography,
message,
} from "antd";
import {
PlusOutlined,
ClockCircleOutlined,
PhoneOutlined,
MessageOutlined,
} from "@ant-design/icons";
import {
getFollowUpList,
addFollowUp,
processFollowUp,
} from "@/pages/pc/ckbox/weChat/api";
import styles from "./index.module.scss";
const { Option } = Select;
const { TextArea } = Input;
const { Text } = Typography;
// 类型映射
const typeMap: { [key: string]: string } = {
"1": "电话",
"2": "消息",
"3": "会议",
"4": "邮件",
"0": "其他",
};
interface FollowupReminder {
id: string;
type: "电话" | "消息" | "其他" | "会议" | "邮件";
status: "待处理" | "已完成" | "已取消";
content: string;
scheduledTime: string;
recipient: string;
title?: string;
description?: string;
friendId?: string;
}
interface FollowupReminderModalProps {
visible: boolean;
onClose: () => void;
recipientName?: string;
friendId?: string;
}
const FollowupReminderModal: React.FC<FollowupReminderModalProps> = ({
visible,
onClose,
recipientName = "客户",
friendId,
}) => {
const [form] = Form.useForm();
const [reminders, setReminders] = useState<FollowupReminder[]>([]);
const [loading, setLoading] = useState(false);
const [addLoading, setAddLoading] = useState(false);
// 跟进方式选项
const followupMethods = [
{ value: "1", label: "电话回访" },
{ value: "2", label: "发送消息" },
{ value: "3", label: "安排会议" },
{ value: "4", label: "发送邮件" },
{ value: "0", label: "其他" },
];
// 加载跟进提醒列表
const loadFollowUpList = useCallback(async () => {
if (!friendId) return;
setLoading(true);
try {
const response = await getFollowUpList({
friendId,
limit: "50",
page: "1",
});
if (response && response.list) {
const formattedReminders = response.list.map((item: any) => ({
id: item.id?.toString() || "",
type: typeMap[item.type] || "其他",
status: item.isProcess === 1 ? "已完成" : "待处理",
content: item.description || item.title || "",
scheduledTime: item.reminderTime || "",
recipient: recipientName,
title: item.title,
description: item.description,
friendId: item.friendId,
}));
setReminders(formattedReminders);
}
} catch (error) {
console.error("加载跟进提醒列表失败:", error);
message.error("加载跟进提醒列表失败");
} finally {
setLoading(false);
}
}, [friendId, recipientName]);
// 当模态框打开时加载数据
useEffect(() => {
if (visible && friendId) {
loadFollowUpList();
}
}, [visible, friendId, loadFollowUpList]);
// 处理添加提醒
const handleAddReminder = async () => {
if (!friendId) {
message.error("缺少好友ID无法添加提醒");
return;
}
setAddLoading(true);
try {
const values = await form.validateFields();
const params = {
friendId,
type: values.method,
title: values.content,
description: values.content,
reminderTime: values.dateTime.format("YYYY-MM-DD HH:mm:ss"),
};
const response = await addFollowUp(params);
if (response) {
message.success("添加跟进提醒成功");
form.resetFields();
// 重新加载列表
loadFollowUpList();
}
} catch (error) {
console.error("添加跟进提醒失败:", error);
message.error("添加跟进提醒失败");
} finally {
setAddLoading(false);
}
};
// 处理跟进提醒
const handleProcessReminder = async (id: string) => {
try {
const response = await processFollowUp({ ids: id });
if (response) {
message.success("处理成功");
// 重新加载列表
loadFollowUpList();
}
} catch (error) {
console.error("处理跟进提醒失败:", error);
message.error("处理跟进提醒失败");
}
};
// 获取状态标签颜色
const getStatusColor = (status: string) => {
switch (status) {
case "待处理":
return "warning";
case "已完成":
return "success";
case "已取消":
return "default";
default:
return "default";
}
};
// 获取类型图标
const getTypeIcon = (type: string) => {
return type === "电话" ? <PhoneOutlined /> : <MessageOutlined />;
};
return (
<Modal
title={
<div className={styles.modalHeader}>
<div className={styles.modalTitle}></div>
<div className={styles.modalSubtitle}></div>
</div>
}
open={visible}
onCancel={onClose}
footer={null}
width={480}
className={styles.followupModal}
>
<div className={styles.modalContent}>
{/* 添加新提醒区域 */}
<div className={styles.addReminderSection}>
<Form form={form} layout="vertical" className={styles.reminderForm}>
<div className={styles.formRow}>
<Form.Item
name="method"
label="跟进方式"
rules={[{ required: true, message: "请选择跟进方式" }]}
className={styles.formItem}
>
<Select placeholder="电话回访" className={styles.selectInput}>
{followupMethods.map(method => (
<Option key={method.value} value={method.value}>
{method.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="dateTime"
label="提醒时间"
rules={[{ required: true, message: "请选择提醒时间" }]}
className={styles.formItem}
>
<DatePicker
showTime
format="YYYY/M/D HH:mm"
placeholder="年/月/日 --:--"
className={styles.dateInput}
/>
</Form.Item>
</div>
<Form.Item
name="content"
label="提醒内容"
rules={[{ required: true, message: "请输入提醒内容" }]}
>
<TextArea
placeholder="提醒内容..."
rows={3}
className={styles.contentInput}
/>
</Form.Item>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddReminder}
className={styles.addButton}
loading={addLoading}
block
>
</Button>
</Form>
</div>
{/* 现有提醒列表 */}
<div className={styles.remindersList}>
<List
dataSource={reminders}
loading={loading}
renderItem={reminder => (
<List.Item className={styles.reminderItem}>
<div className={styles.reminderContent}>
<div className={styles.reminderHeader}>
<Space>
<Tag
icon={getTypeIcon(reminder.type)}
color="blue"
className={styles.typeTag}
>
{reminder.type}
</Tag>
<Tag color={getStatusColor(reminder.status)}>
{reminder.status}
</Tag>
</Space>
<Text className={styles.recipient}>
{reminder.recipient}
</Text>
</div>
<div className={styles.reminderBody}>
<Text className={styles.reminderText}>
{reminder.content}
</Text>
</div>
<div className={styles.reminderFooter}>
<Space>
<ClockCircleOutlined className={styles.clockIcon} />
<Text className={styles.scheduledTime}>
{reminder.scheduledTime}
</Text>
{reminder.status === "待处理" && (
<Button
type="link"
size="small"
onClick={() => handleProcessReminder(reminder.id)}
>
</Button>
)}
</Space>
</div>
</div>
</List.Item>
)}
/>
</div>
</div>
</Modal>
);
};
export default FollowupReminderModal;

View File

@@ -32,10 +32,9 @@ import { FriendSelectionItem } from "@/components/FriendSelection/data";
import styles from "./Person.module.scss";
interface PersonProps {
contract: ContractData | weChatGroup;
onToggleProfile?: () => void;
}
const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
const Person: React.FC<PersonProps> = ({ contract }) => {
const [messageApi, contextHolder] = message.useMessage();
const [isEditingRemark, setIsEditingRemark] = useState(false);
const [remarkValue, setRemarkValue] = useState(contract.conRemark || "");
@@ -504,10 +503,6 @@ const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
chatroomOperateType: 4, // 4 for quit
});
messageApi.success("已退出群聊");
// 可能还需要一个回调来关闭侧边栏或切换到另一个聊天
if (onToggleProfile) {
onToggleProfile();
}
},
});
};

View File

@@ -11,10 +11,9 @@ const { Sider } = Layout;
interface PersonProps {
contract: ContractData | weChatGroup;
onToggleProfile?: () => void;
}
const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
const Person: React.FC<PersonProps> = ({ contract }) => {
const [activeKey, setActiveKey] = useState("profile");
const isGroup = "chatroomId" in contract;
console.log(contract);
@@ -50,12 +49,7 @@ const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
/>
}
>
{activeKey === "profile" && (
<ProfileModules
contract={contract}
onToggleProfile={onToggleProfile}
/>
)}
{activeKey === "profile" && <ProfileModules contract={contract} />}
{activeKey === "quickwords" && (
<QuickWords
words={[]}

View File

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

View File

@@ -0,0 +1,311 @@
import React, { useState, useEffect, useCallback } from "react";
import {
Modal,
Form,
Input,
Select,
DatePicker,
Button,
List,
Checkbox,
Tag,
Space,
Typography,
message,
} from "antd";
import { PlusOutlined, CalendarOutlined } from "@ant-design/icons";
import { getTodoList, addTodo, processTodo } from "@/pages/pc/ckbox/weChat/api";
import styles from "./index.module.scss";
const { Option } = Select;
const { TextArea } = Input;
const { Text } = Typography;
// 优先级映射
const priorityMap: { [key: string]: string } = {
"0": "低",
"1": "中",
"2": "高",
"3": "紧急",
};
interface TodoItem {
id: string;
title: string;
description?: string;
client?: string;
priority: "高" | "中" | "低" | "紧急";
dueDate: string;
completed: boolean;
friendId?: string;
}
interface TodoListModalProps {
visible: boolean;
onClose: () => void;
clientName?: string;
friendId?: string;
}
const TodoListModal: React.FC<TodoListModalProps> = ({
visible,
onClose,
clientName = "客户",
friendId,
}) => {
const [form] = Form.useForm();
const [todos, setTodos] = useState<TodoItem[]>([]);
const [loading, setLoading] = useState(false);
const [addLoading, setAddLoading] = useState(false);
// 优先级选项
const priorityOptions = [
{ value: "2", label: "高优先级", color: "orange" },
{ value: "1", label: "中优先级", color: "blue" },
{ value: "0", label: "低优先级", color: "green" },
{ value: "3", label: "紧急", color: "red" },
];
// 加载待办事项列表
const loadTodoList = useCallback(async () => {
if (!friendId) return;
setLoading(true);
try {
const response = await getTodoList({
friendId,
limit: "50",
page: "1",
});
if (response && response.list) {
const formattedTodos = response.list.map((item: any) => ({
id: item.id?.toString() || "",
title: item.title || "",
description: item.description || "",
client: clientName,
priority: priorityMap[item.level] || "中",
dueDate: item.reminderTime || "",
completed: item.isProcess === 1,
friendId: item.friendId,
}));
setTodos(formattedTodos);
}
} catch (error) {
console.error("加载待办事项列表失败:", error);
message.error("加载待办事项列表失败");
} finally {
setLoading(false);
}
}, [friendId, clientName]);
// 当模态框打开时加载数据
useEffect(() => {
if (visible && friendId) {
loadTodoList();
}
}, [visible, friendId, loadTodoList]);
// 处理添加任务
const handleAddTask = async () => {
if (!friendId) {
message.error("缺少好友ID无法添加任务");
return;
}
setAddLoading(true);
try {
const values = await form.validateFields();
const params = {
friendId,
title: values.title,
description: values.description,
level: values.priority,
reminderTime: values.dueDate.format("YYYY-MM-DD HH:mm:ss"),
};
const response = await addTodo(params);
if (response) {
message.success("添加待办事项成功");
form.resetFields();
// 重新加载列表
loadTodoList();
}
} catch (error) {
console.error("添加待办事项失败:", error);
message.error("添加待办事项失败");
} finally {
setAddLoading(false);
}
};
// 处理任务完成状态切换
const handleToggleComplete = async (id: string) => {
try {
const response = await processTodo({ ids: id });
if (response) {
message.success("任务状态更新成功");
// 重新加载列表
loadTodoList();
}
} catch (error) {
console.error("更新任务状态失败:", error);
message.error("更新任务状态失败");
}
};
// 获取优先级标签颜色
const getPriorityColor = (priority: string) => {
switch (priority) {
case "高":
return "orange";
case "中":
return "blue";
case "低":
return "green";
case "紧急":
return "red";
default:
return "default";
}
};
return (
<Modal
title={
<div className={styles.modalHeader}>
<div className={styles.modalTitle}></div>
<div className={styles.modalSubtitle}></div>
</div>
}
open={visible}
onCancel={onClose}
footer={null}
width={480}
className={styles.todoModal}
>
<div className={styles.modalContent}>
{/* 添加新任务区域 */}
<div className={styles.addTaskSection}>
<Form form={form} layout="vertical" className={styles.taskForm}>
<Form.Item
name="title"
rules={[{ required: true, message: "请输入任务标题" }]}
>
<Input placeholder="任务标题..." className={styles.titleInput} />
</Form.Item>
<Form.Item name="description">
<TextArea
placeholder="任务描述 (可选)..."
rows={2}
className={styles.descriptionInput}
/>
</Form.Item>
<div className={styles.formRow}>
<Form.Item
name="priority"
rules={[{ required: true, message: "请选择优先级" }]}
className={styles.formItem}
>
<Select
placeholder="中优先级"
className={styles.prioritySelect}
>
{priorityOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="dueDate"
rules={[{ required: true, message: "请选择截止时间" }]}
className={styles.formItem}
>
<DatePicker
showTime
format="MM/DD HH:mm"
placeholder="年/月/日 --:--"
className={styles.dateInput}
/>
</Form.Item>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddTask}
className={styles.addButton}
loading={addLoading}
block
>
</Button>
</Form>
</div>
{/* 任务列表 */}
<div className={styles.todoList}>
<List
dataSource={todos}
loading={loading}
renderItem={todo => (
<List.Item className={styles.todoItem}>
<div className={styles.todoContent}>
<div className={styles.todoHeader}>
<Checkbox
checked={todo.completed}
onChange={() => handleToggleComplete(todo.id)}
className={styles.todoCheckbox}
/>
<Text
className={`${styles.todoTitle} ${todo.completed ? styles.completed : ""}`}
>
{todo.title}
</Text>
</div>
{todo.description && (
<div className={styles.todoDescription}>
<Text className={styles.descriptionText}>
{todo.description}
</Text>
</div>
)}
<div className={styles.todoFooter}>
<Space>
<Text className={styles.clientInfo}>
:{todo.client}
</Text>
<Tag
color={getPriorityColor(todo.priority)}
className={styles.priorityTag}
>
{todo.priority}
</Tag>
<Space className={styles.dueDate}>
<CalendarOutlined className={styles.calendarIcon} />
<Text className={styles.dueDateText}>
{todo.dueDate}
</Text>
</Space>
</Space>
</div>
</div>
</List.Item>
)}
/>
</div>
</div>
</Modal>
);
};
export default TodoListModal;

View File

@@ -3,9 +3,10 @@ import { Layout, Button, Avatar, Space, Tooltip, Dropdown } from "antd";
import {
UserOutlined,
TeamOutlined,
InfoCircleOutlined,
RobotOutlined,
DownOutlined,
BellOutlined,
CheckSquareOutlined,
} from "@ant-design/icons";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import styles from "./ChatWindow.module.scss";
@@ -13,6 +14,8 @@ import styles from "./ChatWindow.module.scss";
import ProfileCard from "./components/ProfileCard";
import MessageEnter from "./components/MessageEnter";
import MessageRecord from "./components/MessageRecord";
import FollowupReminderModal from "./components/FollowupReminderModal";
import TodoListModal from "./components/TodoListModal";
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
import { useWeChatStore } from "@/store/module/weChat/weChat";
const { Header, Content } = Layout;
@@ -21,6 +24,12 @@ interface ChatWindowProps {
contract: ContractData | weChatGroup;
}
const typeOptions = [
{ value: 0, label: "人工接待" },
{ value: 1, label: "AI辅助" },
{ value: 2, label: "AI接管" },
];
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
const updateAiQuoteMessageContent = useWeChatStore(
state => state.updateAiQuoteMessageContent,
@@ -29,15 +38,28 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
state => state.aiQuoteMessageContent,
);
const [showProfile, setShowProfile] = useState(true);
const [followupModalVisible, setFollowupModalVisible] = useState(false);
const [todoModalVisible, setTodoModalVisible] = useState(false);
const onToggleProfile = () => {
setShowProfile(!showProfile);
};
const typeOptions = [
{ value: 0, label: "人工接待" },
{ value: 1, label: "AI辅助" },
{ value: 2, label: "AI接管" },
];
const handleFollowupClick = () => {
setFollowupModalVisible(true);
};
const handleFollowupModalClose = () => {
setFollowupModalVisible(false);
};
const handleTodoClick = () => {
setTodoModalVisible(true);
};
const handleTodoModalClose = () => {
setTodoModalVisible(false);
};
const [currentConfig, setCurrentConfig] = useState(
typeOptions.find(option => option.value === aiQuoteMessageContent),
@@ -107,15 +129,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
)}
<Tooltip title="个人资料">
<Button
onClick={onToggleProfile}
type="text"
icon={<InfoCircleOutlined />}
className={styles.headerButton}
/>
<Button onClick={onToggleProfile} icon={<UserOutlined />}>
</Button>
</Tooltip>
</Space>
</Header>
<div className={styles.extend}>
<Button icon={<BellOutlined />} onClick={handleFollowupClick}>
</Button>
<Button icon={<CheckSquareOutlined />} onClick={handleTodoClick}>
</Button>
</div>
{/* 聊天内容 */}
<Content className={styles.chatContent}>
@@ -127,10 +154,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
</Layout>
{/* 右侧个人资料卡片 */}
<ProfileCard
contract={contract}
showProfile={showProfile}
onToggleProfile={onToggleProfile}
{showProfile && <ProfileCard contract={contract} />}
{/* 跟进提醒模态框 */}
<FollowupReminderModal
visible={followupModalVisible}
onClose={handleFollowupModalClose}
recipientName={contract.nickname || contract.name}
friendId={contract.id?.toString()}
/>
{/* 待办事项模态框 */}
<TodoListModal
visible={todoModalVisible}
onClose={handleTodoModalClose}
clientName={contract.nickname || contract.name}
friendId={contract.id?.toString()}
/>
</Layout>
);

View File

@@ -5,8 +5,12 @@ import {
asyncWeChatGroup,
asyncCountLables,
useCkChatStore,
updateIsLoadWeChat,
getIsLoadWeChat,
} from "@/store/module/ckchat/ckchat";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { useUserStore } from "@/store/module/user";
import { weChatGroupService, contractService } from "@/utils/db";
import {
loginWithToken,
@@ -14,38 +18,41 @@ import {
getContactList,
getGroupList,
getAgentList,
getLabelsListByGroup,
} from "./api";
import { useUserStore } from "@/store/module/user";
import {
KfUserListData,
ContractData,
weChatGroup,
} from "@/pages/pc/ckbox/data";
import { WechatGroup } from "./api";
const { login2 } = useUserStore.getState();
//获取触客宝基础信息
export const chatInitAPIdata = async () => {
try {
//获取联系人列表
const contractList = await getAllContactList();
let contractList = [];
let groupList = [];
//获取联系人列表
asyncContractList(contractList);
if (getIsLoadWeChat()) {
//获取联系人列表
contractList = await contractService.findAll();
//获取群列表
groupList = await weChatGroupService.findAll();
} else {
//获取联系人列表
contractList = await getAllContactList();
//获取群列表
const groupList = await getAllGroupList();
//获取群列表
groupList = await getAllGroupList();
updateIsLoadWeChat(true);
}
//获取联系人列表
await asyncContractList(contractList);
await asyncWeChatGroup(groupList);
// 提取不重复的wechatAccountId组
const uniqueWechatAccountIds: number[] = getUniqueWechatAccountIds(
contractList,
groupList,
);
//获取控制终端列表
const kfUserList: KfUserListData[] = await getAgentList();
@@ -53,8 +60,8 @@ export const chatInitAPIdata = async () => {
await asyncKfUserList(kfUserList);
//获取标签列表
// const countLables = await getCountLables();
// await asyncCountLables(countLables);
const countLables = await getCountLables();
await asyncCountLables(countLables);
//获取消息会话列表并按lastUpdateTime排序
const filterUserSessions = contractList?.filter(
@@ -121,15 +128,9 @@ export const initSocket = () => {
};
export const getCountLables = async () => {
const LablesRes = await Promise.all(
[1, 2].map(item =>
WechatGroup({
groupType: item,
}),
),
);
const [friend, group] = LablesRes;
const countLables = [
const Result = await getLabelsListByGroup({});
const LablesRes = Result.list;
return [
...[
{
id: 0,
@@ -137,8 +138,7 @@ export const getCountLables = async () => {
groupType: 2,
},
],
...group,
...friend,
...LablesRes,
...[
{
id: 0,
@@ -147,8 +147,6 @@ export const getCountLables = async () => {
},
],
];
return countLables;
};
/**
* 根据标签组织联系人

View File

@@ -2,21 +2,22 @@ import CkboxPage from "@/pages/pc/ckbox";
import WeChatPage from "@/pages/pc/ckbox/weChat";
import Dashboard from "@/pages/pc/ckbox/dashboard";
import PowerCenter from "@/pages/pc/ckbox/powerCenter";
import PrecisionSend from "@/pages/pc/ckbox/powerCenter/precision-send";
import SopSend from "@/pages/pc/ckbox/powerCenter/sop-send";
import MomentsMarketing from "@/pages/pc/ckbox/powerCenter/moments-marketing";
import TagManagement from "@/pages/pc/ckbox/powerCenter/tag-management";
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";
const ckboxRoutes = [
{
path: "/pc",
element: <CkboxPage />,
auth: true,
children: [
{
path: "commonConfig",
element: <CommonConfig />,
},
{
path: "dashboard",
element: <Dashboard />,
@@ -29,22 +30,6 @@ const ckboxRoutes = [
path: "powerCenter",
element: <PowerCenter />,
},
{
path: "powerCenter/precision-send",
element: <PrecisionSend />,
},
{
path: "powerCenter/sop-send",
element: <SopSend />,
},
{
path: "powerCenter/moments-marketing",
element: <MomentsMarketing />,
},
{
path: "powerCenter/tag-management",
element: <TagManagement />,
},
{
path: "powerCenter/customer-management",
element: <CustomerManagement />,

View File

@@ -34,6 +34,9 @@ export interface CkChatState {
userInfo: CkUserInfo | null;
isLoggedIn: boolean;
searchKeyword: string;
isLoadWeChat: boolean;
getIsLoadWeChat: () => boolean;
updateIsLoadWeChat: (isLoadWeChat: boolean) => void;
contractList: ContractData[];
chatSessions: any[];
kfUserList: KfUserListData[];

View File

@@ -24,16 +24,22 @@ export const useCkChatStore = createPersistStore<CkChatState>(
newContractList: [], //联系人分组
kfSelected: 0, //选中的客服
searchKeyword: "", //搜索关键词
isLoadWeChat: false, //是否加载微信
getIsLoadWeChat: () => {
return useCkChatStore.getState().isLoadWeChat;
},
updateIsLoadWeChat: (isLoadWeChat: boolean) => {
console.log("updateIsLoadWeChat", isLoadWeChat);
set({ isLoadWeChat });
},
//客服列表
asyncKfUserList: async data => {
set({ kfUserList: data });
// await kfUserService.createManyWithServerId(data);
},
// 获取客服列表
getkfUserList: async () => {
const state = useCkChatStore.getState();
return state.kfUserList;
// return await kfUserService.findAll();
},
// 异步设置标签列表
asyncCountLables: async (data: ContactGroupByLabel[]) => {
@@ -545,3 +551,7 @@ export const searchContactsAndGroups = () =>
export const pinChatSessionToTop = (sessionId: number) =>
useCkChatStore.getState().pinChatSessionToTop(sessionId);
useCkChatStore.getState().getKfSelectedUser();
export const updateIsLoadWeChat = (isLoadWeChat: boolean) =>
useCkChatStore.getState().updateIsLoadWeChat(isLoadWeChat);
export const getIsLoadWeChat = () =>
useCkChatStore.getState().getIsLoadWeChat();

View File

@@ -315,3 +315,11 @@ button {
flex: 1;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: white;
border-top: 1px solid #f0f0f0;
}