ab327f5118
- 服务层:12 个 /v1/admin/lottery 接口封装与 API 类型定义 - 活动列表:状态筛选、上架/暂停(带确认)、编辑、管理、发次数 - 奖品管理:3×3 九宫格可视化编辑、概率预览、保底奖警告、佣金元↔分转换、无限库存 - 规则设置:chance_sources 动态编辑,eligibility 简单/高级(JSON)双模式,本地 rulecaps 校验(深度≤8/节点≤64/≤8KB),Stage 1 未接线警示 - 手动发次数:自动生成幂等 source_ref - 路由 /dashboard/lottery、Commerce 菜单项、en-US/zh-CN 双语文案 - 对接文档见 vpn/aaaa.txt(Stage 1)
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import { Badge } from "@workspace/ui/components/badge";
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@workspace/ui/components/sheet";
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@workspace/ui/components/tabs";
|
|
import { getLotteryActivityDetail } from "@workspace/ui/services/admin/lottery";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import PrizeGrid from "./prize-grid";
|
|
import RuleEditor from "./rule-editor";
|
|
|
|
interface ActivityDetailProps {
|
|
activity: API.LotteryActivity | null;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onChanged?: () => void;
|
|
}
|
|
|
|
type TranslateFn = (key: string, defaultValue: string) => string;
|
|
|
|
const STATUS_BADGE_VARIANTS: Record<
|
|
API.LotteryActivityStatus,
|
|
"default" | "secondary" | "destructive" | "outline"
|
|
> = {
|
|
draft: "secondary",
|
|
running: "default",
|
|
paused: "outline",
|
|
ended: "destructive",
|
|
};
|
|
|
|
function statusLabel(
|
|
status: API.LotteryActivityStatus,
|
|
t: TranslateFn
|
|
): string {
|
|
const labels: Record<API.LotteryActivityStatus, string> = {
|
|
draft: t("status.draft", "Draft"),
|
|
running: t("status.running", "Running"),
|
|
paused: t("status.paused", "Paused"),
|
|
ended: t("status.ended", "Ended"),
|
|
};
|
|
return labels[status] ?? status;
|
|
}
|
|
|
|
export default function ActivityDetail({
|
|
activity,
|
|
open,
|
|
onOpenChange,
|
|
onChanged,
|
|
}: ActivityDetailProps) {
|
|
const { t } = useTranslation("lottery");
|
|
// 抽屉打开时拉取详情作为数据源,避免列表行数据不完整时被全量 PUT 覆盖
|
|
const [detail, setDetail] = useState<API.LotteryActivity | null>(null);
|
|
const activityId = activity?.id;
|
|
|
|
useEffect(() => {
|
|
setDetail(null);
|
|
if (!(open && activityId)) return;
|
|
let cancelled = false;
|
|
getLotteryActivityDetail({ id: activityId })
|
|
.then(({ data }) => {
|
|
if (!cancelled && data.data) setDetail(data.data);
|
|
})
|
|
.catch(() => {
|
|
// Request layer already surfaces the error toast; fall back to prop data
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [open, activityId]);
|
|
|
|
const current = detail ?? activity;
|
|
|
|
return (
|
|
<Sheet onOpenChange={onOpenChange} open={open}>
|
|
<SheetContent className="w-[800px] max-w-full sm:max-w-3xl" side="right">
|
|
{current && (
|
|
<>
|
|
<SheetHeader>
|
|
<SheetTitle className="flex items-center gap-2">
|
|
<span className="truncate">{current.title}</span>
|
|
<Badge
|
|
variant={STATUS_BADGE_VARIANTS[current.status] ?? "outline"}
|
|
>
|
|
{statusLabel(current.status, t)}
|
|
</Badge>
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
<div className="max-h-[calc(100dvh-88px)] overflow-y-auto px-6 pb-6">
|
|
<Tabs defaultValue="prizes">
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="prizes">
|
|
{t("detail.prizesTab", "Prizes")}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="rules">
|
|
{t("detail.rulesTab", "Rules")}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="prizes">
|
|
<PrizeGrid activity={current} />
|
|
</TabsContent>
|
|
<TabsContent value="rules">
|
|
<RuleEditor activity={current} onSaved={onChanged} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</>
|
|
)}
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|