update: 持久化系统配置

This commit is contained in:
ctwj
2025-07-11 02:30:57 +08:00
parent 770d9b00cb
commit 45c6e9fec4
18 changed files with 1275 additions and 42 deletions

View File

@@ -78,6 +78,7 @@ func autoMigrate() error {
&entity.ReadyResource{},
&entity.User{},
&entity.SearchStat{},
&entity.SystemConfig{},
)
}

View File

@@ -0,0 +1,60 @@
package converter
import (
"time"
"res_db/db/dto"
"res_db/db/entity"
)
// SystemConfigToResponse 将系统配置实体转换为响应DTO
func SystemConfigToResponse(config *entity.SystemConfig) *dto.SystemConfigResponse {
if config == nil {
return nil
}
return &dto.SystemConfigResponse{
ID: config.ID,
CreatedAt: config.CreatedAt.Format(time.RFC3339),
UpdatedAt: config.UpdatedAt.Format(time.RFC3339),
// SEO 配置
SiteTitle: config.SiteTitle,
SiteDescription: config.SiteDescription,
Keywords: config.Keywords,
Author: config.Author,
Copyright: config.Copyright,
// 自动处理配置
AutoProcessReadyResources: config.AutoProcessReadyResources,
AutoProcessInterval: config.AutoProcessInterval,
// 其他配置
PageSize: config.PageSize,
MaintenanceMode: config.MaintenanceMode,
}
}
// RequestToSystemConfig 将请求DTO转换为系统配置实体
func RequestToSystemConfig(req *dto.SystemConfigRequest) *entity.SystemConfig {
if req == nil {
return nil
}
return &entity.SystemConfig{
// SEO 配置
SiteTitle: req.SiteTitle,
SiteDescription: req.SiteDescription,
Keywords: req.Keywords,
Author: req.Author,
Copyright: req.Copyright,
// 自动处理配置
AutoProcessReadyResources: req.AutoProcessReadyResources,
AutoProcessInterval: req.AutoProcessInterval,
// 其他配置
PageSize: req.PageSize,
MaintenanceMode: req.MaintenanceMode,
}
}

41
db/dto/system_config.go Normal file
View File

