工作台右侧栏精简客户信息并内嵌快捷回复
去掉访问来源区块以缩短信息区;保留浏览轨迹、历史会话与内部备注,下方增加团队/个人快捷回复列表。
This commit is contained in:
+180
-192
@@ -193,24 +193,6 @@ function channelLabel(source?: string) {
|
|||||||
return source.length > 6 ? '网页' : source
|
return source.length > 6 ? '网页' : source
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 UA 摘要设备信息,与后端 summarizeUserAgent 逻辑对齐 */
|
|
||||||
function summarizeDevice(ua?: string) {
|
|
||||||
if (!ua) return ''
|
|
||||||
let browser = '浏览器'
|
|
||||||
let osName = '未知系统'
|
|
||||||
if (ua.includes('Edg/')) browser = 'Edge'
|
|
||||||
else if (ua.includes('Chrome/') && !ua.includes('Edg/')) browser = 'Chrome'
|
|
||||||
else if (ua.includes('Firefox/')) browser = 'Firefox'
|
|
||||||
else if (ua.includes('Safari/') && !ua.includes('Chrome/')) browser = 'Safari'
|
|
||||||
else if (ua.includes('MicroMessenger')) browser = '微信'
|
|
||||||
if (ua.includes('Windows')) osName = 'Windows'
|
|
||||||
else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) osName = 'macOS'
|
|
||||||
else if (ua.includes('Android')) osName = 'Android'
|
|
||||||
else if (ua.includes('iPhone') || ua.includes('iPad')) osName = 'iOS'
|
|
||||||
else if (ua.includes('Linux')) osName = 'Linux'
|
|
||||||
return `${browser} / ${osName}`
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionDetail {
|
interface SessionDetail {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
events: SessionEvent[]
|
events: SessionEvent[]
|
||||||
@@ -236,7 +218,6 @@ function shortPagePath(url?: string): string {
|
|||||||
if (!url) return '—'
|
if (!url) return '—'
|
||||||
try {
|
try {
|
||||||
const u = new URL(url)
|
const u = new URL(url)
|
||||||
// 必须带 hash:demo-shop 等 SPA 靠 #/cart 区分页面
|
|
||||||
const path = u.pathname + (u.search || '') + (u.hash || '')
|
const path = u.pathname + (u.search || '') + (u.hash || '')
|
||||||
return path.length > 56 ? `${path.slice(0, 54)}…` : path || '/'
|
return path.length > 56 ? `${path.slice(0, 54)}…` : path || '/'
|
||||||
} catch {
|
} catch {
|
||||||
@@ -274,6 +255,11 @@ const Dashboard = () => {
|
|||||||
const [quickKeyword, setQuickKeyword] = useState('')
|
const [quickKeyword, setQuickKeyword] = useState('')
|
||||||
const [quickList, setQuickList] = useState<QuickReply[]>([])
|
const [quickList, setQuickList] = useState<QuickReply[]>([])
|
||||||
const [quickLoading, setQuickLoading] = useState(false)
|
const [quickLoading, setQuickLoading] = useState(false)
|
||||||
|
/** 右侧栏快捷回复:团队 / 个人 */
|
||||||
|
const [sidebarQuickScope, setSidebarQuickScope] = useState<'team' | 'personal'>('team')
|
||||||
|
const [sidebarQuickKeyword, setSidebarQuickKeyword] = useState('')
|
||||||
|
const [sidebarQuickList, setSidebarQuickList] = useState<QuickReply[]>([])
|
||||||
|
const [sidebarQuickLoading, setSidebarQuickLoading] = useState(false)
|
||||||
/** 输入框快捷回复建议:slash=/ 输入码;keyword=正文关键字 */
|
/** 输入框快捷回复建议:slash=/ 输入码;keyword=正文关键字 */
|
||||||
const [suggestOpen, setSuggestOpen] = useState(false)
|
const [suggestOpen, setSuggestOpen] = useState(false)
|
||||||
const [suggestMode, setSuggestMode] = useState<'slash' | 'keyword'>('slash')
|
const [suggestMode, setSuggestMode] = useState<'slash' | 'keyword'>('slash')
|
||||||
@@ -723,6 +709,28 @@ const Dashboard = () => {
|
|||||||
}).catch(() => setQuickList([])).finally(() => setQuickLoading(false))
|
}).catch(() => setQuickList([])).finally(() => setQuickLoading(false))
|
||||||
}, [quickOpen, quickKeyword])
|
}, [quickOpen, quickKeyword])
|
||||||
|
|
||||||
|
// 右侧栏快捷回复列表(始终可用)
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setSidebarQuickLoading(true)
|
||||||
|
getQuickReplies({
|
||||||
|
scope: sidebarQuickScope,
|
||||||
|
// 团队仅展示已发布;个人展示自己的全部可用
|
||||||
|
status: sidebarQuickScope === 'team' ? 'published' : undefined,
|
||||||
|
q: sidebarQuickKeyword.trim() || undefined,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
}).then(response => {
|
||||||
|
if (cancelled) return
|
||||||
|
setSidebarQuickList(Array.isArray(response.list) ? response.list : [])
|
||||||
|
}).catch(() => {
|
||||||
|
if (!cancelled) setSidebarQuickList([])
|
||||||
|
}).finally(() => {
|
||||||
|
if (!cancelled) setSidebarQuickLoading(false)
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [sidebarQuickScope, sidebarQuickKeyword])
|
||||||
|
|
||||||
const closeSuggest = useCallback(() => {
|
const closeSuggest = useCallback(() => {
|
||||||
setSuggestOpen(false)
|
setSuggestOpen(false)
|
||||||
setSuggestItems([])
|
setSuggestItems([])
|
||||||
@@ -873,7 +881,6 @@ const Dashboard = () => {
|
|||||||
setCustomerHistory([])
|
setCustomerHistory([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 从已加载会话中筛出该客户的历史(含已结束)
|
|
||||||
getSessions({ page: 1, pageSize: 50 }).then(res => {
|
getSessions({ page: 1, pageSize: 50 }).then(res => {
|
||||||
const list = Array.isArray(res.list) ? res.list : []
|
const list = Array.isArray(res.list) ? res.list : []
|
||||||
setCustomerHistory(
|
setCustomerHistory(
|
||||||
@@ -1745,20 +1752,21 @@ const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 右侧客户信息 320px */}
|
{/* 右侧:上客户信息 + 下快捷回复 */}
|
||||||
<aside className="w-[320px] shrink-0 flex flex-col h-full border-l border-neutral-200 bg-white">
|
<aside className="w-[320px] shrink-0 flex flex-col h-full border-l border-neutral-200 bg-white min-h-0">
|
||||||
{selectedCustomer ? (
|
{selectedCustomer ? (
|
||||||
<>
|
<>
|
||||||
|
{/* —— 上:客户信息(精简) —— */}
|
||||||
<div
|
<div
|
||||||
className="shrink-0 flex items-center justify-between px-4 border-b border-neutral-200"
|
className="shrink-0 flex items-center justify-between px-4 border-b border-neutral-200"
|
||||||
style={{ height: 'var(--header-height)' }}
|
style={{ height: 'var(--header-height)' }}
|
||||||
>
|
>
|
||||||
<span className="font-semibold text-base text-neutral-800">客户信息</span>
|
<span className="font-semibold text-base text-neutral-800">客户信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-4 py-4 no-scrollbar">
|
<div className="shrink-0 max-h-[42%] overflow-y-auto px-4 py-3 no-scrollbar border-b border-neutral-100">
|
||||||
<div className="flex flex-col items-center mb-5">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<div
|
<div
|
||||||
className="w-14 h-14 rounded-full flex items-center justify-center mb-2 text-[22px] font-semibold"
|
className="w-10 h-10 rounded-full flex items-center justify-center text-[16px] font-semibold shrink-0"
|
||||||
style={{
|
style={{
|
||||||
background: selected ? listStatusMeta(selected).avatarBg : '#eff6ff',
|
background: selected ? listStatusMeta(selected).avatarBg : '#eff6ff',
|
||||||
color: selected ? listStatusMeta(selected).avatarColor : '#2563eb',
|
color: selected ? listStatusMeta(selected).avatarColor : '#2563eb',
|
||||||
@@ -1766,182 +1774,96 @@ const Dashboard = () => {
|
|||||||
>
|
>
|
||||||
{selectedCustomer.name.slice(0, 1)}
|
{selectedCustomer.name.slice(0, 1)}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-semibold text-lg text-neutral-900">{selectedCustomer.name}</div>
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-1 mt-1 text-xs text-neutral-400">
|
<div className="font-semibold text-[15px] text-neutral-900 truncate">{selectedCustomer.name}</div>
|
||||||
<span
|
<div className="flex items-center gap-1 mt-0.5 text-[11px] text-neutral-400">
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
<span
|
||||||
style={{ background: selectedCustomer.status === 'online' ? '#16a34a' : '#94a3b8' }}
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
/>
|
style={{ background: selectedCustomer.status === 'online' ? '#16a34a' : '#94a3b8' }}
|
||||||
{selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
|
/>
|
||||||
|
{selectedCustomer.status === 'online' ? '在线' : selectedCustomer.status === 'busy' ? '忙碌' : '离线'}
|
||||||
|
{selectedCustomer.source ? (
|
||||||
|
<span className="text-neutral-300">· {selectedCustomer.source}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="flex flex-col gap-1 text-sm">
|
||||||
<div className="flex items-center gap-1.5 mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">
|
<CustomerInfoField
|
||||||
基本信息
|
label="手机"
|
||||||
<span className="font-normal normal-case tracking-normal text-neutral-400">点击可编辑</span>
|
value={selectedCustomer.phone || ''}
|
||||||
</div>
|
placeholder="点击填写"
|
||||||
<div className="flex flex-col gap-1.5 text-sm">
|
saving={savingCustomerField === 'phone'}
|
||||||
<CustomerInfoField
|
onSave={v => saveCustomerField(selectedCustomer.id, 'phone', v)}
|
||||||
label="手机"
|
/>
|
||||||
value={selectedCustomer.phone || ''}
|
<CustomerInfoField
|
||||||
placeholder="点击填写手机号"
|
label="邮箱"
|
||||||
saving={savingCustomerField === 'phone'}
|
value={selectedCustomer.email || ''}
|
||||||
onSave={v => saveCustomerField(selectedCustomer.id, 'phone', v)}
|
placeholder="点击填写"
|
||||||
/>
|
saving={savingCustomerField === 'email'}
|
||||||
<CustomerInfoField
|
onSave={v => saveCustomerField(selectedCustomer.id, 'email', v)}
|
||||||
label="邮箱"
|
/>
|
||||||
value={selectedCustomer.email || ''}
|
<CustomerInfoField
|
||||||
placeholder="点击填写邮箱"
|
label="微信"
|
||||||
saving={savingCustomerField === 'email'}
|
value={selectedCustomer.wechat || ''}
|
||||||
onSave={v => saveCustomerField(selectedCustomer.id, 'email', v)}
|
placeholder="点击填写"
|
||||||
/>
|
saving={savingCustomerField === 'wechat'}
|
||||||
<CustomerInfoField
|
onSave={v => saveCustomerField(selectedCustomer.id, 'wechat', v)}
|
||||||
label="微信"
|
/>
|
||||||
value={selectedCustomer.wechat || ''}
|
<CustomerInfoField
|
||||||
placeholder="点击填写微信号"
|
label="QQ"
|
||||||
saving={savingCustomerField === 'wechat'}
|
value={selectedCustomer.qq || ''}
|
||||||
onSave={v => saveCustomerField(selectedCustomer.id, 'wechat', v)}
|
placeholder="点击填写"
|
||||||
/>
|
saving={savingCustomerField === 'qq'}
|
||||||
<CustomerInfoField
|
onSave={v => saveCustomerField(selectedCustomer.id, 'qq', v)}
|
||||||
label="QQ"
|
/>
|
||||||
value={selectedCustomer.qq || ''}
|
{(() => {
|
||||||
placeholder="点击填写 QQ 号"
|
const kindLabel = (k: string) =>
|
||||||
saving={savingCustomerField === 'qq'}
|
k === 'phone' ? '手机' : k === 'wechat' ? '微信' : k === 'email' ? '邮箱' : k === 'qq' ? 'QQ' : k
|
||||||
onSave={v => saveCustomerField(selectedCustomer.id, 'qq', v)}
|
const extra = customerContacts.filter(c => {
|
||||||
/>
|
if (c.kind === 'phone' && selectedCustomer.phone && c.value === selectedCustomer.phone) return false
|
||||||
{(() => {
|
if (c.kind === 'email' && selectedCustomer.email && c.value === selectedCustomer.email) return false
|
||||||
const kindLabel = (k: string) =>
|
if (c.kind === 'wechat' && selectedCustomer.wechat && c.value === selectedCustomer.wechat) return false
|
||||||
k === 'phone' ? '手机' : k === 'wechat' ? '微信' : k === 'email' ? '邮箱' : k === 'qq' ? 'QQ' : k
|
if (c.kind === 'qq' && selectedCustomer.qq && c.value === selectedCustomer.qq) return false
|
||||||
// 主字段已展示的联系方式不重复
|
return true
|
||||||
const extra = customerContacts.filter(c => {
|
})
|
||||||
if (c.kind === 'phone' && selectedCustomer.phone && c.value === selectedCustomer.phone) return false
|
if (extra.length === 0) return null
|
||||||
if (c.kind === 'email' && selectedCustomer.email && c.value === selectedCustomer.email) return false
|
return (
|
||||||
if (c.kind === 'wechat' && selectedCustomer.wechat && c.value === selectedCustomer.wechat) return false
|
<div className="flex items-start gap-2 pt-0.5">
|
||||||
if (c.kind === 'qq' && selectedCustomer.qq && c.value === selectedCustomer.qq) return false
|
<span className="shrink-0 text-neutral-400 min-w-12 leading-5 text-sm">更多</span>
|
||||||
return true
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
})
|
{extra.map(c => (
|
||||||
if (extra.length === 0) return null
|
<span key={c.id || `${c.kind}-${c.value}`} className="text-neutral-800 text-xs break-all">
|
||||||
return (
|
<span className="text-neutral-400 mr-1">{kindLabel(c.kind)}</span>
|
||||||
<div className="flex items-start gap-2 pt-1">
|
{c.value}
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12 leading-5">更多</span>
|
</span>
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
))}
|
||||||
{extra.map(c => (
|
|
||||||
<span key={c.id || `${c.kind}-${c.value}`} className="text-neutral-800 text-xs break-all">
|
|
||||||
<span className="text-neutral-400 mr-1">{kindLabel(c.kind)}</span>
|
|
||||||
{c.value}
|
|
||||||
{(c.source === 'draft' || c.source === 'message') && (
|
|
||||||
<span className="ml-1 text-[10px] text-amber-600">自动识别</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
})()}
|
)
|
||||||
<div className="flex items-start gap-2 pt-1">
|
})()}
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">来源</span>
|
|
||||||
<span className="truncate text-neutral-800">{selectedCustomer.source || '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">对话</span>
|
|
||||||
<span className="truncate text-neutral-800">{selectedCustomer.conversation_count} 次</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">地区</span>
|
|
||||||
<span className="truncate text-neutral-800">{selected?.visitor_region || '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="mt-3">
|
||||||
<div className="mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">客户标签</div>
|
<div className="mb-1.5 text-[11px] font-semibold text-neutral-500">标签</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1">
|
||||||
{parseTags(selectedCustomer.tags).length === 0 ? (
|
{parseTags(selectedCustomer.tags).length === 0 ? (
|
||||||
<span className="text-xs text-neutral-400">暂无标签</span>
|
<span className="text-xs text-neutral-400">暂无</span>
|
||||||
) : parseTags(selectedCustomer.tags).map(tag => (
|
) : parseTags(selectedCustomer.tags).map(tag => (
|
||||||
<span key={tag} className="text-xs px-2 py-0.5 rounded-md bg-[#dbeafe] text-[#2563eb] font-medium">
|
<span key={tag} className="text-[11px] px-1.5 py-0.5 rounded-md bg-[#dbeafe] text-[#2563eb] font-medium">
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="mt-4 mb-3">
|
||||||
<div className="mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">访问来源</div>
|
<div className="mb-1.5 text-[11px] font-semibold text-neutral-500">浏览轨迹</div>
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">渠道</span>
|
|
||||||
<span className="text-neutral-800">{channelLabel(selectedCustomer.source)}在线客服</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">设备</span>
|
|
||||||
<span className="text-neutral-800">{summarizeDevice(selected?.user_agent) || '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2 min-w-0">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">落地页</span>
|
|
||||||
{selected?.landing_url ? (
|
|
||||||
<a
|
|
||||||
href={selected.landing_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-[#2563eb] hover:underline break-all min-w-0"
|
|
||||||
title={selected.landing_title || selected.landing_url}
|
|
||||||
>
|
|
||||||
{selected.landing_title || shortPagePath(selected.landing_url)}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-neutral-400">—</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2 min-w-0">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">当前页</span>
|
|
||||||
{selected?.current_url ? (
|
|
||||||
<a
|
|
||||||
href={selected.current_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-neutral-800 hover:text-[#2563eb] hover:underline break-all min-w-0"
|
|
||||||
title={selected.current_title || selected.current_url}
|
|
||||||
>
|
|
||||||
{selected.current_title || shortPagePath(selected.current_url)}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-neutral-400">—</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{selected?.referrer ? (
|
|
||||||
<div className="flex items-start gap-2 min-w-0">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">来路</span>
|
|
||||||
<a
|
|
||||||
href={selected.referrer}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-neutral-500 hover:underline break-all text-xs min-w-0"
|
|
||||||
>
|
|
||||||
{shortPagePath(selected.referrer)}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="shrink-0 text-neutral-400 min-w-12">在线</span>
|
|
||||||
<span className="text-neutral-800 tabular-nums font-medium">
|
|
||||||
{formatOnlineDuration(
|
|
||||||
selected?.created_at,
|
|
||||||
selected?.status === 'ended' || selected?.status === 'archived' ? selected?.ended_at : null,
|
|
||||||
clockTick >= 0 ? Date.now() : Date.now(),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-5">
|
|
||||||
<div className="mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">浏览轨迹</div>
|
|
||||||
{!detail?.pageViews?.length ? (
|
{!detail?.pageViews?.length ? (
|
||||||
<div className="text-xs text-neutral-400">暂无页面记录(需嵌入 widget 并打开会话)</div>
|
<div className="text-xs text-neutral-400">暂无页面记录(需嵌入 widget 并打开会话)</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-0 max-h-48 overflow-y-auto rounded-lg border border-neutral-100">
|
<div className="flex flex-col gap-0 max-h-36 overflow-y-auto rounded-lg border border-neutral-100">
|
||||||
{detail.pageViews.map((pv, idx) => (
|
{detail.pageViews.map((pv, idx) => (
|
||||||
<div
|
<div
|
||||||
key={pv.id || `${pv.url}-${pv.entered_at}`}
|
key={pv.id || `${pv.url}-${pv.entered_at}`}
|
||||||
@@ -1972,9 +1894,9 @@ const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="mb-3">
|
||||||
<div className="mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">历史会话</div>
|
<div className="mb-1.5 text-[11px] font-semibold text-neutral-500">历史会话</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-1.5">
|
||||||
{customerHistory.length === 0 ? (
|
{customerHistory.length === 0 ? (
|
||||||
<div className="text-xs text-neutral-400">暂无其他会话</div>
|
<div className="text-xs text-neutral-400">暂无其他会话</div>
|
||||||
) : customerHistory.map(s => (
|
) : customerHistory.map(s => (
|
||||||
@@ -1982,9 +1904,9 @@ const Dashboard = () => {
|
|||||||
key={s.id}
|
key={s.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedId(s.id)}
|
onClick={() => setSelectedId(s.id)}
|
||||||
className="rounded-lg px-3 py-2 text-left bg-neutral-50 border border-neutral-100 hover:border-blue-200"
|
className="rounded-lg px-2.5 py-2 text-left bg-neutral-50 border border-neutral-100 hover:border-blue-200"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-0.5">
|
||||||
<span className="truncate text-sm font-medium text-neutral-700">会话 #{s.id}</span>
|
<span className="truncate text-sm font-medium text-neutral-700">会话 #{s.id}</span>
|
||||||
<span className="whitespace-nowrap text-xs text-neutral-400">
|
<span className="whitespace-nowrap text-xs text-neutral-400">
|
||||||
{new Date(s.created_at).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })}
|
{new Date(s.created_at).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })}
|
||||||
@@ -1998,9 +1920,9 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2">
|
<div className="mb-1">
|
||||||
<div className="mb-2.5 text-xs font-semibold text-neutral-500 uppercase tracking-wider">内部备注</div>
|
<div className="mb-1.5 text-[11px] font-semibold text-neutral-500">内部备注</div>
|
||||||
<div className="space-y-2 max-h-40 overflow-auto">
|
<div className="space-y-1.5 max-h-32 overflow-auto">
|
||||||
{notes.length === 0 ? (
|
{notes.length === 0 ? (
|
||||||
<div className="text-xs text-neutral-400">暂无内部备注</div>
|
<div className="text-xs text-neutral-400">暂无内部备注</div>
|
||||||
) : notes.map(note => {
|
) : notes.map(note => {
|
||||||
@@ -2062,6 +1984,72 @@ const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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-neutral-100">
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-2">
|
||||||
|
<span className="text-sm font-semibold text-neutral-800">快捷回复</span>
|
||||||
|
<div className="flex items-center gap-0.5 p-0.5 rounded-md bg-neutral-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-6 px-2 rounded text-xs border-0 cursor-pointer transition-colors ${
|
||||||
|
sidebarQuickScope === tab.key
|
||||||
|
? 'bg-white text-[#2563eb] font-medium shadow-sm'
|
||||||
|
: 'bg-transparent text-neutral-500 hover:text-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
allowClear
|
||||||
|
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||||
|
placeholder="搜索标题、内容或输入码"
|
||||||
|
value={sidebarQuickKeyword}
|
||||||
|
onChange={e => setSidebarQuickKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-2 no-scrollbar">
|
||||||
|
{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={!canOperate}
|
||||||
|
title={canOperate ? '点击填入输入框' : '领取会话后可用'}
|
||||||
|
onClick={() => void applyQuickReply(item, 'panel')}
|
||||||
|
className="w-full text-left rounded-lg px-2.5 py-2 border-0 bg-neutral-50 hover:bg-blue-50 hover:ring-1 hover:ring-blue-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center text-sm text-neutral-400">选择会话查看客户信息</div>
|
<div className="flex-1 flex items-center justify-center text-sm text-neutral-400">选择会话查看客户信息</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user