Merge branch 'yongpxu-dev' into develop
This commit is contained in:
@@ -602,7 +602,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
<div className={style["link-label"]}>小程序链接</div>
|
<div className={style["link-label"]}>小程序链接</div>
|
||||||
<div className={style["link-input-wrapper"]}>
|
<div className={style["link-input-wrapper"]}>
|
||||||
<Input
|
<Input
|
||||||
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`}
|
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`}
|
||||||
readOnly
|
readOnly
|
||||||
className={style["link-input"]}
|
className={style["link-input"]}
|
||||||
placeholder="小程序链接"
|
placeholder="小程序链接"
|
||||||
@@ -610,7 +610,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
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);
|
navigator.clipboard.writeText(link);
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: "链接已复制到剪贴板",
|
content: "链接已复制到剪贴板",
|
||||||
|
|||||||
@@ -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设备上正常运行。
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
## 使用技术栈
|
|
||||||
- React 18
|
|
||||||
- TypeScript
|
|
||||||
- Vite(新一代前端构建工具)
|
|
||||||
- axios
|
|
||||||
- sass (scss)
|
|
||||||
- React Router v6
|
|
||||||
- antd-mobile
|
|
||||||
- antd(已设置基础单位为 rem,配合 postcss-pxtorem)
|
|
||||||
- postcss-pxtorem(px 转 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 代码规范配置
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.headerLeft {
|
.headerLeft {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface PowerNavigationProps {
|
|||||||
onBackClick?: () => void;
|
onBackClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
rightContent?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PowerNavigation: React.FC<PowerNavigationProps> = ({
|
const PowerNavigation: React.FC<PowerNavigationProps> = ({
|
||||||
@@ -23,6 +24,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
|
|||||||
onBackClick,
|
onBackClick,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
rightContent,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ const PowerNavigation: React.FC<PowerNavigationProps> = ({
|
|||||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.headerRight}>{rightContent}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
243
Touchkebao/src/components/Upload/AudioUpload/index.module.scss
Normal file
243
Touchkebao/src/components/Upload/AudioUpload/index.module.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
385
Touchkebao/src/components/Upload/AudioUpload/index.tsx
Normal file
385
Touchkebao/src/components/Upload/AudioUpload/index.tsx
Normal 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}>
|
||||||
|
支持 MP3、WAV、AAC 等格式,最大 {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;
|
||||||
@@ -176,12 +176,17 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
} else if (info.file.status === "done") {
|
} else if (info.file.status === "done") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
message.success("文件上传成功!");
|
|
||||||
|
|
||||||
// 从响应中获取上传后的URL
|
// 从响应中获取上传后的URL
|
||||||
let uploadedUrl = "";
|
let uploadedUrl = "";
|
||||||
|
|
||||||
if (info.file.response) {
|
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") {
|
if (typeof info.file.response === "string") {
|
||||||
uploadedUrl = info.file.response;
|
uploadedUrl = info.file.response;
|
||||||
} else if (info.file.response.data) {
|
} else if (info.file.response.data) {
|
||||||
@@ -195,6 +200,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
|
message.success("文件上传成功!");
|
||||||
if (maxCount === 1) {
|
if (maxCount === 1) {
|
||||||
// 单个文件模式
|
// 单个文件模式
|
||||||
onChange?.(uploadedUrl);
|
onChange?.(uploadedUrl);
|
||||||
@@ -208,6 +214,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
const newUrls = [...currentUrls, uploadedUrl];
|
const newUrls = [...currentUrls, uploadedUrl];
|
||||||
onChange?.(newUrls);
|
onChange?.(newUrls);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
message.error("上传失败,未获取到文件URL");
|
||||||
}
|
}
|
||||||
} else if (info.file.status === "error") {
|
} else if (info.file.status === "error") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -108,12 +108,17 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
} else if (info.file.status === "done") {
|
} else if (info.file.status === "done") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
message.success("视频上传成功!");
|
|
||||||
|
|
||||||
// 从响应中获取上传后的URL
|
// 从响应中获取上传后的URL
|
||||||
let uploadedUrl = "";
|
let uploadedUrl = "";
|
||||||
|
|
||||||
if (info.file.response) {
|
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") {
|
if (typeof info.file.response === "string") {
|
||||||
uploadedUrl = info.file.response;
|
uploadedUrl = info.file.response;
|
||||||
} else if (info.file.response.data) {
|
} else if (info.file.response.data) {
|
||||||
@@ -127,6 +132,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
|
message.success("视频上传成功!");
|
||||||
if (maxCount === 1) {
|
if (maxCount === 1) {
|
||||||
// 单个视频模式
|
// 单个视频模式
|
||||||
onChange?.(uploadedUrl);
|
onChange?.(uploadedUrl);
|
||||||
@@ -140,6 +146,8 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
const newUrls = [...currentUrls, uploadedUrl];
|
const newUrls = [...currentUrls, uploadedUrl];
|
||||||
onChange?.(newUrls);
|
onChange?.(newUrls);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
message.error("上传失败,未获取到视频URL");
|
||||||
}
|
}
|
||||||
} else if (info.file.status === "error") {
|
} else if (info.file.status === "error") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const IndexPage: React.FC = () => {
|
|||||||
if (isMobile()) {
|
if (isMobile()) {
|
||||||
navigate("/mobile/dashboard");
|
navigate("/mobile/dashboard");
|
||||||
} else {
|
} else {
|
||||||
navigate("/pc/dashboard");
|
navigate("/pc/weChat");
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export function clearUnreadCount1(params) {
|
|||||||
return request("/v1/kefu/message/readMessage", params, "GET");
|
return request("/v1/kefu/message/readMessage", params, "GET");
|
||||||
}
|
}
|
||||||
export function clearUnreadCount2(params) {
|
export function clearUnreadCount2(params) {
|
||||||
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
return request2("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
//更新配置
|
//更新配置
|
||||||
export function updateConfig(params) {
|
export function updateConfig(params) {
|
||||||
return request("/api/WechatFriend/updateConfig", params, "PUT");
|
return request2("/api/WechatFriend/updateConfig", params, "PUT");
|
||||||
}
|
}
|
||||||
//获取聊天记录-2 获取列表
|
//获取聊天记录-2 获取列表
|
||||||
export function getChatMessages(params: {
|
export function getChatMessages(params: {
|
||||||
@@ -44,7 +44,7 @@ export function getChatMessages(params: {
|
|||||||
Count: number;
|
Count: number;
|
||||||
olderData: boolean;
|
olderData: boolean;
|
||||||
}) {
|
}) {
|
||||||
return request("/api/FriendMessage/SearchMessage", params, "GET");
|
return request2("/api/FriendMessage/SearchMessage", params, "GET");
|
||||||
}
|
}
|
||||||
export function getChatroomMessages(params: {
|
export function getChatroomMessages(params: {
|
||||||
wechatAccountId: number;
|
wechatAccountId: number;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
78
Touchkebao/src/pages/pc/ckbox/commonConfig/index.module.scss
Normal file
78
Touchkebao/src/pages/pc/ckbox/commonConfig/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
70
Touchkebao/src/pages/pc/ckbox/commonConfig/index.tsx
Normal file
70
Touchkebao/src/pages/pc/ckbox/commonConfig/index.tsx
Normal 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;
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
gap: 26px;
|
||||||
.suanli {
|
.suanli {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -131,10 +132,20 @@
|
|||||||
|
|
||||||
.userSection {
|
.userSection {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
cursor: pointer;
|
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 {
|
.userNickname {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Layout, Drawer, Avatar, Space, Button, Badge, Dropdown } from "antd";
|
import { Layout, Drawer, Avatar, Space, Button, Badge, Dropdown } from "antd";
|
||||||
import {
|
import {
|
||||||
MenuOutlined,
|
BarChartOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
@@ -90,24 +90,20 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
<>
|
<>
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<div className={styles.headerLeft}>
|
<div className={styles.headerLeft}>
|
||||||
{!isWeChat() ? (
|
<Button
|
||||||
<Button
|
icon={<BarChartOutlined />}
|
||||||
type="text"
|
type={!isWeChat() ? "primary" : "default"}
|
||||||
size="large"
|
onClick={handleMenuClick}
|
||||||
icon={<WechatOutlined />}
|
>
|
||||||
onClick={handleMenuClick}
|
功能中心
|
||||||
className={styles.menuButton}
|
</Button>
|
||||||
/>
|
<Button
|
||||||
) : (
|
icon={<WechatOutlined />}
|
||||||
<Button
|
type={isWeChat() ? "primary" : "default"}
|
||||||
type="text"
|
onClick={handleMenuClick}
|
||||||
size="large"
|
>
|
||||||
icon={<MenuOutlined />}
|
Ai智能客服
|
||||||
onClick={handleMenuClick}
|
</Button>
|
||||||
className={styles.menuButton}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={styles.title}>{title}</span>
|
<span className={styles.title}>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,9 +120,14 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
<BellOutlined style={{ fontSize: 20 }} />
|
<BellOutlined style={{ fontSize: 20 }} />
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.messageButton}>
|
<Button
|
||||||
<SettingOutlined style={{ fontSize: 20 }} />
|
onClick={() => {
|
||||||
</div>
|
navigate("/pc/commonConfig");
|
||||||
|
}}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
>
|
||||||
|
全局配置
|
||||||
|
</Button>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{ items: userMenuItems }}
|
menu={{ items: userMenuItems }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
@@ -139,6 +140,11 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
src={user?.avatar}
|
src={user?.avatar}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.userInfo2}>
|
||||||
|
<div className={styles.userNickname}>{user.username}</div>
|
||||||
|
<div className={styles.userAccount}>高级客服专员</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import { Outlet } from "react-router-dom";
|
|||||||
import NavCommon from "./components/NavCommon";
|
import NavCommon from "./components/NavCommon";
|
||||||
const CkboxPage: React.FC = () => {
|
const CkboxPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout header={<NavCommon title="触客宝" />}>
|
||||||
header={
|
|
||||||
<NavCommon title="AI自动聊天,懂业务,会引导,客户不停地聊不停" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,41 +3,426 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 头部样式
|
||||||
.header {
|
.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;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
margin-bottom: 24px;
|
||||||
height: 300px;
|
padding: 16px 0;
|
||||||
background: #fafafa;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
border-radius: 6px;
|
.headerLeft {
|
||||||
|
.computeBalance {
|
||||||
p {
|
display: flex;
|
||||||
font-size: 16px;
|
align-items: center;
|
||||||
color: #8c8c8c;
|
gap: 8px;
|
||||||
margin: 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
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 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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<PowerNavigation
|
<PowerNavigation
|
||||||
title="AI模型训练"
|
title="AI模型训练"
|
||||||
subtitle="自定义AI模型训练,打造专属智能客服助手"
|
subtitle="训练和优化AI模型,提升智能服务质量"
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
backButtonText="返回功能中心"
|
backButtonText="返回功能中心"
|
||||||
/>
|
/>
|
||||||
<div className={styles.content}>
|
|
||||||
{/* 功能内容待开发 */}
|
<div className={styles.header}>
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.headerLeft}>
|
||||||
<p>AI模型训练功能正在开发中...</p>
|
<div className={styles.computeBalance}>
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
<span>算力余额: 9307.423</span>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,41 +3,268 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 头部区域样式
|
||||||
.header {
|
.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;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
margin-bottom: 24px;
|
||||||
height: 300px;
|
padding-bottom: 16px;
|
||||||
background: #fafafa;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
border-radius: 6px;
|
.headerLeft {
|
||||||
|
display: flex;
|
||||||
p {
|
align-items: center;
|
||||||
font-size: 16px;
|
gap: 16px;
|
||||||
color: #8c8c8c;
|
|
||||||
margin: 0;
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,344 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import PowerNavigation from "@/components/PowerNavtion";
|
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";
|
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 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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<PowerNavigation
|
{/* 头部区域 */}
|
||||||
title="自动打招呼"
|
<div className={styles.header}>
|
||||||
subtitle="智能识别新好友,自动发送个性化欢迎消息"
|
<div className={styles.headerLeft}>
|
||||||
showBackButton={true}
|
<Button
|
||||||
backButtonText="返回功能中心"
|
type="text"
|
||||||
/>
|
size="large"
|
||||||
<div className={styles.content}>
|
icon={<ArrowLeftOutlined />}
|
||||||
{/* 功能内容待开发 */}
|
onClick={() => navigate(-1)}
|
||||||
<div className={styles.placeholder}>
|
className={styles.backButton}
|
||||||
<p>自动打招呼功能正在开发中...</p>
|
>
|
||||||
|
返回功能中心
|
||||||
|
</Button>
|
||||||
|
<div className={styles.titleSection}>
|
||||||
|
<h1>自动问候</h1>
|
||||||
|
<p>设置智能问候规则,提升客户体验</p>
|
||||||
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,39 +5,289 @@
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
// 导出按钮样式
|
||||||
margin-bottom: 24px;
|
.exportButton {
|
||||||
|
height: 36px;
|
||||||
h1 {
|
border-radius: 6px;
|
||||||
font-size: 24px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
min-height: 400px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 300px;
|
width: 28px;
|
||||||
background: #fafafa;
|
height: 28px;
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
border-radius: 6px;
|
.viewIcon {
|
||||||
|
font-size: 14px;
|
||||||
p {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #8c8c8c;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
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 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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<PowerNavigation
|
<PowerNavigation
|
||||||
title="沟通记录"
|
title="沟通记录"
|
||||||
subtitle="完整记录客户沟通历史,支持多维度查询分析"
|
subtitle="查看和管理所有客户沟通记录"
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
backButtonText="返回功能中心"
|
backButtonText="返回功能中心"
|
||||||
|
rightContent={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={handleExportRecords}
|
||||||
|
className={styles.exportButton}
|
||||||
|
>
|
||||||
|
导出记录
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 顶部搜索和筛选区域 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.headerSection}>
|
||||||
<p>沟通记录功能正在开发中...</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 管理组件统一导出
|
||||||
|
export { default as MaterialManagement } from "./MaterialManagement";
|
||||||
|
export { default as SensitiveWordManagement } from "./SensitiveWordManagement";
|
||||||
|
export { default as KeywordManagement } from "./KeywordManagement";
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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";
|
||||||
@@ -1,31 +1,152 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fff;
|
background: #f5f5f5;
|
||||||
border-radius: 8px;
|
min-height: 100vh;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.headerActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
h1 {
|
:global(.ant-btn) {
|
||||||
font-size: 24px;
|
height: 36px;
|
||||||
font-weight: 600;
|
border-radius: 6px;
|
||||||
color: #262626;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
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 {
|
.content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
padding: 24px;
|
||||||
min-height: 400px;
|
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 {
|
.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -34,10 +155,452 @@
|
|||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
border: 1px dashed #d9d9d9;
|
border: 1px dashed #d9d9d9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
p {
|
color: #8c8c8c;
|
||||||
font-size: 16px;
|
}
|
||||||
color: #8c8c8c;
|
|
||||||
margin: 0;
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
import {
|
||||||
|
MaterialManagement,
|
||||||
|
SensitiveWordManagement,
|
||||||
|
KeywordManagement,
|
||||||
|
MaterialModal,
|
||||||
|
SensitiveWordModal,
|
||||||
|
KeywordModal,
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
const ContentManagement: React.FC = () => {
|
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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<PowerNavigation
|
<PowerNavigation
|
||||||
title="内容管理"
|
title="内容管理"
|
||||||
subtitle="素材管理、数据词汇库、关键词自动回复"
|
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
backButtonText="返回功能中心"
|
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.tabsSection}>
|
||||||
<div className={styles.placeholder}>
|
<br />
|
||||||
<p>内容管理功能正在开发中...</p>
|
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,43 +1,355 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
.searchBar {
|
||||||
.header {
|
display: flex;
|
||||||
margin-bottom: 24px;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
h1 {
|
padding: 16px 0 8px 0;
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #262626;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
// 页面头部
|
||||||
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
justify-content: center;
|
align-items: flex-start;
|
||||||
height: 300px;
|
margin-bottom: 24px;
|
||||||
background: #fafafa;
|
|
||||||
border: 1px dashed #d9d9d9;
|
.headerLeft {
|
||||||
border-radius: 6px;
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
p {
|
font-weight: 600;
|
||||||
font-size: 16px;
|
color: #262626;
|
||||||
color: #8c8c8c;
|
margin: 0 0 8px 0;
|
||||||
margin: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,331 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import PowerNavigation from "@/components/PowerNavtion";
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import styles from "./index.module.scss";
|
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 = () => {
|
const getAvatarColor = (name: string) => {
|
||||||
return (
|
const colors = [
|
||||||
<div className={styles.container}>
|
"#1890ff",
|
||||||
<PowerNavigation
|
"#52c41a",
|
||||||
title="客户好友管理"
|
"#faad14",
|
||||||
subtitle="统一管理客户信息和好友关系,提升服务效率"
|
"#f5222d",
|
||||||
showBackButton={true}
|
"#722ed1",
|
||||||
backButtonText="返回功能中心"
|
"#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>
|
return (
|
||||||
</div>
|
<div
|
||||||
</div>
|
className={styles.avatarPlaceholder}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
backgroundColor: getAvatarColor(name),
|
||||||
|
fontSize: size * 0.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getInitials(name)}
|
||||||
</div>
|
</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;
|
export default CustomerManagement;
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
AimOutlined,
|
AimOutlined,
|
||||||
SendOutlined,
|
ThunderboltOutlined,
|
||||||
CalendarOutlined,
|
RiseOutlined,
|
||||||
TagsOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
RobotOutlined,
|
|
||||||
UserAddOutlined,
|
|
||||||
AppstoreOutlined,
|
|
||||||
SoundOutlined,
|
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
FolderOutlined,
|
CommentOutlined,
|
||||||
BarChartOutlined,
|
FileTextOutlined,
|
||||||
|
SoundOutlined,
|
||||||
|
EditOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
|
// 数据类型定义
|
||||||
export interface FeatureCard {
|
export interface FeatureCard {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -24,96 +18,87 @@ export interface FeatureCard {
|
|||||||
color: string;
|
color: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
|
isHot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabItem {
|
export interface FeatureCategory {
|
||||||
key: string;
|
id: string;
|
||||||
label: string;
|
title: string;
|
||||||
icon?: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
active?: boolean;
|
color: string;
|
||||||
|
count: number;
|
||||||
|
features: FeatureCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const featureCards: FeatureCard[] = [
|
// 功能数据
|
||||||
|
export const featureCategories: FeatureCategory[] = [
|
||||||
{
|
{
|
||||||
id: "precision-send",
|
id: "core",
|
||||||
title: "精准群发",
|
title: "核心功能",
|
||||||
description: "基于客户标签和行为数据进行精准群发",
|
icon: <AimOutlined style={{ fontSize: "24px" }} />,
|
||||||
icon: <AimOutlined />,
|
color: "#1890ff",
|
||||||
color: "#ff6b35",
|
count: 2,
|
||||||
path: "/pc/powerCenter/precision-send",
|
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",
|
id: "ai",
|
||||||
title: "SOP群发",
|
title: "AI智能功能",
|
||||||
description: "使用触客宝SOP标准化流程进行批量消息发送",
|
icon: <ThunderboltOutlined style={{ fontSize: "24px" }} />,
|
||||||
icon: <SendOutlined />,
|
color: "#722ed1",
|
||||||
color: "#4285f4",
|
count: 2,
|
||||||
path: "/pc/powerCenter/sop-send",
|
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",
|
id: "marketing",
|
||||||
title: "朋友圈营销",
|
title: "营销管理",
|
||||||
description: "AI智能生成朋友圈内容,提升品牌曝光度",
|
icon: <RiseOutlined style={{ fontSize: "24px" }} />,
|
||||||
icon: <CalendarOutlined />,
|
color: "#52c41a",
|
||||||
color: "#34a853",
|
count: 1,
|
||||||
path: "/pc/powerCenter/moments-marketing",
|
features: [
|
||||||
},
|
{
|
||||||
{
|
id: "content-management",
|
||||||
id: "tag-management",
|
title: "内容管理",
|
||||||
title: "标签管理",
|
description: "管理营销内容,素材库,提升内容创作效率",
|
||||||
description: "智能客户标签分类,精准用户画像分析",
|
icon: <EditOutlined style={{ fontSize: "24px" }} />,
|
||||||
icon: <TagsOutlined />,
|
color: "#722ed1",
|
||||||
color: "#9c27b0",
|
path: "/pc/powerCenter/content-management",
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
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 /> },
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,127 +1,176 @@
|
|||||||
.powerCenter {
|
.powerCenter {
|
||||||
padding: 24px;
|
padding: 40px;
|
||||||
background-color: #f5f5f5;
|
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
.header {
|
// 功能分类区域
|
||||||
display: flex;
|
.categorySection {
|
||||||
justify-content: space-between;
|
margin-bottom: 48px;
|
||||||
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);
|
|
||||||
|
|
||||||
.headerLeft {
|
.categoryHeader {
|
||||||
.title {
|
display: flex;
|
||||||
font-size: 28px;
|
align-items: center;
|
||||||
font-weight: 600;
|
margin-bottom: 24px;
|
||||||
color: #1a1a1a;
|
padding: 0 8px;
|
||||||
margin: 0 0 8px 0;
|
|
||||||
|
.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 {
|
.categoryInfo {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
color: #666;
|
align-items: center;
|
||||||
margin: 0;
|
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 {
|
.featureCards {
|
||||||
.tabs {
|
.featureCard {
|
||||||
.tab {
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
border: none;
|
||||||
border: 1px solid #e0e0e0;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
color: #666;
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1890ff;
|
transform: translateY(-2px);
|
||||||
border-color: #1890ff;
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.activeTab {
|
.cardContent {
|
||||||
border-radius: 8px;
|
.cardHeader {
|
||||||
background: #1890ff;
|
position: relative;
|
||||||
border-color: #1890ff;
|
display: flex;
|
||||||
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
|
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 {
|
.footer {
|
||||||
border-radius: 16px;
|
text-align: center;
|
||||||
border: none;
|
margin-top: 60px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
padding: 24px 0;
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 200px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
p {
|
||||||
transform: translateY(-4px);
|
font-size: 16px;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
color: #666;
|
||||||
}
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,16 +178,65 @@
|
|||||||
// 响应式设计
|
// 响应式设计
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.powerCenter {
|
.powerCenter {
|
||||||
.header {
|
padding: 32px 24px;
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
.headerRight {
|
.categorySection {
|
||||||
width: 100%;
|
.categoryHeader {
|
||||||
|
.categoryIcon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
|
||||||
.tabs {
|
.anticon {
|
||||||
flex-wrap: wrap;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.powerCenter {
|
.powerCenter {
|
||||||
padding: 16px;
|
padding: 24px 16px;
|
||||||
|
|
||||||
.header {
|
.categorySection {
|
||||||
padding: 16px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
.headerLeft {
|
.categoryHeader {
|
||||||
.title {
|
.categoryIcon {
|
||||||
font-size: 24px;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardGrid {
|
.categoryInfo {
|
||||||
.featureCard {
|
.categoryTitle {
|
||||||
height: 160px;
|
font-size: 20px;
|
||||||
|
|
||||||
.cardContent {
|
|
||||||
.iconWrapper {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardInfo {
|
.categoryCount {
|
||||||
.cardTitle {
|
font-size: 12px;
|
||||||
font-size: 14px;
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.featureCards {
|
||||||
|
.featureCard {
|
||||||
|
height: 160px;
|
||||||
|
width: 240px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
.badge {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardDescription {
|
.cardIcon {
|
||||||
font-size: 11px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
// 卡片颜色主题
|
|
||||||
.featureCard {
|
p {
|
||||||
// 精准群发 - 橙色
|
font-size: 14px;
|
||||||
&[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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import React, { useState, useMemo } from "react";
|
import React from "react";
|
||||||
import { Card, Row, Col, Button, Space } from "antd";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { featureCards, tabItems, FeatureCard } from "./index.data";
|
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
import { FeatureCard, featureCategories } from "./index.data.tsx";
|
||||||
|
import { Col, Row } from "antd";
|
||||||
const PowerCenter: React.FC = () => {
|
const PowerCenter: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState("all");
|
|
||||||
|
|
||||||
const handleCardClick = (card: FeatureCard) => {
|
const handleCardClick = (card: FeatureCard) => {
|
||||||
if (card.path) {
|
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 (
|
return (
|
||||||
<div className={styles.powerCenter}>
|
<div className={styles.powerCenter}>
|
||||||
{/* 页面头部 */}
|
{/* 功能分类展示 */}
|
||||||
<div className={styles.header}>
|
{featureCategories.map(category => (
|
||||||
<div className={styles.headerLeft}>
|
<div key={category.id} className={styles.categorySection}>
|
||||||
<h1 className={styles.title}>功能中心</h1>
|
{/* 分类标题 */}
|
||||||
<p className={styles.subtitle}>触客宝AI私域营销系统 - 所有功能一览</p>
|
<div className={styles.categoryHeader}>
|
||||||
</div>
|
<div
|
||||||
<div className={styles.headerRight}>
|
className={styles.categoryIcon}
|
||||||
<Space className={styles.tabs}>
|
style={{ backgroundColor: category.color }}
|
||||||
{tabItems.map(item => (
|
>
|
||||||
<Button
|
{category.icon}
|
||||||
key={item.key}
|
</div>
|
||||||
type={activeTab === item.key ? "primary" : "text"}
|
<div className={styles.categoryInfo}>
|
||||||
className={
|
<h2 className={styles.categoryTitle}>{category.title}</h2>
|
||||||
activeTab === item.key ? styles.activeTab : styles.tab
|
<span
|
||||||
}
|
className={styles.categoryCount}
|
||||||
onClick={() => handleTabClick(item.key)}
|
style={{ backgroundColor: category.color, color: "#ffffff" }}
|
||||||
icon={item.icon}
|
|
||||||
>
|
>
|
||||||
{item.label}
|
{category.count}个功能
|
||||||
</Button>
|
</span>
|
||||||
))}
|
</div>
|
||||||
</Space>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 功能卡片网格 */}
|
{/* 功能卡片 */}
|
||||||
<div className={styles.cardGrid}>
|
<div className={styles.featureCards}>
|
||||||
<Row gutter={[24, 24]}>
|
<Row gutter={16}>
|
||||||
{filteredCards.map(card => (
|
{category.features.map(card => (
|
||||||
<Col key={card.id} xs={24} sm={12} md={8} lg={6} xl={6}>
|
<Col span={8} key={card.id}>
|
||||||
<Card
|
|
||||||
className={styles.featureCard}
|
|
||||||
hoverable
|
|
||||||
onClick={() => handleCardClick(card)}
|
|
||||||
bodyStyle={{ padding: "24px" }}
|
|
||||||
>
|
|
||||||
<div className={styles.cardContent}>
|
|
||||||
<div
|
<div
|
||||||
className={styles.iconWrapper}
|
key={card.id}
|
||||||
style={{ backgroundColor: card.color }}
|
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>
|
||||||
<div className={styles.cardInfo}>
|
</Col>
|
||||||
<h3 className={styles.cardTitle}>
|
))}
|
||||||
{card.title}
|
</Row>
|
||||||
{card.isNew && (
|
</div>
|
||||||
<span className={styles.newBadge}>新功能</span>
|
</div>
|
||||||
)}
|
))}
|
||||||
</h3>
|
|
||||||
<p className={styles.cardDescription}>{card.description}</p>
|
{/* 页面底部 */}
|
||||||
</div>
|
<div className={styles.footer}>
|
||||||
</div>
|
<p>触客宝 AI私域营销系统 - 让每一次沟通都更有价值</p>
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -37,6 +37,11 @@ export function getContactList(params) {
|
|||||||
export function getGroupList(params) {
|
export function getGroupList(params) {
|
||||||
return request("/v1/kefu/wechatChatroom/list", params, "GET");
|
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 }) => {
|
// export const getContactList = (params: { prevId: number; count: number }) => {
|
||||||
@@ -316,3 +321,65 @@ export const forwardMessage = (
|
|||||||
export const recallMessage = (messageId: string): Promise<void> => {
|
export const recallMessage = (messageId: string): Promise<void> => {
|
||||||
return request2(`/v1/messages/${messageId}/recall`, {}, "PUT");
|
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");
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
.extend {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatHeader {
|
.chatHeader {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -32,10 +32,9 @@ import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
|||||||
import styles from "./Person.module.scss";
|
import styles from "./Person.module.scss";
|
||||||
interface PersonProps {
|
interface PersonProps {
|
||||||
contract: ContractData | weChatGroup;
|
contract: ContractData | weChatGroup;
|
||||||
onToggleProfile?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
|
const Person: React.FC<PersonProps> = ({ contract }) => {
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
const [isEditingRemark, setIsEditingRemark] = useState(false);
|
const [isEditingRemark, setIsEditingRemark] = useState(false);
|
||||||
const [remarkValue, setRemarkValue] = useState(contract.conRemark || "");
|
const [remarkValue, setRemarkValue] = useState(contract.conRemark || "");
|
||||||
@@ -504,10 +503,6 @@ const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
|
|||||||
chatroomOperateType: 4, // 4 for quit
|
chatroomOperateType: 4, // 4 for quit
|
||||||
});
|
});
|
||||||
messageApi.success("已退出群聊");
|
messageApi.success("已退出群聊");
|
||||||
// 可能还需要一个回调来关闭侧边栏或切换到另一个聊天
|
|
||||||
if (onToggleProfile) {
|
|
||||||
onToggleProfile();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ const { Sider } = Layout;
|
|||||||
|
|
||||||
interface PersonProps {
|
interface PersonProps {
|
||||||
contract: ContractData | weChatGroup;
|
contract: ContractData | weChatGroup;
|
||||||
onToggleProfile?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
|
const Person: React.FC<PersonProps> = ({ contract }) => {
|
||||||
const [activeKey, setActiveKey] = useState("profile");
|
const [activeKey, setActiveKey] = useState("profile");
|
||||||
const isGroup = "chatroomId" in contract;
|
const isGroup = "chatroomId" in contract;
|
||||||
console.log(contract);
|
console.log(contract);
|
||||||
@@ -50,12 +49,7 @@ const Person: React.FC<PersonProps> = ({ contract, onToggleProfile }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{activeKey === "profile" && (
|
{activeKey === "profile" && <ProfileModules contract={contract} />}
|
||||||
<ProfileModules
|
|
||||||
contract={contract}
|
|
||||||
onToggleProfile={onToggleProfile}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeKey === "quickwords" && (
|
{activeKey === "quickwords" && (
|
||||||
<QuickWords
|
<QuickWords
|
||||||
words={[]}
|
words={[]}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -3,9 +3,10 @@ import { Layout, Button, Avatar, Space, Tooltip, Dropdown } from "antd";
|
|||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
InfoCircleOutlined,
|
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
|
BellOutlined,
|
||||||
|
CheckSquareOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import styles from "./ChatWindow.module.scss";
|
import styles from "./ChatWindow.module.scss";
|
||||||
@@ -13,6 +14,8 @@ import styles from "./ChatWindow.module.scss";
|
|||||||
import ProfileCard from "./components/ProfileCard";
|
import ProfileCard from "./components/ProfileCard";
|
||||||
import MessageEnter from "./components/MessageEnter";
|
import MessageEnter from "./components/MessageEnter";
|
||||||
import MessageRecord from "./components/MessageRecord";
|
import MessageRecord from "./components/MessageRecord";
|
||||||
|
import FollowupReminderModal from "./components/FollowupReminderModal";
|
||||||
|
import TodoListModal from "./components/TodoListModal";
|
||||||
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
const { Header, Content } = Layout;
|
const { Header, Content } = Layout;
|
||||||
@@ -21,6 +24,12 @@ interface ChatWindowProps {
|
|||||||
contract: ContractData | weChatGroup;
|
contract: ContractData | weChatGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ value: 0, label: "人工接待" },
|
||||||
|
{ value: 1, label: "AI辅助" },
|
||||||
|
{ value: 2, label: "AI接管" },
|
||||||
|
];
|
||||||
|
|
||||||
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
||||||
const updateAiQuoteMessageContent = useWeChatStore(
|
const updateAiQuoteMessageContent = useWeChatStore(
|
||||||
state => state.updateAiQuoteMessageContent,
|
state => state.updateAiQuoteMessageContent,
|
||||||
@@ -29,15 +38,28 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
state => state.aiQuoteMessageContent,
|
state => state.aiQuoteMessageContent,
|
||||||
);
|
);
|
||||||
const [showProfile, setShowProfile] = useState(true);
|
const [showProfile, setShowProfile] = useState(true);
|
||||||
|
const [followupModalVisible, setFollowupModalVisible] = useState(false);
|
||||||
|
const [todoModalVisible, setTodoModalVisible] = useState(false);
|
||||||
|
|
||||||
const onToggleProfile = () => {
|
const onToggleProfile = () => {
|
||||||
setShowProfile(!showProfile);
|
setShowProfile(!showProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeOptions = [
|
const handleFollowupClick = () => {
|
||||||
{ value: 0, label: "人工接待" },
|
setFollowupModalVisible(true);
|
||||||
{ value: 1, label: "AI辅助" },
|
};
|
||||||
{ value: 2, label: "AI接管" },
|
|
||||||
];
|
const handleFollowupModalClose = () => {
|
||||||
|
setFollowupModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTodoClick = () => {
|
||||||
|
setTodoModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTodoModalClose = () => {
|
||||||
|
setTodoModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
const [currentConfig, setCurrentConfig] = useState(
|
const [currentConfig, setCurrentConfig] = useState(
|
||||||
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
||||||
@@ -107,15 +129,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip title="个人资料">
|
<Tooltip title="个人资料">
|
||||||
<Button
|
<Button onClick={onToggleProfile} icon={<UserOutlined />}>
|
||||||
onClick={onToggleProfile}
|
客户信息
|
||||||
type="text"
|
</Button>
|
||||||
icon={<InfoCircleOutlined />}
|
|
||||||
className={styles.headerButton}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
|
<div className={styles.extend}>
|
||||||
|
<Button icon={<BellOutlined />} onClick={handleFollowupClick}>
|
||||||
|
跟进提醒
|
||||||
|
</Button>
|
||||||
|
<Button icon={<CheckSquareOutlined />} onClick={handleTodoClick}>
|
||||||
|
待办事项
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 聊天内容 */}
|
{/* 聊天内容 */}
|
||||||
<Content className={styles.chatContent}>
|
<Content className={styles.chatContent}>
|
||||||
@@ -127,10 +154,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{/* 右侧个人资料卡片 */}
|
{/* 右侧个人资料卡片 */}
|
||||||
<ProfileCard
|
{showProfile && <ProfileCard contract={contract} />}
|
||||||
contract={contract}
|
|
||||||
showProfile={showProfile}
|
{/* 跟进提醒模态框 */}
|
||||||
onToggleProfile={onToggleProfile}
|
<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>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import {
|
|||||||
asyncWeChatGroup,
|
asyncWeChatGroup,
|
||||||
asyncCountLables,
|
asyncCountLables,
|
||||||
useCkChatStore,
|
useCkChatStore,
|
||||||
|
updateIsLoadWeChat,
|
||||||
|
getIsLoadWeChat,
|
||||||
} from "@/store/module/ckchat/ckchat";
|
} from "@/store/module/ckchat/ckchat";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import { weChatGroupService, contractService } from "@/utils/db";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
loginWithToken,
|
loginWithToken,
|
||||||
@@ -14,38 +18,41 @@ import {
|
|||||||
getContactList,
|
getContactList,
|
||||||
getGroupList,
|
getGroupList,
|
||||||
getAgentList,
|
getAgentList,
|
||||||
|
getLabelsListByGroup,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
import { useUserStore } from "@/store/module/user";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KfUserListData,
|
KfUserListData,
|
||||||
ContractData,
|
ContractData,
|
||||||
weChatGroup,
|
weChatGroup,
|
||||||
} from "@/pages/pc/ckbox/data";
|
} from "@/pages/pc/ckbox/data";
|
||||||
|
|
||||||
import { WechatGroup } from "./api";
|
|
||||||
const { login2 } = useUserStore.getState();
|
const { login2 } = useUserStore.getState();
|
||||||
//获取触客宝基础信息
|
//获取触客宝基础信息
|
||||||
export const chatInitAPIdata = async () => {
|
export const chatInitAPIdata = async () => {
|
||||||
try {
|
try {
|
||||||
//获取联系人列表
|
let contractList = [];
|
||||||
const contractList = await getAllContactList();
|
let groupList = [];
|
||||||
|
|
||||||
//获取联系人列表
|
if (getIsLoadWeChat()) {
|
||||||
asyncContractList(contractList);
|
//获取联系人列表
|
||||||
|
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);
|
await asyncWeChatGroup(groupList);
|
||||||
|
|
||||||
// 提取不重复的wechatAccountId组
|
|
||||||
const uniqueWechatAccountIds: number[] = getUniqueWechatAccountIds(
|
|
||||||
contractList,
|
|
||||||
groupList,
|
|
||||||
);
|
|
||||||
|
|
||||||
//获取控制终端列表
|
//获取控制终端列表
|
||||||
const kfUserList: KfUserListData[] = await getAgentList();
|
const kfUserList: KfUserListData[] = await getAgentList();
|
||||||
|
|
||||||
@@ -53,8 +60,8 @@ export const chatInitAPIdata = async () => {
|
|||||||
await asyncKfUserList(kfUserList);
|
await asyncKfUserList(kfUserList);
|
||||||
|
|
||||||
//获取标签列表
|
//获取标签列表
|
||||||
// const countLables = await getCountLables();
|
const countLables = await getCountLables();
|
||||||
// await asyncCountLables(countLables);
|
await asyncCountLables(countLables);
|
||||||
|
|
||||||
//获取消息会话列表并按lastUpdateTime排序
|
//获取消息会话列表并按lastUpdateTime排序
|
||||||
const filterUserSessions = contractList?.filter(
|
const filterUserSessions = contractList?.filter(
|
||||||
@@ -121,15 +128,9 @@ export const initSocket = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCountLables = async () => {
|
export const getCountLables = async () => {
|
||||||
const LablesRes = await Promise.all(
|
const Result = await getLabelsListByGroup({});
|
||||||
[1, 2].map(item =>
|
const LablesRes = Result.list;
|
||||||
WechatGroup({
|
return [
|
||||||
groupType: item,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const [friend, group] = LablesRes;
|
|
||||||
const countLables = [
|
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -137,8 +138,7 @@ export const getCountLables = async () => {
|
|||||||
groupType: 2,
|
groupType: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
...group,
|
...LablesRes,
|
||||||
...friend,
|
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -147,8 +147,6 @@ export const getCountLables = async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
return countLables;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 根据标签组织联系人
|
* 根据标签组织联系人
|
||||||
|
|||||||
@@ -2,21 +2,22 @@ import CkboxPage from "@/pages/pc/ckbox";
|
|||||||
import WeChatPage from "@/pages/pc/ckbox/weChat";
|
import WeChatPage from "@/pages/pc/ckbox/weChat";
|
||||||
import Dashboard from "@/pages/pc/ckbox/dashboard";
|
import Dashboard from "@/pages/pc/ckbox/dashboard";
|
||||||
import PowerCenter from "@/pages/pc/ckbox/powerCenter";
|
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 CustomerManagement from "@/pages/pc/ckbox/powerCenter/customer-management";
|
||||||
import CommunicationRecord from "@/pages/pc/ckbox/powerCenter/communication-record";
|
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 AiTraining from "@/pages/pc/ckbox/powerCenter/ai-training";
|
||||||
import AutoGreeting from "@/pages/pc/ckbox/powerCenter/auto-greeting";
|
import AutoGreeting from "@/pages/pc/ckbox/powerCenter/auto-greeting";
|
||||||
|
import CommonConfig from "@/pages/pc/ckbox/commonConfig";
|
||||||
const ckboxRoutes = [
|
const ckboxRoutes = [
|
||||||
{
|
{
|
||||||
path: "/pc",
|
path: "/pc",
|
||||||
element: <CkboxPage />,
|
element: <CkboxPage />,
|
||||||
auth: true,
|
auth: true,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "commonConfig",
|
||||||
|
element: <CommonConfig />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "dashboard",
|
path: "dashboard",
|
||||||
element: <Dashboard />,
|
element: <Dashboard />,
|
||||||
@@ -29,22 +30,6 @@ const ckboxRoutes = [
|
|||||||
path: "powerCenter",
|
path: "powerCenter",
|
||||||
element: <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",
|
path: "powerCenter/customer-management",
|
||||||
element: <CustomerManagement />,
|
element: <CustomerManagement />,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export interface CkChatState {
|
|||||||
userInfo: CkUserInfo | null;
|
userInfo: CkUserInfo | null;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
searchKeyword: string;
|
searchKeyword: string;
|
||||||
|
isLoadWeChat: boolean;
|
||||||
|
getIsLoadWeChat: () => boolean;
|
||||||
|
updateIsLoadWeChat: (isLoadWeChat: boolean) => void;
|
||||||
contractList: ContractData[];
|
contractList: ContractData[];
|
||||||
chatSessions: any[];
|
chatSessions: any[];
|
||||||
kfUserList: KfUserListData[];
|
kfUserList: KfUserListData[];
|
||||||
|
|||||||
@@ -24,16 +24,22 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
newContractList: [], //联系人分组
|
newContractList: [], //联系人分组
|
||||||
kfSelected: 0, //选中的客服
|
kfSelected: 0, //选中的客服
|
||||||
searchKeyword: "", //搜索关键词
|
searchKeyword: "", //搜索关键词
|
||||||
|
isLoadWeChat: false, //是否加载微信
|
||||||
|
getIsLoadWeChat: () => {
|
||||||
|
return useCkChatStore.getState().isLoadWeChat;
|
||||||
|
},
|
||||||
|
updateIsLoadWeChat: (isLoadWeChat: boolean) => {
|
||||||
|
console.log("updateIsLoadWeChat", isLoadWeChat);
|
||||||
|
set({ isLoadWeChat });
|
||||||
|
},
|
||||||
//客服列表
|
//客服列表
|
||||||
asyncKfUserList: async data => {
|
asyncKfUserList: async data => {
|
||||||
set({ kfUserList: data });
|
set({ kfUserList: data });
|
||||||
// await kfUserService.createManyWithServerId(data);
|
|
||||||
},
|
},
|
||||||
// 获取客服列表
|
// 获取客服列表
|
||||||
getkfUserList: async () => {
|
getkfUserList: async () => {
|
||||||
const state = useCkChatStore.getState();
|
const state = useCkChatStore.getState();
|
||||||
return state.kfUserList;
|
return state.kfUserList;
|
||||||
// return await kfUserService.findAll();
|
|
||||||
},
|
},
|
||||||
// 异步设置标签列表
|
// 异步设置标签列表
|
||||||
asyncCountLables: async (data: ContactGroupByLabel[]) => {
|
asyncCountLables: async (data: ContactGroupByLabel[]) => {
|
||||||
@@ -545,3 +551,7 @@ export const searchContactsAndGroups = () =>
|
|||||||
export const pinChatSessionToTop = (sessionId: number) =>
|
export const pinChatSessionToTop = (sessionId: number) =>
|
||||||
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
||||||
useCkChatStore.getState().getKfSelectedUser();
|
useCkChatStore.getState().getKfSelectedUser();
|
||||||
|
export const updateIsLoadWeChat = (isLoadWeChat: boolean) =>
|
||||||
|
useCkChatStore.getState().updateIsLoadWeChat(isLoadWeChat);
|
||||||
|
export const getIsLoadWeChat = () =>
|
||||||
|
useCkChatStore.getState().getIsLoadWeChat();
|
||||||
|
|||||||
@@ -315,3 +315,11 @@ button {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user