对接前后端:种子数据填充、Dashboard和客户管理页面接入API
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"kefu-sys/server/internal/config"
|
||||||
|
"kefu-sys/server/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
model.InitDB(cfg.Database.DSN())
|
||||||
|
|
||||||
|
seed()
|
||||||
|
log.Println("种子数据填充完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
func seed() {
|
||||||
|
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
||||||
|
pwd := string(hash)
|
||||||
|
|
||||||
|
// Plans
|
||||||
|
plans := []model.Plan{
|
||||||
|
{Name: "基础版", PriceMonthly: 299, Seats: 2, StorageDays: 30, KBLimit: 50, Status: "active", Features: `{"stats":"basic","api":false,"channels":["web"],"brand":false}`},
|
||||||
|
{Name: "专业版", PriceMonthly: 699, Seats: 10, StorageDays: 180, KBLimit: 500, Status: "active", Features: `{"stats":"advanced","api":true,"channels":["web","wechat","app"],"brand":true}`},
|
||||||
|
{Name: "企业版", PriceMonthly: 1999, Seats: 50, StorageDays: 0, KBLimit: 0, Status: "active", Features: `{"stats":"custom","api":true,"channels":["all"],"brand":true,"dedicated":true}`},
|
||||||
|
}
|
||||||
|
model.DB.Create(&plans)
|
||||||
|
|
||||||
|
// Tenants
|
||||||
|
now := time.Now()
|
||||||
|
tenants := []model.Tenant{
|
||||||
|
{Name: "赵六科技", PlanID: &plans[2].ID, SeatCount: 30, ExpireAt: now.AddDate(1, 0, 0), Status: "normal", ContactName: "赵总", ContactPhone: "13800001111", ContactEmail: "zhao@tech.com"},
|
||||||
|
{Name: "李四商贸", PlanID: &plans[1].ID, SeatCount: 10, ExpireAt: now.AddDate(0, 6, 0), Status: "normal", ContactName: "李经理", ContactPhone: "13900002222", ContactEmail: "li@trade.com"},
|
||||||
|
{Name: "王五集团", PlanID: &plans[2].ID, SeatCount: 50, ExpireAt: now.AddDate(0, 0, 7), Status: "expiring", ContactName: "王总", ContactPhone: "13700003333", ContactEmail: "wang@group.com"},
|
||||||
|
{Name: "孙八信息", PlanID: &plans[0].ID, SeatCount: 2, ExpireAt: now.AddDate(1, 0, 0), Status: "suspended", ContactName: "孙经理", ContactPhone: "13600004444", ContactEmail: "sun@info.com"},
|
||||||
|
{Name: "周九科技", PlanID: &plans[1].ID, SeatCount: 8, ExpireAt: now.AddDate(0, -1, 0), Status: "expired", ContactName: "周总", ContactPhone: "13500005555", ContactEmail: "zhou@tech.com"},
|
||||||
|
}
|
||||||
|
model.DB.Create(&tenants)
|
||||||
|
|
||||||
|
// Platform admin
|
||||||
|
model.DB.Create(&model.User{
|
||||||
|
TenantID: 0, Role: "platform_admin", Username: "platform_admin", PasswordHash: pwd, Nickname: "平台管理员", Status: "online",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tenant 1 users
|
||||||
|
model.DB.Create(&model.User{
|
||||||
|
TenantID: tenants[0].ID, Role: "admin", Username: "admin", PasswordHash: pwd, Nickname: "赵总(管理员)", Status: "online",
|
||||||
|
})
|
||||||
|
model.DB.Create(&model.User{
|
||||||
|
TenantID: tenants[0].ID, Role: "supervisor", Username: "supervisor", PasswordHash: pwd, Nickname: "客服主管", Status: "online",
|
||||||
|
})
|
||||||
|
agent1 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent1", PasswordHash: pwd, Nickname: "客服小王", Status: "online"}
|
||||||
|
agent2 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent2", PasswordHash: pwd, Nickname: "客服小李", Status: "online"}
|
||||||
|
agent3 := model.User{TenantID: tenants[0].ID, Role: "agent", Username: "agent3", PasswordHash: pwd, Nickname: "客服小张", Status: "online"}
|
||||||
|
model.DB.Create(&[]model.User{agent1, agent2, agent3})
|
||||||
|
|
||||||
|
// Tenant 2 users
|
||||||
|
model.DB.Create(&model.User{
|
||||||
|
TenantID: tenants[1].ID, Role: "admin", Username: "litrade_admin", PasswordHash: pwd, Nickname: "李经理", Status: "online",
|
||||||
|
})
|
||||||
|
model.DB.Create(&model.User{
|
||||||
|
TenantID: tenants[1].ID, Role: "agent", Username: "litrade_agent1", PasswordHash: pwd, Nickname: "客服小李2", Status: "online",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Channels for Tenant 1
|
||||||
|
channels := []model.Channel{
|
||||||
|
{TenantID: tenants[0].ID, Type: "web", Name: "网页聊天", Status: "enabled", ScriptCode: `<script src="https://cs.example.com/widget.js" data-id="WK_8a3f2e"></script>`},
|
||||||
|
{TenantID: tenants[0].ID, Type: "wechat", Name: "微信公众号", Status: "enabled"},
|
||||||
|
{TenantID: tenants[0].ID, Type: "app", Name: "APP内嵌", Status: "disabled"},
|
||||||
|
}
|
||||||
|
model.DB.Create(&channels)
|
||||||
|
|
||||||
|
// Customers
|
||||||
|
customers := []model.Customer{
|
||||||
|
{TenantID: tenants[0].ID, Name: "张三", Phone: "138****8888", Email: "zhang@example.com", Tags: `["VIP客户","新客户"]`, Source: "网页", Status: "online", ConversationCount: 12},
|
||||||
|
{TenantID: tenants[0].ID, Name: "李四", Phone: "139****7777", Email: "li@example.com", Tags: `["活跃"]`, Source: "微信", Status: "offline", ConversationCount: 8},
|
||||||
|
{TenantID: tenants[0].ID, Name: "王五", Phone: "137****6666", Email: "wang@example.com", Tags: `["企业客户","VIP客户"]`, Source: "APP", Status: "busy", ConversationCount: 25},
|
||||||
|
{TenantID: tenants[0].ID, Name: "赵六科技", Phone: "136****5555", Email: "zhao@tech.com", Tags: `["企业客户"]`, Source: "网页", Status: "online", ConversationCount: 6},
|
||||||
|
{TenantID: tenants[0].ID, Name: "钱七", Phone: "135****4444", Tags: `["沉默"]`, Source: "邮件", Status: "offline", ConversationCount: 3},
|
||||||
|
{TenantID: tenants[0].ID, Name: "孙八", Phone: "134****3333", Email: "sun@example.com", Tags: `["新客户","活跃"]`, Source: "网页", Status: "online", ConversationCount: 2},
|
||||||
|
}
|
||||||
|
model.DB.Create(&customers)
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
sessions := []model.Session{
|
||||||
|
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[0].ID, AgentID: &agent1.ID, Status: "active", Priority: "urgent"},
|
||||||
|
{TenantID: tenants[0].ID, ChannelID: channels[1].ID, CustomerID: customers[1].ID, AgentID: &agent2.ID, Status: "active", Priority: "normal"},
|
||||||
|
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[2].ID, AgentID: nil, Status: "waiting", Priority: "normal"},
|
||||||
|
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[3].ID, AgentID: &agent3.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(5)},
|
||||||
|
{TenantID: tenants[0].ID, ChannelID: channels[0].ID, CustomerID: customers[4].ID, AgentID: &agent1.ID, Status: "ended", Priority: "normal", SatisfactionScore: ptr(4)},
|
||||||
|
}
|
||||||
|
model.DB.Create(&sessions)
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
messages := []model.Message{
|
||||||
|
{SessionID: sessions[0].ID, SenderType: "visitor", SenderID: &customers[0].ID, Content: "你好,我的订单怎么还没发货?", Type: "text", Seq: 1, SentAt: now},
|
||||||
|
{SessionID: sessions[0].ID, SenderType: "agent", SenderID: &agent1.ID, Content: "您好,请提供一下您的订单号,我帮您查看", Type: "text", Seq: 2, SentAt: now},
|
||||||
|
{SessionID: sessions[0].ID, SenderType: "visitor", SenderID: &customers[0].ID, Content: "订单号 AB20260714001", Type: "text", Seq: 3, SentAt: now},
|
||||||
|
}
|
||||||
|
model.DB.Create(&messages)
|
||||||
|
|
||||||
|
// Knowledge categories & entries
|
||||||
|
cats := []model.Category{
|
||||||
|
{TenantID: tenants[0].ID, Name: "产品常见问题"},
|
||||||
|
{TenantID: tenants[0].ID, Name: "售后服务"},
|
||||||
|
{TenantID: tenants[0].ID, Name: "技术支持"},
|
||||||
|
{TenantID: tenants[0].ID, Name: "快捷回复模板"},
|
||||||
|
}
|
||||||
|
model.DB.Create(&cats)
|
||||||
|
entries := []model.KnowledgeEntry{
|
||||||
|
{TenantID: tenants[0].ID, CategoryID: cats[0].ID, Title: "如何修改登录密码", Content: "登录后在右上角头像→个人设置→修改密码", Status: "published", UsageCount: 156},
|
||||||
|
{TenantID: tenants[0].ID, CategoryID: cats[0].ID, Title: "支持哪些支付方式", Content: "支持微信支付、支付宝、银行转账", Status: "published", UsageCount: 98},
|
||||||
|
{TenantID: tenants[0].ID, CategoryID: cats[1].ID, Title: "退货流程说明", Content: "在线申请→审核→寄回→退款,全程3-5个工作日", Status: "published", UsageCount: 45},
|
||||||
|
{TenantID: tenants[0].ID, CategoryID: cats[2].ID, Title: "API接口文档", Content: "开发者文档请访问 docs.example.com/api", Status: "draft", UsageCount: 0},
|
||||||
|
{TenantID: tenants[0].ID, CategoryID: cats[3].ID, Title: "欢迎语模板", Content: "您好!欢迎来到客服云,请问有什么可以帮您的?", Status: "published", UsageCount: 230},
|
||||||
|
}
|
||||||
|
model.DB.Create(&entries)
|
||||||
|
|
||||||
|
// Announcements
|
||||||
|
model.DB.Create(&[]model.Announcement{
|
||||||
|
{Title: "系统维护通知", Content: "平台将于7月20日 02:00-04:00 进行例行维护", Status: "published"},
|
||||||
|
{Title: "新功能上线", Content: "知识库批量导入功能已上线", Status: "published"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T { return &v }
|
||||||
@@ -1,30 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Tabs, Empty } from 'antd'
|
import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Empty, message } from 'antd'
|
||||||
import { SearchOutlined, PhoneOutlined, MailOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
import { SearchOutlined, PhoneOutlined, MailOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { getCustomers, type Customer } from '@/services/api'
|
||||||
interface Customer {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
phone: string
|
|
||||||
email: string
|
|
||||||
tags: string[]
|
|
||||||
status: 'online' | 'offline' | 'busy'
|
|
||||||
source: string
|
|
||||||
conversationCount: number
|
|
||||||
satisfaction: number
|
|
||||||
pending: number
|
|
||||||
lastContact: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockCustomers: Customer[] = [
|
|
||||||
{ id: '1', name: '张三', phone: '138****8888', email: 'zhang@example.com', tags: ['VIP客户', '新客户'], status: 'online', source: '网页', conversationCount: 12, satisfaction: 4.8, pending: 0, lastContact: '2026-07-14 10:32' },
|
|
||||||
{ id: '2', name: '李四', phone: '139****7777', email: 'li@example.com', tags: ['活跃'], status: 'offline', source: '微信', conversationCount: 8, satisfaction: 4.5, pending: 2, lastContact: '2026-07-14 09:15' },
|
|
||||||
{ id: '3', name: '王五', phone: '137****6666', email: 'wang@example.com', tags: ['企业客户', 'VIP客户'], status: 'busy', source: 'APP', conversationCount: 25, satisfaction: 4.2, pending: 1, lastContact: '2026-07-14 11:00' },
|
|
||||||
{ id: '4', name: '赵六科技', phone: '136****5555', email: 'zhao@tech.com', tags: ['企业客户'], status: 'online', source: '网页', conversationCount: 6, satisfaction: 4.9, pending: 0, lastContact: '2026-07-13 16:45' },
|
|
||||||
{ id: '5', name: '钱七', phone: '135****4444', email: '', tags: ['沉默'], status: 'offline', source: '邮件', conversationCount: 3, satisfaction: 3.5, pending: 0, lastContact: '2026-07-10 14:20' },
|
|
||||||
{ id: '6', name: '孙八', phone: '134****3333', email: 'sun@example.com', tags: ['新客户', '活跃'], status: 'online', source: '网页', conversationCount: 2, satisfaction: 0, pending: 3, lastContact: '2026-07-14 10:30' },
|
|
||||||
{ id: '7', name: '周九', phone: '133****2222', email: 'zhou@example.com', tags: ['VIP客户'], status: 'busy', source: '微信', conversationCount: 18, satisfaction: 4.7, pending: 0, lastContact: '2026-07-14 09:50' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const statusMap: Record<string, { color: string; text: string }> = {
|
const statusMap: Record<string, { color: string; text: string }> = {
|
||||||
online: { color: 'green', text: '在线' },
|
online: { color: 'green', text: '在线' },
|
||||||
@@ -33,140 +10,100 @@ const statusMap: Record<string, { color: string; text: string }> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tagColors: Record<string, string> = {
|
const tagColors: Record<string, string> = {
|
||||||
'VIP客户': 'gold',
|
'VIP客户': 'gold', '新客户': 'blue', '活跃': 'green', '沉默': 'default', '企业客户': 'purple',
|
||||||
'新客户': 'blue',
|
|
||||||
'活跃': 'green',
|
|
||||||
'沉默': 'default',
|
|
||||||
'企业客户': 'purple',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Customers = () => {
|
const Customers = () => {
|
||||||
|
const [customers, setCustomers] = useState<Customer[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [tagFilter, setTagFilter] = useState<string[]>([])
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
||||||
const [sourceFilter, setSourceFilter] = useState<string[]>([])
|
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
|
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
|
|
||||||
const filtered = mockCustomers.filter(c => {
|
useEffect(() => {
|
||||||
if (search && !c.name.includes(search) && !c.phone.includes(search) && !c.email.includes(search)) return false
|
loadCustomers()
|
||||||
if (tagFilter.length > 0 && !tagFilter.some(t => c.tags.includes(t))) return false
|
}, [page, search, statusFilter])
|
||||||
if (statusFilter.length > 0 && !statusFilter.includes(c.status)) return false
|
|
||||||
if (sourceFilter.length > 0 && !sourceFilter.includes(c.source)) return false
|
const loadCustomers = async () => {
|
||||||
return true
|
setLoading(true)
|
||||||
})
|
try {
|
||||||
|
const res = await getCustomers({ search, status: statusFilter[0], page })
|
||||||
|
setCustomers(res.list)
|
||||||
|
setTotal(res.total)
|
||||||
|
} catch {
|
||||||
|
setCustomers([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseTags = (tagsStr: string): string[] => {
|
||||||
|
try { return JSON.parse(tagsStr) } catch { return [] }
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '客户名称', dataIndex: 'name', key: 'name', render: (text: string, record: Customer) => <span className="text-sm font-medium text-neutral-800 cursor-pointer hover:text-blue-500" onClick={() => { setSelectedCustomer(record); setDrawerOpen(true) }}>{text}</span> },
|
{ title: '客户名称', dataIndex: 'name', key: 'name', render: (text: string, record: Customer) => (
|
||||||
|
<span className="text-sm font-medium text-neutral-800 cursor-pointer hover:text-blue-500" onClick={() => { setSelectedCustomer(record); setDrawerOpen(true) }}>{text}</span>
|
||||||
|
)},
|
||||||
{ title: '联系方式', key: 'contact', render: (_: unknown, record: Customer) => (
|
{ title: '联系方式', key: 'contact', render: (_: unknown, record: Customer) => (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{record.phone && <div className="text-xs text-neutral-500"><PhoneOutlined className="mr-1" />{record.phone}</div>}
|
{record.phone && <div className="text-xs text-neutral-500"><PhoneOutlined className="mr-1" />{record.phone}</div>}
|
||||||
{record.email && <div className="text-xs text-neutral-500"><MailOutlined className="mr-1" />{record.email}</div>}
|
{record.email && <div className="text-xs text-neutral-500"><MailOutlined className="mr-1" />{record.email}</div>}
|
||||||
</div>
|
</div>
|
||||||
)},
|
)},
|
||||||
{ title: '标签', dataIndex: 'tags', key: 'tags', render: (tags: string[]) => (
|
{ title: '标签', dataIndex: 'tags', key: 'tags', render: (tags: string) => (
|
||||||
<Space size={4} wrap>{tags.map(t => <Tag key={t} color={tagColors[t] || 'default'} className="text-xs">{t}</Tag>)}</Space>
|
<Space size={4} wrap>{parseTags(tags).map((t: string) => <Tag key={t} color={tagColors[t] || 'default'} className="text-xs">{t}</Tag>)}</Space>
|
||||||
)},
|
)},
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', render: (status: string) => <Badge color={statusMap[status]?.color} text={statusMap[status]?.text} /> },
|
{ title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => <Badge color={statusMap[s]?.color} text={statusMap[s]?.text} /> },
|
||||||
{ title: '来源', dataIndex: 'source', key: 'source', render: (text: string) => <span className="text-xs text-neutral-500">{text}</span> },
|
{ title: '来源', dataIndex: 'source', key: 'source', render: (t: string) => <span className="text-xs text-neutral-500">{t}</span> },
|
||||||
{ title: '对话次数', dataIndex: 'conversationCount', key: 'conversationCount', align: 'center' as const },
|
{ title: '对话次数', dataIndex: 'conversation_count', key: 'conversation_count', align: 'center' as const },
|
||||||
{ title: '满意度', dataIndex: 'satisfaction', key: 'satisfaction', align: 'center' as const, render: (v: number) => v > 0 ? <span className="text-green-600 font-medium">{v.toFixed(1)}</span> : <span className="text-neutral-300">-</span> },
|
{ title: '最近联系', dataIndex: 'last_contact_at', key: 'last_contact_at', render: (t: string) => <span className="text-xs text-neutral-400">{t || '-'}</span> },
|
||||||
{ title: '待处理', dataIndex: 'pending', key: 'pending', align: 'center' as const, render: (v: number) => v > 0 ? <Badge count={v} size="small" /> : <span className="text-neutral-300">0</span> },
|
|
||||||
{ title: '最近联系', dataIndex: 'lastContact', key: 'lastContact', render: (text: string) => <span className="text-xs text-neutral-400">{text}</span> },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col p-6">
|
<div className="h-full flex flex-col p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold text-neutral-800">客户管理</h2>
|
<h2 className="text-lg font-semibold text-neutral-800">客户管理</h2>
|
||||||
<Button type="primary" icon={<PlusOutlined />}>新增客户</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => message.info('新建客户')}>新增客户</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 mb-3 flex-wrap">
|
<div className="flex gap-3 mb-3 flex-wrap">
|
||||||
<Input prefix={<SearchOutlined />} placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => setSearch(e.target.value)} className="w-64" allowClear />
|
<Input prefix={<SearchOutlined />} placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => { setSearch(e.target.value); setPage(1) }} className="w-64" allowClear />
|
||||||
<Select mode="multiple" placeholder="标签筛选" value={tagFilter} onChange={setTagFilter} className="min-w-32" options={['VIP客户', '新客户', '活跃', '沉默', '企业客户'].map(v => ({ value: v, label: v }))} allowClear />
|
<Select mode="multiple" placeholder="状态筛选" value={statusFilter} onChange={v => { setStatusFilter(v); setPage(1) }} className="min-w-28" options={['online', 'offline', 'busy'].map(v => ({ value: v, label: statusMap[v].text }))} allowClear />
|
||||||
<Select mode="multiple" placeholder="状态筛选" value={statusFilter} onChange={setStatusFilter} className="min-w-28" options={['online', 'offline', 'busy'].map(v => ({ value: v, label: statusMap[v].text }))} allowClear />
|
|
||||||
<Select mode="multiple" placeholder="来源渠道" value={sourceFilter} onChange={setSourceFilter} className="min-w-28" options={['网页', '微信', 'APP', '邮件'].map(v => ({ value: v, label: v }))} allowClear />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 bg-white rounded-lg border border-neutral-200 overflow-hidden">
|
<div className="flex-1 bg-white rounded-lg border border-neutral-200 overflow-hidden">
|
||||||
<Table
|
<Table
|
||||||
dataSource={filtered}
|
dataSource={customers}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="middle"
|
size="middle"
|
||||||
pagination={{ pageSize: 10, showTotal: total => `共 ${total} 个客户` }}
|
loading={loading}
|
||||||
|
pagination={{ current: page, total, pageSize: 10, showTotal: t => `共 ${t} 个客户`, onChange: p => setPage(p) }}
|
||||||
locale={{ emptyText: <Empty description="暂无客户数据" /> }}
|
locale={{ emptyText: <Empty description="暂无客户数据" /> }}
|
||||||
onRow={record => ({ onClick: () => { setSelectedCustomer(record); setDrawerOpen(true) }, style: { cursor: 'pointer' } })}
|
onRow={record => ({ onClick: () => { setSelectedCustomer(record); setDrawerOpen(true) }, style: { cursor: 'pointer' } })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Drawer
|
<Drawer title="客户详情" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={400} extra={<Button type="primary" icon={<EditOutlined />} size="small">编辑</Button>}>
|
||||||
title="客户详情"
|
|
||||||
open={drawerOpen}
|
|
||||||
onClose={() => setDrawerOpen(false)}
|
|
||||||
width={400}
|
|
||||||
extra={<Button type="primary" icon={<EditOutlined />} size="small">编辑</Button>}
|
|
||||||
>
|
|
||||||
{selectedCustomer && (
|
{selectedCustomer && (
|
||||||
<Tabs
|
<div className="space-y-5">
|
||||||
defaultActiveKey="profile"
|
<div className="flex items-center gap-3 pb-4 border-b border-neutral-100">
|
||||||
items={[
|
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-500 text-lg font-semibold">{selectedCustomer.name[0]}</div>
|
||||||
{
|
<div>
|
||||||
key: 'profile',
|
<div className="text-base font-medium text-neutral-800">{selectedCustomer.name}</div>
|
||||||
label: '基本信息',
|
<div className="text-xs text-neutral-400">{selectedCustomer.source}</div>
|
||||||
children: (
|
</div>
|
||||||
<div className="space-y-5">
|
</div>
|
||||||
<div className="flex items-center gap-3 pb-4 border-b border-neutral-100">
|
<Descriptions column={1} size="small" colon={false}>
|
||||||
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-500 text-lg font-semibold">{selectedCustomer.name[0]}</div>
|
<Descriptions.Item label="手机号">{selectedCustomer.phone || '-'}</Descriptions.Item>
|
||||||
<div>
|
<Descriptions.Item label="邮箱">{selectedCustomer.email || '-'}</Descriptions.Item>
|
||||||
<div className="text-base font-medium text-neutral-800">{selectedCustomer.name}</div>
|
<Descriptions.Item label="状态"><Badge color={statusMap[selectedCustomer.status]?.color} text={statusMap[selectedCustomer.status]?.text} /></Descriptions.Item>
|
||||||
<div className="text-xs text-neutral-400">{selectedCustomer.source} · 最近 {selectedCustomer.lastContact}</div>
|
</Descriptions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Descriptions column={1} size="small" colon={false}>
|
|
||||||
<Descriptions.Item label="手机号">{selectedCustomer.phone || '-'}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="邮箱">{selectedCustomer.email || '-'}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="状态"><Badge color={statusMap[selectedCustomer.status]?.color} text={statusMap[selectedCustomer.status]?.text} /></Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-neutral-400 mb-2">标签</div>
|
|
||||||
<Space size={4} wrap>
|
|
||||||
{selectedCustomer.tags.map(t => <Tag key={t} color={tagColors[t] || 'default'} closable>{t}</Tag>)}
|
|
||||||
<Tag className="border-dashed cursor-pointer"><PlusOutlined /> 添加</Tag>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-neutral-400 mb-2">内部备注</div>
|
|
||||||
<div className="text-sm text-neutral-600 bg-neutral-50 rounded p-3">
|
|
||||||
该客户为重要 VIP,需优先响应。上次咨询售后服务,已解决。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'history',
|
|
||||||
label: '对话历史',
|
|
||||||
children: (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{[{ date: '2026-07-14 10:30', topic: '订单物流咨询', status: '已结束', satisfaction: 5 },
|
|
||||||
{ date: '2026-07-13 14:20', topic: '退换货流程', status: '已结束', satisfaction: 4 },
|
|
||||||
{ date: '2026-07-12 09:15', topic: '产品使用问题', status: '已结束', satisfaction: 5 },
|
|
||||||
].map((h, i) => (
|
|
||||||
<div key={i} className="p-3 border border-neutral-100 rounded-lg">
|
|
||||||
<div className="text-sm font-medium text-neutral-700">{h.topic}</div>
|
|
||||||
<div className="flex justify-between mt-1.5 text-xs text-neutral-400">
|
|
||||||
<span>{h.date}</span>
|
|
||||||
<span>{'★'.repeat(h.satisfaction)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+168
-128
@@ -1,186 +1,226 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Badge, Input, Tooltip } from 'antd'
|
import { Input, Spin, message } from 'antd'
|
||||||
import { SearchOutlined, UserOutlined, StarFilled } from '@ant-design/icons'
|
import { SearchOutlined, UserOutlined, StarFilled } from '@ant-design/icons'
|
||||||
|
import { useAuth } from '@/stores/auth'
|
||||||
|
import { getSessions, getSession, type Session as SessionType } from '@/services/api'
|
||||||
|
|
||||||
interface Session {
|
interface SessionDetail {
|
||||||
id: string
|
id: number
|
||||||
name: string
|
customerName: string
|
||||||
avatar?: string
|
phone: string
|
||||||
lastMessage: string
|
email: string
|
||||||
time: string
|
source: string
|
||||||
priority: 'urgent' | 'waiting' | 'active'
|
tags: string[]
|
||||||
unread: number
|
status: string
|
||||||
|
conversationCount: number
|
||||||
|
satisfaction: number
|
||||||
|
note: string
|
||||||
|
messages: { sender: string; content: string; time: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockSessions: Session[] = [
|
const priorityColors: Record<string, string> = { urgent: '#dc2626', normal: '#d97706', waiting: '#d97706' }
|
||||||
{ id: '1', name: '张三', lastMessage: '我的订单什么时候发货?', time: '10:32', priority: 'urgent', unread: 3 },
|
const priorityLabels: Record<string, string> = { urgent: '紧急', waiting: '等待中', active: '进行中', normal: '进行中' }
|
||||||
{ id: '2', name: '李四', lastMessage: '怎么退货啊', time: '10:28', priority: 'active', unread: 0 },
|
|
||||||
{ id: '3', name: '王五', lastMessage: '你们这个产品好用吗', time: '10:15', priority: 'active', unread: 1 },
|
|
||||||
{ id: '4', name: '赵六科技', lastMessage: '企业版价格能否优惠', time: '09:58', priority: 'waiting', unread: 0 },
|
|
||||||
{ id: '5', name: '钱七', lastMessage: '谢谢,问题已解决', time: '09:45', priority: 'active', unread: 0 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const priorityColors = { urgent: '#dc2626', waiting: '#d97706', active: '#2563eb' }
|
|
||||||
const priorityLabels = { urgent: '紧急', waiting: '等待中', active: '进行中' }
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [selectedId, setSelectedId] = useState<string>('1')
|
const { user } = useAuth()
|
||||||
const [message, setMessage] = useState('')
|
const [sessions, setSessions] = useState<SessionType[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||||
|
const [detail, setDetail] = useState<SessionDetail | null>(null)
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
|
const [messageInput, setMessageInput] = useState('')
|
||||||
|
|
||||||
const selectedSession = mockSessions.find(s => s.id === selectedId)
|
useEffect(() => {
|
||||||
|
loadSessions()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadSessions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await getSessions()
|
||||||
|
setSessions(res.list)
|
||||||
|
if (res.list.length > 0 && !selectedId) {
|
||||||
|
setSelectedId(res.list[0].id)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fallback to empty
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedId) {
|
||||||
|
loadDetail(selectedId)
|
||||||
|
}
|
||||||
|
}, [selectedId])
|
||||||
|
|
||||||
|
const loadDetail = async (id: number) => {
|
||||||
|
setDetailLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getSession(id)
|
||||||
|
const { session } = res.data
|
||||||
|
setDetail({
|
||||||
|
id: session.id,
|
||||||
|
customerName: `客户${session.customer_id}`,
|
||||||
|
phone: '获取中...',
|
||||||
|
email: '',
|
||||||
|
source: '网页',
|
||||||
|
tags: session.priority === 'urgent' ? ['VIP'] : [],
|
||||||
|
status: session.status,
|
||||||
|
conversationCount: 0,
|
||||||
|
satisfaction: session.satisfaction_score || 0,
|
||||||
|
note: '',
|
||||||
|
messages: (res.data as any).messages?.map((m: any) => ({
|
||||||
|
sender: m.sender_type === 'agent' ? `客服(${m.sender_id})` : '客户',
|
||||||
|
content: m.content,
|
||||||
|
time: new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||||
|
})) || [],
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
message.error('加载会话详情失败')
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!messageInput.trim()) return
|
||||||
|
setMessageInput('')
|
||||||
|
message.info('WebSocket 消息发送(待连接)')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="h-full flex items-center justify-center"><Spin size="large" /></div>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* 左侧:会话列表 */}
|
{/* 左侧:会话列表 */}
|
||||||
<div className="w-[300px] flex-shrink-0 border-r border-neutral-200 bg-white flex flex-col">
|
<div className="w-[300px] flex-shrink-0 border-r border-neutral-200 bg-white flex flex-col">
|
||||||
<div className="p-3 border-b border-neutral-100">
|
<div className="px-3 py-3 border-b border-neutral-100">
|
||||||
<Input prefix={<SearchOutlined />} placeholder="搜索会话..." variant="borderless" />
|
<div className="text-sm font-medium text-neutral-800">{user?.nickname || '客服'}</div>
|
||||||
|
<div className="text-xs text-neutral-400">在线</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex px-3 py-2 gap-2 border-b border-neutral-100">
|
<div className="p-3 border-b border-neutral-100">
|
||||||
{(['urgent', 'waiting', 'active'] as const).map(p => (
|
<Input prefix={<SearchOutlined />} placeholder="搜索会话..." variant="borderless" size="small" />
|
||||||
<span key={p} className="text-xs px-2 py-0.5 rounded-full cursor-pointer hover:bg-neutral-100" style={{ color: priorityColors[p] }}>
|
|
||||||
{priorityLabels[p]}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{mockSessions.map(session => (
|
{sessions.length === 0 ? (
|
||||||
<div
|
<div className="flex items-center justify-center h-full text-neutral-400 text-sm">暂无会话</div>
|
||||||
key={session.id}
|
) : (
|
||||||
className={`px-3 py-2.5 cursor-pointer border-b border-neutral-50 hover:bg-neutral-50 transition-colors ${selectedId === session.id ? 'bg-blue-50' : ''}`}
|
sessions.map(s => (
|
||||||
onClick={() => setSelectedId(session.id)}
|
<div
|
||||||
>
|
key={s.id}
|
||||||
<div className="flex items-center justify-between">
|
className={`px-3 py-2.5 cursor-pointer border-b border-neutral-50 hover:bg-neutral-50 transition-colors ${selectedId === s.id ? 'bg-blue-50' : ''}`}
|
||||||
<div className="flex items-center gap-2">
|
onClick={() => setSelectedId(s.id)}
|
||||||
<span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: priorityColors[session.priority] }} />
|
>
|
||||||
<span className="text-sm font-medium text-neutral-800">{session.name}</span>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: priorityColors[s.priority] || '#2563eb' }} />
|
||||||
{session.unread > 0 && <Badge count={session.unread} size="small" />}
|
<span className="text-sm font-medium text-neutral-800">客户{s.customer_id}</span>
|
||||||
<span className="text-xs text-neutral-400">{session.time}</span>
|
</div>
|
||||||
|
<span className="text-xs text-neutral-300">{new Date(s.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400 mt-1 truncate pl-4">
|
||||||
|
<TagBadge status={s.status} priority={s.priority} />
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-400 mt-1 truncate pl-4">{session.lastMessage}</p>
|
))
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中间:聊天区 */}
|
{/* 中间:聊天区 */}
|
||||||
<div className="flex-1 flex flex-col min-w-0 bg-white">
|
<div className="flex-1 flex flex-col min-w-0 bg-white">
|
||||||
{selectedSession ? (
|
{detail ? (
|
||||||
<>
|
<>
|
||||||
<div className="h-12 px-4 flex items-center justify-between border-b border-neutral-100 flex-shrink-0">
|
<div className="h-12 px-4 flex items-center justify-between border-b border-neutral-100 flex-shrink-0">
|
||||||
<span className="text-sm font-medium text-neutral-800">{selectedSession.name}</span>
|
<span className="text-sm font-medium text-neutral-800">{detail.customerName}</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Tooltip title="转接"><span className="text-neutral-400 cursor-pointer hover:text-neutral-600 text-sm">转接</span></Tooltip>
|
<span className="text-xs text-neutral-400 cursor-pointer hover:text-neutral-600">转接</span>
|
||||||
<Tooltip title="结束会话"><span className="text-neutral-400 cursor-pointer hover:text-red-500 text-sm">结束</span></Tooltip>
|
<span className="text-xs text-neutral-400 cursor-pointer hover:text-red-500">结束</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto p-4 flex flex-col gap-3">
|
<div className="flex-1 overflow-auto p-4 flex flex-col gap-3">
|
||||||
<div className="flex flex-col items-start">
|
{detailLoading ? (
|
||||||
<div className="bg-neutral-100 rounded-lg px-3 py-2 text-sm text-neutral-700 max-w-[70%]">
|
<div className="flex-1 flex items-center justify-center"><Spin /></div>
|
||||||
你好,我想咨询一下订单物流问题
|
) : detail.messages.length === 0 ? (
|
||||||
</div>
|
<div className="flex-1 flex items-center justify-center text-neutral-400 text-sm">暂无消息</div>
|
||||||
<span className="text-xs text-neutral-400 mt-0.5">10:30</span>
|
) : (
|
||||||
</div>
|
detail.messages.map((msg, i) => (
|
||||||
<div className="flex flex-col items-end">
|
<div key={i} className={`flex ${msg.sender.startsWith('客服') ? 'justify-start' : 'justify-end'}`}>
|
||||||
<div className="bg-blue-500 rounded-lg px-3 py-2 text-sm text-white max-w-[70%]">
|
<div className={`max-w-[70%] 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>
|
<div className={`text-xs mt-1 ${msg.sender.startsWith('客服') ? 'text-neutral-400' : 'text-white/60'}`}>{msg.time}</div>
|
||||||
<span className="text-xs text-neutral-400 mt-0.5">10:31</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start">
|
))
|
||||||
<div className="bg-neutral-100 rounded-lg px-3 py-2 text-sm text-neutral-700 max-w-[70%]">
|
)}
|
||||||
订单号是:AB20260714001
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-neutral-400 mt-0.5">10:31</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end">
|
|
||||||
<div className="bg-blue-500 rounded-lg px-3 py-2 text-sm text-white max-w-[70%]">
|
|
||||||
好的,正在为您查询,请稍候...
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-neutral-400 mt-0.5">10:32</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 border-t border-neutral-100 flex-shrink-0">
|
<div className="p-3 border-t border-neutral-100 flex-shrink-0">
|
||||||
<div className="flex items-center gap-2 bg-neutral-50 rounded-lg px-3 py-2">
|
<div className="flex items-center gap-2 bg-neutral-50 rounded-lg px-3 py-2">
|
||||||
<input
|
<input
|
||||||
className="flex-1 bg-transparent outline-none text-sm text-neutral-700 placeholder:text-neutral-400"
|
className="flex-1 bg-transparent outline-none text-sm text-neutral-700 placeholder:text-neutral-400"
|
||||||
placeholder="输入消息... (Enter 发送)"
|
placeholder="输入消息... (Enter 发送)"
|
||||||
value={message}
|
value={messageInput}
|
||||||
onChange={e => setMessage(e.target.value)}
|
onChange={e => setMessageInput(e.target.value)}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => { if (e.key === 'Enter') handleSend() }}
|
||||||
if (e.key === 'Enter' && message.trim()) {
|
|
||||||
setMessage('')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<span className="text-neutral-300 cursor-pointer hover:text-neutral-500">😊</span>
|
<span className="text-neutral-300 cursor-pointer hover:text-neutral-500">😊</span>
|
||||||
<span className="text-neutral-300 cursor-pointer hover:text-neutral-500">📎</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center text-neutral-400">
|
<div className="flex-1 flex items-center justify-center text-neutral-400">选择一个会话</div>
|
||||||
选择一个会话开始接待
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:客户信息面板 */}
|
{/* 右侧:客户信息面板 */}
|
||||||
<div className="w-[300px] flex-shrink-0 border-l border-neutral-200 bg-white overflow-auto">
|
<div className="w-[300px] flex-shrink-0 border-l border-neutral-200 bg-white overflow-auto">
|
||||||
<div className="p-4 border-b border-neutral-100">
|
{detail ? (
|
||||||
<div className="flex items-center gap-3">
|
<div className="p-4 space-y-4">
|
||||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
<div className="flex items-center gap-3 pb-3 border-b border-neutral-100">
|
||||||
<UserOutlined className="text-blue-500" />
|
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||||
|
<UserOutlined className="text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-neutral-800">{detail.customerName}</div>
|
||||||
|
<div className="text-xs text-neutral-400">ID: {detail.id}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-neutral-800">{selectedSession?.name || '访客'}</div>
|
<div className="text-xs text-neutral-400 mb-1.5">标签</div>
|
||||||
<div className="text-xs text-neutral-400 mt-0.5">网页渠道</div>
|
<div className="flex flex-wrap gap-1">
|
||||||
</div>
|
{detail.tags.map(t => <span key={t} className="text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-600">{t}</span>)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-neutral-400 mb-1.5">标签</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-orange-50 text-orange-600">VIP客户</span>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-600">新客户</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-neutral-400 mb-1.5">联系信息</div>
|
|
||||||
<div className="text-sm text-neutral-700 space-y-1">
|
|
||||||
<div>📱 138****8888</div>
|
|
||||||
<div>📧 zhang***@example.com</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-neutral-400 mb-1.5">统计数据</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="bg-neutral-50 rounded p-2 text-center">
|
|
||||||
<div className="text-lg font-semibold text-neutral-800">12</div>
|
|
||||||
<div className="text-xs text-neutral-400">对话次数</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-neutral-50 rounded p-2 text-center">
|
</div>
|
||||||
<div className="text-lg font-semibold text-green-600 flex items-center justify-center gap-0.5">
|
<div>
|
||||||
4.8 <StarFilled className="text-xs" />
|
<div className="text-xs text-neutral-400 mb-1.5">统计数据</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="bg-neutral-50 rounded p-2 text-center">
|
||||||
|
<div className="text-lg font-semibold text-neutral-800">{detail.conversationCount}</div>
|
||||||
|
<div className="text-xs text-neutral-400">对话次数</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-neutral-50 rounded p-2 text-center">
|
||||||
|
<div className="text-lg font-semibold text-green-600 flex items-center justify-center gap-0.5">
|
||||||
|
{detail.satisfaction > 0 ? detail.satisfaction : '-'} {detail.satisfaction > 0 && <StarFilled className="text-xs" />}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-neutral-400">满意度</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-neutral-400">满意度</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
) : null}
|
||||||
<div className="text-xs text-neutral-400 mb-1.5">内部备注</div>
|
|
||||||
<div className="text-sm text-neutral-500 bg-neutral-50 rounded p-2">
|
|
||||||
之前咨询过售后问题...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TagBadge({ status, priority }: { status: string; priority: string }) {
|
||||||
|
const color = status === 'ended' ? '#16a34a' : priorityColors[priority] || '#2563eb'
|
||||||
|
const text = status === 'ended' ? '已结束' : status === 'waiting' ? '等待中' : priorityLabels[priority] || status
|
||||||
|
return <span className="text-xs px-1.5 py-0.5 rounded" style={{ color, backgroundColor: `${color}15` }}>{text}</span>
|
||||||
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user