21 Commits

Author SHA1 Message Date
Kerwin
32c8c30c05 chore: bump version to v1.0.9 2025-07-24 11:18:10 +08:00
ctwj
e2d4960c4c update: Finish Auto save 2025-07-24 01:05:46 +08:00
Kerwin
42ffc1e2e8 fix: 修复打包问题 2025-07-23 21:48:00 +08:00
Kerwin
cdd6b9985c update: Dockefile 2025-07-23 19:27:31 +08:00
Kerwin
dbc8fa9c36 update: version 1.0.8 2025-07-23 18:46:57 +08:00
Kerwin
67e15e03dc chore: bump version to v1.0.8 2025-07-23 18:46:23 +08:00
Kerwin
4ad176273e Merge branch 'main' of github.com:ctwj/urldb 2025-07-23 18:43:19 +08:00
Kerwin
a606897253 add: 新增维护模式 2025-07-23 18:42:24 +08:00
ctwj
cf31106cb7 Create CNAME 2025-07-23 15:01:10 +08:00
Kerwin
a21554f1cd fix: 修复UI显示 2025-07-23 12:31:45 +08:00
Kerwin
edfb0a43aa add: 账号管理添加启用禁用功能 2025-07-23 11:41:12 +08:00
ctwj
35052f7735 update: 批量添加资源, 自动处理资源优化 2025-07-23 01:11:42 +08:00
Kerwin
8a3d01fd28 update: README 2025-07-22 09:40:23 +08:00
ctwj
6e59133924 update: api 2025-07-22 00:44:56 +08:00
ctwj
91b743999a update: 资源优化 2025-07-22 00:09:46 +08:00
ctwj
ed6a1567f3 update: admin ui 2025-07-21 23:38:28 +08:00
ctwj
d3ed3ef990 add: 搜索统计 2025-07-21 23:04:46 +08:00
ctwj
ea60d730e2 update: UI opt 2025-07-21 22:52:41 +08:00
ctwj
21e2779d28 update: 优化api 2025-07-21 21:24:50 +08:00
Kerwin
c54a78c67f fix: 修复docker接口访问不对的问题 2025-07-21 19:29:32 +08:00
Kerwin
db41ba5ce3 update:version 1.0.7 2025-07-21 16:37:00 +08:00
57 changed files with 1134 additions and 1253 deletions

View File

@@ -2,27 +2,21 @@
FROM node:20-slim AS frontend-builder
# 安装pnpm
RUN npm install -g pnpm
WORKDIR /app/web
COPY web/package*.json ./
COPY web/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY web/ ./
RUN pnpm run build
RUN npm install --frozen-lockfile
RUN npm run build
# 前端运行阶段
FROM node:18-alpine AS frontend
FROM node:20-alpine AS frontend
RUN npm install -g pnpm
# RUN npm install -g pnpm
ENV NODE_ENV=production
WORKDIR /app
COPY --from=frontend-builder /app/web/.output ./.output
COPY --from=frontend-builder /app/web/package*.json ./
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
# 后端构建阶段

View File

@@ -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.8)
./scripts/version.sh minor # 次版本 1.0.8)
./scripts/version.sh major # 主版本 1.0.8)
# 发布版本到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,16 +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.6 --target frontend .
docker build -t ctwj/urldb-backend:1.0.6 --target backend .
docker push ctwj/urldb-frontend:1.0.6
docker push ctwj/urldb-backend:1.0.6
```
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
```
---

View File

@@ -1 +1 @@
1.0.7
1.0.9

View File

