diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 5e3573c1..d6da5a65 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -602,7 +602,7 @@ const ScenarioList: React.FC = () => {
小程序链接
{
+
{rightContent}
); }; diff --git a/Touchkebao/src/components/Upload/AudioUpload/index.module.scss b/Touchkebao/src/components/Upload/AudioUpload/index.module.scss new file mode 100644 index 00000000..c8dd27df --- /dev/null +++ b/Touchkebao/src/components/Upload/AudioUpload/index.module.scss @@ -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); + } +} \ No newline at end of file diff --git a/Touchkebao/src/components/Upload/AudioUpload/index.tsx b/Touchkebao/src/components/Upload/AudioUpload/index.tsx new file mode 100644 index 00000000..68e82657 --- /dev/null +++ b/Touchkebao/src/components/Upload/AudioUpload/index.tsx @@ -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 = ({ + value = "", + onChange, + disabled = false, + className, + maxSize = 50, + showPreview = true, + maxCount = 1, +}) => { + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + 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 = ( +
+ {loading ? ( +
+
+ +
+
上传中...
+ +
+ ) : ( +
+
+ +
+
+
+ {maxCount === 1 + ? "上传音频" + : `上传音频 (${fileList.length}/${maxCount})`} +
+
+ 支持 MP3、WAV、AAC 等格式,最大 {maxSize}MB + {maxCount > 1 && `,最多上传 ${maxCount} 个音频`} +
+
+
+ )} +
+ ); + + // 自定义文件列表项 + const customItemRender = ( + originNode: React.ReactElement, + file: UploadFile, + ) => { + if (file.status === "uploading") { + return ( +
+
+
+ +
+
+
{file.name}
+
+ {file.size ? formatFileSize(file.size) : "计算中..."} +
+
+
+
+
+ +
+ ); + } + + if (file.status === "done") { + return ( +
+
+
+ +
+
+
{file.name}
+
+ {file.size ? formatFileSize(file.size) : "未知大小"} +
+
+
+ {showPreview && ( +
+
+
+ ); + } + + return originNode; + }; + + const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload"; + + return ( +
+ 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} + + + {/* 音频预览模态框 */} + setPreviewVisible(false)} + footer={null} + width={600} + centered + > +
+ +
+
+
+ ); +}; + +export default AudioUpload; diff --git a/Touchkebao/src/components/Upload/FileUpload/index.tsx b/Touchkebao/src/components/Upload/FileUpload/index.tsx index 9edf1c4f..c70b19db 100644 --- a/Touchkebao/src/components/Upload/FileUpload/index.tsx +++ b/Touchkebao/src/components/Upload/FileUpload/index.tsx @@ -176,12 +176,17 @@ const FileUpload: React.FC = ({ } else if (info.file.status === "done") { setLoading(false); setUploadProgress(100); - message.success("文件上传成功!"); // 从响应中获取上传后的URL let uploadedUrl = ""; if (info.file.response) { + // 检查响应是否成功 + if (info.file.response.code && info.file.response.code !== 200) { + message.error(info.file.response.message || "上传失败"); + return; + } + if (typeof info.file.response === "string") { uploadedUrl = info.file.response; } else if (info.file.response.data) { @@ -195,6 +200,7 @@ const FileUpload: React.FC = ({ } if (uploadedUrl) { + message.success("文件上传成功!"); if (maxCount === 1) { // 单个文件模式 onChange?.(uploadedUrl); @@ -208,6 +214,8 @@ const FileUpload: React.FC = ({ const newUrls = [...currentUrls, uploadedUrl]; onChange?.(newUrls); } + } else { + message.error("上传失败,未获取到文件URL"); } } else if (info.file.status === "error") { setLoading(false); diff --git a/Touchkebao/src/components/Upload/VideoUpload/index.tsx b/Touchkebao/src/components/Upload/VideoUpload/index.tsx index 7775eacd..7017e327 100644 --- a/Touchkebao/src/components/Upload/VideoUpload/index.tsx +++ b/Touchkebao/src/components/Upload/VideoUpload/index.tsx @@ -108,12 +108,17 @@ const VideoUpload: React.FC = ({ } else if (info.file.status === "done") { setLoading(false); setUploadProgress(100); - message.success("视频上传成功!"); // 从响应中获取上传后的URL let uploadedUrl = ""; if (info.file.response) { + // 检查响应是否成功 + if (info.file.response.code && info.file.response.code !== 200) { + message.error(info.file.response.message || "上传失败"); + return; + } + if (typeof info.file.response === "string") { uploadedUrl = info.file.response; } else if (info.file.response.data) { @@ -127,6 +132,7 @@ const VideoUpload: React.FC = ({ } if (uploadedUrl) { + message.success("视频上传成功!"); if (maxCount === 1) { // 单个视频模式 onChange?.(uploadedUrl); @@ -140,6 +146,8 @@ const VideoUpload: React.FC = ({ const newUrls = [...currentUrls, uploadedUrl]; onChange?.(newUrls); } + } else { + message.error("上传失败,未获取到视频URL"); } } else if (info.file.status === "error") { setLoading(false); diff --git a/Touchkebao/src/pages/index.tsx b/Touchkebao/src/pages/index.tsx index ed9c8153..d0857ff5 100644 --- a/Touchkebao/src/pages/index.tsx +++ b/Touchkebao/src/pages/index.tsx @@ -18,7 +18,7 @@ const IndexPage: React.FC = () => { if (isMobile()) { navigate("/mobile/dashboard"); } else { - navigate("/pc/dashboard"); + navigate("/pc/weChat"); } }, [navigate]); diff --git a/Touchkebao/src/pages/pc/ckbox/api.ts b/Touchkebao/src/pages/pc/ckbox/api.ts index e26f0232..1fe61086 100644 --- a/Touchkebao/src/pages/pc/ckbox/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/api.ts @@ -27,12 +27,12 @@ export function clearUnreadCount1(params) { return request("/v1/kefu/message/readMessage", params, "GET"); } export function clearUnreadCount2(params) { - return request("/api/WechatFriend/clearUnreadCount", params, "PUT"); + return request2("/api/WechatFriend/clearUnreadCount", params, "PUT"); } //更新配置 export function updateConfig(params) { - return request("/api/WechatFriend/updateConfig", params, "PUT"); + return request2("/api/WechatFriend/updateConfig", params, "PUT"); } //获取聊天记录-2 获取列表 export function getChatMessages(params: { @@ -44,7 +44,7 @@ export function getChatMessages(params: { Count: number; olderData: boolean; }) { - return request("/api/FriendMessage/SearchMessage", params, "GET"); + return request2("/api/FriendMessage/SearchMessage", params, "GET"); } export function getChatroomMessages(params: { wechatAccountId: number; diff --git a/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts new file mode 100644 index 00000000..aabbc041 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts @@ -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"); +}; diff --git a/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss new file mode 100644 index 00000000..bdf212f2 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss @@ -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; + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx new file mode 100644 index 00000000..c0f419e2 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx @@ -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([]); + const [mode, setMode] = useState(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: , + content: ( +
+

您即将对以下流量池进行批量设置:

+
+ 流量池: + {poolNames} +
+
+ 接待模式: + {selectedModeLabel} +
+

+ 此操作将影响 {selectedPools.length}{" "} + 个流量池的接待设置,确定要继续吗? +

+
+ ), + 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 ( +
+
+ + + 全局接待模式 + + } + > +
+ 支持按流量池批量设置,单个客户设置将覆盖流量池默认配置 +
+ +
+
选择流量池
+ +
+ +
+
接待模式
+ +
+ + +
+
+ +
+ + + 流量池状态 + + } + > + {selectedPools.length > 0 ? ( + ( + + + + {typeOptions.find(opt => opt.value === mode)?.label || + "人工接待"} + + + )} + /> + ) : ( +
+
请先选择流量池
+
+ 选择流量池后将显示其状态信息 +
+
+ )} +
+
+
+ ); +}; + +export default ReceptionSettings; diff --git a/Touchkebao/src/pages/pc/ckbox/commonConfig/index.module.scss b/Touchkebao/src/pages/pc/ckbox/commonConfig/index.module.scss new file mode 100644 index 00000000..23d55035 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/commonConfig/index.module.scss @@ -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; +} diff --git a/Touchkebao/src/pages/pc/ckbox/commonConfig/index.tsx b/Touchkebao/src/pages/pc/ckbox/commonConfig/index.tsx new file mode 100644 index 00000000..0104b905 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/commonConfig/index.tsx @@ -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("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 ; + case "notification": + return
通知设置功能开发中...
; + case "system": + return
系统设置功能开发中...
; + case "security": + return
安全设置功能开发中...
; + case "advanced": + return
高级设置功能开发中...
; + default: + return ; + } + }; + return ( + + +
+ {tabs.map(tab => ( +
handleTabClick(tab.key)} + > + {tab.label} +
+ ))} +
+ + } + > +
{renderTabContent()}
+
+ ); +}; + +export default CommonConfig; diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss index 77ab9442..a8c4e452 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.module.scss @@ -79,6 +79,7 @@ display: flex; align-items: center; padding: 8px 0; + gap: 26px; .suanli { display: flex; align-items: center; @@ -131,10 +132,20 @@ .userSection { display: flex; - align-items: center; gap: 12px; cursor: pointer; - padding: 8px 16px; + .userInfo2 { + line-height: 1; + padding-top: 5px; + font-size: 14px; + .userNickname { + margin-bottom: 4px; + } + .userAccount { + font-size: 12px; + color: #888; + } + } } .userNickname { diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx index b3053eef..f1dc087f 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Layout, Drawer, Avatar, Space, Button, Badge, Dropdown } from "antd"; import { - MenuOutlined, + BarChartOutlined, UserOutlined, BellOutlined, LogoutOutlined, @@ -90,24 +90,20 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { <>
- {!isWeChat() ? ( - + {title}
@@ -124,9 +120,14 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { -
- -
+ = ({ title = "触客宝" }) => { src={user?.avatar} className={styles.avatar} /> + +
+
{user.username}
+
高级客服专员
+
diff --git a/Touchkebao/src/pages/pc/ckbox/index.tsx b/Touchkebao/src/pages/pc/ckbox/index.tsx index 039bac75..a74b6914 100644 --- a/Touchkebao/src/pages/pc/ckbox/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/index.tsx @@ -4,11 +4,7 @@ import { Outlet } from "react-router-dom"; import NavCommon from "./components/NavCommon"; const CkboxPage: React.FC = () => { return ( - - } - > + }> ); diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss index bf1ac1e6..2a9de82d 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss @@ -3,41 +3,426 @@ background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; } +// 头部样式 .header { - margin-bottom: 24px; - - h1 { - font-size: 24px; - font-weight: 600; - color: #262626; - margin: 0 0 8px 0; - } - - p { - font-size: 14px; - color: #8c8c8c; - margin: 0; - } -} - -.content { - min-height: 400px; -} - -.placeholder { display: flex; + justify-content: space-between; align-items: center; - justify-content: center; - height: 300px; - background: #fafafa; - border: 1px dashed #d9d9d9; - border-radius: 6px; - - p { - font-size: 16px; - color: #8c8c8c; - margin: 0; + margin-bottom: 24px; + padding: 16px 0; + border-bottom: 1px solid #f0f0f0; + + .headerLeft { + .computeBalance { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #666; + + .anticon { + color: #1890ff; + } + } } -} \ No newline at end of file + + .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; + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx index 6bcdc00c..6c40674e 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx @@ -1,21 +1,465 @@ -import React from "react"; +import React, { useState } from "react"; +import { Button, Input, Tabs, Card, Tag, message, Modal, Tooltip } from "antd"; +import { + PlusOutlined, + PlayCircleOutlined, + EyeOutlined, + EditOutlined, + DeleteOutlined, + ThunderboltOutlined, + FileTextOutlined, + DatabaseOutlined, + BarChartOutlined, + SaveOutlined, +} from "@ant-design/icons"; import PowerNavigation from "@/components/PowerNavtion"; import styles from "./index.module.scss"; +const { TextArea } = Input; +const { TabPane } = Tabs; + +// 话术数据类型 +interface UtteranceData { + id: string; + title: string; + category: string; + content: string; + status: "active" | "pending"; + createTime: string; + updateTime: string; +} + +// 模型数据类型 +interface ModelData { + id: string; + name: string; + version: string; + status: "training" | "completed" | "failed"; + accuracy: number; + createTime: string; + lastTraining: string; +} + +// 训练分析数据类型 +interface TrainingAnalysis { + totalUtterances: number; + activeUtterances: number; + pendingUtterances: number; + trainingAccuracy: number; + lastTrainingTime: string; + nextTrainingTime: string; +} + const AiTraining: React.FC = () => { + const [activeTab, setActiveTab] = useState("utterance"); + const [utteranceForm, setUtteranceForm] = useState({ + title: "", + category: "", + content: "", + }); + const [utterances, setUtterances] = useState([ + { + 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([ + { + 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({ + 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 = () => ( +
+
+ +
+ + 添加训练话术 +
+

添加高质量的对话内容来训练AI模型

+ +
+
+ + + setUtteranceForm({ ...utteranceForm, title: e.target.value }) + } + /> +
+ +
+ + + setUtteranceForm({ + ...utteranceForm, + category: e.target.value, + }) + } + /> +
+ +
+ +