Files
FileHost/app/pages/settings.vue
T
O96u 0d58d38d50
Publish Docker image / build-and-push (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
release: PicHost v1.0.1
Show app version in admin settings footer and fix mobile upload
preferences panel clipping.
2026-08-22 18:33:46 +08:00

361 lines
11 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
type SettingSource = 'env' | 'db' | 'none'
type WebpQualitySource = 'env' | 'db' | 'default'
interface SettingsResponse {
apiUploadToken: string
tokenSource: SettingSource
envTokenOverride: boolean
webpQuality: number
webpQualitySource: WebpQualitySource
allowedRefererHosts: string
refererSource: SettingSource
refererEnvFallback: string
imageBaseUrl: string
imageBaseUrlConfigured: string
imageBaseUrlSource: SettingSource
allowRegistration: boolean
appVersion: string
}
const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus, isAdmin } = useAuth()
const { defaultFolder, loadPreferences } = useUploadPreferences()
const toast = useToast()
const settings = ref<SettingsResponse | null>(null)
const folderOptions = ref<string[]>(['images'])
const savingServer = ref(false)
const refererDraft = ref('')
const imageBaseUrlDraft = ref('')
const allowRegistrationDraft = ref(false)
const folderSelectItems = computed(() =>
folderOptions.value.map(folder => ({ label: folder, value: folder }))
)
const hasServerChanges = computed(() => {
if (!settings.value) return false
return refererDraft.value !== settings.value.allowedRefererHosts
|| imageBaseUrlDraft.value !== settings.value.imageBaseUrlConfigured
|| allowRegistrationDraft.value !== settings.value.allowRegistration
})
function sourceBadge(source: SettingSource | WebpQualitySource) {
switch (source) {
case 'env':
return { label: '.env', color: 'warning' as const }
case 'db':
return { label: '已保存', color: 'success' as const }
case 'default':
return { label: '默认', color: 'neutral' as const }
default:
return { label: '未设置', color: 'neutral' as const }
}
}
function tokenSourceText(source: SettingSource) {
switch (source) {
case 'env':
return '环境变量'
case 'db':
return '后台生成'
default:
return '未配置'
}
}
function errorStatus(error: unknown): number {
if (typeof error === 'object' && error !== null && 'statusCode' in error) {
return (error as { statusCode: number }).statusCode
}
return 0
}
function applySettings(data: SettingsResponse) {
settings.value = data
refererDraft.value = data.allowedRefererHosts
imageBaseUrlDraft.value = data.imageBaseUrlConfigured
allowRegistrationDraft.value = data.allowRegistration
}
async function patchSettings(body: Record<string, unknown>) {
const data = await $fetch<SettingsResponse>('/api/settings', {
method: 'PATCH',
credentials: 'include',
body
})
applySettings(data)
return data
}
function handlePatchError(error: unknown, fallback: string) {
handleAuthError(error)
if (!isAuthenticated.value) return
if (errorStatus(error) === 409) {
toast.add({ title: '该项已由环境变量覆盖,无法在后台修改', color: 'warning' })
void loadSettings()
return
}
toast.add({ title: fallback, color: 'error' })
}
async function saveServerSettings() {
if (!isAuthenticated.value || !hasServerChanges.value) return
savingServer.value = true
try {
await patchSettings({
allowedRefererHosts: refererDraft.value,
imageBaseUrl: imageBaseUrlDraft.value,
allowRegistration: allowRegistrationDraft.value
})
toast.add({ title: '服务器配置已保存', color: 'success' })
} catch (error: unknown) {
handlePatchError(error, '保存服务器配置失败')
} finally {
savingServer.value = false
}
}
async function loadFolders() {
const data = await $fetch<{ folders: string[] }>('/api/folders', {
credentials: 'include'
})
folderOptions.value = [...data.folders]
if (!folderOptions.value.includes(defaultFolder.value)) {
defaultFolder.value = folderOptions.value[0] ?? 'images'
}
}
async function loadSettings() {
try {
const data = await $fetch<SettingsResponse>('/api/settings', {
credentials: 'include'
})
applySettings(data)
} catch (error: unknown) {
handleAuthError(error)
if (isAuthenticated.value) {
toast.add({ title: '加载设置失败', color: 'error' })
}
}
}
async function loadPage() {
if (!isAdmin.value) {
await navigateTo('/')
return
}
loadPreferences()
await Promise.all([loadSettings(), loadFolders()])
}
onMounted(async () => {
const status = await fetchStatus()
if (!status.initialized && !status.legacyMode) {
await navigateTo('/setup')
return
}
await checkSession()
if (isAuthenticated.value && !isAdmin.value) {
await navigateTo('/')
}
})
watch(isAuthenticated, async (authed) => {
if (authed) {
if (!isAdmin.value) {
await navigateTo('/')
return
}
await nextTick()
await loadPage()
} else {
settings.value = null
}
})
</script>
<template>
<div class="min-h-screen">
<div
v-if="isChecking"
class="flex min-h-screen items-center justify-center"
>
<div class="flex flex-col items-center gap-3 text-muted">
<UIcon
name="i-lucide-loader-circle"
class="size-8 animate-spin"
/>
<p class="text-sm">
正在验证登录状态…
</p>
</div>
</div>
<AdminLoginGate v-else-if="!isAuthenticated" />
<AppShell v-else-if="isAdmin">
<section class="overflow-hidden rounded-2xl border border-default bg-elevated shadow-sm">
<UploadPreferencesPanel embedded />
</section>
<section class="overflow-hidden rounded-2xl border border-default bg-elevated shadow-sm">
<div class="flex items-start gap-2 border-b border-default px-5 py-4 sm:px-6">
<UIcon
name="i-lucide-server"
class="mt-0.5 size-5 shrink-0 text-primary"
/>
<div class="min-w-0">
<h1 class="text-base font-semibold">
系统设置
</h1>
<p class="mt-0.5 text-xs text-muted">
保存后写入数据库;环境变量优先级更高
</p>
</div>
</div>
<div
v-if="settings"
class="p-5 sm:p-6"
>
<div class="grid gap-5 sm:grid-cols-3 sm:gap-6">
<div class="space-y-4">
<h3 class="text-sm font-semibold">
上传与账号
</h3>
<div class="space-y-2">
<label class="text-sm">默认上传目录</label>
<p class="text-xs leading-relaxed text-muted">
保存在本机浏览器,仅影响您的上传目标目录
</p>
<USelect
v-model="defaultFolder"
:items="folderSelectItems"
class="w-full"
/>
</div>
<label class="flex cursor-pointer items-start gap-2.5">
<UCheckbox
v-model="allowRegistrationDraft"
class="mt-0.5"
/>
<span>
<span class="block text-sm">开放用户注册</span>
<span class="mt-1 block text-xs leading-relaxed text-muted">
允许新用户自行注册(普通用户仅可见自己的图片)
</span>
</span>
</label>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between gap-2">
<h3 class="text-sm font-semibold">
访问控制
</h3>
<UBadge
:color="sourceBadge(settings.refererSource).color"
variant="subtle"
size="xs"
>
{{ sourceBadge(settings.refererSource).label }}
</UBadge>
</div>
<p class="text-xs leading-relaxed text-muted">
Referer 防盗链,逗号分隔允许引用的域名,留空不限制
</p>
<UTextarea
v-model="refererDraft"
placeholder="example.com, www.example.com"
:rows="4"
class="w-full font-mono text-sm"
/>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between gap-2">
<h3 class="text-sm font-semibold">
链接生成
</h3>
<UBadge
:color="sourceBadge(settings.imageBaseUrlSource).color"
variant="subtle"
size="xs"
>
{{ sourceBadge(settings.imageBaseUrlSource).label }}
</UBadge>
</div>
<p class="text-xs leading-relaxed text-muted">
生成图片链接的 URL 前缀,内网访问建议填公网域名
</p>
<UInput
v-model="imageBaseUrlDraft"
placeholder="https://pic.example.com"
class="w-full font-mono text-sm"
/>
<p
v-if="settings.imageBaseUrl"
class="text-xs text-muted"
>
当前生效:<span class="font-mono break-all">{{ settings.imageBaseUrl }}</span>
</p>
</div>
</div>
</div>
<div
v-else
class="flex justify-center py-12"
>
<UIcon
name="i-lucide-loader-circle"
class="size-6 animate-spin text-muted"
/>
</div>
<div
v-if="settings"
class="flex flex-wrap items-center justify-between gap-3 border-t border-default px-5 py-4 sm:px-6"
>
<div class="flex min-w-0 items-center gap-2 text-xs text-muted">
<UIcon
name="i-lucide-key-round"
class="size-3.5 shrink-0"
/>
<span>
API Token · {{ tokenSourceText(settings.tokenSource) }}
<span class="text-dimmed">· v{{ settings.appVersion }}</span>
<template v-if="hasServerChanges">
· <span class="text-warning">有未保存的更改</span>
</template>
</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<UButton
label="管理 Token"
icon="i-lucide-external-link"
variant="outline"
color="neutral"
size="sm"
to="/api"
/>
<UButton
label="保存"
icon="i-lucide-save"
size="sm"
:loading="savingServer"
:disabled="!hasServerChanges"
@click="saveServerSettings"
/>
</div>
</div>
</section>
</AppShell>
</div>
</template>