@@ -42,6 +42,7 @@ type PanConfig struct {
ExpiredType int `json:"expiredType"` // 1: 分享永久, 2: 临时
AdFid string `json:"adFid"` // 夸克专用 - 分享时带上这个文件的fid
Stoken string `json:"stoken"`
Cookie string `json:"cookie"`
}
// TransferResult 转存结果

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
@@ -42,6 +43,7 @@ func NewQuarkPanService(config *PanConfig) *QuarkPanService {
"Referer": "https://pan.quark.cn/",
"Referrer-Policy": "strict-origin-when-cross-origin",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Cookie": config.Cookie,
})
})
@@ -66,6 +68,25 @@ func (q *QuarkPanService) UpdateConfig(config *PanConfig) {
defer q.configMutex.Unlock()
q.config = config
// 设置Cookie到header
if config.Cookie != "" {
q.SetHeader("Cookie", config.Cookie)
}
}
// SetCookie 设置Cookie
func (q *QuarkPanService) SetCookie(cookie string) {
q.SetHeader("Cookie", cookie)
q.configMutex.Lock()
if q.config != nil {
q.config.Cookie = cookie
}
q.configMutex.Unlock()
}
// GetCookie 获取当前Cookie
func (q *QuarkPanService) GetCookie() string {
return q.GetHeader("Cookie")
}
// GetServiceType 获取服务类型
@@ -383,11 +404,23 @@ func (q *QuarkPanService) getShareSave(shareID, stoken string, fidList, fidToken
return &response.Data, nil
}
// 生成指定长度的时间戳
func (q *QuarkPanService) generateTimestamp(length int) int64 {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
timestampStr := strconv.FormatInt(timestamp, 10)
if len(timestampStr) > length {
timestampStr = timestampStr[:length]
}
timestamp, _ = strconv.ParseInt(timestampStr, 10, 64)
return timestamp
}
// getShareBtn 分享按钮
func (q *QuarkPanService) getShareBtn(fidList []string, title string) (*ShareBtnResult, error) {
data := map[string]interface{}{
"fid_list": fidList,
"title": title,
"url_type": 1,
"expired_type": 1, // 永久分享
}
@@ -397,7 +430,7 @@ func (q *QuarkPanService) getShareBtn(fidList []string, title string) (*ShareBtn
"uc_param_str": "",
}
respData, err := q.HTTPPost("https://drive-pc.quark.cn/1/clouddrive/share/create", data, queryParams)
respData, err := q.HTTPPost("https://drive-pc.quark.cn/1/clouddrive/share", data, queryParams)
if err != nil {
return nil, err
}
@@ -427,9 +460,11 @@ func (q *QuarkPanService) getShareTask(taskID string, retryIndex int) (*TaskResu
"uc_param_str": "",
"task_id": taskID,
"retry_index": fmt.Sprintf("%d", retryIndex),
"__dt": "21192",
"__t": fmt.Sprintf("%d", q.generateTimestamp(13)),
}
respData, err := q.HTTPGet("https://drive-pc.quark.cn/1/clouddrive/share/sharepage/task", queryParams)
respData, err := q.HTTPGet("https://drive-pc.quark.cn/1/clouddrive/task", queryParams)
if err != nil {
return nil, err
}
@@ -457,10 +492,13 @@ func (q *QuarkPanService) getSharePassword(shareID string) (*PasswordResult, err
"pr": "ucpro",
"fr": "pc",
"uc_param_str": "",
"share_id": shareID,
}
respData, err := q.HTTPGet("https://drive-pc.quark.cn/1/clouddrive/share/sharepage/password", queryParams)
data := map[string]interface{}{
"share_id": shareID,
}
respData, err := q.HTTPPost("https://drive-pc.quark.cn/1/clouddrive/share/password", data, queryParams)
if err != nil {
return nil, err
}

View File

@@ -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{},

View File

@@ -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
@@ -13,7 +14,7 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
Description: resource.Description,
URL: resource.URL,
PanID: resource.PanID,
QuarkURL: resource.QuarkURL,
SaveURL: resource.SaveURL,
FileSize: resource.FileSize,
CategoryID: resource.CategoryID,
ViewCount: resource.ViewCount,
@@ -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,
}
}

View File

