af49d22ca7
Add Telegram, WhatsApp and email account management, realtime messaging, contextual multilingual AI, customer intelligence, knowledge workflows, enterprise controls, a unified design system and complete repository documentation.
129 lines
8.0 KiB
TypeScript
129 lines
8.0 KiB
TypeScript
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { PageLoading, ErrorState } from '@/components/ui/feedback'
|
|
import { PageHeader } from '@/components/common/PageHeader'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Card, CardHeader } from '@/components/ui/card'
|
|
import { StatCard } from '@/components/common/StatCard'
|
|
import { LiveDataBadge } from '@/components/common/LiveDataBadge'
|
|
import { CountryFlag } from '@/components/common/CountryFlag'
|
|
import { ChartCard, TrendChart, DonutChart, FunnelChart, RankBars, SimpleBarChart } from '@/components/charts'
|
|
import { getAnalytics } from '@/services/api/org'
|
|
import { Activity, BadgePercent, Users, Zap, Target } from 'lucide-react'
|
|
|
|
const PERIOD_OPTIONS = [
|
|
{ value: 'today', label: '今日' },
|
|
{ value: '7d', label: '近 7 天' },
|
|
{ value: '30d', label: '近 30 天' },
|
|
{ value: 'quarter', label: '本季度' },
|
|
] as const
|
|
|
|
export function AnalyticsPage() {
|
|
const [period, setPeriod] = useState<(typeof PERIOD_OPTIONS)[number]['value']>('30d')
|
|
const { data, isLoading, isError, refetch } = useQuery({
|
|
queryKey: ['analytics', period],
|
|
queryFn: () => getAnalytics({ period }),
|
|
})
|
|
|
|
if (isLoading) return <PageLoading />
|
|
if (isError || !data) return <ErrorState onRetry={() => refetch()} />
|
|
|
|
const k = data.kpis
|
|
|
|
return (
|
|
<div className="page-shell">
|
|
<PageHeader
|
|
title="业务分析"
|
|
description="从客户获取、沟通效率到成交转化,查看完整经营链路"
|
|
actions={
|
|
<div className="flex items-center gap-2">
|
|
<LiveDataBadge />
|
|
<div className="flex rounded-[10px] border border-slate-200 bg-white p-1 shadow-sm">
|
|
{PERIOD_OPTIONS.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
aria-pressed={period === option.value}
|
|
onClick={() => setPeriod(option.value)}
|
|
className={`rounded-[7px] px-3 py-1.5 text-xs font-medium transition ${
|
|
period === option.value
|
|
? 'bg-slate-900 text-white shadow-sm'
|
|
: 'text-slate-500 hover:bg-slate-50 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-3 xl:grid-cols-5">
|
|
<StatCard title="新增客户" value={k.newCustomers.value} delta={k.newCustomers.delta} icon={Users} />
|
|
<StatCard title="有效询盘" value={k.validInquiries.value} delta={k.validInquiries.delta} icon={Activity} />
|
|
<StatCard title="高意向客户" value={k.highIntent.value} delta={k.highIntent.delta} icon={Target} />
|
|
<StatCard title="转化率" value={`${k.conversionRate.value}%`} delta={k.conversionRate.delta} icon={BadgePercent} />
|
|
<StatCard title="AI采纳率" value={`${k.aiAdoptionRate.value}%`} delta={k.aiAdoptionRate.delta} icon={Zap} />
|
|
</div>
|
|
|
|
<section className="page-section">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2 className="section-title">增长与转化</h2>
|
|
<p className="section-description">观察客户增长速度、渠道贡献和阶段流失</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-12">
|
|
<ChartCard className="xl:col-span-7" title="客户增长趋势" description="所选周期内的新增客户变化" height={290}>
|
|
<TrendChart data={data.customerGrowth} series={[{ key: 'count', name: '新增客户', color: '#2563eb' }]} />
|
|
</ChartCard>
|
|
<ChartCard className="xl:col-span-5" title="客户阶段转化漏斗" description="识别转化链路中的主要流失阶段" height={290}>
|
|
<FunnelChart data={data.funnel} />
|
|
</ChartCard>
|
|
<ChartCard className="xl:col-span-4" title="各渠道询盘数量" height={255}>
|
|
<SimpleBarChart data={data.channelInquiries} xKey="name" barKey="count" name="询盘数" />
|
|
</ChartCard>
|
|
<ChartCard className="xl:col-span-4" title="客户国家/地区分布" height={255}>
|
|
<DonutChart data={data.countryDistribution} />
|
|
</ChartCard>
|
|
<ChartCard className="xl:col-span-4" title="意向分数分布" height={255}>
|
|
<DonutChart data={data.intentDistribution} nameKey="range" />
|
|
</ChartCard>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="page-section">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2 className="section-title">产品与团队效率</h2>
|
|
<p className="section-description">比较产品热度、响应速度和团队转化结果</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
<ChartCard title="产品关注度排行" height={250}><RankBars data={data.productRank.map((p) => ({ name: p.product, value: p.inquiries }))} /></ChartCard>
|
|
<ChartCard title="团队回复效率" height={250}><RankBars data={data.teamReplyRank.map((r) => ({ name: r.name, value: r.avgResponseMin }))} unit="分钟" max={30} /></ChartCard>
|
|
<ChartCard title="团队客户转化" height={250}><RankBars data={data.teamConversionRank.map((r) => ({ name: r.name, value: r.rate }))} unit="%" /></ChartCard>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="page-section">
|
|
<div className="section-heading">
|
|
<div>
|
|
<h2 className="section-title">经营提醒</h2>
|
|
<p className="section-description">需要优先处理的客户、跟进和市场变化</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
<Card><CardHeader title="重点跟进客户" /><div className="divide-y divide-slate-100">{data.bossBoard.urgentHighValue.map((c) => (<div key={c.id} className="flex items-center justify-between px-5 py-3 text-sm"><span className="font-medium text-slate-900">{c.name} <CountryFlag code={c.country} withName={false} /></span><span className="max-w-[55%] truncate text-xs text-slate-500">{c.reason}</span></div>))}</div></Card>
|
|
<Card><CardHeader title="逾期跟进" /><div className="divide-y divide-slate-100">{data.bossBoard.overdueMembers.map((m) => (<div key={m.id} className="flex items-center justify-between px-5 py-3 text-sm"><span className="font-medium text-slate-900">{m.name}</span><Badge variant="danger">{m.overdueCount} 项逾期</Badge></div>))}</div></Card>
|
|
<Card><CardHeader title="增长较快的市场" /><div className="divide-y divide-slate-100">{data.bossBoard.fastestCountries.map((c) => (<div key={c.country} className="flex items-center justify-between px-5 py-3 text-sm"><CountryFlag code={c.country} /><Badge variant="success">+{(c.growth * 100).toFixed(0)}%</Badge></div>))}</div></Card>
|
|
<Card><CardHeader title="渠道转化" /><div className="px-5 py-6"><p className="text-2xl font-semibold tracking-tight text-slate-950">{data.bossBoard.bestChannel.name}</p><p className="mt-1 text-sm text-slate-500">当前转化率 {data.bossBoard.bestChannel.conversion}%</p></div></Card>
|
|
<Card><CardHeader title="产品询盘" /><div className="divide-y divide-slate-100 px-5">{data.bossBoard.topProducts.map((p) => (<div key={p.product} className="flex items-center justify-between py-3 text-sm"><span className="font-medium text-slate-900">{p.product}</span><span className="text-slate-500">{p.inquiries} 次</span></div>))}</div></Card>
|
|
<Card><CardHeader title="久未联系的高意向客户" /><div className="divide-y divide-slate-100">{data.bossBoard.churningHighIntent.map((c) => (<div key={c.id} className="px-5 py-3 text-sm"><span className="font-medium text-slate-900">{c.name} <CountryFlag code={c.country} withName={false} /></span><span className="ml-2 text-xs text-slate-500">{c.daysSilent} 天未联系 · 意向 {c.intentScore} 分</span></div>))}</div></Card>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|