You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"use client";
import { useCallback, useEffect, useState } from "react";
import { AdminLayout } from "@/components/layout/admin-layout";
import { PageHeader } from "@/components/layout/page-header";
import { Skeleton } from "@/components/ui/skeleton";
import { ErrorBanner } from "@/components/ui/error-banner";
import { SecondaryButton } from "@/components/ui/primary-button";
import { PrimaryButton } from "@/components/ui/primary-button";
import { InputField } from "@/components/ui/input-field";
import { SelectField } from "@/components/ui/select-field";
import { Card } from "@/components/ui/card";
import {
adminGetMarkupConfigs,
adminUpdateMarkupConfig,
} from "@/lib/frontend/api-client";
import { formatDateTime, formatPercent, formatUSD } from "@/lib/frontend/format";
import { useAuth } from "@/hooks/use-auth";
import type { DataPageStatus, MarkupConfig, MarkupType } from "@/lib/frontend/types";
const PAGE_SIZE = 10;
function formatMarkupSummary(row: MarkupConfig): string {
if (row.markup_type === "fixed") {
const amount = row.markup_fixed_amount ?? 0;
return amount > 0 ? `固定 ${formatUSD(amount)}` : "未配置";
}
return row.markup_percent > 0 ? formatPercent(row.markup_percent) : "0%";
}
export default function AdminMarkupPage() {
const { token, user } = useAuth();
const [status, setStatus] = useState<DataPageStatus>("loading");
const [rows, setRows] = useState<MarkupConfig[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState("");
const [editing, setEditing] = useState<MarkupConfig | null>(null);
const [markupType, setMarkupType] = useState<MarkupType>("percent");
const [percent, setPercent] = useState("");
const [fixedAmount, setFixedAmount] = useState("");
const [remark, setRemark] = useState("");
const [saving, setSaving] = useState(false);
const [fieldError, setFieldError] = useState<string | undefined>();
const load = useCallback(
async (p: number, kw: string) => {
if (!token) return;
setStatus("loading");
const res = await adminGetMarkupConfigs("", token, p, PAGE_SIZE, kw);
if (res.code !== 0) {
setStatus("error");
return;
}
setRows(res.data.list);
setTotal(res.data.total);
setStatus(res.data.total === 0 ? "empty" : "success");
},
[token],
);
useEffect(() => {
void load(page, keyword);
}, [load, page, keyword]);
const openEdit = (row: MarkupConfig) => {
setEditing(row);
setMarkupType(row.markup_type);
setPercent(String(row.markup_percent));
setFixedAmount(
row.markup_fixed_amount !== null ? String(row.markup_fixed_amount) : "",
);
setRemark(row.remark ?? "");
setFieldError(undefined);
};
const handleSave = async () => {
if (!editing || !token || !user) return;
if (markupType === "percent") {
const n = Number(percent);
if (Number.isNaN(n) || n < 0 || n > 30) {
setFieldError("加价比例须在 0~30% 之间");
return;
}
} else {
const n = Number(fixedAmount);
if (Number.isNaN(n) || n < 0) {
setFieldError("固定加价金额须 ≥ 0");
return;
}
}
setSaving(true);
const res = await adminUpdateMarkupConfig("", token, editing.customer_id, {
markup_type: markupType,
markup_percent:
markupType === "percent" ? Number(percent) : undefined,
markup_fixed_amount:
markupType === "fixed" ? Number(fixedAmount) : undefined,
remark,
});
setSaving(false);
if (res.code !== 0) {
setFieldError(res.message);
return;
}
setRows((list) =>
list.map((r) => (r.customer_id === res.data.customer_id ? res.data : r)),
);
setEditing(null);
};
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
return (
<AdminLayout>
<PageHeader
title="加价配置"
subtitle="按客户对卡派报价设置运费百分比加价或固定金额加价"
action={
<div className="w-64">
<InputField
label="搜索客户"
placeholder="客户 ID"
value={keyword}
onChange={(e) => {
setPage(1);
setKeyword(e.target.value);
}}
/>
</div>
}
/>
{status === "loading" && (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full" />
))}
</div>
)}
{status === "error" && (
<ErrorBanner
action={
<SecondaryButton onClick={() => void load(page, keyword)}>
</SecondaryButton>
}
>
</ErrorBanner>
)}
{status === "empty" && (
<Card className="p-8 text-center text-text-secondary">
</Card>
)}
{status === "success" && (
<div className="space-y-4">
<div className="overflow-x-auto rounded-md border border-border">
<table className="min-w-full text-sm">
<thead className="bg-bg text-left text-text-secondary">
<tr>
<th className="px-4 py-3 font-medium"> ID</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.customer_id} className="border-t border-border">
<td className="px-4 py-3 font-mono">{row.customer_id}</td>
<td className="px-4 py-3">
{row.markup_type === "fixed" ? "固定金额" : "百分比"}
</td>
<td className="px-4 py-3 font-mono text-primary">
{formatMarkupSummary(row)}
</td>
<td className="px-4 py-3 text-text-secondary">
{row.operator_id || "—"}
</td>
<td className="px-4 py-3 text-text-secondary">
{row.remark || "—"}
</td>
<td className="px-4 py-3 text-text-secondary">
{row.operator_id
? formatDateTime(row.updated_at)
: "—"}
</td>
<td className="px-4 py-3">
<SecondaryButton onClick={() => openEdit(row)}>
</SecondaryButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center justify-between text-sm text-text-secondary">
<span>
{total} {page} / {totalPages}
</span>
<div className="flex gap-2">
<SecondaryButton
disabled={page <= 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
</SecondaryButton>
<SecondaryButton
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
</SecondaryButton>
</div>
</div>
</div>
)}
{editing && (
<div
className="fixed inset-0 z-50 flex justify-end bg-black/30"
onClick={() => !saving && setEditing(null)}
>
<div
className="h-full w-full max-w-md bg-surface p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold">
· {editing.customer_id}
</h3>
<p className="mt-1 text-sm text-text-secondary">
30%
</p>
<div className="mt-6 space-y-4">
<SelectField
label="加价方式"
value={markupType}
disabled={saving}
options={[
{ value: "percent", label: "按运费百分比" },
{ value: "fixed", label: "固定金额USD" },
]}
onChange={(e) =>
setMarkupType(e.target.value as MarkupType)
}
/>
{markupType === "percent" ? (
<InputField
label="加价比例 (%)"
type="number"
step="0.1"
min={0}
max={30}
value={percent}
disabled={saving}
error={fieldError}
onChange={(e) => setPercent(e.target.value)}
/>
) : (
<InputField
label="固定加价金额 (USD)"
type="number"
step="0.01"
min={0}
value={fixedAmount}
disabled={saving}
error={fieldError}
onChange={(e) => setFixedAmount(e.target.value)}
/>
)}
<InputField
label="备注"
value={remark}
disabled={saving}
onChange={(e) => setRemark(e.target.value)}
/>
</div>
<div className="mt-8 flex justify-end gap-3">
<SecondaryButton disabled={saving} onClick={() => setEditing(null)}>
</SecondaryButton>
<PrimaryButton loading={saving} onClick={() => void handleSave()}>
</PrimaryButton>
</div>
</div>
</div>
)}
</AdminLayout>
);
}