对接前后端:种子数据填充、Dashboard和客户管理页面接入API
This commit is contained in:
@@ -1,30 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Tabs, Empty } from 'antd'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Table, Input, Select, Tag, Drawer, Button, Space, Badge, Descriptions, Empty, message } from 'antd'
|
||||
import { SearchOutlined, PhoneOutlined, MailOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
|
||||
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' },
|
||||
]
|
||||
import { getCustomers, type Customer } from '@/services/api'
|
||||
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
online: { color: 'green', text: '在线' },
|
||||
@@ -33,140 +10,100 @@ const statusMap: Record<string, { color: string; text: string }> = {
|
||||
}
|
||||
|
||||
const tagColors: Record<string, string> = {
|
||||
'VIP客户': 'gold',
|
||||
'新客户': 'blue',
|
||||
'活跃': 'green',
|
||||
'沉默': 'default',
|
||||
'企业客户': 'purple',
|
||||
'VIP客户': 'gold', '新客户': 'blue', '活跃': 'green', '沉默': 'default', '企业客户': 'purple',
|
||||
}
|
||||
|
||||
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 [tagFilter, setTagFilter] = useState<string[]>([])
|
||||
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
||||
const [sourceFilter, setSourceFilter] = useState<string[]>([])
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
const filtered = mockCustomers.filter(c => {
|
||||
if (search && !c.name.includes(search) && !c.phone.includes(search) && !c.email.includes(search)) return false
|
||||
if (tagFilter.length > 0 && !tagFilter.some(t => c.tags.includes(t))) return false
|
||||
if (statusFilter.length > 0 && !statusFilter.includes(c.status)) return false
|
||||
if (sourceFilter.length > 0 && !sourceFilter.includes(c.source)) return false
|
||||
return true
|
||||
})
|
||||
useEffect(() => {
|
||||
loadCustomers()
|
||||
}, [page, search, statusFilter])
|
||||
|
||||
const loadCustomers = async () => {
|
||||
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 = [
|
||||
{ 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) => (
|
||||
<div className="space-y-0.5">
|
||||
{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>}
|
||||
</div>
|
||||
)},
|
||||
{ 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>
|
||||
{ title: '标签', dataIndex: 'tags', key: 'tags', render: (tags: string) => (
|
||||
<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: 'source', key: 'source', render: (text: string) => <span className="text-xs text-neutral-500">{text}</span> },
|
||||
{ title: '对话次数', dataIndex: 'conversationCount', key: 'conversationCount', 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: '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> },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => <Badge color={statusMap[s]?.color} text={statusMap[s]?.text} /> },
|
||||
{ title: '来源', dataIndex: 'source', key: 'source', render: (t: string) => <span className="text-xs text-neutral-500">{t}</span> },
|
||||
{ title: '对话次数', dataIndex: 'conversation_count', key: 'conversation_count', align: 'center' as const },
|
||||
{ title: '最近联系', dataIndex: 'last_contact_at', key: 'last_contact_at', render: (t: string) => <span className="text-xs text-neutral-400">{t || '-'}</span> },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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 className="flex gap-3 mb-3 flex-wrap">
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => setSearch(e.target.value)} 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={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 />
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索客户名称、手机号、邮箱" value={search} onChange={e => { setSearch(e.target.value); setPage(1) }} className="w-64" 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 />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 bg-white rounded-lg border border-neutral-200 overflow-hidden">
|
||||
<Table
|
||||
dataSource={filtered}
|
||||
dataSource={customers}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
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="暂无客户数据" /> }}
|
||||
onRow={record => ({ onClick: () => { setSelectedCustomer(record); setDrawerOpen(true) }, style: { cursor: 'pointer' } })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
title="客户详情"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={400}
|
||||
extra={<Button type="primary" icon={<EditOutlined />} size="small">编辑</Button>}
|
||||
>
|
||||
<Drawer title="客户详情" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={400} extra={<Button type="primary" icon={<EditOutlined />} size="small">编辑</Button>}>
|
||||
{selectedCustomer && (
|
||||
<Tabs
|
||||
defaultActiveKey="profile"
|
||||
items={[
|
||||
{
|
||||
key: 'profile',
|
||||
label: '基本信息',
|
||||
children: (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-3 pb-4 border-b border-neutral-100">
|
||||
<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>
|
||||
<div className="text-base font-medium text-neutral-800">{selectedCustomer.name}</div>
|
||||
<div className="text-xs text-neutral-400">{selectedCustomer.source} · 最近 {selectedCustomer.lastContact}</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>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-3 pb-4 border-b border-neutral-100">
|
||||
<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>
|
||||
<div className="text-base font-medium text-neutral-800">{selectedCustomer.name}</div>
|
||||
<div className="text-xs text-neutral-400">{selectedCustomer.source}</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>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user