diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 40212d4..5eb5115 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -911,6 +911,7 @@ export function App() { projects={projects} team={currentTeam} onRecharge={(amount, bonusPoints) => action(() => api.recharge({ amount, bonus_points: bonusPoints }), "充值成功")} + onNotify={(type, text) => setNotice({ type, text })} /> ); case "team": diff --git a/core/frontend/src/design-restraint.css b/core/frontend/src/design-restraint.css index edc57cc..9f0fa04 100644 --- a/core/frontend/src/design-restraint.css +++ b/core/frontend/src/design-restraint.css @@ -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 .req { color: var(--accent-crimson); margin-left: 2px; } .field-hint { font-size: 12px; color: var(--black-alpha-48); } +.field-hint.is-error { color: var(--accent-crimson); } .input, .textarea, .select { height: 36px; padding: 0 14px; @@ -783,6 +784,10 @@ main { position: relative; background: var(--background-base); min-width: 0; } border-color: 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:disabled, .textarea:disabled, .select:disabled { background: var(--black-alpha-5); diff --git a/core/frontend/src/routes/account.tsx b/core/frontend/src/routes/account.tsx index 9dfbbb3..492c125 100644 --- a/core/frontend/src/routes/account.tsx +++ b/core/frontend/src/routes/account.tsx @@ -59,6 +59,7 @@ const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: num { amt: 1000, gift: "+800 积分赠送", bonus: true, bonusAmt: 800 }, { 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"] }> = [ { 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; projects: Project[]; // team prop 用于读取团队级月限额(与团队管理页保持同一来源 · #14) team?: Team | null; onRecharge: (amount: number, bonus: number) => void | Promise; + onNotify: (type: "success" | "error" | "info", text: string) => void; }) { const [tab, setTab] = useState("overview"); // 积分汇率(积分/¥):预览换算与后端 recharge 同源,汇率改配后不至于「预览 ×10 实到 ×N」 @@ -175,11 +177,29 @@ export function AccountPage({ billing, projects, team, onRecharge }: { const [billJump, setBillJump] = useState(""); const selectedCard = RECHARGE.find((item) => item.amt === recharge); - const effectiveAmount = Number(customAmt) > 0 ? Number(customAmt) : recharge; - const effectiveBonus = Number(customAmt) > 0 ? 0 : selectedCard?.bonusAmt || 0; + const customAmount = Number(customAmt); + 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+/, "")); + 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() { + if (!validateCustomAmount()) { + setTopupChannel(null); + return; + } if (effectiveAmount <= 0) return; await onRecharge(effectiveAmount, effectiveBonus); // 父级 action 成功后弹全局「充值成功」toast setTopupChannel(null); @@ -319,13 +339,25 @@ export function AccountPage({ billing, projects, team, onRecharge }: {
自定义金额
- setCustomIntegerAmount(event.target.value)} /> + setCustomIntegerAmount(event.target.value)} + /> + {!customAmountIsValid && }
- -