增加统一倍率调整按钮

This commit is contained in:
andy committed 2026-09-23 15:46:21 +08:00
1 parent be655b6569
commit 4195525dc7
23 files changed
+750 -16

No files matched your search

+80 -4
View File
@@ -21,7 +21,7 @@ import {
} from "lucide-react";
import { billingUnitLabel, formatBillingAmount } from "@/lib/billing";
import { parseAdminBillingPayload, parseMemberBillingPayload } from "@/lib/client/billing-api";
import { groupedPriceRuleSections, priceTierEditTarget, type PriceEditTarget } from "@/lib/client/billing-price-catalog";
import { groupedPriceRuleSections, priceCatalogBulkTarget, priceTierEditTarget, type PriceEditTarget } from "@/lib/client/billing-price-catalog";
import { pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion";
import type { BillingAccountConfig, BillingParameterDimension, BillingParameterTier, BillingPriceRule, OrganizationWallet } from "@/lib/types";
@@ -121,6 +121,7 @@ export function BillingManager({ isSuperAdmin }: { isSuperAdmin: boolean }) {
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [priceEditState, setPriceEditState] = useState<PriceEditState | null>(null);
const [bulkPriceEditOpen, setBulkPriceEditOpen] = useState(false);
const managerRef = useRef<HTMLDivElement | null>(null);
const feedbackRef = useRef<HTMLDivElement | null>(null);
@@ -242,6 +243,13 @@ export function BillingManager({ isSuperAdmin }: { isSuperAdmin: boolean }) {
if (saved) setPriceEditState(null);
}
async function saveBulkPriceMultiplier(markupMultiplier: string) {
const ruleIds = priceCatalogBulkTarget(admin?.priceRules || []).ruleIds;
if (!ruleIds.length) return;
const saved = await mutate("/api/admin/billing/prices", { ruleIds, markupMultiplier }, "价格目录中所有模型的倍率已更新,可继续单独调整档位。");
if (saved) setBulkPriceEditOpen(false);
}
async function mutate(url: string, body: Record<string, unknown>, successMessage: string, method = "PATCH"): Promise<boolean> {
setSaving(true);
setError(null);
@@ -301,12 +309,13 @@ export function BillingManager({ isSuperAdmin }: { isSuperAdmin: boolean }) {
{isSuperAdmin && admin && billing ? (
<>
{activeTab === "overview" ? <AdminOverview billing={billing} admin={admin} onChangeTab={setActiveTab} /> : null}
{activeTab === "pricing" ? <PriceManagement admin={admin} saving={saving} onEdit={(rule, target) => setPriceEditState({ rule, target })} /> : null}
{activeTab === "pricing" ? <PriceManagement admin={admin} saving={saving} onEdit={(rule, target) => setPriceEditState({ rule, target })} onBulkEdit={() => { setError(null); setBulkPriceEditOpen(true); }} /> : null}
{activeTab === "balance" ? <BalanceManagement admin={admin} adjustmentDraft={adjustmentDraft} setAdjustmentDraft={setAdjustmentDraft} selectedOrganization={selectedAdjustmentOrganization} saving={saving} onSubmit={submitAdjustment} /> : null}
{activeTab === "account" ? <AccountSettings account={admin.billingAccount} draft={accountDraft} editing={editingAccount} saving={saving} onEdit={() => setEditingAccount(true)} onCancel={() => { setEditingAccount(false); setAccountDraft({ ...emptyAccountDraft, ...admin.billingAccount }); }} onChange={(key, value) => setAccountDraft((current) => ({ ...current, [key]: value }))} onSubmit={saveBillingAccount} /> : null}
</>
) : null}
{priceEditState ? <PriceMultiplierDialog state={priceEditState} saving={saving} onClose={() => setPriceEditState(null)} onSubmit={savePriceMultiplier} /> : null}
{bulkPriceEditOpen && admin ? <BulkPriceMultiplierDialog rules={admin.priceRules} saving={saving} error={error} onClose={() => setBulkPriceEditOpen(false)} onSubmit={saveBulkPriceMultiplier} /> : null}
</div>
);
}
@@ -410,9 +419,9 @@ function AdminOverview({ billing, admin, onChangeTab }: { billing: BillingPayloa
</div>;
}
function PriceManagement({ admin, saving, onEdit }: { admin: AdminPayload; saving: boolean; onEdit: (rule: BillingPriceRule, target: PriceEditTarget) => void }) {
function PriceManagement({ admin, saving, onEdit, onBulkEdit }: { admin: AdminPayload; saving: boolean; onEdit: (rule: BillingPriceRule, target: PriceEditTarget) => void; onBulkEdit: () => void }) {
return <section id="billing-panel-pricing" role="tabpanel" aria-labelledby="billing-tab-pricing" className="billing-section billing-pricing-section billing-tab-panel" data-animate>
<div className="billing-section-heading billing-pricing-heading"><div><h2>价格目录</h2></div><div className="billing-formula" aria-label="计费公式"><span>标准成本</span><b>×</b><span>倍率</span><b>=</b><strong>用户价</strong></div></div>
<div className="billing-section-heading billing-pricing-heading"><div><h2>价格目录</h2></div><div className="billing-pricing-actions"><div className="billing-formula" aria-label="计费公式"><span>标准成本</span><b>×</b><span>倍率</span><b>=</b><strong>用户价</strong></div><button className="billing-secondary-button billing-bulk-price-button" type="button" disabled={saving || admin.priceRules.length === 0} onClick={onBulkEdit}><Settings2 size={15} aria-hidden="true" />统一调整倍率</button></div></div>
<PriceCatalog rules={admin.priceRules} onEdit={onEdit} disabled={saving} />
</section>;
}
@@ -602,6 +611,73 @@ function PriceMultiplierDialog({
);
}
function BulkPriceMultiplierDialog({
rules,
saving,
error,
onClose,
onSubmit
}: {
rules: BillingPriceRule[];
saving: boolean;
error: string | null;
onClose: () => void;
onSubmit: (value: string) => Promise<void>;
}) {
const target = priceCatalogBulkTarget(rules);
const [value, setValue] = useState(target.currentMultiplier === null ? "" : String(target.currentMultiplier));
const inputRef = useRef<HTMLInputElement | null>(null);
const parsedValue = Number(value);
const valid = value.trim() !== "" && Number.isFinite(parsedValue) && parsedValue >= 1 && parsedValue <= 1000;
useEffect(() => {
window.requestAnimationFrame(() => inputRef.current?.focus());
}, []);
useEffect(() => {
function handleKeyDown(event: globalThis.KeyboardEvent) {
if (event.key === "Escape" && !saving) onClose();
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose, saving]);
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!valid || saving || !target.ruleIds.length) return;
void onSubmit(value.trim());
}
return <div className="billing-modal-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget && !saving) onClose(); }}>
<section className="billing-modal billing-price-edit-dialog" role="dialog" aria-modal="true" aria-labelledby="billing-bulk-price-title" aria-describedby="billing-bulk-price-description" onMouseDown={(event) => event.stopPropagation()}>
<div className="billing-modal-header">
<div className="billing-modal-title-wrap">
<span className="billing-modal-icon" aria-hidden="true"><CircleDollarSign size={18} /></span>
<div><h2 id="billing-bulk-price-title">统一调整倍率</h2><p id="billing-bulk-price-description">整个价格目录 · {target.modelCount} 个模型 · {target.tierCount} 个档位</p></div>
</div>
<button className="billing-modal-close" type="button" onClick={onClose} disabled={saving} aria-label="关闭" title="关闭"><X size={17} /></button>
</div>
<form className="billing-price-edit-form" onSubmit={submit}>
{error ? <div className="billing-alert billing-alert-error" role="alert">{error}</div> : null}
<p className="billing-bulk-price-explanation">保存后会将目录中所有模型和档位设为相同倍率,覆盖之前单独设置的倍率。标准成本不变,之后仍可逐档调整。</p>
<label className="billing-modal-field" htmlFor="billing-bulk-price-multiplier">
<span>统一上浮倍率</span>
<div className="billing-modal-input-wrap">
<input ref={inputRef} id="billing-bulk-price-multiplier" type="number" value={value} onChange={(event) => setValue(event.target.value)} min="1" max="1000" step="any" placeholder={target.currentMultiplier === null ? "请输入新倍率" : undefined} aria-invalid={value.trim() !== "" && !valid} required />
<strong>×</strong>
</div>
<small>当前倍率:{target.currentMultiplier === null ? "各档不同" : `${target.currentMultiplier.toFixed(2)}×`} · 可设置范围 1.00× 至 1000.00×</small>
</label>
<p className="billing-bulk-price-hint">组合报价取命中档位中的最高倍率;统一设置后,单独调整某个档位仍受其他命中档位的倍率影响。</p>
<div className="billing-modal-footer">
<button className="billing-secondary-button" type="button" onClick={onClose} disabled={saving}>取消</button>
<button className="billing-action-button" type="submit" disabled={!valid || saving || !target.ruleIds.length}>{saving ? <Loader2 className="spin" size={15} /> : <Check size={15} />}{saving ? "保存中" : "保存统一倍率"}</button>
</div>
</form>
</section>
</div>;
}
function priceEditStandardUnitPriceFen(rule: BillingPriceRule, target: PriceEditTarget): number {
if (!target.dimensionKey || target.tierValue === undefined) return rule.standardUnitPriceFen;
const dimension = rule.parameterDimensions?.find((item) => item.key === target.dimensionKey);