diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 5bf6881dd..39b0deca8 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -192,6 +192,33 @@ const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai"; const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024; const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config"; const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const; +const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record< + (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number], + number +> = { + "480p": 1, + "720p": 1.5, + "1080p": 2, +}; + +function formatCompactNumber(value: number, fractionDigits = 2) { + if (!Number.isFinite(value)) return "0"; + const fixed = value.toFixed(fractionDigits); + return fixed.replace(/\.?0+$/, ""); +} + +function normalizeAdminMultiplier(value: unknown, fallback = 1) { + const parsedValue = Number(value); + if (!Number.isFinite(parsedValue) || parsedValue <= 0) return fallback; + return Math.min(20, Math.max(0.01, Number(parsedValue.toFixed(2)))); +} + +function roundCreditCost(value: unknown) { + const parsedValue = Number(value); + return Number.isFinite(parsedValue) && parsedValue > 0 + ? Math.max(1, Math.round(parsedValue)) + : 0; +} function createEmptyCanvasSkillTag(): CanvasSkillTag { return { @@ -448,17 +475,72 @@ function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) { }; } +function getVideoReferenceDurationCost(config: ImageGenConfig | undefined) { + const durationCostEntries = Object.entries( + config?.creditCostPerVideoByDuration || {} + ) + .map(([durationKey, cost]) => ({ + duration: Number(durationKey), + cost: roundCreditCost(cost), + })) + .filter((item) => Number.isFinite(item.duration) && item.cost > 0) + .sort((left, right) => left.duration - right.duration); + return ( + durationCostEntries[durationCostEntries.length - 1]?.cost || + roundCreditCost(config?.creditCostPerVideo) || + recommendedVideoDurationReferenceCosts[ + String( + recommendedVideoDurationOptions[recommendedVideoDurationOptions.length - 1] + ) + ] || + 1 + ); +} + +function getVideoResolutionMultiplier( + config: ImageGenConfig | undefined, + resolution: (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number] +) { + const referenceCost = getVideoReferenceDurationCost(config); + const rawCost = + config?.creditCostPerVideoByResolution?.[resolution] ?? + referenceCost * DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]; + if (referenceCost <= 0) { + return DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]; + } + return normalizeAdminMultiplier( + Number(rawCost) / referenceCost, + DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution] + ); +} + +function getEnhanceProScaleMultiplier( + pricing: PointsSystemConfig["enhanceProPricing"], + factorKey: string, + cost: number +) { + const baseCost = roundCreditCost(pricing.fixedCost); + if (baseCost <= 0) return normalizeAdminMultiplier(factorKey); + return normalizeAdminMultiplier(Number(cost) / baseCost, normalizeAdminMultiplier(factorKey)); +} + function applyRecommendedEnhanceProPricing( pricing: PointsSystemConfig["enhanceProPricing"] ): PointsSystemConfig["enhanceProPricing"] { const recommended = defaultPointsSystemConfig.enhanceProPricing; + const baseCost = roundCreditCost(pricing.fixedCost || recommended.fixedCost); return { ...pricing, chargeMode: recommended.chargeMode, - fixedCost: recommended.fixedCost, + fixedCost: baseCost, chargeOnlyWhenUpscaling: recommended.chargeOnlyWhenUpscaling, batchSizeMultiplierEnabled: recommended.batchSizeMultiplierEnabled, - scaleFactorCosts: { ...recommended.scaleFactorCosts }, + scaleFactorCosts: Object.fromEntries( + Object.keys(recommended.scaleFactorCosts).map((factorKey) => [ + factorKey, + roundCreditCost(baseCost * normalizeAdminMultiplier(factorKey)), + ]) + ), modelMultipliers: { ...recommended.modelMultipliers }, }; } @@ -2563,6 +2645,9 @@ export function AdminSettingsClient() { const configuredVideoDurationOptions = getConfiguredVideoDurationOptions( modelForm.imageGenConfig ); + const videoReferenceDurationCost = getVideoReferenceDurationCost( + modelForm.imageGenConfig + ); const modelKindHint = (selectedModelProviderType === "runway" || selectedModelProviderType === "krea") && !modelForm.modelId.trim() @@ -3002,21 +3087,53 @@ export function AdminSettingsClient() { }); }; - const updateEnhanceProScaleFactorCost = ( - factorKey: string, - nextCost: number - ) => { + const updateEnhanceProFixedCost = (nextFixedCost: number) => { setSiteSettings((prev) => { const config = normalizePointsSystemConfig(prev.pointsSystemConfig); + const pricing = config.enhanceProPricing; + const fixedCost = roundCreditCost(nextFixedCost); + const scaleFactorCosts = Object.fromEntries( + Object.entries(pricing.scaleFactorCosts).map(([factorKey, cost]) => { + const multiplier = getEnhanceProScaleMultiplier( + pricing, + factorKey, + Number(cost) + ); + return [factorKey, roundCreditCost(fixedCost * multiplier)]; + }) + ); return { ...prev, pointsSystemConfig: { ...config, enhanceProPricing: { - ...config.enhanceProPricing, + ...pricing, + fixedCost, + scaleFactorCosts, + }, + }, + }; + }); + }; + + const updateEnhanceProScaleFactorMultiplier = ( + factorKey: string, + nextMultiplier: number + ) => { + setSiteSettings((prev) => { + const config = normalizePointsSystemConfig(prev.pointsSystemConfig); + const pricing = config.enhanceProPricing; + const fixedCost = roundCreditCost(pricing.fixedCost); + const multiplier = normalizeAdminMultiplier(nextMultiplier, Number(factorKey) || 1); + return { + ...prev, + pointsSystemConfig: { + ...config, + enhanceProPricing: { + ...pricing, scaleFactorCosts: { - ...config.enhanceProPricing.scaleFactorCosts, - [factorKey]: Math.max(0, Math.round(nextCost) || 0), + ...pricing.scaleFactorCosts, + [factorKey]: roundCreditCost(fixedCost * multiplier), }, }, }, @@ -8531,7 +8648,7 @@ export function AdminSettingsClient() { 图像增强 Pro 积分规则
- 默认按基础 20 积分计算;启用放大后按放大倍数折算,例如 1.25x 为 25 积分、2x 为 40 积分。 + 按基础积分 × 档位倍率计算;启用放大后可为 1x、1.25x、1.5x、2x、3x、4x 分别设置倍率。
@@ -8633,19 +8750,9 @@ export function AdminSettingsClient() { type="number" value={pointsSystemConfig.enhanceProPricing.fixedCost} onChange={(e) => - setSiteSettings({ - ...siteSettings, - pointsSystemConfig: { - ...pointsSystemConfig, - enhanceProPricing: { - ...pointsSystemConfig.enhanceProPricing, - fixedCost: Math.max( - 0, - Number.parseInt(e.target.value || "0", 10) || 0 - ), - }, - }, - }) + updateEnhanceProFixedCost( + Number.parseInt(e.target.value || "0", 10) || 0 + ) } />
@@ -8681,34 +8788,52 @@ export function AdminSettingsClient() {
- +
- “放大倍率”计费模式下优先使用精确档位;未配置的小数倍率按基础价 × 倍数自动计算。 + “放大倍率”计费模式下按基础积分 × 档位倍率计算;未配置的小数倍率也会按基础积分 × 倍率自动估算。
-
+
{Object.entries( pointsSystemConfig.enhanceProPricing.scaleFactorCosts ) .sort(([left], [right]) => Number(left) - Number(right)) - .map(([factorKey, cost]) => ( -
- - - updateEnhanceProScaleFactorCost( - factorKey, - Number.parseInt(e.target.value || "0", 10) || 0 - ) - } - /> -
- ))} + .map(([factorKey, cost]) => { + const multiplier = getEnhanceProScaleMultiplier( + pointsSystemConfig.enhanceProPricing, + factorKey, + cost + ); + const exactCost = + roundCreditCost( + pointsSystemConfig.enhanceProPricing.fixedCost + ) * multiplier; + return ( +
+ + + updateEnhanceProScaleFactorMultiplier( + factorKey, + Number(e.target.value || factorKey) + ) + } + /> +
+ 该档积分:{formatCompactNumber(exactCost)} + {roundCreditCost(exactCost) !== exactCost + ? `,扣 ${roundCreditCost(exactCost)}` + : ""} +
+
+ ); + })}
@@ -12319,8 +12444,8 @@ export function AdminSettingsClient() { 推荐视频积分规则

- 按 5/10/15 秒 = 1/1.5/2 倍,480p/720p/1080p 基础价 = - 100/200/250 积分计算。 + 按 5/10/15 秒 = 165/350/600 积分,480p/720p/1080p = + 600 × 1/1.5/2 倍计算。