From 198ff9e1fb16c958094ca56cc23af6da4aebcb55 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 31 Aug 2026 16:25:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B4=E4=BD=93ui-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/layout/AdminSidebar.tsx | 102 +++--- web/src/components/layout/AgentSidebar.tsx | 284 +++++++++-------- web/src/index.css | 96 +++++- web/src/main.tsx | 104 +++++- web/src/pages/Login.tsx | 168 ++++++++-- web/src/pages/admin/Dashboard.tsx | 108 +++---- web/src/pages/admin/Ops.tsx | 85 ++--- web/src/pages/admin/Plans.tsx | 11 +- web/src/pages/admin/Tenants.tsx | 72 +++-- web/src/pages/agent/Blacklist.tsx | 72 +++-- web/src/pages/agent/ChatHistory.tsx | 124 +++---- web/src/pages/agent/Customers.tsx | 133 ++++---- web/src/pages/agent/Dashboard.tsx | 355 ++++++++++----------- web/src/pages/agent/Knowledge.tsx | 101 +++--- web/src/pages/agent/QuickReplies.tsx | 129 ++++---- web/src/pages/agent/Settings.tsx | 29 +- web/src/pages/agent/Statistics.tsx | 227 ++++++------- web/src/services/api.ts | 1 + web/src/widgets/VisitorChat.tsx | 31 +- 19 files changed, 1273 insertions(+), 959 deletions(-) diff --git a/web/src/components/layout/AdminSidebar.tsx b/web/src/components/layout/AdminSidebar.tsx index a92b015..3b31d80 100644 --- a/web/src/components/layout/AdminSidebar.tsx +++ b/web/src/components/layout/AdminSidebar.tsx @@ -1,7 +1,9 @@ import { useLocation, useNavigate } from 'react-router-dom' import { DashboardOutlined, TeamOutlined, GiftOutlined, ToolOutlined, LogoutOutlined, + CrownOutlined, } from '@ant-design/icons' +import { Tooltip } from 'antd' import { useAuth } from '@/stores/auth' const menuItems = [ @@ -17,7 +19,7 @@ const AdminSidebar = () => { const { user, logout } = useAuth() const selectedKey = menuItems.find(item => location.pathname.startsWith(item.key))?.key || '/admin/dashboard' - const initial = (user?.nickname || '管').slice(0, 1) + const initial = (user?.nickname || user?.username || '管').slice(0, 1).toUpperCase() const handleLogout = () => { logout() @@ -26,62 +28,68 @@ const AdminSidebar = () => { return ( ) diff --git a/web/src/components/layout/AgentSidebar.tsx b/web/src/components/layout/AgentSidebar.tsx index 65ada8e..7d0144b 100644 --- a/web/src/components/layout/AgentSidebar.tsx +++ b/web/src/components/layout/AgentSidebar.tsx @@ -5,28 +5,28 @@ import { FileTextOutlined, BarChartOutlined, SettingOutlined, LogoutOutlined, ThunderboltOutlined, DownOutlined, StopOutlined, SafetyOutlined, BellOutlined, BellFilled, } from '@ant-design/icons' -import { Button, Dropdown, message as antMsg, Popover, Switch } from 'antd' +import { Button, Dropdown, message as antMsg, Popover, Switch, Tooltip } from 'antd' import { useAuth } from '@/stores/auth' import { useAgentNotifications } from '@/stores/notifications' import type { AgentPresence } from '@/services/api' const menuItems = [ - { key: '/agent/dashboard', icon: , label: '工作台', permissions: ['session.view'] }, - { key: '/agent/customers', icon: , label: '客户管理', permissions: ['customer.view'] }, - { key: '/agent/chat-history', icon: , label: '对话记录', permissions: ['chat_history.view'] }, - { key: '/agent/knowledge', icon: , label: '知识库', permissions: ['knowledge.view'] }, - { key: '/agent/quick-replies', icon: , label: '快捷回复', permissions: ['quick_reply.view'] }, - { key: '/agent/blacklist', icon: , label: '黑名单', permissions: ['blacklist.view'] }, - { key: '/agent/statistics', icon: , label: '数据统计', permissions: ['statistics.view'] }, - { key: '/agent/staff', icon: , label: '成员账号', permissions: ['settings.staff'] }, - { key: '/agent/roles', icon: , label: '权限控制', permissions: ['permission.view'] }, - { key: '/agent/settings', icon: , label: '系统设置', permissions: ['settings.basic', 'settings.channel', 'settings.assign_rule', 'settings.customer_tag', 'settings.auto_reply', 'settings.worktime', 'settings.notification'] }, + { key: '/agent/dashboard', icon: , label: '工作台', permissions: ['session.view'] }, + { key: '/agent/customers', icon: , label: '客户管理', permissions: ['customer.view'] }, + { key: '/agent/chat-history', icon: , label: '对话记录', permissions: ['chat_history.view'] }, + { key: '/agent/knowledge', icon: , label: '知识库', permissions: ['knowledge.view'] }, + { key: '/agent/quick-replies', icon: , label: '快捷回复', permissions: ['quick_reply.view'] }, + { key: '/agent/blacklist', icon: , label: '黑名单', permissions: ['blacklist.view'] }, + { key: '/agent/statistics', icon: , label: '数据统计', permissions: ['statistics.view'] }, + { key: '/agent/staff', icon: , label: '成员账号', permissions: ['settings.staff'] }, + { key: '/agent/roles', icon: , label: '权限控制', permissions: ['permission.view'] }, + { key: '/agent/settings', icon: , label: '系统设置', permissions: ['settings.basic', 'settings.channel', 'settings.assign_rule', 'settings.customer_tag', 'settings.auto_reply', 'settings.worktime', 'settings.notification'] }, ] -const presenceOptions: { value: AgentPresence; text: string; dot: string }[] = [ - { value: 'online', text: '在线', dot: '#16a34a' }, - { value: 'busy', text: '忙碌', dot: '#d97706' }, - { value: 'offline', text: '离线', dot: '#94a3b8' }, +const presenceOptions: { value: AgentPresence; text: string; dot: string; bg: string }[] = [ + { value: 'online', text: '在线', dot: '#16a34a', bg: '#f0fdf4' }, + { value: 'busy', text: '忙碌', dot: '#d97706', bg: '#fffbeb' }, + { value: 'offline', text: '离线', dot: '#94a3b8', bg: '#f1f5f9' }, ] function presenceMeta(status?: string) { @@ -36,19 +36,18 @@ function presenceMeta(status?: string) { const AgentSidebar = () => { const location = useLocation() const navigate = useNavigate() - const { user, permissions, logout, setPresence } = useAuth() - const { - soundEnabled, - desktopEnabled, - permission, - setSoundEnabled, - setDesktopEnabled, - requestDesktopPermission, - } = useAgentNotifications() - const [switching, setSwitching] = useState(false) - - const visibleMenuItems = menuItems.filter(item => item.permissions.some(code => permissions.has(code))) + const { user, permissions, logout, setPresence } = useAuth() + const { + soundEnabled, + desktopEnabled, + permission, + setSoundEnabled, + setDesktopEnabled, + requestDesktopPermission, + } = useAgentNotifications() + const [switching, setSwitching] = useState(false) + const visibleMenuItems = menuItems.filter(item => item.permissions.some(code => permissions.has(code))) const selectedKey = visibleMenuItems.find(item => location.pathname.startsWith(item.key))?.key || '/agent/dashboard' const handleLogout = () => { @@ -70,136 +69,159 @@ const AgentSidebar = () => { } } - const initial = (user?.nickname || '客').slice(0, 1) - const current = presenceMeta(user?.status || 'online') + const initial = (user?.nickname || user?.username || '客').slice(0, 1).toUpperCase() + const current = presenceMeta(user?.status || 'online') - const notificationContent = ( -
-
-
-
声音提醒
-
收到访客消息时播放提示音
-
- -
-
-
-
浏览器通知
-
- {permission === 'denied' ? '已被浏览器禁止' : permission === 'unsupported' ? '当前浏览器不支持' : '在后台弹出新消息'} -
-
- { - if (enabled) void requestDesktopPermission() - else setDesktopEnabled(false) - }} - /> -
- {permission === 'default' && ( - - )} -
- ) + const notificationContent = ( +
+
+
+
声音提醒
+
收到访客消息时播放提示音
+
+ +
+
+
+
浏览器通知
+
+ {permission === 'denied' ? '已被浏览器禁止' : permission === 'unsupported' ? '当前浏览器不支持' : '在后台弹出新消息通知'} +
+
+ { + if (enabled) void requestDesktopPermission() + else setDesktopEnabled(false) + }} + /> +
+ {permission === 'default' && ( + + )} +
+ ) return ( ) diff --git a/web/src/index.css b/web/src/index.css index 84db326..03d34e6 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,6 +3,7 @@ :root { --brand-primary: #2563eb; --brand-primary-hover: #1d4ed8; + --brand-primary-active: #1e40af; --brand-primary-light: #dbeafe; --brand-primary-lighter: #eff6ff; --neutral-0: #ffffff; @@ -18,6 +19,7 @@ --neutral-900: #0f172a; --neutral-950: #020617; --success: #16a34a; + --success-hover: #15803d; --success-bg: #f0fdf4; --warning: #d97706; --warning-bg: #fffbeb; @@ -25,11 +27,13 @@ --error-bg: #fef2f2; --info: #0891b2; --info-bg: #ecfeff; - --sidebar-width: 240px; + --sidebar-width: 230px; --panel-width: 320px; --header-height: 56px; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.03); - --shadow-floating: 0 8px 24px rgba(0, 0, 0, 0.12); + --shadow-subtle: 0 1px 2px 0 rgba(0, 0, 0, 0.04); + --shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05); + --shadow-card-hover: 0 10px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.04); + --shadow-floating: 0 12px 32px -4px rgba(15, 23, 42, 0.12), 0 4px 12px -2px rgba(15, 23, 42, 0.08); } /* 欢迎语富文本(SafeHtml)气泡内样式 */ @@ -55,12 +59,30 @@ body { margin: 0; - font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; background: var(--neutral-50); color: var(--neutral-900); } +/* 现代精致细滚动条 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.35); + border-radius: 9999px; + transition: background-color 0.2s ease; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.55); +} + @keyframes typingDot { 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-3px); } @@ -76,3 +98,69 @@ body { .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } + +/* 玻璃拟态与交互效果 */ +.glass-panel { + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.glass-dark { + background: rgba(15, 23, 42, 0.82); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} + +.subtle-border { + border-color: rgba(226, 232, 240, 0.8); +} + +/* 浏览器自动填充 (Autofill) 样式重置与深色模式适配 */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + transition: background-color 50000s ease-in-out 0s; +} + +.login-dark-input, +.login-dark-input.ant-input-affix-wrapper, +.login-dark-input.ant-input { + background-color: rgba(30, 41, 59, 0.75) !important; + border-color: rgba(51, 65, 85, 0.8) !important; + color: #f8fafc !important; +} + +.login-dark-input .ant-input { + background-color: transparent !important; + color: #f8fafc !important; +} + +.login-dark-input input:-webkit-autofill, +.login-dark-input input:-webkit-autofill:hover, +.login-dark-input input:-webkit-autofill:focus, +.login-dark-input input:-webkit-autofill:active, +.login-dark-input .ant-input:-webkit-autofill { + -webkit-box-shadow: 0 0 0 1000px #1e293b inset !important; + -webkit-text-fill-color: #f8fafc !important; + caret-color: #f8fafc !important; +} + +.login-dark-input .ant-input-password-icon, +.login-dark-input .anticon { + color: #94a3b8 !important; +} + +.login-dark-input:hover, +.login-dark-input.ant-input-affix-wrapper:hover { + border-color: #3b82f6 !important; +} + +.login-dark-input:focus, +.login-dark-input-focused, +.login-dark-input.ant-input-affix-wrapper-focused { + border-color: #3b82f6 !important; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important; +} + diff --git a/web/src/main.tsx b/web/src/main.tsx index accdddd..24b2161 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { ConfigProvider } from 'antd' +import { ConfigProvider, type ThemeConfig } from 'antd' import zhCN from 'antd/locale/zh_CN' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' @@ -11,11 +11,107 @@ import './index.css' // 日历 / DatePicker 月份、星期与 Ant Design 中文一致 dayjs.locale('zh-cn') -const theme = { +const theme: ThemeConfig = { token: { colorPrimary: '#2563eb', - borderRadius: 6, - fontFamily: "'Inter', 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif", + colorPrimaryHover: '#1d4ed8', + colorPrimaryActive: '#1e40af', + colorSuccess: '#16a34a', + colorWarning: '#d97706', + colorError: '#dc2626', + colorInfo: '#0891b2', + colorTextBase: '#0f172a', + colorBgBase: '#ffffff', + colorBorder: '#e2e8f0', + colorBorderSecondary: '#f1f5f9', + borderRadius: 8, + borderRadiusLG: 12, + borderRadiusSM: 6, + borderRadiusXS: 4, + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + fontSize: 14, + controlHeight: 36, + controlHeightLG: 40, + controlHeightSM: 28, + boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05)', + boxShadowSecondary: '0 4px 16px -2px rgba(15, 23, 42, 0.08), 0 2px 6px -1px rgba(15, 23, 42, 0.04)', + }, + components: { + Button: { + fontWeight: 500, + controlHeight: 36, + controlHeightLG: 42, + controlHeightSM: 30, + borderRadius: 8, + borderRadiusLG: 10, + borderRadiusSM: 6, + defaultBorderColor: '#e2e8f0', + defaultColor: '#334155', + defaultBg: '#ffffff', + defaultHoverBg: '#f8fafc', + defaultHoverColor: '#0f172a', + defaultHoverBorderColor: '#cbd5e1', + primaryShadow: '0 2px 4px 0 rgba(37, 99, 235, 0.2)', + }, + Card: { + borderRadiusLG: 12, + headerHeight: 52, + headerFontSize: 15, + headerFontSizeSM: 14, + colorBorderSecondary: '#f1f5f9', + boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.04)', + }, + Table: { + headerBg: '#f8fafc', + headerColor: '#475569', + headerSplitColor: 'transparent', + rowHoverBg: '#f8fafc', + borderColor: '#f1f5f9', + borderRadius: 10, + fontSize: 13, + }, + Input: { + controlHeight: 36, + borderRadius: 8, + colorBorder: '#e2e8f0', + activeBorderColor: '#2563eb', + hoverBorderColor: '#93c5fd', + activeShadow: '0 0 0 3px rgba(37, 99, 235, 0.12)', + }, + Select: { + controlHeight: 36, + borderRadius: 8, + colorBorder: '#e2e8f0', + optionSelectedBg: '#eff6ff', + optionSelectedColor: '#2563eb', + }, + Modal: { + borderRadiusLG: 16, + titleFontSize: 16, + headerBg: '#ffffff', + contentBg: '#ffffff', + }, + Drawer: { + borderRadiusLG: 16, + }, + Tabs: { + itemColor: '#64748b', + itemHoverColor: '#0f172a', + itemSelectedColor: '#2563eb', + inkBarColor: '#2563eb', + titleFontSize: 14, + horizontalMargin: '0 0 16px 0', + }, + Tag: { + borderRadiusSM: 6, + defaultBg: '#f1f5f9', + defaultColor: '#475569', + }, + Pagination: { + itemSize: 32, + itemSizeSM: 26, + borderRadius: 6, + }, }, } diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx index be809ec..650cf3c 100644 --- a/web/src/pages/Login.tsx +++ b/web/src/pages/Login.tsx @@ -1,6 +1,10 @@ import { useState } from 'react' -import { Form, Input, Button, Card, message } from 'antd' -import { MessageOutlined } from '@ant-design/icons' +import { Form, Input, Button, message } from 'antd' +import { + MessageOutlined, UserOutlined, LockOutlined, + SafetyCertificateOutlined, CrownOutlined, ArrowRightOutlined, + CheckCircleFilled, +} from '@ant-design/icons' import { useNavigate } from 'react-router-dom' import { useAuth } from '@/stores/auth' @@ -23,30 +27,148 @@ const Login = () => { } } + const fillCredentials = (u: string, p: string) => { + form.setFieldsValue({ username: u, password: p }) + } + return ( -
- -
-
- +
+ {/* 动态光晕与网格背景 */} +
+
+
+ + {/* 顶部品牌微标 */} +
+
+
+
-

客服云

-

在线客服系统

+ Kefu Cloud
-
- - - - - - - - - -
- +
+ + + 系统服务正常 + +
+
+ + {/* 核心登录卡片 */} +
+
+
+ {/* 顶端光效描边 */} +
+ + {/* 卡片头部 */} +
+
+ +
+

欢迎登录客服云

+

全渠道多租户智能客服工作台

+
+ + {/* 登录表单 */} +
+ +
+ + } + placeholder="请输入登录账号" + className="!h-11 !rounded-xl !bg-slate-800/80 !border-slate-700 !text-slate-100 placeholder:!text-slate-500 hover:!border-blue-500 focus:!border-blue-500 focus:!ring-2 focus:!ring-blue-500/20 transition-all text-sm" + /> +
+
+ + +
+ + } + placeholder="请输入登录密码" + className="!h-11 !rounded-xl !bg-slate-800/80 !border-slate-700 !text-slate-100 placeholder:!text-slate-500 hover:!border-blue-500 focus:!border-blue-500 focus:!ring-2 focus:!ring-blue-500/20 transition-all text-sm" + /> +
+
+ + + + +
+ + {/* 快捷体验账号 */} +
+
+ 一键填入演示账号 + 点击自动填充 +
+
+ + + +
+
+
+
+
+ + {/* 底部功能特性与版权 */} +
+
+ 多租户数据隔离 + WebSocket 实时长连 + 全渠道客服集成 +
+
+ © {new Date().getFullYear()} Kefu Cloud. All Rights Reserved. +
+
) } diff --git a/web/src/pages/admin/Dashboard.tsx b/web/src/pages/admin/Dashboard.tsx index 3162df4..77ac979 100644 --- a/web/src/pages/admin/Dashboard.tsx +++ b/web/src/pages/admin/Dashboard.tsx @@ -87,7 +87,7 @@ const AdminDashboard = () => { hint: `本月新增 ${newMonth} 家`, badge: newMonth > 0 ? `+${newMonth}` : null, icon: , - iconBg: '#dbeafe', + iconBg: '#eff6ff', iconColor: '#2563eb', }, { @@ -123,14 +123,14 @@ const AdminDashboard = () => { ] return ( -
+
-

运营概览

- {formatDate()} +

运营概览

+ {formatDate()}
@@ -140,10 +140,10 @@ const AdminDashboard = () => { ) : (
-

+

欢迎回来,{user?.nickname || '管理员'}

-

+

以下是平台当前运营数据概览 {stats?.expiring_tenant ? ` · ${stats.expiring_tenant} 家租户即将到期` : ''}

@@ -154,56 +154,56 @@ const AdminDashboard = () => { {kpis.map(k => (
{k.icon}
{k.badge && ( - + {k.badge.startsWith('+') && } {k.badge} )}
- + {k.value} - {k.unit && {k.unit}} + {k.unit && {k.unit}}
-

{k.label}

-

{k.hint}

+

{k.label}

+

{k.hint}

))} {/* 图表区 */}
-
-
+
+
-

租户增长趋势

-

近6个月新增租户数量

+

租户增长趋势

+

近6个月新增租户数量

-
+
- + 新增租户 - + 累计(缩放)
-
+
{growth.length === 0 ? ( -
暂无数据
+
暂无数据
) : (
{growth.map(g => { @@ -213,17 +213,17 @@ const AdminDashboard = () => {
- {g.month} + {g.month}
) })} @@ -232,21 +232,21 @@ const AdminDashboard = () => {
-
-
-

套餐分布占比

-

当前租户套餐类型分布

+
+
+

套餐分布占比

+

当前租户套餐类型分布

-
+
-
+
- {planTotal || stats?.tenant_total || 0} - 总计 + {planTotal || stats?.tenant_total || 0} + 总计
@@ -262,9 +262,9 @@ const AdminDashboard = () => { className="w-2.5 h-2.5 rounded-sm shrink-0" style={{ backgroundColor: planColors[i % planColors.length] }} /> - {item.name} + {item.name}
- + {item.count} · {pct}%
@@ -277,25 +277,25 @@ const AdminDashboard = () => {
{/* 最近操作 */} -
-
-

最近操作记录

-

平台管理操作审计

+
+
+

最近操作记录

+

平台管理操作审计

-
+
- - - - - + + + + + {logs.length === 0 ? ( - @@ -305,22 +305,22 @@ const AdminDashboard = () => { text: log.action, bg: '#f1f5f9', color: '#64748b', } return ( - - + - - - diff --git a/web/src/pages/admin/Ops.tsx b/web/src/pages/admin/Ops.tsx index 3bc6a66..6995c22 100644 --- a/web/src/pages/admin/Ops.tsx +++ b/web/src/pages/admin/Ops.tsx @@ -265,54 +265,55 @@ const Ops = () => { const errThresholdY = 100 - (2 / maxErr) * (100 - 8) - 4 // map 2% to viewBox y return ( -
-
-
-
-

系统运维

-

- 监控平台运行状态、查看操作日志、管理平台公告 - · 指标来自当前 API 实例 -

-
-
- - - {metrics - ? (metrics.overall === 'normal' ? '系统正常运行' : metrics.overall === 'warning' ? '存在告警' : '存在异常') - : '加载中…'} - - - 最后更新:{formatRelative(refreshedAt)} - {metrics ? ` · 运行 ${formatUptime(metrics.uptime_sec)}` : ''} - - -
+
+
+
+

系统运维

+

+ 平台运行监控、服务状态与操作审计日志 +

- {metricsError && ( -
{metricsError}
- )} -
+
+ + + {metrics + ? (metrics.overall === 'normal' ? '系统正常运行' : metrics.overall === 'warning' ? '存在告警' : '存在异常') + : '加载中…'} + + + 最后更新:{formatRelative(refreshedAt)} + {metrics ? ` · 运行 ${formatUptime(metrics.uptime_sec)}` : ''} + + +
+ + + {metricsError && ( +
{metricsError}
+ )}
-
-
- - 服务状态 +
+
+ + 服务状态 {metrics?.websocket != null && ( - + WS 连接 {metrics.websocket.connections} (坐席 {metrics.websocket.agents} / 访客 {metrics.websocket.visitors}) diff --git a/web/src/pages/admin/Plans.tsx b/web/src/pages/admin/Plans.tsx index b3a73a3..14f7e4b 100644 --- a/web/src/pages/admin/Plans.tsx +++ b/web/src/pages/admin/Plans.tsx @@ -272,18 +272,19 @@ const Plans = () => { } return ( -
+
-

套餐定价

+

套餐定价

+

全平台订阅档位配置与功能权益矩阵

diff --git a/web/src/pages/admin/Tenants.tsx b/web/src/pages/admin/Tenants.tsx index 512261d..e6a30ca 100644 --- a/web/src/pages/admin/Tenants.tsx +++ b/web/src/pages/admin/Tenants.tsx @@ -243,29 +243,27 @@ const Tenants = () => { } return ( -
+
{/* 顶栏 56px */}
-
-

租户管理

- - 共 {total} 个租户 - +
+

租户管理

+

全平台企业租户全生命周期管理与授权配额

{/* 工具栏 */} -
+
-
-
- +
+
+ setSearchInput(e.target.value)} onKeyDown={e => { @@ -274,10 +272,10 @@ const Tenants = () => { setPage(1) } }} - className="flex-1 min-w-0 bg-transparent border-0 outline-none text-sm text-neutral-800 placeholder:text-neutral-400" + className="flex-1 min-w-0 bg-transparent border-0 outline-none text-xs text-slate-800 placeholder:text-slate-400" />
-
+
{statusFilters.map(f => { const active = statusFilter === f.key || (!statusFilter && f.key === undefined) return ( @@ -285,10 +283,10 @@ const Tenants = () => { key={f.label} type="button" onClick={() => { setStatusFilter(f.key); setPage(1) }} - className={`px-3 py-1 text-xs rounded border-0 cursor-pointer transition-colors ${ + className={`px-3 py-1 text-xs rounded-lg border-0 cursor-pointer transition-all ${ active - ? 'bg-[#2563eb] text-white font-medium' - : 'bg-transparent text-neutral-600 hover:bg-neutral-50' + ? 'bg-white text-blue-600 font-semibold shadow-xs' + : 'bg-transparent text-slate-500 hover:text-slate-800' }`} > {f.label} @@ -297,15 +295,20 @@ const Tenants = () => { })}
-
{/* 表格 */} -
-
+
+
{loading ? (
) : tenants.length === 0 ? ( @@ -314,11 +317,11 @@ const Tenants = () => {
时间操作详情IP
时间操作详情IP
+ 暂无操作记录
+
{formatTime(log.created_at)} + {meta.text} + {log.detail || '—'} + {log.ip || '—'}
- + {['公司名称', '联系人', '手机号', '套餐类型', '坐席', '开通日期', '到期日期', '状态', '操作'].map((h, i) => ( openDetail(record)} > - - - - -
@@ -335,46 +338,46 @@ const Tenants = () => { return (
{record.name.slice(0, 1)}
-
{record.name}
-
+
{record.name}
+
ID: T{String(record.id).padStart(4, '0')}
+ {record.contact_name || '—'} + {maskPhone(record.contact_phone)} {pname} + {record.seat_count} + {fmtDate(record.created_at)} + {fmtDate(record.expire_at)} @@ -384,12 +387,11 @@ const Tenants = () => {
- | +
+ {/* 顶栏 56px */} +
+
+

黑名单管理

+

拦截恶意访客、封禁 IP 与设备管理

-
+ +
+ + {/* 筛选工具栏 */} +
+
+
+ setSearch(e.target.value)} + className="flex-1 min-w-0 bg-transparent border-0 outline-none text-xs text-slate-800 placeholder:text-slate-400" + /> +
setShowExpired(v === 'all')} options={[ { value: 'active', label: '仅生效中' }, - { value: 'all', label: '含已过期' }, + { value: 'all', label: '包含已过期' }, ]} /> - setSearch(e.target.value)} - />
-
+
{loading && list.length === 0 ? (
) : ( @@ -223,6 +231,8 @@ const Blacklist = () => { pageSize: 20, showTotal: t => `共 ${t} 条`, showSizeChanger: false, + size: 'small', + className: '!mt-4', }} locale={{ emptyText: '暂无黑名单记录' }} scroll={{ x: 900 }} diff --git a/web/src/pages/agent/ChatHistory.tsx b/web/src/pages/agent/ChatHistory.tsx index 44b952f..87f673e 100644 --- a/web/src/pages/agent/ChatHistory.tsx +++ b/web/src/pages/agent/ChatHistory.tsx @@ -330,114 +330,120 @@ const ChatHistory = () => { }, [events]) return ( -
- {/* 顶栏 — 全宽 */} -
-

对话记录

+
+ {/* 顶栏 — 56px */} +
+
+

对话记录

+

历史服务会话回溯、满意度评价与归档

+
{canExport && ( - + )} {canArchive && ( - + )}
- {/* 筛选栏 — 全宽,左右分栏顶边对齐 */} -
-
-
- + {/* 筛选栏 */} +
+
+
+ setSearchInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleSearch() }} - className="flex-1 min-w-0 bg-transparent border-0 outline-none text-sm text-neutral-800 placeholder:text-neutral-400" + className="flex-1 min-w-0 bg-transparent border-0 outline-none text-xs text-slate-800 placeholder:text-slate-400" />
{ setDateRange(v); setPage(1) }} - className="!h-8" + className="!h-8.5 !rounded-xl" allowClear />
-
+
- 渠道: + 渠道: { setAgentFilter(v); setPage(1) }} - className="!w-[120px]" + className="!w-[110px]" size="small" options={agents.map(a => ({ value: a.id, label: a.nickname }))} />
- 状态: + 状态: { setSearch(e.target.value); setPage(1) }} - className="flex-1 bg-transparent outline-none text-[13px] min-w-0 text-neutral-700 placeholder:text-neutral-400 border-0" + className="flex-1 bg-transparent outline-none text-xs min-w-0 text-slate-800 placeholder:text-slate-400 border-0" />
@@ -400,10 +403,10 @@ const Customers = () => { key={chip.key} type="button" onClick={() => setTagFilter(chip.key)} - className={`px-3 py-1.5 rounded-md text-[13px] font-medium whitespace-nowrap border-0 cursor-pointer transition-colors ${ + className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap border-0 cursor-pointer transition-all ${ active - ? 'bg-[#2563eb] text-white' - : 'bg-transparent text-neutral-600 hover:bg-neutral-100' + ? 'bg-blue-50 text-blue-600 font-semibold shadow-2xs' + : 'bg-slate-100 text-slate-600 hover:bg-slate-200/70' }`} > {chip.label} @@ -414,42 +417,42 @@ const Customers = () => {
{canExport && ( - + )} {canCreate && ( - + )}
{/* 表格 + 分页 */} -
-
+
+
{loading ? (
) : displayed.length === 0 ? ( @@ -458,11 +461,11 @@ const Customers = () => {
- + {['客户', '手机号', '邮箱', '来源渠道', '标签', '最后对话', '状态', '操作'].map((h, i) => ( openDetail(record)} - className={`group cursor-pointer border-t border-neutral-100 transition-colors ${ - selected ? 'bg-blue-50/50' : 'hover:bg-neutral-50' + className={`group cursor-pointer border-t border-slate-100 transition-colors ${ + selected ? 'bg-blue-50/60' : 'hover:bg-slate-50/80' }`} > - - - - - - - -
{
+
{(record.name || '?').slice(0, 1)}
-
{record.name}
-
+
{record.name}
+
ID: C{String(record.id).padStart(5, '0')}
+ {maskPhone(record.phone)} + {record.email || '—'} + {record.source || '—'} +
{parseTags(record.tags).length === 0 - ? — + ? — : parseTags(record.tags).map(t => )}
+ {relativeTime(record.last_contact_at)} + e.stopPropagation()}> + e.stopPropagation()}>
- {/* 分页 — 在表格卡片外 */} + {/* 分页 */}
-
- 共 {total} 位客户 +
+ 共 {total} 位客户
{ const [pendingImage, setPendingImage] = useState<{ file: File; preview: string } | null>(null) const [noteInput, setNoteInput] = useState('') const [savingNote, setSavingNote] = useState(false) - const [customerHistory, setCustomerHistory] = useState([]) + const [, setCustomerHistory] = useState([]) const [statusFilter, setStatusFilter] = useState<'all' | 'waiting' | 'active'>('all') const [priorityFilter, setPriorityFilter] = useState<'all' | 'urgent'>('all') const [filterOpen, setFilterOpen] = useState(false) @@ -1438,22 +1438,22 @@ const Dashboard = () => { /> {/* 会话列表面板 320px — 顶栏固定 56px,与中/右对齐 */} -
+
-
- +
+ setSearch(e.target.value)} />
{filteredSessions.length} @@ -1464,9 +1464,9 @@ const Dashboard = () => { trigger="click" placement="bottomRight" content={( -
+
-
会话状态
+
会话状态
{([ { key: 'all', label: '全部' }, @@ -1477,10 +1477,10 @@ const Dashboard = () => { key={item.key} type="button" onClick={() => setStatusFilter(item.key)} - className={`px-2 py-0.5 rounded-md text-xs border ${ + className={`px-2.5 py-1 rounded-lg text-xs border transition-all cursor-pointer ${ statusFilter === item.key - ? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]' - : 'bg-white border-neutral-200 text-neutral-600' + ? 'bg-blue-50 border-blue-400 text-blue-600 font-medium' + : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50' }`} > {item.label} @@ -1489,7 +1489,7 @@ const Dashboard = () => {
-
优先级
+
优先级
{([ { key: 'all', label: '全部' }, @@ -1499,10 +1499,10 @@ const Dashboard = () => { key={item.key} type="button" onClick={() => setPriorityFilter(item.key)} - className={`px-2 py-0.5 rounded-md text-xs border ${ + className={`px-2.5 py-1 rounded-lg text-xs border transition-all cursor-pointer ${ priorityFilter === item.key - ? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]' - : 'bg-white border-neutral-200 text-neutral-600' + ? 'bg-blue-50 border-blue-400 text-blue-600 font-medium' + : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50' }`} > {item.label} @@ -1513,7 +1513,7 @@ const Dashboard = () => { {filterActive && (
-
+
{filteredSessions.length === 0 ? ( -
暂无待处理会话
+
暂无待处理会话
) : filteredSessions.map(session => { const customer = customers[session.customer_id] const name = customer?.name || `客户${session.customer_id}` @@ -1559,52 +1559,60 @@ const Dashboard = () => { key={session.id} type="button" onClick={() => setSelectedId(session.id)} - className={`w-full flex items-stretch text-left border-b border-neutral-100 transition-colors ${ - selected ? 'bg-[#eff6ff]' : 'hover:bg-neutral-50' + className={`w-full flex items-stretch text-left rounded-xl transition-all duration-150 relative border cursor-pointer ${ + selected + ? 'bg-blue-50/80 border-blue-200/80 shadow-xs' + : 'bg-white border-transparent hover:bg-slate-50/80 hover:border-slate-100' }`} > -
-
+ {/* 活跃/紧急侧边标记 */} + {(selected || session.priority === 'urgent' || session.status === 'waiting') && ( +
+ )} +
{name.slice(0, 1)}
- {name} + {name}
- + {relativeTime(session.last_message_at || session.created_at)} {session.unread_count > 0 && ( - + {session.unread_count > 99 ? '99+' : session.unread_count} )}
-
-

+

+

{session.last_message || '暂无消息'}

- + {meta.label}
{session.status === 'waiting' && user?.role === 'agent' && ( -
+
{ e.stopPropagation(); handleClaim(session.id) }} onKeyDown={e => { if (e.key === 'Enter') { e.stopPropagation(); handleClaim(session.id) } }} > - 领取会话 + 领取会话 →
)} @@ -1616,16 +1624,16 @@ const Dashboard = () => {
{/* 主聊天区 */} -
+
{selected && selectedCustomer ? ( <>
{
- {selectedCustomer.name} - + {selectedCustomer.name} + {channelLabel(selectedCustomer.source)} - - + + {selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
- {/* IP | 地区 — 压缩行高,保证三栏顶栏等高 */} -
+ {/* IP | 地区 | 在线时长 */} +
IP: {selected.visitor_ip || '—'} - | + · - {selected.visitor_region || '未知'} + {selected.visitor_region || '未知地区'} - | + · 在线 {formatOnlineDuration( selected.created_at, @@ -1664,7 +1672,7 @@ const Dashboard = () => {
-
+
{ }} disabled={!canOperate} > - - - - -
-
+
{detailLoading ? (
) : ( <> -
- +
+ 会话开始 — {new Date(selected.created_at).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
@@ -1717,10 +1725,10 @@ const Dashboard = () => { const { event } = item return (
- - {eventLabel(event.action)} + + {eventLabel(event.action)} {event.detail || eventLabel(event.action)} - + {new Date(event.created_at).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', })} @@ -1738,33 +1746,33 @@ const Dashboard = () => { className={`flex items-start gap-2.5 mb-4 ${isAgent ? 'flex-row-reverse' : ''}`} >
{isAgent ? agentInitial : visitorInitial}
-
+
{message.type === 'image' ? ( - + ) : ( {message.content} )}
-
+
{new Date(message.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
@@ -1772,12 +1780,12 @@ const Dashboard = () => { ) })} {chatTimeline.length === 0 && ( -
暂无消息,开始对话吧
+
暂无消息,开始对话吧
)} {visitorTyping && selectedCustomer && (
{ {selectedCustomer.name.slice(0, 1)}
-
+
-
访客正在输入…
+
访客正在输入…
)} @@ -1800,12 +1808,12 @@ const Dashboard = () => { )}
-
- {/* 访客实时输入草稿:展示在工具栏/输入框上方 */} +
+ {/* 访客实时输入草稿 */} {(visitorDraft.trim() || visitorTyping) && ( -
-
- +
+
+ 对方正在输入 {visitorTyping && ( @@ -1817,48 +1825,52 @@ const Dashboard = () => { {visitorDraft.trim() ? ':' : ''} {visitorDraft.trim() ? ( - + {visitorDraft.trim()} ) : ( - … + … )}
)} {selected.status !== 'active' || !canOperate ? ( -
+
{selected.status === 'waiting' ? '领取会话后即可回复' : '会话已结束,无法继续发送消息'}
) : ( <> -
+
- - -
+
- - 输入 / 调用话术 + 按 Enter 发送,Shift+Enter 换行
{suggestOpen && (suggestMode === 'slash' || keywordQuery.trim().length >= 2) && ( -
-
+
+
{suggestMode === 'slash' ? (slashPrefix ? `输入码 /${slashPrefix}` : '常用话术(按你的调用频率)') @@ -1867,14 +1879,14 @@ const Dashboard = () => { ↑↓ 选择 · Enter 填入 · Esc 关闭
{suggestItems.length === 0 ? ( -
无匹配话术
+
无匹配话术
) : ( suggestItems.map((item, idx) => ( )) )} @@ -1902,8 +1914,8 @@ const Dashboard = () => {