wendajia/frontend/src/RefundManagementPage.jsx

132 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from "react";
import { Plus, Search, X, Pencil, Trash2 } from "lucide-react";
import { formatDateTime } from "./time.js";
const endpoint = "/api/admin/refund-payment-accounts";
const money = (fen) => (fen / 100).toFixed(2);
const emptyForm = () => ({ name: "", alipay_account: "", balance: "0.00", single_limit: "500.00", is_active: true });
export default function RefundManagementPage({ api }) {
const [data, setData] = useState({ items: [], total: 0 });
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [reload, setReload] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [editing, setEditing] = useState(null);
const [form, setForm] = useState(emptyForm);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState("");
const [deleting, setDeleting] = useState(null);
const [deleteError, setDeleteError] = useState("");
useEffect(() => {
let cancelled = false;
setLoading(true);
setError("");
const timer = setTimeout(async () => {
try {
const result = await api(`${endpoint}?${new URLSearchParams({ q: query, page, page_size: pageSize })}`);
if (!cancelled) setData(result.data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}, 250);
return () => { cancelled = true; clearTimeout(timer); };
}, [api, query, page, pageSize, reload]);
function open(account) {
setEditing(account || {});
setForm(account ? { name: account.name, alipay_account: account.alipay_account,
balance: money(account.balance_fen), single_limit: money(account.single_limit_fen), is_active: account.is_active } : emptyForm());
setSaveError("");
}
async function save(event) {
event.preventDefault();
if (saving) return;
setSaving(true);
setSaveError("");
try {
if (!form.name.trim() || !form.alipay_account.trim()) throw new Error("请填写账户名称和支付宝账户");
for (const [key, label] of [["balance", "可用余额"], ["single_limit", "单次付款限额"]]) {
if (!/^\d+(\.\d{1,2})?$/.test(form[key]) || Number(form[key]) > 9999999.99 ||
(key === "single_limit" && Number(form[key]) <= 0)) {
throw new Error(`${label}需填写有效金额,最多两位小数,最高 9999999.99 元${key === "single_limit" ? ",且必须大于 0" : ""}`);
}
}
await api(editing.id ? `${endpoint}/${editing.id}` : endpoint, {
method: editing.id ? "PATCH" : "POST", body: JSON.stringify(form),
});
setEditing(null);
setPage(1);
setReload((n) => n + 1);
} catch (err) {
setSaveError(typeof err.message === "string" ? err.message : "保存失败,请检查填写内容");
} finally { setSaving(false); }
}
async function remove(account) {
if (deleting !== null || !window.confirm(`确定删除付款账户“${account.name}”(${account.alipay_account})吗?删除后无法恢复。`)) return;
setDeleting(account.id);
setDeleteError("");
try {
await api(`${endpoint}/${account.id}`, { method: "DELETE" });
setPage(1);
setReload((n) => n + 1);
} catch (err) {
setDeleteError(err.message || "删除失败,请重试");
} finally { setDeleting(null); }
}
const pages = Math.max(1, Math.ceil(data.total / pageSize));
return <>
<div className="page-heading"><div><h2>返款管理</h2><p>管理返款使用的支付宝付款账户。</p></div></div>
<section className="panel refund-management">
<div className="refund-management-notice">代付接口暂未接入,此处仅维护付款账户资料,不会发起转账。可用余额为人工录入值,后续接入接口后再同步。</div>
<div className="refund-management-toolbar">
<label className="refund-account-search"><Search size={16} /><input aria-label="搜索付款账户" placeholder="搜索账户名称或支付宝账户" value={query} maxLength={128} onChange={(e) => { setQuery(e.target.value); setPage(1); }} /></label>
<button className="primary-button" onClick={() => open(null)}><Plus size={16} />新增付款账户</button>
</div>
{error ? <div className="form-error" role="alert">{error}<button onClick={() => setReload((n) => n + 1)}>重试</button></div> : null}
{deleteError && <div className="form-error" role="alert">{deleteError}</div>}
<div className="refund-account-table-wrap" aria-busy={loading}>
<table className="refund-account-table"><thead><tr>
<th>类型</th><th>账户名称</th><th>支付宝账户</th><th>可用余额</th><th>单次付款限额</th><th>状态</th><th>更新时间</th><th>操作</th>
</tr></thead><tbody>
{loading ? <tr><td colSpan={8} className="empty-state">加载中…</td></tr> : error ? null : data.items.length ? data.items.map((account) => <tr key={account.id}>
<td><span className="refund-account-tag">支付宝</span></td><td>{account.name}</td><td>{account.alipay_account}</td>
<td className="refund-account-balance">¥{money(account.balance_fen)}</td><td>¥{money(account.single_limit_fen)}</td>
<td><span className={`refund-account-tag ${account.is_active ? "enabled" : "disabled"}`}>{account.is_active ? "启用" : "禁用"}</span></td>
<td>{formatDateTime(account.updated_at)}</td><td><div className="refund-account-actions"><button className="refund-account-edit" disabled={deleting !== null} onClick={() => open(account)}><Pencil size={14} />编辑</button><button className="refund-account-edit refund-account-delete" disabled={deleting !== null} onClick={() => remove(account)}><Trash2 size={14} />{deleting === account.id ? "删除中…" : "删除"}</button></div></td>
</tr>) : <tr><td colSpan={8} className="empty-state">{query ? "未找到匹配账户" : "暂无付款账户,请点击新增付款账户"}</td></tr>}
</tbody></table>
</div>
<div className="refund-account-pagination">
<span>共 {data.total} 条记录</span><button disabled={loading || page <= 1} onClick={() => setPage((n) => n - 1)}>上一页</button>
<span>{page} / {pages}</span><button disabled={loading || page >= pages} onClick={() => setPage((n) => n + 1)}>下一页</button>
<select aria-label="每页条数" value={pageSize} onChange={(e) => { setPageSize(Number(e.target.value)); setPage(1); }}>{[10, 20, 50].map((n) => <option key={n} value={n}>{n} 条/页</option>)}</select>
</div>
</section>
{editing && <div className="refund-account-overlay"><section className="refund-account-modal" role="dialog" aria-modal="true" aria-labelledby="refund-account-title">
<header><h2 id="refund-account-title">{editing.id ? "编辑付款账户" : "新增付款账户"}</h2><button aria-label="关闭" disabled={saving} onClick={() => setEditing(null)}><X size={20} /></button></header>
<form onSubmit={save}>
<label>账户类型<input value="支付宝" disabled /></label>
<label>账户名称<input required maxLength={128} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="例如:公司名称或账户别名" /></label>
<label>支付宝账户<input required maxLength={128} value={form.alipay_account} onChange={(e) => setForm({ ...form, alipay_account: e.target.value })} placeholder="支付宝手机号或邮箱" /></label>
<div className="refund-account-money-fields">
<label>可用余额(元)<input required type="number" min="0" max="9999999.99" step="0.01" value={form.balance} onChange={(e) => setForm({ ...form, balance: e.target.value })} /></label>
<label>单次付款限额(元)<input required type="number" min="0.01" max="9999999.99" step="0.01" value={form.single_limit} onChange={(e) => setForm({ ...form, single_limit: e.target.value })} /></label>
</div>
<label>状态<select value={String(form.is_active)} onChange={(e) => setForm({ ...form, is_active: e.target.value === "true" })}><option value="true">启用</option><option value="false">禁用</option></select></label>
<p className="refund-account-hint">余额由人工维护;保存资料不会产生实际付款。</p>
{saveError && <div className="form-error" role="alert">{saveError}</div>}
<footer><button type="button" disabled={saving} onClick={() => setEditing(null)}>取消</button><button className="primary-button" disabled={saving}>{saving ? "保存中…" : "保存"}</button></footer>
</form>
</section></div>}
</>;
}