增加统一倍率调整按钮
This commit is contained in:
1 parent
be655b6569
commit
4195525dc7
23 files changed
+750
-16
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user