import { useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { Briefcase, Loader2, Pencil, Power, PowerOff, Star, Trash2, Upload } from 'lucide-react' import { PageLayout } from '@/components/common/PageLayout' import { EmptyState } from '@/components/common/EmptyState' import { LoadingState } from '@/components/common/LoadingState' import { ExpertAvatar } from '@/components/common/ExpertAvatar' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { api } from '@/lib/api' import type { Expert } from '@/types' import { CATEGORY_LABELS } from './constants' /** 管理后台 - 专家管理:专家上下架、编辑信息、上传头像 */ export function AdminExperts() { const [experts, setExperts] = useState([]) const [loading, setLoading] = useState(true) const [toggling, setToggling] = useState(null) const [editing, setEditing] = useState(null) const loadExperts = async () => { try { const res = await api.adminListExperts() setExperts(res.results) } catch { /* ignore */ } finally { setLoading(false) } } useEffect(() => { loadExperts() }, []) const handleToggle = async (expert: Expert) => { setToggling(expert.id) try { const newStatus = expert.status === 'active' ? 'inactive' : 'active' await api.adminUpdateExpertStatus(expert.id, newStatus) toast.success(newStatus === 'active' ? '已上架' : '已下架') loadExperts() } catch { toast.error('操作失败') } finally { setToggling(null) } } if (loading) { return ( ) } return ( {experts.length === 0 ? ( ) : (
{experts.map((ex) => { const stars = (ex.rate / 100).toFixed(1) return ( {/* 头部:长方形头像(左) + 姓名/状态(右) */}

{ex.name}

{ex.status === 'active' ? 上架中 : 已下架}

{ex.title}

{stars} ({ex.rating_count}人评价)
{/* 简介 */} {ex.bio && (

{ex.bio}

)} {/* 专长标签 */} {ex.expertise && (
{ex.expertise.split(',').map((tag, i) => ( {tag.trim()} ))}
)}
{/* 底部:分类 + 操作按钮 */}
{CATEGORY_LABELS[ex.category] || ex.category}
) })}
)} {/* 编辑弹窗 */} {editing && ( setEditing(null)} onSuccess={() => { loadExperts(); setEditing(null) }} /> )}
) } /** 编辑专家信息弹窗:含头像上传 + 表单字段 */ function EditExpertDialog({ expert, onClose, onSuccess, }: { expert: Expert onClose: () => void onSuccess: () => void }) { const [name, setName] = useState(expert.name) const [category, setCategory] = useState(expert.category) const [title, setTitle] = useState(expert.title) const [bio, setBio] = useState(expert.bio) const [expertise, setExpertise] = useState(expert.expertise) const [avatarUrl, setAvatarUrl] = useState(expert.avatar_url) const [saving, setSaving] = useState(false) const [uploading, setUploading] = useState(false) const fileInputRef = useRef(null) const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setUploading(true) try { const res = await api.adminUploadExpertAvatar(expert.id, file) setAvatarUrl(res.avatar_url) toast.success('头像上传成功') } catch (err) { toast.error(err instanceof Error ? err.message : '上传失败') } finally { setUploading(false) // 清空 input 允许重复选同一文件 if (fileInputRef.current) fileInputRef.current.value = '' } } /** 删除头像:清空 avatarUrl,保存时传空字符串后端清除为 NULL */ const handleRemoveAvatar = async () => { setAvatarUrl('') toast.success('头像已删除,保存后生效') } const handleSave = async () => { if (!name.trim()) { toast.error('姓名不能为空') return } setSaving(true) try { await api.adminUpdateExpert(expert.id, { name: name.trim(), category, title: title.trim(), bio: bio.trim(), expertise: expertise.trim(), avatar_url: avatarUrl, }) toast.success('专家信息已更新') onSuccess() } catch (err) { toast.error(err instanceof Error ? err.message : '保存失败') } finally { setSaving(false) } } return ( !o && onClose()}> 编辑专家信息 修改专家资料、上传头像
{/* 头像上传区 */}

专家头像

支持 JPG / PNG / WebP,最大 5MB

{avatarUrl && ( )}
{/* 姓名 */}
setName(e.target.value)} placeholder="专家姓名" />
{/* 分类 */}
{/* 职称 */}
setTitle(e.target.value)} placeholder="如:高级律师" />
{/* 简介 */}