包含功能: - 法务参谋/经营决策/生活助理/情绪中心四大AI对话模块 - 专家咨询系统(用户选定→专家确认→咨询→回复→评价) - 角色登录(用户/专家/管理员)+ ProtectedRoute 权限控制 - 管理后台(用户管理/专家管理/AI使用统计/专家活跃度) - 专家工作台(待确认服务/待回复咨询/已回复咨询/服务中用户) - 服务关系取消(用户和专家均可取消,需自己确认) - 全页面移动端响应式适配 - 知识库RAG检索 - 对话历史(普通/保密模式) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
162 lines
7.7 KiB
TypeScript
162 lines
7.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Shield, Activity, Briefcase } from 'lucide-react'
|
|
import {
|
|
PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, Tooltip,
|
|
ResponsiveContainer, CartesianGrid, LineChart, Line, Legend,
|
|
} from 'recharts'
|
|
import { PageLayout } from '@/components/common/PageLayout'
|
|
import { EmptyState } from '@/components/common/EmptyState'
|
|
import { LoadingState } from '@/components/common/LoadingState'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { api } from '@/lib/api'
|
|
import { CATEGORY_LABELS, STATUS_LABELS, PIE_COLORS } from './constants'
|
|
|
|
type ExpertActivity = Awaited<ReturnType<typeof api.adminExpertActivity>>
|
|
|
|
/** 管理后台 - 专家沟通情况:咨询统计、状态/分类分布、专家统计、趋势、用户排行(不含具体内容) */
|
|
export function AdminExpertActivity() {
|
|
const [data, setData] = useState<ExpertActivity | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
api.adminExpertActivity()
|
|
.then(setData)
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
if (loading || !data) {
|
|
return (
|
|
<PageLayout title="专家沟通情况" description="用户与专家的咨询统计(仅统计,不含具体咨询内容)">
|
|
<LoadingState />
|
|
</PageLayout>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PageLayout title="专家沟通情况" description="用户与专家的咨询统计(仅统计,不含具体咨询内容)">
|
|
<div className="space-y-6">
|
|
{/* 概览数字 */}
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Card><CardContent className="py-4 text-center">
|
|
<Activity className="mx-auto h-6 w-6 text-primary" />
|
|
<p className="mt-2 text-2xl font-bold">{data.total}</p>
|
|
<p className="text-sm text-muted-foreground">总咨询数</p>
|
|
</CardContent></Card>
|
|
<Card><CardContent className="py-4 text-center">
|
|
<Shield className="mx-auto h-6 w-6 text-emerald-500" />
|
|
<p className="mt-2 text-2xl font-bold">{data.replied}</p>
|
|
<p className="text-sm text-muted-foreground">已回复</p>
|
|
</CardContent></Card>
|
|
<Card><CardContent className="py-4 text-center">
|
|
<Shield className="mx-auto h-6 w-6 text-amber-500" />
|
|
<p className="mt-2 text-2xl font-bold">{data.pending}</p>
|
|
<p className="text-sm text-muted-foreground">待回复</p>
|
|
</CardContent></Card>
|
|
</div>
|
|
|
|
{/* 趋势图 */}
|
|
{data.daily_trend.length > 0 && (
|
|
<Card><CardContent className="p-4">
|
|
<p className="mb-3 text-sm font-medium">近 7 天咨询趋势</p>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<LineChart data={data.daily_trend.map(d => ({ date: d.date.slice(5), count: d.count }))}>
|
|
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
|
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
|
<YAxis tick={{ fontSize: 12 }} allowDecimals={false} />
|
|
<Tooltip />
|
|
<Line type="monotone" dataKey="count" stroke="#10b981" strokeWidth={2} name="咨询数" />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</CardContent></Card>
|
|
)}
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{/* 状态分布 */}
|
|
<Card><CardContent className="p-4">
|
|
<p className="mb-3 text-sm font-medium">按状态分布</p>
|
|
{data.by_status.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={220}>
|
|
<PieChart>
|
|
<Pie data={data.by_status.map(s => ({ name: STATUS_LABELS[s.status] || s.status, value: s.count }))} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={70} label>
|
|
{data.by_status.map((_, i) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
|
|
</Pie>
|
|
<Tooltip />
|
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
) : <EmptyState title="暂无数据" icon={<Activity className="h-8 w-8" />} />}
|
|
</CardContent></Card>
|
|
|
|
{/* 分类分布 */}
|
|
<Card><CardContent className="p-4">
|
|
<p className="mb-3 text-sm font-medium">按分类分布</p>
|
|
{data.by_category.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={220}>
|
|
<BarChart data={data.by_category.map(c => ({ name: CATEGORY_LABELS[c.category] || c.category, count: c.count }))}>
|
|
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
|
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
|
|
<YAxis tick={{ fontSize: 12 }} allowDecimals={false} />
|
|
<Tooltip />
|
|
<Bar dataKey="count" fill="#3b82f6" name="咨询数" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
) : <EmptyState title="暂无数据" icon={<Briefcase className="h-8 w-8" />} />}
|
|
</CardContent></Card>
|
|
</div>
|
|
|
|
{/* 专家咨询统计 */}
|
|
{data.by_expert.length > 0 && (
|
|
<Card><CardContent className="p-4">
|
|
<p className="mb-3 text-sm font-medium">专家咨询统计</p>
|
|
<div className="space-y-2">
|
|
{data.by_expert.filter(e => e.total > 0).map((e, i) => (
|
|
<div key={i} className="flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<span className="w-6 shrink-0 text-xs text-muted-foreground">{i + 1}</span>
|
|
<span className="font-medium">{e.name}</span>
|
|
<Badge variant="secondary" className="shrink-0 text-xs">{CATEGORY_LABELS[e.category] || e.category}</Badge>
|
|
{e.rating_count > 0 && (
|
|
<span className="text-xs text-amber-500">★ {e.avg_rating}</span>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
<span>总 {e.total}</span>
|
|
<span className="text-emerald-500">已回 {e.replied}</span>
|
|
<span className="text-amber-500">待回 {e.pending}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{data.by_expert.every(e => e.total === 0) && (
|
|
<EmptyState title="暂无咨询记录" icon={<Activity className="h-8 w-8" />} />
|
|
)}
|
|
</div>
|
|
</CardContent></Card>
|
|
)}
|
|
|
|
{/* 用户咨询排行 */}
|
|
{data.by_user.length > 0 && (
|
|
<Card><CardContent className="p-4">
|
|
<p className="mb-3 text-sm font-medium">用户咨询情况(脱敏,Top 50)</p>
|
|
<div className="space-y-2">
|
|
{data.by_user.map((u, i) => (
|
|
<div key={i} className="flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<span className="w-6 shrink-0 text-xs text-muted-foreground">{i + 1}</span>
|
|
<span className="font-medium">{u.phone_masked}</span>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
<span>咨询 {u.consultations}</span>
|
|
<span>{u.last_active ? u.last_active.slice(0, 10) : '-'}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent></Card>
|
|
)}
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|