mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67e15e03dc | ||
|
|
4ad176273e | ||
|
|
a606897253 | ||
|
|
cf31106cb7 | ||
|
|
a21554f1cd | ||
|
|
edfb0a43aa | ||
|
|
35052f7735 | ||
|
|
8a3d01fd28 | ||
|
|
6e59133924 | ||
|
|
91b743999a | ||
|
|
ed6a1567f3 | ||
|
|
d3ed3ef990 | ||
|
|
ea60d730e2 | ||
|
|
21e2779d28 | ||
|
|
c54a78c67f | ||
|
|
db41ba5ce3 | ||
|
|
72f7764e36 | ||
|
|
8ed7cbc181 | ||
|
|
51975ad408 | ||
|
|
1bb14e218e | ||
|
|
3646c371a4 | ||
|
|
687fc6062d | ||
|
|
505e508bca | ||
|
|
d481083140 |
@@ -3,6 +3,7 @@ FROM node:20-slim AS frontend-builder
|
||||
|
||||
# 安装pnpm
|
||||
RUN npm install -g pnpm
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app/web
|
||||
COPY web/package*.json ./
|
||||
|
||||
43
README.md
43
README.md
@@ -1,4 +1,4 @@
|
||||
# 🚀 urlDB - 网盘资源数据库
|
||||
# 🚀 urlDB - 老九网盘资源数据库
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -90,7 +90,7 @@ cd urldb
|
||||
docker compose up --build -d
|
||||
|
||||
# 访问应用
|
||||
# 前端: http://localhost:3000
|
||||
# 前端: http://localhost:3030
|
||||
# 后端API: http://localhost:8080
|
||||
```
|
||||
|
||||
@@ -186,9 +186,9 @@ l9pan/
|
||||
./scripts/version.sh show
|
||||
|
||||
# 更新版本号
|
||||
./scripts/version.sh patch # 修订版本 (1.0.0 -> 1.0.1)
|
||||
./scripts/version.sh minor # 次版本 (1.0.0 -> 1.1.0)
|
||||
./scripts/version.sh major # 主版本 (1.0.0 -> 2.0.0)
|
||||
./scripts/version.sh patch # 修订版本 1.0.7)
|
||||
./scripts/version.sh minor # 次版本 1.0.7)
|
||||
./scripts/version.sh major # 主版本 1.0.7)
|
||||
|
||||
# 发布版本到GitHub
|
||||
./scripts/version.sh release
|
||||
@@ -200,25 +200,6 @@ l9pan/
|
||||
./scripts/version.sh help
|
||||
```
|
||||
|
||||
#### 自动发布流程
|
||||
|
||||
1. **更新版本号**: 修改 `VERSION` 文件
|
||||
2. **同步文件**: 更新 `package.json`、`docker-compose.yml`、`README.md`
|
||||
3. **创建Git标签**: 自动创建版本标签
|
||||
4. **推送代码**: 推送代码和标签到GitHub
|
||||
5. **创建Release**: 自动创建GitHub Release
|
||||
|
||||
#### 版本API接口
|
||||
|
||||
- `GET /api/version` - 获取版本信息
|
||||
- `GET /api/version/string` - 获取版本字符串
|
||||
- `GET /api/version/full` - 获取完整版本信息
|
||||
- `GET /api/version/check-update` - 检查GitHub上的最新版本
|
||||
|
||||
#### 版本信息页面
|
||||
|
||||
访问 `/version` 页面查看详细的版本信息和更新状态。
|
||||
|
||||
#### 详细文档
|
||||
|
||||
查看 [GitHub版本管理指南](docs/github-version-management.md) 了解完整的版本管理流程。
|
||||
@@ -241,9 +222,17 @@ PORT=8080
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| frontend | 3000 | Nuxt.js 前端应用 |
|
||||
| backend | 8080 | Go API 后端服务 |
|
||||
| postgres | 5432 | PostgreSQL 数据库 |
|
||||
| server | 3030 | 应用 |
|
||||
| postgres | 5431 | PostgreSQL 数据库 |
|
||||
|
||||
### 镜像构建
|
||||
|
||||
```
|
||||
docker build -t ctwj/urldb-frontend:1.0.7 --target frontend .
|
||||
docker build -t ctwj/urldb-backend:1.0.7 --target backend .
|
||||
docker push ctwj/urldb-frontend:1.0.7
|
||||
docker push ctwj/urldb-backend:1.0.7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
@@ -54,6 +55,17 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 配置数据库连接池
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
|
||||
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
|
||||
|
||||
// 自动迁移数据库表结构
|
||||
err = DB.AutoMigrate(
|
||||
&entity.User{},
|
||||
|
||||
@@ -3,6 +3,7 @@ package converter
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ToResourceResponse 将Resource实体转换为ResourceResponse
|
||||
@@ -21,6 +22,9 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
IsPublic: resource.IsPublic,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
}
|
||||
|
||||
// 设置分类名称
|
||||
@@ -206,3 +210,25 @@ func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource
|
||||
Extra: req.Extra,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应
|
||||
func SystemConfigToPublicResponse(config *entity.SystemConfig) gin.H {
|
||||
return gin.H{
|
||||
"id": config.ID,
|
||||
"created_at": config.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"updated_at": config.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
"site_title": config.SiteTitle,
|
||||
"site_description": config.SiteDescription,
|
||||
"keywords": config.Keywords,
|
||||
"author": config.Author,
|
||||
"copyright": config.Copyright,
|
||||
"auto_process_ready_resources": config.AutoProcessReadyResources,
|
||||
"auto_process_interval": config.AutoProcessInterval,
|
||||
"auto_transfer_enabled": config.AutoTransferEnabled,
|
||||
"auto_transfer_limit_days": config.AutoTransferLimitDays,
|
||||
"auto_transfer_min_space": config.AutoTransferMinSpace,
|
||||
"auto_fetch_hot_drama_enabled": config.AutoFetchHotDramaEnabled,
|
||||
"page_size": config.PageSize,
|
||||
"maintenance_mode": config.MaintenanceMode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ type CreateResourceRequest struct {
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// UpdateResourceRequest 更新资源请求
|
||||
@@ -72,6 +75,9 @@ type UpdateResourceRequest struct {
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// CreateCategoryRequest 创建分类请求
|
||||
|
||||
@@ -27,6 +27,9 @@ type ResourceResponse struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
|
||||
@@ -22,6 +22,9 @@ type Resource struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
Cover string `json:"cover" gorm:"size:500;comment:封面"`
|
||||
Author string `json:"author" gorm:"size:100;comment:作者"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
|
||||
|
||||
// 关联关系
|
||||
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
|
||||
|
||||
@@ -11,7 +11,7 @@ type SystemConfig struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// SEO 配置
|
||||
SiteTitle string `json:"site_title" gorm:"size:200;not null;default:'网盘资源数据库'"`
|
||||
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"`
|
||||
|
||||
@@ -73,3 +73,7 @@ func (r *BaseRepositoryImpl[T]) FindWithPagination(page, limit int) ([]T, int64,
|
||||
err := r.db.Offset(offset).Limit(limit).Find(&entities).Error
|
||||
return entities, total, err
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetDB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type CksRepository interface {
|
||||
FindByIsValid(isValid bool) ([]entity.Cks, error)
|
||||
UpdateSpace(id uint, space, leftSpace int64) error
|
||||
DeleteByPanID(panID uint) error
|
||||
UpdateWithAllFields(cks *entity.Cks) error
|
||||
}
|
||||
|
||||
// CksRepositoryImpl Cks的Repository实现
|
||||
@@ -71,3 +72,8 @@ func (r *CksRepositoryImpl) FindByID(id uint) (*entity.Cks, error) {
|
||||
}
|
||||
return &cks, nil
|
||||
}
|
||||
|
||||
// UpdateWithAllFields 更新Cks,包括零值字段
|
||||
func (r *CksRepositoryImpl) UpdateWithAllFields(cks *entity.Cks) error {
|
||||
return r.db.Save(cks).Error
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type ReadyResourceRepository interface {
|
||||
BatchCreate(resources []entity.ReadyResource) error
|
||||
DeleteByURL(url string) error
|
||||
FindAllWithinDays(days int) ([]entity.ReadyResource, error)
|
||||
BatchFindByURLs(urls []string) ([]entity.ReadyResource, error)
|
||||
}
|
||||
|
||||
// ReadyResourceRepositoryImpl ReadyResource的Repository实现
|
||||
@@ -68,3 +69,12 @@ func (r *ReadyResourceRepositoryImpl) FindAllWithinDays(days int) ([]entity.Read
|
||||
err := db.Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
func (r *ReadyResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.ReadyResource, error) {
|
||||
var resources []entity.ReadyResource
|
||||
if len(urls) == 0 {
|
||||
return resources, nil
|
||||
}
|
||||
err := r.db.Where("url IN ?", urls).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type ResourceRepository interface {
|
||||
GetCachedLatestResources(limit int) ([]entity.Resource, error)
|
||||
InvalidateCache() error
|
||||
FindExists(url string, excludeID ...uint) (bool, error)
|
||||
BatchFindByURLs(urls []string) ([]entity.Resource, error)
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -344,3 +345,12 @@ func (r *ResourceRepositoryImpl) FindExists(url string, excludeID ...uint) (bool
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *ResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.Resource, error) {
|
||||
var resources []entity.Resource
|
||||
if len(urls) == 0 {
|
||||
return resources, nil
|
||||
}
|
||||
err := r.db.Where("url IN ?", urls).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
@@ -16,6 +17,7 @@ type SearchStatRepository interface {
|
||||
GetHotKeywords(days int, limit int) ([]entity.KeywordStat, error)
|
||||
GetSearchTrend(days int) ([]entity.DailySearchStat, error)
|
||||
GetKeywordTrend(keyword string, days int) ([]entity.DailySearchStat, error)
|
||||
GetSummary() (map[string]int64, error)
|
||||
}
|
||||
|
||||
// SearchStatRepositoryImpl 搜索统计Repository实现
|
||||
@@ -30,51 +32,34 @@ func NewSearchStatRepository(db *gorm.DB) SearchStatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSearch 记录搜索
|
||||
// RecordSearch 记录搜索(每次都插入新记录)
|
||||
func (r *SearchStatRepositoryImpl) RecordSearch(keyword, ip, userAgent string) error {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
// 查找今天是否已有该关键词的记录
|
||||
var stat entity.SearchStat
|
||||
err := r.db.Where("keyword = ? AND date = ?", keyword, today).First(&stat).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
stat = entity.SearchStat{
|
||||
Keyword: keyword,
|
||||
Count: 1,
|
||||
Date: today,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
return r.db.Create(&stat).Error
|
||||
} else if err != nil {
|
||||
return err
|
||||
stat := entity.SearchStat{
|
||||
Keyword: keyword,
|
||||
Count: 1,
|
||||
Date: time.Now(), // 可保留 date 字段,实际用 created_at 统计
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
|
||||
// 更新现有记录
|
||||
stat.Count++
|
||||
stat.IP = ip
|
||||
stat.UserAgent = userAgent
|
||||
return r.db.Save(&stat).Error
|
||||
return r.db.Create(&stat).Error
|
||||
}
|
||||
|
||||
// GetDailyStats 获取每日统计
|
||||
func (r *SearchStatRepositoryImpl) GetDailyStats(days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date DESC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -82,19 +67,19 @@ func (r *SearchStatRepositoryImpl) GetDailyStats(days int) ([]entity.DailySearch
|
||||
func (r *SearchStatRepositoryImpl) GetHotKeywords(days int, limit int) ([]entity.KeywordStat, error) {
|
||||
var keywords []entity.KeywordStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
keyword,
|
||||
SUM(count) as count,
|
||||
RANK() OVER (ORDER BY SUM(count) DESC) as rank
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY keyword
|
||||
ORDER BY count DESC
|
||||
LIMIT ?
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days, limit).Scan(&keywords).Error
|
||||
err := r.db.Raw(query, limit).Scan(&keywords).Error
|
||||
return keywords, err
|
||||
}
|
||||
|
||||
@@ -102,18 +87,18 @@ func (r *SearchStatRepositoryImpl) GetHotKeywords(days int, limit int) ([]entity
|
||||
func (r *SearchStatRepositoryImpl) GetSearchTrend(days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -121,17 +106,54 @@ func (r *SearchStatRepositoryImpl) GetSearchTrend(days int) ([]entity.DailySearc
|
||||
func (r *SearchStatRepositoryImpl) GetKeywordTrend(keyword string, days int) ([]entity.DailySearchStat, error) {
|
||||
var stats []entity.DailySearchStat
|
||||
|
||||
query := `
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date,
|
||||
SUM(count) as total_searches,
|
||||
COUNT(DISTINCT keyword) as unique_keywords
|
||||
FROM search_stats
|
||||
WHERE keyword = ? AND date >= CURRENT_DATE - INTERVAL '? days'
|
||||
WHERE keyword = ? AND date >= CURRENT_DATE - INTERVAL '%d days'
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`
|
||||
`, days)
|
||||
|
||||
err := r.db.Raw(query, keyword, days).Scan(&stats).Error
|
||||
err := r.db.Raw(query, keyword).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// GetSummary 获取搜索统计汇总
|
||||
func (r *SearchStatRepositoryImpl) GetSummary() (map[string]int64, error) {
|
||||
var total, today, week, month, keywords int64
|
||||
now := time.Now()
|
||||
todayStr := now.Format("2006-01-02")
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format("2006-01-02") // 周一
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
// 总搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 今日搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("DATE(created_at) = ?", todayStr).Count(&today).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 本周搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("created_at >= ?", weekStart).Count(&week).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 本月搜索次数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Where("created_at >= ?", monthStart).Count(&month).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 总关键词数
|
||||
if err := r.db.Model(&entity.SearchStat{}).Distinct("keyword").Count(&keywords).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]int64{
|
||||
"total": total,
|
||||
"today": today,
|
||||
"week": week,
|
||||
"month": month,
|
||||
"keywords": keywords,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() (*entity.SystemConfig,
|
||||
if err != nil {
|
||||
// 创建默认配置
|
||||
defaultConfig := &entity.SystemConfig{
|
||||
SiteTitle: "网盘资源数据库",
|
||||
SiteDescription: "专业的网盘资源数据库",
|
||||
SiteTitle: "老九网盘资源数据库",
|
||||
SiteDescription: "专业的老九网盘资源数据库",
|
||||
Keywords: "网盘,资源管理,文件分享",
|
||||
Author: "系统管理员",
|
||||
Copyright: "© 2024 网盘资源数据库",
|
||||
Copyright: "© 2024 老九网盘资源数据库",
|
||||
AutoProcessReadyResources: false,
|
||||
AutoProcessInterval: 30,
|
||||
PageSize: 100,
|
||||
|
||||
@@ -20,9 +20,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
image: ctwj/urldb-backend:0.0.1
|
||||
expose:
|
||||
- "8080"
|
||||
image: ctwj/urldb-backend:1.0.7
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
@@ -39,9 +37,10 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
image: ctwj/urldb-frontend:0.0.1
|
||||
image: ctwj/urldb-frontend:1.0.7
|
||||
environment:
|
||||
API_BASE: http://backend:8080/api
|
||||
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
|
||||
NUXT_PUBLIC_API_CLIENT: /api
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -38,7 +38,7 @@ $DOCKER_COMPOSE ps
|
||||
|
||||
echo ""
|
||||
echo "✅ 系统启动完成!"
|
||||
echo "🌐 前端访问地址: http://localhost:3000"
|
||||
echo "🌐 前端访问地址: http://localhost:3030"
|
||||
echo "🔧 后端API地址: http://localhost:8080"
|
||||
echo "🗄️ 数据库地址: localhost:5432"
|
||||
echo ""
|
||||
|
||||
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
p.l9.lc
|
||||
@@ -1,6 +1,6 @@
|
||||
# 🚀 urlDB - 网盘资源数据库
|
||||
# 🚀 urlDB - 老九网盘资源数据库
|
||||
|
||||
> 一个现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘
|
||||
> 一个现代化的老九网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 概述
|
||||
|
||||
网盘资源数据库提供了一套完整的 RESTful API 接口,支持资源管理、搜索、热门剧获取等功能。所有 API 都需要进行认证,使用 API Token 进行身份验证。
|
||||
老九网盘资源数据库提供了一套完整的 RESTful API 接口,支持资源管理、搜索、热门剧获取等功能。所有 API 都需要进行认证,使用 API Token 进行身份验证。
|
||||
|
||||
## 基础信息
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ docker compose ps
|
||||
|
||||
可以通过修改 `docker-compose.yml` 文件中的环境变量来配置服务:
|
||||
|
||||
后端 backend
|
||||
```yaml
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
@@ -55,7 +56,12 @@ environment:
|
||||
DB_PASSWORD: password
|
||||
DB_NAME: url_db
|
||||
PORT: 8080
|
||||
API_BASE: http://localhost:8080/api
|
||||
```
|
||||
|
||||
前端 frontend
|
||||
```yaml
|
||||
environment:
|
||||
API_BASE: /api
|
||||
```
|
||||
|
||||
### 端口映射
|
||||
|
||||
@@ -23,7 +23,7 @@ docker compose up --build -d
|
||||
|
||||
启动成功后,您可以通过以下地址访问:
|
||||
|
||||
- **前端界面**: http://localhost:3000
|
||||
- **前端界面**: http://localhost:3030
|
||||
默认用户密码: admin/password
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>urlDB - 网盘资源数据库</title>
|
||||
<title>urlDB - 老九网盘资源数据库</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="一个现代化的网盘资源数据库,支持多网盘自动化转存分享">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
|
||||
@@ -192,6 +192,8 @@ func UpdateCks(c *gin.Context) {
|
||||
if req.Ck != "" {
|
||||
cks.Ck = req.Ck
|
||||
}
|
||||
// 对于 bool 类型,我们需要检查请求中是否包含该字段
|
||||
// 由于 Go 的 JSON 解析,如果字段存在且为 false,也会被正确解析
|
||||
cks.IsValid = req.IsValid
|
||||
if req.LeftSpace != 0 {
|
||||
cks.LeftSpace = req.LeftSpace
|
||||
@@ -210,7 +212,8 @@ func UpdateCks(c *gin.Context) {
|
||||
cks.Remark = req.Remark
|
||||
}
|
||||
|
||||
err = repoManager.CksRepository.Update(cks)
|
||||
// 使用专门的方法更新,确保更新所有字段包括零值
|
||||
err = repoManager.CksRepository.UpdateWithAllFields(cks)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
@@ -35,41 +33,25 @@ func NewPublicAPIHandler() *PublicAPIHandler {
|
||||
func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
|
||||
var req dto.ReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "请求参数错误: " + err.Error(),
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "标题不能为空",
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "标题不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Url == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "URL不能为空",
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "URL不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
readyResource := converter.RequestToReadyResource(&req)
|
||||
if readyResource == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "数据转换失败",
|
||||
"code": 500,
|
||||
})
|
||||
ErrorResponse(c, "数据转换失败", 500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,21 +61,12 @@ func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
|
||||
// 保存到数据库
|
||||
err := repoManager.ReadyResourceRepository.Create(readyResource)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "添加资源失败: " + err.Error(),
|
||||
"code": 500,
|
||||
})
|
||||
ErrorResponse(c, "添加资源失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "资源添加成功,已进入待处理列表",
|
||||
"data": gin.H{
|
||||
"id": readyResource.ID,
|
||||
},
|
||||
"code": 200,
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": readyResource.ID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,40 +86,24 @@ func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
|
||||
func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
var req dto.BatchReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "请求参数错误: " + err.Error(),
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Resources) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "资源列表不能为空",
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "资源列表不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个资源
|
||||
for i, resource := range req.Resources {
|
||||
if resource.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "第" + strconv.Itoa(i+1) + "个资源标题不能为空",
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源标题不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if resource.Url == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "第" + strconv.Itoa(i+1) + "个资源URL不能为空",
|
||||
"code": 400,
|
||||
})
|
||||
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源URL不能为空", 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -164,14 +121,9 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "批量添加成功,共添加 " + strconv.Itoa(len(createdResources)) + " 个资源",
|
||||
"data": gin.H{
|
||||
"created_count": len(createdResources),
|
||||
"created_ids": createdResources,
|
||||
},
|
||||
"code": 200,
|
||||
SuccessResponse(c, gin.H{
|
||||
"created_count": len(createdResources),
|
||||
"created_ids": createdResources,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,11 +182,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// 执行搜索
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "搜索失败: " + err.Error(),
|
||||
"code": 500,
|
||||
})
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,16 +200,11 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "搜索成功",
|
||||
"data": gin.H{
|
||||
"resources": resourceResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
"code": 200,
|
||||
SuccessResponse(c, gin.H{
|
||||
"list": resourceResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,11 +238,7 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
// 获取热门剧
|
||||
hotDramas, total, err := repoManager.HotDramaRepository.FindAll(page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "获取热门剧失败: " + err.Error(),
|
||||
"code": 500,
|
||||
})
|
||||
ErrorResponse(c, "获取热门剧失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -322,15 +261,10 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "获取热门剧成功",
|
||||
"data": gin.H{
|
||||
"hot_dramas": hotDramaResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
"code": 200,
|
||||
SuccessResponse(c, gin.H{
|
||||
"hot_dramas": hotDramaResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,6 +54,21 @@ func CreateReadyResource(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
// 检查待处理资源表
|
||||
readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs([]string{req.URL})
|
||||
if len(readyList) > 0 {
|
||||
ErrorResponse(c, "该URL已存在于待处理资源列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// 检查资源表
|
||||
resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs([]string{req.URL})
|
||||
if len(resourceList) > 0 {
|
||||
ErrorResponse(c, "该URL已存在于资源列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resource := &entity.ReadyResource{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
@@ -86,8 +101,50 @@ func BatchCreateReadyResources(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 先收集所有待提交的URL,去重
|
||||
urlSet := make(map[string]struct{})
|
||||
for _, reqResource := range req.Resources {
|
||||
if reqResource.URL == "" {
|
||||
continue
|
||||
}
|
||||
urlSet[reqResource.URL] = struct{}{}
|
||||
}
|
||||
uniqueUrls := make([]string, 0, len(urlSet))
|
||||
for url := range urlSet {
|
||||
uniqueUrls = append(uniqueUrls, url)
|
||||
}
|
||||
|
||||
// 2. 批量查询待处理资源表中已存在的URL
|
||||
existReadyUrls := make(map[string]struct{})
|
||||
if len(uniqueUrls) > 0 {
|
||||
readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs(uniqueUrls)
|
||||
for _, r := range readyList {
|
||||
existReadyUrls[r.URL] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 批量查询资源表中已存在的URL
|
||||
existResourceUrls := make(map[string]struct{})
|
||||
if len(uniqueUrls) > 0 {
|
||||
resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs(uniqueUrls)
|
||||
for _, r := range resourceList {
|
||||
existResourceUrls[r.URL] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 过滤掉已存在的URL
|
||||
var resources []entity.ReadyResource
|
||||
for _, reqResource := range req.Resources {
|
||||
url := reqResource.URL
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := existReadyUrls[url]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := existResourceUrls[url]; ok {
|
||||
continue
|
||||
}
|
||||
resource := entity.ReadyResource{
|
||||
Title: reqResource.Title,
|
||||
Description: reqResource.Description,
|
||||
@@ -102,6 +159,14 @@ func BatchCreateReadyResources(c *gin.Context) {
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
SuccessResponse(c, gin.H{
|
||||
"count": 0,
|
||||
"message": "无新增资源,所有URL均已存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.BatchCreate(resources)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -15,35 +15,36 @@ import (
|
||||
func GetResources(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
categoryID := c.Query("category_id")
|
||||
panID := c.Query("pan_id")
|
||||
search := c.Query("search")
|
||||
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
var err error
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
|
||||
// 设置响应头,启用缓存
|
||||
c.Header("Cache-Control", "public, max-age=300") // 5分钟缓存
|
||||
if search := c.Query("search"); search != "" {
|
||||
params["search"] = search
|
||||
}
|
||||
if panID := c.Query("pan_id"); panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
params["pan_id"] = uint(id)
|
||||
}
|
||||
}
|
||||
if categoryID := c.Query("category_id"); categoryID != "" {
|
||||
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
|
||||
params["category_id"] = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
if search != "" && panID != "" {
|
||||
// 平台内搜索
|
||||
panIDUint, _ := strconv.ParseUint(panID, 10, 32)
|
||||
resources, total, err = repoManager.ResourceRepository.SearchByPanID(search, uint(panIDUint), page, pageSize)
|
||||
} else if search != "" {
|
||||
// 全局搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(search, nil, page, pageSize)
|
||||
} else if panID != "" {
|
||||
// 按平台筛选
|
||||
panIDUint, _ := strconv.ParseUint(panID, 10, 32)
|
||||
resources, total, err = repoManager.ResourceRepository.FindByPanIDPaginated(uint(panIDUint), page, pageSize)
|
||||
} else if categoryID != "" {
|
||||
// 按分类筛选
|
||||
categoryIDUint, _ := strconv.ParseUint(categoryID, 10, 32)
|
||||
resources, total, err = repoManager.ResourceRepository.FindByCategoryIDPaginated(uint(categoryIDUint), page, pageSize)
|
||||
} else {
|
||||
// 使用分页查询,避免加载所有数据
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
|
||||
// 搜索统计(仅非管理员)
|
||||
if search, ok := params["search"].(string); ok && search != "" {
|
||||
user, _ := c.Get("user")
|
||||
if user == nil || (user != nil && user.(entity.User).Role != "admin") {
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
repoManager.SearchStatRepository.RecordSearch(search, ip, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -52,7 +53,7 @@ func GetResources(c *gin.Context) {
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"data": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
@@ -124,6 +125,9 @@ func CreateResource(c *gin.Context) {
|
||||
CategoryID: req.CategoryID,
|
||||
IsValid: req.IsValid,
|
||||
IsPublic: req.IsPublic,
|
||||
Cover: req.Cover,
|
||||
Author: req.Author,
|
||||
ErrorMsg: req.ErrorMsg,
|
||||
}
|
||||
|
||||
err := repoManager.ResourceRepository.Create(resource)
|
||||
@@ -192,6 +196,15 @@ func UpdateResource(c *gin.Context) {
|
||||
}
|
||||
resource.IsValid = req.IsValid
|
||||
resource.IsPublic = req.IsPublic
|
||||
if req.Cover != "" {
|
||||
resource.Cover = req.Cover
|
||||
}
|
||||
if req.Author != "" {
|
||||
resource.Author = req.Author
|
||||
}
|
||||
if req.ErrorMsg != "" {
|
||||
resource.ErrorMsg = req.ErrorMsg
|
||||
}
|
||||
|
||||
// 处理标签关联
|
||||
if len(req.TagIDs) > 0 {
|
||||
@@ -245,6 +258,10 @@ func SearchResources(c *gin.Context) {
|
||||
} else {
|
||||
// 有搜索关键词时,执行搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
|
||||
// 新增:记录搜索关键词
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
repoManager.SearchStatRepository.RecordSearch(query, ip, userAgent)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -259,3 +276,37 @@ func SearchResources(c *gin.Context) {
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// 增加资源浏览次数
|
||||
func IncrementResourceViewCount(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = repoManager.ResourceRepository.IncrementViewCount(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, "增加浏览次数失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
SuccessResponse(c, gin.H{"message": "浏览次数+1"})
|
||||
}
|
||||
|
||||
// BatchDeleteResources 批量删除资源
|
||||
func BatchDeleteResources(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
|
||||
ErrorResponse(c, "参数错误", 400)
|
||||
return
|
||||
}
|
||||
count := 0
|
||||
for _, id := range req.IDs {
|
||||
if err := repoManager.ResourceRepository.Delete(id); err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
SuccessResponse(c, gin.H{"deleted": count, "message": "批量删除成功"})
|
||||
}
|
||||
|
||||
@@ -142,3 +142,13 @@ func GetKeywordTrend(c *gin.Context) {
|
||||
response := converter.ToDailySearchStatResponseList(trend)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetSearchStatsSummary 获取搜索统计汇总
|
||||
func GetSearchStatsSummary(c *gin.Context) {
|
||||
summary, err := repoManager.SearchStatRepository.GetSummary()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取搜索统计汇总失败", 500)
|
||||
return
|
||||
}
|
||||
SuccessResponse(c, summary)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -44,16 +45,23 @@ func GetPerformanceStats(c *gin.Context) {
|
||||
sqlDB, err := db.DB.DB()
|
||||
var dbStats gin.H
|
||||
if err == nil {
|
||||
stats := sqlDB.Stats()
|
||||
dbStats = gin.H{
|
||||
"max_open_connections": sqlDB.Stats().MaxOpenConnections,
|
||||
"open_connections": sqlDB.Stats().OpenConnections,
|
||||
"in_use": sqlDB.Stats().InUse,
|
||||
"idle": sqlDB.Stats().Idle,
|
||||
"max_open_connections": stats.MaxOpenConnections,
|
||||
"open_connections": stats.OpenConnections,
|
||||
"in_use": stats.InUse,
|
||||
"idle": stats.Idle,
|
||||
"wait_count": stats.WaitCount,
|
||||
"wait_duration": stats.WaitDuration,
|
||||
}
|
||||
// 添加调试日志
|
||||
utils.Info("数据库连接池状态 - MaxOpen: %d, Open: %d, InUse: %d, Idle: %d",
|
||||
stats.MaxOpenConnections, stats.OpenConnections, stats.InUse, stats.Idle)
|
||||
} else {
|
||||
dbStats = gin.H{
|
||||
"error": "无法获取数据库连接池状态",
|
||||
"error": "无法获取数据库连接池状态: " + err.Error(),
|
||||
}
|
||||
utils.Error("获取数据库连接池状态失败: %v", err)
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
|
||||
@@ -179,3 +179,14 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfig)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// 新增:公开获取系统配置(不含api_token)
|
||||
func GetPublicSystemConfig(c *gin.Context) {
|
||||
config, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
configResponse := converter.SystemConfigToPublicResponse(config)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
7
main.go
7
main.go
@@ -124,6 +124,8 @@ func main() {
|
||||
api.DELETE("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteResource)
|
||||
api.GET("/resources/:id", handlers.GetResourceByID)
|
||||
api.GET("/resources/check-exists", handlers.CheckResourceExists)
|
||||
api.POST("/resources/:id/view", handlers.IncrementResourceViewCount)
|
||||
api.DELETE("/resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchDeleteResources)
|
||||
|
||||
// 分类管理
|
||||
api.GET("/categories", handlers.GetCategories)
|
||||
@@ -183,11 +185,14 @@ func main() {
|
||||
api.GET("/search-stats/daily", handlers.GetDailyStats)
|
||||
api.GET("/search-stats/trend", handlers.GetSearchTrend)
|
||||
api.GET("/search-stats/keyword/:keyword/trend", handlers.GetKeywordTrend)
|
||||
api.POST("/search-stats", handlers.RecordSearch)
|
||||
api.POST("/search-stats/record", handlers.RecordSearch)
|
||||
api.GET("/search-stats/summary", handlers.GetSearchStatsSummary)
|
||||
|
||||
// 系统配置路由
|
||||
api.GET("/system/config", handlers.GetSystemConfig)
|
||||
api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig)
|
||||
api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig)
|
||||
api.GET("/public/system-config", handlers.GetPublicSystemConfig)
|
||||
|
||||
// 热播剧管理路由(查询接口无需认证)
|
||||
api.GET("/hot-dramas", handlers.GetHotDramaList)
|
||||
|
||||
@@ -94,7 +94,7 @@ func GenerateToken(user *entity.User) (string, error) {
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * 24 * time.Hour)), // 30天有效期
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
|
||||
@@ -110,9 +110,22 @@ update_version_in_files() {
|
||||
|
||||
# 更新Docker镜像标签
|
||||
if [ -f "docker-compose.yml" ]; then
|
||||
sed -i.bak "s/image:.*:.*/image: urldb:${new_version}/" docker-compose.yml
|
||||
# 获取当前镜像版本
|
||||
current_backend_version=$(grep -o "ctwj/urldb-backend:[0-9]\+\.[0-9]\+\.[0-9]\+" docker-compose.yml | head -1)
|
||||
current_frontend_version=$(grep -o "ctwj/urldb-frontend:[0-9]\+\.[0-9]\+\.[0-9]\+" docker-compose.yml | head -1)
|
||||
|
||||
if [ -n "$current_backend_version" ]; then
|
||||
sed -i.bak "s|$current_backend_version|ctwj/urldb-backend:${new_version}|" docker-compose.yml
|
||||
echo -e " ✅ 更新 backend 镜像: ${current_backend_version} -> ctwj/urldb-backend:${new_version}"
|
||||
fi
|
||||
|
||||
if [ -n "$current_frontend_version" ]; then
|
||||
sed -i.bak "s|$current_frontend_version|ctwj/urldb-frontend:${new_version}|" docker-compose.yml
|
||||
echo -e " ✅ 更新 frontend 镜像: ${current_frontend_version} -> ctwj/urldb-frontend:${new_version}"
|
||||
fi
|
||||
|
||||
rm -f docker-compose.yml.bak
|
||||
echo -e " ✅ 更新 docker-compose.yml"
|
||||
echo -e " ✅ 更新 docker-compose.yml 完成"
|
||||
fi
|
||||
|
||||
# 更新README中的版本信息
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// 测试新的AdminHeader样式是否与首页完全对齐
|
||||
const testAdminHeaderStyle = async () => {
|
||||
console.log('测试新的AdminHeader样式是否与首页完全对齐...')
|
||||
|
||||
// 测试前端页面AdminHeader
|
||||
console.log('\n1. 测试前端页面AdminHeader:')
|
||||
|
||||
const adminPages = [
|
||||
{ name: '管理后台', url: 'http://localhost:3000/admin' },
|
||||
{ name: '用户管理', url: 'http://localhost:3000/users' },
|
||||
{ name: '分类管理', url: 'http://localhost:3000/categories' },
|
||||
{ name: '标签管理', url: 'http://localhost:3000/tags' },
|
||||
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
|
||||
{ name: '资源管理', url: 'http://localhost:3000/resources' }
|
||||
]
|
||||
|
||||
for (const page of adminPages) {
|
||||
try {
|
||||
const response = await fetch(page.url)
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`\n${page.name}页面:`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查是否包含AdminHeader组件
|
||||
if (html.includes('AdminHeader')) {
|
||||
console.log('✅ 包含AdminHeader组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AdminHeader组件')
|
||||
}
|
||||
|
||||
// 检查是否包含首页样式(深色背景)
|
||||
if (html.includes('bg-slate-800') && html.includes('dark:bg-gray-800')) {
|
||||
console.log('✅ 包含首页样式(深色背景)')
|
||||
} else {
|
||||
console.log('❌ 未找到首页样式')
|
||||
}
|
||||
|
||||
// 检查是否包含首页标题样式
|
||||
if (html.includes('text-2xl sm:text-3xl font-bold mb-4')) {
|
||||
console.log('✅ 包含首页标题样式')
|
||||
} else {
|
||||
console.log('❌ 未找到首页标题样式')
|
||||
}
|
||||
|
||||
// 检查是否包含n-button组件(与首页一致)
|
||||
if (html.includes('n-button') && html.includes('size="tiny"') && html.includes('type="tertiary"')) {
|
||||
console.log('✅ 包含n-button组件(与首页一致)')
|
||||
} else {
|
||||
console.log('❌ 未找到n-button组件')
|
||||
}
|
||||
|
||||
// 检查是否包含右上角绝对定位的按钮
|
||||
if (html.includes('absolute right-4 top-4')) {
|
||||
console.log('✅ 包含右上角绝对定位的按钮')
|
||||
} else {
|
||||
console.log('❌ 未找到右上角绝对定位的按钮')
|
||||
}
|
||||
|
||||
// 检查是否包含首页、添加、退出按钮
|
||||
if (html.includes('fa-home') && html.includes('fa-plus') && html.includes('fa-sign-out-alt')) {
|
||||
console.log('✅ 包含首页、添加、退出按钮')
|
||||
} else {
|
||||
console.log('❌ 未找到完整的按钮组')
|
||||
}
|
||||
|
||||
// 检查是否包含用户信息
|
||||
if (html.includes('欢迎') && html.includes('管理员')) {
|
||||
console.log('✅ 包含用户信息')
|
||||
} else {
|
||||
console.log('❌ 未找到用户信息')
|
||||
}
|
||||
|
||||
// 检查是否包含移动端适配
|
||||
if (html.includes('sm:hidden') && html.includes('hidden sm:flex')) {
|
||||
console.log('✅ 包含移动端适配')
|
||||
} else {
|
||||
console.log('❌ 未找到移动端适配')
|
||||
}
|
||||
|
||||
// 检查是否不包含导航链接(除了首页和添加资源)
|
||||
if (!html.includes('用户管理') && !html.includes('分类管理') && !html.includes('标签管理')) {
|
||||
console.log('✅ 不包含导航链接(符合预期)')
|
||||
} else {
|
||||
console.log('❌ 包含导航链接(不符合预期)')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试首页样式对比
|
||||
console.log('\n2. 测试首页样式对比:')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/')
|
||||
const html = await response.text()
|
||||
|
||||
console.log('首页页面:')
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查首页是否包含相同的样式
|
||||
if (html.includes('bg-slate-800') && html.includes('dark:bg-gray-800')) {
|
||||
console.log('✅ 首页包含相同的深色背景样式')
|
||||
} else {
|
||||
console.log('❌ 首页不包含相同的深色背景样式')
|
||||
}
|
||||
|
||||
// 检查首页是否包含相同的布局结构
|
||||
if (html.includes('text-2xl sm:text-3xl font-bold mb-4')) {
|
||||
console.log('✅ 首页包含相同的标题样式')
|
||||
} else {
|
||||
console.log('❌ 首页不包含相同的标题样式')
|
||||
}
|
||||
|
||||
// 检查首页是否包含相同的n-button样式
|
||||
if (html.includes('n-button') && html.includes('size="tiny"') && html.includes('type="tertiary"')) {
|
||||
console.log('✅ 首页包含相同的n-button样式')
|
||||
} else {
|
||||
console.log('❌ 首页不包含相同的n-button样式')
|
||||
}
|
||||
|
||||
// 检查首页是否包含相同的绝对定位
|
||||
if (html.includes('absolute right-4 top-0')) {
|
||||
console.log('✅ 首页包含相同的绝对定位')
|
||||
} else {
|
||||
console.log('❌ 首页不包含相同的绝对定位')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 首页测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试系统配置API
|
||||
console.log('\n3. 测试系统配置API:')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8080/api/system-config')
|
||||
const data = await response.json()
|
||||
|
||||
console.log('系统配置API响应:')
|
||||
console.log(`状态: ${data.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
if (data.success) {
|
||||
console.log(`网站标题: ${data.data?.site_title || 'N/A'}`)
|
||||
console.log(`版权信息: ${data.data?.copyright || 'N/A'}`)
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
console.log('✅ 系统配置API测试通过')
|
||||
} else {
|
||||
console.log('❌ 系统配置API测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 系统配置API测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ AdminHeader样式测试完成')
|
||||
console.log('\n总结:')
|
||||
console.log('- ✅ AdminHeader样式与首页完全一致')
|
||||
console.log('- ✅ 使用相同的深色背景和圆角设计')
|
||||
console.log('- ✅ 使用相同的n-button组件样式')
|
||||
console.log('- ✅ 按钮位于右上角绝对定位')
|
||||
console.log('- ✅ 包含首页、添加、退出按钮')
|
||||
console.log('- ✅ 包含用户信息和角色显示')
|
||||
console.log('- ✅ 响应式设计,适配移动端')
|
||||
console.log('- ✅ 移除了导航链接,只保留必要操作')
|
||||
console.log('- ✅ 系统配置集成正常')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testAdminHeaderStyle()
|
||||
@@ -1,188 +0,0 @@
|
||||
// 测试AdminHeader组件和版本显示功能
|
||||
const testAdminHeader = async () => {
|
||||
console.log('测试AdminHeader组件和版本显示功能...')
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
// 测试后端版本接口
|
||||
console.log('\n1. 测试后端版本接口:')
|
||||
|
||||
try {
|
||||
const { stdout: versionOutput } = await execAsync('curl -s http://localhost:8080/api/version')
|
||||
const versionData = JSON.parse(versionOutput)
|
||||
|
||||
console.log('版本接口响应:')
|
||||
console.log(`状态: ${versionData.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
console.log(`版本号: ${versionData.data.version}`)
|
||||
console.log(`Git提交: ${versionData.data.git_commit}`)
|
||||
console.log(`构建时间: ${versionData.data.build_time}`)
|
||||
|
||||
if (versionData.success) {
|
||||
console.log('✅ 后端版本接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 后端版本接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 后端版本接口测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试版本字符串接口
|
||||
console.log('\n2. 测试版本字符串接口:')
|
||||
|
||||
try {
|
||||
const { stdout: versionStringOutput } = await execAsync('curl -s http://localhost:8080/api/version/string')
|
||||
const versionStringData = JSON.parse(versionStringOutput)
|
||||
|
||||
console.log('版本字符串接口响应:')
|
||||
console.log(`状态: ${versionStringData.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
console.log(`版本字符串: ${versionStringData.data.version}`)
|
||||
|
||||
if (versionStringData.success) {
|
||||
console.log('✅ 版本字符串接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 版本字符串接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本字符串接口测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试完整版本信息接口
|
||||
console.log('\n3. 测试完整版本信息接口:')
|
||||
|
||||
try {
|
||||
const { stdout: fullVersionOutput } = await execAsync('curl -s http://localhost:8080/api/version/full')
|
||||
const fullVersionData = JSON.parse(fullVersionOutput)
|
||||
|
||||
console.log('完整版本信息接口响应:')
|
||||
console.log(`状态: ${fullVersionData.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
if (fullVersionData.success) {
|
||||
console.log(`版本信息:`, JSON.stringify(fullVersionData.data.version_info, null, 2))
|
||||
}
|
||||
|
||||
if (fullVersionData.success) {
|
||||
console.log('✅ 完整版本信息接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 完整版本信息接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 完整版本信息接口测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试版本更新检查接口
|
||||
console.log('\n4. 测试版本更新检查接口:')
|
||||
|
||||
try {
|
||||
const { stdout: updateCheckOutput } = await execAsync('curl -s http://localhost:8080/api/version/check-update')
|
||||
const updateCheckData = JSON.parse(updateCheckOutput)
|
||||
|
||||
console.log('版本更新检查接口响应:')
|
||||
console.log(`状态: ${updateCheckData.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
if (updateCheckData.success) {
|
||||
console.log(`当前版本: ${updateCheckData.data.current_version}`)
|
||||
console.log(`最新版本: ${updateCheckData.data.latest_version}`)
|
||||
console.log(`有更新: ${updateCheckData.data.has_update}`)
|
||||
console.log(`下载链接: ${updateCheckData.data.download_url || 'N/A'}`)
|
||||
}
|
||||
|
||||
if (updateCheckData.success) {
|
||||
console.log('✅ 版本更新检查接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 版本更新检查接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本更新检查接口测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试前端页面
|
||||
console.log('\n5. 测试前端页面:')
|
||||
|
||||
const testPages = [
|
||||
{ name: '管理后台', url: 'http://localhost:3000/admin' },
|
||||
{ name: '用户管理', url: 'http://localhost:3000/users' },
|
||||
{ name: '分类管理', url: 'http://localhost:3000/categories' },
|
||||
{ name: '标签管理', url: 'http://localhost:3000/tags' },
|
||||
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
|
||||
{ name: '资源管理', url: 'http://localhost:3000/resources' }
|
||||
]
|
||||
|
||||
for (const page of testPages) {
|
||||
try {
|
||||
const response = await fetch(page.url)
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`\n${page.name}页面:`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查是否包含AdminHeader组件
|
||||
if (html.includes('AdminHeader') || html.includes('版本管理')) {
|
||||
console.log('✅ 包含AdminHeader组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AdminHeader组件')
|
||||
}
|
||||
|
||||
// 检查是否包含版本信息
|
||||
if (html.includes('版本') || html.includes('version')) {
|
||||
console.log('✅ 包含版本信息')
|
||||
} else {
|
||||
console.log('❌ 未找到版本信息')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试版本管理脚本
|
||||
console.log('\n6. 测试版本管理脚本:')
|
||||
|
||||
try {
|
||||
const { stdout: scriptHelp } = await execAsync('./scripts/version.sh help')
|
||||
console.log('版本管理脚本帮助信息:')
|
||||
console.log(scriptHelp)
|
||||
|
||||
const { stdout: scriptShow } = await execAsync('./scripts/version.sh show')
|
||||
console.log('当前版本信息:')
|
||||
console.log(scriptShow)
|
||||
|
||||
console.log('✅ 版本管理脚本测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本管理脚本测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试Git标签
|
||||
console.log('\n7. 测试Git标签:')
|
||||
|
||||
try {
|
||||
const { stdout: tagOutput } = await execAsync('git tag -l')
|
||||
console.log('当前Git标签:')
|
||||
console.log(tagOutput || '暂无标签')
|
||||
|
||||
const { stdout: logOutput } = await execAsync('git log --oneline -3')
|
||||
console.log('最近3次提交:')
|
||||
console.log(logOutput)
|
||||
|
||||
console.log('✅ Git标签测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Git标签测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ AdminHeader组件和版本显示功能测试完成')
|
||||
console.log('\n总结:')
|
||||
console.log('- ✅ 后端版本接口正常工作')
|
||||
console.log('- ✅ 前端AdminHeader组件已集成')
|
||||
console.log('- ✅ 版本信息在管理页面右下角显示')
|
||||
console.log('- ✅ 首页已移除版本显示')
|
||||
console.log('- ✅ 版本管理脚本功能完整')
|
||||
console.log('- ✅ Git标签管理正常')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testAdminHeader()
|
||||
@@ -1,155 +0,0 @@
|
||||
// 测试admin layout功能
|
||||
const testAdminLayout = async () => {
|
||||
console.log('测试admin layout功能...')
|
||||
|
||||
// 测试前端页面admin layout
|
||||
console.log('\n1. 测试前端页面admin layout:')
|
||||
|
||||
const adminPages = [
|
||||
{ name: '管理后台', url: 'http://localhost:3000/admin' },
|
||||
{ name: '用户管理', url: 'http://localhost:3000/users' },
|
||||
{ name: '分类管理', url: 'http://localhost:3000/categories' },
|
||||
{ name: '标签管理', url: 'http://localhost:3000/tags' },
|
||||
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
|
||||
{ name: '资源管理', url: 'http://localhost:3000/resources' }
|
||||
]
|
||||
|
||||
for (const page of adminPages) {
|
||||
try {
|
||||
const response = await fetch(page.url)
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`\n${page.name}页面:`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查是否包含AdminHeader组件
|
||||
if (html.includes('AdminHeader')) {
|
||||
console.log('✅ 包含AdminHeader组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AdminHeader组件')
|
||||
}
|
||||
|
||||
// 检查是否包含AppFooter组件
|
||||
if (html.includes('AppFooter')) {
|
||||
console.log('✅ 包含AppFooter组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AppFooter组件')
|
||||
}
|
||||
|
||||
// 检查是否包含admin layout的样式
|
||||
if (html.includes('bg-gray-50 dark:bg-gray-900')) {
|
||||
console.log('✅ 包含admin layout样式')
|
||||
} else {
|
||||
console.log('❌ 未找到admin layout样式')
|
||||
}
|
||||
|
||||
// 检查是否包含页面加载状态
|
||||
if (html.includes('正在加载') || html.includes('初始化管理后台')) {
|
||||
console.log('✅ 包含页面加载状态')
|
||||
} else {
|
||||
console.log('❌ 未找到页面加载状态')
|
||||
}
|
||||
|
||||
// 检查是否包含max-w-7xl mx-auto容器
|
||||
if (html.includes('max-w-7xl mx-auto')) {
|
||||
console.log('✅ 包含标准容器布局')
|
||||
} else {
|
||||
console.log('❌ 未找到标准容器布局')
|
||||
}
|
||||
|
||||
// 检查是否不包含重复的布局代码
|
||||
const adminHeaderCount = (html.match(/AdminHeader/g) || []).length
|
||||
if (adminHeaderCount === 1) {
|
||||
console.log('✅ AdminHeader组件只出现一次(无重复)')
|
||||
} else {
|
||||
console.log(`❌ AdminHeader组件出现${adminHeaderCount}次(可能有重复)`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试admin layout文件是否存在
|
||||
console.log('\n2. 测试admin layout文件:')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/layouts/admin.vue')
|
||||
console.log('admin layout文件状态:', response.status)
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log('✅ admin layout文件存在')
|
||||
} else {
|
||||
console.log('❌ admin layout文件不存在或无法访问')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ admin layout文件测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试definePageMeta是否正确设置
|
||||
console.log('\n3. 测试definePageMeta设置:')
|
||||
|
||||
const pagesWithLayout = [
|
||||
{ name: '管理后台', file: 'web/pages/admin.vue' },
|
||||
{ name: '用户管理', file: 'web/pages/users.vue' },
|
||||
{ name: '分类管理', file: 'web/pages/categories.vue' }
|
||||
]
|
||||
|
||||
for (const page of pagesWithLayout) {
|
||||
try {
|
||||
const fs = require('fs')
|
||||
const content = fs.readFileSync(page.file, 'utf8')
|
||||
|
||||
if (content.includes("definePageMeta({") && content.includes("layout: 'admin'")) {
|
||||
console.log(`✅ ${page.name}页面正确设置了admin layout`)
|
||||
} else {
|
||||
console.log(`❌ ${page.name}页面未正确设置admin layout`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面文件读取失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试首页不使用admin layout
|
||||
console.log('\n4. 测试首页不使用admin layout:')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/')
|
||||
const html = await response.text()
|
||||
|
||||
console.log('首页页面:')
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查首页是否不包含AdminHeader
|
||||
if (!html.includes('AdminHeader')) {
|
||||
console.log('✅ 首页不包含AdminHeader(符合预期)')
|
||||
} else {
|
||||
console.log('❌ 首页包含AdminHeader(不符合预期)')
|
||||
}
|
||||
|
||||
// 检查首页是否使用默认layout
|
||||
if (html.includes('bg-gray-50 dark:bg-gray-900') && html.includes('AppFooter')) {
|
||||
console.log('✅ 首页使用默认layout')
|
||||
} else {
|
||||
console.log('❌ 首页可能使用了错误的layout')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 首页测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ admin layout测试完成')
|
||||
console.log('\n总结:')
|
||||
console.log('- ✅ 创建了admin layout文件')
|
||||
console.log('- ✅ 管理页面使用admin layout')
|
||||
console.log('- ✅ 移除了重复的布局代码')
|
||||
console.log('- ✅ 统一了管理页面的样式和结构')
|
||||
console.log('- ✅ 首页继续使用默认layout')
|
||||
console.log('- ✅ 页面加载状态和错误处理统一')
|
||||
console.log('- ✅ 响应式设计和容器布局统一')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testAdminLayout()
|
||||
@@ -1,140 +0,0 @@
|
||||
// 测试Footer中的版本信息显示
|
||||
const testFooterVersion = async () => {
|
||||
console.log('测试Footer中的版本信息显示...')
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
// 测试后端版本接口
|
||||
console.log('\n1. 测试后端版本接口:')
|
||||
|
||||
try {
|
||||
const { stdout: versionOutput } = await execAsync('curl -s http://localhost:8080/api/version')
|
||||
const versionData = JSON.parse(versionOutput)
|
||||
|
||||
console.log('版本接口响应:')
|
||||
console.log(`状态: ${versionData.success ? '✅ 成功' : '❌ 失败'}`)
|
||||
console.log(`版本号: ${versionData.data.version}`)
|
||||
console.log(`Git提交: ${versionData.data.git_commit}`)
|
||||
console.log(`构建时间: ${versionData.data.build_time}`)
|
||||
|
||||
if (versionData.success) {
|
||||
console.log('✅ 后端版本接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 后端版本接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 后端版本接口测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试前端页面Footer
|
||||
console.log('\n2. 测试前端页面Footer:')
|
||||
|
||||
const testPages = [
|
||||
{ name: '首页', url: 'http://localhost:3000/' },
|
||||
{ name: '热播剧', url: 'http://localhost:3000/hot-dramas' },
|
||||
{ name: '系统监控', url: 'http://localhost:3000/monitor' },
|
||||
{ name: 'API文档', url: 'http://localhost:3000/api-docs' }
|
||||
]
|
||||
|
||||
for (const page of testPages) {
|
||||
try {
|
||||
const response = await fetch(page.url)
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`\n${page.name}页面:`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查是否包含AppFooter组件
|
||||
if (html.includes('AppFooter')) {
|
||||
console.log('✅ 包含AppFooter组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AppFooter组件')
|
||||
}
|
||||
|
||||
// 检查是否包含版本信息
|
||||
if (html.includes('v1.0.0') || html.includes('version')) {
|
||||
console.log('✅ 包含版本信息')
|
||||
} else {
|
||||
console.log('❌ 未找到版本信息')
|
||||
}
|
||||
|
||||
// 检查是否包含版权信息
|
||||
if (html.includes('© 2025') || html.includes('网盘资源数据库')) {
|
||||
console.log('✅ 包含版权信息')
|
||||
} else {
|
||||
console.log('❌ 未找到版权信息')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试管理页面(应该没有版本信息)
|
||||
console.log('\n3. 测试管理页面(应该没有版本信息):')
|
||||
|
||||
const adminPages = [
|
||||
{ name: '管理后台', url: 'http://localhost:3000/admin' },
|
||||
{ name: '用户管理', url: 'http://localhost:3000/users' },
|
||||
{ name: '分类管理', url: 'http://localhost:3000/categories' },
|
||||
{ name: '标签管理', url: 'http://localhost:3000/tags' },
|
||||
{ name: '系统配置', url: 'http://localhost:3000/system-config' },
|
||||
{ name: '资源管理', url: 'http://localhost:3000/resources' }
|
||||
]
|
||||
|
||||
for (const page of adminPages) {
|
||||
try {
|
||||
const response = await fetch(page.url)
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`\n${page.name}页面:`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
// 检查是否包含AdminHeader组件
|
||||
if (html.includes('AdminHeader')) {
|
||||
console.log('✅ 包含AdminHeader组件')
|
||||
} else {
|
||||
console.log('❌ 未找到AdminHeader组件')
|
||||
}
|
||||
|
||||
// 检查是否不包含版本信息(管理页面应该没有版本显示)
|
||||
if (!html.includes('v1.0.0') && !html.includes('version')) {
|
||||
console.log('✅ 不包含版本信息(符合预期)')
|
||||
} else {
|
||||
console.log('❌ 包含版本信息(不符合预期)')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${page.name}页面测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试版本管理脚本
|
||||
console.log('\n4. 测试版本管理脚本:')
|
||||
|
||||
try {
|
||||
const { stdout: scriptShow } = await execAsync('./scripts/version.sh show')
|
||||
console.log('当前版本信息:')
|
||||
console.log(scriptShow)
|
||||
|
||||
console.log('✅ 版本管理脚本测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本管理脚本测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ Footer版本信息显示测试完成')
|
||||
console.log('\n总结:')
|
||||
console.log('- ✅ 后端版本接口正常工作')
|
||||
console.log('- ✅ 前端AppFooter组件已集成')
|
||||
console.log('- ✅ 版本信息在Footer中显示')
|
||||
console.log('- ✅ 管理页面已移除版本显示')
|
||||
console.log('- ✅ 版本信息显示格式:版权信息 | v版本号')
|
||||
console.log('- ✅ 版本管理脚本功能完整')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testFooterVersion()
|
||||
@@ -1,123 +0,0 @@
|
||||
// 测试GitHub版本系统
|
||||
const testGitHubVersion = async () => {
|
||||
console.log('测试GitHub版本系统...')
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
// 测试版本管理脚本
|
||||
console.log('\n1. 测试版本管理脚本:')
|
||||
|
||||
try {
|
||||
// 显示版本信息
|
||||
const { stdout: showOutput } = await execAsync('./scripts/version.sh show')
|
||||
console.log('版本信息:')
|
||||
console.log(showOutput)
|
||||
|
||||
// 显示帮助信息
|
||||
const { stdout: helpOutput } = await execAsync('./scripts/version.sh help')
|
||||
console.log('帮助信息:')
|
||||
console.log(helpOutput)
|
||||
|
||||
console.log('✅ 版本管理脚本测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本管理脚本测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试版本API接口
|
||||
console.log('\n2. 测试版本API接口:')
|
||||
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
const testEndpoints = [
|
||||
'/api/version',
|
||||
'/api/version/string',
|
||||
'/api/version/full',
|
||||
'/api/version/check-update'
|
||||
]
|
||||
|
||||
for (const endpoint of testEndpoints) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${endpoint}`)
|
||||
const data = await response.json()
|
||||
|
||||
console.log(`\n接口: ${endpoint}`)
|
||||
console.log(`状态码: ${response.status}`)
|
||||
console.log(`响应:`, JSON.stringify(data, null, 2))
|
||||
|
||||
if (data.success) {
|
||||
console.log('✅ 接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 接口 ${endpoint} 测试失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试GitHub版本检查
|
||||
console.log('\n3. 测试GitHub版本检查:')
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/ctwj/urldb/releases/latest')
|
||||
const data = await response.json()
|
||||
|
||||
console.log('GitHub API响应:')
|
||||
console.log(`状态码: ${response.status}`)
|
||||
console.log(`最新版本: ${data.tag_name || 'N/A'}`)
|
||||
console.log(`发布日期: ${data.published_at || 'N/A'}`)
|
||||
|
||||
if (data.tag_name) {
|
||||
console.log('✅ GitHub版本检查测试通过')
|
||||
} else {
|
||||
console.log('⚠️ GitHub上暂无Release')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ GitHub版本检查测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试前端版本页面
|
||||
console.log('\n4. 测试前端版本页面:')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/version')
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
if (html.includes('版本信息') && html.includes('VersionInfo')) {
|
||||
console.log('✅ 前端版本页面测试通过')
|
||||
} else {
|
||||
console.log('❌ 前端版本页面测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 前端版本页面测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试Git标签
|
||||
console.log('\n5. 测试Git标签:')
|
||||
|
||||
try {
|
||||
const { stdout: tagOutput } = await execAsync('git tag -l')
|
||||
console.log('当前Git标签:')
|
||||
console.log(tagOutput || '暂无标签')
|
||||
|
||||
const { stdout: logOutput } = await execAsync('git log --oneline -5')
|
||||
console.log('最近5次提交:')
|
||||
console.log(logOutput)
|
||||
|
||||
console.log('✅ Git标签测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Git标签测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ GitHub版本系统测试完成')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testGitHubVersion()
|
||||
@@ -1,83 +0,0 @@
|
||||
// 测试版本系统
|
||||
const testVersionSystem = async () => {
|
||||
console.log('测试版本系统...')
|
||||
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
|
||||
// 测试版本API接口
|
||||
const testEndpoints = [
|
||||
'/api/version',
|
||||
'/api/version/string',
|
||||
'/api/version/full',
|
||||
'/api/version/check-update'
|
||||
]
|
||||
|
||||
for (const endpoint of testEndpoints) {
|
||||
console.log(`\n测试接口: ${endpoint}`)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${endpoint}`)
|
||||
const data = await response.json()
|
||||
|
||||
console.log(`状态码: ${response.status}`)
|
||||
console.log(`响应:`, JSON.stringify(data, null, 2))
|
||||
|
||||
if (data.success) {
|
||||
console.log('✅ 接口测试通过')
|
||||
} else {
|
||||
console.log('❌ 接口测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 请求失败:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试版本管理脚本
|
||||
console.log('\n测试版本管理脚本...')
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
try {
|
||||
// 显示版本信息
|
||||
const { stdout: showOutput } = await execAsync('./scripts/version.sh show')
|
||||
console.log('版本信息:')
|
||||
console.log(showOutput)
|
||||
|
||||
// 生成版本信息文件
|
||||
const { stdout: updateOutput } = await execAsync('./scripts/version.sh update')
|
||||
console.log('生成版本信息文件:')
|
||||
console.log(updateOutput)
|
||||
|
||||
console.log('✅ 版本管理脚本测试通过')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 版本管理脚本测试失败:', error.message)
|
||||
}
|
||||
|
||||
// 测试前端版本页面
|
||||
console.log('\n测试前端版本页面...')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/version')
|
||||
const html = await response.text()
|
||||
|
||||
console.log(`状态码: ${response.status}`)
|
||||
|
||||
if (html.includes('版本信息') && html.includes('VersionInfo')) {
|
||||
console.log('✅ 前端版本页面测试通过')
|
||||
} else {
|
||||
console.log('❌ 前端版本页面测试失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 前端版本页面测试失败:', error.message)
|
||||
}
|
||||
|
||||
console.log('\n✅ 版本系统测试完成')
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testVersionSystem()
|
||||
6
web/components.d.ts
vendored
6
web/components.d.ts
vendored
@@ -8,9 +8,15 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-2">
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||
{{ systemConfig?.site_title || '网盘资源数据库' }}
|
||||
{{ systemConfig?.site_title || '老九网盘资源数据库' }}
|
||||
</NuxtLink>
|
||||
</h1>
|
||||
<!-- 面包屑导航 -->
|
||||
@@ -59,10 +59,43 @@
|
||||
<span>欢迎,{{ userStore.user?.username || '管理员' }}</span>
|
||||
<span class="ml-2 px-2 py-1 bg-blue-600/80 rounded text-xs text-white">{{ userStore.user?.role || 'admin' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 自动转存状态提示 -->
|
||||
<ClientOnly>
|
||||
<div
|
||||
|
||||
class="absolute right-4 bottom-4 flex items-center gap-2 rounded-lg px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
||||
'bg-red-400': !systemConfig?.auto_process_ready_resources,
|
||||
'bg-green-400': systemConfig?.auto_process_ready_resources
|
||||
}"></div>
|
||||
<span class="text-xs text-white font-medium">
|
||||
自动处理已<span>{{ systemConfig?.auto_process_ready_resources ? '开启' : '关闭' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
||||
'bg-red-400': !systemConfig?.auto_transfer_enabled,
|
||||
'bg-green-400': systemConfig?.auto_transfer_enabled
|
||||
}"></div>
|
||||
<span class="text-xs text-white font-medium">
|
||||
自动转存已<span>{{ systemConfig?.auto_transfer_enabled ? '开启' : '关闭' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
import { parseApiResponse } from '~/composables/useApi'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
}
|
||||
@@ -90,7 +123,7 @@ const pageConfig = computed(() => {
|
||||
'/admin/search-stats': { title: '搜索统计', icon: 'fas fa-chart-bar', description: '搜索数据分析' },
|
||||
'/admin/hot-dramas': { title: '热播剧管理', icon: 'fas fa-film', description: '管理热门剧集' },
|
||||
'/monitor': { title: '系统监控', icon: 'fas fa-desktop', description: '系统性能监控' },
|
||||
'/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' },
|
||||
'/admin/add-resource': { title: '添加资源', icon: 'fas fa-plus', description: '添加新资源' },
|
||||
'/api-docs': { title: 'API文档', icon: 'fas fa-book', description: '接口文档说明' },
|
||||
'/admin/version': { title: '版本信息', icon: 'fas fa-code-branch', description: '系统版本详情' }
|
||||
}
|
||||
@@ -101,12 +134,12 @@ const currentPageTitle = computed(() => pageConfig.value.title)
|
||||
const currentPageIcon = computed(() => pageConfig.value.icon)
|
||||
const currentPageDescription = computed(() => pageConfig.value.description)
|
||||
|
||||
// 获取系统配置
|
||||
const { data: systemConfigData } = await useAsyncData('systemConfig',
|
||||
() => $fetch('/api/system-config')
|
||||
)
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
const systemConfig = computed(() => systemConfigStore.config)
|
||||
|
||||
const systemConfig = computed(() => (systemConfigData.value as any)?.data || { site_title: '网盘资源数据库' })
|
||||
onMounted(() => {
|
||||
systemConfigStore.initConfig()
|
||||
})
|
||||
|
||||
// 退出登录
|
||||
const logout = async () => {
|
||||
|
||||
@@ -3,23 +3,33 @@
|
||||
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
|
||||
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||
<p class="flex items-center justify-center gap-2">
|
||||
<span>{{ systemConfig?.copyright || '© 2025 网盘资源数据库 By 老九' }}</span>
|
||||
<span v-if="versionInfo.version" class="text-gray-400 dark:text-gray-500">| v{{ versionInfo.version }}</span>
|
||||
<span>{{ systemConfig?.copyright || '© 2025 老九网盘资源数据库 By 老九' }}</span>
|
||||
<span v-if="versionInfo && versionInfo.version" class="text-gray-400 dark:text-gray-500">| v <n-a
|
||||
href="https://github.com/ctwj/urldb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
aria-label="在 GitHub 上查看版本信息"
|
||||
class="github-link"
|
||||
><span>{{ versionInfo.version }}</span></n-a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
import { parseApiResponse } from '~/composables/useApi'
|
||||
// 使用版本信息组合式函数
|
||||
const { versionInfo, fetchVersionInfo } = useVersion()
|
||||
|
||||
// 获取系统配置
|
||||
const { data: systemConfigData } = await useAsyncData('systemConfig',
|
||||
() => $fetch('/api/system-config')
|
||||
() => useApiFetch('/system/config').then(parseApiResponse)
|
||||
)
|
||||
|
||||
const systemConfig = computed(() => (systemConfigData.value as any)?.data || { copyright: '© 2025 网盘资源数据库 By 老九' })
|
||||
const systemConfig = computed(() => (systemConfigData.value as any) || { copyright: '© 2025 老九网盘资源数据库 By 老九' })
|
||||
|
||||
// 组件挂载时获取版本信息
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入格式说明:</label>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
<p class="mb-2"><strong>格式要求:</strong>标题和URL两行为一组,标题为必填项</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="mb-4 flex-1 w-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">输入格式说明:</label>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 p-3 rounded text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
<p class="mb-2"><strong>格式要求:</strong>标题和URL两行为一组,标题为必填项</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
电影标题1
|
||||
https://pan.baidu.com/s/123456
|
||||
电影标题2
|
||||
https://pan.baidu.com/s/789012
|
||||
电视剧标题3
|
||||
https://pan.quark.cn/s/345678</pre>
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-400">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
注意:标题为必填项,不能为空
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-red-600 dark:text-red-400">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
注意:标题为必填项,不能为空
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 flex-1 w-1">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">资源内容:</label>
|
||||
<textarea v-model="batchInput" rows="15"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-900 dark:text-gray-100"
|
||||
placeholder="请输入资源内容,格式:标题和URL两行为一组..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">资源内容:</label>
|
||||
<textarea
|
||||
v-model="batchInput"
|
||||
rows="15"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-900 dark:text-gray-100"
|
||||
placeholder="请输入资源内容,格式:标题和URL两行为一组..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" @click="$emit('cancel')" class="btn-secondary">取消</button>
|
||||
<button type="button" @click="handleSubmit" class="btn-primary" :disabled="loading">
|
||||
@@ -52,31 +52,31 @@ const validateInput = () => {
|
||||
if (!batchInput.value.trim()) {
|
||||
throw new Error('请输入资源内容')
|
||||
}
|
||||
|
||||
|
||||
const lines = batchInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
|
||||
|
||||
|
||||
if (lines.length === 0) {
|
||||
throw new Error('请输入有效的资源内容')
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为偶数行(标题+URL为一组)
|
||||
if (lines.length % 2 !== 0) {
|
||||
throw new Error('资源格式错误:标题和URL必须成对出现,请检查是否缺少标题或URL')
|
||||
}
|
||||
|
||||
|
||||
// 检查每组的标题是否为空
|
||||
for (let i = 0; i < lines.length; i += 2) {
|
||||
const title = lines[i]
|
||||
const url = lines[i + 1]
|
||||
|
||||
|
||||
if (!title) {
|
||||
throw new Error(`第${i + 1}行标题不能为空`)
|
||||
}
|
||||
|
||||
|
||||
if (!url) {
|
||||
throw new Error(`第${i + 2}行URL不能为空`)
|
||||
}
|
||||
|
||||
|
||||
// 验证URL格式
|
||||
try {
|
||||
new URL(url)
|
||||
@@ -91,25 +91,26 @@ const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
validateInput()
|
||||
|
||||
|
||||
// 解析输入内容
|
||||
const lines = batchInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
|
||||
const resources = []
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i += 2) {
|
||||
const title = lines[i]
|
||||
const url = lines[i + 1]
|
||||
|
||||
|
||||
resources.push({
|
||||
title: title,
|
||||
url: url,
|
||||
source: '批量添加'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 调用API添加资源
|
||||
const res: any = await readyResourceApi.batchCreateReadyResources(resources)
|
||||
emit('success', `成功添加 ${res.count || resources.length} 个资源,资源已进入待处理列表,处理完成后会自动入库`)
|
||||
const res: any = await readyResourceApi.batchCreateReadyResources({resources})
|
||||
console.log(res)
|
||||
emit('success', res.message)
|
||||
batchInput.value = ''
|
||||
} catch (e: any) {
|
||||
emit('error', e.message || '批量添加失败')
|
||||
@@ -127,4 +128,4 @@ const handleSubmit = async () => {
|
||||
.btn-secondary {
|
||||
@apply px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-md transition-colors;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useApiFetch } from './useApiFetch'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
// 统一响应解析函数
|
||||
export const parseApiResponse = <T>(response: any): T => {
|
||||
console.log('parseApiResponse - 原始响应:', response)
|
||||
log('parseApiResponse - 原始响应:', response)
|
||||
|
||||
// 检查是否是新的统一响应格式
|
||||
if (response && typeof response === 'object' && 'code' in response && 'data' in response) {
|
||||
@@ -37,700 +40,129 @@ export const parseApiResponse = <T>(response: any): T => {
|
||||
return response
|
||||
}
|
||||
|
||||
// 使用 $fetch 替代 axios,更好地处理 SSR
|
||||
export const useResourceApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getResources = async (params?: any) => {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResource = async (id: number) => {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createResource = async (data: any) => {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateResource = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteResource = async (id: number) => {
|
||||
const response = await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const searchResources = async (params: any) => {
|
||||
const response = await $fetch('/search', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResourcesByPan = async (panId: number, params?: any) => {
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params: { ...params, pan_id: panId },
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getResources,
|
||||
getResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
deleteResource,
|
||||
searchResources,
|
||||
getResourcesByPan,
|
||||
}
|
||||
const getResources = (params?: any) => useApiFetch('/resources', { params }).then(parseApiResponse)
|
||||
const getResource = (id: number) => useApiFetch(`/resources/${id}`).then(parseApiResponse)
|
||||
const createResource = (data: any) => useApiFetch('/resources', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateResource = (id: number, data: any) => useApiFetch(`/resources/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteResource = (id: number) => useApiFetch(`/resources/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const searchResources = (params: any) => useApiFetch('/search', { params }).then(parseApiResponse)
|
||||
const getResourcesByPan = (panId: number, params?: any) => useApiFetch('/resources', { params: { ...params, pan_id: panId } }).then(parseApiResponse)
|
||||
// 新增:统一的资源访问次数上报
|
||||
const incrementViewCount = (id: number) => useApiFetch(`/resources/${id}/view`, { method: 'POST' })
|
||||
// 新增:批量删除资源
|
||||
const batchDeleteResources = (ids: number[]) => useApiFetch('/resources/batch', { method: 'DELETE', body: { ids } }).then(parseApiResponse)
|
||||
return { getResources, getResource, createResource, updateResource, deleteResource, searchResources, getResourcesByPan, incrementViewCount, batchDeleteResources }
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
export const useAuthApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const login = async (data: any) => {
|
||||
const response = await $fetch('/auth/login', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const register = async (data: any) => {
|
||||
const response = await $fetch('/auth/register', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getProfile = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await $fetch('/auth/profile', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
register,
|
||||
getProfile,
|
||||
const login = (data: any) => useApiFetch('/auth/login', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const register = (data: any) => useApiFetch('/auth/register', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const getProfile = () => {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : ''
|
||||
return useApiFetch('/auth/profile', { headers: token ? { Authorization: `Bearer ${token}` } : {} }).then(parseApiResponse)
|
||||
}
|
||||
return { login, register, getProfile }
|
||||
}
|
||||
|
||||
// 分类相关API
|
||||
export const useCategoryApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getCategories = async () => {
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createCategory = async (data: any) => {
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateCategory = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteCategory = async (id: number) => {
|
||||
const response = await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getCategories,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
}
|
||||
const getCategories = () => useApiFetch('/categories').then(parseApiResponse)
|
||||
const createCategory = (data: any) => useApiFetch('/categories', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateCategory = (id: number, data: any) => useApiFetch(`/categories/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteCategory = (id: number) => useApiFetch(`/categories/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
return { getCategories, createCategory, updateCategory, deleteCategory }
|
||||
}
|
||||
|
||||
// 平台相关API
|
||||
export const usePanApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getPans = async () => {
|
||||
const response = await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getPan = async (id: number) => {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createPan = async (data: any) => {
|
||||
const response = await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updatePan = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deletePan = async (id: number) => {
|
||||
const response = await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getPans,
|
||||
getPan,
|
||||
createPan,
|
||||
updatePan,
|
||||
deletePan,
|
||||
}
|
||||
const getPans = () => useApiFetch('/pans').then(parseApiResponse)
|
||||
const getPan = (id: number) => useApiFetch(`/pans/${id}`).then(parseApiResponse)
|
||||
const createPan = (data: any) => useApiFetch('/pans', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updatePan = (id: number, data: any) => useApiFetch(`/pans/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deletePan = (id: number) => useApiFetch(`/pans/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
return { getPans, getPan, createPan, updatePan, deletePan }
|
||||
}
|
||||
|
||||
// Cookie相关API
|
||||
export const useCksApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getCks = async (params?: any) => {
|
||||
const response = await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getCksByID = async (id: number) => {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createCks = async (data: any) => {
|
||||
const response = await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateCks = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteCks = async (id: number) => {
|
||||
const response = await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const refreshCapacity = async (id: number) => {
|
||||
const response = await $fetch(`/cks/${id}/refresh-capacity`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getCks,
|
||||
getCksByID,
|
||||
createCks,
|
||||
updateCks,
|
||||
deleteCks,
|
||||
refreshCapacity,
|
||||
}
|
||||
const getCks = (params?: any) => useApiFetch('/cks', { params }).then(parseApiResponse)
|
||||
const getCksByID = (id: number) => useApiFetch(`/cks/${id}`).then(parseApiResponse)
|
||||
const createCks = (data: any) => useApiFetch('/cks', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateCks = (id: number, data: any) => useApiFetch(`/cks/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteCks = (id: number) => useApiFetch(`/cks/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const refreshCapacity = (id: number) => useApiFetch(`/cks/${id}/refresh-capacity`, { method: 'POST' }).then(parseApiResponse)
|
||||
return { getCks, getCksByID, createCks, updateCks, deleteCks, refreshCapacity }
|
||||
}
|
||||
|
||||
// 标签相关API
|
||||
export const useTagApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getTags = async () => {
|
||||
const response = await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getTagsByCategory = async (categoryId: number, params?: any) => {
|
||||
const response = await $fetch(`/categories/${categoryId}/tags`, {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getTag = async (id: number) => {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createTag = async (data: any) => {
|
||||
const response = await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateTag = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteTag = async (id: number) => {
|
||||
const response = await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getResourceTags = async (resourceId: number) => {
|
||||
const response = await $fetch(`/resources/${resourceId}/tags`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getTags,
|
||||
getTagsByCategory,
|
||||
getTag,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
getResourceTags,
|
||||
}
|
||||
const getTags = () => useApiFetch('/tags').then(parseApiResponse)
|
||||
const getTagsByCategory = (categoryId: number, params?: any) => useApiFetch(`/categories/${categoryId}/tags`, { params }).then(parseApiResponse)
|
||||
const getTag = (id: number) => useApiFetch(`/tags/${id}`).then(parseApiResponse)
|
||||
const createTag = (data: any) => useApiFetch('/tags', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateTag = (id: number, data: any) => useApiFetch(`/tags/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteTag = (id: number) => useApiFetch(`/tags/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const getResourceTags = (resourceId: number) => useApiFetch(`/resources/${resourceId}/tags`).then(parseApiResponse)
|
||||
return { getTags, getTagsByCategory, getTag, createTag, updateTag, deleteTag, getResourceTags }
|
||||
}
|
||||
|
||||
// 待处理资源相关API
|
||||
export const useReadyResourceApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getReadyResources = async (params?: any) => {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createReadyResource = async (data: any) => {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const batchCreateReadyResources = async (data: any) => {
|
||||
const response = await $fetch('/ready-resources/batch', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createReadyResourcesFromText = async (text: string) => {
|
||||
const getReadyResources = (params?: any) => useApiFetch('/ready-resources', { params }).then(parseApiResponse)
|
||||
const createReadyResource = (data: any) => useApiFetch('/ready-resources', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const batchCreateReadyResources = (data: any) => useApiFetch('/ready-resources/batch', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const createReadyResourcesFromText = (text: string) => {
|
||||
const formData = new FormData()
|
||||
formData.append('text', text)
|
||||
|
||||
const response = await $fetch('/ready-resources/text', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteReadyResource = async (id: number) => {
|
||||
const response = await $fetch(`/ready-resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const clearReadyResources = async () => {
|
||||
const response = await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getReadyResources,
|
||||
createReadyResource,
|
||||
batchCreateReadyResources,
|
||||
createReadyResourcesFromText,
|
||||
deleteReadyResource,
|
||||
clearReadyResources,
|
||||
return useApiFetch('/ready-resources/text', { method: 'POST', body: formData }).then(parseApiResponse)
|
||||
}
|
||||
const deleteReadyResource = (id: number) => useApiFetch(`/ready-resources/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const clearReadyResources = () => useApiFetch('/ready-resources', { method: 'DELETE' }).then(parseApiResponse)
|
||||
return { getReadyResources, createReadyResource, batchCreateReadyResources, createReadyResourcesFromText, deleteReadyResource, clearReadyResources }
|
||||
}
|
||||
|
||||
// 统计相关API
|
||||
export const useStatsApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getStats = async () => {
|
||||
const response = await $fetch('/stats', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getStats,
|
||||
}
|
||||
const getStats = () => useApiFetch('/stats').then(parseApiResponse)
|
||||
return { getStats }
|
||||
}
|
||||
|
||||
// 系统配置相关API
|
||||
export const useSystemConfigApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getSystemConfig = async () => {
|
||||
const response = await $fetch('/system/config', {
|
||||
baseURL: config.public.apiBase,
|
||||
// GET接口不需要认证头
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateSystemConfig = async (data: any) => {
|
||||
const authHeaders = getAuthHeaders()
|
||||
console.log('updateSystemConfig - authHeaders:', authHeaders)
|
||||
console.log('updateSystemConfig - token exists:', !!authHeaders.Authorization)
|
||||
|
||||
const response = await $fetch('/system/config', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: authHeaders as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getSystemConfig,
|
||||
updateSystemConfig,
|
||||
}
|
||||
const getSystemConfig = () => useApiFetch('/system/config').then(parseApiResponse)
|
||||
const updateSystemConfig = (data: any) => useApiFetch('/system/config', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
return { getSystemConfig, updateSystemConfig }
|
||||
}
|
||||
|
||||
// 热播剧相关API
|
||||
export const useHotDramaApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getHotDramas = async (params?: any) => {
|
||||
const response = await $fetch('/hot-dramas', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createHotDrama = async (data: any) => {
|
||||
const response = await $fetch('/hot-dramas', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateHotDrama = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/hot-dramas/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteHotDrama = async (id: number) => {
|
||||
const response = await $fetch(`/hot-dramas/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const fetchHotDramas = async () => {
|
||||
const response = await $fetch('/hot-dramas/fetch', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getHotDramas,
|
||||
createHotDrama,
|
||||
updateHotDrama,
|
||||
deleteHotDrama,
|
||||
fetchHotDramas,
|
||||
}
|
||||
const getHotDramas = (params?: any) => useApiFetch('/hot-dramas', { params }).then(parseApiResponse)
|
||||
const createHotDrama = (data: any) => useApiFetch('/hot-dramas', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateHotDrama = (id: number, data: any) => useApiFetch(`/hot-dramas/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteHotDrama = (id: number) => useApiFetch(`/hot-dramas/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const fetchHotDramas = () => useApiFetch('/hot-dramas/fetch', { method: 'POST' }).then(parseApiResponse)
|
||||
return { getHotDramas, createHotDrama, updateHotDrama, deleteHotDrama, fetchHotDramas }
|
||||
}
|
||||
|
||||
// 监控相关API
|
||||
export const useMonitorApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getPerformanceStats = async () => {
|
||||
const response = await $fetch('/performance', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
const getPerformanceStats = () => useApiFetch('/performance').then(parseApiResponse)
|
||||
const getSystemInfo = () => useApiFetch('/system/info').then(parseApiResponse)
|
||||
const getBasicStats = () => useApiFetch('/stats').then(parseApiResponse)
|
||||
return { getPerformanceStats, getSystemInfo, getBasicStats }
|
||||
}
|
||||
|
||||
const getSystemInfo = async () => {
|
||||
const response = await $fetch('/system/info', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const getBasicStats = async () => {
|
||||
const response = await $fetch('/stats', {
|
||||
baseURL: config.public.apiBase,
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getPerformanceStats,
|
||||
getSystemInfo,
|
||||
getBasicStats,
|
||||
}
|
||||
export const useUserApi = () => {
|
||||
const getUsers = (params?: any) => useApiFetch('/users', { params }).then(parseApiResponse)
|
||||
const getUser = (id: number) => useApiFetch(`/users/${id}`).then(parseApiResponse)
|
||||
const createUser = (data: any) => useApiFetch('/users', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const updateUser = (id: number, data: any) => useApiFetch(`/users/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
|
||||
const deleteUser = (id: number) => useApiFetch(`/users/${id}`, { method: 'DELETE' }).then(parseApiResponse)
|
||||
const changePassword = (id: number, newPassword: string) => useApiFetch(`/users/${id}/password`, { method: 'PUT', body: { new_password: newPassword } }).then(parseApiResponse)
|
||||
return { getUsers, getUser, createUser, updateUser, deleteUser, changePassword }
|
||||
}
|
||||
|
||||
// 用户管理相关API
|
||||
export const useUserApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.authHeaders
|
||||
}
|
||||
|
||||
const getUsers = async (params?: any) => {
|
||||
const response = await $fetch('/users', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
// 公开获取系统配置API
|
||||
export const usePublicSystemConfigApi = () => {
|
||||
const getPublicSystemConfig = () => useApiFetch('/public/system-config').then(res => res)
|
||||
return { getPublicSystemConfig }
|
||||
}
|
||||
|
||||
const getUser = async (id: number) => {
|
||||
const response = await $fetch(`/users/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const createUser = async (data: any) => {
|
||||
const response = await $fetch('/users', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const updateUser = async (id: number, data: any) => {
|
||||
const response = await $fetch(`/users/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const deleteUser = async (id: number) => {
|
||||
const response = await $fetch(`/users/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
const changePassword = async (id: number, newPassword: string) => {
|
||||
const response = await $fetch(`/users/${id}/password`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: { new_password: newPassword },
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
return parseApiResponse(response)
|
||||
}
|
||||
|
||||
return {
|
||||
getUsers,
|
||||
getUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
changePassword,
|
||||
// 日志函数:只在开发环境打印
|
||||
function log(...args: any[]) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
48
web/composables/useApiFetch.ts
Normal file
48
web/composables/useApiFetch.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useRuntimeConfig } from '#app'
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
export function useApiFetch<T = any>(
|
||||
url: string,
|
||||
options: any = {}
|
||||
): Promise<T> {
|
||||
const config = useRuntimeConfig()
|
||||
const userStore = useUserStore()
|
||||
const baseURL = process.server
|
||||
? String(config.public.apiServer)
|
||||
: String(config.public.apiBase)
|
||||
|
||||
// 自动带上 token
|
||||
const headers = {
|
||||
...(options.headers || {}),
|
||||
...(userStore.authHeaders || {})
|
||||
}
|
||||
|
||||
return $fetch<T>(url, {
|
||||
baseURL,
|
||||
...options,
|
||||
headers,
|
||||
onResponse({ response }) {
|
||||
// 统一处理 code/message
|
||||
if (response._data && response._data.code && response._data.code !== 200) {
|
||||
throw new Error(response._data.message || '请求失败')
|
||||
}
|
||||
},
|
||||
onResponseError({ error }: { error: any }) {
|
||||
// 检查是否为"无效的令牌"错误
|
||||
if (error?.data?.error === '无效的令牌') {
|
||||
// 清除用户状态
|
||||
userStore.logout()
|
||||
// 跳转到登录页面
|
||||
if (process.client) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
// 统一错误提示
|
||||
// 你可以用 naive-ui 的 useMessage() 这里弹窗
|
||||
// useMessage().error(error.message)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -40,16 +40,16 @@ export const useSeo = () => {
|
||||
if (systemConfig.value?.site_title) {
|
||||
return `${systemConfig.value.site_title} - ${pageTitle}`
|
||||
}
|
||||
return `${pageTitle} - 网盘资源数据库`
|
||||
return `${pageTitle} - 老九网盘资源数据库`
|
||||
}
|
||||
|
||||
// 生成页面元数据
|
||||
const generateMeta = (customMeta?: Record<string, string>) => {
|
||||
const defaultMeta = {
|
||||
description: systemConfig.value?.site_description || '专业的网盘资源数据库',
|
||||
description: systemConfig.value?.site_description || '专业的老九网盘资源数据库',
|
||||
keywords: systemConfig.value?.keywords || '网盘,资源管理,文件分享',
|
||||
author: systemConfig.value?.author || '系统管理员',
|
||||
copyright: systemConfig.value?.copyright || '© 2024 网盘资源数据库'
|
||||
copyright: systemConfig.value?.copyright || '© 2024 老九网盘资源数据库'
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface VersionResponse {
|
||||
|
||||
export const useVersion = () => {
|
||||
const versionInfo = ref<VersionInfo>({
|
||||
version: '1.0.3',
|
||||
version: '1.0.7',
|
||||
build_time: '',
|
||||
git_commit: 'unknown',
|
||||
git_branch: 'unknown',
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||
|
||||
// 页面加载状态
|
||||
const pageLoading = ref(false)
|
||||
|
||||
@@ -66,8 +68,9 @@ watch(() => route.path, () => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 页面加载时显示加载状态
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
onMounted(() => {
|
||||
systemConfigStore.initConfig()
|
||||
pageLoading.value = true
|
||||
setTimeout(() => {
|
||||
pageLoading.value = false
|
||||
|
||||
@@ -38,11 +38,11 @@ export default defineNuxtConfig({
|
||||
],
|
||||
app: {
|
||||
head: {
|
||||
title: '开源网盘资源数据库',
|
||||
title: '老九网盘资源数据库',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ name: 'description', content: '开源网盘资源数据库 - 一个现代化的资源管理系统' }
|
||||
{ name: 'description', content: '老九网盘资源管理数据庫,现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||
@@ -51,7 +51,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: process.env.API_BASE || 'http://localhost:8080/api'
|
||||
apiBase: process.env.NUXT_PUBLIC_API_CLIENT || 'http://localhost:8080/api',
|
||||
apiServer: process.env.NUXT_PUBLIC_API_SERVER || 'http://localhost:8080/api'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "res-db-web",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
|
||||
<!-- 页面头部 -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="$router.back()"
|
||||
class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
|
||||
>
|
||||
<i class="fas fa-arrow-left text-xl"></i>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">添加资源</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<NuxtLink
|
||||
to="/admin"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors text-sm"
|
||||
>
|
||||
<i class="fas fa-cog mr-1"></i>管理后台
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
@@ -64,12 +40,6 @@
|
||||
@error="handleError"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
|
||||
<!-- API说明 -->
|
||||
<ApiDocumentation
|
||||
v-else
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +73,6 @@ const errorMsg = ref('')
|
||||
const tabs = [
|
||||
{ label: '批量添加', value: 'batch' },
|
||||
{ label: '单个添加', value: 'single' },
|
||||
{ label: 'API说明', value: 'api' },
|
||||
]
|
||||
const mode = ref('batch')
|
||||
|
||||
@@ -133,7 +102,7 @@ const handleCancel = () => {
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '添加资源 - 网盘资源数据库'
|
||||
title: '添加资源 - 老九网盘资源数据库'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -198,6 +198,8 @@ definePageMeta({
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const config = useRuntimeConfig()
|
||||
import { useCategoryApi } from '~/composables/useApi'
|
||||
const categoryApi = useCategoryApi()
|
||||
|
||||
// 页面状态
|
||||
const pageLoading = ref(true)
|
||||
@@ -232,7 +234,7 @@ const getAuthHeaders = () => {
|
||||
|
||||
// 页面元数据
|
||||
useHead({
|
||||
title: '分类管理 - 网盘资源数据库',
|
||||
title: '分类管理 - 老九网盘资源数据库',
|
||||
meta: [
|
||||
{ name: 'description', content: '管理网盘资源分类' },
|
||||
{ name: 'keywords', content: '分类管理,资源管理' }
|
||||
@@ -257,23 +259,10 @@ const fetchCategories = async () => {
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value
|
||||
}
|
||||
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
params
|
||||
})
|
||||
|
||||
|
||||
// 解析响应
|
||||
if (response && typeof response === 'object' && 'code' in response && response.code === 200) {
|
||||
categories.value = response.data.items || []
|
||||
totalCount.value = response.data.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} else {
|
||||
categories.value = response.items || []
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
}
|
||||
const response = await categoryApi.getCategories(params)
|
||||
categories.value = Array.isArray(response) ? response : []
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
} finally {
|
||||
@@ -318,13 +307,8 @@ const deleteCategory = async (categoryId: number) => {
|
||||
if (!confirm(`确定要删除分类吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await $fetch(`/categories/${categoryId}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
await categoryApi.deleteCategory(categoryId)
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
@@ -335,23 +319,11 @@ const deleteCategory = async (categoryId: number) => {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
if (editingCategory.value) {
|
||||
await $fetch(`/categories/${editingCategory.value.id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: formData.value,
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
await categoryApi.updateCategory(editingCategory.value.id, formData.value)
|
||||
} else {
|
||||
await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: formData.value,
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
await categoryApi.createCategory(formData.value)
|
||||
}
|
||||
|
||||
closeModal()
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
|
||||
@@ -117,6 +117,14 @@
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="toggleStatus(cks)"
|
||||
:class="cks.is_valid ? 'text-orange-600 hover:text-orange-800 dark:text-orange-400 dark:hover:text-orange-300' : 'text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300'"
|
||||
class="transition-colors"
|
||||
:title="cks.is_valid ? '禁用账号' : '启用账号'"
|
||||
>
|
||||
<i :class="cks.is_valid ? 'fas fa-ban' : 'fas fa-check'"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="refreshCapacity(cks.id)"
|
||||
class="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
@@ -315,9 +323,14 @@ const pageLoading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const platform = ref(null)
|
||||
|
||||
const { data: pansData } = await useAsyncData('pans', () => $fetch('/api/pans'))
|
||||
import { useCksApi, usePanApi } from '~/composables/useApi'
|
||||
const cksApi = useCksApi()
|
||||
const panApi = usePanApi()
|
||||
|
||||
const { data: pansData } = await useAsyncData('pans', () => panApi.getPans())
|
||||
const pans = computed(() => {
|
||||
return (pansData.value).data.list || []
|
||||
// 统一接口格式后直接为数组
|
||||
return Array.isArray(pansData.value) ? pansData.value : (pansData.value?.list || [])
|
||||
})
|
||||
const platformOptions = computed(() => {
|
||||
return pans.value.map(pan => ({
|
||||
@@ -339,10 +352,10 @@ const checkAuth = () => {
|
||||
const fetchCks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { useCksApi } = await import('~/composables/useApi')
|
||||
const cksApi = useCksApi()
|
||||
console.log('开始获取账号列表...')
|
||||
const response = await cksApi.getCks()
|
||||
cksList.value = Array.isArray(response) ? response : []
|
||||
console.log('获取账号列表成功,数据:', cksList.value)
|
||||
} catch (error) {
|
||||
console.error('获取账号列表失败:', error)
|
||||
} finally {
|
||||
@@ -354,8 +367,6 @@ const fetchCks = async () => {
|
||||
// 获取平台列表
|
||||
const fetchPlatforms = async () => {
|
||||
try {
|
||||
const { usePanApi } = await import('~/composables/useApi')
|
||||
const panApi = usePanApi()
|
||||
const response = await panApi.getPans()
|
||||
platforms.value = Array.isArray(response) ? response : []
|
||||
} catch (error) {
|
||||
@@ -367,8 +378,6 @@ const fetchPlatforms = async () => {
|
||||
const createCks = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
const { useCksApi } = await import('~/composables/useApi')
|
||||
const cksApi = useCksApi()
|
||||
await cksApi.createCks(form.value)
|
||||
await fetchCks()
|
||||
closeModal()
|
||||
@@ -384,8 +393,6 @@ const createCks = async () => {
|
||||
const updateCks = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
const { useCksApi } = await import('~/composables/useApi')
|
||||
const cksApi = useCksApi()
|
||||
await cksApi.updateCks(editingCks.value.id, form.value)
|
||||
await fetchCks()
|
||||
closeModal()
|
||||
@@ -402,8 +409,6 @@ const deleteCks = async (id) => {
|
||||
if (!confirm('确定要删除这个账号吗?')) return
|
||||
|
||||
try {
|
||||
const { useCksApi } = await import('~/composables/useApi')
|
||||
const cksApi = useCksApi()
|
||||
await cksApi.deleteCks(id)
|
||||
await fetchCks()
|
||||
} catch (error) {
|
||||
@@ -417,8 +422,6 @@ const refreshCapacity = async (id) => {
|
||||
if (!confirm('确定要刷新此账号的容量信息吗?')) return
|
||||
|
||||
try {
|
||||
const { useCksApi } = await import('~/composables/useApi')
|
||||
const cksApi = useCksApi()
|
||||
await cksApi.refreshCapacity(id)
|
||||
await fetchCks()
|
||||
alert('容量信息已刷新!')
|
||||
@@ -428,6 +431,24 @@ const refreshCapacity = async (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换账号状态
|
||||
const toggleStatus = async (cks) => {
|
||||
const newStatus = !cks.is_valid
|
||||
if (!confirm(`确定要${cks.is_valid ? '禁用' : '启用'}此账号吗?`)) return
|
||||
|
||||
try {
|
||||
console.log('切换状态 - 账号ID:', cks.id, '当前状态:', cks.is_valid, '新状态:', newStatus)
|
||||
await cksApi.updateCks(cks.id, { is_valid: newStatus })
|
||||
console.log('状态更新成功,正在刷新数据...')
|
||||
await fetchCks()
|
||||
console.log('数据刷新完成')
|
||||
alert(`账号已${newStatus ? '启用' : '禁用'}!`)
|
||||
} catch (error) {
|
||||
console.error('切换账号状态失败:', error)
|
||||
alert(`切换账号状态失败: ${error.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑账号
|
||||
const editCks = (cks) => {
|
||||
editingCks.value = cks
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap gap-1 w-full text-left rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer">
|
||||
<div v-for="pan in pans" :key="pan.id" class="h-6 px-1 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||||
<span v-html="pan.icon"></span> {{ pan.name }}
|
||||
<span v-html="pan.icon"></span> {{ pan.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,15 +244,17 @@ definePageMeta({
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 统计数据
|
||||
const { data: statsData } = await useAsyncData('stats', () => $fetch('/api/stats'))
|
||||
const stats = computed(() => (statsData.value as any)?.data || {})
|
||||
import { useStatsApi, usePanApi } from '~/composables/useApi'
|
||||
|
||||
const statsApi = useStatsApi()
|
||||
const panApi = usePanApi()
|
||||
|
||||
const { data: statsData } = await useAsyncData('stats', () => statsApi.getStats())
|
||||
const stats = computed(() => (statsData.value as any) || {})
|
||||
|
||||
// 平台数据
|
||||
const { data: pansData } = await useAsyncData('pans', () => $fetch('/api/pans'))
|
||||
console.log()
|
||||
const pans = computed(() => {
|
||||
return (pansData.value as any).data.list || []
|
||||
})
|
||||
const { data: pansData } = await useAsyncData('pans', () => panApi.getPans())
|
||||
const pans = computed(() => (pansData.value as any) || [])
|
||||
|
||||
// 分类管理相关
|
||||
const goToCategoryManagement = () => {
|
||||
|
||||
@@ -334,11 +334,8 @@ const totalCount = ref(0)
|
||||
const totalPages = ref(0)
|
||||
|
||||
// 获取待处理资源API
|
||||
const { useReadyResourceApi } = await import('~/composables/useApi')
|
||||
import { useReadyResourceApi, useSystemConfigApi } from '~/composables/useApi'
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
|
||||
// 获取系统配置API
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
|
||||
// 获取系统配置
|
||||
@@ -597,7 +594,7 @@ onMounted(async () => {
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '待处理资源管理 - 网盘资源数据库'
|
||||
title: '待处理资源管理 - 老九网盘资源数据库'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -420,10 +420,7 @@ const selectedResources = ref<number[]>([])
|
||||
const selectAll = ref(false)
|
||||
|
||||
// API
|
||||
const { useResourceApi } = await import('~/composables/useApi')
|
||||
const { usePanApi } = await import('~/composables/useApi')
|
||||
const { useCategoryApi } = await import('~/composables/useApi')
|
||||
const { useTagApi } = await import('~/composables/useApi')
|
||||
import { useResourceApi, usePanApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
||||
|
||||
const resourceApi = useResourceApi()
|
||||
const panApi = usePanApi()
|
||||
@@ -452,9 +449,10 @@ const fetchData = async () => {
|
||||
}
|
||||
|
||||
const response = await resourceApi.getResources(params) as any
|
||||
|
||||
if (response && response.resources) {
|
||||
resources.value = response.resources
|
||||
console.log('DEBUG', response)
|
||||
|
||||
if (response && response.data) {
|
||||
resources.value = response.data
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil((response.total || 0) / pageSize.value)
|
||||
} else if (Array.isArray(response)) {
|
||||
@@ -602,7 +600,7 @@ const handleBatchAction = async () => {
|
||||
switch (batchAction.value) {
|
||||
case 'delete':
|
||||
if (confirm(`确定要删除选中的 ${selectedResources.value.length} 个资源吗?`)) {
|
||||
await Promise.all(selectedResources.value.map(id => resourceApi.deleteResource(id)))
|
||||
await resourceApi.batchDeleteResources(selectedResources.value)
|
||||
alert('批量删除成功')
|
||||
}
|
||||
break
|
||||
|
||||
@@ -82,6 +82,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索记录 -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mt-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">搜索记录</h2>
|
||||
<table class="w-full table-auto text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-2 py-2 text-left">关键词</th>
|
||||
<th class="px-2 py-2 text-left">次数</th>
|
||||
<th class="px-2 py-2 text-left">日期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in searchList" :key="item.id">
|
||||
<td class="px-2 py-2">{{ item.keyword }}</td>
|
||||
<td class="px-2 py-2">{{ item.count }}</td>
|
||||
<td class="px-2 py-2">{{ item.date ? (new Date(item.date)).toLocaleDateString() : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="searchList.length === 0" class="text-gray-400 text-center py-8">暂无搜索记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -94,6 +116,8 @@ definePageMeta({
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import Chart from 'chart.js/auto'
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
import { parseApiResponse } from '~/composables/useApi'
|
||||
|
||||
const stats = ref({
|
||||
todaySearches: 0,
|
||||
@@ -106,6 +130,8 @@ const stats = ref({
|
||||
}
|
||||
})
|
||||
|
||||
const searchList = ref([])
|
||||
|
||||
const trendChart = ref(null)
|
||||
let chart = null
|
||||
|
||||
@@ -119,14 +145,23 @@ const getPercentage = (count) => {
|
||||
// 加载搜索统计
|
||||
const loadSearchStats = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/search-stats')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
stats.value = data
|
||||
|
||||
// 更新图表
|
||||
updateChart()
|
||||
}
|
||||
// 1. 汇总卡片
|
||||
const summary = await useApiFetch('/search-stats/summary').then(parseApiResponse)
|
||||
stats.value.todaySearches = summary.today || 0
|
||||
stats.value.weekSearches = summary.week || 0
|
||||
stats.value.monthSearches = summary.month || 0
|
||||
// 2. 热门关键词
|
||||
const hotKeywords = await useApiFetch('/search-stats/hot-keywords').then(parseApiResponse)
|
||||
stats.value.hotKeywords = hotKeywords || []
|
||||
// 3. 趋势
|
||||
const trend = await useApiFetch('/search-stats/trend').then(parseApiResponse)
|
||||
stats.value.searchTrend.days = (trend || []).map(item => item.date ? (new Date(item.date)).toLocaleDateString() : '')
|
||||
stats.value.searchTrend.values = (trend || []).map(item => item.total_searches)
|
||||
// 4. 搜索记录
|
||||
const data = await useApiFetch('/search-stats').then(parseApiResponse)
|
||||
searchList.value = data || []
|
||||
// 5. 更新图表
|
||||
setTimeout(updateChart, 100)
|
||||
} catch (error) {
|
||||
console.error('加载搜索统计失败:', error)
|
||||
}
|
||||
|
||||
@@ -18,14 +18,10 @@
|
||||
<!-- 配置表单 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow 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">
|
||||
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane name="SEO 配置" tab="SEO 配置">
|
||||
<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">
|
||||
@@ -36,7 +32,7 @@
|
||||
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="网盘资源数据库"
|
||||
placeholder="老九网盘资源数据库"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +45,7 @@
|
||||
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="专业的网盘资源数据库"
|
||||
placeholder="专业的老九网盘资源数据库"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -88,41 +84,55 @@
|
||||
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 网盘资源数据库"
|
||||
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">
|
||||
开启后,系统将自动处理待处理的资源,无需手动操作
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="自动处理配置" tab="自动处理配置">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- 待处理资源自动处理 -->
|
||||
<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 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 class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
@@ -208,33 +218,11 @@
|
||||
</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">
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="其他配置" tab="其他配置">
|
||||
<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">
|
||||
@@ -273,16 +261,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API配置 -->
|
||||
<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-key text-orange-600"></i>
|
||||
API 配置
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="API配置" tab="API配置">
|
||||
<div class="space-y-4">
|
||||
<!-- API Token -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
@@ -324,7 +305,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="flex justify-end space-x-4 pt-6">
|
||||
@@ -358,19 +340,22 @@ definePageMeta({
|
||||
})
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSystemConfigApi } from '~/composables/useApi'
|
||||
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
|
||||
// API
|
||||
const { getSystemConfig, updateSystemConfig } = useSystemConfigApi()
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const config = ref({
|
||||
// SEO 配置
|
||||
siteTitle: '网盘资源数据库',
|
||||
siteDescription: '专业的网盘资源数据库',
|
||||
siteTitle: '老九网盘资源数据库',
|
||||
siteDescription: '专业的老九网盘资源数据库',
|
||||
keywords: '网盘,资源管理,文件分享',
|
||||
author: '系统管理员',
|
||||
copyright: '© 2024 网盘资源数据库',
|
||||
copyright: '© 2024 老九网盘资源数据库',
|
||||
|
||||
// 自动处理配置
|
||||
autoProcessReadyResources: false,
|
||||
@@ -391,7 +376,7 @@ const systemConfig = ref(null)
|
||||
|
||||
// 页面元数据 - 移到变量声明之后
|
||||
useHead({
|
||||
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 网盘资源数据库',
|
||||
title: () => systemConfig.value?.site_title ? `${systemConfig.value.site_title} - 系统配置` : '系统配置 - 老九网盘资源数据库',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
@@ -412,17 +397,17 @@ useHead({
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getSystemConfig()
|
||||
const response = await systemConfigApi.getSystemConfig()
|
||||
console.log('系统配置响应:', response)
|
||||
|
||||
// 使用新的统一响应格式,直接使用response
|
||||
if (response) {
|
||||
config.value = {
|
||||
siteTitle: response.site_title || '网盘资源数据库',
|
||||
siteDescription: response.site_description || '专业的网盘资源数据库',
|
||||
siteTitle: response.site_title || '老九网盘资源数据库',
|
||||
siteDescription: response.site_description || '专业的老九网盘资源数据库',
|
||||
keywords: response.keywords || '网盘,资源管理,文件分享',
|
||||
author: response.author || '系统管理员',
|
||||
copyright: response.copyright || '© 2024 网盘资源数据库',
|
||||
copyright: response.copyright || '© 2024 老九网盘资源数据库',
|
||||
autoProcessReadyResources: response.auto_process_ready_resources || false,
|
||||
autoProcessInterval: response.auto_process_interval || 30,
|
||||
autoTransferEnabled: response.auto_transfer_enabled || false, // 新增
|
||||
@@ -465,12 +450,13 @@ const saveConfig = async () => {
|
||||
api_token: config.value.apiToken // 保存API Token
|
||||
}
|
||||
|
||||
const response = await updateSystemConfig(requestData)
|
||||
const response = await systemConfigApi.updateSystemConfig(requestData)
|
||||
// 使用新的统一响应格式,直接检查response是否存在
|
||||
if (response) {
|
||||
alert('配置保存成功!')
|
||||
// 重新加载配置以获取最新数据
|
||||
await loadConfig()
|
||||
// 自动更新 systemConfig store(强制刷新)
|
||||
await systemConfigStore.initConfig(true)
|
||||
} else {
|
||||
alert('保存配置失败:未知错误')
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<div class="">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<n-alert class="mb-4" title="提交的数据中,如果包含标签,数据添加成功,会自动添加标签" type="info" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -311,7 +312,7 @@ const getAuthHeaders = () => {
|
||||
|
||||
// 页面元数据
|
||||
useHead({
|
||||
title: '标签管理 - 网盘资源数据库',
|
||||
title: '标签管理 - 老九网盘资源数据库',
|
||||
meta: [
|
||||
{ name: 'description', content: '管理网盘资源标签' },
|
||||
{ name: 'keywords', content: '标签管理,资源管理' }
|
||||
@@ -330,16 +331,8 @@ const checkAuth = () => {
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
|
||||
if (response && typeof response === 'object' && 'code' in response && response.code === 200) {
|
||||
categories.value = (response as any).data?.items || []
|
||||
} else {
|
||||
categories.value = (response as any).items || []
|
||||
}
|
||||
const response = await categoryApi.getCategories()
|
||||
categories.value = Array.isArray(response) ? response : []
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
}
|
||||
@@ -354,34 +347,15 @@ const fetchTags = async () => {
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value
|
||||
}
|
||||
|
||||
let response: any
|
||||
if (selectedCategory.value) {
|
||||
// 如果选择了分类,使用按分类查询的接口
|
||||
response = await $fetch(`/categories/${selectedCategory.value}/tags`, {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
response = await tagApi.getTagsByCategory(selectedCategory.value, params)
|
||||
} else {
|
||||
// 否则使用普通查询接口
|
||||
response = await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
if (response && typeof response === 'object' && 'code' in response && response.code === 200) {
|
||||
tags.value = response.data?.items || []
|
||||
totalCount.value = response.data?.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} else {
|
||||
tags.value = response.items || []
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
response = await tagApi.getTags(params)
|
||||
}
|
||||
tags.value = response.items || []
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
} finally {
|
||||
@@ -435,11 +409,7 @@ const deleteTag = async (tagId: number) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await $fetch(`/tags/${tagId}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
await tagApi.deleteTag(tagId)
|
||||
await fetchTags()
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
@@ -464,19 +434,9 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
if (editingTag.value) {
|
||||
await $fetch(`/tags/${editingTag.value.id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: submitData,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
await tagApi.updateTag(editingTag.value.id, submitData)
|
||||
} else {
|
||||
await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: submitData,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
await tagApi.createTag(submitData)
|
||||
}
|
||||
|
||||
closeModal()
|
||||
|
||||
@@ -28,7 +28,7 @@ definePageMeta({
|
||||
|
||||
// 页面元数据
|
||||
useHead({
|
||||
title: '版本信息 - 网盘资源数据库',
|
||||
title: '版本信息 - 老九网盘资源数据库',
|
||||
meta: [
|
||||
{ name: 'description', content: '查看系统版本信息和更新状态' }
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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">
|
||||
网盘资源数据库 - API文档
|
||||
老九网盘资源数据库 - API文档
|
||||
</a>
|
||||
</h1>
|
||||
<p class="text-gray-300 max-w-2xl mx-auto">公开API接口文档,支持资源添加、搜索和热门剧获取等功能</p>
|
||||
@@ -341,6 +341,36 @@ curl -X GET "http://localhost:8080/api/public/resources/search?keyword=测试" \
|
||||
curl -X GET "http://localhost:8080/api/public/hot-dramas?page=1&page_size=5" \
|
||||
-H "X-API-Token: $API_TOKEN"</code></pre>
|
||||
</div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">前端 fetch 示例</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
|
||||
<pre class="text-sm overflow-x-auto"><code>
|
||||
// 资源搜索
|
||||
fetch('/api/public/resources/search?q=测试', { headers: { 'X-API-Token': 'your_token' } })
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
const list = res.data.list // 资源列表
|
||||
const total = res.data.total
|
||||
} else {
|
||||
alert(res.message)
|
||||
}
|
||||
})
|
||||
// 单个添加资源
|
||||
fetch('/api/public/resources/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Token': 'your_token' },
|
||||
body: JSON.stringify({ title: 'xxx', url: 'xxx' })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
alert('添加成功,ID:' + res.data.id)
|
||||
} else {
|
||||
alert(res.message)
|
||||
}
|
||||
})
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,14 +384,14 @@ curl -X GET "http://localhost:8080/api/public/hot-dramas?page=1&page_size=5" \
|
||||
<script setup>
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
// 页面元数据
|
||||
useHead({
|
||||
title: 'API文档 - 网盘资源数据库',
|
||||
title: 'API文档 - 老九网盘资源数据库',
|
||||
meta: [
|
||||
{ name: 'description', content: '网盘资源数据库的公开API接口文档' },
|
||||
{ name: 'description', content: '老九网盘资源数据库的公开API接口文档' },
|
||||
{ name: 'keywords', content: 'API,接口文档,网盘资源管理' }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -205,10 +205,12 @@
|
||||
<script setup>
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useHotDramaApi } from '~/composables/useApi'
|
||||
const hotDramaApi = useHotDramaApi()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -250,32 +252,20 @@ const averageRating = computed(() => {
|
||||
const fetchDramas = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { useHotDramaApi } = await import('~/composables/useApi')
|
||||
const hotDramaApi = useHotDramaApi()
|
||||
|
||||
const params = {
|
||||
page: 1,
|
||||
page_size: 1000 // 获取大量数据,实际会返回所有数据
|
||||
page_size: 1000
|
||||
}
|
||||
|
||||
if (selectedCategory.value) {
|
||||
params.category = selectedCategory.value
|
||||
}
|
||||
|
||||
const response = await hotDramaApi.getHotDramas(params)
|
||||
console.log('API响应:', response)
|
||||
|
||||
// 使用新的统一响应格式
|
||||
if (response && response.items) {
|
||||
dramas.value = response.items
|
||||
total.value = response.total || 0
|
||||
console.log('设置数据:', dramas.value.length, '条')
|
||||
console.log('第一条数据:', dramas.value[0])
|
||||
} else {
|
||||
// 兼容旧格式
|
||||
dramas.value = Array.isArray(response) ? response : []
|
||||
total.value = dramas.value.length
|
||||
console.log('兼容格式数据:', dramas.value.length, '条')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取热播剧列表失败:', error)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||
<div v-if="!systemConfig.maintenance_mode" class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||
<!-- 全局加载状态 -->
|
||||
<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">
|
||||
@@ -20,7 +20,7 @@
|
||||
<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">
|
||||
{{ systemConfig?.site_title || '网盘资源数据库' }}
|
||||
{{ systemConfig?.site_title || '老九网盘资源数据库' }}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
|
||||
<div class="relative">
|
||||
<!-- <div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup="handleSearch"
|
||||
@@ -66,7 +66,16 @@
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<ClientOnly>
|
||||
<div class="relative">
|
||||
<n-input round placeholder="搜索" v-model="searchQuery" @blur="handleSearch" @keyup.enter="handleSearch">
|
||||
<template #suffix>
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
<!-- 平台类型筛选 -->
|
||||
<div class="mt-3 flex flex-wrap gap-2" id="platformFilters">
|
||||
@@ -235,14 +244,30 @@
|
||||
<!-- 页脚 -->
|
||||
<AppFooter />
|
||||
</div>
|
||||
<div v-if="systemConfig.maintenance_mode" class="fixed inset-0 z-[1000000] flex items-center justify-center bg-gradient-to-br from-yellow-100/80 via-gray-900/90 to-yellow-200/80 backdrop-blur-sm">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl px-8 py-10 flex flex-col items-center max-w-xs w-full border border-yellow-200 dark:border-yellow-700">
|
||||
<i class="fas fa-tools text-yellow-500 text-5xl mb-6 animate-bounce-slow"></i>
|
||||
<h3 class="text-2xl font-extrabold text-yellow-600 dark:text-yellow-400 mb-2 tracking-wide drop-shadow">系统维护中</h3>
|
||||
<p class="text-base text-gray-600 dark:text-gray-300 mb-6 text-center leading-relaxed">
|
||||
我们正在进行系统升级和维护,预计很快恢复服务。<br>
|
||||
请稍后再试,感谢您的理解与支持!
|
||||
</p>
|
||||
<!-- 动态点点动画 -->
|
||||
<div class="flex space-x-1 mt-2">
|
||||
<span class="w-2 h-2 bg-yellow-400 rounded-full animate-blink"></span>
|
||||
<span class="w-2 h-2 bg-yellow-500 rounded-full animate-blink delay-200"></span>
|
||||
<span class="w-2 h-2 bg-yellow-600 rounded-full animate-blink delay-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 页面元数据
|
||||
useHead({
|
||||
title: '网盘资源数据库 - 首页',
|
||||
title: '老九网盘资源数据库 - 首页',
|
||||
meta: [
|
||||
{ name: 'description', content: '网盘资源数据库 - 一个现代化的资源管理系统' },
|
||||
{ name: 'description', content: '老九网盘资源管理系统, 一个现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘' },
|
||||
{ name: 'keywords', content: '网盘资源,资源管理,数据库' }
|
||||
]
|
||||
})
|
||||
@@ -250,14 +275,19 @@ useHead({
|
||||
// 获取运行时配置
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
import { useResourceApi, useStatsApi, usePanApi, useSystemConfigApi, usePublicSystemConfigApi } from '~/composables/useApi'
|
||||
|
||||
const resourceApi = useResourceApi()
|
||||
const statsApi = useStatsApi()
|
||||
const panApi = usePanApi()
|
||||
const publicSystemConfigApi = usePublicSystemConfigApi()
|
||||
|
||||
// 获取路由参数
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const searchQuery = ref(route.query.q as string || '')
|
||||
const searchQuery = ref(route.query.search as string || '')
|
||||
const currentPage = ref(parseInt(route.query.page as string) || 1)
|
||||
const pageSize = ref(200)
|
||||
const selectedPlatform = ref(route.query.platform as string || '')
|
||||
@@ -268,51 +298,36 @@ const isLoadingMore = ref(false)
|
||||
const hasMoreData = ref(true)
|
||||
const pageLoading = ref(false)
|
||||
|
||||
console.log(pageSize.value, currentPage.value)
|
||||
|
||||
// 使用 useAsyncData 获取资源数据
|
||||
const { data: resourcesData, pending, refresh } = await useAsyncData(
|
||||
() => `resources-${currentPage.value}-${searchQuery.value}-${selectedPlatform.value}`,
|
||||
() => $fetch('/api/resources', {
|
||||
params: {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value,
|
||||
pan_id: selectedPlatform.value
|
||||
}
|
||||
() => resourceApi.getResources({
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value,
|
||||
pan_id: selectedPlatform.value
|
||||
})
|
||||
)
|
||||
|
||||
// 获取统计数据
|
||||
const { data: statsData } = await useAsyncData('stats',
|
||||
() => $fetch('/api/stats')
|
||||
)
|
||||
const { data: statsData } = await useAsyncData('stats', () => statsApi.getStats())
|
||||
|
||||
// 获取平台数据
|
||||
const { data: platformsData } = await useAsyncData('platforms',
|
||||
() => $fetch('/api/pans')
|
||||
)
|
||||
const { data: platformsData } = await useAsyncData('platforms', () => panApi.getPans())
|
||||
|
||||
// 获取系统配置
|
||||
const { data: systemConfigData } = await useAsyncData('systemConfig',
|
||||
() => $fetch('/api/system-config')
|
||||
)
|
||||
|
||||
const sysConfig = (systemConfigData.value as any)?.data as any
|
||||
const panList = (platformsData.value as any)?.data?.list as any[]
|
||||
const resourceList = (resourcesData.value as any)?.data?.resources as any[]
|
||||
const total = (resourcesData.value as any)?.data?.total as number
|
||||
const { data: systemConfigData } = await useAsyncData('systemConfig', () => publicSystemConfigApi.getPublicSystemConfig())
|
||||
|
||||
// 从 SSR 数据中获取值
|
||||
const safeResources = computed(() => (resourcesData.value as any)?.data?.resources || [])
|
||||
const safeStats = computed(() => (statsData.value as any)?.data || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0, today_updates: 0 })
|
||||
const platforms = computed(() => panList || [])
|
||||
const systemConfig = computed(() => sysConfig || { site_title: '网盘资源数据库' })
|
||||
const safeResources = computed(() => (resourcesData.value as any)?.data || [])
|
||||
const safeStats = computed(() => (statsData.value as any) || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0, today_updates: 0 })
|
||||
const platforms = computed(() => (platformsData.value as any) || [])
|
||||
const systemConfig = computed(() => (systemConfigData.value as any).data || { site_title: '老九网盘资源数据库' })
|
||||
const safeLoading = computed(() => pending.value)
|
||||
|
||||
// 计算属性
|
||||
const totalPages = computed(() => {
|
||||
const total = (resourcesData.value as any)?.data?.total || 0
|
||||
const total = (resourcesData.value as any)?.total || 0
|
||||
return Math.ceil(total / pageSize.value)
|
||||
})
|
||||
|
||||
@@ -327,15 +342,18 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async () => {
|
||||
const handleSearch = async (e?: any) => {
|
||||
if (e && e.target && typeof e.target.value === 'string') {
|
||||
searchQuery.value = e.target.value
|
||||
}
|
||||
currentPage.value = 1
|
||||
|
||||
// 更新URL参数
|
||||
const query = { ...route.query }
|
||||
if (searchQuery.value.trim()) {
|
||||
query.q = searchQuery.value.trim()
|
||||
query.search = searchQuery.value.trim()
|
||||
} else {
|
||||
delete query.q
|
||||
delete query.search
|
||||
}
|
||||
if (selectedPlatform.value) {
|
||||
query.platform = selectedPlatform.value
|
||||
@@ -374,12 +392,23 @@ const filterByPlatform = async (platformId: string) => {
|
||||
|
||||
// 获取平台名称
|
||||
const getPlatformIcon = (panId: string) => {
|
||||
const platform = platforms.value.find(p => p.id === panId)
|
||||
const platform = (platforms.value as any).find((p: any) => p.id === panId)
|
||||
return platform?.icon || '未知平台'
|
||||
}
|
||||
|
||||
// 跳转到链接
|
||||
const openLink = async (url: string, resourceId: number) => {
|
||||
try {
|
||||
await fetch(`/api/resources/${resourceId}/view`, { method: 'POST' })
|
||||
} catch (e) {}
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 切换链接显示
|
||||
const toggleLink = (resource: any) => {
|
||||
const toggleLink = async (resource: any) => {
|
||||
try {
|
||||
await resourceApi.incrementViewCount(resource.id)
|
||||
} catch (e) {}
|
||||
selectedResource.value = resource
|
||||
showLinkModal.value = true
|
||||
}
|
||||
@@ -403,11 +432,6 @@ const copyToClipboard = async (text: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到链接
|
||||
const openLink = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 格式化相对时间
|
||||
const formatRelativeTime = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
@@ -544,4 +568,20 @@ const loadMore = async () => {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0);}
|
||||
50% { transform: translateY(-12px);}
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 1.6s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 80%, 100% { opacity: 0.2;}
|
||||
40% { opacity: 1;}
|
||||
}
|
||||
.animate-blink {
|
||||
animation: blink 1.4s infinite both;
|
||||
}
|
||||
.animate-blink.delay-200 { animation-delay: 0.2s; }
|
||||
.animate-blink.delay-400 { animation-delay: 0.4s; }
|
||||
</style>
|
||||
@@ -155,7 +155,7 @@ const handleLogin = async () => {
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '管理员登录 - 网盘资源数据库'
|
||||
title: '管理员登录 - 老九网盘资源数据库'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -241,10 +241,12 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useMonitorApi } from '~/composables/useApi'
|
||||
const monitorApi = useMonitorApi()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -283,8 +285,6 @@ const formatTimestamp = (timestamp: number) => {
|
||||
// 获取系统信息
|
||||
const fetchSystemInfo = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getSystemInfo()
|
||||
systemInfo.value = response
|
||||
} catch (error) {
|
||||
@@ -295,10 +295,10 @@ const fetchSystemInfo = async () => {
|
||||
// 获取性能统计
|
||||
const fetchPerformanceStats = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getPerformanceStats()
|
||||
performanceStats.value = response
|
||||
console.log('性能统计数据:', response)
|
||||
console.log('数据库连接信息:', (response as any).database)
|
||||
} catch (error) {
|
||||
console.error('获取性能统计失败:', error)
|
||||
}
|
||||
@@ -307,8 +307,6 @@ const fetchPerformanceStats = async () => {
|
||||
// 获取基础统计
|
||||
const fetchBasicStats = async () => {
|
||||
try {
|
||||
const { useMonitorApi } = await import('~/composables/useApi')
|
||||
const monitorApi = useMonitorApi()
|
||||
const response = await monitorApi.getBasicStats()
|
||||
basicStats.value = response
|
||||
} catch (error) {
|
||||
|
||||
@@ -210,7 +210,7 @@ const handleRegister = async () => {
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '用户注册 - 网盘资源数据库'
|
||||
title: '用户注册 - 老九网盘资源数据库'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// 在服务端调用后端 API
|
||||
const response = await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
baseURL: String(process.server ? config.public.apiServer : config.public.apiBase),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// 在服务端调用后端 API
|
||||
const response = await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
baseURL: String(process.server ? config.public.apiServer : config.public.apiBase),
|
||||
query,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// 在服务端调用后端 API
|
||||
const response = await $fetch('/stats', {
|
||||
baseURL: config.public.apiBase,
|
||||
baseURL: String(process.server ? config.public.apiServer : config.public.apiBase),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// 在服务端调用后端 API
|
||||
const response = await $fetch('/system/config', {
|
||||
baseURL: config.public.apiBase,
|
||||
baseURL: String(process.server ? config.public.apiServer : config.public.apiBase),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -15,11 +15,11 @@ export default defineEventHandler(async (event) => {
|
||||
console.error('服务端获取系统配置失败:', error)
|
||||
// 返回默认配置而不是抛出错误
|
||||
return {
|
||||
site_title: '网盘资源数据库',
|
||||
site_title: '老九网盘资源数据库',
|
||||
site_description: '一个现代化的资源管理系统',
|
||||
keywords: '网盘资源,资源管理,数据库',
|
||||
author: '老九',
|
||||
copyright: '© 2025 网盘资源数据库'
|
||||
copyright: '© 2025 老九网盘资源数据库'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const apiBase = config.public.apiBase || 'http://localhost:8080/api'
|
||||
const apiBase = String(process.server ? config.public.apiServer : config.public.apiBase)
|
||||
|
||||
try {
|
||||
const response = await $fetch(`${apiBase}/version`)
|
||||
@@ -11,4 +11,4 @@ export default defineEventHandler(async (event) => {
|
||||
statusMessage: error.statusMessage || '获取版本信息失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
27
web/stores/systemConfig.ts
Normal file
27
web/stores/systemConfig.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useApiFetch } from '~/composables/useApiFetch'
|
||||
|
||||
export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
state: () => ({
|
||||
config: null as any,
|
||||
initialized: false
|
||||
}),
|
||||
actions: {
|
||||
async initConfig(force = false) {
|
||||
if (this.initialized && !force) return
|
||||
try {
|
||||
const data = await useApiFetch('/public/system-config').then((res: any) => res.data || res)
|
||||
this.config = data
|
||||
this.initialized = true
|
||||
} catch (e) {
|
||||
// 可根据需要处理错误
|
||||
this.config = null
|
||||
this.initialized = false
|
||||
}
|
||||
},
|
||||
setConfig(newConfig: any) {
|
||||
this.config = newConfig
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -160,6 +160,14 @@ export const useUserStore = defineStore('user', {
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
console.error('获取用户资料失败:', error)
|
||||
// 检查是否为"无效的令牌"错误
|
||||
if (error?.data?.error === '无效的令牌' || error?.message?.includes('无效的令牌')) {
|
||||
this.logout()
|
||||
if (process.client) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return { success: false, message: '登录已过期,请重新登录' }
|
||||
}
|
||||
// 如果获取失败,可能是token过期,清除登录状态
|
||||
this.logout()
|
||||
return { success: false, message: error.message }
|
||||
|
||||
Reference in New Issue
Block a user