feat(ui): 全平台 UI/UX 视觉重构、现代化设计规范、记住密码功能与专属品牌 Logo
This commit is contained in:
+6
-10
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user