@@ -0,0 +1,41 @@
package dto
// SystemConfigRequest 系统配置请求
type SystemConfigRequest struct {
// SEO 配置
SiteTitle string `json:"site_title" validate:"required"`
SiteDescription string `json:"site_description"`
Keywords string `json:"keywords"`
Author string `json:"author"`
Copyright string `json:"copyright"`
// 自动处理配置
AutoProcessReadyResources bool `json:"auto_process_ready_resources"`
AutoProcessInterval int `json:"auto_process_interval" validate:"min=1,max=1440"`
// 其他配置
PageSize int `json:"page_size" validate:"min=10,max=500"`
MaintenanceMode bool `json:"maintenance_mode"`
}
// SystemConfigResponse 系统配置响应
type SystemConfigResponse struct {
ID uint `json:"id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
// SEO 配置
SiteTitle string `json:"site_title"`
SiteDescription string `json:"site_description"`
Keywords string `json:"keywords"`
Author string `json:"author"`
Copyright string `json:"copyright"`
// 自动处理配置
AutoProcessReadyResources bool `json:"auto_process_ready_resources"`
AutoProcessInterval int `json:"auto_process_interval"`
// 其他配置
PageSize int `json:"page_size"`
MaintenanceMode bool `json:"maintenance_mode"`
}

View File

@@ -0,0 +1,32 @@
package entity
import (
"time"
)
// SystemConfig 系统配置实体
type SystemConfig struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// SEO 配置
SiteTitle string `json:"site_title" gorm:"size:200;not null;default:'网盘资源管理系统'"`
SiteDescription string `json:"site_description" gorm:"size:500"`
Keywords string `json:"keywords" gorm:"size:500"`
Author string `json:"author" gorm:"size:100"`
Copyright string `json:"copyright" gorm:"size:200"`
// 自动处理配置
AutoProcessReadyResources bool `json:"auto_process_ready_resources" gorm:"default:false"`
AutoProcessInterval int `json:"auto_process_interval" gorm:"default:30"` // 分钟
// 其他配置
PageSize int `json:"page_size" gorm:"default:100"`
MaintenanceMode bool `json:"maintenance_mode" gorm:"default:false"`
}
// TableName 指定表名
func (SystemConfig) TableName() string {
return "system_configs"
}

View File

@@ -14,6 +14,7 @@ type RepositoryManager struct {
ReadyResourceRepository ReadyResourceRepository
UserRepository UserRepository
SearchStatRepository SearchStatRepository
SystemConfigRepository SystemConfigRepository
}
// NewRepositoryManager 创建Repository管理器
@@ -27,5 +28,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
ReadyResourceRepository: NewReadyResourceRepository(db),
UserRepository: NewUserRepository(db),
SearchStatRepository: NewSearchStatRepository(db),
SystemConfigRepository: NewSystemConfigRepository(db),
}
}

View File

@@ -0,0 +1,80 @@
package repo
import (
"res_db/db/entity"
"gorm.io/gorm"
)
// SystemConfigRepository 系统配置Repository接口
type SystemConfigRepository interface {
BaseRepository[entity.SystemConfig]
FindFirst() (*entity.SystemConfig, error)
GetOrCreateDefault() (*entity.SystemConfig, error)
Upsert(config *entity.SystemConfig) error
}
// SystemConfigRepositoryImpl 系统配置Repository实现
type SystemConfigRepositoryImpl struct {
BaseRepositoryImpl[entity.SystemConfig]
}
// NewSystemConfigRepository 创建系统配置Repository
func NewSystemConfigRepository(db *gorm.DB) SystemConfigRepository {
return &SystemConfigRepositoryImpl{
BaseRepositoryImpl: BaseRepositoryImpl[entity.SystemConfig]{db: db},
}
}
// FindFirst 获取第一个配置(通常只有一个配置)
func (r *SystemConfigRepositoryImpl) FindFirst() (*entity.SystemConfig, error) {
var config entity.SystemConfig
err := r.db.First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// Upsert 创建或更新系统配置
func (r *SystemConfigRepositoryImpl) Upsert(config *entity.SystemConfig) error {
var existingConfig entity.SystemConfig
err := r.db.First(&existingConfig).Error
if err != nil {
// 如果不存在,则创建
return r.db.Create(config).Error
} else {
// 如果存在,则更新
config.ID = existingConfig.ID
return r.db.Save(config).Error
}
}
// GetOrCreateDefault 获取配置或创建默认配置
func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() (*entity.SystemConfig, error) {
config, err := r.FindFirst()
if err != nil {
// 创建默认配置
defaultConfig := &entity.SystemConfig{
SiteTitle: "网盘资源管理系统",
SiteDescription: "专业的网盘资源管理系统",
Keywords: "网盘,资源管理,文件分享",
Author: "系统管理员",
Copyright: "© 2024 网盘资源管理系统",
AutoProcessReadyResources: false,
AutoProcessInterval: 30,
PageSize: 100,
MaintenanceMode: false,
}
err = r.db.Create(defaultConfig).Error
if err != nil {
return nil, err
}
return defaultConfig, nil
}
return config, nil
}

View File

@@ -0,0 +1,144 @@
package handlers
import (
"net/http"
"res_db/db/converter"
"res_db/db/dto"
"res_db/db/repo"
"github.com/gin-gonic/gin"
)
// SystemConfigHandler 系统配置处理器
type SystemConfigHandler struct {
systemConfigRepo repo.SystemConfigRepository
}
// NewSystemConfigHandler 创建系统配置处理器
func NewSystemConfigHandler(systemConfigRepo repo.SystemConfigRepository) *SystemConfigHandler {
return &SystemConfigHandler{
systemConfigRepo: systemConfigRepo,
}
}
// GetConfig 获取系统配置
func (h *SystemConfigHandler) GetConfig(c *gin.Context) {
config, err := h.systemConfigRepo.GetOrCreateDefault()
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "获取系统配置失败")
return
}
configResponse := converter.SystemConfigToResponse(config)
SuccessResponse(c, configResponse, "获取系统配置成功")
}
// UpdateConfig 更新系统配置
func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) {
var req dto.SystemConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, "请求参数错误")
return
}
// 验证参数
if req.SiteTitle == "" {
ErrorResponse(c, http.StatusBadRequest, "网站标题不能为空")
return
}
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
ErrorResponse(c, http.StatusBadRequest, "自动处理间隔必须在1-1440分钟之间")
return
}
if req.PageSize < 10 || req.PageSize > 500 {
ErrorResponse(c, http.StatusBadRequest, "每页显示数量必须在10-500之间")
return
}
// 转换为实体
config := converter.RequestToSystemConfig(&req)
if config == nil {
ErrorResponse(c, http.StatusInternalServerError, "数据转换失败")
return
}
// 保存配置
err := h.systemConfigRepo.Upsert(config)
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "保存系统配置失败")
return
}
// 返回更新后的配置
updatedConfig, err := h.systemConfigRepo.FindFirst()
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "获取更新后的配置失败")
return
}
configResponse := converter.SystemConfigToResponse(updatedConfig)
SuccessResponse(c, configResponse, "系统配置保存成功")
}
// GetSystemConfig 获取系统配置使用全局repoManager
func GetSystemConfig(c *gin.Context) {
config, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "获取系统配置失败")
return
}
configResponse := converter.SystemConfigToResponse(config)
SuccessResponse(c, configResponse, "获取系统配置成功")
}
// UpdateSystemConfig 更新系统配置使用全局repoManager
func UpdateSystemConfig(c *gin.Context) {
var req dto.SystemConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, "请求参数错误")
return
}
// 验证参数
if req.SiteTitle == "" {
ErrorResponse(c, http.StatusBadRequest, "网站标题不能为空")
return
}
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
ErrorResponse(c, http.StatusBadRequest, "自动处理间隔必须在1-1440分钟之间")
return
}
if req.PageSize < 10 || req.PageSize > 500 {
ErrorResponse(c, http.StatusBadRequest, "每页显示数量必须在10-500之间")
return
}
// 转换为实体
config := converter.RequestToSystemConfig(&req)
if config == nil {
ErrorResponse(c, http.StatusInternalServerError, "数据转换失败")
return
}
// 保存配置
err := repoManager.SystemConfigRepository.Upsert(config)
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "保存系统配置失败")
return
}
// 返回更新后的配置
updatedConfig, err := repoManager.SystemConfigRepository.FindFirst()
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "获取更新后的配置失败")
return
}
configResponse := converter.SystemConfigToResponse(updatedConfig)
SuccessResponse(c, configResponse, "系统配置保存成功")
}

View File

@@ -51,7 +51,7 @@ func Login(c *gin.Context) {
User: converter.ToUserResponse(user),
}
c.JSON(http.StatusOK, response)
SuccessResponse(c, response, "登录成功")
}
// Register 用户注册

View File

@@ -111,6 +111,10 @@ func main() {
api.GET("/search-stats/trend", handlers.GetSearchTrend)
api.GET("/search-stats/keyword/:keyword/trend", handlers.GetKeywordTrend)
api.POST("/search-stats/record", handlers.RecordSearch)
// 系统配置路由
api.GET("/system/config", handlers.GetSystemConfig)
api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig)
}
// 静态文件服务

View File

@@ -0,0 +1,26 @@
<template>
<div v-if="show" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ title }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ message }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
show: boolean
title?: string
message?: string
}
withDefaults(defineProps<Props>(), {
title: '正在加载...',
message: '请稍候'
})
</script>

View File

@@ -430,3 +430,34 @@ export const useStatsApi = () => {
getStats,
}
}
// 系统配置相关API
export const useSystemConfigApi = () => {
const config = useRuntimeConfig()
const getAuthHeaders = () => {
const userStore = useUserStore()
return userStore.authHeaders
}
const getSystemConfig = async () => {
return await $fetch('/system/config', {
baseURL: config.public.apiBase,
// GET接口不需要认证头
})
}
const updateSystemConfig = async (data: any) => {
return await $fetch('/system/config', {
baseURL: config.public.apiBase,
method: 'POST',
body: data,
headers: getAuthHeaders() as Record<string, string>
})
}
return {
getSystemConfig,
updateSystemConfig,
}
}

84
web/composables/useSeo.ts Normal file
View File

@@ -0,0 +1,84 @@
import { ref, computed } from 'vue'
interface SystemConfig {
id: number
site_title: string
site_description: string
keywords: string
author: string
copyright: string
auto_process_ready_resources: boolean
auto_process_interval: number
page_size: number
maintenance_mode: boolean
created_at: string
updated_at: string
}
export const useSeo = () => {
const systemConfig = ref<SystemConfig | null>(null)
const { getSystemConfig } = useSystemConfigApi()
// 获取系统配置
const fetchSystemConfig = async () => {
try {
const response = await getSystemConfig() as any
console.log('系统配置响应:', response)
if (response && response.success && response.data) {
systemConfig.value = response.data
} else if (response && response.data) {
// 兼容非标准格式
systemConfig.value = response.data
}
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
// 生成页面标题
const generateTitle = (pageTitle: string) => {
if (systemConfig.value?.site_title) {
return `${systemConfig.value.site_title} - ${pageTitle}`
}
return `${pageTitle} - 网盘资源管理系统`
}
// 生成页面元数据
const generateMeta = (customMeta?: Record<string, string>) => {
const defaultMeta = {
description: systemConfig.value?.site_description || '专业的网盘资源管理系统',
keywords: systemConfig.value?.keywords || '网盘,资源管理,文件分享',
author: systemConfig.value?.author || '系统管理员',
copyright: systemConfig.value?.copyright || '© 2024 网盘资源管理系统'
}
return {
...defaultMeta,
...customMeta
}
}
// 设置页面SEO
const setPageSeo = (pageTitle: string, customMeta?: Record<string, string>) => {
const title = generateTitle(pageTitle)
const meta = generateMeta(customMeta)
useHead({
title,
meta: [
{ name: 'description', content: meta.description },
{ name: 'keywords', content: meta.keywords },
{ name: 'author', content: meta.author },
{ name: 'copyright', content: meta.copyright }
]
})
}
return {
systemConfig,
fetchSystemConfig,
generateTitle,
generateMeta,
setPageSeo
}
}

View File

@@ -1,11 +1,26 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在初始化管理后台</p>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 text-white rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
{{ systemConfig?.site_title || '网盘资源管理系统' }}
</NuxtLink>
</h1>
<div class="flex items-center gap-4">
<div class="text-sm">
@@ -268,6 +283,9 @@ definePageMeta({
middleware: 'auth'
})
// API
const { getSystemConfig } = useSystemConfigApi()
const router = useRouter()
const userStore = useUserStore()
const { $api } = useNuxtApp()
@@ -275,15 +293,59 @@ const { $api } = useNuxtApp()
const user = ref(null)
const stats = ref(null)
const showAddResourceModal = ref(false)
const pageLoading = ref(true) // 添加页面加载状态
const systemConfig = ref(null) // 添加系统配置状态
// 页面元数据 - 移到变量声明之后
useHead({
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 管理后台` : '管理后台 - 网盘资源管理系统',
meta: [
{
name: 'description',
content: () => systemConfig.value?.site_description || '网盘资源管理系统管理后台'
},
{
name: 'keywords',
content: () => systemConfig.value?.keywords || '网盘,资源管理,管理后台'
},
{
name: 'author',
content: () => systemConfig.value?.author || '系统管理员'
}
]
})
// 获取系统配置
const fetchSystemConfig = async () => {
try {
const response = await getSystemConfig()
console.log('admin系统配置响应:', response)
if (response && response.success && response.data) {
systemConfig.value = response.data
} else if (response && response.data) {
// 兼容非标准格式
systemConfig.value = response.data
}
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
// 检查认证状态
const checkAuth = () => {
console.log('admin - checkAuth 开始')
userStore.initAuth()
console.log('admin - isAuthenticated:', userStore.isAuthenticated)
console.log('admin - user:', userStore.userInfo)
if (!userStore.isAuthenticated) {
console.log('admin - 用户未认证,重定向到首页')
router.push('/')
return
}
console.log('admin - 用户已认证,继续')
}
// 获取统计信息
@@ -324,7 +386,7 @@ const goToBatchAdd = () => {
}
const goToSystemSettings = () => {
// 实现系统设置页面跳转
router.push('/system-config')
}
const goToUserManagement = () => {
@@ -336,9 +398,19 @@ const goToHotKeywords = () => {
}
// 页面加载时检查认证
onMounted(() => {
checkAuth()
fetchStats()
onMounted(async () => {
try {
checkAuth()
await Promise.all([
fetchStats(),
fetchSystemConfig()
])
} catch (error) {
console.error('admin页面初始化失败:', error)
} finally {
// 所有数据加载完成后,关闭加载状态
pageLoading.value = false
}
})
</script>

View File

@@ -1,21 +1,36 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在初始化系统</p>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源管理系统</a>
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
{{ systemConfig?.site_title || '网盘资源管理系统' }}
</a>
</h1>
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4 right-4 top-0 absolute">
<NuxtLink
v-if="!userStore.isAuthenticated"
v-if="authInitialized && !userStore.isAuthenticated"
to="/login"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-sign-in-alt"></i> 登录
</NuxtLink>
<NuxtLink
v-if="userStore.isAuthenticated"
v-if="authInitialized && userStore.isAuthenticated"
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
@@ -202,7 +217,7 @@
<footer class="mt-8 py-6 border-t border-gray-200">
<div class="max-w-7xl mx-auto text-center text-gray-600 text-sm">
<p class="mb-2">本站内容由网络爬虫自动抓取本站不储存复制传播任何文件仅作个人公益学习请在获取后24小内删除!!!</p>
<p>© 2025 网盘资源管理系统 By 小七</p>
<p>{{ systemConfig?.copyright || 2025 网盘资源管理系统 By 小七' }}</p>
</div>
</footer>
</div>
@@ -213,8 +228,77 @@ const store = useResourceStore()
const userStore = useUserStore()
const { resources, categories, stats, loading } = storeToRefs(store)
// 响应式数据
const searchQuery = ref('')
const selectedPlatform = ref('')
const authInitialized = ref(false) // 添加认证状态初始化标志
const pageLoading = ref(true) // 添加页面加载状态
const systemConfig = ref<SystemConfig | null>(null) // 添加系统配置状态
// 动态SEO配置
const seoConfig = computed(() => ({
title: systemConfig.value?.site_title || '网盘资源管理系统',
meta: [
{
name: 'description',
content: systemConfig.value?.site_description || '专业的网盘资源管理系统'
},
{
name: 'keywords',
content: systemConfig.value?.keywords || '网盘,资源管理,文件分享'
},
{
name: 'author',
content: systemConfig.value?.author || '系统管理员'
},
{
name: 'copyright',
content: systemConfig.value?.copyright || '© 2024 网盘资源管理系统'
}
]
}))
// 页面元数据 - 使用watchEffect来避免组件卸载时的错误
watchEffect(() => {
if (systemConfig.value) {
useHead({
title: systemConfig.value.site_title || '网盘资源管理系统',
meta: [
{
name: 'description',
content: systemConfig.value.site_description || '专业的网盘资源管理系统'
},
{
name: 'keywords',
content: systemConfig.value.keywords || '网盘,资源管理,文件分享'
},
{
name: 'author',
content: systemConfig.value.author || '系统管理员'
},
{
name: 'copyright',
content: systemConfig.value.copyright || '© 2024 网盘资源管理系统'
}
]
})
} else {
// 默认SEO配置
useHead({
title: '网盘资源管理系统',
meta: [
{ name: 'description', content: '专业的网盘资源管理系统' },
{ name: 'keywords', content: '网盘,资源管理,文件分享' },
{ name: 'author', content: '系统管理员' },
{ name: 'copyright', content: '© 2024 网盘资源管理系统' }
]
})
}
})
// API
const { getSystemConfig } = useSystemConfigApi()
// const showAddResourceModal = ref(false)
const editingResource = ref(null)
const currentPage = ref(1)
@@ -250,6 +334,21 @@ interface ExtendedResource {
showLink?: boolean
}
interface SystemConfig {
id: number
site_title: string
site_description: string
keywords: string
author: string
copyright: string
auto_process_ready_resources: boolean
auto_process_interval: number
page_size: number
maintenance_mode: boolean
created_at: string
updated_at: string
}
const platforms = ref<Platform[]>([])
const todayUpdates = ref(0)
@@ -262,18 +361,50 @@ const debounceSearch = () => {
}, 500)
}
// 获取系统配置
const fetchSystemConfig = async () => {
try {
const response = await getSystemConfig() as any
console.log('首页系统配置响应:', response)
if (response && response.success && response.data) {
systemConfig.value = response.data
} else if (response && response.data) {
// 兼容非标准格式
systemConfig.value = response.data
}
} catch (error) {
console.error('获取系统配置失败:', error)
}
}
// 获取数据
onMounted(async () => {
console.log('首页 - onMounted 开始')
// 初始化用户状态
userStore.initAuth()
authInitialized.value = true // 设置认证状态初始化完成
await Promise.all([
store.fetchResources(),
store.fetchCategories(),
store.fetchStats(),
fetchPlatforms(),
])
animateCounters()
console.log('首页 - authInitialized:', authInitialized.value)
console.log('首页 - isAuthenticated:', userStore.isAuthenticated)
console.log('首页 - user:', userStore.userInfo)
try {
await Promise.all([
store.fetchResources(),
store.fetchCategories(),
store.fetchStats(),
fetchPlatforms(),
fetchSystemConfig(), // 获取系统配置
])
animateCounters()
} catch (error) {
console.error('页面数据加载失败:', error)
} finally {
// 所有数据加载完成后,关闭加载状态
pageLoading.value = false
console.log('首页 - onMounted 完成')
}
})
// 获取平台列表

View File

@@ -1,11 +1,21 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<!-- 全局加载状态 -->
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<div class="text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候正在加载待处理资源</p>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源管理系统</NuxtLink>
</h1>
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
@@ -13,13 +23,12 @@
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
<button
@click="showAddModal = true"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i> 批量添加
</button>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">网盘资源管理系统</NuxtLink>
</h1>
</div>
</div>
<!-- 批量添加模态框 -->
@@ -72,7 +81,12 @@ https://pan.baidu.com/s/345678</pre>
<!-- 操作按钮 -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-900">待处理资源管理</h2>
<button
@click="showAddModal = true"
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-plus"></i> 批量添加
</button>
<div class="flex gap-2">
<button
@click="refreshData"
@@ -237,6 +251,7 @@ const readyResources = ref<ReadyResource[]>([])
const loading = ref(false)
const showAddModal = ref(false)
const resourceText = ref('')
const pageLoading = ref(true) // 添加页面加载状态
// 分页相关状态
const currentPage = ref(1)
@@ -414,8 +429,15 @@ const checkUrlSafety = (url: string) => {
}
// 页面加载时获取数据
onMounted(() => {
fetchData()
onMounted(async () => {
try {
await fetchData()
} catch (error) {
console.error('页面初始化失败:', error)
} finally {
// 数据加载完成后,关闭加载状态
pageLoading.value = false
}
})
</script>

353
web/pages/system-config.vue Normal file
View File

@@ -0,0 +1,353 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
<div class="max-w-4xl mx-auto">
<!-- 头部 -->
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
<NuxtLink
to="/admin"
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
>
<i class="fas fa-arrow-left"></i> 返回
</NuxtLink>
</nav>
<div class="flex-1">
<h1 class="text-2xl sm:text-3xl font-bold">
{{ systemConfig?.site_title ? `${systemConfig.site_title} - 系统配置` : '系统配置' }}
</h1>
</div>
</div>
<!-- 配置表单 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<form @submit.prevent="saveConfig" class="space-y-6">
<!-- SEO 配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-search text-blue-600"></i>
SEO 配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网站标题 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站标题 *
</label>
<input
v-model="config.siteTitle"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘资源管理系统"
/>
</div>
<!-- 网站描述 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
网站描述
</label>
<input
v-model="config.siteDescription"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="专业的网盘资源管理系统"
/>
</div>
<!-- 关键词 -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
关键词 (用逗号分隔)
</label>
<input
v-model="config.keywords"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="网盘,资源管理,文件分享"
/>
</div>
<!-- 作者 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
作者
</label>
<input
v-model="config.author"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="系统管理员"
/>
</div>
<!-- 版权信息 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
版权信息
</label>
<input
v-model="config.copyright"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="© 2024 网盘资源管理系统"
/>
</div>
</div>
</div>
<!-- 自动处理配置 -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-cogs text-green-600"></i>
自动处理配置
</h2>
<div class="space-y-4">
<!-- 待处理资源自动处理 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
待处理资源自动处理
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后系统将自动处理待处理的资源无需手动操作
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.autoProcessReadyResources"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
<!-- 自动处理间隔 -->
<div v-if="config.autoProcessReadyResources" class="ml-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
自动处理间隔 (分钟)
</label>
<input
v-model.number="config.autoProcessInterval"
type="number"
min="1"
max="1440"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="30"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
建议设置 5-60 分钟避免过于频繁的处理
</p>
</div>
</div>
</div>
<!-- 其他配置 -->
<div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i class="fas fa-info-circle text-purple-600"></i>
其他配置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 每页显示数量 -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
每页显示数量
</label>
<select
v-model.number="config.pageSize"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
>
<option value="20">20 </option>
<option value="50">50 </option>
<option value="100">100 </option>
<option value="200">200 </option>
</select>
</div>
<!-- 系统维护模式 -->
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex-1">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
维护模式
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
开启后普通用户无法访问系统
</p>
</div>
<div class="ml-4">
<label class="relative inline-flex items-center cursor-pointer">
<input
v-model="config.maintenanceMode"
type="checkbox"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-red-300 dark:peer-focus:ring-red-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-red-600"></div>
</label>
</div>
</div>
</div>
</div>
<!-- 保存按钮 -->
<div class="flex justify-end space-x-4 pt-6">
<button
type="button"
@click="resetForm"
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
重置
</button>
<button
type="submit"
:disabled="loading"
class="px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<i v-if="loading" class="fas fa-spinner fa-spin mr-2"></i>
{{ loading ? '保存中...' : '保存配置' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// API
const { getSystemConfig, updateSystemConfig } = useSystemConfigApi()
// 响应式数据
const loading = ref(false)
const config = ref({
// SEO 配置
siteTitle: '网盘资源管理系统',
siteDescription: '专业的网盘资源管理系统',
keywords: '网盘,资源管理,文件分享',
author: '系统管理员',
copyright: '© 2024 网盘资源管理系统',
// 自动处理配置
autoProcessReadyResources: false,
autoProcessInterval: 30,
// 其他配置
pageSize: 100,
maintenanceMode: false
})
// 系统配置状态用于SEO
const systemConfig = ref(null)
// 页面元数据 - 移到变量声明之后
useHead({
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 网盘资源管理系统',
meta: [
{
name: 'description',
content: () => systemConfig.value?.site_description || '系统配置管理页面'
},
{
name: 'keywords',
content: () => systemConfig.value?.keywords || '系统配置,管理'
},
{
name: 'author',
content: () => systemConfig.value?.author || '系统管理员'
}
]
})
// 加载配置
const loadConfig = async () => {
try {
loading.value = true
const response = await getSystemConfig()
console.log('系统配置响应:', response)
if (response && response.success && response.data) {
config.value = {
siteTitle: response.data.site_title || '网盘资源管理系统',
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
keywords: response.data.keywords || '网盘,资源管理,文件分享',
author: response.data.author || '系统管理员',
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
autoProcessInterval: response.data.auto_process_interval || 30,
pageSize: response.data.page_size || 100,
maintenanceMode: response.data.maintenance_mode || false
}
systemConfig.value = response.data // 更新系统配置状态
} else if (response && response.data) {
// 兼容非标准格式
config.value = {
siteTitle: response.data.site_title || '网盘资源管理系统',
siteDescription: response.data.site_description || '专业的网盘资源管理系统',
keywords: response.data.keywords || '网盘,资源管理,文件分享',
author: response.data.author || '系统管理员',
copyright: response.data.copyright || '© 2024 网盘资源管理系统',
autoProcessReadyResources: response.data.auto_process_ready_resources || false,
autoProcessInterval: response.data.auto_process_interval || 30,
pageSize: response.data.page_size || 100,
maintenanceMode: response.data.maintenance_mode || false
}
systemConfig.value = response.data
}
} catch (error) {
console.error('加载配置失败:', error)
// 显示错误提示
} finally {
loading.value = false
}
}
// 保存配置
const saveConfig = async () => {
try {
loading.value = true
const requestData = {
site_title: config.value.siteTitle,
site_description: config.value.siteDescription,
keywords: config.value.keywords,
author: config.value.author,
copyright: config.value.copyright,
auto_process_ready_resources: config.value.autoProcessReadyResources,
auto_process_interval: config.value.autoProcessInterval,
page_size: config.value.pageSize,
maintenance_mode: config.value.maintenanceMode
}
const response = await updateSystemConfig(requestData)
if (response.success) {
alert('配置保存成功!')
} else {
alert('保存配置失败:' + (response.message || '未知错误'))
}
} catch (error) {
console.error('保存配置失败:', error)
alert('保存配置失败,请重试')
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
if (confirm('确定要重置所有配置吗?')) {
loadConfig()
}
}
// 页面加载时获取配置
onMounted(() => {
loadConfig()
})
</script>

134
web/pages/test-auth.vue Normal file
View File

@@ -0,0 +1,134 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-bold mb-6">登录状态测试</h1>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">当前状态</h2>
<div class="space-y-2">
<div class="flex justify-between">
<span>是否已认证:</span>
<span :class="userStore.isAuthenticated ? 'text-green-600' : 'text-red-600'">
{{ userStore.isAuthenticated ? '是' : '否' }}
</span>
</div>
<div class="flex justify-between">
<span>用户名:</span>
<span>{{ userStore.userInfo?.username || '未登录' }}</span>
</div>
<div class="flex justify-between">
<span>角色:</span>
<span>{{ userStore.userInfo?.role || '未登录' }}</span>
</div>
<div class="flex justify-between">
<span>Token:</span>
<span>{{ userStore.token ? '存在' : '不存在' }}</span>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">localStorage 状态</h2>
<div class="space-y-2">
<div class="flex justify-between">
<span>Token:</span>
<span>{{ localStorageToken ? '存在' : '不存在' }}</span>
</div>
<div class="flex justify-between">
<span>用户信息:</span>
<span>{{ localStorageUser ? '存在' : '不存在' }}</span>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">操作</h2>
<div class="space-y-4">
<button
@click="initAuth"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
重新初始化认证状态
</button>
<button
@click="clearStorage"
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 ml-2"
>
清除localStorage
</button>
<button
@click="refreshPage"
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 ml-2"
>
刷新页面
</button>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4">调试信息</h2>
<pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded text-sm overflow-auto">{{ debugInfo }}</pre>
</div>
</div>
</div>
</template>
<script setup>
const userStore = useUserStore()
const localStorageToken = ref('')
const localStorageUser = ref('')
const debugInfo = ref('')
// 检查localStorage
const checkLocalStorage = () => {
if (typeof window !== 'undefined') {
localStorageToken.value = localStorage.getItem('token') ? '存在' : '不存在'
localStorageUser.value = localStorage.getItem('user') ? '存在' : '不存在'
}
}
// 初始化认证状态
const initAuth = () => {
userStore.initAuth()
checkLocalStorage()
updateDebugInfo()
}
// 清除localStorage
const clearStorage = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('token')
localStorage.removeItem('user')
checkLocalStorage()
updateDebugInfo()
}
}
// 刷新页面
const refreshPage = () => {
window.location.reload()
}
// 更新调试信息
const updateDebugInfo = () => {
debugInfo.value = JSON.stringify({
store: {
isAuthenticated: userStore.isAuthenticated,
user: userStore.userInfo,
token: userStore.token ? '存在' : '不存在'
},
localStorage: {
token: localStorageToken.value,
user: localStorageUser.value
}
}, null, 2)
}
// 页面加载时检查状态
onMounted(() => {
userStore.initAuth()
checkLocalStorage()
updateDebugInfo()
})
</script>

View File

@@ -46,15 +46,21 @@ export const useUserStore = defineStore('user', {
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
console.log('initAuth - token:', token ? 'exists' : 'not found')
console.log('initAuth - userStr:', userStr ? 'exists' : 'not found')
if (token && userStr) {
try {
this.token = token
this.user = JSON.parse(userStr)
this.isAuthenticated = true
console.log('initAuth - 状态恢复成功:', this.user?.username)
} catch (error) {
console.error('解析用户信息失败:', error)
this.logout()
}
} else {
console.log('initAuth - 没有找到有效的登录信息')
}
}
},
@@ -66,19 +72,29 @@ export const useUserStore = defineStore('user', {
const authApi = useAuthApi()
const response = await authApi.login(credentials)
if (response.token) {
this.token = response.token
this.user = response.user
this.isAuthenticated = true
console.log('login - 响应:', response)
// 保存到localStorage
localStorage.setItem('token', response.token)
localStorage.setItem('user', JSON.stringify(response.user))
// 处理标准化的响应格式
if (response.success && response.data) {
const { token, user } = response.data
if (token && user) {
this.token = token
this.user = user
this.isAuthenticated = true
return { success: true }
} else {
return { success: false, message: '登录失败,服务器未返回有效令牌' }
// 保存到localStorage
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
console.log('login - 状态保存成功:', user.username)
console.log('login - localStorage token:', localStorage.getItem('token') ? 'saved' : 'not saved')
console.log('login - localStorage user:', localStorage.getItem('user') ? 'saved' : 'not saved')
return { success: true }
}
}
return { success: false, message: '登录失败,服务器未返回有效数据' }
} catch (error: any) {
console.error('登录错误:', error)
// 处理HTTP错误响应