feat(distribution): 分销站点增加用户数量统计与用户信息展示模块
- 数据模型与迁移: 新增 000082 迁移在 users 表追加 distribution_site_id 并建索引,用户首次在分销站点注册/短信登录时自动绑定站点归属 - 分销管理接口: 新增 GET /admin/distribution/users 接口,聚合本站注册用户与下单客户,返回完整11位手机号、累计消费金额、订单数及来源标签 - 统计与大盘扩展: GET /admin/distribution/sites/:id/analytics 补全今日注册、累计用户、成交客户指标,主站分销商列表追加用户体量展示 - 分销后台前端: 新增「客户与用户」页面 (Users.tsx) 支持一键复制手机号与订单穿透,大盘 (Dashboard.tsx) 增加用户规模核心卡片 - 自动化测试与门禁: 新增 TestListDistributionUsersAndAnalytics 单元测试,通过全量 Go 后端集成测试及前后端构建检查
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
GiftOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { getMe, logout, type CurrentMember, getStoredToken } from './api'
|
||||
import { MemberContext } from './MemberContext'
|
||||
@@ -31,6 +32,7 @@ import Wallet from './pages/Wallet'
|
||||
import Announcements from './pages/Announcements'
|
||||
import SiteSettings from './pages/SiteSettings'
|
||||
import Invitations from './pages/Invitations'
|
||||
import Users from './pages/Users'
|
||||
import Login from './pages/Login'
|
||||
import Storefront from './pages/Storefront'
|
||||
import './style.css'
|
||||
@@ -44,6 +46,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
...(member.is_superadmin
|
||||
? [{ key: '/sites', icon: <ShopOutlined />, label: <Link to="/sites">分销站点管理</Link> }]
|
||||
: []),
|
||||
{ key: '/users', icon: <TeamOutlined />, label: <Link to="/users">客户与用户</Link> },
|
||||
{ key: '/orders', icon: <OrderedListOutlined />, label: <Link to="/orders">全部分销订单</Link> },
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号订单</Link> },
|
||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号管理</Link> },
|
||||
@@ -169,6 +172,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
<Route path="/sold" element={<Orders kind="sold" />} />
|
||||
<Route path="/receive" element={<Orders kind="receive" />} />
|
||||
<Route path="/orders" element={<Orders kind="all" />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/invitations" element={<Invitations />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -138,6 +138,8 @@ export interface Site {
|
||||
admin_username?: string
|
||||
admin_nickname?: string
|
||||
member_type?: string
|
||||
user_count?: number
|
||||
order_user_count?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -820,6 +822,9 @@ export interface DistributionAnalytics {
|
||||
today_profit_cent: number
|
||||
month_profit_cent: number
|
||||
available_balance_cent: number
|
||||
today_user_count?: number
|
||||
total_user_count?: number
|
||||
total_order_user_count?: number
|
||||
trend_days: {
|
||||
date: string
|
||||
received_count: number
|
||||
@@ -834,6 +839,53 @@ export async function fetchDashboardAnalytics(siteID: number): Promise<Distribut
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export interface DistributionUser {
|
||||
id: number
|
||||
phone: string
|
||||
nickname: string
|
||||
avatar_url: string
|
||||
realname_status: string
|
||||
status: string
|
||||
source: 'register' | 'order' | 'both'
|
||||
order_count: number
|
||||
total_spent_cent: number
|
||||
last_order_at?: string
|
||||
created_at: string
|
||||
last_login_at?: string
|
||||
}
|
||||
|
||||
export interface DistributionUserPage {
|
||||
items: DistributionUser[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
total_users: number
|
||||
order_users: number
|
||||
}
|
||||
|
||||
export interface ListDistributionUsersParams {
|
||||
siteID?: number
|
||||
keyword?: string
|
||||
source?: 'all' | 'register' | 'order'
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export async function listDistributionUsers(
|
||||
params: ListDistributionUsersParams = {}
|
||||
): Promise<DistributionUserPage> {
|
||||
const resp = await api.get<ApiResponse<DistributionUserPage>>('/admin/distribution/users', {
|
||||
params: {
|
||||
site_id: params.siteID,
|
||||
keyword: params.keyword,
|
||||
source: params.source,
|
||||
page: params.page,
|
||||
page_size: params.pageSize,
|
||||
},
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export interface SiteSMSConfig {
|
||||
enabled: boolean
|
||||
provider: string
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
LineChartOutlined,
|
||||
ArrowRightOutlined,
|
||||
SkinOutlined,
|
||||
TeamOutlined,
|
||||
ShoppingOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
@@ -197,34 +199,35 @@ export default function Dashboard() {
|
||||
|
||||
{/* 核心业务指标看板 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日收号 / 累计收号"
|
||||
value={analytics?.today_received_count ?? 0}
|
||||
suffix={`/ ${analytics?.total_received_count ?? 0} 个`}
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/receive" style={{ fontSize: 12 }}>查看收号资产 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="在售可租货源"
|
||||
value={analytics?.active_listing_count ?? 0}
|
||||
prefix={<AppstoreOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="个"
|
||||
title="今日注册 / 累计用户"
|
||||
value={analytics?.today_user_count ?? 0}
|
||||
suffix={`/ ${analytics?.total_user_count ?? 0} 人`}
|
||||
prefix={<TeamOutlined style={{ color: '#52c41a' }} />}
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/accounts" style={{ fontSize: 12 }}>管理商品货架 <ArrowRightOutlined /></Link>
|
||||
<Link to="/users" style={{ fontSize: 12 }}>查看客户列表 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fff7e6', border: '1px solid #ffd591' }} loading={loading}>
|
||||
<Statistic
|
||||
title="本站成交客户"
|
||||
value={analytics?.total_order_user_count ?? 0}
|
||||
suffix="人"
|
||||
prefix={<ShoppingOutlined style={{ color: '#fa8c16' }} />}
|
||||
valueStyle={{ color: '#fa8c16', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/users" style={{ fontSize: 12 }}>成交客户分析 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日订单量"
|
||||
@@ -238,7 +241,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日成交 GMV"
|
||||
@@ -251,7 +254,34 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="今日收号 / 累计收号"
|
||||
value={analytics?.today_received_count ?? 0}
|
||||
suffix={`/ ${analytics?.total_received_count ?? 0} 个`}
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/receive" style={{ fontSize: 12 }}>查看收号资产 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="在售可租货源"
|
||||
value={analytics?.active_listing_count ?? 0}
|
||||
prefix={<AppstoreOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="个"
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/accounts" style={{ fontSize: 12 }}>管理商品货架 <ArrowRightOutlined /></Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#fafafa' }} loading={loading}>
|
||||
<Statistic
|
||||
title="本月预估分润收益"
|
||||
@@ -265,14 +295,14 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={4}>
|
||||
<Card bordered={false} style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }} loading={loading}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card bordered={false} style={{ background: '#e6f4ff', border: '1px solid #91caff' }} loading={loading}>
|
||||
<Statistic
|
||||
title="钱包可提现余额"
|
||||
value={((analytics?.available_balance_cent ?? 0) / 100).toFixed(2)}
|
||||
prefix={<WalletOutlined style={{ color: '#52c41a' }} />}
|
||||
prefix={<WalletOutlined style={{ color: '#1677ff' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link to="/wallet" style={{ fontSize: 12 }}>立即提现 <ArrowRightOutlined /></Link>
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Input,
|
||||
Select,
|
||||
Tag,
|
||||
Button,
|
||||
Avatar,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
message,
|
||||
Tooltip,
|
||||
} from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
CopyOutlined,
|
||||
CheckOutlined,
|
||||
ShoppingOutlined,
|
||||
TeamOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
listDistributionUsers,
|
||||
listSites,
|
||||
formatMoney,
|
||||
type DistributionUser,
|
||||
type Site,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
|
||||
function formatDateTime(str?: string): string {
|
||||
if (!str) return '-'
|
||||
try {
|
||||
const d = new Date(str)
|
||||
if (isNaN(d.getTime())) return str
|
||||
const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`)
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
export default function Users() {
|
||||
const member = useMember()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
const [users, setUsers] = useState<DistributionUser[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [totalUsers, setTotalUsers] = useState(0)
|
||||
const [orderUsers, setOrderUsers] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 筛选条件
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [source, setSource] = useState<'all' | 'register' | 'order'>('all')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
|
||||
// 复制状态记录
|
||||
const [copiedPhone, setCopiedPhone] = useState<string | null>(null)
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard?.writeText(text)
|
||||
setCopiedPhone(text)
|
||||
message.success(`已复制手机号:${text}`)
|
||||
setTimeout(() => {
|
||||
setCopiedPhone(null)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const loadData = useCallback(
|
||||
async (curSiteID?: number, curPage = page, curSize = pageSize) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await listDistributionUsers({
|
||||
siteID: curSiteID,
|
||||
keyword: keyword.trim() || undefined,
|
||||
source,
|
||||
page: curPage,
|
||||
pageSize: curSize,
|
||||
})
|
||||
setUsers(res.items || [])
|
||||
setTotal(res.total || 0)
|
||||
setTotalUsers(res.total_users || 0)
|
||||
setOrderUsers(res.order_users || 0)
|
||||
} catch {
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[keyword, source, page, pageSize]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
if (member?.is_superadmin) {
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
if (!siteID && rows.length > 0) {
|
||||
setSiteID(rows[0].id)
|
||||
void loadData(rows[0].id, 1, pageSize)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
void loadData(siteID, 1, pageSize)
|
||||
}
|
||||
void init()
|
||||
}, [member]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSearch = () => {
|
||||
setPage(1)
|
||||
void loadData(siteID, 1, pageSize)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword('')
|
||||
setSource('all')
|
||||
setPage(1)
|
||||
setTimeout(() => {
|
||||
void loadData(siteID, 1, pageSize)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户 ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 90,
|
||||
render: (id: number) => (
|
||||
<Typography.Text code style={{ fontSize: 12 }}>
|
||||
#{id}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 170,
|
||||
render: (phone: string) => (
|
||||
<Space size="small">
|
||||
<Typography.Text strong style={{ fontFamily: 'monospace', fontSize: 13, color: '#1677ff' }}>
|
||||
{phone}
|
||||
</Typography.Text>
|
||||
<Tooltip title={copiedPhone === phone ? '已复制' : '点击复制完整手机号'}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedPhone === phone ? <CheckOutlined style={{ color: '#52c41a' }} /> : <CopyOutlined />}
|
||||
onClick={() => copyToClipboard(phone)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '昵称 / 头像',
|
||||
key: 'user_info',
|
||||
width: 160,
|
||||
render: (_: unknown, row: DistributionUser) => (
|
||||
<Space size="small">
|
||||
<Avatar
|
||||
size="small"
|
||||
src={row.avatar_url || undefined}
|
||||
icon={!row.avatar_url ? <UserOutlined /> : undefined}
|
||||
style={{ backgroundColor: '#1677ff' }}
|
||||
/>
|
||||
<Typography.Text style={{ fontSize: 13 }}>{row.nickname || '-'}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '归属来源',
|
||||
dataIndex: 'source',
|
||||
key: 'source',
|
||||
width: 130,
|
||||
render: (src: string) => {
|
||||
if (src === 'both') {
|
||||
return <Tag color="blue">注册且下单</Tag>
|
||||
}
|
||||
if (src === 'order') {
|
||||
return <Tag color="orange">本站下单客户</Tag>
|
||||
}
|
||||
return <Tag color="green">本站注册用户</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '本站下单数',
|
||||
dataIndex: 'order_count',
|
||||
key: 'order_count',
|
||||
width: 120,
|
||||
render: (count: number, row: DistributionUser) => {
|
||||
if (count > 0) {
|
||||
return (
|
||||
<Tooltip title="点击查看该用户在当前站点的全部订单">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: 0, fontWeight: 'bold' }}
|
||||
onClick={() => navigate(`/orders?q=${encodeURIComponent(row.phone)}`)}
|
||||
>
|
||||
{count} 单 ↗
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return <Typography.Text type="secondary">0 单</Typography.Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '本站累计消费',
|
||||
dataIndex: 'total_spent_cent',
|
||||
key: 'total_spent_cent',
|
||||
width: 130,
|
||||
render: (cent: number) => {
|
||||
if (cent > 0) {
|
||||
return (
|
||||
<Typography.Text strong style={{ color: '#fa8c16' }}>
|
||||
{formatMoney(cent)}
|
||||
</Typography.Text>
|
||||
)
|
||||
}
|
||||
return <Typography.Text type="secondary">¥0.00</Typography.Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '实名状态',
|
||||
dataIndex: 'realname_status',
|
||||
key: 'realname_status',
|
||||
width: 110,
|
||||
render: (st: string) => {
|
||||
if (st === 'verified') {
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
已实名
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||
未实名
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 170,
|
||||
render: (t: string) => (
|
||||
<Typography.Text style={{ fontSize: 12, color: '#666' }}>{formatDateTime(t)}</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'last_login_at',
|
||||
key: 'last_login_at',
|
||||
width: 170,
|
||||
render: (t?: string) => (
|
||||
<Typography.Text style={{ fontSize: 12, color: '#888' }}>{formatDateTime(t)}</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const selectedSiteName =
|
||||
member?.site_name || sites.find((s) => s.id === siteID)?.site_name || '当前分销商'
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" className="page" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
客户与用户管理
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
当前站点:{selectedSiteName} · 查看在本分销站注册的专属用户以及在本站产生交易的客户
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
{member?.is_superadmin && sites.length > 1 && (
|
||||
<Select
|
||||
placeholder="切换分销网站"
|
||||
style={{ width: 220 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({
|
||||
label: `${site.site_name} (${site.site_code})`,
|
||||
value: site.id,
|
||||
}))}
|
||||
onChange={(val) => {
|
||||
setSiteID(val)
|
||||
setPage(1)
|
||||
void loadData(val, 1, pageSize)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData(siteID, page, pageSize)}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 顶部统计卡片 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card bordered={false} style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}>
|
||||
<Statistic
|
||||
title="本站注册用户"
|
||||
value={totalUsers}
|
||||
prefix={<TeamOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="人"
|
||||
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: '#52c41a' }}>
|
||||
通过本分销站点/专属域名注册绑定的私域用户
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card bordered={false} style={{ background: '#fff7e6', border: '1px solid #ffd591' }}>
|
||||
<Statistic
|
||||
title="本站成交客户"
|
||||
value={orderUsers}
|
||||
prefix={<ShoppingOutlined style={{ color: '#fa8c16' }} />}
|
||||
suffix="人"
|
||||
valueStyle={{ color: '#fa8c16', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: '#fa8c16' }}>
|
||||
在本分销站点完成过账号租借交易的有效客户
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card bordered={false} style={{ background: '#e6f4ff', border: '1px solid #91caff' }}>
|
||||
<Statistic
|
||||
title="关联用户总规模"
|
||||
value={total}
|
||||
prefix={<UserOutlined style={{ color: '#1677ff' }} />}
|
||||
suffix="人"
|
||||
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: '#1677ff' }}>
|
||||
本站注册或产生过消费行为的全部用户总计
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 筛选与检索栏 */}
|
||||
<Card bordered={false}>
|
||||
<Space wrap size="middle" style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="搜索用户手机号 / 昵称"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 240 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
value={source}
|
||||
onChange={(val) => {
|
||||
setSource(val)
|
||||
setPage(1)
|
||||
}}
|
||||
style={{ width: 150 }}
|
||||
options={[
|
||||
{ label: '全部来源客户', value: 'all' },
|
||||
{ label: '仅本站注册用户', value: 'register' },
|
||||
{ label: '仅本站下单客户', value: 'order' },
|
||||
]}
|
||||
/>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 个用户`,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p)
|
||||
setPageSize(ps)
|
||||
void loadData(siteID, p, ps)
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 1100 }}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user