同步上游至最新版本并重新应用自定义配置
上游新增功能: - 双模式用户消息队列(串行队列 + 软性限速) - 自定义菜单页面(iframe嵌入 + CSP注入) - 代理URL集中验证与全局fail-fast - 新用户默认订阅设置 - 指纹缓存TTL懒续期机制 - 分组用量分布图表 - 代理密码可见性 + 复制代理URL - 大量bug修复和性能优化 自定义配置保留: - 品牌化:Sub2API → StarFireAPI - 链接:GitHub → anthropic.edu.pl 官网 - docker-compose:starfireapi镜像、端口6580、外部Redis、项目名xinghuoapi - 更新模块禁用:后端始终返回无更新、前端隐藏更新UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -579,7 +579,7 @@
|
||||
{{ t('admin.settings.defaults.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-6 p-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
@@ -613,6 +613,127 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.defaults.defaultSubscriptions') }}
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.defaults.defaultSubscriptionsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="addDefaultSubscription"
|
||||
:disabled="subscriptionGroups.length === 0"
|
||||
>
|
||||
{{ t('admin.settings.defaults.addDefaultSubscription') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.default_subscriptions.length === 0"
|
||||
class="rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
|
||||
>
|
||||
{{ t('admin.settings.defaults.defaultSubscriptionsEmpty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="(item, index) in form.default_subscriptions"
|
||||
:key="`default-sub-${index}`"
|
||||
class="grid grid-cols-1 gap-3 rounded border border-gray-200 p-3 md:grid-cols-[1fr_160px_auto] dark:border-dark-600"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.defaults.subscriptionGroup') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="item.group_id"
|
||||
class="default-sub-group-select"
|
||||
:options="defaultSubscriptionGroupOptions"
|
||||
:placeholder="t('admin.settings.defaults.subscriptionGroup')"
|
||||
>
|
||||
<template #selected="{ option }">
|
||||
<GroupBadge
|
||||
v-if="option"
|
||||
:name="(option as unknown as DefaultSubscriptionGroupOption).label"
|
||||
:platform="(option as unknown as DefaultSubscriptionGroupOption).platform"
|
||||
:subscription-type="(option as unknown as DefaultSubscriptionGroupOption).subscriptionType"
|
||||
:rate-multiplier="(option as unknown as DefaultSubscriptionGroupOption).rate"
|
||||
/>
|
||||
<span v-else class="text-gray-400">
|
||||
{{ t('admin.settings.defaults.subscriptionGroup') }}
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<GroupOptionItem
|
||||
:name="(option as unknown as DefaultSubscriptionGroupOption).label"
|
||||
:platform="(option as unknown as DefaultSubscriptionGroupOption).platform"
|
||||
:subscription-type="(option as unknown as DefaultSubscriptionGroupOption).subscriptionType"
|
||||
:rate-multiplier="(option as unknown as DefaultSubscriptionGroupOption).rate"
|
||||
:description="(option as unknown as DefaultSubscriptionGroupOption).description"
|
||||
:selected="selected"
|
||||
/>
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.defaults.subscriptionValidityDays') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="item.validity_days"
|
||||
type="number"
|
||||
min="1"
|
||||
max="36500"
|
||||
class="input h-[42px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary default-sub-delete-btn w-full text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
@click="removeDefaultSubscription(index)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Code Settings -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.claudeCode.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.claudeCode.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.claudeCode.minVersion') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.min_claude_code_version"
|
||||
type="text"
|
||||
class="input max-w-xs font-mono text-sm"
|
||||
:placeholder="t('admin.settings.claudeCode.minVersionPlaceholder')"
|
||||
pattern="\d+\.\d+\.\d+"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.claudeCode.minVersionHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -711,64 +832,14 @@
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.site.siteLogo') }}
|
||||
</label>
|
||||
<div class="flex items-start gap-6">
|
||||
<!-- Logo Preview -->
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 dark:border-dark-600 dark:bg-dark-800"
|
||||
:class="{ 'border-solid': form.site_logo }"
|
||||
>
|
||||
<img
|
||||
v-if="form.site_logo"
|
||||
:src="form.site_logo"
|
||||
alt="Site Logo"
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
class="h-8 w-8 text-gray-400 dark:text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload Controls -->
|
||||
<div class="flex-1 space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="btn btn-secondary btn-sm cursor-pointer">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleLogoUpload"
|
||||
/>
|
||||
<Icon name="upload" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||
{{ t('admin.settings.site.uploadImage') }}
|
||||
</label>
|
||||
<button
|
||||
v-if="form.site_logo"
|
||||
type="button"
|
||||
@click="form.site_logo = ''"
|
||||
class="btn btn-secondary btn-sm text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
>
|
||||
<Icon name="trash" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||
{{ t('admin.settings.site.remove') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.site.logoHint') }}
|
||||
</p>
|
||||
<p v-if="logoError" class="text-xs text-red-500">{{ logoError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ImageUpload
|
||||
v-model="form.site_logo"
|
||||
mode="image"
|
||||
:upload-label="t('admin.settings.site.uploadImage')"
|
||||
:remove-label="t('admin.settings.site.remove')"
|
||||
:hint="t('admin.settings.site.logoHint')"
|
||||
:max-size="300 * 1024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Home Content -->
|
||||
@@ -991,6 +1062,172 @@
|
||||
{{ t('admin.settings.purchase.iframeWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Integration Docs -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<a
|
||||
href="https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/docs/ADMIN_PAYMENT_INTEGRATION_API.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 hover:underline dark:text-blue-400"
|
||||
download="ADMIN_PAYMENT_INTEGRATION_API.md"
|
||||
>
|
||||
{{ t('admin.settings.purchase.integrationDoc') }}
|
||||
</a>
|
||||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.purchase.integrationDocHint') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sora Client Toggle -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.soraClient.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.soraClient.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-6 p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.soraClient.enabled')
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.soraClient.enabledHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.sora_client_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Menu Items -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.settings.customMenu.title') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.customMenu.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4 p-6">
|
||||
<!-- Existing menu items -->
|
||||
<div
|
||||
v-for="(item, index) in form.custom_menu_items"
|
||||
:key="item.id || index"
|
||||
class="rounded-lg border border-gray-200 p-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.settings.customMenu.itemLabel', { n: index + 1 }) }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Move up -->
|
||||
<button
|
||||
v-if="index > 0"
|
||||
type="button"
|
||||
class="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700"
|
||||
:title="t('admin.settings.customMenu.moveUp')"
|
||||
@click="moveMenuItem(index, -1)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" /></svg>
|
||||
</button>
|
||||
<!-- Move down -->
|
||||
<button
|
||||
v-if="index < form.custom_menu_items.length - 1"
|
||||
type="button"
|
||||
class="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700"
|
||||
:title="t('admin.settings.customMenu.moveDown')"
|
||||
@click="moveMenuItem(index, 1)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
||||
</button>
|
||||
<!-- Delete -->
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-red-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
:title="t('admin.settings.customMenu.remove')"
|
||||
@click="removeMenuItem(index)"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<!-- Label -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.customMenu.name') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="item.label"
|
||||
type="text"
|
||||
class="input text-sm"
|
||||
:placeholder="t('admin.settings.customMenu.namePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Visibility -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.customMenu.visibility') }}
|
||||
</label>
|
||||
<select v-model="item.visibility" class="input text-sm">
|
||||
<option value="user">{{ t('admin.settings.customMenu.visibilityUser') }}</option>
|
||||
<option value="admin">{{ t('admin.settings.customMenu.visibilityAdmin') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- URL (full width) -->
|
||||
<div class="sm:col-span-2">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.customMenu.url') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="item.url"
|
||||
type="url"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.customMenu.urlPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- SVG Icon (full width) -->
|
||||
<div class="sm:col-span-2">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.settings.customMenu.iconSvg') }}
|
||||
</label>
|
||||
<ImageUpload
|
||||
:model-value="item.icon_svg"
|
||||
mode="svg"
|
||||
size="sm"
|
||||
:upload-label="t('admin.settings.customMenu.uploadSvg')"
|
||||
:remove-label="t('admin.settings.customMenu.removeSvg')"
|
||||
@update:model-value="(v: string) => item.icon_svg = v"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add button -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg border-2 border-dashed border-gray-300 py-3 text-sm text-gray-500 transition-colors hover:border-primary-400 hover:text-primary-600 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-500 dark:hover:text-primary-400"
|
||||
@click="addMenuItem"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
|
||||
{{ t('admin.settings.customMenu.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1083,10 +1320,19 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api'
|
||||
import type { SystemSettings, UpdateSettingsRequest } from '@/api/admin/settings'
|
||||
import type {
|
||||
SystemSettings,
|
||||
UpdateSettingsRequest,
|
||||
DefaultSubscriptionSetting
|
||||
} from '@/api/admin/settings'
|
||||
import type { AdminGroup } from '@/types'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||
import GroupOptionItem from '@/components/common/GroupOptionItem.vue'
|
||||
import Toggle from '@/components/common/Toggle.vue'
|
||||
import ImageUpload from '@/components/common/ImageUpload.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
@@ -1099,7 +1345,6 @@ const saving = ref(false)
|
||||
const testingSmtp = ref(false)
|
||||
const sendingTestEmail = ref(false)
|
||||
const testEmailAddress = ref('')
|
||||
const logoError = ref('')
|
||||
|
||||
// Admin API Key 状态
|
||||
const adminApiKeyLoading = ref(true)
|
||||
@@ -1107,6 +1352,7 @@ const adminApiKeyExists = ref(false)
|
||||
const adminApiKeyMasked = ref('')
|
||||
const adminApiKeyOperating = ref(false)
|
||||
const newAdminApiKey = ref('')
|
||||
const subscriptionGroups = ref<AdminGroup[]>([])
|
||||
|
||||
// Stream Timeout 状态
|
||||
const streamTimeoutLoading = ref(true)
|
||||
@@ -1119,6 +1365,16 @@ const streamTimeoutForm = reactive({
|
||||
threshold_window_minutes: 10
|
||||
})
|
||||
|
||||
interface DefaultSubscriptionGroupOption {
|
||||
value: number
|
||||
label: string
|
||||
description: string | null
|
||||
platform: AdminGroup['platform']
|
||||
subscriptionType: AdminGroup['subscription_type']
|
||||
rate: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type SettingsForm = SystemSettings & {
|
||||
smtp_password: string
|
||||
turnstile_secret_key: string
|
||||
@@ -1135,7 +1391,8 @@ const form = reactive<SettingsForm>({
|
||||
totp_encryption_key_configured: false,
|
||||
default_balance: 0,
|
||||
default_concurrency: 1,
|
||||
site_name: 'Sub2API',
|
||||
default_subscriptions: [],
|
||||
site_name: 'StarFireAPI',
|
||||
site_logo: '',
|
||||
site_subtitle: 'Subscription to API Conversion Platform',
|
||||
api_base_url: '',
|
||||
@@ -1145,6 +1402,8 @@ const form = reactive<SettingsForm>({
|
||||
hide_ccs_import_button: false,
|
||||
purchase_subscription_enabled: false,
|
||||
purchase_subscription_url: '',
|
||||
sora_client_enabled: false,
|
||||
custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>,
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_username: '',
|
||||
@@ -1177,9 +1436,22 @@ const form = reactive<SettingsForm>({
|
||||
ops_monitoring_enabled: true,
|
||||
ops_realtime_monitoring_enabled: true,
|
||||
ops_query_mode_default: 'auto',
|
||||
ops_metrics_interval_seconds: 60
|
||||
ops_metrics_interval_seconds: 60,
|
||||
// Claude Code version check
|
||||
min_claude_code_version: ''
|
||||
})
|
||||
|
||||
const defaultSubscriptionGroupOptions = computed<DefaultSubscriptionGroupOption[]>(() =>
|
||||
subscriptionGroups.value.map((group) => ({
|
||||
value: group.id,
|
||||
label: group.name,
|
||||
description: group.description,
|
||||
platform: group.platform,
|
||||
subscriptionType: group.subscription_type,
|
||||
rate: group.rate_multiplier
|
||||
}))
|
||||
)
|
||||
|
||||
// LinuxDo OAuth redirect URL suggestion
|
||||
const linuxdoRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
@@ -1196,42 +1468,37 @@ async function setAndCopyLinuxdoRedirectUrl() {
|
||||
await copyToClipboard(url, t('admin.settings.linuxdo.redirectUrlSetAndCopied'))
|
||||
}
|
||||
|
||||
function handleLogoUpload(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
logoError.value = ''
|
||||
// Custom menu item management
|
||||
function addMenuItem() {
|
||||
form.custom_menu_items.push({
|
||||
id: '',
|
||||
label: '',
|
||||
icon_svg: '',
|
||||
url: '',
|
||||
visibility: 'user',
|
||||
sort_order: form.custom_menu_items.length,
|
||||
})
|
||||
}
|
||||
|
||||
if (!file) return
|
||||
function removeMenuItem(index: number) {
|
||||
form.custom_menu_items.splice(index, 1)
|
||||
// Re-index sort_order
|
||||
form.custom_menu_items.forEach((item, i) => {
|
||||
item.sort_order = i
|
||||
})
|
||||
}
|
||||
|
||||
// Check file size (300KB = 307200 bytes)
|
||||
const maxSize = 300 * 1024
|
||||
if (file.size > maxSize) {
|
||||
logoError.value = t('admin.settings.site.logoSizeError', {
|
||||
size: (file.size / 1024).toFixed(1)
|
||||
})
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// Check file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
logoError.value = t('admin.settings.site.logoTypeError')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
form.site_logo = e.target?.result as string
|
||||
}
|
||||
reader.onerror = () => {
|
||||
logoError.value = t('admin.settings.site.logoReadError')
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// Reset input
|
||||
input.value = ''
|
||||
function moveMenuItem(index: number, direction: -1 | 1) {
|
||||
const targetIndex = index + direction
|
||||
if (targetIndex < 0 || targetIndex >= form.custom_menu_items.length) return
|
||||
const items = form.custom_menu_items
|
||||
const temp = items[index]
|
||||
items[index] = items[targetIndex]
|
||||
items[targetIndex] = temp
|
||||
// Re-index sort_order
|
||||
items.forEach((item, i) => {
|
||||
item.sort_order = i
|
||||
})
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
@@ -1239,6 +1506,14 @@ async function loadSettings() {
|
||||
try {
|
||||
const settings = await adminAPI.settings.getSettings()
|
||||
Object.assign(form, settings)
|
||||
form.default_subscriptions = Array.isArray(settings.default_subscriptions)
|
||||
? settings.default_subscriptions
|
||||
.filter((item) => item.group_id > 0 && item.validity_days > 0)
|
||||
.map((item) => ({
|
||||
group_id: item.group_id,
|
||||
validity_days: item.validity_days
|
||||
}))
|
||||
: []
|
||||
form.smtp_password = ''
|
||||
form.turnstile_secret_key = ''
|
||||
form.linuxdo_connect_client_secret = ''
|
||||
@@ -1251,9 +1526,60 @@ async function loadSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubscriptionGroups() {
|
||||
try {
|
||||
const groups = await adminAPI.groups.getAll()
|
||||
subscriptionGroups.value = groups.filter(
|
||||
(group) => group.subscription_type === 'subscription' && group.status === 'active'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscription groups:', error)
|
||||
subscriptionGroups.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function addDefaultSubscription() {
|
||||
if (subscriptionGroups.value.length === 0) return
|
||||
const existing = new Set(form.default_subscriptions.map((item) => item.group_id))
|
||||
const candidate = subscriptionGroups.value.find((group) => !existing.has(group.id))
|
||||
if (!candidate) return
|
||||
form.default_subscriptions.push({
|
||||
group_id: candidate.id,
|
||||
validity_days: 30
|
||||
})
|
||||
}
|
||||
|
||||
function removeDefaultSubscription(index: number) {
|
||||
form.default_subscriptions.splice(index, 1)
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
saving.value = true
|
||||
try {
|
||||
const normalizedDefaultSubscriptions = form.default_subscriptions
|
||||
.filter((item) => item.group_id > 0 && item.validity_days > 0)
|
||||
.map((item: DefaultSubscriptionSetting) => ({
|
||||
group_id: item.group_id,
|
||||
validity_days: Math.min(36500, Math.max(1, Math.floor(item.validity_days)))
|
||||
}))
|
||||
|
||||
const seenGroupIDs = new Set<number>()
|
||||
const duplicateDefaultSubscription = normalizedDefaultSubscriptions.find((item) => {
|
||||
if (seenGroupIDs.has(item.group_id)) {
|
||||
return true
|
||||
}
|
||||
seenGroupIDs.add(item.group_id)
|
||||
return false
|
||||
})
|
||||
if (duplicateDefaultSubscription) {
|
||||
appStore.showError(
|
||||
t('admin.settings.defaults.defaultSubscriptionsDuplicate', {
|
||||
groupId: duplicateDefaultSubscription.group_id
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const payload: UpdateSettingsRequest = {
|
||||
registration_enabled: form.registration_enabled,
|
||||
email_verify_enabled: form.email_verify_enabled,
|
||||
@@ -1263,6 +1589,7 @@ async function saveSettings() {
|
||||
totp_enabled: form.totp_enabled,
|
||||
default_balance: form.default_balance,
|
||||
default_concurrency: form.default_concurrency,
|
||||
default_subscriptions: normalizedDefaultSubscriptions,
|
||||
site_name: form.site_name,
|
||||
site_logo: form.site_logo,
|
||||
site_subtitle: form.site_subtitle,
|
||||
@@ -1273,6 +1600,8 @@ async function saveSettings() {
|
||||
hide_ccs_import_button: form.hide_ccs_import_button,
|
||||
purchase_subscription_enabled: form.purchase_subscription_enabled,
|
||||
purchase_subscription_url: form.purchase_subscription_url,
|
||||
sora_client_enabled: form.sora_client_enabled,
|
||||
custom_menu_items: form.custom_menu_items,
|
||||
smtp_host: form.smtp_host,
|
||||
smtp_port: form.smtp_port,
|
||||
smtp_username: form.smtp_username,
|
||||
@@ -1293,7 +1622,8 @@ async function saveSettings() {
|
||||
fallback_model_gemini: form.fallback_model_gemini,
|
||||
fallback_model_antigravity: form.fallback_model_antigravity,
|
||||
enable_identity_patch: form.enable_identity_patch,
|
||||
identity_patch_prompt: form.identity_patch_prompt
|
||||
identity_patch_prompt: form.identity_patch_prompt,
|
||||
min_claude_code_version: form.min_claude_code_version
|
||||
}
|
||||
const updated = await adminAPI.settings.updateSettings(payload)
|
||||
Object.assign(form, updated)
|
||||
@@ -1459,7 +1789,18 @@ async function saveStreamTimeoutSettings() {
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
loadSubscriptionGroups()
|
||||
loadAdminApiKey()
|
||||
loadStreamTimeoutSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.default-sub-group-select :deep(.select-trigger) {
|
||||
@apply h-[42px];
|
||||
}
|
||||
|
||||
.default-sub-delete-btn {
|
||||
@apply h-[42px];
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user