feat: 专家库显示服务联系状态,未联系需先建立才能咨询
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

- 后端 /experts 返回 connected 字段(基于 expert_consultations 表)
- 前端卡片显示"已联系"徽章
- 未联系时按钮为"建立联系",调用 selectExpert API
- 已联系后按钮变为"咨询",可跳转对应场景

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-31 14:59:18 +08:00
co-authored by Devin
parent c7f539f41a
commit f9d2d0687e
3 changed files with 70 additions and 12 deletions
+43 -9
View File
@@ -1,9 +1,10 @@
/** 专家库:浏览所有专家,选择后跳转到对应智囊场景咨询
* 卡片样式参考管理端专家管理:长方形头像 + 姓名/评分 + 简介 + 专长标签
* 已建立服务联系的可直接咨询,未建立的需先建立联系
*/
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Users, Star, ChevronRight, Briefcase } from 'lucide-react'
import { Users, Star, ChevronRight, Briefcase, Link2, CheckCircle2 } from 'lucide-react'
import { PageLayout } from '@/components/common/PageLayout'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
@@ -30,6 +31,7 @@ export function Experts() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [filter, setFilter] = useState<Scene | 'all'>('all')
const [connecting, setConnecting] = useState<number | null>(null)
const load = () => {
setLoading(true); setError('')
@@ -40,6 +42,20 @@ export function Experts() {
useEffect(load, [filter])
/** 建立服务联系 */
const handleConnect = async (ex: Expert) => {
setConnecting(ex.id)
try {
await api.selectExpert(ex.id)
// 更新本地状态
setExperts((prev) => prev.map((e) => e.id === ex.id ? { ...e, connected: true } : e))
} catch (e: any) {
setError(e.message || '建立联系失败')
} finally {
setConnecting(null)
}
}
if (loading) return <LoadingState label="加载专家库..." />
if (error) return <ErrorState message={error} onRetry={load} />
@@ -73,6 +89,7 @@ export function Experts() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{experts.map((ex) => {
const stars = (ex.rate / 100).toFixed(1)
const isConnected = !!ex.connected
return (
<Card key={ex.id} className="transition-shadow hover:shadow-md">
<CardContent className="p-4 sm:p-5">
@@ -88,6 +105,11 @@ export function Experts() {
<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>
{isConnected && (
<Badge variant="outline" className="border-emerald-300 text-emerald-600 text-xs">
<CheckCircle2 className="mr-0.5 h-3 w-3" />已联系
</Badge>
)}
</div>
<p className="mt-0.5 truncate text-sm text-muted-foreground">{ex.title}</p>
{ex.rating_count > 0 && (
@@ -112,18 +134,30 @@ export function Experts() {
</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>
{isConnected ? (
<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>
) : (
<Button
size="sm"
variant="outline"
disabled={connecting === ex.id}
onClick={() => handleConnect(ex)}
>
<Link2 className="mr-1 h-3.5 w-3.5" />
{connecting === ex.id ? '建立中...' : '建立联系'}
</Button>
)}
</div>
</CardContent>
</Card>