Files
urldb/web/pages/admin/site-config.vue
2025-10-20 23:57:27 +08:00

580 lines
19 KiB
Vue
Raw 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.
<template>
<AdminPageLayout>
<!-- 页面头部 - 标题和保存按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">站点配置</h1>
<p class="text-gray-600 dark:text-gray-400">管理网站基本信息和设置</p>
</div>
<n-button type="primary" @click="saveConfig" :loading="saving">
<template #icon>
<i class="fas fa-save"></i>
</template>
保存配置
</n-button>
</template>
<!-- 内容区 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
>
<n-tab-pane name="basic" tab="基本信息">
<div class="tab-content-container">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- 网站标题 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">网站标题</label>
<span class="text-xs text-gray-500 dark:text-gray-400">网站的主要标识显示在浏览器标签页和搜索结果中</span>
</div>
<n-input
v-model:value="configForm.site_title"
placeholder="请输入网站标题"
/>
</div>
<!-- 网站描述 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">网站描述</label>
<span class="text-xs text-gray-500 dark:text-gray-400">网站的简要介绍用于SEO和社交媒体分享</span>
</div>
<n-input
v-model:value="configForm.site_description"
placeholder="请输入网站描述"
/>
</div>
<!-- 关键词 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">关键词</label>
<span class="text-xs text-gray-500 dark:text-gray-400">用于SEO优化多个关键词用逗号分隔</span>
</div>
<n-input
v-model:value="configForm.keywords"
placeholder="请输入关键词,用逗号分隔"
/>
</div>
<!-- 网站Logo -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">网站Logo</label>
<span class="text-xs text-gray-500 dark:text-gray-400">选择网站Logo图片建议使用正方形图片</span>
</div>
<div class="flex items-center space-x-4">
<div v-if="configForm.site_logo" class="flex-shrink-0">
<n-image
:src="getImageUrl(configForm.site_logo)"
alt="网站Logo"
width="80"
height="80"
object-fit="cover"
class="rounded-lg border"
/>
</div>
<div class="flex-1">
<n-button type="primary" @click="openLogoSelector">
<template #icon>
<i class="fas fa-image"></i>
</template>
{{ configForm.site_logo ? '更换Logo' : '选择Logo' }}
</n-button>
<n-button v-if="configForm.site_logo" @click="clearLogo" class="ml-2">
<template #icon>
<i class="fas fa-times"></i>
</template>
清除
</n-button>
</div>
</div>
</div>
<!-- 版权信息 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">版权信息</label>
<span class="text-xs text-gray-500 dark:text-gray-400">网站底部的版权声明信息</span>
</div>
<n-input
v-model:value="configForm.copyright"
placeholder="请输入版权信息"
/>
</div>
</div>
</n-form>
</div>
</n-tab-pane>
<n-tab-pane name="security" tab="安全设置">
<div class="tab-content-container">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- 维护模式 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">维护模式</label>
<span class="text-xs text-gray-500 dark:text-gray-400">开启后网站将显示维护页面暂停用户访问</span>
</div>
<n-switch v-model:value="configForm.maintenance_mode" />
</div>
<!-- 违禁词 -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">违禁词</label>
<span class="text-xs text-gray-500 dark:text-gray-400">包含这些词汇的资源将被过滤多个词汇用逗号分隔</span>
</div>
<a
href="https://raw.githubusercontent.com/ctwj/urldb/refs/heads/main/db/forbidden.txt"
target="_blank"
class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"
>
开源违禁词
</a>
</div>
<n-input
v-model:value="configForm.forbidden_words"
placeholder="请输入违禁词,用逗号分隔"
type="textarea"
:rows="4"
/>
</div>
<!-- 开启注册 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">开启注册</label>
<span class="text-xs text-gray-500 dark:text-gray-400">开启后用户才能注册新账号关闭后注册页面将显示"当前系统已关闭注册功能"</span>
</div>
<n-switch v-model:value="configForm.enable_register" />
</div>
</div>
</n-form>
</div>
</n-tab-pane>
<n-tab-pane name="ui" tab="界面配置">
<div class="tab-content-container">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- 公告配置组件 -->
<AnnouncementConfig
v-model="announcementConfig"
@update:modelValue="handleAnnouncementUpdate"
/>
<!-- 浮动按钮配置组件 -->
<FloatButtonsConfig
v-model="floatButtonsConfig"
@update:modelValue="handleFloatButtonsUpdate"
@openWechatSelector="showWechatSelector = true"
@openTelegramSelector="showTelegramSelector = true"
/>
<!-- 微信图片选择器 -->
<ImageSelectorModal
v-model:show="showWechatSelector"
title="选择微信搜一搜图片"
@select="handleWechatImageSelect"
/>
<!-- Telegram图片选择器 -->
<ImageSelectorModal
v-model:show="showTelegramSelector"
title="选择Telegram二维码图片"
@select="handleTelegramImageSelect"
/>
</div>
</n-form>
</div>
</n-tab-pane>
</n-tabs>
</div>
</template>
</AdminPageLayout>
<!-- ImageSelectorModal 组件 -->
<ImageSelectorModal
v-model:show="showLogoSelector"
title="选择Logo图片"
@select="handleLogoSelect"
/>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin',
ssr: false
})
import { useImageUrl } from '~/composables/useImageUrl'
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
import AnnouncementConfig from '~/components/Admin/AnnouncementConfig.vue'
import FloatButtonsConfig from '~/components/Admin/FloatButtonsConfig.vue'
import ImageSelectorModal from '~/components/Admin/ImageSelectorModal.vue'
const notification = useNotification()
const { getImageUrl } = useImageUrl()
const formRef = ref()
const saving = ref(false)
const activeTab = ref('basic')
// Logo选择器相关数据
const showLogoSelector = ref(false)
// 微信和Telegram选择器相关数据
const showWechatSelector = ref(false)
const showTelegramSelector = ref(false)
// 公告类型接口
interface Announcement {
content: string
enabled: boolean
}
// 配置表单数据类型
interface SiteConfigForm {
site_title: string
site_description: string
keywords: string
copyright: string
site_logo: string
maintenance_mode: boolean
enable_register: boolean
forbidden_words: string
enable_announcements: boolean
announcements: Announcement[]
enable_float_buttons: boolean
wechat_search_image: string
telegram_qr_image: string
}
// 公告配置子组件数据
const announcementConfig = computed({
get: () => ({
enable_announcements: configForm.value.enable_announcements,
announcements: configForm.value.announcements
}),
set: (value: any) => {
configForm.value.enable_announcements = value.enable_announcements
configForm.value.announcements = value.announcements
}
})
// 浮动按钮配置子组件数据
const floatButtonsConfig = computed({
get: () => ({
enable_float_buttons: configForm.value.enable_float_buttons,
wechat_search_image: configForm.value.wechat_search_image,
telegram_qr_image: configForm.value.telegram_qr_image
}),
set: (value: any) => {
configForm.value.enable_float_buttons = value.enable_float_buttons
configForm.value.wechat_search_image = value.wechat_search_image
configForm.value.telegram_qr_image = value.telegram_qr_image
}
})
// 使用配置改动检测
const {
setOriginalConfig,
updateCurrentConfig,
getChangedConfig,
hasChanges,
getChangedDetails,
updateOriginalConfig,
saveConfig: saveConfigWithDetection
} = useConfigChangeDetection<SiteConfigForm>({
debug: true,
// 自定义比较函数,处理数组深层比较
customCompare: (key: string, currentValue: any, originalValue: any) => {
// 对于数组类型使用JSON字符串比较
if (Array.isArray(currentValue) && Array.isArray(originalValue)) {
return JSON.stringify(currentValue) !== JSON.stringify(originalValue)
}
// 其他类型使用默认比较
return currentValue !== originalValue
},
// 字段映射:前端字段名 -> 后端字段名
fieldMapping: {
site_title: 'site_title',
site_description: 'site_description',
keywords: 'keywords',
copyright: 'copyright',
site_logo: 'site_logo',
maintenance_mode: 'maintenance_mode',
enable_register: 'enable_register',
forbidden_words: 'forbidden_words',
enable_announcements: 'enable_announcements',
announcements: 'announcements',
enable_float_buttons: 'enable_float_buttons',
wechat_search_image: 'wechat_search_image',
telegram_qr_image: 'telegram_qr_image'
}
})
// 公告类型选项(如果需要的话可以保留,但根据反馈暂时移除)
// 配置表单数据
const configForm = ref<SiteConfigForm>({
site_title: '',
site_description: '',
keywords: '',
copyright: '',
site_logo: '',
maintenance_mode: false,
enable_register: false,
forbidden_words: '',
enable_announcements: false,
announcements: [],
enable_float_buttons: false,
wechat_search_image: '',
telegram_qr_image: ''
})
// 表单验证规则
const rules = {
site_title: {
required: true,
message: '请输入网站标题',
trigger: 'blur'
},
site_description: {
required: true,
message: '请输入网站描述',
trigger: 'blur'
}
}
// 获取系统配置
const fetchConfig = async () => {
try {
const { useSystemConfigApi } = await import('~/composables/useApi')
const systemConfigApi = useSystemConfigApi()
const response = await systemConfigApi.getSystemConfig() as any
if (response) {
const configData = {
site_title: response.site_title || '',
site_description: response.site_description || '',
keywords: response.keywords || '',
copyright: response.copyright || '',
site_logo: response.site_logo || '',
maintenance_mode: response.maintenance_mode || false,
enable_register: response.enable_register || false,
forbidden_words: response.forbidden_words || '',
enable_announcements: response.enable_announcements || false,
announcements: response.announcements ? JSON.parse(response.announcements) : [],
enable_float_buttons: response.enable_float_buttons || false,
wechat_search_image: response.wechat_search_image || '',
telegram_qr_image: response.telegram_qr_image || ''
}
// 设置表单数据和原始数据
configForm.value = { ...configData }
setOriginalConfig(configData)
}
} catch (error) {
console.error('获取系统配置失败:', error)
notification.error({
content: '获取系统配置失败',
duration: 3000
})
}
}
// 保存配置
const saveConfig = async () => {
try {
await formRef.value?.validate()
saving.value = true
// 更新当前配置数据
updateCurrentConfig(configForm.value)
const { useSystemConfigApi } = await import('~/composables/useApi')
const systemConfigApi = useSystemConfigApi()
// 使用通用保存函数
const result = await saveConfigWithDetection(
systemConfigApi.updateSystemConfig,
{
onlyChanged: true,
includeAllFields: true
},
// 成功回调
async () => {
notification.success({
content: '站点配置保存成功',
duration: 3000
})
// 刷新系统配置状态,确保顶部导航同步更新
const { useSystemConfigStore } = await import('~/stores/systemConfig')
const systemConfigStore = useSystemConfigStore()
await systemConfigStore.initConfig(true, true)
},
// 错误回调
(error) => {
console.error('保存站点配置失败:', error)
notification.error({
content: '保存站点配置失败',
duration: 3000
})
}
)
// 如果没有改动,显示提示
if (result && result.message === '没有检测到任何改动') {
notification.info({
content: '没有检测到任何改动',
duration: 3000
})
}
} finally {
saving.value = false
}
}
// Logo选择器方法
const openLogoSelector = () => {
showLogoSelector.value = true
}
const clearLogo = () => {
configForm.value = {
...configForm.value,
site_logo: ''
}
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
// 子组件更新处理方法
const handleAnnouncementUpdate = (newValue: any) => {
// 直接更新整个表单
configForm.value = {
...configForm.value,
enable_announcements: newValue.enable_announcements,
announcements: newValue.announcements
}
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
const handleFloatButtonsUpdate = (newValue: any) => {
configForm.value = {
...configForm.value,
enable_float_buttons: newValue.enable_float_buttons,
wechat_search_image: newValue.wechat_search_image,
telegram_qr_image: newValue.telegram_qr_image
}
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
// Logo选择处理
const handleLogoSelect = (file: any) => {
configForm.value = {
...configForm.value,
site_logo: file.access_url
}
showLogoSelector.value = false
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
// 微信图片选择处理
const handleWechatImageSelect = (file: any) => {
configForm.value = {
...configForm.value,
wechat_search_image: file.access_url
}
showWechatSelector.value = false
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
// Telegram图片选择处理
const handleTelegramImageSelect = (file: any) => {
configForm.value = {
...configForm.value,
telegram_qr_image: file.access_url
}
showTelegramSelector.value = false
// 强制触发更新
updateCurrentConfig({ ...configForm.value })
}
// 页面加载时获取配置
onMounted(() => {
fetchConfig()
})
</script>
<style scoped>
/* 站点配置页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
/* 配置标签容器 - 支持滚动 */
.config-tabs-container {
height: calc(100vh - 200px);
overflow-y: auto;
padding: 0.5rem 0;
}
/* tab内容容器 - 个别内容滚动 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>