feat: 迁移专家工作台和管理后台到web2
专家工作台(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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user