From 2492daae3b7d89d22897a1efd33ea68b4380c33a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 14 Jul 2026 11:05:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AE=BF=E5=AE=A2=E8=81=8A?= =?UTF-8?q?=E5=A4=A9Widget=E7=BB=84=E4=BB=B6=E3=80=81Admin=E7=AB=AF?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E6=A1=86=E6=9E=B6=E5=8F=8A=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/layout/AdminLayout.tsx | 20 +++ web/src/components/layout/AdminSidebar.tsx | 42 ++++++ web/src/pages/WidgetPreview.tsx | 17 +++ web/src/pages/admin/Dashboard.tsx | 6 + web/src/pages/admin/Ops.tsx | 6 + web/src/pages/admin/Plans.tsx | 6 + web/src/pages/admin/Tenants.tsx | 6 + web/src/router/index.tsx | 19 ++- web/src/widgets/VisitorChat.tsx | 151 +++++++++++++++++++++ 9 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 web/src/components/layout/AdminLayout.tsx create mode 100644 web/src/components/layout/AdminSidebar.tsx create mode 100644 web/src/pages/WidgetPreview.tsx create mode 100644 web/src/pages/admin/Dashboard.tsx create mode 100644 web/src/pages/admin/Ops.tsx create mode 100644 web/src/pages/admin/Plans.tsx create mode 100644 web/src/pages/admin/Tenants.tsx create mode 100644 web/src/widgets/VisitorChat.tsx diff --git a/web/src/components/layout/AdminLayout.tsx b/web/src/components/layout/AdminLayout.tsx new file mode 100644 index 0000000..48fd118 --- /dev/null +++ b/web/src/components/layout/AdminLayout.tsx @@ -0,0 +1,20 @@ +import { Outlet } from 'react-router-dom' +import { Layout } from 'antd' +import AdminSidebar from './AdminSidebar' + +const { Content } = Layout + +const AdminLayout = () => { + return ( + + + + + + + + + ) +} + +export default AdminLayout diff --git a/web/src/components/layout/AdminSidebar.tsx b/web/src/components/layout/AdminSidebar.tsx new file mode 100644 index 0000000..1c80f41 --- /dev/null +++ b/web/src/components/layout/AdminSidebar.tsx @@ -0,0 +1,42 @@ +import { useLocation, useNavigate } from 'react-router-dom' +import { Layout, Menu } from 'antd' +import { + DashboardOutlined, + TeamOutlined, + GiftOutlined, + ToolOutlined, +} from '@ant-design/icons' + +const { Sider } = Layout + +const menuItems = [ + { key: '/admin/dashboard', icon: , label: '运营概览' }, + { key: '/admin/tenants', icon: , label: '租户管理' }, + { key: '/admin/plans', icon: , label: '套餐管理' }, + { key: '/admin/ops', icon: , label: '系统运维' }, +] + +const AdminSidebar = () => { + const location = useLocation() + const navigate = useNavigate() + + const selectedKey = menuItems.find(item => location.pathname.startsWith(item.key))?.key || '/admin/dashboard' + + return ( + +
+ + 客服云管理 +
+ navigate(key)} + className="border-e-0 mt-2" + /> + + ) +} + +export default AdminSidebar diff --git a/web/src/pages/WidgetPreview.tsx b/web/src/pages/WidgetPreview.tsx new file mode 100644 index 0000000..dee7808 --- /dev/null +++ b/web/src/pages/WidgetPreview.tsx @@ -0,0 +1,17 @@ +import VisitorChat from '@/widgets/VisitorChat' + +const WidgetPreview = () => { + return ( +
+
+
+

示例网站页面

+

访客聊天 Widget 已嵌入右下角,点击气泡按钮开始对话

+
+
+ +
+ ) +} + +export default WidgetPreview diff --git a/web/src/pages/admin/Dashboard.tsx b/web/src/pages/admin/Dashboard.tsx new file mode 100644 index 0000000..a2e3a9b --- /dev/null +++ b/web/src/pages/admin/Dashboard.tsx @@ -0,0 +1,6 @@ +const AdminDashboard = () => ( +
+
运营概览
开发中...
+
+) +export default AdminDashboard diff --git a/web/src/pages/admin/Ops.tsx b/web/src/pages/admin/Ops.tsx new file mode 100644 index 0000000..a2ba66f --- /dev/null +++ b/web/src/pages/admin/Ops.tsx @@ -0,0 +1,6 @@ +const Ops = () => ( +
+
系统运维
开发中...
+
+) +export default Ops diff --git a/web/src/pages/admin/Plans.tsx b/web/src/pages/admin/Plans.tsx new file mode 100644 index 0000000..23bf9ff --- /dev/null +++ b/web/src/pages/admin/Plans.tsx @@ -0,0 +1,6 @@ +const Plans = () => ( +
+
套餐管理
开发中...
+
+) +export default Plans diff --git a/web/src/pages/admin/Tenants.tsx b/web/src/pages/admin/Tenants.tsx new file mode 100644 index 0000000..a0a05ef --- /dev/null +++ b/web/src/pages/admin/Tenants.tsx @@ -0,0 +1,6 @@ +const Tenants = () => ( +
+
租户管理
开发中...
+
+) +export default Tenants diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 200cf49..c4c38c4 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -3,12 +3,18 @@ import { Navigate, createBrowserRouter } from 'react-router-dom' import { Spin } from 'antd' const AgentLayout = lazy(() => import('@/components/layout/AgentLayout')) +const AdminLayout = lazy(() => import('@/components/layout/AdminLayout')) const Dashboard = lazy(() => import('@/pages/agent/Dashboard')) const ChatHistory = lazy(() => import('@/pages/agent/ChatHistory')) const Customers = lazy(() => import('@/pages/agent/Customers')) const Knowledge = lazy(() => import('@/pages/agent/Knowledge')) const Statistics = lazy(() => import('@/pages/agent/Statistics')) const Settings = lazy(() => import('@/pages/agent/Settings')) +const AdminDashboard = lazy(() => import('@/pages/admin/Dashboard')) +const Tenants = lazy(() => import('@/pages/admin/Tenants')) +const Plans = lazy(() => import('@/pages/admin/Plans')) +const Ops = lazy(() => import('@/pages/admin/Ops')) +const WidgetPreview = lazy(() => import('@/pages/WidgetPreview')) const loading = (
@@ -21,9 +27,20 @@ function Lazy({ children }: { children: React.ReactNode }) { } export const router = createBrowserRouter([ + { + path: '/widget/preview', + element: , + }, { path: '/admin', - element:
管理端
, + element: , + children: [ + { index: true, element: }, + { path: 'dashboard', element: }, + { path: 'tenants', element: }, + { path: 'plans', element: }, + { path: 'ops', element: }, + ], }, { path: '/', diff --git a/web/src/widgets/VisitorChat.tsx b/web/src/widgets/VisitorChat.tsx new file mode 100644 index 0000000..a15f60e --- /dev/null +++ b/web/src/widgets/VisitorChat.tsx @@ -0,0 +1,151 @@ +interface Message { + id: string + sender: 'visitor' | 'agent' + content: string + time: string +} + +const initialMessages: Message[] = [ + { id: '1', sender: 'agent', content: '您好!欢迎咨询客服云,请问有什么可以帮您的?', time: '10:30' }, +] + +const quickQuestions = ['产品功能介绍', '价格咨询', '售后服务', '合作咨询'] + +import { useState } from 'react' +import { CloseOutlined, MessageOutlined, SmileOutlined, PaperClipOutlined, SendOutlined, StarFilled } from '@ant-design/icons' + +const VisitorChat = () => { + const [open, setOpen] = useState(false) + const [messages, setMessages] = useState(initialMessages) + const [input, setInput] = useState('') + const [typing, setTyping] = useState(false) + const [showRating, setShowRating] = useState(false) + const [rated, setRated] = useState(false) + + const sendMessage = (text: string) => { + if (!text.trim()) return + const msg: Message = { id: Date.now().toString(), sender: 'visitor', content: text, time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } + setMessages(prev => [...prev, msg]) + setInput('') + setTyping(true) + setTimeout(() => { + setTyping(false) + const reply: Message = { id: (Date.now() + 1).toString(), sender: 'agent', content: '感谢您的咨询,客服正在为您处理中...', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } + setMessages(prev => [...prev, reply]) + }, 1500) + } + + const handleEnd = () => { + if (!rated) { + setShowRating(true) + } + setOpen(false) + } + + return ( + <> + {/* 悬浮按钮 */} + {!open && ( + + )} + + {/* 聊天窗口 */} + {open && ( +
+ {/* 顶部 */} +
+
+
+ +
+
+
客服云
+
{typing ? '正在输入...' : '在线'}
+
+
+ +
+ + {/* 消息区域 */} +
+ {messages.map(msg => ( +
+
+ {msg.content} +
{msg.time}
+
+
+ ))} + {typing && ( +
+
+
+ + + +
+
+
+ )} +
+ + {/* 快捷问题 */} + {messages.length <= 1 && ( +
+
+ {quickQuestions.map((q, i) => ( + + ))} +
+
+ )} + + {/* 输入框 */} +
+
+ + + setInput(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') { sendMessage(input) } }} + /> + sendMessage(input)} /> +
+
+ + {/* 满意度弹窗 */} + {showRating && ( +
+
+
本次服务如何?
+
请对我们的服务进行评价
+
+ {[1, 2, 3, 4, 5].map(star => ( + { setRated(true); setShowRating(false) }} + /> + ))} +
+ +
+
+ )} +
+ )} + + ) +} + +export default VisitorChat