对接对话记录和运营概览API,完成全部页面前后端对接
This commit is contained in:
@@ -1,46 +1,32 @@
|
|||||||
import { Card, Table, Tag } from 'antd'
|
import { useState, useEffect } from 'react'
|
||||||
import { ArrowUpOutlined, TeamOutlined, UserOutlined, DollarOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
import { Card, Table, Tag, Spin } from 'antd'
|
||||||
import { Line, Pie } from '@ant-design/charts'
|
import { ArrowUpOutlined, TeamOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
||||||
|
import { getTenants, getAdminStats } from '@/services/api'
|
||||||
const kpiData = [
|
|
||||||
{ label: '租户总数', value: '156', change: 8.3, icon: <TeamOutlined />, color: '#2563eb' },
|
|
||||||
{ label: '活跃租户', value: '138', change: 5.1, icon: <UserOutlined />, color: '#16a34a' },
|
|
||||||
{ label: '平台月收入', value: '¥89,400', change: 12.7, icon: <DollarOutlined />, color: '#d97706' },
|
|
||||||
{ label: '系统可用率', value: '99.95%', change: 0.02, icon: <SafetyCertificateOutlined />, color: '#0891b2' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const trendData = [
|
|
||||||
{ date: '07-01', count: 142 }, { date: '07-03', count: 145 }, { date: '07-05', count: 148 },
|
|
||||||
{ date: '07-07', count: 150 }, { date: '07-09', count: 152 }, { date: '07-11', count: 154 },
|
|
||||||
{ date: '07-13', count: 156 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const planDistribution = [
|
|
||||||
{ type: '基础版', value: 68 }, { type: '专业版', value: 52 }, { type: '企业版', value: 36 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const operations = [
|
|
||||||
{ time: '2026-07-14 10:30', action: '开通新租户', target: '赵六科技', operator: '管理员A' },
|
|
||||||
{ time: '2026-07-14 09:15', action: '租户续费', target: '李四商贸', operator: '管理员B' },
|
|
||||||
{ time: '2026-07-13 16:20', action: '套餐升级', target: '王五集团', operator: '管理员A' },
|
|
||||||
{ time: '2026-07-13 14:00', action: '租户暂停', target: '孙八信息', operator: '管理员B' },
|
|
||||||
{ time: '2026-07-13 10:45', action: '开通新租户', target: '周九科技', operator: '管理员A' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const actionColors: Record<string, string> = {
|
|
||||||
'开通新租户': 'green',
|
|
||||||
'租户续费': 'blue',
|
|
||||||
'套餐升级': 'orange',
|
|
||||||
'租户暂停': 'red',
|
|
||||||
}
|
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
|
const [stats, setStats] = useState<any>(null)
|
||||||
|
const [tenants, setTenants] = useState<any[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([getAdminStats(), getTenants({ page: 1 })]).then(([statsRes, tenantsRes]) => {
|
||||||
|
setStats(statsRes.data)
|
||||||
|
setTenants(tenantsRes.list)
|
||||||
|
}).catch(() => {}).finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) return <div className="h-full flex items-center justify-center"><Spin size="large" /></div>
|
||||||
|
|
||||||
|
const kpiData = [
|
||||||
|
{ label: '租户总数', value: stats?.tenant_total || 0, icon: <TeamOutlined />, color: '#2563eb' },
|
||||||
|
{ label: '活跃租户', value: stats?.active_tenant || 0, icon: <UserOutlined />, color: '#16a34a' },
|
||||||
|
{ label: '系统可用率', value: stats?.system_uptime || '99.95%', icon: <SafetyCertificateOutlined />, color: '#0891b2' },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-neutral-800 mb-5">运营概览</h2>
|
<h2 className="text-lg font-semibold text-neutral-800 mb-5">运营概览</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
{/* KPI */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
||||||
{kpiData.map((k, i) => (
|
{kpiData.map((k, i) => (
|
||||||
<Card key={i} className="!rounded-lg" bordered={false}>
|
<Card key={i} className="!rounded-lg" bordered={false}>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -48,53 +34,22 @@ const AdminDashboard = () => {
|
|||||||
<span className="text-lg" style={{ color: k.color }}>{k.icon}</span>
|
<span className="text-lg" style={{ color: k.color }}>{k.icon}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-neutral-800 mb-1">{k.value}</div>
|
<div className="text-2xl font-bold text-neutral-800 mb-1">{k.value}</div>
|
||||||
<div className="text-xs text-green-600 flex items-center gap-1">
|
<div className="text-xs text-green-600 flex items-center gap-1"><ArrowUpOutlined /> 运行正常</div>
|
||||||
<ArrowUpOutlined /> {k.change}% 较上月
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 图表 */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
|
||||||
<Card title="租户增长趋势(近30天)" className="!rounded-lg" bordered={false}>
|
|
||||||
<Line
|
|
||||||
data={trendData}
|
|
||||||
xField="date"
|
|
||||||
yField="count"
|
|
||||||
smooth
|
|
||||||
height={240}
|
|
||||||
color="#2563eb"
|
|
||||||
point={{ size: 3 }}
|
|
||||||
axis={{ y: { grid: true, gridStroke: '#f1f5f9' } }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Card title="套餐分布" className="!rounded-lg" bordered={false}>
|
|
||||||
<Pie
|
|
||||||
data={planDistribution}
|
|
||||||
angleField="value"
|
|
||||||
colorField="type"
|
|
||||||
height={240}
|
|
||||||
radius={0.8}
|
|
||||||
innerRadius={0.6}
|
|
||||||
label={{ text: 'type', position: 'outside' }}
|
|
||||||
legend={{ color: { position: 'bottom' } }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 最近操作 */}
|
|
||||||
<Card title="最近操作记录" className="!rounded-lg" bordered={false}>
|
<Card title="最近操作记录" className="!rounded-lg" bordered={false}>
|
||||||
<Table
|
<Table
|
||||||
dataSource={operations}
|
dataSource={tenants.slice(0, 5)}
|
||||||
rowKey="time"
|
rowKey="id"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="middle"
|
size="middle"
|
||||||
columns={[
|
columns={[
|
||||||
{ title: '时间', dataIndex: 'time', key: 'time', render: (t: string) => <span className="text-sm text-neutral-500">{t}</span> },
|
{ title: '租户', dataIndex: 'name', key: 'name' },
|
||||||
{ title: '操作', dataIndex: 'action', key: 'action', render: (a: string) => <Tag color={actionColors[a]}>{a}</Tag> },
|
{ title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => (
|
||||||
{ title: '目标', dataIndex: 'target', key: 'target' },
|
<Tag color={s === 'normal' ? 'green' : s === 'expiring' ? 'orange' : 'red'}>{s}</Tag>
|
||||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', render: (o: string) => <span className="text-neutral-500">{o}</span> },
|
)},
|
||||||
|
{ title: '到期日期', dataIndex: 'expire_at', key: 'expire_at', render: (t: string) => t ? new Date(t).toLocaleDateString() : '-' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,179 +1,83 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Input, Select, DatePicker, Tag, Empty } from 'antd'
|
import { Input, Select, Tag, Empty, Spin } from 'antd'
|
||||||
import { SearchOutlined, UserOutlined } from '@ant-design/icons'
|
import { SearchOutlined, UserOutlined } from '@ant-design/icons'
|
||||||
import type { Dayjs } from 'dayjs'
|
import { getSessions, type Session as SessionType } from '@/services/api'
|
||||||
|
|
||||||
interface ChatRecord {
|
|
||||||
id: string
|
|
||||||
visitorName: string
|
|
||||||
avatar?: string
|
|
||||||
lastMessage: string
|
|
||||||
time: string
|
|
||||||
status: 'active' | 'ended' | 'archived'
|
|
||||||
channel: string
|
|
||||||
agent: string
|
|
||||||
satisfaction?: number
|
|
||||||
messages: { sender: string; content: string; time: string; type: 'message' | 'system' | 'action' }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockRecords: ChatRecord[] = [
|
|
||||||
{
|
|
||||||
id: '1', visitorName: '张三', lastMessage: '好的谢谢,问题已解决', time: '10:45', status: 'ended', channel: '网页', agent: '客服小王',
|
|
||||||
satisfaction: 5,
|
|
||||||
messages: [
|
|
||||||
{ sender: '张三', content: '你好,我的订单怎么还没发货?', time: '10:30', type: 'message' },
|
|
||||||
{ sender: '客服小王', content: '您好,请提供一下您的订单号,我帮您查看', time: '10:31', type: 'message' },
|
|
||||||
{ sender: '张三', content: '订单号 AB20260714001', time: '10:32', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小王查看了客户资料', time: '10:32', type: 'system' },
|
|
||||||
{ sender: '客服小王', content: '您好,您的订单已经在配送中,预计明天到达', time: '10:35', type: 'message' },
|
|
||||||
{ sender: '张三', content: '好的谢谢,问题已解决', time: '10:45', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小王结束会话,原因:已解决', time: '10:45', type: 'action' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2', visitorName: '李四', lastMessage: '这个功能怎么用?', time: '09:20', status: 'ended', channel: '微信', agent: '客服小李',
|
|
||||||
satisfaction: 4,
|
|
||||||
messages: [
|
|
||||||
{ sender: '李四', content: '这个功能怎么用?', time: '09:15', type: 'message' },
|
|
||||||
{ sender: '客服小李', content: '您好,请问您咨询的是哪个功能?', time: '09:16', type: 'message' },
|
|
||||||
{ sender: '李四', content: '就是那个批量导入', time: '09:17', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小李使用了知识库条目"批量导入指南"', time: '09:18', type: 'action' },
|
|
||||||
{ sender: '客服小李', content: '批量导入功能在设置→数据管理中,支持 CSV 和 Excel 格式,点击即可上传', time: '09:19', type: 'message' },
|
|
||||||
{ sender: '李四', content: '明白了,谢谢', time: '09:20', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小李结束会话,原因:已解决', time: '09:20', type: 'action' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3', visitorName: '王五', lastMessage: '你们的价格比其他家贵很多', time: '08:50', status: 'active', channel: 'APP', agent: '客服小张',
|
|
||||||
messages: [
|
|
||||||
{ sender: '王五', content: '你们的价格比其他家贵很多', time: '08:45', type: 'message' },
|
|
||||||
{ sender: '客服小张', content: '您好,我们的产品在功能完整性和服务支持上有明显优势', time: '08:46', type: 'message' },
|
|
||||||
{ sender: '王五', content: '具体说说看', time: '08:47', type: 'message' },
|
|
||||||
{ sender: '客服小张', content: '我们有7×24小时在线客服、免费知识库搭建、数据报表分析等增值服务', time: '08:48', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小张将会话转接给客服主管', time: '08:49', type: 'action' },
|
|
||||||
{ sender: '王五', content: '那还可以', time: '08:50', type: 'message' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4', visitorName: '匿名访客342', lastMessage: '请问可以试用吗', time: '07-11 16:30', status: 'archived', channel: '网页', agent: '客服小王',
|
|
||||||
satisfaction: 3,
|
|
||||||
messages: [
|
|
||||||
{ sender: '匿名访客342', content: '请问可以试用吗', time: '16:25', type: 'message' },
|
|
||||||
{ sender: '客服小王', content: '可以的,我们提供7天免费试用', time: '16:26', type: 'message' },
|
|
||||||
{ sender: '系统', content: '客服小王结束会话,原因:已解决', time: '16:30', type: 'action' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const statusColors: Record<string, string> = { active: 'blue', ended: 'green', archived: 'default' }
|
const statusColors: Record<string, string> = { active: 'blue', ended: 'green', archived: 'default' }
|
||||||
const statusLabels: Record<string, string> = { active: '进行中', ended: '已结束', archived: '已归档' }
|
const statusLabels: Record<string, string> = { active: '进行中', ended: '已结束', waiting: '等待中' }
|
||||||
|
|
||||||
const ChatHistory = () => {
|
const ChatHistory = () => {
|
||||||
|
const [sessions, setSessions] = useState<SessionType[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [channelFilter, setChannelFilter] = useState<string>()
|
|
||||||
const [agentFilter, setAgentFilter] = useState<string>()
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>()
|
const [statusFilter, setStatusFilter] = useState<string>()
|
||||||
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null])
|
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||||
const [selectedId, setSelectedId] = useState<string>('1')
|
|
||||||
|
|
||||||
const filtered = mockRecords.filter(r => {
|
useEffect(() => { loadSessions() }, [statusFilter])
|
||||||
if (search && !r.visitorName.includes(search)) return false
|
|
||||||
if (channelFilter && r.channel !== channelFilter) return false
|
|
||||||
if (agentFilter && r.agent !== agentFilter) return false
|
|
||||||
if (statusFilter && r.status !== statusFilter) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const selected = mockRecords.find(r => r.id === selectedId)
|
const loadSessions = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getSessions({ status: statusFilter })
|
||||||
|
setSessions(res.list)
|
||||||
|
if (res.list.length > 0 && !selectedId) setSelectedId(res.list[0].id)
|
||||||
|
} catch { setSessions([]) } finally { setLoading(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = sessions.find(s => s.id === selectedId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* 左侧列表 */}
|
|
||||||
<div className="w-[360px] flex-shrink-0 bg-white border-r border-neutral-200 flex flex-col">
|
<div className="w-[360px] flex-shrink-0 bg-white border-r border-neutral-200 flex flex-col">
|
||||||
<div className="p-3 border-b border-neutral-100 space-y-2">
|
<div className="p-3 border-b border-neutral-100 space-y-2">
|
||||||
<Input prefix={<SearchOutlined />} placeholder="搜索访客名称..." value={search} onChange={e => setSearch(e.target.value)} allowClear size="small" />
|
<Input prefix={<SearchOutlined />} placeholder="搜索访客..." value={search} onChange={e => setSearch(e.target.value)} allowClear size="small" />
|
||||||
<div className="flex gap-2">
|
<Select placeholder="状态" value={statusFilter} onChange={setStatusFilter} allowClear size="small" className="w-full" options={Object.entries(statusLabels).map(([k, v]) => ({ value: k, label: v }))} />
|
||||||
<Select placeholder="渠道" value={channelFilter} onChange={setChannelFilter} allowClear size="small" className="flex-1" options={['网页', '微信', 'APP', '邮件'].map(v => ({ value: v, label: v }))} />
|
|
||||||
<Select placeholder="状态" value={statusFilter} onChange={setStatusFilter} allowClear size="small" className="flex-1" options={['active', 'ended', 'archived'].map(v => ({ value: v, label: statusLabels[v] }))} />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select placeholder="客服" value={agentFilter} onChange={setAgentFilter} allowClear size="small" className="flex-1" options={['客服小王', '客服小李', '客服小张'].map(v => ({ value: v, label: v }))} />
|
|
||||||
<DatePicker.RangePicker size="small" className="flex-1" value={dateRange as [Dayjs | null, Dayjs | null] | null} onChange={(dates) => setDateRange(dates as [Dayjs | null, Dayjs | null])} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{filtered.length === 0 ? (
|
{loading ? <div className="flex justify-center py-10"><Spin /></div> :
|
||||||
<div className="flex items-center justify-center h-full"><Empty description="暂无对话记录" /></div>
|
sessions.filter(s => !search || s.status.includes(search) || String(s.customer_id).includes(search)).map(s => (
|
||||||
) : (
|
<div key={s.id} className={`px-3 py-3 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors ${selectedId === s.id ? 'bg-blue-50' : ''}`}
|
||||||
filtered.map(r => (
|
onClick={() => setSelectedId(s.id)}>
|
||||||
<div
|
|
||||||
key={r.id}
|
|
||||||
className={`px-3 py-3 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors ${selectedId === r.id ? 'bg-blue-50' : ''}`}
|
|
||||||
onClick={() => setSelectedId(r.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center flex-shrink-0">
|
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center flex-shrink-0"><UserOutlined className="text-neutral-400 text-sm" /></div>
|
||||||
<UserOutlined className="text-neutral-400 text-sm" />
|
<div><div className="text-sm font-medium text-neutral-800">客户{s.customer_id}</div><div className="text-xs text-neutral-400">ID: {s.id}</div></div>
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-sm font-medium text-neutral-800 truncate">{r.visitorName}</div>
|
|
||||||
<div className="text-xs text-neutral-400">{r.channel} · {r.agent}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Tag color={statusColors[r.status]} className="text-xs">{statusLabels[r.status]}</Tag>
|
<Tag color={statusColors[s.status]} className="text-xs">{statusLabels[s.status] || s.status}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-400 mt-1.5 truncate">{r.lastMessage}</p>
|
|
||||||
<div className="flex items-center justify-between mt-1">
|
<div className="flex items-center justify-between mt-1">
|
||||||
<span className="text-xs text-neutral-300">{r.time}</span>
|
<span className="text-xs text-neutral-300">{new Date(s.created_at).toLocaleString('zh-CN')}</span>
|
||||||
{r.satisfaction && <span className="text-xs text-yellow-500">{'★'.repeat(r.satisfaction)}</span>}
|
{s.satisfaction_score && <span className="text-xs text-yellow-500">{'★'.repeat(s.satisfaction_score)}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
)}
|
{!loading && sessions.length === 0 && <div className="flex items-center justify-center h-full"><Empty description="暂无对话记录" /></div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧详情 */}
|
|
||||||
<div className="flex-1 bg-white overflow-auto">
|
<div className="flex-1 bg-white overflow-auto">
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<div className="p-6 max-w-3xl mx-auto">
|
<div className="p-6 max-w-3xl mx-auto">
|
||||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-neutral-100">
|
<div className="flex items-center justify-between mb-6 pb-4 border-b border-neutral-100">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold text-neutral-800">{selected.visitorName}</h3>
|
<h3 className="text-lg font-semibold text-neutral-800">会话 {selected.id}</h3>
|
||||||
<Tag color={statusColors[selected.status]}>{statusLabels[selected.status]}</Tag>
|
<Tag color={statusColors[selected.status]}>{statusLabels[selected.status] || selected.status}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-neutral-400 mt-1">{selected.channel} · {selected.agent} · {selected.time}</div>
|
<div className="text-sm text-neutral-400 mt-1">客户ID: {selected.customer_id} · {new Date(selected.created_at).toLocaleString('zh-CN')}</div>
|
||||||
</div>
|
</div>
|
||||||
{selected.satisfaction && (
|
{selected.satisfaction_score && (
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xl text-yellow-500">{'★'.repeat(selected.satisfaction)}{'☆'.repeat(5 - selected.satisfaction)}</div>
|
<div className="text-xl text-yellow-500">{'★'.repeat(selected.satisfaction_score)}{'☆'.repeat(5 - selected.satisfaction_score)}</div>
|
||||||
<div className="text-xs text-neutral-400">满意度评分</div>
|
<div className="text-xs text-neutral-400">满意度评分</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selected.messages.map((msg, i) => (
|
<div className="text-sm text-neutral-400 text-center">消息内容通过 WebSocket 实时传输</div>
|
||||||
<div key={i} className={`flex ${msg.type === 'system' || msg.type === 'action' ? 'justify-center' : msg.sender.startsWith('客服') ? 'justify-start' : 'justify-end'}`}>
|
|
||||||
{msg.type === 'message' ? (
|
|
||||||
<div className={`max-w-[60%] rounded-lg px-3 py-2 text-sm ${msg.sender.startsWith('客服') ? 'bg-neutral-100 text-neutral-700' : 'bg-blue-500 text-white'}`}>
|
|
||||||
{msg.content}
|
|
||||||
<div className={`text-xs mt-1 ${msg.sender.startsWith('客服') ? 'text-neutral-400' : 'text-white/60'}`}>{msg.sender} · {msg.time}</div>
|
|
||||||
</div>
|
|
||||||
) : msg.type === 'system' ? (
|
|
||||||
<span className="text-xs text-neutral-300">{msg.content}</span>
|
|
||||||
) : (
|
|
||||||
<div className="bg-neutral-50 border border-neutral-200 rounded px-3 py-1.5 text-xs text-neutral-500">{msg.content}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : <div className="h-full flex items-center justify-center text-neutral-400">选择一个对话查看详情</div>}
|
||||||
<div className="h-full flex items-center justify-center text-neutral-400">选择一个对话查看详情</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user