322d60b648
- 活动列表加"删除"(ConfirmButton→DELETE /activities/:id) - activity-form 格子数选项 [8,9],新建默认 8(原来写死只有 9) - service: deleteLotteryActivity;i18n 删除文案 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
291 lines
9.7 KiB
TypeScript
291 lines
9.7 KiB
TypeScript
import { Badge } from "@workspace/ui/components/badge";
|
|
import { Button } from "@workspace/ui/components/button";
|
|
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
|
import {
|
|
ProTable,
|
|
type ProTableActions,
|
|
} from "@workspace/ui/composed/pro-table/pro-table";
|
|
import {
|
|
createLotteryActivity,
|
|
deleteLotteryActivity,
|
|
getLotteryActivityList,
|
|
pauseLotteryActivity,
|
|
publishLotteryActivity,
|
|
updateLotteryActivity,
|
|
} from "@workspace/ui/services/admin/lottery";
|
|
import { useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { formatDate } from "@/utils/common";
|
|
import ActivityDetail from "./activity-detail";
|
|
import ActivityForm from "./activity-form";
|
|
import GrantDialog from "./grant-dialog";
|
|
|
|
export default function Lottery() {
|
|
const { t } = useTranslation("lottery");
|
|
const [loading, setLoading] = useState(false);
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
const [grantOpen, setGrantOpen] = useState(false);
|
|
const [selectedActivity, setSelectedActivity] =
|
|
useState<API.LotteryActivity | null>(null);
|
|
const ref = useRef<ProTableActions>(null);
|
|
|
|
const statusConfig: Record<
|
|
API.LotteryActivityStatus,
|
|
{
|
|
label: string;
|
|
variant: "secondary" | "destructive" | "outline";
|
|
className?: string;
|
|
}
|
|
> = {
|
|
draft: { label: t("status.draft", "Draft"), variant: "secondary" },
|
|
running: {
|
|
label: t("status.running", "Running"),
|
|
variant: "outline",
|
|
className: "border-transparent bg-green-500 text-white",
|
|
},
|
|
paused: {
|
|
label: t("status.paused", "Paused"),
|
|
variant: "outline",
|
|
className: "border-transparent bg-yellow-500 text-white",
|
|
},
|
|
ended: { label: t("status.ended", "Ended"), variant: "destructive" },
|
|
};
|
|
|
|
const statusOptions = [
|
|
{ label: t("statusAll", "All"), value: "all" },
|
|
{ label: t("status.draft", "Draft"), value: "draft" },
|
|
{ label: t("status.running", "Running"), value: "running" },
|
|
{ label: t("status.paused", "Paused"), value: "paused" },
|
|
{ label: t("status.ended", "Ended"), value: "ended" },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<ProTable<API.LotteryActivity, { status: string }>
|
|
action={ref}
|
|
actions={{
|
|
render: (row) => {
|
|
const rowActions: React.ReactNode[] = [];
|
|
if (row.status === "draft") {
|
|
rowActions.push(
|
|
<ConfirmButton
|
|
cancelText={t("cancel", "Cancel")}
|
|
confirmText={t("confirm", "Confirm")}
|
|
description={t(
|
|
"publishWarning",
|
|
"Once published, the activity becomes visible to users."
|
|
)}
|
|
key="publish"
|
|
onConfirm={async () => {
|
|
try {
|
|
await publishLotteryActivity({ id: row.id });
|
|
toast.success(t("publishSuccess", "Publish Success"));
|
|
ref.current?.refresh();
|
|
} catch {
|
|
// Request layer already surfaces the error toast
|
|
}
|
|
}}
|
|
title={t(
|
|
"confirmPublish",
|
|
"Are you sure you want to publish this activity?"
|
|
)}
|
|
trigger={<Button size="sm">{t("publish", "Publish")}</Button>}
|
|
/>
|
|
);
|
|
}
|
|
if (row.status === "running") {
|
|
rowActions.push(
|
|
<ConfirmButton
|
|
cancelText={t("cancel", "Cancel")}
|
|
confirmText={t("confirm", "Confirm")}
|
|
description={t(
|
|
"pauseWarning",
|
|
"Users will not be able to draw while the activity is paused."
|
|
)}
|
|
key="pause"
|
|
onConfirm={async () => {
|
|
try {
|
|
await pauseLotteryActivity({ id: row.id });
|
|
toast.success(t("pauseSuccess", "Pause Success"));
|
|
ref.current?.refresh();
|
|
} catch {
|
|
// Request layer already surfaces the error toast
|
|
}
|
|
}}
|
|
title={t(
|
|
"confirmPause",
|
|
"Are you sure you want to pause this activity?"
|
|
)}
|
|
trigger={
|
|
<Button size="sm" variant="destructive">
|
|
{t("pause", "Pause")}
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
rowActions.push(
|
|
<ActivityForm
|
|
initialValues={row}
|
|
key="edit"
|
|
loading={loading}
|
|
onSubmit={async (values) => {
|
|
setLoading(true);
|
|
try {
|
|
await updateLotteryActivity({ ...values, id: row.id });
|
|
toast.success(t("updateSuccess", "Update Success"));
|
|
ref.current?.refresh();
|
|
setLoading(false);
|
|
return true;
|
|
} catch (_error) {
|
|
setLoading(false);
|
|
return false;
|
|
}
|
|
}}
|
|
title={t("editActivity", "Edit Activity")}
|
|
trigger={t("edit", "Edit")}
|
|
/>,
|
|
<Button
|
|
key="manage"
|
|
onClick={() => {
|
|
setSelectedActivity(row);
|
|
setDetailOpen(true);
|
|
}}
|
|
size="sm"
|
|
variant="outline"
|
|
>
|
|
{t("manage", "Manage")}
|
|
</Button>,
|
|
<Button
|
|
key="grant"
|
|
onClick={() => {
|
|
setSelectedActivity(row);
|
|
setGrantOpen(true);
|
|
}}
|
|
size="sm"
|
|
variant="outline"
|
|
>
|
|
{t("grantChances", "Grant Chances")}
|
|
</Button>,
|
|
<ConfirmButton
|
|
cancelText={t("cancel", "Cancel")}
|
|
confirmText={t("delete", "Delete")}
|
|
description={t(
|
|
"confirmDeleteActivityDesc",
|
|
"This permanently deletes the activity and its prizes. A running activity must be paused first. This cannot be undone."
|
|
)}
|
|
key="delete"
|
|
onConfirm={async () => {
|
|
try {
|
|
await deleteLotteryActivity({ id: row.id });
|
|
toast.success(t("deleteSuccess", "Deleted"));
|
|
ref.current?.refresh();
|
|
} catch {
|
|
// Request layer already surfaces the error toast
|
|
}
|
|
}}
|
|
title={t("confirmDeleteActivity", "Delete this activity?")}
|
|
trigger={
|
|
<Button size="sm" variant="destructive">
|
|
{t("delete", "Delete")}
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
return rowActions;
|
|
},
|
|
}}
|
|
columns={[
|
|
{
|
|
accessorKey: "id",
|
|
header: t("id", "ID"),
|
|
},
|
|
{
|
|
accessorKey: "title",
|
|
header: t("title", "Title"),
|
|
},
|
|
{
|
|
accessorKey: "status",
|
|
header: t("statusLabel", "Status"),
|
|
cell: ({ row }) => {
|
|
const status = statusConfig[row.original.status];
|
|
return (
|
|
<Badge className={status.className} variant={status.variant}>
|
|
{status.label}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "start_at",
|
|
header: t("activityTime", "Activity Time"),
|
|
cell: ({ row }) =>
|
|
`${formatDate(row.original.start_at)} - ${formatDate(
|
|
row.original.end_at
|
|
)}`,
|
|
},
|
|
{
|
|
accessorKey: "grid_size",
|
|
header: t("gridSize", "Grid Size"),
|
|
},
|
|
]}
|
|
header={{
|
|
toolbar: (
|
|
<ActivityForm
|
|
loading={loading}
|
|
onSubmit={async (values) => {
|
|
setLoading(true);
|
|
try {
|
|
await createLotteryActivity(values);
|
|
toast.success(t("createSuccess", "Create Success"));
|
|
ref.current?.refresh();
|
|
setLoading(false);
|
|
return true;
|
|
} catch (_error) {
|
|
setLoading(false);
|
|
return false;
|
|
}
|
|
}}
|
|
title={t("createActivity", "Create Activity")}
|
|
trigger={t("create", "Create")}
|
|
/>
|
|
),
|
|
}}
|
|
params={[
|
|
{
|
|
key: "status",
|
|
placeholder: t("statusLabel", "Status"),
|
|
options: statusOptions,
|
|
},
|
|
]}
|
|
request={async (pagination, filters) => {
|
|
const { data } = await getLotteryActivityList({
|
|
...pagination,
|
|
...filters,
|
|
status:
|
|
filters?.status && filters.status !== "all"
|
|
? filters.status
|
|
: undefined,
|
|
});
|
|
return {
|
|
list: data.data?.list || [],
|
|
total: data.data?.total || 0,
|
|
};
|
|
}}
|
|
/>
|
|
<ActivityDetail
|
|
activity={selectedActivity}
|
|
onChanged={() => ref.current?.refresh()}
|
|
onOpenChange={setDetailOpen}
|
|
open={detailOpen}
|
|
/>
|
|
<GrantDialog
|
|
activity={selectedActivity}
|
|
onOpenChange={setGrantOpen}
|
|
open={grantOpen}
|
|
/>
|
|
</>
|
|
);
|
|
}
|