@@ -52,12 +52,15 @@ type CreateResourceRequest struct {
Description string `json:"description"`
URL string `json:"url"`
PanID *uint `json:"pan_id"`
QuarkURL string `json:"quark_url"`
SaveURL string `json:"save_url"`
FileSize string `json:"file_size"`
CategoryID *uint `json:"category_id"`
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 更新资源请求
@@ -66,12 +69,15 @@ type UpdateResourceRequest struct {
Description string `json:"description"`
URL string `json:"url"`
PanID *uint `json:"pan_id"`
QuarkURL string `json:"quark_url"`
SaveURL string `json:"save_url"`
FileSize string `json:"file_size"`
CategoryID *uint `json:"category_id"`
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 创建分类请求

View File

@@ -17,7 +17,7 @@ type ResourceResponse struct {
Description string `json:"description"`
URL string `json:"url"`
PanID *uint `json:"pan_id"`
QuarkURL string `json:"quark_url"`
SaveURL string `json:"save_url"`
FileSize string `json:"file_size"`
CategoryID *uint `json:"category_id"`
CategoryName string `json:"category_name"`
@@ -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 分类响应

View File

@@ -13,7 +13,7 @@ type Resource struct {
Description string `json:"description" gorm:"type:text;comment:资源描述"`
URL string `json:"url" gorm:"size:128;comment:资源链接"`
PanID *uint `json:"pan_id" gorm:"comment:平台ID"`
QuarkURL string `json:"quark_url" gorm:"size:500;comment:夸克链接"`
SaveURL string `json:"save_url" gorm:"size:500;comment:转存后的链接"`
FileSize string `json:"file_size" gorm:"size:100;comment:文件大小"`
CategoryID *uint `json:"category_id" gorm:"comment:分类ID"`
ViewCount int `json:"view_count" gorm:"default:0;comment:浏览次数"`
@@ -22,6 +22,11 @@ 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:转存失败原因"`
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
// 关联关系
Category Category `json:"category" gorm:"foreignKey:CategoryID"`

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -31,6 +31,8 @@ type ResourceRepository interface {
GetCachedLatestResources(limit int) ([]entity.Resource, error)
InvalidateCache() error
FindExists(url string, excludeID ...uint) (bool, error)
BatchFindByURLs(urls []string) ([]entity.Resource, error)
GetResourcesForTransfer(panID uint, sinceTime time.Time) ([]*entity.Resource, error)
}
// ResourceRepositoryImpl Resource的Repository实现
@@ -344,3 +346,26 @@ 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
}
// GetResourcesForTransfer 获取需要转存的资源
func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime time.Time) ([]*entity.Resource, error) {
var resources []*entity.Resource
query := r.db.Where("pan_id = ? AND (save_url = '' OR save_url IS NULL) AND (error_msg = '' OR error_msg IS NULL)", panID)
if !sinceTime.IsZero() {
query = query.Where("created_at >= ?", sinceTime)
}
err := query.Order("created_at DESC").Find(&resources).Error
if err != nil {
return nil, err
}
return resources, nil
}

View File

@@ -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
}

View File

@@ -20,9 +20,7 @@ services:
- app-network
backend:
image: ctwj/urldb-backend:1.0.6
expose:
- "8080"
image: ctwj/urldb-backend:1.0.8
environment:
DB_HOST: postgres
DB_PORT: 5432
@@ -39,9 +37,10 @@ services:
- app-network
frontend:
image: ctwj/urldb-frontend:1.0.6
image: ctwj/urldb-frontend:1.0.8
environment:
API_BASE: /api
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
NUXT_PUBLIC_API_CLIENT: /api
depends_on:
- backend
networks:

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
p.l9.lc

View File

@@ -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

View File

@@ -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,
})
}

View File

@@ -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)

View File

@@ -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,
@@ -119,11 +120,14 @@ func CreateResource(c *gin.Context) {
Description: req.Description,
URL: req.URL,
PanID: req.PanID,
QuarkURL: req.QuarkURL,
SaveURL: req.SaveURL,
FileSize: req.FileSize,
CategoryID: req.CategoryID,
IsValid: req.IsValid,
IsPublic: req.IsPublic,
Cover: req.Cover,
Author: req.Author,
ErrorMsg: req.ErrorMsg,
}
err := repoManager.ResourceRepository.Create(resource)
@@ -181,8 +185,8 @@ func UpdateResource(c *gin.Context) {
if req.PanID != nil {
resource.PanID = req.PanID
}
if req.QuarkURL != "" {
resource.QuarkURL = req.QuarkURL
if req.SaveURL != "" {
resource.SaveURL = req.SaveURL
}
if req.FileSize != "" {
resource.FileSize = req.FileSize
@@ -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": "批量删除成功"})
}

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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()),
},

