feat(users): integrate user info and services APIs into 3-tab detail panel with proper padding
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { CheckCircle2, Shield, XCircle } from 'lucide-react'
|
||||
import { fetchUserDetail } from '@/api/modules/users'
|
||||
import {
|
||||
Award,
|
||||
Briefcase,
|
||||
CheckCircle2,
|
||||
FileText,
|
||||
GraduationCap,
|
||||
MapPin,
|
||||
Shield,
|
||||
Star,
|
||||
Tag,
|
||||
User,
|
||||
Wrench,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { fetchUserDetail, fetchUserInfo, fetchUserServices } from '@/api/modules/users'
|
||||
import { fetchExcProvider } from '@/api/modules/exc'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,6 +25,7 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
interface UserDetailSheetProps {
|
||||
uid: string | null
|
||||
@@ -19,17 +34,67 @@ interface UserDetailSheetProps {
|
||||
}
|
||||
|
||||
export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheetProps) {
|
||||
const query = useQuery({
|
||||
// 1. Basic user account query (GET /api/admin/v1/users/{uid})
|
||||
const userQuery = useQuery({
|
||||
queryKey: ['users', uid],
|
||||
queryFn: () => (uid ? fetchUserDetail(uid) : null),
|
||||
enabled: uid !== null,
|
||||
})
|
||||
|
||||
const user = query.data?.data
|
||||
// 2. Full user profile, careers, educations, honors, addresses (GET /api/admin/v1/users/{uid}/info)
|
||||
const userInfoQuery = useQuery({
|
||||
queryKey: ['user-info', uid],
|
||||
queryFn: async () => {
|
||||
if (!uid) return null
|
||||
try {
|
||||
const res = await fetchUserInfo(uid)
|
||||
return res.data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
enabled: uid !== null,
|
||||
})
|
||||
|
||||
// 3. User provided services (GET /api/admin/v1/users/{uid}/services)
|
||||
const userServicesQuery = useQuery({
|
||||
queryKey: ['user-services', uid],
|
||||
queryFn: async () => {
|
||||
if (!uid) return null
|
||||
try {
|
||||
const res = await fetchUserServices(uid)
|
||||
return res.data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
enabled: uid !== null,
|
||||
})
|
||||
|
||||
// 4. Provider profile query (GET /api/admin/v1/exc/providers/{uid})
|
||||
const providerQuery = useQuery({
|
||||
queryKey: ['exc-provider', uid],
|
||||
queryFn: async () => {
|
||||
if (!uid) return null
|
||||
try {
|
||||
const res = await fetchExcProvider(uid)
|
||||
return res.data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
enabled: uid !== null,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const user = userQuery.data?.data
|
||||
const userInfo = userInfoQuery.data
|
||||
const userServices = userServicesQuery.data?.expertises ?? []
|
||||
const provider = providerQuery.data
|
||||
|
||||
return (
|
||||
<Sheet open={uid !== null} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent className="sm:max-w-md overflow-y-auto">
|
||||
<SheetContent className="sm:max-w-xl p-6 overflow-y-auto flex flex-col">
|
||||
<SheetHeader className="border-b pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="size-12">
|
||||
@@ -39,8 +104,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<SheetTitle className="truncate text-base font-semibold">
|
||||
{user?.display_name ?? user?.username ?? '用户详情'}
|
||||
<SheetTitle className="truncate text-base font-semibold flex items-center gap-2">
|
||||
<span>{userInfo?.profile?.computed_fullname ?? user?.display_name ?? user?.username ?? '用户详情'}</span>
|
||||
{userInfo?.profile?.gender && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{userInfo.profile.gender}
|
||||
</Badge>
|
||||
)}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="truncate text-xs font-mono">
|
||||
{user?.uid}
|
||||
@@ -49,170 +119,537 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
{query.isPending ? (
|
||||
{userQuery.isPending ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">加载中…</div>
|
||||
) : !user ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">未找到用户信息</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6 py-4">
|
||||
{/* Status overview */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={user.status ? 'default' : 'destructive'}>
|
||||
{user.status ? '正常启用' : '已禁用'}
|
||||
</Badge>
|
||||
{user.is_robot ? (
|
||||
<Badge variant="secondary">机器人账号</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">真人账号</Badge>
|
||||
)}
|
||||
{user.sid_display && (
|
||||
<Badge variant="outline" className="font-mono">
|
||||
SID: {user.sid_display}
|
||||
<Tabs defaultValue="account" className="mt-2 flex-1 flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="account" className="gap-1.5 text-xs">
|
||||
<User className="size-3.5" />
|
||||
账号基础
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="info" className="gap-1.5 text-xs">
|
||||
<FileText className="size-3.5" />
|
||||
个人资料
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="services" className="gap-1.5 text-xs">
|
||||
<Wrench className="size-3.5" />
|
||||
服务事项 ({userServices.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* TAB 1: ACCOUNT BASIC */}
|
||||
<TabsContent value="account" className="flex flex-col gap-6 py-4 outline-none">
|
||||
{/* Status overview */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={user.status ? 'default' : 'destructive'}>
|
||||
{user.status ? '正常启用' : '已禁用'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account Info */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
账号基本信息
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">用户名</div>
|
||||
<div className="font-medium">{user.username ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">显示名称</div>
|
||||
<div className="font-medium">{user.display_name ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">SID</div>
|
||||
<div className="font-mono text-xs">{user.sid ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">专业领域</div>
|
||||
<div className="font-medium">{user.expertise ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact & Verification */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
联系方式与验证
|
||||
</h4>
|
||||
<div className="grid gap-2 rounded-lg border p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">邮箱</div>
|
||||
<div className="font-medium">{user.email ?? '—'}</div>
|
||||
</div>
|
||||
{user.email && (
|
||||
<Badge variant={user.email_verified_at ? 'secondary' : 'outline'} className="gap-1">
|
||||
{user.email_verified_at ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 text-emerald-500" />
|
||||
已验证
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="size-3 text-amber-500" />
|
||||
未验证
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t pt-2">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">手机号</div>
|
||||
<div className="font-medium">{user.phone ?? '—'}</div>
|
||||
</div>
|
||||
{user.phone && (
|
||||
<Badge variant={user.phone_verified_at ? 'secondary' : 'outline'} className="gap-1">
|
||||
{user.phone_verified_at ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 text-emerald-500" />
|
||||
已验证
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="size-3 text-amber-500" />
|
||||
未验证
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Locale & Region */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
地区与时区
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">国家/地区</div>
|
||||
<div className="font-medium">{user.country_code ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">城市</div>
|
||||
<div className="font-medium">{user.city ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">时区</div>
|
||||
<div className="font-medium">{user.timezone ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">语言偏好</div>
|
||||
<div className="font-medium">{user.locale ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roles */}
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
已分配角色
|
||||
</h4>
|
||||
{onAssignRoles && (
|
||||
<Button variant="ghost" size="sm" onClick={onAssignRoles} className="h-6 text-xs gap-1">
|
||||
<Shield className="size-3" />
|
||||
分配角色
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 rounded-lg border p-3">
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
user.roles.map((role) => (
|
||||
<Badge key={role} variant="secondary">
|
||||
{role}
|
||||
</Badge>
|
||||
))
|
||||
{user.is_robot ? (
|
||||
<Badge variant="secondary">机器人账号</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">未分配任何角色</span>
|
||||
<Badge variant="outline">真人账号</Badge>
|
||||
)}
|
||||
{user.sid_display && (
|
||||
<Badge variant="outline" className="font-mono">
|
||||
SID: {user.sid_display}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span>注册时间:</span>
|
||||
<span>{new Date(user.created_at).toLocaleString('zh-CN')}</span>
|
||||
{/* Account Basic Info */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
账号基础属性
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">用户名</div>
|
||||
<div className="font-medium">{user.username ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">显示名称</div>
|
||||
<div className="font-medium">{user.display_name ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">SID</div>
|
||||
<div className="font-mono text-xs">{user.sid ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">专业领域</div>
|
||||
<div className="font-medium">{user.expertise ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user.updated_at && (
|
||||
|
||||
{/* Contact & Verification */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
联系方式与验证
|
||||
</h4>
|
||||
<div className="grid gap-2 rounded-lg border p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">邮箱</div>
|
||||
<div className="font-medium">{user.email ?? '—'}</div>
|
||||
</div>
|
||||
{user.email && (
|
||||
<Badge variant={user.email_verified_at ? 'secondary' : 'outline'} className="gap-1">
|
||||
{user.email_verified_at ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 text-emerald-500" />
|
||||
已验证
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="size-3 text-amber-500" />
|
||||
未验证
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t pt-2">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">手机号</div>
|
||||
<div className="font-medium">{user.phone ?? '—'}</div>
|
||||
</div>
|
||||
{user.phone && (
|
||||
<Badge variant={user.phone_verified_at ? 'secondary' : 'outline'} className="gap-1">
|
||||
{user.phone_verified_at ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 text-emerald-500" />
|
||||
已验证
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="size-3 text-amber-500" />
|
||||
未验证
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roles */}
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
已分配角色
|
||||
</h4>
|
||||
{onAssignRoles && (
|
||||
<Button variant="ghost" size="sm" onClick={onAssignRoles} className="h-6 text-xs gap-1">
|
||||
<Shield className="size-3" />
|
||||
分配角色
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 rounded-lg border p-3">
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
user.roles.map((role) => (
|
||||
<Badge key={role} variant="secondary">
|
||||
{role}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">未分配任何角色</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span>更新时间:</span>
|
||||
<span>{new Date(user.updated_at).toLocaleString('zh-CN')}</span>
|
||||
<span>注册时间:</span>
|
||||
<span>{new Date(user.created_at).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
{user.updated_at && (
|
||||
<div>
|
||||
<span>更新时间:</span>
|
||||
<span>{new Date(user.updated_at).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* TAB 2: DETAILED INFO */}
|
||||
<TabsContent value="info" className="flex flex-col gap-6 py-4 outline-none">
|
||||
{userInfoQuery.isPending ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">加载个人资料中…</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Profile Headline & Bio */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
个人名片与简介
|
||||
</h4>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-xs space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-sm text-foreground">
|
||||
{userInfo?.profile?.computed_fullname ?? user.display_name ?? '—'}
|
||||
</span>
|
||||
{userInfo?.profile?.gender && (
|
||||
<Badge variant="outline">{userInfo.profile.gender}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{userInfo?.profile?.headline && (
|
||||
<div className="font-medium text-foreground">{userInfo.profile.headline}</div>
|
||||
)}
|
||||
{userInfo?.profile?.bio ? (
|
||||
<div className="text-muted-foreground leading-relaxed">{userInfo.profile.bio}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground italic">暂无个人简介</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Region & Timezone */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
地区与语言偏好
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">国家/地区</div>
|
||||
<div className="font-medium">{user.country_code ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">城市</div>
|
||||
<div className="font-medium">{user.city ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">时区</div>
|
||||
<div className="font-medium">{user.timezone ?? '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">语言偏好</div>
|
||||
<div className="font-medium">{user.locale ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Careers / Work Experience */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<Briefcase className="size-3.5" />
|
||||
工作经历 ({(userInfo?.careers ?? []).length})
|
||||
</h4>
|
||||
{userInfo?.careers && userInfo.careers.length > 0 ? (
|
||||
<div className="space-y-2 rounded-lg border p-3 text-xs">
|
||||
{userInfo.careers.map((career, idx) => (
|
||||
<div key={idx} className="border-b pb-2 last:border-b-0 last:pb-0">
|
||||
<div className="flex items-center justify-between font-medium text-sm">
|
||||
<span>{career.company_name ?? '未知公司'}</span>
|
||||
{career.is_current && <Badge variant="secondary" className="text-[10px]">在职</Badge>}
|
||||
</div>
|
||||
<div className="text-muted-foreground font-medium mt-0.5">
|
||||
{career.position} {career.department ? `· ${career.department}` : ''}
|
||||
</div>
|
||||
{(career.start_date || career.end_date) && (
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{career.start_date ?? '—'} ~ {career.is_current ? '至今' : career.end_date ?? '—'}
|
||||
</div>
|
||||
)}
|
||||
{career.description && (
|
||||
<div className="text-muted-foreground mt-1 text-[11px] leading-relaxed">
|
||||
{career.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
暂无工作经历记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Education */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<GraduationCap className="size-3.5" />
|
||||
教育经历 ({(userInfo?.educations ?? []).length})
|
||||
</h4>
|
||||
{userInfo?.educations && userInfo.educations.length > 0 ? (
|
||||
<div className="space-y-2 rounded-lg border p-3 text-xs">
|
||||
{userInfo.educations.map((edu, idx) => (
|
||||
<div key={idx} className="border-b pb-2 last:border-b-0 last:pb-0">
|
||||
<div className="flex items-center justify-between font-medium text-sm">
|
||||
<span>{edu.school_name ?? '学校'}</span>
|
||||
{edu.degree && <Badge variant="outline" className="text-[10px]">{edu.degree}</Badge>}
|
||||
</div>
|
||||
{edu.major && <div className="text-muted-foreground mt-0.5">{edu.major}</div>}
|
||||
{(edu.start_date || edu.end_date) && (
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{edu.start_date ?? '—'} ~ {edu.is_current ? '至今' : edu.end_date ?? '—'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
暂无教育经历记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Honors */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<Award className="size-3.5" />
|
||||
荣誉与奖项 ({(userInfo?.honors ?? []).length})
|
||||
</h4>
|
||||
{userInfo?.honors && userInfo.honors.length > 0 ? (
|
||||
<div className="space-y-2 rounded-lg border p-3 text-xs">
|
||||
{userInfo.honors.map((honor, idx) => (
|
||||
<div key={idx} className="border-b pb-2 last:border-b-0 last:pb-0">
|
||||
<div className="flex items-center justify-between font-medium">
|
||||
<span>{honor.title ?? '荣誉奖项'}</span>
|
||||
{honor.date && <span className="text-[11px] text-muted-foreground">{honor.date}</span>}
|
||||
</div>
|
||||
{honor.issuer && <div className="text-muted-foreground text-[11px] mt-0.5">颁发单位:{honor.issuer}</div>}
|
||||
{honor.description && <div className="text-muted-foreground text-[11px] mt-0.5">{honor.description}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
暂无荣誉与奖项记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Addresses */}
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<MapPin className="size-3.5" />
|
||||
常用服务地址 ({(userInfo?.addresses ?? []).length})
|
||||
</h4>
|
||||
{userInfo?.addresses && userInfo.addresses.length > 0 ? (
|
||||
<div className="space-y-2 rounded-lg border p-3 text-xs">
|
||||
{userInfo.addresses.map((addr, idx) => (
|
||||
<div key={idx} className="border-b pb-2 last:border-b-0 last:pb-0 space-y-0.5">
|
||||
<div className="flex items-center justify-between font-medium">
|
||||
<span>{addr.contact_name ?? '联系人'}</span>
|
||||
{addr.contact_phone_number && (
|
||||
<span className="font-mono text-muted-foreground">{addr.contact_phone_number}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-[11px]">
|
||||
{[addr.province, addr.city, addr.district, addr.address_line]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
暂无保存的服务地址
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* TAB 3: SERVICES */}
|
||||
<TabsContent value="services" className="flex flex-col gap-6 py-4 outline-none">
|
||||
{userServicesQuery.isPending ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">加载服务事项中…</div>
|
||||
) : userServices.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
专长项目与服务变体 ({userServices.length})
|
||||
</h4>
|
||||
{userServices.map((exp) => (
|
||||
<div key={exp.id ?? exp.name} className="rounded-lg border p-3 shadow-xs space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h5 className="font-semibold text-sm flex items-center gap-1.5">
|
||||
<Wrench className="size-4 text-primary" />
|
||||
{exp.name}
|
||||
</h5>
|
||||
{exp.tags && exp.tags.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{exp.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-[10px] gap-0.5">
|
||||
<Tag className="size-2.5" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{exp.description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{exp.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service Variants */}
|
||||
{exp.services && exp.services.length > 0 && (
|
||||
<div className="space-y-2 border-t pt-2">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">包含的服务变体</div>
|
||||
<div className="grid gap-2">
|
||||
{exp.services.map((svc) => (
|
||||
<div
|
||||
key={svc.id ?? svc.service_type}
|
||||
className="flex items-center justify-between rounded-md bg-muted/40 p-2.5 text-xs"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Badge variant="outline" className="text-[10px] uppercase">
|
||||
{svc.service_type === 'onsite'
|
||||
? '上门服务'
|
||||
: svc.service_type === 'online'
|
||||
? '在线服务'
|
||||
: svc.service_type === 'remote'
|
||||
? '远程协助'
|
||||
: svc.service_type ?? '服务'}
|
||||
</Badge>
|
||||
<span>
|
||||
{svc.charge_type === 'time'
|
||||
? '按时计费'
|
||||
: svc.charge_type === 'fixed'
|
||||
? '固定价格'
|
||||
: svc.charge_type === 'quote'
|
||||
? '按需报价'
|
||||
: svc.charge_type}
|
||||
</span>
|
||||
</div>
|
||||
{svc.duration && (
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
预估服务时长: {svc.duration} 分钟
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-sm text-foreground">
|
||||
{svc.price != null
|
||||
? `${svc.currency_code ?? '¥'} ${(svc.price / 100).toFixed(2)}`
|
||||
: '按需报价'}
|
||||
</div>
|
||||
<Badge
|
||||
variant={svc.enabled ? 'secondary' : 'outline'}
|
||||
className="text-[10px] mt-0.5"
|
||||
>
|
||||
{svc.enabled ? '已上架' : '已下架'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : provider ? (
|
||||
/* Fallback to provider overview if userServices is empty */
|
||||
<div className="grid gap-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
专长服务商资格
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">在线状态</div>
|
||||
<div className="mt-0.5 flex items-center gap-1.5 font-medium">
|
||||
<span
|
||||
className={`size-2 rounded-full ${
|
||||
provider.status === 'online'
|
||||
? 'bg-emerald-500'
|
||||
: provider.status === 'busy'
|
||||
? 'bg-amber-500'
|
||||
: 'bg-muted-foreground'
|
||||
}`}
|
||||
/>
|
||||
<span>
|
||||
{provider.status === 'online'
|
||||
? '在线'
|
||||
: provider.status === 'busy'
|
||||
? '忙碌'
|
||||
: provider.status === 'offline'
|
||||
? '离线'
|
||||
: provider.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">实名认证</div>
|
||||
<div className="mt-0.5">
|
||||
<Badge variant={provider.is_verified ? 'secondary' : 'outline'}>
|
||||
{provider.is_verified ? '已认证' : '未认证'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">评分</div>
|
||||
<div className="mt-0.5 flex items-center gap-1 font-medium">
|
||||
<Star className="size-3.5 fill-amber-400 text-amber-400" />
|
||||
<span>{provider.rating ? provider.rating.toFixed(1) : '暂无评分'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">已完结订单</div>
|
||||
<div className="mt-0.5 font-medium">{provider.completed_jobs ?? 0} 单</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
|
||||
<Wrench className="mx-auto mb-2 size-8 text-muted-foreground/50" />
|
||||
<p className="font-medium text-foreground">暂无服务事项</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
该用户尚未创建或发布任何专长项目与服务变体。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Profile Settings if available */}
|
||||
{provider && (
|
||||
<div className="grid gap-3 pt-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
专长服务商抢单与派单参数
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">自动抢单模式</div>
|
||||
<div className="font-medium">
|
||||
{provider.dispatch_setting?.auto_grab_enabled ? '已开启' : '已关闭'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">今日接单量</div>
|
||||
<div className="font-medium">
|
||||
{provider.dispatch_setting?.accepted_today ?? 0} /{' '}
|
||||
{provider.dispatch_setting?.daily_accept_cap ?? '无限制'}
|
||||
</div>
|
||||
</div>
|
||||
{provider.dispatch_setting?.min_price != null && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">最低接单起价</div>
|
||||
<div className="font-medium">¥{provider.dispatch_setting.min_price}</div>
|
||||
</div>
|
||||
)}
|
||||
{provider.dispatch_setting?.max_radius_meters != null && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">服务接单半径</div>
|
||||
<div className="font-medium">
|
||||
{(provider.dispatch_setting.max_radius_meters / 1000).toFixed(1)} km
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
Reference in New Issue
Block a user