专家工作台(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>
111 lines
4.4 KiB
TypeScript
111 lines
4.4 KiB
TypeScript
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>
|
|
)
|
|
}
|