feat: 重构管理端列表与风控/结算导航,新增表格导出和结算审核确认
This commit is contained in:
@@ -128,8 +128,6 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
const [periodStartLocal, setPeriodStartLocal] = useState("");
|
||||
const [periodEndLocal, setPeriodEndLocal] = useState("");
|
||||
const [scopeLines, setScopeLines] = useState("");
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [itemsJson, setItemsJson] = useState("[]");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const loadJobs = useCallback(async () => {
|
||||
@@ -194,25 +192,7 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let itemsPayload: Parameters<typeof postAdminReconcileJob>[0]["items"];
|
||||
|
||||
if (showAdvanced) {
|
||||
const trimmed = itemsJson.trim();
|
||||
if (trimmed !== "" && trimmed !== "[]") {
|
||||
try {
|
||||
itemsPayload = JSON.parse(trimmed) as NonNullable<
|
||||
Parameters<typeof postAdminReconcileJob>[0]["items"]
|
||||
>;
|
||||
} catch {
|
||||
toast.error(t("advancedJsonInvalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsPayload === undefined) {
|
||||
itemsPayload = scopeLinesToItems(scopeLines);
|
||||
}
|
||||
const itemsPayload = scopeLinesToItems(scopeLines);
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
@@ -225,9 +205,6 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
toast.success(t("createSuccess"));
|
||||
setPage(1);
|
||||
setScopeLines("");
|
||||
if (showAdvanced) {
|
||||
setItemsJson("[]");
|
||||
}
|
||||
await loadJobs();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("createFailed"));
|
||||
@@ -240,17 +217,16 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
const im = items?.meta;
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-none flex-col gap-8">
|
||||
<div className="flex w-full max-w-none flex-col gap-6">
|
||||
{canCreate ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("createTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("createDesc")}
|
||||
</CardDescription>
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header">
|
||||
<CardTitle className="admin-list-title">{t("createTitle")}</CardTitle>
|
||||
<CardDescription className="mt-1">{t("createDesc")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid max-w-3xl gap-4">
|
||||
<div className="grid gap-1.5">
|
||||
<CardContent className="admin-list-content pt-4">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(220px,0.9fr)_minmax(180px,0.7fr)_minmax(180px,0.7fr)_auto] lg:items-end">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="rc-type">{t("reconcileType")}</Label>
|
||||
<Select
|
||||
modal={false}
|
||||
@@ -261,7 +237,7 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="rc-type" className="w-full max-w-md">
|
||||
<SelectTrigger id="rc-type" className="w-full">
|
||||
<SelectValue>{reconcileTypeLabel(reconcileType, t)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start">
|
||||
@@ -272,8 +248,7 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="rc-start">{t("startTime")}</Label>
|
||||
<Input
|
||||
@@ -292,6 +267,9 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
onChange={(e) => setPeriodEndLocal(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" className="w-full lg:w-auto" onClick={() => void onCreate()} disabled={submitting}>
|
||||
{submitting ? t("submitting") : t("createTask")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="rc-scope">{t("scope")}</Label>
|
||||
@@ -299,66 +277,36 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
id="rc-scope"
|
||||
value={scopeLines}
|
||||
onChange={(e) => setScopeLines(e.target.value)}
|
||||
rows={5}
|
||||
rows={3}
|
||||
placeholder={t("scopePlaceholder")}
|
||||
className="min-h-[100px] text-sm"
|
||||
className="min-h-20 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("scopeHint")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 border-t pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-fit px-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setShowAdvanced((x) => !x)}
|
||||
>
|
||||
{showAdvanced ? t("advancedToggleClose") : t("advancedToggleOpen")}
|
||||
</Button>
|
||||
{showAdvanced ? (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="rc-items-adv">{t("advancedJson")}</Label>
|
||||
<Textarea
|
||||
id="rc-items-adv"
|
||||
value={itemsJson}
|
||||
onChange={(e) => setItemsJson(e.target.value)}
|
||||
rows={6}
|
||||
className="font-mono text-xs"
|
||||
placeholder='[{"side_a_ref":"TO-1","side_b_ref":"MAIN-1","difference_amount":100,"status":"mismatch"}]'
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Button type="button" onClick={() => void onCreate()} disabled={submitting}>
|
||||
{submitting ? t("submitting") : t("createTask")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">{t("noCreatePermission")}</p>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header flex flex-row flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>{t("jobsTitle")}</CardTitle>
|
||||
<CardTitle className="admin-list-title">{t("jobsTitle")}</CardTitle>
|
||||
<CardDescription className="mt-1.5">{t("jobsDesc")}</CardDescription>
|
||||
</div>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void loadJobs()}>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="admin-list-content pt-4">
|
||||
{jobsErr ? <p className="text-sm text-red-600 dark:text-red-400">{jobsErr}</p> : null}
|
||||
{jobsLoading && !jobs ? (
|
||||
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
|
||||
) : null}
|
||||
{jobs ? (
|
||||
<>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<div className="admin-table-shell">
|
||||
<Table id="reconcile-jobs-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-24">ID</TableHead>
|
||||
@@ -432,12 +380,12 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
</Card>
|
||||
|
||||
{selectedId != null ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("detailsTitle")}</CardTitle>
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header">
|
||||
<CardTitle className="admin-list-title">{t("detailsTitle")}</CardTitle>
|
||||
<CardDescription className="font-mono text-xs">#{selectedId}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="admin-list-content pt-4">
|
||||
{itemsLoading && !items ? (
|
||||
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
|
||||
) : null}
|
||||
@@ -446,8 +394,8 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
{items.job_no ? (
|
||||
<p className="font-mono text-sm text-muted-foreground">{t("jobNo")} {items.job_no}</p>
|
||||
) : null}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<div className="admin-table-shell">
|
||||
<Table id={`reconcile-items-table-${selectedId ?? "none"}`}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">ID</TableHead>
|
||||
|
||||
Reference in New Issue
Block a user