fix(users): surface composed profile errors

This commit is contained in:
2026-09-08 12:48:37 +08:00
parent 6dcc31a093
commit f243b7180b
+23 -126
View File
@@ -7,14 +7,12 @@ import {
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'
@@ -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)
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
}
},
queryFn: async () => (uid ? (await fetchUserInfo(uid)).data : 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
}
},
queryFn: async () => (uid ? (await fetchUserServices(uid)).data : 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()}>
@@ -121,6 +86,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
{userQuery.isPending ? (
<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 ? (
<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">
{userInfoQuery.isPending ? (
<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 */}
@@ -463,6 +442,13 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
<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>
) : 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 ? (
<div className="grid gap-4">
<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>
) : 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" />
@@ -610,44 +545,6 @@ export function UserDetailSheet({ uid, onClose, onAssignRoles }: UserDetailSheet
</p>
</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>
)}