fix: 限制自定义充值最低金额

This commit is contained in:
hh
2026-07-15 13:47:32 +08:00
parent e81d7170ed
commit fdd3bbfad2
3 changed files with 44 additions and 6 deletions
+1
View File
@@ -911,6 +911,7 @@ export function App() {
projects={projects} projects={projects}
team={currentTeam} team={currentTeam}
onRecharge={(amount, bonusPoints) => action(() => api.recharge({ amount, bonus_points: bonusPoints }), "充值成功")} onRecharge={(amount, bonusPoints) => action(() => api.recharge({ amount, bonus_points: bonusPoints }), "充值成功")}
onNotify={(type, text) => setNotice({ type, text })}
/> />
); );
case "team": case "team":
+5
View File
@@ -766,6 +766,7 @@ main { position: relative; background: var(--background-base); min-width: 0; }
.field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); } .field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.field-label .req { color: var(--accent-crimson); margin-left: 2px; } .field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.field-hint { font-size: 12px; color: var(--black-alpha-48); } .field-hint { font-size: 12px; color: var(--black-alpha-48); }
.field-hint.is-error { color: var(--accent-crimson); }
.input, .textarea, .select { .input, .textarea, .select {
height: 36px; height: 36px;
padding: 0 14px; padding: 0 14px;
@@ -783,6 +784,10 @@ main { position: relative; background: var(--background-base); min-width: 0; }
border-color: var(--heat-40); border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40); box-shadow: inset 0 0 0 1px var(--heat-40);
} }
.input.is-invalid, .textarea.is-invalid, .select.is-invalid {
border-color: var(--accent-crimson);
box-shadow: inset 0 0 0 1px var(--accent-crimson);
}
.input::placeholder, .textarea::placeholder { color: var(--black-alpha-48); } .input::placeholder, .textarea::placeholder { color: var(--black-alpha-48); }
.input:disabled, .textarea:disabled, .select:disabled { .input:disabled, .textarea:disabled, .select:disabled {
background: var(--black-alpha-5); background: var(--black-alpha-5);
+38 -6
View File
@@ -59,6 +59,7 @@ const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: num
{ amt: 1000, gift: "+800 积分赠送", bonus: true, bonusAmt: 800 }, { amt: 1000, gift: "+800 积分赠送", bonus: true, bonusAmt: 800 },
{ amt: 3000, gift: "+3000 积分赠送", bonus: true, bonusAmt: 3000 } { amt: 3000, gift: "+3000 积分赠送", bonus: true, bonusAmt: 3000 }
]; ];
const MIN_RECHARGE_AMOUNT = 50;
const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [ const STAGES: Array<{ k: string; color: string; bucket: keyof BillingTrend["by_stage"] }> = [
{ k: "视频片段(Seedance)", color: "var(--heat)", bucket: "video" }, { k: "视频片段(Seedance)", color: "var(--heat)", bucket: "video" },
@@ -113,12 +114,13 @@ function TopupModal({ open, channel, amount, bonus, rate, close, onDone }: {
); );
} }
export function AccountPage({ billing, projects, team, onRecharge }: { export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
billing: BillingSummary | null; billing: BillingSummary | null;
projects: Project[]; projects: Project[];
// team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14) // team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14)
team?: Team | null; team?: Team | null;
onRecharge: (amount: number, bonus: number) => void | Promise<unknown>; onRecharge: (amount: number, bonus: number) => void | Promise<unknown>;
onNotify: (type: "success" | "error" | "info", text: string) => void;
}) { }) {
const [tab, setTab] = useState<Tab>("overview"); const [tab, setTab] = useState<Tab>("overview");
// 积分汇率(积分/¥):预览换算与后端 recharge 同源,汇率改配后不至于「预览 ×10 实到 ×N」 // 积分汇率(积分/¥):预览换算与后端 recharge 同源,汇率改配后不至于「预览 ×10 实到 ×N」
@@ -175,11 +177,29 @@ export function AccountPage({ billing, projects, team, onRecharge }: {
const [billJump, setBillJump] = useState(""); const [billJump, setBillJump] = useState("");
const selectedCard = RECHARGE.find((item) => item.amt === recharge); const selectedCard = RECHARGE.find((item) => item.amt === recharge);
const effectiveAmount = Number(customAmt) > 0 ? Number(customAmt) : recharge; const customAmount = Number(customAmt);
const effectiveBonus = Number(customAmt) > 0 ? 0 : selectedCard?.bonusAmt || 0; const hasCustomAmount = customAmt.length > 0;
const customAmountIsValid = !hasCustomAmount || (Number.isFinite(customAmount) && customAmount >= MIN_RECHARGE_AMOUNT);
const effectiveAmount = hasCustomAmount ? customAmount : recharge;
const effectiveBonus = hasCustomAmount ? 0 : selectedCard?.bonusAmt || 0;
const setCustomIntegerAmount = (value: string) => setCustomAmt(value.replace(/\D/g, "").replace(/^0+/, "")); const setCustomIntegerAmount = (value: string) => setCustomAmt(value.replace(/\D/g, "").replace(/^0+/, ""));
function validateCustomAmount() {
if (customAmountIsValid) return true;
onNotify("error", `最小充值金额为 ${MIN_RECHARGE_AMOUNT}`);
return false;
}
function openTopup(channel: "wechat" | "alipay") {
if (!validateCustomAmount()) return;
setTopupChannel(channel);
}
async function submitRecharge() { async function submitRecharge() {
if (!validateCustomAmount()) {
setTopupChannel(null);
return;
}
if (effectiveAmount <= 0) return; if (effectiveAmount <= 0) return;
await onRecharge(effectiveAmount, effectiveBonus); // 父级 action 成功后弹全局「充值成功」toast await onRecharge(effectiveAmount, effectiveBonus); // 父级 action 成功后弹全局「充值成功」toast
setTopupChannel(null); setTopupChannel(null);
@@ -319,13 +339,25 @@ export function AccountPage({ billing, projects, team, onRecharge }: {
</div> </div>
<div className="pay-row"> <div className="pay-row">
<div className="pay-title"></div> <div className="pay-title"></div>
<input className="input" placeholder="最低 ¥50,可输入任意金额" type="text" inputMode="numeric" pattern="[0-9]*" value={customAmt} onChange={(event) => setCustomIntegerAmount(event.target.value)} /> <input
className={`input${!customAmountIsValid ? " is-invalid" : ""}`}
placeholder="最低 ¥50,可输入任意金额"
type="number"
min={MIN_RECHARGE_AMOUNT}
step="1"
inputMode="numeric"
value={customAmt}
aria-invalid={!customAmountIsValid}
aria-describedby="custom-recharge-error"
onChange={(event) => setCustomIntegerAmount(event.target.value)}
/>
{!customAmountIsValid && <div className="field-hint is-error" id="custom-recharge-error" role="alert"> {MIN_RECHARGE_AMOUNT} </div>}
<div className="pay-btn-row"> <div className="pay-btn-row">
<button className="btn pay-method-btn pay-wechat" type="button" aria-label="微信支付" onClick={() => setTopupChannel("wechat")}> <button className="btn pay-method-btn pay-wechat" type="button" aria-label="微信支付" onClick={() => openTopup("wechat")}>
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-wechat.png" alt="" /></span> <span className="pay-logo" aria-hidden="true"><img src="/assets/pay-wechat.png" alt="" /></span>
</button> </button>
<button className="btn pay-method-btn pay-alipay" type="button" aria-label="支付宝" onClick={() => setTopupChannel("alipay")}> <button className="btn pay-method-btn pay-alipay" type="button" aria-label="支付宝" onClick={() => openTopup("alipay")}>
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-alipay.png" alt="" /></span> <span className="pay-logo" aria-hidden="true"><img src="/assets/pay-alipay.png" alt="" /></span>
</button> </button>