代码格式修复
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
<div id="toast-container" class="toast-container relative z-[70]"></div>
|
<div id="toast-container" class="toast-container relative z-[70]"></div>
|
||||||
|
|
||||||
<!-- Header (Hide on Admin & Login) -->
|
<!-- Header (Hide on Admin & Login) -->
|
||||||
<Header v-if="!isAdminOrLogin" class="relative z-50" />
|
<Header v-if="!isAdminOrLogin" class="z-50" />
|
||||||
|
|
||||||
<!-- Mobile Menu (Hide on Admin & Login) -->
|
<!-- Mobile Menu (Hide on Admin & Login) -->
|
||||||
<MobileMenu v-if="!isAdminOrLogin" class="relative z-50" />
|
<MobileMenu v-if="!isAdminOrLogin" class="relative z-50" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="hidden md:flex items-center gap-10">
|
<nav class="hidden md:flex items-center gap-10">
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('home')"
|
||||||
@click="goTo('/')"
|
@click="goTo('/')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'home' }"
|
:class="{ 'text-white after:w-full': activeNav === 'home' }"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
首页
|
首页
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('blog')"
|
||||||
@click="goTo('/blog')"
|
@click="goTo('/blog')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'blog' }"
|
:class="{ 'text-white after:w-full': activeNav === 'blog' }"
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
思考
|
思考
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('columns')"
|
||||||
@click="goTo('/columns')"
|
@click="goTo('/columns')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'columns' }"
|
:class="{ 'text-white after:w-full': activeNav === 'columns' }"
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
专栏
|
专栏
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('works')"
|
||||||
@click="goTo('/works')"
|
@click="goTo('/works')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'works' }"
|
:class="{ 'text-white after:w-full': activeNav === 'works' }"
|
||||||
@@ -38,6 +42,7 @@
|
|||||||
作品
|
作品
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('snippets')"
|
||||||
@click="goTo('/snippets')"
|
@click="goTo('/snippets')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'snippets' }"
|
:class="{ 'text-white after:w-full': activeNav === 'snippets' }"
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
代码
|
代码
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="visibleMenus.includes('about')"
|
||||||
@click="goTo('/about')"
|
@click="goTo('/about')"
|
||||||
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
|
||||||
:class="{ 'text-white after:w-full': activeNav === 'about' }"
|
:class="{ 'text-white after:w-full': activeNav === 'about' }"
|
||||||
@@ -59,7 +65,11 @@
|
|||||||
<a href="/admin" target="_blank" class="text-art-muted hover:text-white transition-colors" title="后台管理">
|
<a href="/admin" target="_blank" class="text-art-muted hover:text-white transition-colors" title="后台管理">
|
||||||
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
|
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
|
||||||
</a>
|
</a>
|
||||||
<button @click="goTo('/services')" class="px-5 py-2 text-xs font-bold tracking-widest uppercase border border-white/20 hover:border-art-accent hover:text-art-accent transition-all rounded-full">
|
<button
|
||||||
|
v-if="visibleMenus.includes('services')"
|
||||||
|
@click="goTo('/services')"
|
||||||
|
class="px-5 py-2 text-xs font-bold tracking-widest uppercase border border-white/20 hover:border-art-accent hover:text-art-accent transition-all rounded-full"
|
||||||
|
>
|
||||||
合作
|
合作
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +89,7 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const activeNav = ref('home')
|
const activeNav = ref('home')
|
||||||
const siteTitle = ref<string>('')
|
const siteTitle = ref<string>('')
|
||||||
|
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'snippets', 'about', 'services']) // 默认显示所有菜单
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
const menu = document.getElementById('mobile-menu')
|
const menu = document.getElementById('mobile-menu')
|
||||||
@@ -123,6 +134,20 @@ const loadSiteSettings = async () => {
|
|||||||
if (settings.site_title) {
|
if (settings.site_title) {
|
||||||
siteTitle.value = settings.site_title
|
siteTitle.value = settings.site_title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取菜单显示配置
|
||||||
|
if (settings.visible_menus) {
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON格式
|
||||||
|
const parsed = JSON.parse(settings.visible_menus)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
visibleMenus.value = parsed
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,按逗号分隔
|
||||||
|
visibleMenus.value = settings.visible_menus.split(',').map((m: string) => m.trim()).filter((m: string) => m)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load site settings:', error)
|
console.error('Failed to load site settings:', error)
|
||||||
// 使用默认值,不抛出错误
|
// 使用默认值,不抛出错误
|
||||||
|
|||||||
@@ -1,20 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="mobile-menu" class="fixed inset-0 z-40 bg-art-bg/95 backdrop-blur-xl flex flex-col items-center justify-center space-y-8 transition-all duration-300 menu-closed md:hidden">
|
<div id="mobile-menu" class="fixed inset-0 z-40 bg-art-bg/95 backdrop-blur-xl flex flex-col items-center justify-center space-y-8 transition-all duration-300 menu-closed md:hidden">
|
||||||
<button @click="toggleMobileMenu" class="absolute top-6 right-6 text-white"><i data-lucide="x" class="w-8 h-8"></i></button>
|
<button @click="toggleMobileMenu" class="absolute top-6 right-6 text-white"><i data-lucide="x" class="w-8 h-8"></i></button>
|
||||||
<a @click="navigate('/')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">首页</a>
|
<a v-if="visibleMenus.includes('home')" @click="navigate('/')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">首页</a>
|
||||||
<a @click="navigate('/blog')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">思考</a>
|
<a v-if="visibleMenus.includes('blog')" @click="navigate('/blog')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">思考</a>
|
||||||
<a @click="navigate('/works')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">作品</a>
|
<a v-if="visibleMenus.includes('columns')" @click="navigate('/columns')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">专栏</a>
|
||||||
<a @click="navigate('/snippets')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">代码</a>
|
<a v-if="visibleMenus.includes('works')" @click="navigate('/works')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">作品</a>
|
||||||
<a @click="navigate('/about')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">关于</a>
|
<a v-if="visibleMenus.includes('snippets')" @click="navigate('/snippets')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">代码</a>
|
||||||
|
<a v-if="visibleMenus.includes('about')" @click="navigate('/about')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">关于</a>
|
||||||
|
<a v-if="visibleMenus.includes('services')" @click="navigate('/services')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">合作</a>
|
||||||
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-8">管理入口</a>
|
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-8">管理入口</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { onMounted, onUpdated } from 'vue'
|
import { onMounted, onUpdated, ref } from 'vue'
|
||||||
|
import { getPublicSettings } from '../services/api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'snippets', 'about', 'services']) // 默认显示所有菜单
|
||||||
|
|
||||||
|
// 获取菜单显示配置
|
||||||
|
const loadMenuSettings = async () => {
|
||||||
|
try {
|
||||||
|
const settings = await getPublicSettings()
|
||||||
|
if (settings.visible_menus) {
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON格式
|
||||||
|
const parsed = JSON.parse(settings.visible_menus)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
visibleMenus.value = parsed
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,按逗号分隔
|
||||||
|
visibleMenus.value = settings.visible_menus.split(',').map((m: string) => m.trim()).filter((m: string) => m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load menu settings:', error)
|
||||||
|
// 使用默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
const menu = document.getElementById('mobile-menu')
|
const menu = document.getElementById('mobile-menu')
|
||||||
@@ -40,6 +66,9 @@ const refreshIcons = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshIcons)
|
onMounted(() => {
|
||||||
|
refreshIcons()
|
||||||
|
loadMenuSettings()
|
||||||
|
})
|
||||||
onUpdated(refreshIcons)
|
onUpdated(refreshIcons)
|
||||||
</script>
|
</script>
|
||||||
@@ -8,76 +8,139 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 配置表单 -->
|
<!-- Tab + 卡片布局 -->
|
||||||
<div class="admin-card p-6">
|
<div v-if="settings.length === 0" class="admin-card p-16 text-center">
|
||||||
<div v-if="settings.length === 0" class="p-16 text-center">
|
|
||||||
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">⚙️</div>
|
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">⚙️</div>
|
||||||
<h3 class="text-white font-medium mb-2">暂无配置</h3>
|
<h3 class="text-white font-medium mb-2">暂无配置</h3>
|
||||||
<button @click="showAddModal = true" class="admin-btn-primary mt-4">添加第一个配置</button>
|
<button @click="showAddModal = true" class="admin-btn-primary mt-4">添加第一个配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form v-else @submit.prevent="saveAllSettings" class="space-y-6">
|
<div v-else class="space-y-6">
|
||||||
<div
|
<!-- Tab 导航 -->
|
||||||
v-for="setting in settings"
|
<div class="border-b border-white/10 overflow-x-auto">
|
||||||
:key="setting.id"
|
<div class="flex gap-2 min-w-max">
|
||||||
class="p-4 border border-white/5 rounded-lg bg-white/2 hover:bg-white/5 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-start">
|
|
||||||
<!-- 键名(只读) -->
|
|
||||||
<div class="md:col-span-3 space-y-2">
|
|
||||||
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">键名</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:value="setting.keyName"
|
|
||||||
disabled
|
|
||||||
class="admin-input font-mono disabled:opacity-50 disabled:cursor-not-allowed bg-white/5"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 值(可编辑) -->
|
|
||||||
<div class="md:col-span-4 space-y-2">
|
|
||||||
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">值</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="setting.value"
|
|
||||||
class="admin-input"
|
|
||||||
placeholder="配置项的值"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 描述(可编辑) -->
|
|
||||||
<div class="md:col-span-4 space-y-2">
|
|
||||||
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">描述</label>
|
|
||||||
<textarea
|
|
||||||
v-model="setting.description"
|
|
||||||
rows="2"
|
|
||||||
class="admin-input resize-none"
|
|
||||||
placeholder="该配置项的用途说明"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 删除按钮 -->
|
|
||||||
<div class="md:col-span-1 space-y-2 flex items-end">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
v-for="setting in allSettings"
|
||||||
@click="deleteSetting(setting.keyName)"
|
:key="setting.id"
|
||||||
class="admin-btn-danger py-2 px-3 text-xs w-full"
|
@click="activeTab = setting.id"
|
||||||
title="删除配置"
|
:class="[
|
||||||
|
'px-4 py-3 text-sm font-medium transition-all duration-200 border-b-2 relative',
|
||||||
|
activeTab === setting.id
|
||||||
|
? 'text-art-accent border-art-accent'
|
||||||
|
: 'text-art-muted border-transparent hover:text-white/80'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
{{ getSettingDisplayName(setting.keyName) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片内容 -->
|
||||||
|
<div class="admin-card p-6 border border-white/10 rounded-lg bg-white/5 shadow-lg">
|
||||||
|
<!-- 菜单显示配置卡片 -->
|
||||||
|
<div v-if="activeSetting && activeSetting.keyName === 'visible_menus'" class="space-y-6">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="flex items-center justify-between pb-4 border-b border-white/10">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-white mb-1">{{ activeSetting.keyName }}</h3>
|
||||||
|
<p class="text-sm text-art-muted">{{ activeSetting.description || '前台菜单显示配置' }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="deleteSetting(activeSetting.keyName)"
|
||||||
|
class="text-red-400 hover:text-red-300 transition-colors p-2 hover:bg-red-500/10 rounded"
|
||||||
|
title="删除配置"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单多选框(优化UI) -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-base font-medium text-white mb-4">显示菜单项</label>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<label
|
||||||
|
v-for="menu in menuOptions"
|
||||||
|
:key="menu.key"
|
||||||
|
class="flex items-center gap-3 cursor-pointer group p-3 rounded-lg border border-white/10 bg-white/5 hover:bg-white/10 hover:border-white/20 transition-all"
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center justify-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="menu.key"
|
||||||
|
v-model="selectedMenus"
|
||||||
|
class="sr-only peer"
|
||||||
|
>
|
||||||
|
<div class="w-5 h-5 rounded border-2 border-white/30 bg-white/5 peer-checked:bg-art-accent peer-checked:border-art-accent transition-all duration-200 flex items-center justify-center">
|
||||||
|
<svg v-if="selectedMenus.includes(menu.key)" class="w-3 h-3 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-white/90 group-hover:text-white transition-colors flex-1">{{ menu.label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
<!-- 保存按钮 -->
|
||||||
<div class="pt-4 flex justify-end gap-3 border-t border-white/5">
|
<div class="pt-6 border-t border-white/10">
|
||||||
<button type="button" @click="fetchSettings" class="admin-btn-secondary">重置</button>
|
<button
|
||||||
<button type="submit" class="admin-btn-primary" :disabled="isSaving">
|
@click="saveMenuSetting"
|
||||||
{{ isSaving ? '保存中...' : '保存所有配置' }}
|
:disabled="savingSettings.has(activeSetting.id)"
|
||||||
|
class="admin-btn-primary w-full py-3 text-base font-medium"
|
||||||
|
>
|
||||||
|
{{ savingSettings.has(activeSetting.id) ? '保存中...' : '保存配置' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<!-- 普通配置项卡片 -->
|
||||||
|
<div v-else-if="activeSetting" class="space-y-6">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="flex items-center justify-between pb-4 border-b border-white/10">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold text-white mb-1 font-mono">{{ activeSetting.keyName }}</h3>
|
||||||
|
<p class="text-sm text-art-muted">{{ activeSetting.description || '无描述' }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="deleteSetting(activeSetting.keyName)"
|
||||||
|
class="text-red-400 hover:text-red-300 transition-colors p-2 hover:bg-red-500/10 rounded"
|
||||||
|
title="删除配置"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 值输入 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-base font-medium text-white">配置值</label>
|
||||||
|
<textarea
|
||||||
|
v-if="activeSetting.keyName === 'site_description' || activeSetting.keyName === 'site_keywords'"
|
||||||
|
v-model="activeSetting.value"
|
||||||
|
rows="5"
|
||||||
|
class="admin-input resize-none w-full"
|
||||||
|
placeholder="配置项的值"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
v-model="activeSetting.value"
|
||||||
|
class="admin-input w-full text-base"
|
||||||
|
placeholder="配置项的值"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div class="pt-6 border-t border-white/10">
|
||||||
|
<button
|
||||||
|
@click="saveSingleSetting(activeSetting)"
|
||||||
|
:disabled="savingSettings.has(activeSetting.id)"
|
||||||
|
class="admin-btn-primary w-full py-3 text-base font-medium"
|
||||||
|
>
|
||||||
|
{{ savingSettings.has(activeSetting.id) ? '保存中...' : '保存配置' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增配置 Modal -->
|
<!-- 新增配置 Modal -->
|
||||||
@@ -141,14 +204,65 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { getSettings, createSetting, updateSetting, deleteSetting as deleteSettingApi, Setting } from '../../services/api'
|
import { getSettings, createSetting, updateSetting, deleteSetting as deleteSettingApi, Setting } from '../../services/api'
|
||||||
import { useToast } from '../../composables/useToast'
|
import { useToast } from '../../composables/useToast'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const settings = ref<Setting[]>([])
|
const settings = ref<Setting[]>([])
|
||||||
const showAddModal = ref(false)
|
const showAddModal = ref(false)
|
||||||
const isSaving = ref(false)
|
const savingSettings = ref<Set<number>>(new Set())
|
||||||
|
const activeTab = ref<number | null>(null)
|
||||||
|
|
||||||
|
// 菜单选项配置
|
||||||
|
const menuOptions = [
|
||||||
|
{ key: 'home', label: '首页' },
|
||||||
|
{ key: 'blog', label: '思考' },
|
||||||
|
{ key: 'columns', label: '专栏' },
|
||||||
|
{ key: 'works', label: '作品' },
|
||||||
|
{ key: 'snippets', label: '代码' },
|
||||||
|
{ key: 'about', label: '关于' },
|
||||||
|
{ key: 'services', label: '合作' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 选中的菜单
|
||||||
|
const selectedMenus = ref<string[]>([])
|
||||||
|
|
||||||
|
// 菜单配置项
|
||||||
|
const menuSetting = computed(() => {
|
||||||
|
return settings.value.find(s => s.keyName === 'visible_menus')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 所有配置项(用于tab)
|
||||||
|
const allSettings = computed(() => {
|
||||||
|
// 将菜单配置放在第一位
|
||||||
|
const menu = settings.value.find(s => s.keyName === 'visible_menus')
|
||||||
|
const others = settings.value.filter(s => s.keyName !== 'visible_menus')
|
||||||
|
return menu ? [menu, ...others] : others
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前激活的配置项
|
||||||
|
const activeSetting = computed(() => {
|
||||||
|
if (activeTab.value === null && allSettings.value.length > 0) {
|
||||||
|
return allSettings.value[0]
|
||||||
|
}
|
||||||
|
return allSettings.value.find(s => s.id === activeTab.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取配置项显示名称
|
||||||
|
const getSettingDisplayName = (keyName: string) => {
|
||||||
|
const nameMap: Record<string, string> = {
|
||||||
|
'visible_menus': '菜单显示',
|
||||||
|
'site_title': '网站标题',
|
||||||
|
'site_description': '网站描述',
|
||||||
|
'site_author': '网站作者',
|
||||||
|
'site_keywords': '网站关键词',
|
||||||
|
'posts_per_page': '文章分页',
|
||||||
|
'works_per_page': '作品分页',
|
||||||
|
'snippets_per_page': '代码分页'
|
||||||
|
}
|
||||||
|
return nameMap[keyName] || keyName
|
||||||
|
}
|
||||||
|
|
||||||
const newForm = ref({
|
const newForm = ref({
|
||||||
keyName: '',
|
keyName: '',
|
||||||
@@ -159,34 +273,82 @@ const newForm = ref({
|
|||||||
const fetchSettings = async () => {
|
const fetchSettings = async () => {
|
||||||
try {
|
try {
|
||||||
settings.value = await getSettings()
|
settings.value = await getSettings()
|
||||||
|
|
||||||
|
// 初始化激活的tab(第一个配置项)
|
||||||
|
if (settings.value.length > 0 && activeTab.value === null) {
|
||||||
|
activeTab.value = allSettings.value[0]?.id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化菜单选择
|
||||||
|
if (menuSetting.value) {
|
||||||
|
const menuValue = menuSetting.value.value || ''
|
||||||
|
if (menuValue) {
|
||||||
|
// 支持逗号分隔或JSON数组格式
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(menuValue)
|
||||||
|
selectedMenus.value = Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,按逗号分隔
|
||||||
|
selectedMenus.value = menuValue.split(',').map(m => m.trim()).filter(m => m)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 默认显示所有菜单
|
||||||
|
selectedMenus.value = menuOptions.map(m => m.key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有菜单配置,默认显示所有菜单
|
||||||
|
selectedMenus.value = menuOptions.map(m => m.key)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching settings:', error)
|
console.error('Error fetching settings:', error)
|
||||||
toast.showToast('获取系统配置失败', 'error')
|
toast.showToast('获取系统配置失败', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveAllSettings = async () => {
|
// 保存单个配置项
|
||||||
isSaving.value = true
|
const saveSingleSetting = async (setting: Setting) => {
|
||||||
|
savingSettings.value.add(setting.id)
|
||||||
try {
|
try {
|
||||||
// 批量更新所有配置项
|
await updateSetting({
|
||||||
const updatePromises = settings.value.map(setting =>
|
|
||||||
updateSetting({
|
|
||||||
id: setting.id,
|
id: setting.id,
|
||||||
keyName: setting.keyName,
|
keyName: setting.keyName,
|
||||||
value: setting.value,
|
value: setting.value,
|
||||||
description: setting.description
|
description: setting.description
|
||||||
})
|
})
|
||||||
)
|
toast.showToast('配置保存成功', 'success')
|
||||||
|
|
||||||
await Promise.all(updatePromises)
|
|
||||||
toast.showToast('所有配置保存成功', 'success')
|
|
||||||
// 重新获取配置以确保数据同步
|
// 重新获取配置以确保数据同步
|
||||||
await fetchSettings()
|
await fetchSettings()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving settings:', error)
|
console.error('Error saving setting:', error)
|
||||||
toast.showToast('保存配置失败', 'error')
|
toast.showToast('保存配置失败', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false
|
savingSettings.value.delete(setting.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存菜单配置
|
||||||
|
const saveMenuSetting = async () => {
|
||||||
|
if (!menuSetting.value) return
|
||||||
|
|
||||||
|
savingSettings.value.add(menuSetting.value.id)
|
||||||
|
try {
|
||||||
|
// 将选中的菜单保存为JSON数组格式
|
||||||
|
const menuValue = JSON.stringify(selectedMenus.value)
|
||||||
|
|
||||||
|
await updateSetting({
|
||||||
|
id: menuSetting.value.id,
|
||||||
|
keyName: menuSetting.value.keyName,
|
||||||
|
value: menuValue,
|
||||||
|
description: menuSetting.value.description
|
||||||
|
})
|
||||||
|
toast.showToast('菜单配置保存成功', 'success')
|
||||||
|
// 重新获取配置以确保数据同步
|
||||||
|
await fetchSettings()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving menu setting:', error)
|
||||||
|
toast.showToast('保存菜单配置失败', 'error')
|
||||||
|
} finally {
|
||||||
|
savingSettings.value.delete(menuSetting.value.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@ export interface PublicSettings {
|
|||||||
site_description?: string
|
site_description?: string
|
||||||
site_author?: string
|
site_author?: string
|
||||||
site_keywords?: string
|
site_keywords?: string
|
||||||
|
visible_menus?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||||
|
|||||||
@@ -60,20 +60,6 @@
|
|||||||
@apply border-art-accent ring-1 ring-art-accent outline-none bg-art-surface;
|
@apply border-art-accent ring-1 ring-art-accent outline-none bg-art-surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix autofill background color for admin inputs */
|
|
||||||
.admin-input:-webkit-autofill,
|
|
||||||
.admin-input:-webkit-autofill:hover,
|
|
||||||
.admin-input:-webkit-autofill:focus,
|
|
||||||
.admin-input:-webkit-autofill:active {
|
|
||||||
-webkit-box-shadow: 0 0 0 30px #0a0a0a inset !important;
|
|
||||||
-webkit-text-fill-color: white !important;
|
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-input:focus:-webkit-autofill {
|
|
||||||
-webkit-box-shadow: 0 0 0 30px #121214 inset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-btn-primary {
|
.admin-btn-primary {
|
||||||
@apply bg-art-accent text-black font-medium px-5 py-2.5 rounded-md hover:opacity-90 active:scale-95 transition-all text-sm tracking-wide;
|
@apply bg-art-accent text-black font-medium px-5 py-2.5 rounded-md hover:opacity-90 active:scale-95 transition-all text-sm tracking-wide;
|
||||||
}
|
}
|
||||||
@@ -186,23 +172,3 @@ body {
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global autofill fix for all inputs (dark theme) */
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
input:-webkit-autofill:active {
|
|
||||||
-webkit-box-shadow: 0 0 0 30px #0a0a0a inset !important;
|
|
||||||
-webkit-text-fill-color: white !important;
|
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specific fix for login page */
|
|
||||||
.login-input:-webkit-autofill,
|
|
||||||
.login-input:-webkit-autofill:hover,
|
|
||||||
.login-input:-webkit-autofill:focus,
|
|
||||||
.login-input:-webkit-autofill:active {
|
|
||||||
-webkit-box-shadow: 0 0 0 30px #0a0a0a inset !important;
|
|
||||||
-webkit-text-fill-color: white !important;
|
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func GetSettings(c *gin.Context) {
|
|||||||
|
|
||||||
// 只返回前端需要的公开配置项
|
// 只返回前端需要的公开配置项
|
||||||
publicSettings := make(map[string]string)
|
publicSettings := make(map[string]string)
|
||||||
publicKeys := []string{"site_title", "site_description", "site_author", "site_keywords"}
|
publicKeys := []string{"site_title", "site_description", "site_author", "site_keywords", "visible_menus"}
|
||||||
|
|
||||||
for _, key := range publicKeys {
|
for _, key := range publicKeys {
|
||||||
if value, exists := settingsMap[key]; exists {
|
if value, exists := settingsMap[key]; exists {
|
||||||
|
|||||||
Reference in New Issue
Block a user