Files
hi-frontend/apps/admin/src/sections/user/user-invite-stats-sheet.tsx
T
shanshanzhong147 6b92979c7c
Build and Release / Build (push) Has been cancelled
feat: 自定义版本功能更新
- 新增家庭共享订阅管理
- 新增用户邀请统计
- 新增签名和订阅模式设置表单
- 更新 API 服务层和国际化文件
- UI 组件优化(enhanced-input、pro-table)
2026-03-19 01:56:13 -07:00

229 lines
7.3 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@workspace/ui/components/avatar";
import { Badge } from "@workspace/ui/components/badge";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@workspace/ui/components/sheet";
import { Skeleton } from "@workspace/ui/components/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@workspace/ui/components/table";
import {
getAdminUserInviteList,
getAdminUserInviteStats,
} from "@workspace/ui/services/admin/user";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
import { formatDate } from "@/utils/common";
interface UserInviteStatsSheetProps {
userId: number;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function UserInviteStatsSheet({
userId,
open,
onOpenChange,
}: UserInviteStatsSheetProps) {
const { t } = useTranslation("user");
const [page, setPage] = useState(1);
const pageSize = 200;
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ["adminUserInviteStats", userId],
queryFn: async () => {
const { data } = await getAdminUserInviteStats({ user_id: userId });
return data.data;
},
enabled: open && !!userId,
});
const { data: listResult, isLoading: listLoading } = useQuery({
queryKey: ["adminUserInviteList", userId, page],
queryFn: async () => {
const { data } = await getAdminUserInviteList({
user_id: userId,
page,
size: pageSize,
});
return data.data;
},
enabled: open && !!userId,
});
const inviteList = listResult?.list ?? [];
const total = listResult?.total ?? 0;
const totalPages = Math.ceil(total / pageSize);
return (
<Sheet onOpenChange={onOpenChange} open={open}>
<SheetContent className="w-[560px] overflow-y-auto sm:max-w-[560px]">
<SheetHeader>
<SheetTitle>{t("inviteStats", "Invite Statistics")}</SheetTitle>
</SheetHeader>
{/* 概览卡片 */}
<div className="mt-4 grid grid-cols-2 gap-3">
<StatCard
label={t("inviteCount", "Invited Users")}
loading={statsLoading}
>
<span className="font-semibold text-2xl">
{stats?.invite_count ?? 0}
</span>
</StatCard>
<StatCard
label={t("totalCommission", "Total Commission")}
loading={statsLoading}
>
<Display type="currency" value={stats?.total_commission} />
</StatCard>
<StatCard
label={t("currentCommission", "Current Commission")}
loading={statsLoading}
>
<Display type="currency" value={stats?.current_commission} />
</StatCard>
<StatCard
label={t("referralPercentage", "Referral %")}
loading={statsLoading}
>
<span className="font-semibold text-2xl">
{stats?.referral_percentage
? `${stats.referral_percentage}%`
: t("globalDefault", "Global Default")}
</span>
{stats?.only_first_purchase && (
<span className="ml-1 text-muted-foreground text-xs">
({t("firstPurchaseOnly", "First purchase only")})
</span>
)}
</StatCard>
</div>
{/* 邀请用户列表 */}
<div className="mt-6">
<h3 className="mb-3 font-medium text-sm">
{t("invitedUsers", "Invited Users")} ({total})
</h3>
{listLoading ? (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton className="h-10 w-full" key={i} />
))}
</div>
) : inviteList.length === 0 ? (
<p className="py-8 text-center text-muted-foreground text-sm">
{t("noInvitedUsers", "No invited users yet")}
</p>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("user", "User")}</TableHead>
<TableHead>{t("status", "Status")}</TableHead>
<TableHead>{t("registeredAt", "Registered At")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{inviteList.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center gap-2">
<Avatar className="h-7 w-7">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-xs">
{user.identifier?.charAt(0)?.toUpperCase() ?? "U"}
</AvatarFallback>
</Avatar>
<span className="max-w-[160px] truncate text-sm">
{user.identifier || `#${user.id}`}
</span>
</div>
</TableCell>
<TableCell>
<Badge variant={user.enable ? "default" : "secondary"}>
{user.enable
? t("enabled", "Enabled")
: t("disabled", "Disabled")}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{formatDate(user.created_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* 分页 */}
{totalPages > 1 && (
<div className="mt-4 flex justify-center gap-2">
<button
className="rounded border px-3 py-1 text-sm disabled:opacity-40"
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
type="button"
>
{t("prev", "Prev")}
</button>
<span className="px-3 py-1 text-sm">
{page} / {totalPages}
</span>
<button
className="rounded border px-3 py-1 text-sm disabled:opacity-40"
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
type="button"
>
{t("next", "Next")}
</button>
</div>
)}
</>
)}
</div>
</SheetContent>
</Sheet>
);
}
function StatCard({
label,
loading,
children,
}: {
label: string;
loading: boolean;
children: React.ReactNode;
}) {
return (
<div className="space-y-1 rounded-lg border p-4">
<p className="text-muted-foreground text-xs">{label}</p>
{loading ? (
<Skeleton className="h-8 w-24" />
) : (
<div className="flex items-baseline gap-1 font-semibold text-2xl">
{children}
</div>
)}
</div>
);
}