132 lines
8.6 KiB
JavaScript
132 lines
8.6 KiB
JavaScript
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>}
|
||
</>;
|
||
}
|