feat: 添加货币管理功能,更新国际化支持,移除报表相关代码
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
export const reportsModuleMeta = {
|
||||
segment: "reports",
|
||||
title: "报表导出",
|
||||
description: "",
|
||||
} as const;
|
||||
@@ -1,298 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
downloadAdminReportJob,
|
||||
getAdminReportJobs,
|
||||
postAdminReportJob,
|
||||
} from "@/api/admin-reports";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminReportJobListData } from "@/types/api/admin-reports";
|
||||
|
||||
const REPORT_TYPES = [
|
||||
{ value: "draw_profit_summary" },
|
||||
{ value: "daily_profit_summary" },
|
||||
{ value: "player_win_loss" },
|
||||
{ value: "wallet_transfer_report" },
|
||||
{ value: "hot_number_risk_report" },
|
||||
{ value: "play_dimension_report" },
|
||||
{ value: "sold_out_number_report" },
|
||||
{ value: "rebate_commission_report" },
|
||||
{ value: "audit_operation_report" },
|
||||
{ value: "wallet_txns_daily" },
|
||||
{ value: "transfer_orders_daily" },
|
||||
] as const;
|
||||
|
||||
export function ReportsConsole(): React.ReactElement {
|
||||
const { t } = useTranslation(["reports", "common"]);
|
||||
const formatTs = useAdminDateTimeFormatter();
|
||||
const [data, setData] = useState<AdminReportJobListData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
|
||||
const [reportType, setReportType] = useState<string>(REPORT_TYPES[0].value);
|
||||
const [exportFormat, setExportFormat] = useState<"csv" | "xlsx">("csv");
|
||||
const [filterJsonText, setFilterJsonText] = useState('{\n "currency_code": "NPR"\n}');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setErr(null);
|
||||
try {
|
||||
const d = await getAdminReportJobs({ page, per_page: perPage });
|
||||
setData(d);
|
||||
} catch (e) {
|
||||
setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void load();
|
||||
});
|
||||
}, [load]);
|
||||
|
||||
async function onCreate(): Promise<void> {
|
||||
let filter_json: Record<string, unknown> | null = null;
|
||||
const trimmed = filterJsonText.trim();
|
||||
if (trimmed !== "") {
|
||||
try {
|
||||
filter_json = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
} catch {
|
||||
toast.error(t("parseFilterFailed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await postAdminReportJob({
|
||||
report_type: reportType,
|
||||
export_format: exportFormat,
|
||||
parameters: filter_json,
|
||||
filter_json,
|
||||
});
|
||||
toast.success(t("createSuccess"));
|
||||
setPage(1);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("createFailed"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDownload(rowId: number): Promise<void> {
|
||||
try {
|
||||
const blob = await downloadAdminReportJob(rowId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
toast.error(t("downloadFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
const meta = data?.meta;
|
||||
const lastPage = meta
|
||||
? Math.max(1, meta.last_page)
|
||||
: 1;
|
||||
const reportFormatLabel = (value: string) =>
|
||||
t(`formatOptions.${value}`, { defaultValue: value.toUpperCase() });
|
||||
const reportStatusLabel = (value: string) => t(`statusOptions.${value}`, { defaultValue: value });
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-none flex-col gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("createExport")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>{t("reportType")}</Label>
|
||||
<Select
|
||||
modal={false}
|
||||
value={reportType}
|
||||
onValueChange={(v) => {
|
||||
if (v) {
|
||||
setReportType(v);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{REPORT_TYPES.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{t(`reportTypes.${o.value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>{t("exportFormat")}</Label>
|
||||
<Select
|
||||
modal={false}
|
||||
value={exportFormat}
|
||||
onValueChange={(v) => {
|
||||
if (v === "csv" || v === "xlsx") {
|
||||
setExportFormat(v);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="csv">{t("formatOptions.csv")}</SelectItem>
|
||||
<SelectItem value="xlsx">{t("formatOptions.xlsx")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="sm:col-span-2 lg:col-span-3 grid gap-1.5">
|
||||
<Label htmlFor="report-filter-json">{t("filterJson")}</Label>
|
||||
<Textarea
|
||||
id="report-filter-json"
|
||||
value={filterJsonText}
|
||||
onChange={(e) => setFilterJsonText(e.target.value)}
|
||||
rows={5}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 lg:col-span-3">
|
||||
<Button type="button" onClick={() => void onCreate()} disabled={submitting}>
|
||||
{submitting ? t("actions.submitting", { ns: "common" }) : t("actions.createTask", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>{t("taskList")}</CardTitle>
|
||||
</div>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
|
||||
{t("actions.refresh", { ns: "common" })}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
|
||||
{loading && !data ? (
|
||||
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
||||
) : null}
|
||||
{data ? (
|
||||
<>
|
||||
<div className="rounded-md border">
|
||||
<Table id="reports-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-24">{t("id")}</TableHead>
|
||||
<TableHead>{t("jobId")}</TableHead>
|
||||
<TableHead>{t("type")}</TableHead>
|
||||
<TableHead>{t("format")}</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead>{t("output")}</TableHead>
|
||||
<TableHead>{t("download")}</TableHead>
|
||||
<TableHead>{t("createdAt")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-muted-foreground">
|
||||
{t("empty")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.items.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell className="tabular-nums">{row.id}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{row.job_no}</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{t(`reportTypes.${row.report_type}`, {
|
||||
defaultValue: row.report_type,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>{reportFormatLabel(row.export_format)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{reportStatusLabel(row.status)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[12rem] truncate text-xs text-muted-foreground">
|
||||
{row.output_path ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void onDownload(row.id)}
|
||||
>
|
||||
{t("actions.download", { ns: "common" })}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap font-mono text-[11px] text-muted-foreground">
|
||||
{formatTs(row.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{meta ? (
|
||||
<AdminListPaginationFooter
|
||||
selectId="report-jobs-per-page"
|
||||
total={meta.total}
|
||||
page={meta.current_page}
|
||||
lastPage={lastPage}
|
||||
perPage={meta.per_page}
|
||||
loading={loading}
|
||||
onPerPageChange={(n) => {
|
||||
setPerPage(n);
|
||||
setPage(1);
|
||||
}}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user