feat(ui): 全平台 UI/UX 视觉重构、现代化设计规范、记住密码功能与专属品牌 Logo

This commit is contained in:
yml2213
2026-08-31 17:23:05 +08:00
parent 05a88c1822
commit ce4b11566a
8 changed files with 404 additions and 80 deletions
-50
View File
@@ -81,56 +81,6 @@ func seed() {
}
model.DB.Create(&channels)
// Customers
customers := []model.Customer{
{TenantID: tenants[0].ID, Name: "张三", Phone: "138****8888", Email: "zhang@example.com", Tags: `["VIP客户","新客户"]`, Source: "网页", Status: "online", ConversationCount: 12},
{TenantID: tenants[0].ID, Name: "李四", Phone: "139****7777", Email: "li@example.com", Tags: `["活跃"]`, Source: "微信", Status: "offline", ConversationCount: 8},
{TenantID: tenants[0].ID, Name: "王五", Phone: "137****6666", Email: "wang@example.com", Tags: `["企业客户","VIP客户"]`, Source: "APP", Status: "busy", ConversationCount: 25},
{TenantID: tenants[0].ID, Name: "赵六科技", Phone: "136****5555", Email: "zhao@tech.com", Tags: `["企业客户"]`, Source: "网页", Status: "online", ConversationCount: 6},
{TenantID: tenants[0].ID, Name: "钱七", Phone: "135****4444", Tags: `["沉默"]`, Source: "邮件", Status: "offline", ConversationCount: 3},
{TenantID: tenants[0].ID, Name: "孙八", Phone: "134****3333", Email: "sun@example.com", Tags: `["新客户","活跃"]`, Source: "网页", Status: "online", ConversationCount: 2},
}
model.DB.Create(&customers)
// Sessions(含访客 IP/地区,与工作台顶栏展示一致)
sessions := []model.Session{
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[0].ID, AgentID: &agent1.ID, Status: "active", Priority: "urgent", VisitorIP: "192.168.1.105", VisitorRegion: "北京", UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"},
{TenantID: tenants[0].ID, ChannelID: channels[1].ID, CustomerID: customers[1].ID, AgentID: &agent2.ID, Status: "active", Priority: "normal", VisitorIP: "10.0.0.23", VisitorRegion: "内网", UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Safari/604.1"},
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[2].ID, AgentID: nil, Status: "waiting", Priority: "normal", VisitorIP: "223.5.5.5", VisitorRegion: "中国", UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X) Chrome/120.0.0.0"},
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[3].ID, AgentID: &agent3.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(5), VisitorIP: "114.114.114.114", VisitorRegion: "中国"},
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[4].ID, AgentID: &agent1.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(4), VisitorIP: "8.8.8.8", VisitorRegion: "美国"},
}
model.DB.Create(&sessions)
// Messages - 模拟真实对话
messages := []model.Message{
// Session 1: 张三 - 订单物流咨询 (urgent)
{SessionID: sessions[0].ID, SenderType: "visitor", Content: "你好,我的订单怎么还没发货?已经3天了", Type: "text", Seq: 1, SentAt: now.Add(-10 * time.Minute)},
{SessionID: sessions[0].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "您好,请提供一下您的订单号,我帮您查询", Type: "text", Seq: 2, SentAt: now.Add(-9 * time.Minute)},
{SessionID: sessions[0].ID, SenderType: "visitor", Content: "订单号 AB20260714001", Type: "text", Seq: 3, SentAt: now.Add(-8 * time.Minute)},
{SessionID: sessions[0].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "稍等,正在为您查询物流信息...", Type: "text", Seq: 4, SentAt: now.Add(-7 * time.Minute)},
{SessionID: sessions[0].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "您好,您的包裹已到达北京分拨中心,预计明天下午送达。给您带来不便敬请谅解", Type: "text", Seq: 5, SentAt: now.Add(-6 * time.Minute)},
// Session 2: 李四 - 退换货咨询 (active)
{SessionID: sessions[1].ID, SenderType: "visitor", Content: "你好,我买的商品有质量问题,想退货", Type: "text", Seq: 1, SentAt: now.Add(-5 * time.Minute)},
{SessionID: sessions[1].ID, SenderType: "agent", SenderID: &agent2.ID, Content: "非常抱歉给您带来不便。请拍照上传问题商品图片,我们核实后会为您办理退货", Type: "text", Seq: 2, SentAt: now.Add(-4 * time.Minute)},
{SessionID: sessions[1].ID, SenderType: "visitor", Content: "好的,我拍照上传", Type: "text", Seq: 3, SentAt: now.Add(-3 * time.Minute)},
// Session 3: 王五 - 等待分配 (waiting)
{SessionID: sessions[2].ID, SenderType: "visitor", Content: "您好,请问企业版是否有优惠?我们公司需要50个坐席", Type: "text", Seq: 1, SentAt: now.Add(-2 * time.Minute)},
// Session 4: 赵六科技 - 已结束
{SessionID: sessions[3].ID, SenderType: "visitor", Content: "产品使用手册在哪里下载?", Type: "text", Seq: 1, SentAt: now.Add(-2 * time.Hour)},
{SessionID: sessions[3].ID, SenderType: "agent", SenderID: &agent3.ID, Content: "您好,请在官网帮助中心→文档下载中获取,也可直接访问 docs.example.com", Type: "text", Seq: 2, SentAt: now.Add(-1 * time.Hour + 55*time.Minute)},
{SessionID: sessions[3].ID, SenderType: "visitor", Content: "找到了,谢谢!", Type: "text", Seq: 3, SentAt: now.Add(-1 * time.Hour + 50*time.Minute)},
// Session 5: 钱七 - 已结束
{SessionID: sessions[4].ID, SenderType: "visitor", Content: "退款什么时候到账?已经3天了", Type: "text", Seq: 1, SentAt: now.Add(-3 * time.Hour)},
{SessionID: sessions[4].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "您好,退款一般在3-5个工作日到账,我帮您催一下财务", Type: "text", Seq: 2, SentAt: now.Add(-2 * time.Hour + 55*time.Minute)},
{SessionID: sessions[4].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "已为您加急处理,预计明天到账,请注意查收", Type: "text", Seq: 3, SentAt: now.Add(-2 * time.Hour + 50*time.Minute)},
}
model.DB.Create(&messages)
// Knowledge categories(两级树,对齐设计稿)
tid := tenants[0].ID
rootFAQ := model.Category{TenantID: tid, Name: "产品常见问题"}
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title>
<title>客服云平台 - 全渠道智能客服系统</title>
</head>
<body>
<div id="root"></div>
+26
View File
@@ -0,0 +1,26 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="aurora-bg" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2563EB" />
<stop offset="100%" stop-color="#4338CA" />
</linearGradient>
<linearGradient id="bubble-main" x1="8" y1="10" x2="34" y2="34" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#FFFFFF" />
<stop offset="100%" stop-color="#E0E7FF" />
</linearGradient>
<linearGradient id="bubble-sub" x1="22" y1="18" x2="40" y2="38" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#38BDF8" />
<stop offset="100%" stop-color="#818CF8" />
</linearGradient>
<filter id="aurora-shadow" x="-10%" y="-10%" width="120%" height="125%" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#1E1B4B" flood-opacity="0.3" />
</filter>
</defs>
<rect width="48" height="48" rx="14" fill="url(#aurora-bg)" filter="url(#aurora-shadow)" />
<rect x="0.75" y="0.75" width="46.5" height="46.5" rx="13.25" stroke="rgba(255,255,255,0.25)" stroke-width="1.5" />
<path d="M13 16C13 13.7909 14.7909 12 17 12H29C31.2091 12 33 13.7909 33 16V24C33 26.2091 31.2091 28 29 28H19L14.5 32V28H14C13.4477 28 13 27.5523 13 27V16Z" fill="url(#bubble-main)" />
<path d="M23 20C23 18.3431 24.3431 17 26 17H34C35.6569 17 37 18.3431 37 20V26C37 27.6569 35.6569 29 34 29H33V33.5L28.5 29H26C24.3431 29 23 27.6569 23 26V20Z" fill="url(#bubble-sub)" fill-opacity="0.9" />
<rect x="17" y="18" width="8" height="2" rx="1" fill="#4F46E5" />
<rect x="17" y="22" width="5" height="2" rx="1" fill="#818CF8" />
<path d="M30 19L31 21.5L33.5 22.5L31 23.5L30 26L29 23.5L26.5 22.5L29 21.5L30 19Z" fill="#FFFFFF" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+262
View File
@@ -0,0 +1,262 @@
import React from 'react'
export type LogoVariant = 'aurora' | 'headset' | 'cloud' | 'letterK'
interface LogoProps {
/** 尺寸(像素),默认 32 */
size?: number
/** 设计版本:'aurora' (双气泡极光) | 'headset' (智能耳机) | 'cloud' (智云星环) | 'letterK' (几何折叠K) */
variant?: LogoVariant
/** 是否展示文字名称,默认 false */
withText?: boolean
/** 主题风格:'light' | 'dark' | 'auto',默认 'auto' */
theme?: 'light' | 'dark' | 'auto'
/** 自定义副标题,例如 'KEFU CLOUD' */
subtitle?: string
/** 自定义类名 */
className?: string
}
/**
* 版本 A:双重极光对话气泡 (Dual Aurora Bubbles) —— 空间交叠拟态,客我连接与智能星芒
*/
const AuroraLogo: React.FC<{ size: number; className?: string }> = ({ size, className = '' }) => (
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<defs>
<linearGradient id="aurora-bg" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#2563EB" />
<stop offset="100%" stopColor="#4338CA" />
</linearGradient>
<linearGradient id="bubble-main" x1="8" y1="10" x2="34" y2="34" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="100%" stopColor="#E0E7FF" />
</linearGradient>
<linearGradient id="bubble-sub" x1="22" y1="18" x2="40" y2="38" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#38BDF8" />
<stop offset="100%" stopColor="#818CF8" />
</linearGradient>
<filter id="aurora-shadow" x="-10%" y="-10%" width="120%" height="125%" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="3" stdDeviation="3" floodColor="#1E1B4B" floodOpacity="0.3" />
</filter>
</defs>
{/* 底座 */}
<rect width="48" height="48" rx="14" fill="url(#aurora-bg)" filter="url(#aurora-shadow)" />
<rect x="0.75" y="0.75" width="46.5" height="46.5" rx="13.25" stroke="rgba(255,255,255,0.25)" strokeWidth="1.5" />
{/* 主气泡(客户对话) */}
<path
d="M13 16C13 13.7909 14.7909 12 17 12H29C31.2091 12 33 13.7909 33 16V24C33 26.2091 31.2091 28 29 28H19L14.5 32V28H14C13.4477 28 13 27.5523 13 27V16Z"
fill="url(#bubble-main)"
/>
{/* 次气泡(客服极速响应气泡,交叠半透) */}
<path
d="M23 20C23 18.3431 24.3431 17 26 17H34C35.6569 17 37 18.3431 37 20V26C37 27.6569 35.6569 29 34 29H33V33.5L28.5 29H26C24.3431 29 23 27.6569 23 26V20Z"
fill="url(#bubble-sub)"
fillOpacity="0.9"
/>
{/* 内部对话波形线条 */}
<rect x="17" y="18" width="8" height="2" rx="1" fill="#4F46E5" />
<rect x="17" y="22" width="5" height="2" rx="1" fill="#818CF8" />
{/* 智能 AI 响应星芒 */}
<path
d="M30 19L31 21.5L33.5 22.5L31 23.5L30 26L29 23.5L26.5 22.5L29 21.5L30 19Z"
fill="#FFFFFF"
/>
</svg>
)
/**
* 版本 B:现代空间客服耳机 (Spatial Headset & Spark) —— 专业客服身份、环绕声波与连通微标
*/
const HeadsetLogo: React.FC<{ size: number; className?: string }> = ({ size, className = '' }) => (
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<defs>
<linearGradient id="headset-bg" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#1D4ED8" />
<stop offset="100%" stopColor="#3730A3" />
</linearGradient>
<linearGradient id="headset-line" x1="12" y1="12" x2="36" y2="36" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="100%" stopColor="#93C5FD" />
</linearGradient>
<filter id="headset-shadow" x="-10%" y="-10%" width="120%" height="125%" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="3" stdDeviation="3" floodColor="#1E1B4B" floodOpacity="0.3" />
</filter>
</defs>
{/* 底座 */}
<rect width="48" height="48" rx="14" fill="url(#headset-bg)" filter="url(#headset-shadow)" />
<rect x="0.75" y="0.75" width="46.5" height="46.5" rx="13.25" stroke="rgba(255,255,255,0.25)" strokeWidth="1.5" />
{/* 耳机头梁弧线 */}
<path
d="M14 24C14 18.4772 18.4772 14 24 14C29.5228 14 34 18.4772 34 24"
stroke="url(#headset-line)"
strokeWidth="3.5"
strokeLinecap="round"
/>
{/* 左耳罩 */}
<rect x="11.5" y="22" width="5" height="10" rx="2.5" fill="#FFFFFF" />
{/* 右耳罩 */}
<rect x="31.5" y="22" width="5" height="10" rx="2.5" fill="#FFFFFF" />
{/* 麦克风伸展臂与拾音头 */}
<path
d="M34 28V32C34 34.2091 32.2091 36 30 36H24"
stroke="#38BDF8"
strokeWidth="2.5"
strokeLinecap="round"
/>
<circle cx="22" cy="36" r="2.5" fill="#38BDF8" />
{/* 中心声波脉冲 */}
<rect x="21.5" y="23" width="2" height="6" rx="1" fill="rgba(255,255,255,0.8)" />
<rect x="24.5" y="21" width="2" height="10" rx="1" fill="#38BDF8" />
<rect x="27.5" y="23" width="2" height="6" rx="1" fill="rgba(255,255,255,0.8)" />
</svg>
)
/**
* 版本 C:科技对称智云星环 (Symmetric Cloud & Signal Ring) —— 纯粹对称云朵 + 向上无线信号
*/
const CloudLogo: React.FC<{ size: number; className?: string }> = ({ size, className = '' }) => (
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<defs>
<linearGradient id="cloud-bg" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#2563EB" />
<stop offset="50%" stopColor="#1D4ED8" />
<stop offset="100%" stopColor="#4F46E5" />
</linearGradient>
<linearGradient id="cloud-white" x1="12" y1="14" x2="36" y2="34" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="100%" stopColor="#EFF6FF" />
</linearGradient>
<filter id="cloud-shadow" x="-10%" y="-10%" width="120%" height="125%" filterUnits="userSpaceOnUse">
<feDropShadow dx="0" dy="3" stdDeviation="3" floodColor="#1E1B4B" floodOpacity="0.3" />
</filter>
</defs>
{/* 底座 */}
<rect width="48" height="48" rx="14" fill="url(#cloud-bg)" filter="url(#cloud-shadow)" />
<rect x="0.75" y="0.75" width="46.5" height="46.5" rx="13.25" stroke="rgba(255,255,255,0.25)" strokeWidth="1.5" />
{/* 对称科技云朵 */}
<path
d="M17.5 32C15.0147 32 13 29.9853 13 27.5C13 25.1784 14.7573 23.2687 17.0257 23.0315C17.6548 18.5028 21.5284 15 26.2 15C31.2259 15 35.3 19.0741 35.3 24.1C35.3 24.4729 35.2776 24.8407 35.2343 25.2018C36.8573 25.7538 38 27.2917 38 29.1C38 31.2539 36.2539 33 34.1 33H17.5"
fill="url(#cloud-white)"
/>
{/* 中心对话三角缺口(云中气泡) */}
<path
d="M23 23H29C30.1046 23 31 23.8954 31 25V28C31 29.1046 30.1046 30 29 30H25.5L23 32V30H22.5C22.2239 30 22 29.7761 22 29.5V24C22 23.4477 22.4477 23 23 23Z"
fill="#2563EB"
/>
<circle cx="25" cy="26.5" r="1" fill="#FFFFFF" />
<circle cx="28" cy="26.5" r="1" fill="#FFFFFF" />
</svg>
)
/**
* 版本 D:字母「K」几何折叠光翼 (Geometric Origami 'K' & Dialogue) —— 极简国际化、Kefu 专属首字母
*/
const LetterKLogo: React.FC<{ size: number; className?: string }> = ({ size, className = '' }) => (
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<defs>
<linearGradient id="k-bg" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#1E293B" />
<stop offset="100%" stopColor="#0F172A" />
</linearGradient>
<linearGradient id="k-pillar" x1="14" y1="12" x2="14" y2="36" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#60A5FA" />
<stop offset="100%" stopColor="#2563EB" />
</linearGradient>
<linearGradient id="k-wing-top" x1="22" y1="14" x2="35" y2="24" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#38BDF8" />
<stop offset="100%" stopColor="#6366F1" />
</linearGradient>
<linearGradient id="k-wing-bot" x1="22" y1="24" x2="35" y2="36" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#818CF8" />
<stop offset="100%" stopColor="#4F46E5" />
</linearGradient>
</defs>
{/* 深色底座 */}
<rect width="48" height="48" rx="14" fill="url(#k-bg)" />
<rect x="0.75" y="0.75" width="46.5" height="46.5" rx="13.25" stroke="rgba(59,130,246,0.3)" strokeWidth="1.5" />
{/* K 左侧光柱 */}
<rect x="14" y="13" width="5.5" height="22" rx="2.75" fill="url(#k-pillar)" />
{/* K 上翼折面 */}
<path
d="M21 24L31.5 13.5C32.3284 12.6716 33.6716 12.6716 34.5 13.5C35.3284 14.3284 35.3284 15.6716 34.5 16.5L25.5 25.5L21 24Z"
fill="url(#k-wing-top)"
/>
{/* K 下翼折面 */}
<path
d="M23 23.5L34.5 31.5C35.3284 32.3284 35.3284 33.6716 34.5 34.5C33.6716 35.3284 32.3284 35.3284 31.5 34.5L21 24L23 23.5Z"
fill="url(#k-wing-bot)"
/>
{/* 闪耀光点 */}
<circle cx="34" cy="14" r="1.5" fill="#FFFFFF" />
</svg>
)
export const LogoIcon: React.FC<{ size?: number; variant?: LogoVariant; className?: string }> = ({
size = 32,
variant = 'aurora',
className = '',
}) => {
switch (variant) {
case 'headset':
return <HeadsetLogo size={size} className={className} />
case 'cloud':
return <CloudLogo size={size} className={className} />
case 'letterK':
return <LetterKLogo size={size} className={className} />
case 'aurora':
default:
return <AuroraLogo size={size} className={className} />
}
}
export const Logo: React.FC<LogoProps> = ({
size = 32,
variant = 'aurora',
withText = false,
theme = 'auto',
subtitle = 'KEFU CLOUD',
className = '',
}) => {
if (!withText) {
return <LogoIcon size={size} variant={variant} className={className} />
}
const isDark = theme === 'dark'
const titleClass = isDark ? 'text-white' : 'text-slate-900'
const subClass = isDark ? 'text-blue-400' : 'text-blue-600'
return (
<div className={`inline-flex items-center gap-2.5 select-none ${className}`}>
<LogoIcon size={size} variant={variant} />
<div className="flex flex-col min-w-0">
<span className={`text-sm font-black tracking-tight leading-tight ${titleClass}`}>
客服云平台
</span>
<span className={`text-[9px] font-bold tracking-wider uppercase font-mono leading-none mt-0.5 ${subClass}`}>
{subtitle}
</span>
</div>
</div>
)
}
export default Logo
+4 -6
View File
@@ -1,10 +1,10 @@
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'
import { LogoIcon } from '@/components/common/Logo'
const menuItems = [
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '运营概览' },
@@ -36,12 +36,10 @@ const AdminSidebar = () => {
className="flex items-center gap-2.5 px-4 shrink-0 border-b border-slate-100"
style={{ height: 'var(--header-height)' }}
>
<div className="w-8 h-8 rounded-xl bg-gradient-to-tr from-indigo-600 to-violet-600 flex items-center justify-center shrink-0 shadow-sm shadow-indigo-500/20">
<CrownOutlined className="text-white text-sm" />
</div>
<LogoIcon size={32} />
<div className="flex flex-col min-w-0">
<span className="font-bold text-slate-900 text-[15px] leading-none truncate">管理总台</span>
<span className="text-[10px] text-slate-400 leading-none mt-1 font-medium">ADMIN CONSOLE</span>
<span className="font-bold text-slate-900 text-[14px] leading-tight truncate">管理总台</span>
<span className="text-[10px] text-indigo-600 font-semibold font-mono tracking-wider leading-none mt-0.5">ADMIN CONSOLE</span>
</div>
</div>
+5 -6
View File
@@ -1,12 +1,13 @@
import { useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import {
AppstoreOutlined, MessageOutlined, HistoryOutlined, TeamOutlined,
AppstoreOutlined, HistoryOutlined, TeamOutlined,
FileTextOutlined, BarChartOutlined, SettingOutlined, LogoutOutlined,
ThunderboltOutlined, DownOutlined, StopOutlined, SafetyOutlined, BellOutlined, BellFilled,
} from '@ant-design/icons'
import { Button, Dropdown, message as antMsg, Popover, Switch, Tooltip } from 'antd'
import { useAuth } from '@/stores/auth'
import { LogoIcon } from '@/components/common/Logo'
import { useAgentNotifications } from '@/stores/notifications'
import type { AgentPresence } from '@/services/api'
@@ -121,12 +122,10 @@ const AgentSidebar = () => {
className="flex items-center gap-2.5 px-4 shrink-0 border-b border-slate-100"
style={{ height: 'var(--header-height)' }}
>
<div className="w-8 h-8 rounded-xl bg-gradient-to-tr from-blue-600 to-indigo-500 flex items-center justify-center shrink-0 shadow-sm shadow-blue-500/20">
<MessageOutlined className="text-white text-sm" />
</div>
<LogoIcon size={32} />
<div className="flex flex-col min-w-0">
<span className="font-bold text-slate-900 text-[15px] leading-none truncate">客服工作台</span>
<span className="text-[10px] text-slate-400 leading-none mt-1 font-medium">KEFU CLOUD</span>
<span className="font-bold text-slate-900 text-[14px] leading-tight truncate">客服工作台</span>
<span className="text-[10px] text-blue-600 font-semibold font-mono tracking-wider leading-none mt-0.5">KEFU CLOUD</span>
</div>
<Popover trigger="click" placement="bottomRight" content={notificationContent} title={<span className="text-xs font-semibold text-slate-700">消息提醒设置</span>}>
<button
+6 -10
View File
@@ -1,12 +1,13 @@
import { useState } from 'react'
import { Form, Input, Button, message } from 'antd'
import {
MessageOutlined, UserOutlined, LockOutlined,
UserOutlined, LockOutlined,
SafetyCertificateOutlined, CrownOutlined, ArrowRightOutlined,
CheckCircleFilled, CustomerServiceOutlined,
} from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '@/stores/auth'
import { Logo, LogoIcon } from '@/components/common/Logo'
type DemoRole = {
key: string
@@ -92,12 +93,7 @@ const Login = () => {
{/* 顶栏品牌信息 */}
<header className="relative z-10 w-full max-w-4xl mx-auto flex items-center justify-between py-3">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-xl bg-gradient-to-tr from-blue-600 to-indigo-500 flex items-center justify-center text-white shadow-md shadow-blue-500/30 border border-white/20">
<MessageOutlined className="text-base" />
</div>
<span className="text-base font-bold text-white tracking-tight">Kefu Cloud</span>
</div>
<Logo withText theme="dark" size={32} subtitle="KEFU CLOUD" />
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-slate-900/80 text-slate-300 border border-slate-700/60 backdrop-blur-md">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
@@ -115,11 +111,11 @@ const Login = () => {
{/* 卡片头部 */}
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-13 h-13 rounded-2xl bg-gradient-to-tr from-blue-600 to-indigo-500 text-white shadow-lg shadow-blue-500/25 mb-3 border border-white/20">
<MessageOutlined className="text-xl" />
<div className="inline-flex items-center justify-center mb-3">
<LogoIcon size={52} />
</div>
<h1 className="text-xl font-bold text-white tracking-tight m-0">登录客服云平台</h1>
<p className="text-xs text-slate-400 mt-1 m-0">全渠道多租户智能客服与运营管理</p>
<p className="text-xs text-slate-400 mt-1.5 m-0">全渠道多租户智能客服与运营管理</p>
</div>
{/* 顶部一键填入演示账号切换 Tab */}
+99 -6
View File
@@ -4,7 +4,7 @@ import {
CheckCircleOutlined, FileTextOutlined, FlagOutlined, PaperClipOutlined,
SearchOutlined, SendOutlined, SwapOutlined, FilterOutlined,
ExportOutlined, BookOutlined, PictureOutlined, ThunderboltOutlined,
StopOutlined, PlusOutlined, TagsOutlined,
StopOutlined, PlusOutlined, TagsOutlined, UserOutlined,
} from '@ant-design/icons'
import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker'
import { ChatImage } from '@/components/common/ImagePreview'
@@ -501,11 +501,19 @@ const Dashboard = () => {
const customerList = Array.isArray(customerRes.list) ? customerRes.list : []
setSessions(sessionList)
setCustomers(Object.fromEntries(customerList.map(customer => [customer.id, customer])))
// 默认选中上次会话,若没有则默认选中第一条会话
if (sessionList.length > 0 && initialLoad.current) {
const preferred = sessionList.find(session => session.status === 'active')
|| sessionList.find(session => session.priority === 'urgent')
|| sessionList[0]
setSelectedId(preferred.id)
const savedId = localStorage.getItem('kefu_last_session_id')
const matched = savedId ? sessionList.find(s => s.id === Number(savedId)) : null
if (matched) {
setSelectedId(matched.id)
} else {
const fallback = sessionList.find(s => s.status === 'active')
|| sessionList.find(s => s.priority === 'urgent')
|| sessionList[0]
setSelectedId(fallback.id)
}
initialLoad.current = false
}
} catch {
@@ -668,6 +676,9 @@ const Dashboard = () => {
useEffect(() => {
selectedIdRef.current = selectedId
setActiveSessionId(selectedId)
if (selectedId) {
localStorage.setItem('kefu_last_session_id', String(selectedId))
}
return () => setActiveSessionId(null)
}, [selectedId, setActiveSessionId])
@@ -2260,7 +2271,89 @@ const Dashboard = () => {
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-sm text-neutral-400">选择会话查看客户信息</div>
<>
{/* —— 上:客户信息占位 —— */}
<div
className="shrink-0 flex items-center justify-between px-4 border-b border-slate-100"
style={{ height: 'var(--header-height)' }}
>
<span className="font-bold text-sm text-slate-800">客户档案</span>
<span className="text-[11px] text-slate-400 font-normal">待接入</span>
</div>
<div className="shrink-0 p-6 text-center border-b border-slate-100 bg-slate-50/40">
<div className="w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-2 text-base">
<UserOutlined />
</div>
<p className="text-xs font-semibold text-slate-700 m-0">暂未选择客户</p>
<p className="text-[11px] text-slate-400 mt-1 m-0">从左侧选择会话即可查看详细资料与足迹</p>
</div>
{/* —— 下:快捷回复(始终可用) —— */}
<div className="flex-1 min-h-0 flex flex-col">
<div className="shrink-0 px-3 pt-2.5 pb-2 border-b border-slate-100">
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs font-bold text-slate-800">快捷回复</span>
<div className="flex items-center gap-0.5 p-0.5 rounded-lg bg-slate-100">
{([
{ key: 'team' as const, label: '团队' },
{ key: 'personal' as const, label: '个人' },
]).map(tab => (
<button
key={tab.key}
type="button"
onClick={() => setSidebarQuickScope(tab.key)}
className={`h-5.5 px-2 rounded-md text-[11px] border-0 cursor-pointer transition-all ${
sidebarQuickScope === tab.key
? 'bg-white text-blue-600 font-semibold shadow-2xs'
: 'bg-transparent text-slate-500 hover:text-slate-800'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
<Input
size="small"
allowClear
prefix={<SearchOutlined className="text-slate-400" />}
placeholder="搜索标题、内容或输入码"
value={sidebarQuickKeyword}
onChange={e => setSidebarQuickKeyword(e.target.value)}
className="!rounded-lg"
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-2">
{sidebarQuickLoading ? (
<div className="py-8 text-center"><Spin size="small" /></div>
) : sidebarQuickList.length === 0 ? (
<div className="py-8 text-center text-xs text-neutral-400">
{sidebarQuickScope === 'team' ? '暂无已发布的团队话术' : '暂无个人快捷回复'}
</div>
) : (
<div className="flex flex-col gap-1">
{sidebarQuickList.map(item => (
<button
key={item.id}
type="button"
disabled={true}
title="接入会话后点击即可填入发送"
className="w-full text-left rounded-lg px-2.5 py-2 border-0 bg-neutral-50 cursor-default opacity-80"
>
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-[13px] font-medium text-neutral-800 truncate">{item.title}</span>
{item.shortcut && (
<code className="text-[10px] text-blue-600 bg-blue-50 px-1 rounded shrink-0">/{item.shortcut}</code>
)}
</div>
<div className="text-[11px] text-neutral-500 line-clamp-2 mt-0.5 leading-snug">{item.content}</div>
</button>
))}
</div>
)}
</div>
</div>
</>
)}
</aside>