fix(users): surface composed profile errors
This commit is contained in:
@@ -7,14 +7,12 @@ import {
|
|||||||
GraduationCap,
|
GraduationCap,
|
||||||
MapPin,
|
MapPin,
|
||||||
Shield,
|
Shield,
|
||||||
Star,
|
|
||||||
Tag,
|
Tag,
|
||||||
User,
|
User,
|
||||||
Wrench,
|
Wrench,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { fetchUserDetail, fetchUserInfo, fetchUserServices } from '@/api/modules/users'
|
import { fetchUserDetail, fetchUserInfo, fetchUserServices } from '@/api/modules/users'
|
||||||
import { fetchExcProvider } from '@/api/modules/exc'
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -44,53 +42,20 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
// 2. Full user profile, careers, educations, honors, addresses (GET /api/admin/v1/users/{uid}/info)
|
// 2. Full user profile, careers, educations, honors, addresses (GET /api/admin/v1/users/{uid}/info)
|
||||||
const userInfoQuery = useQuery({
|
const userInfoQuery = useQuery({
|
||||||
queryKey: ['user-info', uid],
|
queryKey: ['user-info', uid],
|
||||||
queryFn: async () => {
|
queryFn: async () => (uid ? (await fetchUserInfo(uid)).data : null),
|
||||||
if (!uid) return null
|
|
||||||
try {
|
|
||||||
const res = await fetchUserInfo(uid)
|
|
||||||
return res.data
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled: uid !== null,
|
enabled: uid !== null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. User provided services (GET /api/admin/v1/users/{uid}/services)
|
// 3. User provided services (GET /api/admin/v1/users/{uid}/services)
|
||||||
const userServicesQuery = useQuery({
|
const userServicesQuery = useQuery({
|
||||||
queryKey: ['user-services', uid],
|
queryKey: ['user-services', uid],
|
||||||
queryFn: async () => {
|
queryFn: async () => (uid ? (await fetchUserServices(uid)).data : null),
|
||||||
if (!uid) return null
|
|
||||||
try {
|
|
||||||
const res = await fetchUserServices(uid)
|
|
||||||
return res.data
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled: uid !== 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 user = userQuery.data?.data
|
||||||
const userInfo = userInfoQuery.data
|
const userInfo = userInfoQuery.data
|
||||||
const userServices = userServicesQuery.data?.expertises ?? []
|
const userServices = userServicesQuery.data?.expertises ?? []
|
||||||
const provider = providerQuery.data
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={uid !== null} onOpenChange={(open) => !open && onClose()}>
|
<Sheet open={uid !== null} onOpenChange={(open) => !open && onClose()}>
|
||||||
@@ -121,6 +86,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
|
|
||||||
{userQuery.isPending ? (
|
{userQuery.isPending ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">加载中…</div>
|
<div className="py-8 text-center text-sm text-muted-foreground">加载中…</div>
|
||||||
|
) : userQuery.isError ? (
|
||||||
|
<div className="py-8 text-center text-sm text-destructive">
|
||||||
|
<p>用户账号加载失败。</p>
|
||||||
|
<Button className="mt-3" variant="outline" size="sm" onClick={() => void userQuery.refetch()}>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : !user ? (
|
) : !user ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">未找到用户信息</div>
|
<div className="py-8 text-center text-sm text-muted-foreground">未找到用户信息</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -280,6 +252,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
<TabsContent value="info" className="flex flex-col gap-6 py-4 outline-none">
|
<TabsContent value="info" className="flex flex-col gap-6 py-4 outline-none">
|
||||||
{userInfoQuery.isPending ? (
|
{userInfoQuery.isPending ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">加载个人资料中…</div>
|
<div className="py-8 text-center text-sm text-muted-foreground">加载个人资料中…</div>
|
||||||
|
) : userInfoQuery.isError ? (
|
||||||
|
<div className="py-8 text-center text-sm text-destructive">
|
||||||
|
<p>个人资料加载失败。</p>
|
||||||
|
<Button className="mt-3" variant="outline" size="sm" onClick={() => void userInfoQuery.refetch()}>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Profile Headline & Bio */}
|
{/* Profile Headline & Bio */}
|
||||||
@@ -463,6 +442,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
<TabsContent value="services" className="flex flex-col gap-6 py-4 outline-none">
|
<TabsContent value="services" className="flex flex-col gap-6 py-4 outline-none">
|
||||||
{userServicesQuery.isPending ? (
|
{userServicesQuery.isPending ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">加载服务事项中…</div>
|
<div className="py-8 text-center text-sm text-muted-foreground">加载服务事项中…</div>
|
||||||
|
) : userServicesQuery.isError ? (
|
||||||
|
<div className="py-8 text-center text-sm text-destructive">
|
||||||
|
<p>服务事项加载失败。</p>
|
||||||
|
<Button className="mt-3" variant="outline" size="sm" onClick={() => void userServicesQuery.refetch()}>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : userServices.length > 0 ? (
|
) : userServices.length > 0 ? (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
@@ -550,57 +536,6 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
</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">
|
<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" />
|
<Wrench className="mx-auto mb-2 size-8 text-muted-foreground/50" />
|
||||||
@@ -610,44 +545,6 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
|
|||||||
</p>
|
</p>
|
||||||
</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>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user