实现对话记录页并对齐工作台三栏布局
完善会话列表筛选与摘要字段、归档接口;对话记录按效果图重构;工作台顶栏等高对齐;默认管理员账号改为 kefu_admin / kefu_admin123。
This commit is contained in:
+112
-107
@@ -451,111 +451,113 @@ const Dashboard = () => {
|
||||
onChange={event => { handleImage(event.target.files?.[0]); event.currentTarget.value = '' }}
|
||||
/>
|
||||
|
||||
{/* 会话列表面板 320px */}
|
||||
{/* 会话列表面板 320px — 顶栏固定 56px,与中/右对齐 */}
|
||||
<section className="w-[320px] shrink-0 flex flex-col h-full border-r border-neutral-200 bg-white">
|
||||
<div className="shrink-0 px-3 py-3 border-b border-neutral-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center flex-1 min-w-0 rounded-lg px-2.5 h-[34px] bg-neutral-100 border border-neutral-200">
|
||||
<SearchOutlined className="text-neutral-400 text-xs mr-1.5 shrink-0" />
|
||||
<input
|
||||
className="bg-transparent border-none outline-none flex-1 min-w-0 text-sm text-neutral-900 placeholder:text-neutral-400"
|
||||
placeholder="搜索访客或会话"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
open={filterOpen}
|
||||
onOpenChange={setFilterOpen}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
content={(
|
||||
<div className="w-52 space-y-3">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400 mb-1.5">会话状态</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{([
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'waiting', label: '等待中' },
|
||||
{ key: 'active', label: '进行中' },
|
||||
] as const).map(item => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setStatusFilter(item.key)}
|
||||
className={`px-2 py-0.5 rounded-md text-xs border ${
|
||||
statusFilter === item.key
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-white border-neutral-200 text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="shrink-0 px-3 flex items-center gap-2 border-b border-neutral-200"
|
||||
style={{ height: 'var(--header-height)' }}
|
||||
>
|
||||
<div className="flex items-center flex-1 min-w-0 rounded-lg px-2.5 h-8 bg-neutral-100 border border-neutral-200">
|
||||
<SearchOutlined className="text-neutral-400 text-xs mr-1.5 shrink-0" />
|
||||
<input
|
||||
className="bg-transparent border-none outline-none flex-1 min-w-0 text-sm text-neutral-900 placeholder:text-neutral-400"
|
||||
placeholder="搜索访客或会话"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="inline-flex items-center h-8 px-2 rounded-md text-xs font-medium bg-[#dbeafe] text-[#2563eb] whitespace-nowrap shrink-0"
|
||||
title={`当前会话 ${filteredSessions.length} 个`}
|
||||
>
|
||||
{filteredSessions.length}
|
||||
</span>
|
||||
<Popover
|
||||
open={filterOpen}
|
||||
onOpenChange={setFilterOpen}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
content={(
|
||||
<div className="w-52 space-y-3">
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400 mb-1.5">会话状态</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{([
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'waiting', label: '等待中' },
|
||||
{ key: 'active', label: '进行中' },
|
||||
] as const).map(item => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setStatusFilter(item.key)}
|
||||
className={`px-2 py-0.5 rounded-md text-xs border ${
|
||||
statusFilter === item.key
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-white border-neutral-200 text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400 mb-1.5">优先级</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{([
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'urgent', label: '仅紧急' },
|
||||
] as const).map(item => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setPriorityFilter(item.key)}
|
||||
className={`px-2 py-0.5 rounded-md text-xs border ${
|
||||
priorityFilter === item.key
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-white border-neutral-200 text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{filterActive && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-neutral-500 hover:text-neutral-700"
|
||||
onClick={() => { setStatusFilter('all'); setPriorityFilter('all') }}
|
||||
>
|
||||
清除筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs text-neutral-400 mb-1.5">优先级</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{([
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'urgent', label: '仅紧急' },
|
||||
] as const).map(item => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setPriorityFilter(item.key)}
|
||||
className={`px-2 py-0.5 rounded-md text-xs border ${
|
||||
priorityFilter === item.key
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-white border-neutral-200 text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{filterActive && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-neutral-500 hover:text-neutral-700"
|
||||
onClick={() => { setStatusFilter('all'); setPriorityFilter('all') }}
|
||||
>
|
||||
清除筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`w-8 h-8 rounded-lg border flex items-center justify-center shrink-0 relative ${
|
||||
filterActive
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-neutral-100 border-neutral-200 text-neutral-500'
|
||||
}`}
|
||||
title="筛选"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`w-[34px] h-[34px] rounded-lg border flex items-center justify-center shrink-0 relative ${
|
||||
filterActive
|
||||
? 'bg-[#dbeafe] border-[#2563eb] text-[#2563eb]'
|
||||
: 'bg-neutral-100 border-neutral-200 text-neutral-500'
|
||||
}`}
|
||||
title="筛选"
|
||||
>
|
||||
<FilterOutlined className="text-xs" />
|
||||
{filterActive && <span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-[#2563eb]" />}
|
||||
</button>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-[#dbeafe] text-[#2563eb]">
|
||||
当前会话 {filteredSessions.length} 个
|
||||
</span>
|
||||
<a
|
||||
href="/widget/preview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs text-[#2563eb] hover:bg-[#eff6ff]"
|
||||
>
|
||||
<ExportOutlined className="text-[10px]" />
|
||||
预览访客窗口
|
||||
</a>
|
||||
</div>
|
||||
<FilterOutlined className="text-xs" />
|
||||
{filterActive && <span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-[#2563eb]" />}
|
||||
</button>
|
||||
</Popover>
|
||||
<a
|
||||
href="/widget/preview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-8 h-8 rounded-lg border border-neutral-200 bg-neutral-100 text-[#2563eb] hover:bg-[#eff6ff] flex items-center justify-center shrink-0"
|
||||
title="预览访客窗口"
|
||||
>
|
||||
<ExportOutlined className="text-xs" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto no-scrollbar">
|
||||
@@ -631,10 +633,13 @@ const Dashboard = () => {
|
||||
<section className="flex-1 flex flex-col min-w-0 h-full bg-neutral-50">
|
||||
{selected && selectedCustomer ? (
|
||||
<>
|
||||
<div className="shrink-0 flex items-center justify-between px-5 py-2.5 bg-white border-b border-neutral-200 min-h-14">
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between px-5 bg-white border-b border-neutral-200"
|
||||
style={{ height: 'var(--header-height)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-[38px] h-[38px] rounded-full flex items-center justify-center shrink-0 text-base font-semibold"
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center shrink-0 text-sm font-semibold"
|
||||
style={{
|
||||
background: listStatusMeta(selected).avatarBg,
|
||||
color: listStatusMeta(selected).avatarColor,
|
||||
@@ -642,9 +647,9 @@ const Dashboard = () => {
|
||||
>
|
||||
{selectedCustomer.name.slice(0, 1)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-semibold text-base text-neutral-900">{selectedCustomer.name}</span>
|
||||
<span className="truncate font-semibold text-[15px] text-neutral-900">{selectedCustomer.name}</span>
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-[#ecfeff] text-[#0891b2]">
|
||||
{channelLabel(selectedCustomer.source)}
|
||||
</span>
|
||||
@@ -653,8 +658,8 @@ const Dashboard = () => {
|
||||
{selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
|
||||
</span>
|
||||
</div>
|
||||
{/* 与设计稿一致:姓名下方展示 IP | 地区 */}
|
||||
<div className="flex items-center gap-1.5 mt-0.5 text-xs text-neutral-400 min-w-0">
|
||||
{/* IP | 地区 — 压缩行高,保证三栏顶栏等高 */}
|
||||
<div className="flex items-center gap-1.5 mt-0.5 text-[11px] text-neutral-400 min-w-0">
|
||||
<span className="truncate">
|
||||
IP: {selected.visitor_ip || '—'}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user