feat: add OpenAI image generation controls

This commit is contained in:
2ue
2026-05-05 03:26:54 +08:00
parent 4de28fec8c
commit 6faa344916
85 changed files with 6086 additions and 568 deletions
+186 -2
View File
@@ -666,6 +666,40 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t("admin.groups.imagePricing.description") }}
</p>
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.allow_image_generation"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.image_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.independentMultiplier") }}
</label>
</div>
<div
v-if="createForm.image_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t("admin.groups.imagePricing.imageMultiplier")
}}</label>
<input
v-model.number="createForm.image_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">1K ($)</label>
@@ -701,6 +735,22 @@
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.groups.imagePricing.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("admin.groups.imagePricing.finalPricePreview") }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in createImageFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
@@ -1801,6 +1851,40 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t("admin.groups.imagePricing.description") }}
</p>
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.allow_image_generation"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.image_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.independentMultiplier") }}
</label>
</div>
<div
v-if="editForm.image_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t("admin.groups.imagePricing.imageMultiplier")
}}</label>
<input
v-model.number="editForm.image_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">1K ($)</label>
@@ -1836,6 +1920,22 @@
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.groups.imagePricing.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("admin.groups.imagePricing.finalPricePreview") }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in editImageFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
@@ -3009,7 +3109,10 @@ const createForm = reactive({
daily_limit_usd: null as number | null,
weekly_limit_usd: null as number | null,
monthly_limit_usd: null as number | null,
// 图片生成计费配置(仅 antigravity 平台使用)
// 图片生成计费配置
allow_image_generation: false,
image_rate_independent: false,
image_rate_multiplier: 1,
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
@@ -3291,7 +3394,10 @@ const editForm = reactive({
daily_limit_usd: null as number | null,
weekly_limit_usd: null as number | null,
monthly_limit_usd: null as number | null,
// 图片生成计费配置(仅 antigravity 平台使用)
// 图片生成计费配置
allow_image_generation: false,
image_rate_independent: false,
image_rate_multiplier: 1,
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
@@ -3321,6 +3427,62 @@ const editForm = reactive({
rpm_limit: 0 as number,
});
type ImagePricingFormState = {
rate_multiplier: number;
image_rate_independent: boolean;
image_rate_multiplier: number;
image_price_1k: number | string | null;
image_price_2k: number | string | null;
image_price_4k: 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 normalizePreviewNumber = (value: number | string | null | undefined, fallback = 0) => {
if (value === null || value === undefined || value === "") {
return fallback;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const formatImagePricePreview = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === "") {
return t("admin.groups.imagePricing.notConfigured");
}
const price = Number(value);
if (!Number.isFinite(price) || price < 0) {
return t("admin.groups.imagePricing.notConfigured");
}
return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`;
};
const buildImageFinalPricePreview = (form: ImagePricingFormState) => {
const multiplier = form.image_rate_independent
? normalizePreviewNumber(form.image_rate_multiplier, 1)
: normalizePreviewNumber(form.rate_multiplier, 1);
return imagePricingTiers.map((tier) => {
const basePrice = normalizePreviewNumber(form[tier.key]);
return {
label: tier.label,
value: basePrice > 0
? formatImagePricePreview(basePrice * multiplier)
: t("admin.groups.imagePricing.notConfigured"),
};
});
};
const createImageFinalPricePreview = computed(() =>
buildImageFinalPricePreview(createForm),
);
const editImageFinalPricePreview = computed(() =>
buildImageFinalPricePreview(editForm),
);
// 根据分组类型返回不同的删除确认消息
const deleteConfirmMessage = computed(() => {
if (!deletingGroup.value) {
@@ -3479,6 +3641,9 @@ const closeCreateModal = () => {
createForm.daily_limit_usd = null;
createForm.weekly_limit_usd = null;
createForm.monthly_limit_usd = null;
createForm.allow_image_generation = false;
createForm.image_rate_independent = false;
createForm.image_rate_multiplier = 1;
createForm.image_price_1k = null;
createForm.image_price_2k = null;
createForm.image_price_4k = null;
@@ -3513,6 +3678,16 @@ const normalizeOptionalLimit = (
return Number.isFinite(value) && value > 0 ? value : null;
};
const normalizeImageRateMultiplier = (
value: number | string | null | undefined,
): number => {
if (value === null || value === undefined || value === "") {
return 1;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 1;
};
const handleCreateGroup = async () => {
if (!createForm.name.trim()) {
appStore.showError(t("admin.groups.nameRequired"));
@@ -3551,6 +3726,9 @@ const handleCreateGroup = async () => {
requestData.daily_limit_usd = emptyToNull(requestData.daily_limit_usd);
requestData.weekly_limit_usd = emptyToNull(requestData.weekly_limit_usd);
requestData.monthly_limit_usd = emptyToNull(requestData.monthly_limit_usd);
requestData.image_rate_multiplier = normalizeImageRateMultiplier(
requestData.image_rate_multiplier,
);
await adminAPI.groups.create(requestData);
appStore.showSuccess(t("admin.groups.groupCreated"));
closeCreateModal();
@@ -3582,6 +3760,9 @@ const handleEdit = async (group: AdminGroup) => {
editForm.daily_limit_usd = group.daily_limit_usd;
editForm.weekly_limit_usd = group.weekly_limit_usd;
editForm.monthly_limit_usd = group.monthly_limit_usd;
editForm.allow_image_generation = group.allow_image_generation ?? false;
editForm.image_rate_independent = group.image_rate_independent ?? false;
editForm.image_rate_multiplier = group.image_rate_multiplier ?? 1;
editForm.image_price_1k = group.image_price_1k;
editForm.image_price_2k = group.image_price_2k;
editForm.image_price_4k = group.image_price_4k;
@@ -3676,6 +3857,9 @@ const handleUpdateGroup = async () => {
payload.daily_limit_usd = emptyToNull(payload.daily_limit_usd);
payload.weekly_limit_usd = emptyToNull(payload.weekly_limit_usd);
payload.monthly_limit_usd = emptyToNull(payload.monthly_limit_usd);
payload.image_rate_multiplier = normalizeImageRateMultiplier(
payload.image_rate_multiplier,
);
await adminAPI.groups.update(editingGroup.value.id, payload);
appStore.showSuccess(t("admin.groups.groupUpdated"));
closeEditModal();