fix: split Grok image and video pricing

This commit is contained in:
Heatherm Huang
2026-07-08 13:50:49 +08:00
parent 890cc2bb98
commit 4d702e3234
55 changed files with 2758 additions and 72 deletions
+268 -2
View File
@@ -787,7 +787,7 @@
</div>
</div>
<!-- 图片/视频生成计费配置 -->
<!-- 图片生成计费配置 -->
<div
v-if="supportsImagePricingPlatform(createForm.platform)"
class="border-t pt-4"
@@ -939,6 +939,98 @@
</p>
</div>
<!-- 视频生成计费配置(仅 Grok 平台) -->
<div
v-if="supportsVideoPricingPlatform(createForm.platform)"
class="border-t pt-4"
>
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t(videoPricingI18nKey("title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t(videoPricingI18nKey("description")) }}
</p>
<div class="mb-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.video_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t(videoPricingI18nKey("independentMultiplier")) }}
</label>
</div>
<div
v-if="createForm.video_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t(videoPricingI18nKey("videoMultiplier"))
}}</label>
<input
v-model.number="createForm.video_rate_multiplier"
type="number"
step="0.0001"
min="0"
class="input"
placeholder="1"
/>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="input-label">480p ($)</label>
<input
v-model.number="createForm.video_price_480p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
<div>
<label class="input-label">720p ($)</label>
<input
v-model.number="createForm.video_price_720p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
<div>
<label class="input-label">1080p ($)</label>
<input
v-model.number="createForm.video_price_1080p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t(videoPricingI18nKey("modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t(videoPricingI18nKey("finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in createVideoFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
<div v-if="createForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -2174,7 +2266,7 @@
</div>
</div>
<!-- 图片/视频生成计费配置 -->
<!-- 图片生成计费配置 -->
<div
v-if="supportsImagePricingPlatform(editForm.platform)"
class="border-t pt-4"
@@ -2326,6 +2418,98 @@
</p>
</div>
<!-- 视频生成计费配置(仅 Grok 平台) -->
<div
v-if="supportsVideoPricingPlatform(editForm.platform)"
class="border-t pt-4"
>
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t(videoPricingI18nKey("title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t(videoPricingI18nKey("description")) }}
</p>
<div class="mb-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.video_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t(videoPricingI18nKey("independentMultiplier")) }}
</label>
</div>
<div
v-if="editForm.video_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t(videoPricingI18nKey("videoMultiplier"))
}}</label>
<input
v-model.number="editForm.video_rate_multiplier"
type="number"
step="0.0001"
min="0"
class="input"
placeholder="1"
/>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="input-label">480p ($)</label>
<input
v-model.number="editForm.video_price_480p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
<div>
<label class="input-label">720p ($)</label>
<input
v-model.number="editForm.video_price_720p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
<div>
<label class="input-label">1080p ($)</label>
<input
v-model.number="editForm.video_price_1080p"
type="number"
step="0.001"
min="0"
class="input"
placeholder="0.201"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t(videoPricingI18nKey("modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t(videoPricingI18nKey("finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in editVideoFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
<div v-if="editForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -3315,6 +3499,8 @@ import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModel
import {
imagePricingI18nKey,
supportsImagePricingPlatform,
supportsVideoPricingPlatform,
videoPricingI18nKey,
} from "./groupsImagePricing";
const { t } = useI18n();
@@ -3657,6 +3843,12 @@ const createForm = reactive({
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
// 视频生成计费配置(仅 Grok 平台)
video_rate_independent: false,
video_rate_multiplier: 1,
video_price_480p: null as number | null,
video_price_720p: null as number | null,
video_price_1080p: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
@@ -3996,6 +4188,12 @@ const editForm = reactive({
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
// 视频生成计费配置(仅 Grok 平台)
video_rate_independent: false,
video_rate_multiplier: 1,
video_price_480p: null as number | null,
video_price_720p: null as number | null,
video_price_1080p: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
@@ -4045,12 +4243,27 @@ type ImagePricingFormState = {
peak_rate_multiplier: number;
};
type VideoPricingFormState = {
rate_multiplier: number;
video_rate_independent: boolean;
video_rate_multiplier: number;
video_price_480p: number | string | null;
video_price_720p: number | string | null;
video_price_1080p: number | string | null;
};
const imagePricingTiers = [
{ key: "image_price_1k", label: "1K" },
{ key: "image_price_2k", label: "2K" },
{ key: "image_price_4k", label: "4K" },
] as const;
const videoPricingTiers = [
{ key: "video_price_480p", label: "480p" },
{ key: "video_price_720p", label: "720p" },
{ key: "video_price_1080p", label: "1080p" },
] as const;
const normalizePreviewNumber = (value: number | string | null | undefined, fallback = 0) => {
if (value === null || value === undefined || value === "") {
return fallback;
@@ -4070,6 +4283,17 @@ const formatImagePricePreview = (value: number | string | null | undefined) => {
return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`;
};
const formatVideoPricePreview = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === "") {
return t("admin.groups.videoPricing.notConfigured");
}
const price = Number(value);
if (!Number.isFinite(price) || price < 0) {
return t("admin.groups.videoPricing.notConfigured");
}
return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`;
};
const buildImageFinalPricePreview = (form: ImagePricingFormState) => {
const imageMultiplier = form.image_rate_independent
? normalizePreviewNumber(form.image_rate_multiplier, 1)
@@ -4086,12 +4310,33 @@ const buildImageFinalPricePreview = (form: ImagePricingFormState) => {
});
};
const buildVideoFinalPricePreview = (form: VideoPricingFormState) => {
const multiplier = form.video_rate_independent
? normalizePreviewNumber(form.video_rate_multiplier, 1)
: normalizePreviewNumber(form.rate_multiplier, 1);
return videoPricingTiers.map((tier) => {
const basePrice = normalizePreviewNumber(form[tier.key]);
return {
label: tier.label,
value: basePrice > 0
? formatVideoPricePreview(basePrice * multiplier)
: t("admin.groups.videoPricing.notConfigured"),
};
});
};
const createImageFinalPricePreview = computed(() =>
buildImageFinalPricePreview(createForm),
);
const editImageFinalPricePreview = computed(() =>
buildImageFinalPricePreview(editForm),
);
const createVideoFinalPricePreview = computed(() =>
buildVideoFinalPricePreview(createForm),
);
const editVideoFinalPricePreview = computed(() =>
buildVideoFinalPricePreview(editForm),
);
const resetDisabledBatchImagePricing = (
form: Pick<
@@ -4293,6 +4538,11 @@ const closeCreateModal = () => {
createForm.image_price_1k = null;
createForm.image_price_2k = null;
createForm.image_price_4k = null;
createForm.video_rate_independent = false;
createForm.video_rate_multiplier = 1;
createForm.video_price_480p = null;
createForm.video_price_720p = null;
createForm.video_price_1080p = null;
createForm.peak_rate_enabled = false;
createForm.peak_start = "";
createForm.peak_end = "";
@@ -4393,6 +4643,9 @@ const handleCreateGroup = async () => {
requestData.batch_image_hold_multiplier = normalizeRateMultiplier(
requestData.batch_image_hold_multiplier,
);
requestData.video_rate_multiplier = normalizeRateMultiplier(
requestData.video_rate_multiplier,
);
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
requestData.peak_start = createForm.peak_start;
requestData.peak_end = createForm.peak_end;
@@ -4441,6 +4694,11 @@ const handleEdit = async (group: AdminGroup) => {
editForm.image_price_1k = group.image_price_1k;
editForm.image_price_2k = group.image_price_2k;
editForm.image_price_4k = group.image_price_4k;
editForm.video_rate_independent = group.video_rate_independent ?? false;
editForm.video_rate_multiplier = group.video_rate_multiplier ?? 1;
editForm.video_price_480p = group.video_price_480p;
editForm.video_price_720p = group.video_price_720p;
editForm.video_price_1080p = group.video_price_1080p;
editForm.peak_rate_enabled = group.peak_rate_enabled ?? false;
editForm.peak_start = group.peak_start ?? "";
editForm.peak_end = group.peak_end ?? "";
@@ -4493,6 +4751,11 @@ const closeEditModal = () => {
editForm.peak_start = "";
editForm.peak_end = "";
editForm.peak_rate_multiplier = 1.0;
editForm.video_rate_independent = false;
editForm.video_rate_multiplier = 1;
editForm.video_price_480p = null;
editForm.video_price_720p = null;
editForm.video_price_1080p = null;
resetMessagesDispatchFormState(editForm);
resetModelsListState(editModelsListState);
};
@@ -4558,6 +4821,9 @@ const handleUpdateGroup = async () => {
payload.batch_image_hold_multiplier = normalizeRateMultiplier(
payload.batch_image_hold_multiplier,
);
payload.video_rate_multiplier = normalizeRateMultiplier(
payload.video_rate_multiplier,
);
payload.peak_rate_enabled = editForm.peak_rate_enabled;
payload.peak_start = editForm.peak_start;
payload.peak_end = editForm.peak_end;
@@ -4,24 +4,29 @@ import {
imagePricingPlatforms,
imagePricingI18nKey,
supportsImagePricingPlatform,
supportsVideoPricingPlatform,
videoPricingI18nKey,
} from "../groupsImagePricing";
describe("groups image pricing platform support", () => {
it("includes Grok media groups", () => {
it("includes Grok image groups", () => {
expect(supportsImagePricingPlatform("grok")).toBe(true);
expect(imagePricingPlatforms.has("grok")).toBe(true);
});
it("enables video pricing controls for Grok only", () => {
expect(supportsVideoPricingPlatform("grok")).toBe(true);
expect(supportsVideoPricingPlatform("openai")).toBe(false);
});
it("keeps non-media group platforms out of the image pricing controls", () => {
expect(supportsImagePricingPlatform("anthropic")).toBe(false);
});
it("uses media pricing copy for Grok groups only", () => {
it("keeps image and video pricing copy separate", () => {
expect(imagePricingI18nKey("grok", "title")).toBe(
"admin.groups.mediaPricing.title",
);
expect(imagePricingI18nKey("openai", "title")).toBe(
"admin.groups.imagePricing.title",
);
expect(videoPricingI18nKey("title")).toBe("admin.groups.videoPricing.title");
});
});
@@ -8,7 +8,11 @@ export const imagePricingPlatforms = new Set([
export const supportsImagePricingPlatform = (platform: string): boolean =>
imagePricingPlatforms.has(platform);
export const imagePricingI18nKey = (platform: string, key: string): string =>
platform === "grok"
? `admin.groups.mediaPricing.${key}`
: `admin.groups.imagePricing.${key}`;
export const supportsVideoPricingPlatform = (platform: string): boolean =>
platform === "grok";
export const imagePricingI18nKey = (_platform: string, key: string): string =>
`admin.groups.imagePricing.${key}`;
export const videoPricingI18nKey = (key: string): string =>
`admin.groups.videoPricing.${key}`;