完善访客结束流程与转接可见性

- 会话结束后支持重新咨询,评价改为选星+快捷文案再提交
- 转接/分配事件写入操作人与目标坐席,工作台时间线与备注可见
This commit is contained in:
yml2213
2026-07-15 14:49:55 +08:00
parent 1359bfe597
commit a1336c630b
3 changed files with 422 additions and 125 deletions
+99 -13
View File
@@ -422,7 +422,8 @@ const Dashboard = () => {
if (payload.type === 'session_created' || payload.type === 'session_updated') {
void loadAllRef.current({ silent: true })
if (sameSession) void syncAfterSeqRef.current(sid, { markRead: false })
// 转接/分配/结束等会写 SessionEvent,需全量刷新详情(含 events)
if (sameSession) void loadDetailRef.current(sid, false, { silent: true })
}
} catch {
// ignore malformed frames
@@ -524,7 +525,51 @@ const Dashboard = () => {
return tb - ta
})
const notes = detail?.events.filter(event => event.action === 'note' || event.action === 'offline_leave' || event.action === 'auto_assign').slice().reverse() || []
const timelineEvents = (detail?.events || []).filter(event =>
event.action === 'transfer'
|| event.action === 'assign'
|| event.action === 'auto_assign'
|| event.action === 'end'
|| event.action === 'offline_leave',
)
const notes = detail?.events
.filter(event =>
event.action === 'note'
|| event.action === 'offline_leave'
|| event.action === 'auto_assign'
|| event.action === 'assign'
|| event.action === 'transfer'
|| event.action === 'end',
)
.slice()
.reverse() || []
type ChatTimelineItem =
| { kind: 'message'; at: number; message: Message }
| { kind: 'event'; at: number; event: SessionEvent }
const chatTimeline: ChatTimelineItem[] = (() => {
const items: ChatTimelineItem[] = []
for (const message of detail?.messages || []) {
items.push({ kind: 'message', at: new Date(message.sent_at).getTime(), message })
}
for (const event of timelineEvents) {
items.push({ kind: 'event', at: new Date(event.created_at).getTime(), event })
}
items.sort((a, b) => a.at - b.at || (a.kind === 'event' ? -1 : 1))
return items
})()
const eventLabel = (action: string) => {
switch (action) {
case 'transfer': return '会话转接'
case 'assign': return '人工分配'
case 'auto_assign': return '自动分配'
case 'end': return '结束会话'
case 'offline_leave': return '离线留言'
default: return '系统记录'
}
}
const emitTyping = () => {
if (!selectedId || !canOperate) return
@@ -939,12 +984,29 @@ const Dashboard = () => {
{new Date(selected.created_at).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
</span>
</div>
{(detail?.messages || []).map(message => {
{chatTimeline.map(item => {
if (item.kind === 'event') {
const { event } = item
return (
<div key={`ev-${event.id}`} className="flex justify-center mb-4">
<span className="inline-flex flex-col items-center max-w-[90%] px-3 py-1.5 rounded-lg text-xs text-violet-700 bg-violet-50 border border-violet-100">
<span className="font-medium text-violet-800">{eventLabel(event.action)}</span>
<span className="text-center leading-relaxed mt-0.5">{event.detail || eventLabel(event.action)}</span>
<span className="text-[11px] text-violet-500/80 mt-0.5">
{new Date(event.created_at).toLocaleString('zh-CN', {
month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit',
})}
</span>
</span>
</div>
)
}
const message = item.message
const isAgent = message.sender_type === 'agent'
const visitorInitial = selectedCustomer.name.slice(0, 1)
return (
<div
key={message.id}
key={`msg-${message.id}`}
className={`flex items-start gap-2.5 mb-4 ${isAgent ? 'flex-row-reverse' : ''}`}
>
<div
@@ -976,7 +1038,7 @@ const Dashboard = () => {
</div>
)
})}
{detail?.messages.length === 0 && (
{chatTimeline.length === 0 && (
<div className="text-center text-sm text-neutral-400 py-10"></div>
)}
{visitorTyping && selectedCustomer && (
@@ -1207,18 +1269,42 @@ const Dashboard = () => {
<div className="text-xs text-neutral-400"></div>
) : notes.map(note => {
const isLeave = note.action === 'offline_leave'
const isAssign = note.action === 'auto_assign'
const isAuto = note.action === 'auto_assign'
const isTransfer = note.action === 'transfer'
const isAssign = note.action === 'assign'
const isEnd = note.action === 'end'
const box = isLeave
? 'bg-orange-50 text-orange-900 border-orange-100'
: isAssign
? 'bg-blue-50 text-blue-900 border-blue-100'
: 'bg-amber-50 text-amber-900 border-amber-100'
const timeCls = isLeave ? 'text-orange-600/70' : isAssign ? 'text-blue-600/70' : 'text-amber-600/70'
: isTransfer
? 'bg-violet-50 text-violet-900 border-violet-100'
: (isAuto || isAssign)
? 'bg-blue-50 text-blue-900 border-blue-100'
: isEnd
? 'bg-neutral-100 text-neutral-700 border-neutral-200'
: 'bg-amber-50 text-amber-900 border-amber-100'
const timeCls = isLeave
? 'text-orange-600/70'
: isTransfer
? 'text-violet-600/70'
: (isAuto || isAssign)
? 'text-blue-600/70'
: isEnd
? 'text-neutral-500'
: 'text-amber-600/70'
const title = isLeave
? '离线留言'
: isTransfer
? '会话转接'
: isAuto
? '自动分配'
: isAssign
? '人工分配'
: isEnd
? '结束会话'
: null
return (
<div key={note.id} className={`rounded-lg p-2 text-xs whitespace-pre-wrap border ${box}`}>
{(isLeave || isAssign) && (
<div className="font-medium mb-0.5">{isLeave ? '离线留言' : '自动分配'}</div>
)}
{title && <div className="font-medium mb-0.5">{title}</div>}
{note.detail}
<div className={`mt-1 ${timeCls}`}>{new Date(note.created_at).toLocaleString('zh-CN')}</div>
</div>