View File

@@ -10,6 +10,7 @@ import (
commonutils "github.com/ctwj/urldb/common/utils"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"gorm.io/gorm"
)
// Scheduler 定时任务管理器
@@ -284,7 +285,7 @@ func (s *Scheduler) StartReadyResourceScheduler() {
go func() {
// 获取系统配置中的间隔时间
config, err := s.systemConfigRepo.GetOrCreateDefault()
interval := 5 * time.Minute // 默认5分钟
interval := 3 * time.Minute // 默认5分钟
if err == nil && config.AutoProcessInterval > 0 {
interval = time.Duration(config.AutoProcessInterval) * time.Minute
}
@@ -658,22 +659,45 @@ func (s *Scheduler) processAutoTransfer() {
return
}
// 获取所有有效的网盘账号
// 获取quark平台ID
panRepoImpl, ok := s.panRepo.(interface{ GetDB() *gorm.DB })
if !ok {
Error("panRepo不支持GetDB方法")
return
}
var quarkPan entity.Pan
err = panRepoImpl.GetDB().Where("name = ?", "quark").First(&quarkPan).Error
if err != nil {
Error("未找到quark平台: %v", err)
return
}
quarkPanID := quarkPan.ID
// 获取所有账号
accounts, err := s.cksRepo.FindAll()
if err != nil {
Error("获取网盘账号失败: %v", err)
return
}
if len(accounts) == 0 {
Info("没有可用的网盘账号")
// 过滤只保留已激活、quark平台、剩余空间足够的账号
minSpaceBytes := int64(config.AutoTransferMinSpace) * 1024 * 1024 * 1024
var validAccounts []entity.Cks
for _, acc := range accounts {
if acc.IsValid && acc.PanID == quarkPanID && acc.LeftSpace >= minSpaceBytes {
validAccounts = append(validAccounts, acc)
}
}
if len(validAccounts) == 0 {
Info("没有可用的quark网盘账号")
return
}
Info("找到 %d 个网盘账号,开始自动转存处理...", len(accounts))
Info("找到 %d 个可用quark网盘账号,开始自动转存处理...", len(validAccounts))
// 获取需要转存的资源
resources, err := s.getResourcesForTransfer(config)
resources, err := s.getResourcesForTransfer(config, quarkPanID)
if err != nil {
Error("获取需要转存的资源失败: %v", err)
return
@@ -686,55 +710,120 @@ func (s *Scheduler) processAutoTransfer() {
Info("找到 %d 个需要转存的资源", len(resources))
// 执行自动转存
transferCount := 0
for _, resource := range resources {
if err := s.transferResource(resource, accounts, config); err != nil {
Error("转存资源失败 (ID: %d): %v", resource.ID, err)
} else {
transferCount++
Info("成功转存资源: %s", resource.Title)
}
// 并发自动转存
resourceCh := make(chan *entity.Resource, len(resources))
for _, res := range resources {
resourceCh <- res
}
close(resourceCh)
Info("自动转存处理完成,共转存 %d 个资源", transferCount)
var wg sync.WaitGroup
for _, account := range validAccounts {
wg.Add(1)
go func(acc entity.Cks) {
defer wg.Done()
factory := panutils.GetInstance() // 使用单例模式
for res := range resourceCh {
if err := s.transferResource(res, []entity.Cks{acc}, config, factory); err != nil {
Error("转存资源失败 (ID: %d): %v", res.ID, err)
} else {
Info("成功转存资源: %s", res.Title)
}
}
}(account)
}
wg.Wait()
Info("自动转存处理完成,账号数: %d资源数: %d", len(validAccounts), len(resources))
}
// getResourcesForTransfer 获取需要转存的资源
func (s *Scheduler) getResourcesForTransfer(config *entity.SystemConfig) ([]*entity.Resource, error) {
// TODO: 实现获取需要转存的资源逻辑
// 1. 获取所有有效的资源
// 2. 根据配置的转存限制天数过滤资源
// 3. 排除已经转存过的资源
// 4. 按优先级排序(可以根据浏览次数、创建时间等)
Info("获取需要转存的资源 - 限制天数: %d", config.AutoTransferLimitDays)
// 临时返回空数组,等待具体实现
return []*entity.Resource{}, nil
}
// transferResource 转存单个资源
func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entity.Cks, config *entity.SystemConfig) error {
// TODO: 实现单个资源的转存逻辑
// 1. 选择合适的网盘账号根据剩余空间、VIP状态等
// 2. 检查账号剩余空间是否满足最小空间要求
// 3. 调用网盘API进行转存
// 4. 更新资源状态和转存记录
// 5. 更新账号使用空间
Info("开始转存资源: %s (ID: %d)", resource.Title, resource.ID)
// 选择最佳账号
selectedAccount := s.selectBestAccount(accounts, config)
if selectedAccount == nil {
return fmt.Errorf("没有合适的网盘账号")
func (s *Scheduler) getResourcesForTransfer(config *entity.SystemConfig, quarkPanID uint) ([]*entity.Resource, error) {
days := config.AutoTransferLimitDays
var sinceTime time.Time
if days > 0 {
sinceTime = time.Now().AddDate(0, 0, -days)
} else {
sinceTime = time.Time{}
}
Info("选择账号: %s (剩余空间: %d GB)", selectedAccount.Username, selectedAccount.LeftSpace/1024/1024/1024)
repoImpl, ok := s.resourceRepo.(*repo.ResourceRepositoryImpl)
if !ok {
return nil, fmt.Errorf("resourceRepo不是ResourceRepositoryImpl类型")
}
return repoImpl.GetResourcesForTransfer(quarkPanID, sinceTime)
}
// TODO: 执行实际的转存操作
// 这里需要调用网盘API进行转存
var resourceUpdateMutex sync.Mutex // 全局互斥锁,保证多协程安全
// transferResource 转存单个资源
func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entity.Cks, config *entity.SystemConfig, factory *panutils.PanFactory) error {
if len(accounts) == 0 {
return fmt.Errorf("没有可用的网盘账号")
}
account := accounts[0]
service, err := factory.CreatePanService(resource.URL, &panutils.PanConfig{
URL: resource.URL,
ExpiredType: 0,
IsType: 0,
Cookie: account.Ck,
})
if err != nil {
return fmt.Errorf("创建网盘服务失败: %v", err)
}
shareID, _ := commonutils.ExtractShareIdString(resource.URL)
result, err := service.Transfer(shareID)
if err != nil {
resourceUpdateMutex.Lock()
defer resourceUpdateMutex.Unlock()
s.resourceRepo.Update(&entity.Resource{
ID: resource.ID,
ErrorMsg: err.Error(),
})
return fmt.Errorf("转存失败: %v", err)
}
if result == nil || !result.Success {
errMsg := "转存失败"
if result != nil && result.Message != "" {
errMsg = result.Message
}
resourceUpdateMutex.Lock()
defer resourceUpdateMutex.Unlock()
s.resourceRepo.Update(&entity.Resource{
ID: resource.ID,
ErrorMsg: errMsg,
})
return fmt.Errorf("转存失败: %s", errMsg)
}
// 提取转存链接、fid等
var saveURL, fid string
if data, ok := result.Data.(map[string]interface{}); ok {
if v, ok := data["shareUrl"]; ok {
saveURL, _ = v.(string)
}
if v, ok := data["fid"]; ok {
fid, _ = v.(string)
}
}
if saveURL == "" {
saveURL = result.ShareURL
}
resourceUpdateMutex.Lock()
defer resourceUpdateMutex.Unlock()
err = s.resourceRepo.Update(&entity.Resource{
ID: resource.ID,
SaveURL: saveURL,
CkID: &account.ID,
Fid: fid,
ErrorMsg: "",
})
if err != nil {
return fmt.Errorf("保存转存结果失败: %v", err)
}
return nil
}

2
web/components.d.ts vendored
View File

@@ -12,6 +12,8 @@ declare module 'vue' {
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']

View File

@@ -61,21 +61,41 @@
</div>
<!-- 自动转存状态提示 -->
<div
v-if="systemConfig?.auto_transfer_enabled"
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 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-xs text-white font-medium">
自动转存已开启
</span>
<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>
</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
}
@@ -103,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: '系统版本详情' }
}
@@ -114,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 () => {

View File

@@ -4,7 +4,7 @@
<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 <n-a
<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"
@@ -19,15 +19,17 @@
</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(() => {

View File

@@ -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>

View File

@@ -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)
}
}

View 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
}
})
}

