Files
BossAi/web2/src/pages/Experts.tsx
T
selfreleaseandDevin dd01d94ade
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled
feat: 老板台2.0 + 手机端适配 + 专家库 + 智囊入口
- 老板台: AI执行摘要卡 + 待办优先级排序 + 时间线视图 + 环形健康度
- 手机端: 事项看板折叠列表 + 7个页面响应式修复 + 底部安全边距
- 专家库: 新增用户端专家浏览页(参考管理端卡片风格)
- 智囊入口: 底部Tab加"智囊" + 4个智囊页场景切换栏 + "我"页分组入口
- FAB弹窗: 场景快速入口 + 指令卡片优化 + 输入区sticky
- 后端: /dashboard/summary + /dashboard/timeline API
- 后端: life场景事实维度扩充(2→5) + 17个生活关键词优先级
- 前端: 抑制React DevTools startTime噪音错误

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-31 14:11:09 +08:00

137 lines
5.6 KiB
TypeScript

/** 专家库:浏览所有专家,选择后跳转到对应智囊场景咨询
* 卡片样式参考管理端专家管理:长方形头像 + 姓名/评分 + 简介 + 专长标签
*/
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Users, Star, ChevronRight, Briefcase } from 'lucide-react'
import { PageLayout } from '@/components/common/PageLayout'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { LoadingState } from '@/components/common/LoadingState'
import { ErrorState } from '@/components/common/ErrorState'
import { EmptyState } from '@/components/common/EmptyState'
import { ExpertAvatar } from '@/components/common/ExpertAvatar'
import { api } from '@/lib/api'
import { SCENE_LABELS, cn } from '@/lib/utils'
import type { Expert, Scene } from '@/types'
const CATEGORY_FILTERS: { value: Scene | 'all'; label: string }[] = [
{ value: 'all', label: '全部' },
{ value: 'legal', label: '法务' },
{ value: 'decision', label: '决策' },
{ value: 'life', label: '生活' },
{ value: 'emotion', label: '情绪' },
]
export function Experts() {
const navigate = useNavigate()
const [experts, setExperts] = useState<Expert[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [filter, setFilter] = useState<Scene | 'all'>('all')
const load = () => {
setLoading(true); setError('')
api.listExperts(filter === 'all' ? undefined : filter)
.then((r) => { setExperts(r.results); setLoading(false) })
.catch((e) => { setError(e.message); setLoading(false) })
}
useEffect(load, [filter])
if (loading) return <LoadingState label="加载专家库..." />
if (error) return <ErrorState message={error} onRetry={load} />
return (
<PageLayout title="专家库" description={`共 ${experts.length} 位专家可咨询`}>
{/* 分类筛选 */}
<div className="flex items-center gap-2 overflow-x-auto pb-1">
{CATEGORY_FILTERS.map((f) => (
<button
key={f.value}
onClick={() => setFilter(f.value)}
className={cn(
'shrink-0 rounded-full border px-3 py-1.5 text-sm transition-colors',
filter === f.value
? 'border-primary bg-primary/10 text-primary font-medium'
: 'border-border bg-card text-muted-foreground hover:bg-accent',
)}
>
{f.label}
</button>
))}
</div>
{experts.length === 0 ? (
<EmptyState
title="暂无专家"
description="该分类下还没有可用专家"
icon={Users}
/>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{experts.map((ex) => {
const stars = (ex.rate / 100).toFixed(1)
return (
<Card key={ex.id} className="transition-shadow hover:shadow-md">
<CardContent className="p-4 sm:p-5">
{/* 头部:长方形头像(左) + 姓名/评分(右) */}
<div className="flex items-start gap-4">
<ExpertAvatar
name={ex.name}
avatarUrl={ex.avatar_url}
size="xl"
shape="rect"
className="shrink-0"
/>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="truncate font-medium text-foreground">{ex.name}</h3>
</div>
<p className="mt-0.5 truncate text-sm text-muted-foreground">{ex.title}</p>
{ex.rating_count > 0 && (
<div className="mt-1 flex items-center gap-1 text-xs">
<Star className="h-3 w-3 fill-amber-400 text-amber-400" />
<span className="font-medium">{stars}</span>
<span className="text-muted-foreground">({ex.rating_count}人评价)</span>
</div>
)}
{/* 简介 */}
{ex.bio && (
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{ex.bio}</p>
)}
{/* 专长标签 */}
{ex.expertise && (
<div className="mt-2 flex flex-wrap gap-1">
{ex.expertise.split(',').map((tag, i) => (
<Badge key={i} variant="outline" className="text-xs">{tag.trim()}</Badge>
))}
</div>
)}
</div>
</div>
{/* 底部:分类 + 咨询按钮 */}
<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
<Badge variant="secondary">
{SCENE_LABELS[ex.category] || ex.category}
</Badge>
<Button
size="sm"
onClick={() => navigate(`/${ex.category}`, { state: { expertId: ex.id } })}
>
<Briefcase className="mr-1 h-3.5 w-3.5" /> 咨询
<ChevronRight className="ml-0.5 h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
</PageLayout>
)
}