feat: 迁移专家工作台和管理后台到web2
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

专家工作台(4页面):
- WorkbenchPendingServices: 待确认服务
- WorkbenchPending: 待回复咨询
- WorkbenchReplied: 已回复咨询
- WorkbenchUsers: 服务中用户
- shared.tsx: 共享hook/弹窗/外壳

管理后台(5页面):
- AdminStats: 平台统计
- AdminAiUsage: AI使用情况(含图表)
- AdminExpertActivity: 专家沟通情况(含图表)
- AdminUsers: 用户管理(搜索/重置密码/停用)
- AdminExperts: 专家管理(上下架/编辑/头像)
- constants.ts: 标签映射和图表配色

路由: workbench/* 和 admin/* 接入实际页面
适配: EmptyState icon 从JSX元素改为ComponentType
修正: 管理端场景标签"法务参谋"→"私人法务"

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 15:46:01 +08:00
co-authored by Devin
parent 5272ed5339
commit 9310ba141d
15 changed files with 1577 additions and 5 deletions
+110
View File
@@ -0,0 +1,110 @@
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import { Briefcase, Loader2, XCircle } from 'lucide-react'
import { EmptyState } from '@/components/common/EmptyState'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog'
import type { Consultation, ExpertService } from '@/types'
import { api } from '@/lib/api'
import { useWorkbenchData, WorkbenchShell, maskPhone } from './shared'
/** 专家工作台 - 服务中用户:已确认服务关系的用户列表,可取消服务 */
export function WorkbenchUsers() {
const { expert, services, consultations, loading, error, loadData } = useWorkbenchData()
const [cancelService, setCancelService] = useState<ExpertService | null>(null)
const [cancelling, setCancelling] = useState(false)
const activeServices = services.filter(s => s.status === 'active')
/** 按服务用户分组咨询 */
const consultationsByUser = useMemo(() => {
const map = new Map<string, Consultation[]>()
for (const c of consultations) {
const phone = c.user_phone || 'unknown'
if (!map.has(phone)) {
map.set(phone, [])
}
map.get(phone)!.push(c)
}
return map
}, [consultations])
/** 确认取消服务关系 */
const handleCancel = async () => {
if (!cancelService) return
setCancelling(true)
try {
await api.cancelExpertService(cancelService.service_id)
toast.success('服务关系已取消')
setCancelService(null)
loadData()
} catch (e) {
toast.error(e instanceof Error ? e.message : '取消失败')
} finally {
setCancelling(false)
}
}
return (
<WorkbenchShell expert={expert} loading={loading} error={error} title="服务中用户" description="已确认服务关系的用户">
{activeServices.length === 0 ? (
<EmptyState title="暂无服务中用户" description="确认接单的用户会显示在这里" icon={Briefcase} />
) : (
<div className="space-y-2">
{activeServices.map((s) => {
const userConsultations = consultationsByUser.get(s.user_phone || '') || []
return (
<Card key={s.service_id}>
<CardContent className="p-4">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="text-sm font-medium">用户:{maskPhone(s.user_phone || '')}</p>
<p className="text-xs text-muted-foreground">
签约时间:{s.signed_at}
{userConsultations.length > 0 && ` · ${userConsultations.length} 条咨询`}
</p>
</div>
<Badge className="shrink-0">服务中</Badge>
</div>
<div className="mt-3 border-t pt-3">
<Button
size="sm"
variant="outline"
className="text-rose-600 hover:bg-rose-50"
onClick={() => setCancelService(s)}
>
<XCircle className="h-3.5 w-3.5" /> 取消服务
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
{/* 取消服务确认弹窗 */}
<Dialog open={!!cancelService} onOpenChange={(o) => !o && setCancelService(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>取消服务关系</DialogTitle>
<DialogDescription>
确认取消与用户「{cancelService ? maskPhone(cancelService.user_phone || '') : ''}」的服务关系?取消后不可恢复。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setCancelService(null)}>再想想</Button>
<Button variant="destructive" onClick={handleCancel} disabled={cancelling}>
{cancelling ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <XCircle className="h-3.5 w-3.5" />}
确认取消
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</WorkbenchShell>
)
}