View File

@@ -18,7 +18,7 @@ interface VersionResponse {
export const useVersion = () => {
const versionInfo = ref<VersionInfo>({
version: '1.0.6',
version: '1.0.8',
build_time: '',
git_commit: 'unknown',
git_branch: 'unknown',

View File

@@ -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

View File

@@ -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: {

View File

@@ -1,6 +1,6 @@
{
"name": "res-db-web",
"version": "1.0.6",
"version": "1.0.8",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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')

View File

@@ -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)
@@ -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) {

View File

@@ -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

View File

@@ -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 = () => {

View File

@@ -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()
// 获取系统配置

View File

@@ -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

View File

@@ -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)
}

View File

@@ -20,7 +20,7 @@
<form @submit.prevent="saveConfig" class="space-y-6">
<n-tabs type="line" animated>
<n-tab-pane name="SEO 配置" tab="seo">
<n-tab-pane name="SEO 配置" tab="SEO 配置">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网站标题 -->
<div>
@@ -91,27 +91,48 @@
</n-tab-pane>
<n-tab-pane name="自动处理配置" tab="自动处理配置">
<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">
开启后系统将自动处理待处理的资源无需手动操作
<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">
@@ -197,22 +218,7 @@
</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>
</n-tab-pane>
<n-tab-pane name="其他配置" tab="其他配置">
@@ -334,9 +340,12 @@ 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)
@@ -388,7 +397,7 @@ useHead({
const loadConfig = async () => {
try {
loading.value = true
const response = await getSystemConfig()
const response = await systemConfigApi.getSystemConfig()
console.log('系统配置响应:', response)
// 使用新的统一响应格式直接使用response
@@ -441,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('保存配置失败:未知错误')
}

View File

@@ -331,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)
}
@@ -355,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 {
@@ -436,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)
@@ -465,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()

View File

@@ -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>

View File

@@ -209,6 +209,8 @@ definePageMeta({
})
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)

View File

@@ -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">
@@ -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,6 +244,22 @@
<!-- 页脚 -->
<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">
@@ -250,14 +275,21 @@ 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 initSerch = route.query.search || ''
const oldQuery = ref(initSerch)
const searchQuery = ref(oldQuery)
const currentPage = ref(parseInt(route.query.page as string) || 1)
const pageSize = ref(200)
const selectedPlatform = ref(route.query.platform as string || '')
@@ -268,51 +300,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 +344,22 @@ onMounted(() => {
})
// 搜索处理
const handleSearch = async () => {
const handleSearch = async (e?: any) => {
if (e && e.target && typeof e.target.value === 'string') {
searchQuery.value = e.target.value
}
if (oldQuery.value === searchQuery.value) {
return
}
oldQuery.value = searchQuery.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 +398,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 +438,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 +574,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>

View File

@@ -245,6 +245,8 @@ definePageMeta({
})
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) {

View File

@@ -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'
}

View File

@@ -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'

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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 || '获取版本信息失败'
})
}
})
})

View 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
}
}
})

View File

@@ -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 }