From f5ffb196040cacae1b4dd86ffb48da41e3ba0fd1 Mon Sep 17 00:00:00 2001 From: Pine Date: Sun, 30 Aug 2026 22:32:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(finance):=20=E6=96=B0=E5=A2=9E=E8=B4=A2?= =?UTF-8?q?=E5=8A=A1=E5=88=86=E7=BB=84=E4=B8=8E=E5=85=85=E5=80=BC=E5=A5=97?= =?UTF-8?q?=E9=A4=90=E7=AE=A1=E7=90=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 套餐(面额/赠送/折扣/上架)写入 system_configs.recharge_packages,支付服务约60秒缓存生效 Co-Authored-By: Claude --- src/App.tsx | 2 + src/lib/rbac.tsx | 7 ++ src/pages/finance/packages.tsx | 132 +++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/pages/finance/packages.tsx diff --git a/src/App.tsx b/src/App.tsx index c16bfcb..cec5e22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ import TaskEditor from "@/pages/tasks/editor"; import TaskDetail from "@/pages/tasks/detail"; import ProvidersPage from "@/pages/providers"; import ConfigPage from "@/pages/config"; +import RechargePackagesPage from "@/pages/finance/packages"; import ArticleEditorPage from "@/pages/article-editor"; import ParkLayout from "@/pages/park"; import CompanyEditPage from "@/pages/park/company-edit"; @@ -83,6 +84,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/lib/rbac.tsx b/src/lib/rbac.tsx index 126b989..8883541 100644 --- a/src/lib/rbac.tsx +++ b/src/lib/rbac.tsx @@ -1,5 +1,6 @@ import type { LucideIcon } from "lucide-react"; import { + Wallet, Users, GraduationCap, Calendar, @@ -72,6 +73,12 @@ export const NAV: NavGroup[] = [ { to: "/plans", label: "流程启动", icon: Flag, roles: ["operator"], perm: "menu:admin_plans" }, ], }, + { + title: "财务", + items: [ + { to: "/finance/packages", label: "充值套餐", icon: Wallet, roles: ["operator"] }, + ], + }, { title: "业务运营", items: [ diff --git a/src/pages/finance/packages.tsx b/src/pages/finance/packages.tsx new file mode 100644 index 0000000..87b9240 --- /dev/null +++ b/src/pages/finance/packages.tsx @@ -0,0 +1,132 @@ +import * as React from "react"; +import { PageHeader } from "@/components/generic/PageHeader"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { configApi } from "@/api/resources"; + +/** 充值套餐项(system_configs.recharge_packages JSON)。 + * amount=面额(元)、bonus=赠送(元)、discount=折扣%(100=原价)、enabled=上架开关 */ +interface Pkg { + id: string; + amount: number; + bonus: number; + discount: number; + label: string; + enabled: boolean; +} + +const CONFIG_KEY = "recharge_packages"; +const EMPTY: Pkg = { id: "", amount: 10, bonus: 0, discount: 100, label: "", enabled: true }; + +export default function RechargePackagesPage() { + const [items, setItems] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [saving, setSaving] = React.useState(false); + + const load = React.useCallback(async () => { + setLoading(true); + try { + const rows = await configApi.list(); + const row = (rows as { key: string; value: string }[]).find((r) => r.key === CONFIG_KEY); + const parsed = row?.value ? JSON.parse(row.value) : []; + setItems( + (Array.isArray(parsed) ? parsed : []).map((p: Partial, i: number) => ({ + id: p.id || `p${i + 1}`, + amount: Number(p.amount ?? 0), + bonus: Number(p.bonus ?? 0), + discount: Number(p.discount ?? 100), + label: p.label || "", + enabled: p.enabled !== false, + })), + ); + } catch (e) { + window.alert((e as Error).message || "加载套餐配置失败"); + } finally { + setLoading(false); + } + }, []); + + React.useEffect(() => { load(); }, [load]); + + const patch = (i: number, fields: Partial) => + setItems((arr) => arr.map((it, k) => (k === i ? { ...it, ...fields } : it))); + + const save = async () => { + // 校验:面额>0、折扣 1~100、id 唯一 + const ids = new Set(); + for (const [i, it] of items.entries()) { + if (!(it.amount > 0)) { window.alert(`第 ${i + 1} 行:面额必须大于 0`); return; } + if (it.discount < 1 || it.discount > 100) { window.alert(`第 ${i + 1} 行:折扣需在 1~100 之间`); return; } + const id = it.id.trim() || `p${i + 1}`; + if (ids.has(id)) { window.alert(`套餐 ID 重复:${id}`); return; } + ids.add(id); + } + setSaving(true); + try { + await configApi.set( + CONFIG_KEY, + JSON.stringify(items.map((it, i) => ({ ...it, id: it.id.trim() || `p${i + 1}` }))), + "算力充值套餐(app/pay 支付子应用读取)", + ); + window.alert("套餐已保存,最长 60 秒内生效"); + load(); + } catch (e) { + window.alert((e as Error).message || "保存失败"); + } finally { + setSaving(false); + } + }; + + return ( +
+ + + + +
+ } + /> + + + {loading ? ( +

加载中…

+ ) : items.length === 0 ? ( +

暂无套餐,点击「新增套餐」创建。未配置时前端使用服务端默认套餐。

+ ) : ( +
+
+ 套餐 ID面额(元)赠送(元)折扣(%)显示名称上架 +
+ {items.map((it, i) => { + const pay = Math.round((it.amount * it.discount) / 100); + return ( +
+ patch(i, { id: e.target.value })} placeholder="p10" /> + patch(i, { amount: Number(e.target.value) })} /> + patch(i, { bonus: Number(e.target.value) })} /> + patch(i, { discount: Number(e.target.value) })} /> + patch(i, { label: e.target.value })} placeholder="前端显示名(留空自动)" /> +
+ patch(i, { enabled: e.target.checked })} /> + ¥{pay} +
+ +
+ ); + })} +

+ 实付金额 = 面额 × 折扣%(表内 ¥ 为实付);用户到账 = 实付 + 赠送。折扣 100 表示原价。 +

+
+ )} +
+
+ + ); +}