Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8fd405d74 | ||
|
|
5f8d998c65 | ||
|
|
b5b3c55573 | ||
|
|
1d3ed2f8aa | ||
|
|
215f3170cd | ||
|
|
0700de36f5 | ||
|
|
14130eac8b | ||
|
|
bad6da4488 | ||
|
|
1126f84a3a | ||
|
|
24d644dc8b | ||
|
|
d0ac53320e | ||
|
|
853bb50854 | ||
|
|
dfb6a1707c | ||
|
|
9098b28ba6 | ||
|
|
b5e5052146 | ||
|
|
e88b8411b5 | ||
|
|
d1b406b1ee | ||
|
|
10432c1db6 | ||
|
|
440049c974 | ||
|
|
afb5a38f15 | ||
|
|
1ea7e87e6f | ||
|
|
e6b4455428 | ||
|
|
6aacf9aed8 | ||
|
|
1f6fdfba1a | ||
|
|
4d466af99e | ||
|
|
c1b19cf937 | ||
|
|
4d3f4a082e | ||
|
|
ba7dd4d064 | ||
|
|
78b147da47 | ||
|
|
f9ecbad0a7 | ||
|
|
53fbaabc63 | ||
|
|
97f92ea26c | ||
|
|
d7b273dfae | ||
|
|
4c56289bfe | ||
|
|
cf3376eb31 | ||
|
|
312ecb041a | ||
|
|
a5c5e41cc4 | ||
|
|
f0e5c93a48 | ||
|
|
2582920e2c | ||
|
|
50ee23db1c | ||
|
|
6cbd1f5d17 | ||
|
|
eba01b540b | ||
|
|
0434d069ce | ||
|
|
443d67ad78 | ||
|
|
4463960447 | ||
|
|
595c44b437 | ||
|
|
00606ef73e | ||
|
|
d4fe64819f |
1
.gitignore
vendored
@@ -15,6 +15,7 @@ go.work.sum
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!web/.env
|
||||
web/.output/
|
||||
|
||||
# IDE
|
||||
|
||||
@@ -76,10 +76,4 @@
|
||||
1. 在提交代码时使用规范的提交信息2. 在Pull Request中描述您的更改
|
||||
3. 遵循项目的贡献指南
|
||||
|
||||
---
|
||||
|
||||
## 链接
|
||||
|
||||
- [项目主页](https://github.com/your-username/l9pan)
|
||||
- [问题反馈](https://github.com/your-username/l9pan/issues)
|
||||
- [讨论区](https://github.com/your-username/l9
|
||||
---
|
||||
12
Dockerfile
@@ -5,6 +5,8 @@ FROM node:20-slim AS frontend-builder
|
||||
WORKDIR /app/web
|
||||
COPY web/ ./
|
||||
RUN npm install --frozen-lockfile
|
||||
ARG NUXT_PUBLIC_API_SERVER=http://backend:8080/api
|
||||
ARG NUXT_PUBLIC_API_CLIENT=/api
|
||||
RUN npm run build
|
||||
|
||||
# 前端运行阶段
|
||||
@@ -26,12 +28,18 @@ WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 先复制VERSION文件,确保构建时能正确读取版本号
|
||||
COPY VERSION ./
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||
|
||||
# 后端运行阶段
|
||||
FROM alpine:latest AS backend
|
||||
|
||||
# 安装时区数据
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# 复制后端二进制文件
|
||||
@@ -40,6 +48,10 @@ COPY --from=backend-builder /app/main .
|
||||
# 创建uploads目录
|
||||
RUN mkdir -p uploads
|
||||
|
||||
# 设置环境变量
|
||||
ENV GIN_MODE=release
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
58
README.md
@@ -10,7 +10,7 @@
|
||||
|
||||
**一个现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘 **
|
||||
|
||||
🌐 [在线演示](#) | 📖 [文档](#) | 🐛 [问题反馈](#) | ⭐ [给个星标](#)
|
||||
🌐 [在线演示](https://pan.l9.lc) | 📖 [文档](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink) | 🐛 [问题反馈](https://github.com/ctwj/urldb/issues) | ⭐ [给个星标](https://github.com/ctwj/urldb)
|
||||
|
||||
### 支持的网盘平台
|
||||
|
||||
@@ -41,6 +41,27 @@
|
||||
|
||||
---
|
||||
|
||||
## 📸 项目截图
|
||||
|
||||
[文档说明](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink) [服务器要求](https://ecn5khs4t956.feishu.cn/wiki/W8YBww1Mmiu4Cdkp5W4c8pFNnMf?from=from_copylink)
|
||||
|
||||
### 🏠 首页
|
||||

|
||||
|
||||
### 🔧 后台管理
|
||||

|
||||
|
||||
### ⚙️ 系统配置
|
||||

|
||||
|
||||
### 🔍 搜索统计
|
||||

|
||||
|
||||
### 👤 多账号管理
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 🎯 核心功能
|
||||
@@ -68,6 +89,7 @@
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
@@ -175,35 +197,6 @@ l9pan/
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 版本管理
|
||||
|
||||
项目使用GitHub进行版本管理,支持自动创建Release和标签。
|
||||
|
||||
#### 版本管理脚本
|
||||
|
||||
```bash
|
||||
# 显示当前版本信息
|
||||
./scripts/version.sh show
|
||||
|
||||
# 更新版本号
|
||||
./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
|
||||
|
||||
# 生成版本信息文件
|
||||
./scripts/version.sh update
|
||||
|
||||
# 查看帮助
|
||||
./scripts/version.sh help
|
||||
```
|
||||
|
||||
#### 详细文档
|
||||
|
||||
查看 [GitHub版本管理指南](docs/github-version-management.md) 了解完整的版本管理流程。
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
@@ -216,6 +209,9 @@ DB_NAME=url_db
|
||||
|
||||
# 服务器配置
|
||||
PORT=8080
|
||||
|
||||
# 时区配置
|
||||
TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
### Docker 服务说明
|
||||
@@ -242,7 +238,7 @@ docker push ctwj/urldb-backend:1.0.7
|
||||
|
||||
提供,批量入库和搜索api,通过 apiToken 授权
|
||||
|
||||
> 📖 完整API文档请访问:`http://p.l9.lc/doc.html`
|
||||
> 📖 完整API文档请访问:`http://doc.l9.lc/`
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package pan
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -84,7 +83,7 @@ func (a *AlipanService) Transfer(shareID string) (*TransferResult, error) {
|
||||
config := a.config
|
||||
a.configMutex.RUnlock()
|
||||
|
||||
log.Printf("开始处理阿里云盘分享: %s", shareID)
|
||||
fmt.Printf("开始处理阿里云盘分享: %s", shareID)
|
||||
|
||||
// 获取access token
|
||||
accessToken, err := a.manageAccessToken()
|
||||
|
||||
@@ -16,6 +16,10 @@ const (
|
||||
BaiduPan
|
||||
UC
|
||||
NotFound
|
||||
Xunlei
|
||||
Tianyi
|
||||
Pan123
|
||||
Pan115
|
||||
)
|
||||
|
||||
// String 返回服务类型的字符串表示
|
||||
@@ -29,6 +33,14 @@ func (s ServiceType) String() string {
|
||||
return "baidu"
|
||||
case UC:
|
||||
return "uc"
|
||||
case Xunlei:
|
||||
return "xunlei"
|
||||
case Tianyi:
|
||||
return "tianyi"
|
||||
case Pan123:
|
||||
return "123pan"
|
||||
case Pan115:
|
||||
return "115"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -133,6 +145,10 @@ func (f *PanFactory) CreatePanServiceByType(serviceType ServiceType, config *Pan
|
||||
return NewBaiduPanService(config), nil
|
||||
case UC:
|
||||
return NewUCService(config), nil
|
||||
// case Xunlei:
|
||||
// return NewXunleiService(config), nil
|
||||
// case Tianyi:
|
||||
// return NewTianyiService(config), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的服务类型: %d", serviceType)
|
||||
}
|
||||
@@ -166,6 +182,11 @@ func (f *PanFactory) GetUCService(config *PanConfig) PanService {
|
||||
func ExtractServiceType(url string) ServiceType {
|
||||
url = strings.ToLower(url)
|
||||
|
||||
// "https://www.123pan.com/s/i4uaTd-WHn0", // 公开分享
|
||||
// "https://www.123912.com/s/U8f2Td-ZeOX",
|
||||
// "https://www.123684.coms/u9izjv-k3uWv",
|
||||
// "https://www.123pan.com/s/A6cA-AKH11", // 外链不存在
|
||||
|
||||
patterns := map[string]ServiceType{
|
||||
"pan.quark.cn": Quark,
|
||||
"www.alipan.com": Alipan,
|
||||
@@ -173,6 +194,14 @@ func ExtractServiceType(url string) ServiceType {
|
||||
"pan.baidu.com": BaiduPan,
|
||||
"drive.uc.cn": UC,
|
||||
"fast.uc.cn": UC,
|
||||
"pan.xunlei.com": Xunlei,
|
||||
"cloud.189.cn": Tianyi,
|
||||
"www.123pan.com": Pan123,
|
||||
"www.123912.com": Pan123,
|
||||
"www.123684.com": Pan123,
|
||||
"115cdn.com": Pan115,
|
||||
"anxia.com": Pan115,
|
||||
"115.com/": Pan115,
|
||||
}
|
||||
|
||||
for pattern, serviceType := range patterns {
|
||||
@@ -192,13 +221,24 @@ func ExtractShareId(url string) (string, ServiceType) {
|
||||
}
|
||||
|
||||
// 提取分享ID
|
||||
shareID := ""
|
||||
substring := strings.Index(url, "/s/")
|
||||
if substring == -1 {
|
||||
substring = strings.Index(url, "/t/") // 天翼云 是 t
|
||||
shareID = url[substring+3:]
|
||||
}
|
||||
if substring == -1 {
|
||||
substring = strings.Index(url, "/web/share?code=") // 天翼云 带密码
|
||||
shareID = url[substring+11:]
|
||||
}
|
||||
if substring == -1 {
|
||||
substring = strings.Index(url, "/p/") // 天翼云 是 p
|
||||
shareID = url[substring+3:]
|
||||
}
|
||||
if substring == -1 {
|
||||
return "", NotFound
|
||||
}
|
||||
|
||||
shareID := url[substring+3:] // 去除 '/s/' 部分
|
||||
|
||||
// 去除可能的锚点
|
||||
if hashIndex := strings.Index(shareID, "#"); hashIndex != -1 {
|
||||
shareID = shareID[:hashIndex]
|
||||
|
||||
135
db/connection.go
@@ -17,19 +17,16 @@ var DB *gorm.DB
|
||||
// InitDB 初始化数据库连接
|
||||
func InitDB() error {
|
||||
host := os.Getenv("DB_HOST")
|
||||
fmt.Printf("DB_HOST=%s\n", host)
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
port := os.Getenv("DB_PORT")
|
||||
fmt.Printf("DB_HOST=%s\n", port)
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
|
||||
user := os.Getenv("DB_USER")
|
||||
fmt.Printf("DB_HOST=%s\n", user)
|
||||
if user == "" {
|
||||
user = "postgres"
|
||||
}
|
||||
@@ -66,26 +63,34 @@ func InitDB() error {
|
||||
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
|
||||
|
||||
// 自动迁移数据库表结构
|
||||
err = DB.AutoMigrate(
|
||||
&entity.User{},
|
||||
&entity.Category{},
|
||||
&entity.Pan{},
|
||||
&entity.Cks{},
|
||||
&entity.Tag{},
|
||||
&entity.Resource{},
|
||||
&entity.ResourceTag{},
|
||||
&entity.ReadyResource{},
|
||||
&entity.SearchStat{},
|
||||
&entity.SystemConfig{},
|
||||
&entity.HotDrama{},
|
||||
)
|
||||
if err != nil {
|
||||
utils.Fatal("数据库迁移失败: %v", err)
|
||||
// 检查是否需要迁移(只在开发环境或首次启动时)
|
||||
if shouldRunMigration() {
|
||||
utils.Info("开始数据库迁移...")
|
||||
err = DB.AutoMigrate(
|
||||
&entity.User{},
|
||||
&entity.Category{},
|
||||
&entity.Pan{},
|
||||
&entity.Cks{},
|
||||
&entity.Tag{},
|
||||
&entity.Resource{},
|
||||
&entity.ResourceTag{},
|
||||
&entity.ReadyResource{},
|
||||
&entity.SearchStat{},
|
||||
&entity.SystemConfig{},
|
||||
&entity.HotDrama{},
|
||||
)
|
||||
if err != nil {
|
||||
utils.Fatal("数据库迁移失败: %v", err)
|
||||
}
|
||||
utils.Info("数据库迁移完成")
|
||||
} else {
|
||||
utils.Info("跳过数据库迁移(表结构已是最新)")
|
||||
}
|
||||
|
||||
// 创建索引以提高查询性能
|
||||
createIndexes(DB)
|
||||
// 创建索引以提高查询性能(只在需要迁移时)
|
||||
if shouldRunMigration() {
|
||||
createIndexes(DB)
|
||||
}
|
||||
|
||||
// 插入默认数据(只在数据库为空时)
|
||||
if err := insertDefaultDataIfEmpty(); err != nil {
|
||||
@@ -96,9 +101,36 @@ func InitDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldRunMigration 检查是否需要运行数据库迁移
|
||||
func shouldRunMigration() bool {
|
||||
// 通过环境变量控制是否运行迁移
|
||||
skipMigration := os.Getenv("SKIP_MIGRATION")
|
||||
if skipMigration == "true" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
env := os.Getenv("ENV")
|
||||
if env == "production" {
|
||||
// 生产环境:检查是否有迁移标记
|
||||
var count int64
|
||||
DB.Raw("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'schema_migrations'").Count(&count)
|
||||
if count == 0 {
|
||||
// 没有迁移表,说明是首次部署
|
||||
return true
|
||||
}
|
||||
// 有迁移表,检查是否需要迁移(这里可以添加更复杂的逻辑)
|
||||
return false
|
||||
}
|
||||
|
||||
// 开发环境:总是运行迁移
|
||||
return true
|
||||
}
|
||||
|
||||
// autoMigrate 自动迁移表结构
|
||||
func autoMigrate() error {
|
||||
return DB.AutoMigrate(
|
||||
&entity.SystemConfig{}, // 系统配置表(独立表,先创建)
|
||||
&entity.Pan{},
|
||||
&entity.Cks{},
|
||||
&entity.Category{},
|
||||
@@ -108,16 +140,13 @@ func autoMigrate() error {
|
||||
&entity.ReadyResource{},
|
||||
&entity.User{},
|
||||
&entity.SearchStat{},
|
||||
&entity.SystemConfig{},
|
||||
&entity.HotDrama{},
|
||||
)
|
||||
}
|
||||
|
||||
// createIndexes 创建数据库索引以提高查询性能
|
||||
func createIndexes(db *gorm.DB) {
|
||||
// 资源表索引
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_title ON resources USING gin(to_tsvector('chinese', title))")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_description ON resources USING gin(to_tsvector('chinese', description))")
|
||||
// 资源表索引(移除全文搜索索引,使用Meilisearch替代)
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_category_id ON resources(category_id)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_pan_id ON resources(pan_id)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_created_at ON resources(created_at DESC)")
|
||||
@@ -125,8 +154,17 @@ func createIndexes(db *gorm.DB) {
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_is_valid ON resources(is_valid)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_is_public ON resources(is_public)")
|
||||
|
||||
// 为Meilisearch准备的基础文本索引(用于精确匹配)
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_title ON resources(title)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resources_description ON resources(description)")
|
||||
|
||||
// 待处理资源表索引
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_key ON ready_resource(key)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_url ON ready_resource(url)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_ready_resource_create_time ON ready_resource(create_time DESC)")
|
||||
|
||||
// 搜索统计表索引
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_search_stats_query ON search_stats(query)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_search_stats_keyword ON search_stats(keyword)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_search_stats_created_at ON search_stats(created_at DESC)")
|
||||
|
||||
// 热播剧表索引
|
||||
@@ -138,7 +176,7 @@ func createIndexes(db *gorm.DB) {
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_resource_id ON resource_tags(resource_id)")
|
||||
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_tag_id ON resource_tags(tag_id)")
|
||||
|
||||
utils.Info("数据库索引创建完成")
|
||||
utils.Info("数据库索引创建完成(已移除全文搜索索引,准备使用Meilisearch)")
|
||||
}
|
||||
|
||||
// insertDefaultDataIfEmpty 只在数据库为空时插入默认数据
|
||||
@@ -159,11 +197,18 @@ func insertDefaultDataIfEmpty() error {
|
||||
|
||||
// 插入默认分类(使用FirstOrCreate避免重复)
|
||||
defaultCategories := []entity.Category{
|
||||
{Name: "文档", Description: "各种文档资料"},
|
||||
{Name: "软件", Description: "软件工具"},
|
||||
{Name: "视频", Description: "视频教程"},
|
||||
{Name: "图片", Description: "图片资源"},
|
||||
{Name: "音频", Description: "音频文件"},
|
||||
{Name: "电影", Description: "电影"},
|
||||
{Name: "电视剧", Description: "电视剧"},
|
||||
{Name: "短剧", Description: "短剧"},
|
||||
{Name: "综艺", Description: "综艺"},
|
||||
{Name: "动漫", Description: "动漫"},
|
||||
{Name: "纪录片", Description: "纪录片"},
|
||||
{Name: "视频教程", Description: "视频教程"},
|
||||
{Name: "学习资料", Description: "学习资料"},
|
||||
{Name: "游戏", Description: "其他游戏资源"},
|
||||
{Name: "软件", Description: "软件"},
|
||||
{Name: "APP", Description: "APP"},
|
||||
{Name: "AI", Description: "AI"},
|
||||
{Name: "其他", Description: "其他资源"},
|
||||
}
|
||||
|
||||
@@ -194,6 +239,32 @@ func insertDefaultDataIfEmpty() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 插入默认系统配置
|
||||
defaultSystemConfigs := []entity.SystemConfig{
|
||||
{Key: entity.ConfigKeySiteTitle, Value: entity.ConfigDefaultSiteTitle, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeySiteDescription, Value: entity.ConfigDefaultSiteDescription, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyKeywords, Value: entity.ConfigDefaultKeywords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAuthor, Value: entity.ConfigDefaultAuthor, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyCopyright, Value: entity.ConfigDefaultCopyright, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAutoProcessReadyResources, Value: entity.ConfigDefaultAutoProcessReadyResources, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAutoProcessInterval, Value: entity.ConfigDefaultAutoProcessInterval, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoTransferEnabled, Value: entity.ConfigDefaultAutoTransferEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAutoTransferLimitDays, Value: entity.ConfigDefaultAutoTransferLimitDays, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyForbiddenWords, Value: entity.ConfigDefaultForbiddenWords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
}
|
||||
|
||||
for _, config := range defaultSystemConfigs {
|
||||
if err := DB.Where("key = ?", config.Key).FirstOrCreate(&config).Error; err != nil {
|
||||
utils.Error("插入系统配置 %s 失败: %v", config.Key, err)
|
||||
// 继续执行,不因为单个配置失败而停止
|
||||
}
|
||||
}
|
||||
|
||||
// 插入默认管理员用户
|
||||
defaultAdmin := entity.User{
|
||||
Username: "admin",
|
||||
|
||||
@@ -3,7 +3,6 @@ package converter
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ToResourceResponse 将Resource实体转换为ResourceResponse
|
||||
@@ -171,16 +170,19 @@ func ToCksResponseList(cksList []entity.Cks) []dto.CksResponse {
|
||||
// ToReadyResourceResponse 将ReadyResource实体转换为ReadyResourceResponse
|
||||
func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceResponse {
|
||||
return dto.ReadyResourceResponse{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
URL: resource.URL,
|
||||
Category: resource.Category,
|
||||
Tags: resource.Tags,
|
||||
Img: resource.Img,
|
||||
Source: resource.Source,
|
||||
Extra: resource.Extra,
|
||||
CreateTime: resource.CreateTime,
|
||||
IP: resource.IP,
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
Category: resource.Category,
|
||||
Tags: resource.Tags,
|
||||
Img: resource.Img,
|
||||
Source: resource.Source,
|
||||
Extra: resource.Extra,
|
||||
Key: resource.Key,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
CreateTime: resource.CreateTime,
|
||||
IP: resource.IP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,41 +196,20 @@ func ToReadyResourceResponseList(resources []entity.ReadyResource) []dto.ReadyRe
|
||||
}
|
||||
|
||||
// RequestToReadyResource 将ReadyResourceRequest转换为ReadyResource实体
|
||||
func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
// func RequestToReadyResource(req *dto.ReadyResourceRequest) *entity.ReadyResource {
|
||||
// if req == nil {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
return &entity.ReadyResource{
|
||||
Title: &req.Title,
|
||||
Description: req.Description,
|
||||
URL: req.Url,
|
||||
Category: req.Category,
|
||||
Tags: req.Tags,
|
||||
Img: req.Img,
|
||||
Source: req.Source,
|
||||
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,
|
||||
}
|
||||
}
|
||||
// return &entity.ReadyResource{
|
||||
// Title: &req.Title,
|
||||
// Description: req.Description,
|
||||
// URL: req.Url,
|
||||
// Category: req.Category,
|
||||
// Tags: req.Tags,
|
||||
// Img: req.Img,
|
||||
// Source: req.Source,
|
||||
// Extra: req.Extra,
|
||||
// Key: req.Key,
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,74 +1,237 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// SystemConfigToResponse 将系统配置实体转换为响应DTO
|
||||
func SystemConfigToResponse(config *entity.SystemConfig) *dto.SystemConfigResponse {
|
||||
if config == nil {
|
||||
return nil
|
||||
// SystemConfigToResponse 将系统配置实体列表转换为响应DTO
|
||||
func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResponse {
|
||||
if len(configs) == 0 {
|
||||
return getDefaultConfigResponse()
|
||||
}
|
||||
|
||||
return &dto.SystemConfigResponse{
|
||||
ID: config.ID,
|
||||
CreatedAt: config.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: config.UpdatedAt.Format(time.RFC3339),
|
||||
response := getDefaultConfigResponse()
|
||||
|
||||
// SEO 配置
|
||||
SiteTitle: config.SiteTitle,
|
||||
SiteDescription: config.SiteDescription,
|
||||
Keywords: config.Keywords,
|
||||
Author: config.Author,
|
||||
Copyright: config.Copyright,
|
||||
|
||||
// 自动处理配置组
|
||||
AutoProcessReadyResources: config.AutoProcessReadyResources,
|
||||
AutoProcessInterval: config.AutoProcessInterval,
|
||||
AutoTransferEnabled: config.AutoTransferEnabled,
|
||||
AutoTransferLimitDays: config.AutoTransferLimitDays,
|
||||
AutoTransferMinSpace: config.AutoTransferMinSpace,
|
||||
AutoFetchHotDramaEnabled: config.AutoFetchHotDramaEnabled,
|
||||
|
||||
// API配置
|
||||
ApiToken: config.ApiToken,
|
||||
|
||||
// 其他配置
|
||||
PageSize: config.PageSize,
|
||||
MaintenanceMode: config.MaintenanceMode,
|
||||
// 将键值对转换为结构体
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeySiteTitle:
|
||||
response.SiteTitle = config.Value
|
||||
case entity.ConfigKeySiteDescription:
|
||||
response.SiteDescription = config.Value
|
||||
case entity.ConfigKeyKeywords:
|
||||
response.Keywords = config.Value
|
||||
case entity.ConfigKeyAuthor:
|
||||
response.Author = config.Value
|
||||
case entity.ConfigKeyCopyright:
|
||||
response.Copyright = config.Value
|
||||
case entity.ConfigKeyAutoProcessReadyResources:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.AutoProcessReadyResources = val
|
||||
}
|
||||
case entity.ConfigKeyAutoProcessInterval:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response.AutoProcessInterval = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.AutoTransferEnabled = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferLimitDays:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response.AutoTransferLimitDays = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferMinSpace:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response.AutoTransferMinSpace = val
|
||||
}
|
||||
case entity.ConfigKeyAutoFetchHotDramaEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.AutoFetchHotDramaEnabled = val
|
||||
}
|
||||
case entity.ConfigKeyApiToken:
|
||||
response.ApiToken = config.Value
|
||||
case entity.ConfigKeyForbiddenWords:
|
||||
response.ForbiddenWords = config.Value
|
||||
case entity.ConfigKeyPageSize:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response.PageSize = val
|
||||
}
|
||||
case entity.ConfigKeyMaintenanceMode:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.MaintenanceMode = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置时间戳(使用第一个配置的时间)
|
||||
if len(configs) > 0 {
|
||||
response.CreatedAt = configs[0].CreatedAt.Format(time.RFC3339)
|
||||
response.UpdatedAt = configs[0].UpdatedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// RequestToSystemConfig 将请求DTO转换为系统配置实体
|
||||
func RequestToSystemConfig(req *dto.SystemConfigRequest) *entity.SystemConfig {
|
||||
// RequestToSystemConfig 将请求DTO转换为系统配置实体列表
|
||||
func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entity.SystemConfig{
|
||||
// SEO 配置
|
||||
SiteTitle: req.SiteTitle,
|
||||
SiteDescription: req.SiteDescription,
|
||||
Keywords: req.Keywords,
|
||||
Author: req.Author,
|
||||
Copyright: req.Copyright,
|
||||
var configs []entity.SystemConfig
|
||||
|
||||
// 自动处理配置组
|
||||
AutoProcessReadyResources: req.AutoProcessReadyResources,
|
||||
AutoProcessInterval: req.AutoProcessInterval,
|
||||
AutoTransferEnabled: req.AutoTransferEnabled,
|
||||
AutoTransferLimitDays: req.AutoTransferLimitDays,
|
||||
AutoTransferMinSpace: req.AutoTransferMinSpace,
|
||||
AutoFetchHotDramaEnabled: req.AutoFetchHotDramaEnabled,
|
||||
// 只添加有值的字段
|
||||
if req.SiteTitle != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteTitle, Value: req.SiteTitle, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.SiteDescription != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeySiteDescription, Value: req.SiteDescription, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.Keywords != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyKeywords, Value: req.Keywords, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.Author != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAuthor, Value: req.Author, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.Copyright != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyCopyright, Value: req.Copyright, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.ApiToken != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyApiToken, Value: req.ApiToken, Type: entity.ConfigTypeString})
|
||||
}
|
||||
if req.ForbiddenWords != "" {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyForbiddenWords, Value: req.ForbiddenWords, Type: entity.ConfigTypeString})
|
||||
}
|
||||
|
||||
// API配置
|
||||
ApiToken: req.ApiToken,
|
||||
// 布尔值字段 - 只处理实际提交的字段
|
||||
// 注意:由于 Go 的零值机制,我们需要通过其他方式判断字段是否被提交
|
||||
// 这里暂时保持原样,但建议前端只提交有变化的字段
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessReadyResources, Value: strconv.FormatBool(req.AutoProcessReadyResources), Type: entity.ConfigTypeBool})
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferEnabled, Value: strconv.FormatBool(req.AutoTransferEnabled), Type: entity.ConfigTypeBool})
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: strconv.FormatBool(req.AutoFetchHotDramaEnabled), Type: entity.ConfigTypeBool})
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMaintenanceMode, Value: strconv.FormatBool(req.MaintenanceMode), Type: entity.ConfigTypeBool})
|
||||
|
||||
// 其他配置
|
||||
PageSize: req.PageSize,
|
||||
MaintenanceMode: req.MaintenanceMode,
|
||||
// 整数字段 - 只添加非零值
|
||||
if req.AutoProcessInterval != 0 {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoProcessInterval, Value: strconv.Itoa(req.AutoProcessInterval), Type: entity.ConfigTypeInt})
|
||||
}
|
||||
if req.AutoTransferLimitDays != 0 {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferLimitDays, Value: strconv.Itoa(req.AutoTransferLimitDays), Type: entity.ConfigTypeInt})
|
||||
}
|
||||
if req.AutoTransferMinSpace != 0 {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAutoTransferMinSpace, Value: strconv.Itoa(req.AutoTransferMinSpace), Type: entity.ConfigTypeInt})
|
||||
}
|
||||
if req.PageSize != 0 {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyPageSize, Value: strconv.Itoa(req.PageSize), Type: entity.ConfigTypeInt})
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
// SystemConfigToPublicResponse 返回不含 api_token 的系统配置响应
|
||||
func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]interface{} {
|
||||
response := map[string]interface{}{
|
||||
entity.ConfigResponseFieldID: 0,
|
||||
entity.ConfigResponseFieldCreatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldUpdatedAt: utils.GetCurrentTimeString(),
|
||||
entity.ConfigResponseFieldSiteTitle: entity.ConfigDefaultSiteTitle,
|
||||
entity.ConfigResponseFieldSiteDescription: entity.ConfigDefaultSiteDescription,
|
||||
entity.ConfigResponseFieldKeywords: entity.ConfigDefaultKeywords,
|
||||
entity.ConfigResponseFieldAuthor: entity.ConfigDefaultAuthor,
|
||||
entity.ConfigResponseFieldCopyright: entity.ConfigDefaultCopyright,
|
||||
entity.ConfigResponseFieldAutoProcessReadyResources: false,
|
||||
entity.ConfigResponseFieldAutoProcessInterval: 30,
|
||||
entity.ConfigResponseFieldAutoTransferEnabled: false,
|
||||
entity.ConfigResponseFieldAutoTransferLimitDays: 0,
|
||||
entity.ConfigResponseFieldAutoTransferMinSpace: 100,
|
||||
entity.ConfigResponseFieldAutoFetchHotDramaEnabled: false,
|
||||
entity.ConfigResponseFieldForbiddenWords: "",
|
||||
entity.ConfigResponseFieldPageSize: 100,
|
||||
entity.ConfigResponseFieldMaintenanceMode: false,
|
||||
}
|
||||
|
||||
// 将键值对转换为map
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeySiteTitle:
|
||||
response[entity.ConfigResponseFieldSiteTitle] = config.Value
|
||||
case entity.ConfigKeySiteDescription:
|
||||
response[entity.ConfigResponseFieldSiteDescription] = config.Value
|
||||
case entity.ConfigKeyKeywords:
|
||||
response[entity.ConfigResponseFieldKeywords] = config.Value
|
||||
case entity.ConfigKeyAuthor:
|
||||
response[entity.ConfigResponseFieldAuthor] = config.Value
|
||||
case entity.ConfigKeyCopyright:
|
||||
response[entity.ConfigResponseFieldCopyright] = config.Value
|
||||
case entity.ConfigKeyAutoProcessReadyResources:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoProcessReadyResources] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoProcessInterval:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoProcessInterval] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferLimitDays:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferLimitDays] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoTransferMinSpace:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoTransferMinSpace] = val
|
||||
}
|
||||
case entity.ConfigKeyAutoFetchHotDramaEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldAutoFetchHotDramaEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyForbiddenWords:
|
||||
response[entity.ConfigResponseFieldForbiddenWords] = config.Value
|
||||
case entity.ConfigKeyPageSize:
|
||||
if val, err := strconv.Atoi(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldPageSize] = val
|
||||
}
|
||||
case entity.ConfigKeyMaintenanceMode:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldMaintenanceMode] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置时间戳(使用第一个配置的时间)
|
||||
if len(configs) > 0 {
|
||||
response[entity.ConfigResponseFieldCreatedAt] = configs[0].CreatedAt.Format("2006-01-02 15:04:05")
|
||||
response[entity.ConfigResponseFieldUpdatedAt] = configs[0].UpdatedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// getDefaultConfigResponse 获取默认配置响应
|
||||
func getDefaultConfigResponse() *dto.SystemConfigResponse {
|
||||
return &dto.SystemConfigResponse{
|
||||
SiteTitle: entity.ConfigDefaultSiteTitle,
|
||||
SiteDescription: entity.ConfigDefaultSiteDescription,
|
||||
Keywords: entity.ConfigDefaultKeywords,
|
||||
Author: entity.ConfigDefaultAuthor,
|
||||
Copyright: entity.ConfigDefaultCopyright,
|
||||
AutoProcessReadyResources: false,
|
||||
AutoProcessInterval: 30,
|
||||
AutoTransferEnabled: false,
|
||||
AutoTransferLimitDays: 0,
|
||||
AutoTransferMinSpace: 100,
|
||||
AutoFetchHotDramaEnabled: false,
|
||||
ApiToken: entity.ConfigDefaultApiToken,
|
||||
ForbiddenWords: entity.ConfigDefaultForbiddenWords,
|
||||
PageSize: 100,
|
||||
MaintenanceMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package dto
|
||||
|
||||
// ReadyResourceRequest 待处理资源请求
|
||||
type ReadyResourceRequest struct {
|
||||
Title string `json:"title" validate:"required" example:"示例资源标题"`
|
||||
Description string `json:"description" example:"这是一个示例资源描述"`
|
||||
Url string `json:"url" validate:"required" example:"https://example.com/resource"`
|
||||
Category string `json:"category" example:"示例分类"`
|
||||
Tags string `json:"tags" example:"标签1,标签2"`
|
||||
Img string `json:"img" example:"https://example.com/image.jpg"`
|
||||
Source string `json:"source" example:"数据来源"`
|
||||
Extra string `json:"extra" example:"额外信息"`
|
||||
Title string `json:"title" validate:"required" example:"示例资源标题"`
|
||||
Description string `json:"description" example:"这是一个示例资源描述"`
|
||||
Url []string `json:"url" validate:"required" example:"https://example.com/resource"`
|
||||
Category string `json:"category" example:"示例分类"`
|
||||
Tags string `json:"tags" example:"标签1,标签2"`
|
||||
Img string `json:"img" example:"https://example.com/image.jpg"`
|
||||
Source string `json:"source" example:"数据来源"`
|
||||
Extra string `json:"extra" example:"额外信息"`
|
||||
ErrorMsg string `json:"error_msg" example:"错误信息"`
|
||||
}
|
||||
|
||||
// BatchReadyResourceRequest 批量待处理资源请求
|
||||
|
||||
@@ -108,15 +108,16 @@ type UpdateTagRequest struct {
|
||||
|
||||
// CreateReadyResourceRequest 创建待处理资源请求
|
||||
type CreateReadyResourceRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Category string `json:"category"`
|
||||
Tags string `json:"tags"`
|
||||
Img string `json:"img"`
|
||||
Source string `json:"source"`
|
||||
Extra string `json:"extra"`
|
||||
IP *string `json:"ip"`
|
||||
Title *string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL []string `json:"url" binding:"required"`
|
||||
Category string `json:"category"`
|
||||
Tags string `json:"tags"`
|
||||
Img string `json:"img"`
|
||||
Source string `json:"source"`
|
||||
Extra string `json:"extra"`
|
||||
IP *string `json:"ip"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// BatchCreateReadyResourceRequest 批量创建待处理资源请求
|
||||
|
||||
@@ -88,6 +88,8 @@ type ReadyResourceResponse struct {
|
||||
Img string `json:"img"`
|
||||
Source string `json:"source"`
|
||||
Extra string `json:"extra"`
|
||||
Key string `json:"key"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
IP *string `json:"ip"`
|
||||
}
|
||||
|
||||
@@ -3,25 +3,28 @@ package dto
|
||||
// SystemConfigRequest 系统配置请求
|
||||
type SystemConfigRequest struct {
|
||||
// SEO 配置
|
||||
SiteTitle string `json:"site_title" validate:"required"`
|
||||
SiteTitle string `json:"site_title"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
Keywords string `json:"keywords"`
|
||||
Author string `json:"author"`
|
||||
Copyright string `json:"copyright"`
|
||||
|
||||
// 自动处理配置组
|
||||
AutoProcessReadyResources bool `json:"auto_process_ready_resources"` // 自动处理待处理资源
|
||||
AutoProcessInterval int `json:"auto_process_interval" validate:"min=1,max=1440"` // 自动处理间隔(分钟)
|
||||
AutoTransferEnabled bool `json:"auto_transfer_enabled"` // 开启自动转存
|
||||
AutoTransferLimitDays int `json:"auto_transfer_limit_days" validate:"min=0,max=365"` // 自动转存限制天数(0表示不限制)
|
||||
AutoTransferMinSpace int `json:"auto_transfer_min_space" validate:"min=100,max=1024"` // 最小存储空间(GB)
|
||||
AutoFetchHotDramaEnabled bool `json:"auto_fetch_hot_drama_enabled"` // 自动拉取热播剧名字
|
||||
AutoProcessReadyResources bool `json:"auto_process_ready_resources"` // 自动处理待处理资源
|
||||
AutoProcessInterval int `json:"auto_process_interval"` // 自动处理间隔(分钟)
|
||||
AutoTransferEnabled bool `json:"auto_transfer_enabled"` // 开启自动转存
|
||||
AutoTransferLimitDays int `json:"auto_transfer_limit_days"` // 自动转存限制天数(0表示不限制)
|
||||
AutoTransferMinSpace int `json:"auto_transfer_min_space"` // 最小存储空间(GB)
|
||||
AutoFetchHotDramaEnabled bool `json:"auto_fetch_hot_drama_enabled"` // 自动拉取热播剧名字
|
||||
|
||||
// API配置
|
||||
ApiToken string `json:"api_token"` // 公开API访问令牌
|
||||
|
||||
// 违禁词配置
|
||||
ForbiddenWords string `json:"forbidden_words"` // 违禁词列表,用逗号分隔
|
||||
|
||||
// 其他配置
|
||||
PageSize int `json:"page_size" validate:"min=10,max=500"`
|
||||
PageSize int `json:"page_size"`
|
||||
MaintenanceMode bool `json:"maintenance_mode"`
|
||||
}
|
||||
|
||||
@@ -49,7 +52,22 @@ type SystemConfigResponse struct {
|
||||
// API配置
|
||||
ApiToken string `json:"api_token"` // 公开API访问令牌
|
||||
|
||||
// 违禁词配置
|
||||
ForbiddenWords string `json:"forbidden_words"` // 违禁词列表,用逗号分隔
|
||||
|
||||
// 其他配置
|
||||
PageSize int `json:"page_size"`
|
||||
MaintenanceMode bool `json:"maintenance_mode"`
|
||||
}
|
||||
|
||||
// SystemConfigItem 单个配置项
|
||||
type SystemConfigItem struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// SystemConfigListResponse 配置列表响应
|
||||
type SystemConfigListResponse struct {
|
||||
Configs []SystemConfigItem `json:"configs"`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ type ReadyResource struct {
|
||||
Img string `json:"img" gorm:"size:500;comment:封面链接"`
|
||||
Source string `json:"source" gorm:"size:100;comment:数据来源"`
|
||||
Extra string `json:"extra" gorm:"type:text;comment:额外附加数据"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"type:text;comment:处理失败时的错误信息"`
|
||||
CreateTime time.Time `json:"create_time" gorm:"default:CURRENT_TIMESTAMP"`
|
||||
IP *string `json:"ip" gorm:"size:45;comment:IP地址"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -27,6 +27,7 @@ type Resource struct {
|
||||
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"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
|
||||
// 关联关系
|
||||
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
|
||||
|
||||
@@ -4,33 +4,16 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// SystemConfig 系统配置实体
|
||||
// SystemConfig 系统配置实体(键值对形式)
|
||||
type SystemConfig struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// SEO 配置
|
||||
SiteTitle string `json:"site_title" gorm:"size:200;not null;default:'老九网盘资源数据库'"`
|
||||
SiteDescription string `json:"site_description" gorm:"size:500"`
|
||||
Keywords string `json:"keywords" gorm:"size:500"`
|
||||
Author string `json:"author" gorm:"size:100"`
|
||||
Copyright string `json:"copyright" gorm:"size:200"`
|
||||
|
||||
// 自动处理配置组
|
||||
AutoProcessReadyResources bool `json:"auto_process_ready_resources" gorm:"default:false"` // 自动处理待处理资源
|
||||
AutoProcessInterval int `json:"auto_process_interval" gorm:"default:30"` // 自动处理间隔(分钟)
|
||||
AutoTransferEnabled bool `json:"auto_transfer_enabled" gorm:"default:false"` // 开启自动转存
|
||||
AutoTransferLimitDays int `json:"auto_transfer_limit_days" gorm:"default:0"` // 自动转存限制天数(0表示不限制)
|
||||
AutoTransferMinSpace int `json:"auto_transfer_min_space" gorm:"default:100"` // 最小存储空间(GB)
|
||||
AutoFetchHotDramaEnabled bool `json:"auto_fetch_hot_drama_enabled" gorm:"default:false"` // 自动拉取热播剧名字
|
||||
|
||||
// API配置
|
||||
ApiToken string `json:"api_token" gorm:"size:100;uniqueIndex"` // 公开API访问令牌
|
||||
|
||||
// 其他配置
|
||||
PageSize int `json:"page_size" gorm:"default:100"`
|
||||
MaintenanceMode bool `json:"maintenance_mode" gorm:"default:false"`
|
||||
// 键值对配置
|
||||
Key string `json:"key" gorm:"size:100;not null;unique;comment:配置键"`
|
||||
Value string `json:"value" gorm:"size:1000"`
|
||||
Type string `json:"type" gorm:"size:20;default:'string'"` // string, int, bool, json
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
98
db/entity/system_config_constants.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package entity
|
||||
|
||||
// ConfigKey 配置键常量
|
||||
const (
|
||||
// SEO 配置
|
||||
ConfigKeySiteTitle = "site_title"
|
||||
ConfigKeySiteDescription = "site_description"
|
||||
ConfigKeyKeywords = "keywords"
|
||||
ConfigKeyAuthor = "author"
|
||||
ConfigKeyCopyright = "copyright"
|
||||
|
||||
// 自动处理配置组
|
||||
ConfigKeyAutoProcessReadyResources = "auto_process_ready_resources"
|
||||
ConfigKeyAutoProcessInterval = "auto_process_interval"
|
||||
ConfigKeyAutoTransferEnabled = "auto_transfer_enabled"
|
||||
ConfigKeyAutoTransferLimitDays = "auto_transfer_limit_days"
|
||||
ConfigKeyAutoTransferMinSpace = "auto_transfer_min_space"
|
||||
ConfigKeyAutoFetchHotDramaEnabled = "auto_fetch_hot_drama_enabled"
|
||||
|
||||
// API配置
|
||||
ConfigKeyApiToken = "api_token"
|
||||
|
||||
// 违禁词配置
|
||||
ConfigKeyForbiddenWords = "forbidden_words"
|
||||
|
||||
// 其他配置
|
||||
ConfigKeyPageSize = "page_size"
|
||||
ConfigKeyMaintenanceMode = "maintenance_mode"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
const (
|
||||
ConfigTypeString = "string"
|
||||
ConfigTypeInt = "int"
|
||||
ConfigTypeBool = "bool"
|
||||
ConfigTypeJSON = "json"
|
||||
)
|
||||
|
||||
// ConfigResponseField API响应字段名常量
|
||||
const (
|
||||
// 基础字段
|
||||
ConfigResponseFieldID = "id"
|
||||
ConfigResponseFieldCreatedAt = "created_at"
|
||||
ConfigResponseFieldUpdatedAt = "updated_at"
|
||||
|
||||
// SEO 配置字段
|
||||
ConfigResponseFieldSiteTitle = "site_title"
|
||||
ConfigResponseFieldSiteDescription = "site_description"
|
||||
ConfigResponseFieldKeywords = "keywords"
|
||||
ConfigResponseFieldAuthor = "author"
|
||||
ConfigResponseFieldCopyright = "copyright"
|
||||
|
||||
// 自动处理配置字段
|
||||
ConfigResponseFieldAutoProcessReadyResources = "auto_process_ready_resources"
|
||||
ConfigResponseFieldAutoProcessInterval = "auto_process_interval"
|
||||
ConfigResponseFieldAutoTransferEnabled = "auto_transfer_enabled"
|
||||
ConfigResponseFieldAutoTransferLimitDays = "auto_transfer_limit_days"
|
||||
ConfigResponseFieldAutoTransferMinSpace = "auto_transfer_min_space"
|
||||
ConfigResponseFieldAutoFetchHotDramaEnabled = "auto_fetch_hot_drama_enabled"
|
||||
|
||||
// API配置字段
|
||||
ConfigResponseFieldApiToken = "api_token"
|
||||
|
||||
// 违禁词配置字段
|
||||
ConfigResponseFieldForbiddenWords = "forbidden_words"
|
||||
|
||||
// 其他配置字段
|
||||
ConfigResponseFieldPageSize = "page_size"
|
||||
ConfigResponseFieldMaintenanceMode = "maintenance_mode"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
const (
|
||||
// SEO 配置默认值
|
||||
ConfigDefaultSiteTitle = "老九网盘资源数据库"
|
||||
ConfigDefaultSiteDescription = "专业的老九网盘资源数据库"
|
||||
ConfigDefaultKeywords = "网盘,资源管理,文件分享"
|
||||
ConfigDefaultAuthor = "系统管理员"
|
||||
ConfigDefaultCopyright = "© 2024 老九网盘资源数据库"
|
||||
|
||||
// 自动处理配置默认值
|
||||
ConfigDefaultAutoProcessReadyResources = "false"
|
||||
ConfigDefaultAutoProcessInterval = "30"
|
||||
ConfigDefaultAutoTransferEnabled = "false"
|
||||
ConfigDefaultAutoTransferLimitDays = "0"
|
||||
ConfigDefaultAutoTransferMinSpace = "100"
|
||||
ConfigDefaultAutoFetchHotDramaEnabled = "false"
|
||||
|
||||
// API配置默认值
|
||||
ConfigDefaultApiToken = ""
|
||||
|
||||
// 违禁词配置默认值
|
||||
ConfigDefaultForbiddenWords = ""
|
||||
|
||||
// 其他配置默认值
|
||||
ConfigDefaultPageSize = "100"
|
||||
ConfigDefaultMaintenanceMode = "false"
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type CategoryRepository interface {
|
||||
BaseRepository[entity.Category]
|
||||
FindByName(name string) (*entity.Category, error)
|
||||
FindByNameIncludingDeleted(name string) (*entity.Category, error)
|
||||
FindWithResources() ([]entity.Category, error)
|
||||
FindWithTags() ([]entity.Category, error)
|
||||
GetResourceCount(categoryID uint) (int64, error)
|
||||
@@ -17,6 +18,7 @@ type CategoryRepository interface {
|
||||
GetTagNames(categoryID uint) ([]string, error)
|
||||
FindWithPagination(page, pageSize int) ([]entity.Category, int64, error)
|
||||
Search(query string, page, pageSize int) ([]entity.Category, int64, error)
|
||||
RestoreDeletedCategory(id uint) error
|
||||
}
|
||||
|
||||
// CategoryRepositoryImpl Category的Repository实现
|
||||
@@ -41,6 +43,21 @@ func (r *CategoryRepositoryImpl) FindByName(name string) (*entity.Category, erro
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// FindByNameIncludingDeleted 根据名称查找(包括已删除的记录)
|
||||
func (r *CategoryRepositoryImpl) FindByNameIncludingDeleted(name string) (*entity.Category, error) {
|
||||
var category entity.Category
|
||||
err := r.db.Unscoped().Where("name = ?", name).First(&category).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &category, nil
|
||||
}
|
||||
|
||||
// RestoreDeletedCategory 恢复已删除的分类
|
||||
func (r *CategoryRepositoryImpl) RestoreDeletedCategory(id uint) error {
|
||||
return r.db.Unscoped().Model(&entity.Category{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// FindWithResources 查找包含资源的分类
|
||||
func (r *CategoryRepositoryImpl) FindWithResources() ([]entity.Category, error) {
|
||||
var categories []entity.Category
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -13,10 +14,17 @@ type ReadyResourceRepository interface {
|
||||
BaseRepository[entity.ReadyResource]
|
||||
FindByURL(url string) (*entity.ReadyResource, error)
|
||||
FindByIP(ip string) ([]entity.ReadyResource, error)
|
||||
FindByKey(key string) ([]entity.ReadyResource, error)
|
||||
BatchCreate(resources []entity.ReadyResource) error
|
||||
DeleteByURL(url string) error
|
||||
DeleteByKey(key string) error
|
||||
FindAllWithinDays(days int) ([]entity.ReadyResource, error)
|
||||
BatchFindByURLs(urls []string) ([]entity.ReadyResource, error)
|
||||
GenerateUniqueKey() (string, error)
|
||||
FindWithErrors() ([]entity.ReadyResource, error)
|
||||
FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error)
|
||||
FindWithoutErrors() ([]entity.ReadyResource, error)
|
||||
ClearErrorMsg(id uint) error
|
||||
}
|
||||
|
||||
// ReadyResourceRepositoryImpl ReadyResource的Repository实现
|
||||
@@ -78,3 +86,71 @@ func (r *ReadyResourceRepositoryImpl) BatchFindByURLs(urls []string) ([]entity.R
|
||||
err := r.db.Where("url IN ?", urls).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// FindByKey 根据Key查找
|
||||
func (r *ReadyResourceRepositoryImpl) FindByKey(key string) ([]entity.ReadyResource, error) {
|
||||
var resources []entity.ReadyResource
|
||||
err := r.db.Where("key = ?", key).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// DeleteByKey 根据Key删除
|
||||
func (r *ReadyResourceRepositoryImpl) DeleteByKey(key string) error {
|
||||
return r.db.Where("key = ?", key).Delete(&entity.ReadyResource{}).Error
|
||||
}
|
||||
|
||||
// GenerateUniqueKey 生成唯一的6位Base62 key
|
||||
func (r *ReadyResourceRepositoryImpl) GenerateUniqueKey() (string, error) {
|
||||
for i := 0; i < 20; i++ {
|
||||
key, err := gonanoid.Generate("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 6)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var count int64
|
||||
err = r.db.Model(&entity.ReadyResource{}).Where("key = ?", key).Count(&count).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if count == 0 {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return "", gorm.ErrInvalidData
|
||||
}
|
||||
|
||||
// FindWithErrors 查找有错误信息的资源(deleted_at为空且存在error_msg)
|
||||
func (r *ReadyResourceRepositoryImpl) FindWithErrors() ([]entity.ReadyResource, error) {
|
||||
var resources []entity.ReadyResource
|
||||
err := r.db.Where("deleted_at IS NULL AND error_msg != '' AND error_msg IS NOT NULL").Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// FindWithErrorsPaginated 分页查找有错误信息的资源(deleted_at为空且存在error_msg)
|
||||
func (r *ReadyResourceRepositoryImpl) FindWithErrorsPaginated(page, limit int) ([]entity.ReadyResource, int64, error) {
|
||||
var resources []entity.ReadyResource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
db := r.db.Model(&entity.ReadyResource{}).Where("deleted_at IS NULL AND error_msg != '' AND error_msg IS NOT NULL")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// FindWithoutErrors 查找没有错误信息的资源
|
||||
func (r *ReadyResourceRepositoryImpl) FindWithoutErrors() ([]entity.ReadyResource, error) {
|
||||
var resources []entity.ReadyResource
|
||||
err := r.db.Where("error_msg = '' OR error_msg IS NULL").Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// ClearErrorMsg 清除指定资源的错误信息
|
||||
func (r *ReadyResourceRepositoryImpl) ClearErrorMsg(id uint) error {
|
||||
return r.db.Model(&entity.ReadyResource{}).Where("id = ?", id).Update("error_msg", "").Error
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type ResourceRepository interface {
|
||||
FindExists(url string, excludeID ...uint) (bool, error)
|
||||
BatchFindByURLs(urls []string) ([]entity.Resource, error)
|
||||
GetResourcesForTransfer(panID uint, sinceTime time.Time) ([]*entity.Resource, error)
|
||||
CreateResourceTag(resourceID, tagID uint) error
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -216,36 +217,57 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
// 处理参数
|
||||
for key, value := range params {
|
||||
switch key {
|
||||
case "query":
|
||||
case "search": // 添加search参数支持
|
||||
if query, ok := value.(string); ok && query != "" {
|
||||
db = db.Where("title ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%")
|
||||
}
|
||||
case "category_id":
|
||||
if categoryID, ok := value.(uint); ok {
|
||||
db = db.Where("category_id = ?", categoryID)
|
||||
case "category": // 添加category参数支持(字符串形式)
|
||||
if category, ok := value.(string); ok && category != "" {
|
||||
// 根据分类名称查找分类ID
|
||||
var categoryEntity entity.Category
|
||||
if err := r.db.Where("name ILIKE ?", "%"+category+"%").First(&categoryEntity).Error; err == nil {
|
||||
db = db.Where("category_id = ?", categoryEntity.ID)
|
||||
}
|
||||
}
|
||||
case "is_valid":
|
||||
if isValid, ok := value.(bool); ok {
|
||||
db = db.Where("is_valid = ?", isValid)
|
||||
}
|
||||
case "is_public":
|
||||
if isPublic, ok := value.(bool); ok {
|
||||
db = db.Where("is_public = ?", isPublic)
|
||||
}
|
||||
case "pan_id":
|
||||
if panID, ok := value.(uint); ok {
|
||||
db = db.Where("pan_id = ?", panID)
|
||||
case "tag": // 添加tag参数支持
|
||||
if tag, ok := value.(string); ok && tag != "" {
|
||||
// 根据标签名称查找相关资源
|
||||
var tagEntity entity.Tag
|
||||
if err := r.db.Where("name ILIKE ?", "%"+tag+"%").First(&tagEntity).Error; err == nil {
|
||||
// 通过中间表查找包含该标签的资源
|
||||
db = db.Joins("JOIN resource_tags ON resources.id = resource_tags.resource_id").
|
||||
Where("resource_tags.tag_id = ?", tagEntity.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db = db.Where("is_valid = true and is_public = true")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 处理分页参数
|
||||
page := 1
|
||||
pageSize := 20
|
||||
|
||||
if pageVal, ok := params["page"].(int); ok && pageVal > 0 {
|
||||
page = pageVal
|
||||
}
|
||||
if pageSizeVal, ok := params["page_size"].(int); ok && pageSizeVal > 0 {
|
||||
pageSize = pageSizeVal
|
||||
// 限制最大page_size为100
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
}
|
||||
|
||||
// 计算偏移量
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取分页数据,按更新时间倒序
|
||||
err := db.Order("updated_at DESC").Find(&resources).Error
|
||||
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
@@ -333,7 +355,7 @@ func (r *ResourceRepositoryImpl) InvalidateCache() error {
|
||||
// FindExists 检查是否存在相同URL的资源
|
||||
func (r *ResourceRepositoryImpl) FindExists(url string, excludeID ...uint) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.Model(&entity.Resource{}).Where("url = ?", url)
|
||||
query := r.db.Model(&entity.Resource{}).Where("url = ? OR save_url = ?", url, url)
|
||||
|
||||
// 如果有排除ID,则排除该记录(用于更新时排除自己)
|
||||
if len(excludeID) > 0 {
|
||||
@@ -369,3 +391,12 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// CreateResourceTag 创建资源与标签的关联
|
||||
func (r *ResourceRepositoryImpl) CreateResourceTag(resourceID, tagID uint) error {
|
||||
resourceTag := &entity.ResourceTag{
|
||||
ResourceID: resourceID,
|
||||
TagID: tagID,
|
||||
}
|
||||
return r.GetDB().Create(resourceTag).Error
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -9,72 +12,231 @@ import (
|
||||
// SystemConfigRepository 系统配置Repository接口
|
||||
type SystemConfigRepository interface {
|
||||
BaseRepository[entity.SystemConfig]
|
||||
FindFirst() (*entity.SystemConfig, error)
|
||||
GetOrCreateDefault() (*entity.SystemConfig, error)
|
||||
Upsert(config *entity.SystemConfig) error
|
||||
FindAll() ([]entity.SystemConfig, error)
|
||||
FindByKey(key string) (*entity.SystemConfig, error)
|
||||
GetOrCreateDefault() ([]entity.SystemConfig, error)
|
||||
UpsertConfigs(configs []entity.SystemConfig) error
|
||||
GetConfigValue(key string) (string, error)
|
||||
GetConfigBool(key string) (bool, error)
|
||||
GetConfigInt(key string) (int, error)
|
||||
GetCachedConfigs() map[string]string
|
||||
ClearConfigCache()
|
||||
}
|
||||
|
||||
// SystemConfigRepositoryImpl 系统配置Repository实现
|
||||
type SystemConfigRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.SystemConfig]
|
||||
|
||||
// 配置缓存
|
||||
configCache map[string]string // key -> value
|
||||
configCacheOnce sync.Once
|
||||
configCacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewSystemConfigRepository 创建系统配置Repository
|
||||
func NewSystemConfigRepository(db *gorm.DB) SystemConfigRepository {
|
||||
return &SystemConfigRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.SystemConfig]{db: db},
|
||||
configCache: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// FindFirst 获取第一个配置(通常只有一个配置)
|
||||
func (r *SystemConfigRepositoryImpl) FindFirst() (*entity.SystemConfig, error) {
|
||||
// FindAll 获取所有配置
|
||||
func (r *SystemConfigRepositoryImpl) FindAll() ([]entity.SystemConfig, error) {
|
||||
var configs []entity.SystemConfig
|
||||
err := r.db.Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// FindByKey 根据键查找配置
|
||||
func (r *SystemConfigRepositoryImpl) FindByKey(key string) (*entity.SystemConfig, error) {
|
||||
var config entity.SystemConfig
|
||||
err := r.db.First(&config).Error
|
||||
err := r.db.Where("key = ?", key).First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Upsert 创建或更新系统配置
|
||||
func (r *SystemConfigRepositoryImpl) Upsert(config *entity.SystemConfig) error {
|
||||
var existingConfig entity.SystemConfig
|
||||
err := r.db.First(&existingConfig).Error
|
||||
// UpsertConfigs 批量创建或更新配置
|
||||
func (r *SystemConfigRepositoryImpl) UpsertConfigs(configs []entity.SystemConfig) error {
|
||||
for _, config := range configs {
|
||||
var existingConfig entity.SystemConfig
|
||||
err := r.db.Where("key = ?", config.Key).First(&existingConfig).Error
|
||||
|
||||
if err != nil {
|
||||
// 如果不存在,则创建
|
||||
return r.db.Create(config).Error
|
||||
} else {
|
||||
// 如果存在,则更新
|
||||
config.ID = existingConfig.ID
|
||||
return r.db.Save(config).Error
|
||||
if err != nil {
|
||||
// 如果不存在,则创建
|
||||
if err := r.db.Create(&config).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 如果存在,则更新
|
||||
config.ID = existingConfig.ID
|
||||
if err := r.db.Save(&config).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置后刷新缓存
|
||||
r.refreshConfigCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrCreateDefault 获取配置或创建默认配置
|
||||
func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() (*entity.SystemConfig, error) {
|
||||
config, err := r.FindFirst()
|
||||
func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig, error) {
|
||||
configs, err := r.FindAll()
|
||||
if err != nil {
|
||||
// 创建默认配置
|
||||
defaultConfig := &entity.SystemConfig{
|
||||
SiteTitle: "老九网盘资源数据库",
|
||||
SiteDescription: "专业的老九网盘资源数据库",
|
||||
Keywords: "网盘,资源管理,文件分享",
|
||||
Author: "系统管理员",
|
||||
Copyright: "© 2024 老九网盘资源数据库",
|
||||
AutoProcessReadyResources: false,
|
||||
AutoProcessInterval: 30,
|
||||
PageSize: 100,
|
||||
MaintenanceMode: false,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果没有配置,创建默认配置
|
||||
if len(configs) == 0 {
|
||||
defaultConfigs := []entity.SystemConfig{
|
||||
{Key: entity.ConfigKeySiteTitle, Value: entity.ConfigDefaultSiteTitle, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeySiteDescription, Value: entity.ConfigDefaultSiteDescription, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyKeywords, Value: entity.ConfigDefaultKeywords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAuthor, Value: entity.ConfigDefaultAuthor, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyCopyright, Value: entity.ConfigDefaultCopyright, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAutoProcessReadyResources, Value: entity.ConfigDefaultAutoProcessReadyResources, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAutoProcessInterval, Value: entity.ConfigDefaultAutoProcessInterval, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoTransferEnabled, Value: entity.ConfigDefaultAutoTransferEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAutoTransferLimitDays, Value: entity.ConfigDefaultAutoTransferLimitDays, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
}
|
||||
|
||||
err = r.db.Create(defaultConfig).Error
|
||||
err = r.UpsertConfigs(defaultConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return defaultConfig, nil
|
||||
return defaultConfigs, nil
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// initConfigCache 初始化配置缓存
|
||||
func (r *SystemConfigRepositoryImpl) initConfigCache() {
|
||||
r.configCacheOnce.Do(func() {
|
||||
// 获取所有配置
|
||||
configs, err := r.FindAll()
|
||||
if err != nil {
|
||||
// 如果获取失败,尝试创建默认配置
|
||||
configs, err = r.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化缓存
|
||||
r.configCacheMutex.Lock()
|
||||
defer r.configCacheMutex.Unlock()
|
||||
|
||||
for _, config := range configs {
|
||||
r.configCache[config.Key] = config.Value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// refreshConfigCache 刷新配置缓存
|
||||
func (r *SystemConfigRepositoryImpl) refreshConfigCache() {
|
||||
// 重置Once,允许重新初始化
|
||||
r.configCacheOnce = sync.Once{}
|
||||
|
||||
// 清空缓存
|
||||
r.configCacheMutex.Lock()
|
||||
r.configCache = make(map[string]string)
|
||||
r.configCacheMutex.Unlock()
|
||||
|
||||
// 重新初始化缓存
|
||||
r.initConfigCache()
|
||||
}
|
||||
|
||||
// GetConfigValue 获取配置值(字符串)
|
||||
func (r *SystemConfigRepositoryImpl) GetConfigValue(key string) (string, error) {
|
||||
// 初始化缓存
|
||||
r.initConfigCache()
|
||||
|
||||
// 从缓存中读取
|
||||
r.configCacheMutex.RLock()
|
||||
value, exists := r.configCache[key]
|
||||
r.configCacheMutex.RUnlock()
|
||||
|
||||
if exists {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// 如果缓存中没有,尝试从数据库获取(可能是新添加的配置)
|
||||
config, err := r.FindByKey(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
r.configCacheMutex.Lock()
|
||||
r.configCache[key] = config.Value
|
||||
r.configCacheMutex.Unlock()
|
||||
|
||||
return config.Value, nil
|
||||
}
|
||||
|
||||
// GetConfigBool 获取配置值(布尔)
|
||||
func (r *SystemConfigRepositoryImpl) GetConfigBool(key string) (bool, error) {
|
||||
value, err := r.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch value {
|
||||
case "true", "1", "yes":
|
||||
return true, nil
|
||||
case "false", "0", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigInt 获取配置值(整数)
|
||||
func (r *SystemConfigRepositoryImpl) GetConfigInt(key string) (int, error) {
|
||||
value, err := r.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 这里需要导入 strconv 包,但为了避免循环导入,我们使用简单的转换
|
||||
var result int
|
||||
_, err = fmt.Sscanf(value, "%d", &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetCachedConfigs 获取所有缓存的配置(用于调试)
|
||||
func (r *SystemConfigRepositoryImpl) GetCachedConfigs() map[string]string {
|
||||
r.initConfigCache()
|
||||
|
||||
r.configCacheMutex.RLock()
|
||||
defer r.configCacheMutex.RUnlock()
|
||||
|
||||
// 返回缓存的副本
|
||||
result := make(map[string]string)
|
||||
for k, v := range r.configCache {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearConfigCache 清空配置缓存(用于测试或手动刷新)
|
||||
func (r *SystemConfigRepositoryImpl) ClearConfigCache() {
|
||||
r.configCacheMutex.Lock()
|
||||
r.configCache = make(map[string]string)
|
||||
r.configCacheMutex.Unlock()
|
||||
|
||||
// 重置Once,允许重新初始化
|
||||
r.configCacheOnce = sync.Once{}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type TagRepository interface {
|
||||
BaseRepository[entity.Tag]
|
||||
FindByName(name string) (*entity.Tag, error)
|
||||
FindByNameIncludingDeleted(name string) (*entity.Tag, error)
|
||||
FindWithResources() ([]entity.Tag, error)
|
||||
FindByCategoryID(categoryID uint) ([]entity.Tag, error)
|
||||
FindByCategoryIDPaginated(categoryID uint, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
@@ -18,6 +19,8 @@ type TagRepository interface {
|
||||
FindWithPagination(page, pageSize int) ([]entity.Tag, int64, error)
|
||||
Search(query string, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
UpdateWithNulls(tag *entity.Tag) error
|
||||
GetByID(id uint) (*entity.Tag, error)
|
||||
RestoreDeletedTag(id uint) error
|
||||
}
|
||||
|
||||
// TagRepositoryImpl Tag的Repository实现
|
||||
@@ -42,6 +45,16 @@ func (r *TagRepositoryImpl) FindByName(name string) (*entity.Tag, error) {
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// FindByNameIncludingDeleted 根据名称查找(包括已删除的记录)
|
||||
func (r *TagRepositoryImpl) FindByNameIncludingDeleted(name string) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.Unscoped().Where("name = ?", name).First(&tag).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// FindWithResources 查找包含资源的标签
|
||||
func (r *TagRepositoryImpl) FindWithResources() ([]entity.Tag, error) {
|
||||
var tags []entity.Tag
|
||||
@@ -144,3 +157,18 @@ func (r *TagRepositoryImpl) UpdateWithNulls(tag *entity.Tag) error {
|
||||
// 使用Select方法明确指定要更新的字段,包括null值
|
||||
return r.db.Model(tag).Select("name", "description", "category_id", "updated_at").Updates(tag).Error
|
||||
}
|
||||
|
||||
// GetByID 通过ID查找标签
|
||||
func (r *TagRepositoryImpl) GetByID(id uint) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
err := r.db.First(&tag, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// RestoreDeletedTag 恢复已删除的标签
|
||||
func (r *TagRepositoryImpl) RestoreDeletedTag(id uint) error {
|
||||
return r.db.Unscoped().Model(&entity.Tag{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
image: ctwj/urldb-backend:1.0.8
|
||||
image: ctwj/urldb-backend:1.0.10
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
@@ -28,6 +28,7 @@ services:
|
||||
DB_PASSWORD: password
|
||||
DB_NAME: url_db
|
||||
PORT: 8080
|
||||
TIMEZONE: Asia/Shanghai
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -37,7 +38,7 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
image: ctwj/urldb-frontend:1.0.8
|
||||
image: ctwj/urldb-frontend:1.0.10
|
||||
environment:
|
||||
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
|
||||
NUXT_PUBLIC_API_CLIENT: /api
|
||||
|
||||
@@ -1 +1 @@
|
||||
p.l9.lc
|
||||
doc.l9.lc
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
* [🏠 首页](/)
|
||||
* [🚀 快速开始](guide/quick-start.md)
|
||||
* [⚙️ 系统配置](guide/configuration.md)
|
||||
* [🐳 Docker部署](guide/docker-deployment.md)
|
||||
* [💻 本地开发](guide/local-development.md)
|
||||
|
||||
* 📚 API 文档
|
||||
* [公开API](api/overview.md)
|
||||
|
||||
* 📖 使用指南
|
||||
* [配置多账号](usage/user-account.md)
|
||||
* [配置自动处理资源](usage/resource-auto.md)
|
||||
* [配置自动转存分享](usage/save-auto.md)
|
||||
|
||||
* 📄 其他
|
||||
* [常见问题](faq.md)
|
||||
* [更新日志](changelog.md)
|
||||
* [许可证](license.md)
|
||||
* [许可证](license.md)
|
||||
* [版本管理](github-version-management.md)
|
||||
@@ -1,10 +1,14 @@
|
||||
// docsify 配置文件
|
||||
window.$docsify = {
|
||||
name: 'URL数据库管理系统',
|
||||
name: '老九网盘链接数据库',
|
||||
repo: 'https://github.com/ctwj/urldb',
|
||||
loadSidebar: true,
|
||||
loadSidebar: '_sidebar.md',
|
||||
subMaxLevel: 3,
|
||||
auto2top: true,
|
||||
// 添加侧边栏配置
|
||||
sidebarDisplayLevel: 1,
|
||||
// 添加错误处理
|
||||
notFoundPage: true,
|
||||
search: {
|
||||
maxAge: 86400000,
|
||||
paths: 'auto',
|
||||
@@ -34,6 +38,16 @@ window.$docsify = {
|
||||
'最后更新: ' + new Date().toLocaleDateString('zh-CN') +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
// 添加侧边栏加载调试
|
||||
hook.doneEach(function() {
|
||||
console.log('Docsify loaded, sidebar should be visible');
|
||||
if (document.querySelector('.sidebar-nav')) {
|
||||
console.log('Sidebar element found');
|
||||
} else {
|
||||
console.log('Sidebar element not found');
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -12,8 +12,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./docsify.config.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||
<script src="docsify.config.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/copy-code.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/pagination.min.js"></script>
|
||||
|
||||
@@ -7,6 +7,10 @@ DB_NAME=url_db
|
||||
|
||||
# 服务器配置
|
||||
PORT=8080
|
||||
GIN_MODE=release
|
||||
|
||||
# 时区配置
|
||||
TIMEZONE=Asia/Shanghai
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_DIR=./uploads
|
||||
|
||||
BIN
github/account.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
github/admin.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
github/config.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
github/index.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
github/search.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
1
go.mod
@@ -35,6 +35,7 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
|
||||
2
go.sum
@@ -76,6 +76,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -18,6 +19,8 @@ func GetCategories(c *gin.Context) {
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
search := c.Query("search")
|
||||
|
||||
utils.Debug("获取分类列表 - 分页参数: page=%d, pageSize=%d, search=%s", page, pageSize, search)
|
||||
|
||||
var categories []entity.Category
|
||||
var total int64
|
||||
var err error
|
||||
@@ -35,6 +38,8 @@ func GetCategories(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("查询到分类数量: %d, 总数: %d", len(categories), total)
|
||||
|
||||
// 获取每个分类的资源数量和标签名称
|
||||
resourceCounts := make(map[uint]int64)
|
||||
tagNamesMap := make(map[uint][]string)
|
||||
@@ -73,12 +78,50 @@ func CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首先检查是否存在已删除的同名分类
|
||||
deletedCategory, err := repoManager.CategoryRepository.FindByNameIncludingDeleted(req.Name)
|
||||
if err == nil && deletedCategory.DeletedAt.Valid {
|
||||
utils.Debug("找到已删除的分类: ID=%d, Name=%s", deletedCategory.ID, deletedCategory.Name)
|
||||
|
||||
// 如果存在已删除的同名分类,则恢复它
|
||||
err = repoManager.CategoryRepository.RestoreDeletedCategory(deletedCategory.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "恢复已删除分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("分类恢复成功: ID=%d", deletedCategory.ID)
|
||||
|
||||
// 重新获取恢复后的分类
|
||||
restoredCategory, err := repoManager.CategoryRepository.FindByID(deletedCategory.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取恢复的分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("重新获取到恢复的分类: ID=%d, Name=%s", restoredCategory.ID, restoredCategory.Name)
|
||||
|
||||
// 更新分类信息
|
||||
restoredCategory.Description = req.Description
|
||||
err = repoManager.CategoryRepository.Update(restoredCategory)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "更新恢复的分类失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
utils.Debug("分类信息更新成功: ID=%d, Description=%s", restoredCategory.ID, restoredCategory.Description)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "分类恢复成功",
|
||||
"category": converter.ToCategoryResponse(restoredCategory, 0, []string{}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不存在已删除的同名分类,则创建新分类
|
||||
category := &entity.Category{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
err := repoManager.CategoryRepository.Create(category)
|
||||
err = repoManager.CategoryRepository.Create(category)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -324,7 +324,7 @@ func RefreshCapacity(c *gin.Context) {
|
||||
cks.UsedSpace = userInfo.UsedSpace
|
||||
cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性
|
||||
|
||||
err = repoManager.CksRepository.Update(cks)
|
||||
err = repoManager.CksRepository.UpdateWithAllFields(cks)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -3,8 +3,8 @@ package handlers
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -17,59 +17,6 @@ func NewPublicAPIHandler() *PublicAPIHandler {
|
||||
return &PublicAPIHandler{}
|
||||
}
|
||||
|
||||
// AddSingleResource godoc
|
||||
// @Summary 单个添加资源
|
||||
// @Description 通过公开API添加单个资源到待处理列表
|
||||
// @Tags PublicAPI
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-API-Token header string true "API访问令牌"
|
||||
// @Param data body dto.ReadyResourceRequest true "资源信息"
|
||||
// @Success 200 {object} map[string]interface{} "添加成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/resources/add [post]
|
||||
func (h *PublicAPIHandler) AddSingleResource(c *gin.Context) {
|
||||
var req dto.ReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Title == "" {
|
||||
ErrorResponse(c, "标题不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Url == "" {
|
||||
ErrorResponse(c, "URL不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
readyResource := converter.RequestToReadyResource(&req)
|
||||
if readyResource == nil {
|
||||
ErrorResponse(c, "数据转换失败", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置来源
|
||||
readyResource.Source = "公开API"
|
||||
|
||||
// 保存到数据库
|
||||
err := repoManager.ReadyResourceRepository.Create(readyResource)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "添加资源失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": readyResource.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// AddBatchResources godoc
|
||||
// @Summary 批量添加资源
|
||||
// @Description 通过公开API批量添加多个资源到待处理列表
|
||||
@@ -95,26 +42,62 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个资源
|
||||
for i, resource := range req.Resources {
|
||||
if resource.Title == "" {
|
||||
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源标题不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if resource.Url == "" {
|
||||
ErrorResponse(c, "第"+strconv.Itoa(i+1)+"个资源URL不能为空", 400)
|
||||
return
|
||||
// 收集所有待提交的URL,去重
|
||||
urlSet := make(map[string]struct{})
|
||||
for _, resource := range req.Resources {
|
||||
for _, u := range resource.Url {
|
||||
if u != "" {
|
||||
urlSet[u] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
uniqueUrls := make([]string, 0, len(urlSet))
|
||||
for url := range urlSet {
|
||||
uniqueUrls = append(uniqueUrls, url)
|
||||
}
|
||||
|
||||
// 批量查重
|
||||
readyList, _ := repoManager.ReadyResourceRepository.BatchFindByURLs(uniqueUrls)
|
||||
existReadyUrls := make(map[string]struct{})
|
||||
for _, r := range readyList {
|
||||
existReadyUrls[r.URL] = struct{}{}
|
||||
}
|
||||
resourceList, _ := repoManager.ResourceRepository.BatchFindByURLs(uniqueUrls)
|
||||
existResourceUrls := make(map[string]struct{})
|
||||
for _, r := range resourceList {
|
||||
existResourceUrls[r.URL] = struct{}{}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
var createdResources []uint
|
||||
for _, resourceReq := range req.Resources {
|
||||
readyResource := converter.RequestToReadyResource(&resourceReq)
|
||||
if readyResource != nil {
|
||||
readyResource.Source = "公开API批量添加"
|
||||
err := repoManager.ReadyResourceRepository.Create(readyResource)
|
||||
// 生成 key(每组同一个 key)
|
||||
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
for _, url := range resourceReq.Url {
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := existReadyUrls[url]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := existResourceUrls[url]; ok {
|
||||
continue
|
||||
}
|
||||
readyResource := entity.ReadyResource{
|
||||
Title: &resourceReq.Title,
|
||||
Description: resourceReq.Description,
|
||||
URL: url,
|
||||
Category: resourceReq.Category,
|
||||
Tags: resourceReq.Tags,
|
||||
Img: resourceReq.Img,
|
||||
Source: "api",
|
||||
Extra: resourceReq.Extra,
|
||||
Key: key,
|
||||
}
|
||||
err := repoManager.ReadyResourceRepository.Create(&readyResource)
|
||||
if err == nil {
|
||||
createdResources = append(createdResources, readyResource.ID)
|
||||
}
|
||||
|
||||
@@ -46,53 +46,6 @@ func GetReadyResources(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// CreateReadyResource 创建待处理资源
|
||||
func CreateReadyResource(c *gin.Context) {
|
||||
var req dto.CreateReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||
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,
|
||||
URL: req.URL,
|
||||
Category: req.Category,
|
||||
Tags: req.Tags,
|
||||
Img: req.Img,
|
||||
Source: req.Source,
|
||||
Extra: req.Extra,
|
||||
IP: req.IP,
|
||||
}
|
||||
|
||||
err := repoManager.ReadyResourceRepository.Create(resource)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"id": resource.ID,
|
||||
"message": "待处理资源创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// BatchCreateReadyResources 批量创建待处理资源
|
||||
func BatchCreateReadyResources(c *gin.Context) {
|
||||
var req dto.BatchCreateReadyResourceRequest
|
||||
@@ -104,10 +57,14 @@ func BatchCreateReadyResources(c *gin.Context) {
|
||||
// 1. 先收集所有待提交的URL,去重
|
||||
urlSet := make(map[string]struct{})
|
||||
for _, reqResource := range req.Resources {
|
||||
if reqResource.URL == "" {
|
||||
if len(reqResource.URL) == 0 {
|
||||
continue
|
||||
}
|
||||
urlSet[reqResource.URL] = struct{}{}
|
||||
for _, u := range reqResource.URL {
|
||||
if u != "" {
|
||||
urlSet[u] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
uniqueUrls := make([]string, 0, len(urlSet))
|
||||
for url := range urlSet {
|
||||
@@ -132,31 +89,42 @@ func BatchCreateReadyResources(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 过滤掉已存在的URL
|
||||
// 5. 过滤掉已存在的URL
|
||||
var resources []entity.ReadyResource
|
||||
for _, reqResource := range req.Resources {
|
||||
url := reqResource.URL
|
||||
if url == "" {
|
||||
if len(reqResource.URL) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := existReadyUrls[url]; ok {
|
||||
continue
|
||||
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "生成批量资源组标识失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, ok := existResourceUrls[url]; ok {
|
||||
continue
|
||||
for _, url := range 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,
|
||||
URL: url,
|
||||
Category: reqResource.Category,
|
||||
Tags: reqResource.Tags,
|
||||
Img: reqResource.Img,
|
||||
Source: reqResource.Source,
|
||||
Extra: reqResource.Extra,
|
||||
IP: reqResource.IP,
|
||||
Key: key,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
resource := entity.ReadyResource{
|
||||
Title: reqResource.Title,
|
||||
Description: reqResource.Description,
|
||||
URL: reqResource.URL,
|
||||
Category: reqResource.Category,
|
||||
Tags: reqResource.Tags,
|
||||
Img: reqResource.Img,
|
||||
Source: reqResource.Source,
|
||||
Extra: reqResource.Extra,
|
||||
IP: reqResource.IP,
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
@@ -261,3 +229,239 @@ func ClearReadyResources(c *gin.Context) {
|
||||
"message": "所有待处理资源已清空",
|
||||
})
|
||||
}
|
||||
|
||||
// GetReadyResourcesByKey 根据key获取待处理资源
|
||||
func GetReadyResourcesByKey(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
if key == "" {
|
||||
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToReadyResourceResponseList(resources)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": responses,
|
||||
"key": key,
|
||||
"count": len(resources),
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReadyResourcesByKey 根据key删除待处理资源
|
||||
func DeleteReadyResourcesByKey(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
if key == "" {
|
||||
ErrorResponse(c, "key参数不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 先查询要删除的资源数量
|
||||
resources, err := repoManager.ReadyResourceRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
ErrorResponse(c, "未找到指定key的资源", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 删除所有具有相同key的资源
|
||||
err = repoManager.ReadyResourceRepository.DeleteByKey(key)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"deleted_count": len(resources),
|
||||
"key": key,
|
||||
"message": "资源组删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
// getRetryableErrorCount 统计可重试的错误数量
|
||||
func getRetryableErrorCount(resources []entity.ReadyResource) int {
|
||||
count := 0
|
||||
|
||||
for _, resource := range resources {
|
||||
if resource.ErrorMsg != "" {
|
||||
errorMsg := strings.ToUpper(resource.ErrorMsg)
|
||||
// 检查错误类型标记
|
||||
if strings.Contains(resource.ErrorMsg, "[NO_ACCOUNT]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[NO_VALID_ACCOUNT]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[TRANSFER_FAILED]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[LINK_CHECK_FAILED]") {
|
||||
count++
|
||||
} else if strings.Contains(errorMsg, "没有可用的网盘账号") ||
|
||||
strings.Contains(errorMsg, "没有有效的网盘账号") ||
|
||||
strings.Contains(errorMsg, "网盘信息获取失败") ||
|
||||
strings.Contains(errorMsg, "链接检查失败") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetReadyResourcesWithErrors 获取有错误信息的待处理资源
|
||||
func GetReadyResourcesWithErrors(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "100")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||
if err != nil || pageSize < 1 || pageSize > 1000 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
// 获取有错误的资源(分页)
|
||||
resources, total, err := repoManager.ReadyResourceRepository.FindWithErrorsPaginated(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToReadyResourceResponseList(resources)
|
||||
|
||||
// 统计错误类型
|
||||
errorTypeStats := make(map[string]int)
|
||||
for _, resource := range resources {
|
||||
if resource.ErrorMsg != "" {
|
||||
// 尝试从错误信息中提取错误类型
|
||||
if len(resource.ErrorMsg) > 0 && resource.ErrorMsg[0] == '[' {
|
||||
endIndex := strings.Index(resource.ErrorMsg, "]")
|
||||
if endIndex > 0 {
|
||||
errorType := resource.ErrorMsg[1:endIndex]
|
||||
errorTypeStats[errorType]++
|
||||
} else {
|
||||
errorTypeStats["UNKNOWN"]++
|
||||
}
|
||||
} else {
|
||||
// 如果没有错误类型标记,尝试从错误信息中推断
|
||||
errorMsg := strings.ToUpper(resource.ErrorMsg)
|
||||
if strings.Contains(errorMsg, "不支持的链接") {
|
||||
errorTypeStats["UNSUPPORTED_LINK"]++
|
||||
} else if strings.Contains(errorMsg, "链接无效") {
|
||||
errorTypeStats["INVALID_LINK"]++
|
||||
} else if strings.Contains(errorMsg, "没有可用的网盘账号") {
|
||||
errorTypeStats["NO_ACCOUNT"]++
|
||||
} else if strings.Contains(errorMsg, "没有有效的网盘账号") {
|
||||
errorTypeStats["NO_VALID_ACCOUNT"]++
|
||||
} else if strings.Contains(errorMsg, "网盘信息获取失败") {
|
||||
errorTypeStats["TRANSFER_FAILED"]++
|
||||
} else if strings.Contains(errorMsg, "创建网盘服务失败") {
|
||||
errorTypeStats["SERVICE_CREATION_FAILED"]++
|
||||
} else if strings.Contains(errorMsg, "处理标签失败") {
|
||||
errorTypeStats["TAG_PROCESSING_FAILED"]++
|
||||
} else if strings.Contains(errorMsg, "处理分类失败") {
|
||||
errorTypeStats["CATEGORY_PROCESSING_FAILED"]++
|
||||
} else if strings.Contains(errorMsg, "资源保存失败") {
|
||||
errorTypeStats["RESOURCE_SAVE_FAILED"]++
|
||||
} else if strings.Contains(errorMsg, "未找到对应的平台ID") {
|
||||
errorTypeStats["PLATFORM_NOT_FOUND"]++
|
||||
} else if strings.Contains(errorMsg, "链接检查失败") {
|
||||
errorTypeStats["LINK_CHECK_FAILED"]++
|
||||
} else {
|
||||
errorTypeStats["UNKNOWN"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": responses,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"total": total,
|
||||
"count": len(resources),
|
||||
"error_stats": errorTypeStats,
|
||||
"retryable_count": getRetryableErrorCount(resources),
|
||||
})
|
||||
}
|
||||
|
||||
// ClearErrorMsg 清除指定资源的错误信息
|
||||
func ClearErrorMsg(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.ReadyResourceRepository.ClearErrorMsg(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "错误信息已清除"})
|
||||
}
|
||||
|
||||
// RetryFailedResources 重试失败的资源
|
||||
func RetryFailedResources(c *gin.Context) {
|
||||
// 获取有错误的资源
|
||||
resources, err := repoManager.ReadyResourceRepository.FindWithErrors()
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "没有需要重试的资源",
|
||||
"count": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 只重试可重试的错误
|
||||
clearedCount := 0
|
||||
skippedCount := 0
|
||||
|
||||
for _, resource := range resources {
|
||||
isRetryable := false
|
||||
errorMsg := strings.ToUpper(resource.ErrorMsg)
|
||||
|
||||
// 检查错误类型标记
|
||||
if strings.Contains(resource.ErrorMsg, "[NO_ACCOUNT]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[NO_VALID_ACCOUNT]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[TRANSFER_FAILED]") ||
|
||||
strings.Contains(resource.ErrorMsg, "[LINK_CHECK_FAILED]") {
|
||||
isRetryable = true
|
||||
} else if strings.Contains(errorMsg, "没有可用的网盘账号") ||
|
||||
strings.Contains(errorMsg, "没有有效的网盘账号") ||
|
||||
strings.Contains(errorMsg, "网盘信息获取失败") ||
|
||||
strings.Contains(errorMsg, "链接检查失败") {
|
||||
isRetryable = true
|
||||
}
|
||||
|
||||
if isRetryable {
|
||||
if err := repoManager.ReadyResourceRepository.ClearErrorMsg(resource.ID); err == nil {
|
||||
clearedCount++
|
||||
}
|
||||
} else {
|
||||
skippedCount++
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "已清除可重试资源的错误信息,资源将在下次调度时重新处理",
|
||||
"total_count": len(resources),
|
||||
"cleared_count": clearedCount,
|
||||
"skipped_count": skippedCount,
|
||||
"retryable_count": getRetryableErrorCount(resources),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ func GetSchedulerStatus(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
|
||||
status := gin.H{
|
||||
@@ -36,6 +38,8 @@ func StartHotDramaScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if scheduler.IsHotDramaSchedulerRunning() {
|
||||
ErrorResponse(c, "热播剧定时任务已在运行中", http.StatusBadRequest)
|
||||
@@ -54,6 +58,8 @@ func StopHotDramaScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if !scheduler.IsHotDramaSchedulerRunning() {
|
||||
ErrorResponse(c, "热播剧定时任务未在运行", http.StatusBadRequest)
|
||||
@@ -72,6 +78,8 @@ func TriggerHotDramaScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
scheduler.StartHotDramaScheduler() // 直接启动一次
|
||||
SuccessResponse(c, gin.H{"message": "手动触发热播剧定时任务成功"})
|
||||
@@ -86,6 +94,8 @@ func FetchHotDramaNames(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
names, err := scheduler.GetHotDramaNames()
|
||||
if err != nil {
|
||||
@@ -104,6 +114,8 @@ func StartReadyResourceScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if scheduler.IsReadyResourceRunning() {
|
||||
ErrorResponse(c, "待处理资源自动处理任务已在运行中", http.StatusBadRequest)
|
||||
@@ -122,6 +134,8 @@ func StopReadyResourceScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if !scheduler.IsReadyResourceRunning() {
|
||||
ErrorResponse(c, "待处理资源自动处理任务未在运行", http.StatusBadRequest)
|
||||
@@ -140,6 +154,8 @@ func TriggerReadyResourceScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
// 手动触发一次处理
|
||||
scheduler.ProcessReadyResources()
|
||||
@@ -155,6 +171,8 @@ func StartAutoTransferScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if scheduler.IsAutoTransferRunning() {
|
||||
ErrorResponse(c, "自动转存定时任务已在运行中", http.StatusBadRequest)
|
||||
@@ -173,6 +191,8 @@ func StopAutoTransferScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if !scheduler.IsAutoTransferRunning() {
|
||||
ErrorResponse(c, "自动转存定时任务未在运行", http.StatusBadRequest)
|
||||
@@ -191,6 +211,8 @@ func TriggerAutoTransferScheduler(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
// 手动触发一次处理
|
||||
scheduler.ProcessAutoTransfer()
|
||||
|
||||
@@ -24,7 +24,7 @@ func GetStats(c *gin.Context) {
|
||||
|
||||
// 获取今日更新数量
|
||||
var todayUpdates int64
|
||||
today := time.Now().Format("2006-01-02")
|
||||
today := utils.GetCurrentTime().Format("2006-01-02")
|
||||
db.DB.Model(&entity.Resource{}).Where("DATE(updated_at) = ?", today).Count(&todayUpdates)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
@@ -65,7 +65,7 @@ func GetPerformanceStats(c *gin.Context) {
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"timestamp": utils.GetCurrentTime().Unix(),
|
||||
"memory": gin.H{
|
||||
"alloc": m.Alloc,
|
||||
"total_alloc": m.TotalAlloc,
|
||||
@@ -89,8 +89,8 @@ func GetPerformanceStats(c *gin.Context) {
|
||||
func GetSystemInfo(c *gin.Context) {
|
||||
SuccessResponse(c, gin.H{
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"start_time": startTime.Format("2006-01-02 15:04:05"),
|
||||
"version": "1.0.0",
|
||||
"start_time": utils.FormatTime(startTime, "2006-01-02 15:04:05"),
|
||||
"version": utils.Version,
|
||||
"environment": gin.H{
|
||||
"gin_mode": gin.Mode(),
|
||||
},
|
||||
@@ -98,4 +98,4 @@ func GetSystemInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 记录启动时间
|
||||
var startTime = time.Now()
|
||||
var startTime = utils.GetCurrentTime()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
@@ -25,13 +26,13 @@ func NewSystemConfigHandler(systemConfigRepo repo.SystemConfigRepository) *Syste
|
||||
|
||||
// GetConfig 获取系统配置
|
||||
func (h *SystemConfigHandler) GetConfig(c *gin.Context) {
|
||||
config, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
configs, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(config)
|
||||
configResponse := converter.SystemConfigToResponse(configs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
@@ -43,67 +44,67 @@ func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if req.SiteTitle == "" {
|
||||
ErrorResponse(c, "网站标题不能为空", http.StatusBadRequest)
|
||||
// 验证参数 - 只验证提交的字段
|
||||
if req.SiteTitle != "" && (len(req.SiteTitle) < 1 || len(req.SiteTitle) > 100) {
|
||||
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
|
||||
if req.AutoProcessInterval != 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) {
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PageSize < 10 || req.PageSize > 500 {
|
||||
if req.PageSize != 0 && (req.PageSize < 10 || req.PageSize > 500) {
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证自动转存配置
|
||||
if req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365 {
|
||||
if req.AutoTransferLimitDays != 0 && (req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365) {
|
||||
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024 {
|
||||
if req.AutoTransferMinSpace != 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) {
|
||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := converter.RequestToSystemConfig(&req)
|
||||
if config == nil {
|
||||
configs := converter.RequestToSystemConfig(&req)
|
||||
if configs == nil {
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
err := h.systemConfigRepo.Upsert(config)
|
||||
err := h.systemConfigRepo.UpsertConfigs(configs)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的配置
|
||||
updatedConfig, err := h.systemConfigRepo.FindFirst()
|
||||
updatedConfigs, err := h.systemConfigRepo.FindAll()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取更新后的配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfig)
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfigs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// GetSystemConfig 获取系统配置(使用全局repoManager)
|
||||
func GetSystemConfig(c *gin.Context) {
|
||||
config, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
configs, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(config)
|
||||
configResponse := converter.SystemConfigToResponse(configs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
@@ -115,42 +116,45 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if req.SiteTitle == "" {
|
||||
ErrorResponse(c, "网站标题不能为空", http.StatusBadRequest)
|
||||
// 调试信息
|
||||
utils.Info("接收到的配置请求: %+v", req)
|
||||
|
||||
// 验证参数 - 只验证提交的字段
|
||||
if req.SiteTitle != "" && (len(req.SiteTitle) < 1 || len(req.SiteTitle) > 100) {
|
||||
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440 {
|
||||
if req.AutoProcessInterval != 0 && (req.AutoProcessInterval < 1 || req.AutoProcessInterval > 1440) {
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PageSize < 10 || req.PageSize > 500 {
|
||||
if req.PageSize != 0 && (req.PageSize < 10 || req.PageSize > 500) {
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证自动转存配置
|
||||
if req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365 {
|
||||
if req.AutoTransferLimitDays != 0 && (req.AutoTransferLimitDays < 0 || req.AutoTransferLimitDays > 365) {
|
||||
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024 {
|
||||
if req.AutoTransferMinSpace != 0 && (req.AutoTransferMinSpace < 100 || req.AutoTransferMinSpace > 1024) {
|
||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
config := converter.RequestToSystemConfig(&req)
|
||||
if config == nil {
|
||||
configs := converter.RequestToSystemConfig(&req)
|
||||
if configs == nil {
|
||||
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
err := repoManager.SystemConfigRepository.Upsert(config)
|
||||
err := repoManager.SystemConfigRepository.UpsertConfigs(configs)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -164,29 +168,94 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if scheduler != nil {
|
||||
scheduler.UpdateSchedulerStatusWithAutoTransfer(req.AutoFetchHotDramaEnabled, req.AutoProcessReadyResources, req.AutoTransferEnabled)
|
||||
}
|
||||
|
||||
// 返回更新后的配置
|
||||
updatedConfig, err := repoManager.SystemConfigRepository.FindFirst()
|
||||
updatedConfigs, err := repoManager.SystemConfigRepository.FindAll()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取更新后的配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfig)
|
||||
configResponse := converter.SystemConfigToResponse(updatedConfigs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// 新增:公开获取系统配置(不含api_token)
|
||||
func GetPublicSystemConfig(c *gin.Context) {
|
||||
config, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
configs, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
configResponse := converter.SystemConfigToPublicResponse(config)
|
||||
configResponse := converter.SystemConfigToPublicResponse(configs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
// 新增:切换自动处理配置
|
||||
func ToggleAutoProcess(c *gin.Context) {
|
||||
var req struct {
|
||||
AutoProcessReadyResources bool `json:"auto_process_ready_resources"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前配置
|
||||
configs, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新自动处理配置
|
||||
for i, config := range configs {
|
||||
if config.Key == entity.ConfigKeyAutoProcessReadyResources {
|
||||
configs[i].Value = "true"
|
||||
if !req.AutoProcessReadyResources {
|
||||
configs[i].Value = "false"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
err = repoManager.SystemConfigRepository.UpsertConfigs(configs)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "保存系统配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新定时任务状态
|
||||
scheduler := utils.GetGlobalScheduler(
|
||||
repoManager.HotDramaRepository,
|
||||
repoManager.ReadyResourceRepository,
|
||||
repoManager.ResourceRepository,
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
if scheduler != nil {
|
||||
// 获取其他配置值
|
||||
autoFetchHotDrama, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoFetchHotDramaEnabled)
|
||||
autoTransfer, _ := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
|
||||
scheduler.UpdateSchedulerStatusWithAutoTransfer(
|
||||
autoFetchHotDrama,
|
||||
req.AutoProcessReadyResources,
|
||||
autoTransfer,
|
||||
)
|
||||
}
|
||||
|
||||
// 返回更新后的配置
|
||||
configResponse := converter.SystemConfigToResponse(configs)
|
||||
SuccessResponse(c, configResponse)
|
||||
}
|
||||
|
||||
@@ -65,13 +65,47 @@ func CreateTag(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首先检查是否存在已删除的同名标签
|
||||
deletedTag, err := repoManager.TagRepository.FindByNameIncludingDeleted(req.Name)
|
||||
if err == nil && deletedTag.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名标签,则恢复它
|
||||
err = repoManager.TagRepository.RestoreDeletedTag(deletedTag.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "恢复已删除标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新获取恢复后的标签
|
||||
restoredTag, err := repoManager.TagRepository.FindByID(deletedTag.ID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取恢复的标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新标签信息
|
||||
restoredTag.Description = req.Description
|
||||
restoredTag.CategoryID = req.CategoryID
|
||||
err = repoManager.TagRepository.UpdateWithNulls(restoredTag)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "更新恢复的标签失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "标签恢复成功",
|
||||
"tag": converter.ToTagResponse(restoredTag, 0),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不存在已删除的同名标签,则创建新标签
|
||||
tag := &entity.Tag{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CategoryID: req.CategoryID,
|
||||
}
|
||||
|
||||
err := repoManager.TagRepository.Create(tag)
|
||||
err = repoManager.TagRepository.Create(tag)
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -27,7 +28,7 @@ func GetVersion(c *gin.Context) {
|
||||
Success: true,
|
||||
Data: versionInfo,
|
||||
Message: "版本信息获取成功",
|
||||
Time: time.Now(),
|
||||
Time: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
@@ -43,7 +44,7 @@ func GetVersionString(c *gin.Context) {
|
||||
"version": versionString,
|
||||
},
|
||||
Message: "版本字符串获取成功",
|
||||
Time: time.Now(),
|
||||
Time: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
@@ -59,7 +60,7 @@ func GetFullVersionInfo(c *gin.Context) {
|
||||
"version_info": fullInfo,
|
||||
},
|
||||
Message: "完整版本信息获取成功",
|
||||
Time: time.Now(),
|
||||
Time: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
@@ -72,8 +73,8 @@ func CheckUpdate(c *gin.Context) {
|
||||
// 从GitHub API获取最新版本信息
|
||||
latestVersion, err := getLatestVersionFromGitHub()
|
||||
if err != nil {
|
||||
// 如果GitHub API失败,使用模拟数据
|
||||
latestVersion = "1.0.0"
|
||||
// 如果GitHub API失败,使用当前版本作为最新版本
|
||||
latestVersion = currentVersion
|
||||
}
|
||||
|
||||
hasUpdate := utils.IsVersionNewer(latestVersion, currentVersion)
|
||||
@@ -88,7 +89,7 @@ func CheckUpdate(c *gin.Context) {
|
||||
"update_url": "https://github.com/ctwj/urldb/releases/latest",
|
||||
},
|
||||
Message: "更新检查完成",
|
||||
Time: time.Now(),
|
||||
Time: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
@@ -96,10 +97,25 @@ func CheckUpdate(c *gin.Context) {
|
||||
|
||||
// getLatestVersionFromGitHub 从GitHub获取最新版本
|
||||
func getLatestVersionFromGitHub() (string, error) {
|
||||
// 使用GitHub API获取最新Release
|
||||
// 首先尝试从VERSION文件URL获取最新版本
|
||||
versionURL := "https://raw.githubusercontent.com/ctwj/urldb/refs/heads/main/VERSION"
|
||||
|
||||
resp, err := http.Get(versionURL)
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
version := strings.TrimSpace(string(body))
|
||||
if version != "" {
|
||||
return version, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果VERSION文件获取失败,尝试GitHub API获取最新Release
|
||||
url := "https://api.github.com/repos/ctwj/urldb/releases/latest"
|
||||
|
||||
resp, err := http.Get(url)
|
||||
resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
99
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/handlers"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
@@ -28,6 +29,38 @@ func main() {
|
||||
utils.Info("未找到.env文件,使用默认配置")
|
||||
}
|
||||
|
||||
// 初始化时区设置
|
||||
utils.InitTimezone()
|
||||
|
||||
// 设置Gin运行模式
|
||||
ginMode := os.Getenv("GIN_MODE")
|
||||
if ginMode == "" {
|
||||
// 如果没有设置GIN_MODE,根据环境判断
|
||||
if os.Getenv("ENV") == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
utils.Info("设置Gin为Release模式")
|
||||
} else {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
utils.Info("设置Gin为Debug模式")
|
||||
}
|
||||
} else {
|
||||
// 如果已经设置了GIN_MODE,根据值设置模式
|
||||
switch ginMode {
|
||||
case "release":
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
utils.Info("设置Gin为Release模式 (来自环境变量)")
|
||||
case "debug":
|
||||
gin.SetMode(gin.DebugMode)
|
||||
utils.Info("设置Gin为Debug模式 (来自环境变量)")
|
||||
case "test":
|
||||
gin.SetMode(gin.TestMode)
|
||||
utils.Info("设置Gin为Test模式 (来自环境变量)")
|
||||
default:
|
||||
gin.SetMode(gin.DebugMode)
|
||||
utils.Info("未知的GIN_MODE值: %s,使用Debug模式", ginMode)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
if err := db.InitDB(); err != nil {
|
||||
utils.Fatal("数据库连接失败: %v", err)
|
||||
@@ -44,36 +77,47 @@ func main() {
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.PanRepository,
|
||||
repoManager.CksRepository,
|
||||
repoManager.TagRepository,
|
||||
repoManager.CategoryRepository,
|
||||
)
|
||||
|
||||
// 确保默认配置存在
|
||||
// _, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
// if err != nil {
|
||||
// utils.Error("初始化默认配置失败: %v", err)
|
||||
// } else {
|
||||
// utils.Info("默认配置初始化完成")
|
||||
// }
|
||||
|
||||
// 检查系统配置,决定是否启动各种自动任务
|
||||
systemConfig, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
autoProcessReadyResources, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
if err != nil {
|
||||
utils.Error("获取系统配置失败: %v", err)
|
||||
utils.Error("获取自动处理待处理资源配置失败: %v", err)
|
||||
} else if autoProcessReadyResources {
|
||||
scheduler.StartReadyResourceScheduler()
|
||||
utils.Info("已启动待处理资源自动处理任务")
|
||||
} else {
|
||||
// 检查是否启动待处理资源自动处理任务
|
||||
if systemConfig.AutoProcessReadyResources {
|
||||
scheduler.StartReadyResourceScheduler()
|
||||
utils.Info("已启动待处理资源自动处理任务")
|
||||
} else {
|
||||
utils.Info("系统配置中自动处理待处理资源功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
utils.Info("系统配置中自动处理待处理资源功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
|
||||
// 检查是否启动热播剧自动拉取任务
|
||||
if systemConfig.AutoFetchHotDramaEnabled {
|
||||
scheduler.StartHotDramaScheduler()
|
||||
utils.Info("已启动热播剧自动拉取任务")
|
||||
} else {
|
||||
utils.Info("系统配置中自动拉取热播剧功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
autoFetchHotDramaEnabled, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoFetchHotDramaEnabled)
|
||||
if err != nil {
|
||||
utils.Error("获取自动拉取热播剧配置失败: %v", err)
|
||||
} else if autoFetchHotDramaEnabled {
|
||||
scheduler.StartHotDramaScheduler()
|
||||
utils.Info("已启动热播剧自动拉取任务")
|
||||
} else {
|
||||
utils.Info("系统配置中自动拉取热播剧功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
|
||||
// 检查是否启动自动转存任务
|
||||
if systemConfig.AutoTransferEnabled {
|
||||
scheduler.StartAutoTransferScheduler()
|
||||
utils.Info("已启动自动转存任务")
|
||||
} else {
|
||||
utils.Info("系统配置中自动转存功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
autoTransferEnabled, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
if err != nil {
|
||||
utils.Error("获取自动转存配置失败: %v", err)
|
||||
} else if autoTransferEnabled {
|
||||
scheduler.StartAutoTransferScheduler()
|
||||
utils.Info("已启动自动转存任务")
|
||||
} else {
|
||||
utils.Info("系统配置中自动转存功能已禁用,跳过启动定时任务")
|
||||
}
|
||||
|
||||
// 创建Gin实例
|
||||
@@ -102,8 +146,6 @@ func main() {
|
||||
publicAPI := api.Group("/public")
|
||||
publicAPI.Use(middleware.PublicAPIAuth())
|
||||
{
|
||||
// 单个添加资源
|
||||
publicAPI.POST("/resources/add", publicAPIHandler.AddSingleResource)
|
||||
// 批量添加资源
|
||||
publicAPI.POST("/resources/batch-add", publicAPIHandler.AddBatchResources)
|
||||
// 资源搜索
|
||||
@@ -166,11 +208,15 @@ func main() {
|
||||
|
||||
// 待处理资源管理
|
||||
api.GET("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResources)
|
||||
api.POST("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResource)
|
||||
api.POST("/ready-resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchCreateReadyResources)
|
||||
api.POST("/ready-resources/text", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResourcesFromText)
|
||||
api.DELETE("/ready-resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResource)
|
||||
api.DELETE("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearReadyResources)
|
||||
api.GET("/ready-resources/key/:key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResourcesByKey)
|
||||
api.DELETE("/ready-resources/key/:key", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResourcesByKey)
|
||||
api.GET("/ready-resources/errors", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResourcesWithErrors)
|
||||
api.POST("/ready-resources/:id/clear-error", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearErrorMsg)
|
||||
api.POST("/ready-resources/retry-failed", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.RetryFailedResources)
|
||||
|
||||
// 用户管理(仅管理员)
|
||||
api.GET("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetUsers)
|
||||
@@ -192,6 +238,7 @@ func main() {
|
||||
// 系统配置路由
|
||||
api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig)
|
||||
api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig)
|
||||
api.POST("/system/config/toggle-auto-process", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ToggleAutoProcess)
|
||||
api.GET("/public/system-config", handlers.GetPublicSystemConfig)
|
||||
|
||||
// 热播剧管理路由(查询接口无需认证)
|
||||
|
||||
@@ -94,9 +94,9 @@ func GenerateToken(user *entity.User) (string, error) {
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * 24 * time.Hour)), // 30天有效期
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(utils.GetCurrentTime().Add(30 * 24 * time.Hour)), // 30天有效期
|
||||
IssuedAt: jwt.NewNumericDate(utils.GetCurrentTime()),
|
||||
NotBefore: jwt.NewNumericDate(utils.GetCurrentTime()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -45,7 +46,8 @@ func PublicAPIAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := repoManager.SystemConfigRepository.FindFirst()
|
||||
// 验证API Token
|
||||
apiTokenConfig, err := repoManager.SystemConfigRepository.GetConfigValue(entity.ConfigKeyApiToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
@@ -56,7 +58,7 @@ func PublicAPIAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if config.ApiToken == "" {
|
||||
if apiTokenConfig == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"success": false,
|
||||
"message": "API Token未配置",
|
||||
@@ -66,7 +68,7 @@ func PublicAPIAuth() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if config.ApiToken != apiToken {
|
||||
if apiTokenConfig != apiToken {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "API Token无效",
|
||||
@@ -77,7 +79,18 @@ func PublicAPIAuth() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// 检查维护模式
|
||||
if config.MaintenanceMode {
|
||||
maintenanceMode, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyMaintenanceMode)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "系统配置获取失败",
|
||||
"code": 500,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if maintenanceMode {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"success": false,
|
||||
"message": "系统维护中,请稍后再试",
|
||||
|
||||
153
utils/errors.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package utils
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrorType 错误类型枚举
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
// ErrorTypeUnsupportedLink 不支持的链接
|
||||
ErrorTypeUnsupportedLink ErrorType = "UNSUPPORTED_LINK"
|
||||
// ErrorTypeInvalidLink 无效链接
|
||||
ErrorTypeInvalidLink ErrorType = "INVALID_LINK"
|
||||
// ErrorTypeNoAccount 没有可用账号
|
||||
ErrorTypeNoAccount ErrorType = "NO_ACCOUNT"
|
||||
// ErrorTypeNoValidAccount 没有有效账号
|
||||
ErrorTypeNoValidAccount ErrorType = "NO_VALID_ACCOUNT"
|
||||
// ErrorTypeServiceCreation 服务创建失败
|
||||
ErrorTypeServiceCreation ErrorType = "SERVICE_CREATION_FAILED"
|
||||
// ErrorTypeTransferFailed 转存失败
|
||||
ErrorTypeTransferFailed ErrorType = "TRANSFER_FAILED"
|
||||
// ErrorTypeTagProcessing 标签处理失败
|
||||
ErrorTypeTagProcessing ErrorType = "TAG_PROCESSING_FAILED"
|
||||
// ErrorTypeCategoryProcessing 分类处理失败
|
||||
ErrorTypeCategoryProcessing ErrorType = "CATEGORY_PROCESSING_FAILED"
|
||||
// ErrorTypeResourceSave 资源保存失败
|
||||
ErrorTypeResourceSave ErrorType = "RESOURCE_SAVE_FAILED"
|
||||
// ErrorTypePlatformNotFound 平台未找到
|
||||
ErrorTypePlatformNotFound ErrorType = "PLATFORM_NOT_FOUND"
|
||||
// ErrorTypeLinkCheckFailed 链接检查失败
|
||||
ErrorTypeLinkCheckFailed ErrorType = "LINK_CHECK_FAILED"
|
||||
)
|
||||
|
||||
// ResourceError 资源处理错误
|
||||
type ResourceError struct {
|
||||
Type ErrorType `json:"type"`
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (e *ResourceError) Error() string {
|
||||
if e.Details != "" {
|
||||
return fmt.Sprintf("[%s] %s: %s", e.Type, e.Message, e.Details)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", e.Type, e.Message)
|
||||
}
|
||||
|
||||
// NewResourceError 创建新的资源错误
|
||||
func NewResourceError(errorType ErrorType, message string, url string, details string) *ResourceError {
|
||||
return &ResourceError{
|
||||
Type: errorType,
|
||||
Message: message,
|
||||
URL: url,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnsupportedLinkError 创建不支持的链接错误
|
||||
func NewUnsupportedLinkError(url string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeUnsupportedLink, "不支持的链接地址", url, "")
|
||||
}
|
||||
|
||||
// NewInvalidLinkError 创建无效链接错误
|
||||
func NewInvalidLinkError(url string, details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeInvalidLink, "链接无效", url, details)
|
||||
}
|
||||
|
||||
// NewNoAccountError 创建没有账号错误
|
||||
func NewNoAccountError(platform string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeNoAccount, "没有可用的网盘账号", "", fmt.Sprintf("平台: %s", platform))
|
||||
}
|
||||
|
||||
// NewNoValidAccountError 创建没有有效账号错误
|
||||
func NewNoValidAccountError(platform string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeNoValidAccount, "没有有效的网盘账号", "", fmt.Sprintf("平台: %s", platform))
|
||||
}
|
||||
|
||||
// NewServiceCreationError 创建服务创建失败错误
|
||||
func NewServiceCreationError(url string, details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeServiceCreation, "创建网盘服务失败", url, details)
|
||||
}
|
||||
|
||||
// NewTransferFailedError 创建转存失败错误
|
||||
func NewTransferFailedError(url string, details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeTransferFailed, "网盘信息获取失败", url, details)
|
||||
}
|
||||
|
||||
// NewTagProcessingError 创建标签处理失败错误
|
||||
func NewTagProcessingError(details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeTagProcessing, "处理标签失败", "", details)
|
||||
}
|
||||
|
||||
// NewCategoryProcessingError 创建分类处理失败错误
|
||||
func NewCategoryProcessingError(details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeCategoryProcessing, "处理分类失败", "", details)
|
||||
}
|
||||
|
||||
// NewResourceSaveError 创建资源保存失败错误
|
||||
func NewResourceSaveError(url string, details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeResourceSave, "资源保存失败", url, details)
|
||||
}
|
||||
|
||||
// NewPlatformNotFoundError 创建平台未找到错误
|
||||
func NewPlatformNotFoundError(platform string) *ResourceError {
|
||||
return NewResourceError(ErrorTypePlatformNotFound, "未找到对应的平台ID", "", fmt.Sprintf("平台: %s", platform))
|
||||
}
|
||||
|
||||
// NewLinkCheckError 创建链接检查失败错误
|
||||
func NewLinkCheckError(url string, details string) *ResourceError {
|
||||
return NewResourceError(ErrorTypeLinkCheckFailed, "链接检查失败", url, details)
|
||||
}
|
||||
|
||||
// IsResourceError 检查是否为资源错误
|
||||
func IsResourceError(err error) bool {
|
||||
_, ok := err.(*ResourceError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetResourceError 获取资源错误
|
||||
func GetResourceError(err error) *ResourceError {
|
||||
if resourceErr, ok := err.(*ResourceError); ok {
|
||||
return resourceErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetErrorType 获取错误类型
|
||||
func GetErrorType(err error) ErrorType {
|
||||
if resourceErr := GetResourceError(err); resourceErr != nil {
|
||||
return resourceErr.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsRetryableError 检查是否为可重试的错误
|
||||
func IsRetryableError(err error) bool {
|
||||
errorType := GetErrorType(err)
|
||||
switch errorType {
|
||||
case ErrorTypeNoAccount, ErrorTypeNoValidAccount, ErrorTypeTransferFailed, ErrorTypeLinkCheckFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetErrorSummary 获取错误摘要
|
||||
func GetErrorSummary(err error) string {
|
||||
if resourceErr := GetResourceError(err); resourceErr != nil {
|
||||
return fmt.Sprintf("%s: %s", resourceErr.Type, resourceErr.Message)
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
@@ -18,10 +18,10 @@ var (
|
||||
)
|
||||
|
||||
// GetGlobalScheduler 获取全局调度器实例(单例模式)
|
||||
func GetGlobalScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository) *GlobalScheduler {
|
||||
func GetGlobalScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository, tagRepo repo.TagRepository, categoryRepo repo.CategoryRepository) *GlobalScheduler {
|
||||
once.Do(func() {
|
||||
globalScheduler = &GlobalScheduler{
|
||||
scheduler: NewScheduler(hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo, panRepo, cksRepo),
|
||||
scheduler: NewScheduler(hotDramaRepo, readyResourceRepo, resourceRepo, systemConfigRepo, panRepo, cksRepo, tagRepo, categoryRepo),
|
||||
}
|
||||
})
|
||||
return globalScheduler
|
||||
|
||||
@@ -269,7 +269,7 @@ func (lv *LogViewer) CleanOldLogs(days int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cutoffTime := time.Now().AddDate(0, 0, -days)
|
||||
cutoffTime := GetCurrentTime().AddDate(0, 0, -days)
|
||||
deletedCount := 0
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
@@ -153,7 +153,7 @@ func (l *Logger) initLogFile() error {
|
||||
}
|
||||
|
||||
// 创建新的日志文件
|
||||
logFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", time.Now().Format("2006-01-02")))
|
||||
logFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", GetCurrentTime().Format("2006-01-02")))
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建日志文件失败: %v", err)
|
||||
@@ -291,8 +291,8 @@ func (l *Logger) rotateLog() {
|
||||
}
|
||||
|
||||
// 重命名当前日志文件
|
||||
currentLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", time.Now().Format("2006-01-02")))
|
||||
backupLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s_%s.log", time.Now().Format("2006-01-02"), time.Now().Format("15-04-05")))
|
||||
currentLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", GetCurrentTime().Format("2006-01-02")))
|
||||
backupLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s_%s.log", GetCurrentTime().Format("2006-01-02"), GetCurrentTime().Format("15-04-05")))
|
||||
|
||||
if _, err := os.Stat(currentLogFile); err == nil {
|
||||
os.Rename(currentLogFile, backupLogFile)
|
||||
@@ -314,7 +314,7 @@ func (l *Logger) cleanOldLogs() {
|
||||
return
|
||||
}
|
||||
|
||||
cutoffTime := time.Now().AddDate(0, 0, -l.config.MaxAge)
|
||||
cutoffTime := GetCurrentTime().AddDate(0, 0, -l.config.MaxAge)
|
||||
|
||||
for _, file := range files {
|
||||
fileInfo, err := os.Stat(file)
|
||||
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -15,13 +16,16 @@ import (
|
||||
|
||||
// Scheduler 定时任务管理器
|
||||
type Scheduler struct {
|
||||
doubanService *DoubanService
|
||||
hotDramaRepo repo.HotDramaRepository
|
||||
readyResourceRepo repo.ReadyResourceRepository
|
||||
resourceRepo repo.ResourceRepository
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
panRepo repo.PanRepository
|
||||
cksRepo repo.CksRepository
|
||||
doubanService *DoubanService
|
||||
hotDramaRepo repo.HotDramaRepository
|
||||
readyResourceRepo repo.ReadyResourceRepository
|
||||
resourceRepo repo.ResourceRepository
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
panRepo repo.PanRepository
|
||||
cksRepo repo.CksRepository
|
||||
// 新增
|
||||
tagRepo repo.TagRepository
|
||||
categoryRepo repo.CategoryRepository
|
||||
stopChan chan bool
|
||||
isRunning bool
|
||||
readyResourceRunning bool
|
||||
@@ -36,7 +40,7 @@ type Scheduler struct {
|
||||
}
|
||||
|
||||
// NewScheduler 创建新的定时任务管理器
|
||||
func NewScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository) *Scheduler {
|
||||
func NewScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.ReadyResourceRepository, resourceRepo repo.ResourceRepository, systemConfigRepo repo.SystemConfigRepository, panRepo repo.PanRepository, cksRepo repo.CksRepository, tagRepo repo.TagRepository, categoryRepo repo.CategoryRepository) *Scheduler {
|
||||
return &Scheduler{
|
||||
doubanService: NewDoubanService(),
|
||||
hotDramaRepo: hotDramaRepo,
|
||||
@@ -45,6 +49,8 @@ func NewScheduler(hotDramaRepo repo.HotDramaRepository, readyResourceRepo repo.R
|
||||
systemConfigRepo: systemConfigRepo,
|
||||
panRepo: panRepo,
|
||||
cksRepo: cksRepo,
|
||||
tagRepo: tagRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
stopChan: make(chan bool),
|
||||
isRunning: false,
|
||||
readyResourceRunning: false,
|
||||
@@ -284,10 +290,9 @@ func (s *Scheduler) StartReadyResourceScheduler() {
|
||||
|
||||
go func() {
|
||||
// 获取系统配置中的间隔时间
|
||||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||||
interval := 3 * time.Minute // 默认5分钟
|
||||
if err == nil && config.AutoProcessInterval > 0 {
|
||||
interval = time.Duration(config.AutoProcessInterval) * time.Minute
|
||||
interval := 3 * time.Minute // 默认3分钟
|
||||
if autoProcessInterval, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoProcessInterval); err == nil && autoProcessInterval > 0 {
|
||||
interval = time.Duration(autoProcessInterval) * time.Minute
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -335,19 +340,20 @@ func (s *Scheduler) processReadyResources() {
|
||||
Info("开始处理待处理资源...")
|
||||
|
||||
// 检查系统配置,确认是否启用自动处理
|
||||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||||
autoProcess, err := s.systemConfigRepo.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
|
||||
if err != nil {
|
||||
Error("获取系统配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.AutoProcessReadyResources {
|
||||
if !autoProcess {
|
||||
Info("自动处理待处理资源功能已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有待处理资源
|
||||
// 获取所有没有错误的待处理资源
|
||||
readyResources, err := s.readyResourceRepo.FindAll()
|
||||
// readyResources, err := s.readyResourceRepo.FindWithoutErrors()
|
||||
if err != nil {
|
||||
Error("获取待处理资源失败: %v", err)
|
||||
return
|
||||
@@ -378,10 +384,24 @@ func (s *Scheduler) processReadyResources() {
|
||||
|
||||
if err := s.convertReadyResourceToResource(readyResource, factory); err != nil {
|
||||
Error("处理资源失败 (ID: %d): %v", readyResource.ID, err)
|
||||
|
||||
// 保存完整的错误信息
|
||||
readyResource.ErrorMsg = err.Error()
|
||||
|
||||
if updateErr := s.readyResourceRepo.Update(&readyResource); updateErr != nil {
|
||||
Error("更新错误信息失败 (ID: %d): %v", readyResource.ID, updateErr)
|
||||
} else {
|
||||
Info("已保存错误信息到资源 (ID: %d): %s", readyResource.ID, err.Error())
|
||||
}
|
||||
|
||||
// 处理失败后删除资源,避免重复处理
|
||||
s.readyResourceRepo.Delete(readyResource.ID)
|
||||
} else {
|
||||
// 处理成功,删除readyResource
|
||||
s.readyResourceRepo.Delete(readyResource.ID)
|
||||
processedCount++
|
||||
Info("成功处理资源: %s", readyResource.URL)
|
||||
}
|
||||
s.readyResourceRepo.Delete(readyResource.ID)
|
||||
processedCount++
|
||||
Info("成功处理资源: %s", readyResource.URL)
|
||||
}
|
||||
|
||||
Info("待处理资源处理完成,共处理 %d 个资源", processedCount)
|
||||
@@ -395,121 +415,152 @@ func (s *Scheduler) convertReadyResourceToResource(readyResource entity.ReadyRes
|
||||
shareID, serviceType := panutils.ExtractShareId(readyResource.URL)
|
||||
if serviceType == panutils.NotFound {
|
||||
Warn("不支持的链接地址: %s", readyResource.URL)
|
||||
return nil
|
||||
return NewUnsupportedLinkError(readyResource.URL)
|
||||
}
|
||||
|
||||
Debug("检测到服务类型: %s, 分享ID: %s", serviceType.String(), shareID)
|
||||
|
||||
resource := &entity.Resource{
|
||||
Title: derefString(readyResource.Title),
|
||||
Description: readyResource.Description,
|
||||
URL: readyResource.URL,
|
||||
Cover: readyResource.Img,
|
||||
IsValid: true,
|
||||
IsPublic: true,
|
||||
Key: readyResource.Key,
|
||||
PanID: s.getPanIDByServiceType(serviceType),
|
||||
}
|
||||
|
||||
// 不是夸克,直接保存,
|
||||
if serviceType != panutils.Quark {
|
||||
// 检测是否有效
|
||||
checkResult, _ := commonutils.CheckURL(readyResource.URL)
|
||||
checkResult, err := commonutils.CheckURL(readyResource.URL)
|
||||
if err != nil {
|
||||
Error("链接检查失败: %v", err)
|
||||
return NewLinkCheckError(readyResource.URL, err.Error())
|
||||
}
|
||||
if !checkResult.Status {
|
||||
Warn("链接无效: %s", readyResource.URL)
|
||||
return nil
|
||||
return NewInvalidLinkError(readyResource.URL, "链接状态检查失败")
|
||||
}
|
||||
} else {
|
||||
// 获取夸克网盘账号的 cookie
|
||||
panID := s.getPanIDByServiceType(serviceType)
|
||||
if panID == nil {
|
||||
Error("未找到对应的平台ID")
|
||||
return NewPlatformNotFoundError(serviceType.String())
|
||||
}
|
||||
|
||||
// 入库
|
||||
}
|
||||
|
||||
// 准备配置
|
||||
config := &panutils.PanConfig{
|
||||
URL: readyResource.URL,
|
||||
Code: "", // 可以从readyResource中获取
|
||||
IsType: 1, // 转存并分享后的资源信息 0 转存后分享, 1 只获取基本信息
|
||||
ExpiredType: 1, // 永久分享
|
||||
AdFid: "",
|
||||
Stoken: "",
|
||||
}
|
||||
|
||||
// 通过工厂获取对应的网盘服务单例
|
||||
panService, err := factory.CreatePanService(readyResource.URL, config)
|
||||
if err != nil {
|
||||
Error("获取网盘服务失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 阿里云盘特殊处理:检查URL有效性
|
||||
// if serviceType == panutils.Alipan {
|
||||
// checkResult, _ := CheckURL(readyResource.URL)
|
||||
// if !checkResult.Status {
|
||||
// log.Printf("阿里云盘链接无效: %s", readyResource.URL)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // 如果有标题,直接创建资源
|
||||
// if readyResource.Title != nil && *readyResource.Title != "" {
|
||||
// resource := &entity.Resource{
|
||||
// Title: *readyResource.Title,
|
||||
// Description: readyResource.Description,
|
||||
// URL: readyResource.URL,
|
||||
// PanID: s.determinePanID(readyResource.URL),
|
||||
// IsValid: true,
|
||||
// IsPublic: true,
|
||||
// }
|
||||
|
||||
// // 如果有分类信息,尝试查找或创建分类
|
||||
// if readyResource.Category != "" {
|
||||
// categoryID, err := s.getOrCreateCategory(readyResource.Category)
|
||||
// if err == nil {
|
||||
// resource.CategoryID = &categoryID
|
||||
// }
|
||||
// }
|
||||
|
||||
// return s.resourceRepo.Create(resource)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 统一处理:尝试转存获取标题
|
||||
result, err := panService.Transfer(shareID)
|
||||
if err != nil {
|
||||
Error("网盘信息获取失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
Error("网盘信息获取失败: %s", result.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 提取转存结果
|
||||
if resultData, ok := result.Data.(map[string]interface{}); ok {
|
||||
title := resultData["title"].(string)
|
||||
shareURL := resultData["shareUrl"].(string)
|
||||
// fid := resultData["fid"].(string) // 暂时未使用
|
||||
|
||||
// 创建资源记录
|
||||
resource := &entity.Resource{
|
||||
Title: title,
|
||||
Description: readyResource.Description,
|
||||
URL: shareURL,
|
||||
PanID: s.getPanIDByServiceType(serviceType),
|
||||
IsValid: true,
|
||||
IsPublic: true,
|
||||
accounts, err := s.cksRepo.FindByPanID(*panID)
|
||||
if err != nil {
|
||||
Error("获取夸克网盘账号失败: %v", err)
|
||||
return NewServiceCreationError(readyResource.URL, fmt.Sprintf("获取网盘账号失败: %v", err))
|
||||
}
|
||||
|
||||
// 如果有分类信息,尝试查找或创建分类
|
||||
if readyResource.Category != "" {
|
||||
categoryID, err := s.getOrCreateCategory(readyResource.Category)
|
||||
if err == nil {
|
||||
resource.CategoryID = &categoryID
|
||||
if len(accounts) == 0 {
|
||||
Error("没有可用的夸克网盘账号")
|
||||
return NewNoAccountError(serviceType.String())
|
||||
}
|
||||
|
||||
// 选择第一个有效的账号
|
||||
var selectedAccount *entity.Cks
|
||||
for _, account := range accounts {
|
||||
if account.IsValid {
|
||||
selectedAccount = &account
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return s.resourceRepo.Create(resource)
|
||||
if selectedAccount == nil {
|
||||
Error("没有有效的夸克网盘账号")
|
||||
return NewNoValidAccountError(serviceType.String())
|
||||
}
|
||||
|
||||
Debug("使用夸克网盘账号: %d, Cookie: %s", selectedAccount.ID, selectedAccount.Ck[:20]+"...")
|
||||
|
||||
// 准备配置
|
||||
config := &panutils.PanConfig{
|
||||
URL: readyResource.URL,
|
||||
Code: "", // 可以从readyResource中获取
|
||||
IsType: 1, // 转存并分享后的资源信息 0 转存后分享, 1 只获取基本信息
|
||||
ExpiredType: 1, // 永久分享
|
||||
AdFid: "",
|
||||
Stoken: "",
|
||||
Cookie: selectedAccount.Ck, // 添加 cookie
|
||||
}
|
||||
|
||||
// 通过工厂获取对应的网盘服务单例
|
||||
panService, err := factory.CreatePanService(readyResource.URL, config)
|
||||
if err != nil {
|
||||
Error("获取网盘服务失败: %v", err)
|
||||
return NewServiceCreationError(readyResource.URL, err.Error())
|
||||
}
|
||||
|
||||
// 统一处理:尝试转存获取标题
|
||||
result, err := panService.Transfer(shareID)
|
||||
if err != nil {
|
||||
Error("网盘信息获取失败: %v", err)
|
||||
return NewTransferFailedError(readyResource.URL, err.Error())
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
Error("网盘信息获取失败: %s", result.Message)
|
||||
return NewTransferFailedError(readyResource.URL, result.Message)
|
||||
}
|
||||
|
||||
// 如果获取到了标题,更新资源标题
|
||||
// if result.Title != "" {
|
||||
// resource.Title = result.Title
|
||||
// }
|
||||
}
|
||||
|
||||
Error("转存结果格式异常")
|
||||
// 处理标签
|
||||
tagIDs, err := s.handleTags(readyResource.Tags)
|
||||
if err != nil {
|
||||
Error("处理标签失败: %v", err)
|
||||
return NewTagProcessingError(err.Error())
|
||||
}
|
||||
// 如果没有标签,tagIDs 可能为 nil,这是正常的
|
||||
if tagIDs == nil {
|
||||
tagIDs = []uint{} // 初始化为空数组
|
||||
}
|
||||
// 处理分类
|
||||
categoryID, err := s.resolveCategory(readyResource.Category, tagIDs)
|
||||
if err != nil {
|
||||
Error("处理分类失败: %v", err)
|
||||
return NewCategoryProcessingError(err.Error())
|
||||
}
|
||||
if categoryID != nil {
|
||||
resource.CategoryID = categoryID
|
||||
}
|
||||
// 保存资源
|
||||
err = s.resourceRepo.Create(resource)
|
||||
if err != nil {
|
||||
Error("资源保存失败: %v", err)
|
||||
return NewResourceSaveError(readyResource.URL, err.Error())
|
||||
}
|
||||
// 插入 resource_tags 关联
|
||||
if len(tagIDs) > 0 {
|
||||
for _, tagID := range tagIDs {
|
||||
err := s.resourceRepo.CreateResourceTag(resource.ID, tagID)
|
||||
if err != nil {
|
||||
Error("插入资源标签关联失败: %v", err)
|
||||
// 这里不返回错误,因为资源已经保存成功,标签关联失败不影响主要功能
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Debug("没有标签,跳过插入资源标签关联")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOrCreateCategory 获取或创建分类
|
||||
func (s *Scheduler) getOrCreateCategory(categoryName string) (uint, error) {
|
||||
// 这里需要实现分类的查找和创建逻辑
|
||||
// 由于没有CategoryRepository的注入,这里先返回0
|
||||
// 你可以根据需要添加CategoryRepository的依赖
|
||||
return 0, nil
|
||||
}
|
||||
// // getOrCreateCategory 获取或创建分类
|
||||
// func (s *Scheduler) getOrCreateCategory(categoryName string) (uint, error) {
|
||||
// // 这里需要实现分类的查找和创建逻辑
|
||||
// // 由于没有CategoryRepository的注入,这里先返回0
|
||||
// // 你可以根据需要添加CategoryRepository的依赖
|
||||
// return 0, nil
|
||||
// }
|
||||
|
||||
// initPanCache 初始化平台映射缓存
|
||||
func (s *Scheduler) initPanCache() {
|
||||
@@ -527,6 +578,10 @@ func (s *Scheduler) initPanCache() {
|
||||
"alipan": "aliyun", // 阿里云盘在数据库中的名称是 aliyun
|
||||
"baidu": "baidu",
|
||||
"uc": "uc",
|
||||
"xunlei": "xunlei",
|
||||
"tianyi": "tianyi",
|
||||
"123pan": "123pan",
|
||||
"115": "115",
|
||||
"unknown": "other",
|
||||
}
|
||||
|
||||
@@ -592,10 +647,9 @@ func (s *Scheduler) StartAutoTransferScheduler() {
|
||||
|
||||
go func() {
|
||||
// 获取系统配置中的间隔时间
|
||||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||||
interval := 5 * time.Minute // 默认5分钟
|
||||
if err == nil && config.AutoProcessInterval > 0 {
|
||||
interval = time.Duration(config.AutoProcessInterval) * time.Minute
|
||||
if autoProcessInterval, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoProcessInterval); err == nil && autoProcessInterval > 0 {
|
||||
interval = time.Duration(autoProcessInterval) * time.Minute
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -648,13 +702,13 @@ func (s *Scheduler) processAutoTransfer() {
|
||||
Info("开始处理自动转存...")
|
||||
|
||||
// 检查系统配置,确认是否启用自动转存
|
||||
config, err := s.systemConfigRepo.GetOrCreateDefault()
|
||||
autoTransferEnabled, err := s.systemConfigRepo.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
|
||||
if err != nil {
|
||||
Error("获取系统配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.AutoTransferEnabled {
|
||||
if !autoTransferEnabled {
|
||||
Info("自动转存功能已禁用")
|
||||
return
|
||||
}
|
||||
@@ -680,8 +734,15 @@ func (s *Scheduler) processAutoTransfer() {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取最小存储空间配置
|
||||
autoTransferMinSpace, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoTransferMinSpace)
|
||||
if err != nil {
|
||||
Error("获取最小存储空间配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 过滤:只保留已激活、quark平台、剩余空间足够的账号
|
||||
minSpaceBytes := int64(config.AutoTransferMinSpace) * 1024 * 1024 * 1024
|
||||
minSpaceBytes := int64(autoTransferMinSpace) * 1024 * 1024 * 1024
|
||||
var validAccounts []entity.Cks
|
||||
for _, acc := range accounts {
|
||||
if acc.IsValid && acc.PanID == quarkPanID && acc.LeftSpace >= minSpaceBytes {
|
||||
@@ -697,7 +758,7 @@ func (s *Scheduler) processAutoTransfer() {
|
||||
Info("找到 %d 个可用quark网盘账号,开始自动转存处理...", len(validAccounts))
|
||||
|
||||
// 获取需要转存的资源
|
||||
resources, err := s.getResourcesForTransfer(config, quarkPanID)
|
||||
resources, err := s.getResourcesForTransfer(quarkPanID)
|
||||
if err != nil {
|
||||
Error("获取需要转存的资源失败: %v", err)
|
||||
return
|
||||
@@ -710,9 +771,43 @@ func (s *Scheduler) processAutoTransfer() {
|
||||
|
||||
Info("找到 %d 个需要转存的资源", len(resources))
|
||||
|
||||
// 获取违禁词配置
|
||||
forbiddenWords, err := s.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||||
if err != nil {
|
||||
Error("获取违禁词配置失败: %v", err)
|
||||
forbiddenWords = "" // 如果获取失败,使用空字符串
|
||||
}
|
||||
|
||||
// 过滤包含违禁词的资源
|
||||
var filteredResources []*entity.Resource
|
||||
if forbiddenWords != "" {
|
||||
words := strings.Split(forbiddenWords, ",")
|
||||
for _, resource := range resources {
|
||||
shouldSkip := false
|
||||
title := strings.ToLower(resource.Title)
|
||||
description := strings.ToLower(resource.Description)
|
||||
|
||||
for _, word := range words {
|
||||
word = strings.TrimSpace(word)
|
||||
if word != "" && (strings.Contains(title, strings.ToLower(word)) || strings.Contains(description, strings.ToLower(word))) {
|
||||
Info("跳过包含违禁词 '%s' 的资源: %s", word, resource.Title)
|
||||
shouldSkip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldSkip {
|
||||
filteredResources = append(filteredResources, resource)
|
||||
}
|
||||
}
|
||||
Info("违禁词过滤后,剩余 %d 个资源需要转存", len(filteredResources))
|
||||
} else {
|
||||
filteredResources = resources
|
||||
}
|
||||
|
||||
// 并发自动转存
|
||||
resourceCh := make(chan *entity.Resource, len(resources))
|
||||
for _, res := range resources {
|
||||
resourceCh := make(chan *entity.Resource, len(filteredResources))
|
||||
for _, res := range filteredResources {
|
||||
resourceCh <- res
|
||||
}
|
||||
close(resourceCh)
|
||||
@@ -724,24 +819,34 @@ func (s *Scheduler) processAutoTransfer() {
|
||||
defer wg.Done()
|
||||
factory := panutils.GetInstance() // 使用单例模式
|
||||
for res := range resourceCh {
|
||||
if err := s.transferResource(res, []entity.Cks{acc}, config, factory); err != nil {
|
||||
if err := s.transferResource(res, []entity.Cks{acc}, factory); err != nil {
|
||||
Error("转存资源失败 (ID: %d): %v", res.ID, err)
|
||||
} else {
|
||||
Info("成功转存资源: %s", res.Title)
|
||||
rand.Seed(GetCurrentTime().UnixNano())
|
||||
sleepSec := rand.Intn(3) + 1 // 1,2,3
|
||||
time.Sleep(time.Duration(sleepSec) * time.Second)
|
||||
}
|
||||
}
|
||||
}(account)
|
||||
}
|
||||
wg.Wait()
|
||||
Info("自动转存处理完成,账号数: %d,资源数: %d", len(validAccounts), len(resources))
|
||||
Info("自动转存处理完成,账号数: %d,资源数: %d", len(validAccounts), len(filteredResources))
|
||||
}
|
||||
|
||||
// getResourcesForTransfer 获取需要转存的资源
|
||||
func (s *Scheduler) getResourcesForTransfer(config *entity.SystemConfig, quarkPanID uint) ([]*entity.Resource, error) {
|
||||
days := config.AutoTransferLimitDays
|
||||
func (s *Scheduler) getResourcesForTransfer(quarkPanID uint) ([]*entity.Resource, error) {
|
||||
// 获取自动转存限制天数配置
|
||||
autoTransferLimitDays, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoTransferLimitDays)
|
||||
if err != nil {
|
||||
Error("获取自动转存限制天数配置失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
days := autoTransferLimitDays
|
||||
var sinceTime time.Time
|
||||
if days > 0 {
|
||||
sinceTime = time.Now().AddDate(0, 0, -days)
|
||||
sinceTime = GetCurrentTime().AddDate(0, 0, -days)
|
||||
} else {
|
||||
sinceTime = time.Time{}
|
||||
}
|
||||
@@ -756,7 +861,7 @@ func (s *Scheduler) getResourcesForTransfer(config *entity.SystemConfig, quarkPa
|
||||
var resourceUpdateMutex sync.Mutex // 全局互斥锁,保证多协程安全
|
||||
|
||||
// transferResource 转存单个资源
|
||||
func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entity.Cks, config *entity.SystemConfig, factory *panutils.PanFactory) error {
|
||||
func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entity.Cks, factory *panutils.PanFactory) error {
|
||||
if len(accounts) == 0 {
|
||||
return fmt.Errorf("没有可用的网盘账号")
|
||||
}
|
||||
@@ -772,7 +877,23 @@ func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entit
|
||||
return fmt.Errorf("创建网盘服务失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取最小存储空间配置
|
||||
autoTransferMinSpace, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoTransferMinSpace)
|
||||
if err != nil {
|
||||
Error("获取最小存储空间配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查账号剩余空间
|
||||
minSpaceBytes := int64(autoTransferMinSpace) * 1024 * 1024 * 1024
|
||||
if account.LeftSpace < minSpaceBytes {
|
||||
return fmt.Errorf("账号剩余空间不足,需要 %d GB,当前剩余 %d GB", autoTransferMinSpace, account.LeftSpace/1024/1024/1024)
|
||||
}
|
||||
|
||||
// 提取分享ID
|
||||
shareID, _ := commonutils.ExtractShareIdString(resource.URL)
|
||||
|
||||
// 转存资源
|
||||
result, err := service.Transfer(shareID)
|
||||
if err != nil {
|
||||
resourceUpdateMutex.Lock()
|
||||
@@ -828,35 +949,34 @@ func (s *Scheduler) transferResource(resource *entity.Resource, accounts []entit
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectBestAccount 选择最佳网盘账号
|
||||
func (s *Scheduler) selectBestAccount(accounts []entity.Cks, config *entity.SystemConfig) *entity.Cks {
|
||||
// TODO: 实现账号选择逻辑
|
||||
// 1. 过滤出有效的账号
|
||||
// 2. 检查剩余空间是否满足最小要求
|
||||
// 3. 优先选择VIP账号
|
||||
// 4. 优先选择剩余空间大的账号
|
||||
// 5. 考虑账号的使用频率(避免单个账号过度使用)
|
||||
// selectBestAccount 选择最佳账号
|
||||
func (s *Scheduler) selectBestAccount(accounts []entity.Cks) *entity.Cks {
|
||||
if len(accounts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
minSpaceBytes := int64(config.AutoTransferMinSpace) * 1024 * 1024 * 1024 // 转换为字节
|
||||
// 获取最小存储空间配置
|
||||
autoTransferMinSpace, err := s.systemConfigRepo.GetConfigInt(entity.ConfigKeyAutoTransferMinSpace)
|
||||
if err != nil {
|
||||
Error("获取最小存储空间配置失败: %v", err)
|
||||
return &accounts[0] // 返回第一个账号
|
||||
}
|
||||
|
||||
minSpaceBytes := int64(autoTransferMinSpace) * 1024 * 1024 * 1024
|
||||
|
||||
var bestAccount *entity.Cks
|
||||
var maxScore int64 = -1
|
||||
var bestScore int64 = -1
|
||||
|
||||
for _, account := range accounts {
|
||||
if !account.IsValid {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查剩余空间
|
||||
for i := range accounts {
|
||||
account := &accounts[i]
|
||||
if account.LeftSpace < minSpaceBytes {
|
||||
continue
|
||||
continue // 跳过空间不足的账号
|
||||
}
|
||||
|
||||
// 计算账号评分
|
||||
score := s.calculateAccountScore(&account)
|
||||
if score > maxScore {
|
||||
maxScore = score
|
||||
bestAccount = &account
|
||||
score := s.calculateAccountScore(account)
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestAccount = account
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,3 +1009,128 @@ func (s *Scheduler) calculateAccountScore(account *entity.Cks) int64 {
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// 分割标签,支持中英文逗号
|
||||
func splitTags(tagStr string) []string {
|
||||
tagStr = strings.ReplaceAll(tagStr, ",", ",")
|
||||
return strings.Split(tagStr, ",")
|
||||
}
|
||||
|
||||
// 处理标签,返回所有标签ID
|
||||
func (s *Scheduler) handleTags(tagStr string) ([]uint, error) {
|
||||
if tagStr == "" {
|
||||
Debug("标签字符串为空,返回空数组")
|
||||
return []uint{}, nil // 返回空数组而不是 nil
|
||||
}
|
||||
|
||||
Debug("开始处理标签字符串: %s", tagStr)
|
||||
tagNames := splitTags(tagStr)
|
||||
Debug("分割后的标签名称: %v", tagNames)
|
||||
|
||||
var tagIDs []uint
|
||||
for _, name := range tagNames {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
Debug("跳过空标签名称")
|
||||
continue
|
||||
}
|
||||
|
||||
Debug("查找标签: %s", name)
|
||||
tag, err := s.tagRepo.FindByName(name)
|
||||
if err != nil {
|
||||
// 检查是否存在已删除的同名标签
|
||||
Debug("标签 %s 不存在,检查是否有已删除的同名标签", name)
|
||||
deletedTag, err2 := s.tagRepo.FindByNameIncludingDeleted(name)
|
||||
if err2 == nil && deletedTag.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名标签,则恢复它
|
||||
Debug("找到已删除的同名标签 %s,正在恢复", name)
|
||||
err2 = s.tagRepo.RestoreDeletedTag(deletedTag.ID)
|
||||
if err2 != nil {
|
||||
Error("恢复已删除标签 %s 失败: %v", name, err2)
|
||||
return nil, fmt.Errorf("恢复已删除标签 %s 失败: %v", name, err2)
|
||||
}
|
||||
tag = deletedTag
|
||||
Debug("成功恢复标签: %s (ID: %d)", name, tag.ID)
|
||||
} else {
|
||||
// 如果不存在已删除的同名标签,则创建新标签
|
||||
Debug("标签 %s 不存在,创建新标签", name)
|
||||
tag = &entity.Tag{Name: name}
|
||||
err2 = s.tagRepo.Create(tag)
|
||||
if err2 != nil {
|
||||
Error("创建标签 %s 失败: %v", name, err2)
|
||||
return nil, fmt.Errorf("创建标签 %s 失败: %v", name, err2)
|
||||
}
|
||||
Debug("成功创建标签: %s (ID: %d)", name, tag.ID)
|
||||
}
|
||||
} else {
|
||||
Debug("找到已存在的标签: %s (ID: %d)", name, tag.ID)
|
||||
}
|
||||
tagIDs = append(tagIDs, tag.ID)
|
||||
}
|
||||
|
||||
Debug("处理完成,标签ID列表: %v", tagIDs)
|
||||
return tagIDs, nil
|
||||
}
|
||||
|
||||
// 分类处理逻辑
|
||||
func (s *Scheduler) resolveCategory(categoryName string, tagIDs []uint) (*uint, error) {
|
||||
Debug("开始处理分类,分类名称: %s, 标签ID列表: %v", categoryName, tagIDs)
|
||||
|
||||
if categoryName != "" {
|
||||
Debug("查找分类: %s", categoryName)
|
||||
cat, err := s.categoryRepo.FindByName(categoryName)
|
||||
if err != nil {
|
||||
// 检查是否存在已删除的同名分类
|
||||
Debug("分类 %s 不存在,检查是否有已删除的同名分类", categoryName)
|
||||
deletedCat, err2 := s.categoryRepo.FindByNameIncludingDeleted(categoryName)
|
||||
if err2 == nil && deletedCat.DeletedAt.Valid {
|
||||
// 如果存在已删除的同名分类,则恢复它
|
||||
Debug("找到已删除的同名分类 %s,正在恢复", categoryName)
|
||||
err2 = s.categoryRepo.RestoreDeletedCategory(deletedCat.ID)
|
||||
if err2 != nil {
|
||||
Error("恢复已删除分类 %s 失败: %v", categoryName, err2)
|
||||
return nil, fmt.Errorf("恢复已删除分类 %s 失败: %v", categoryName, err2)
|
||||
}
|
||||
cat = deletedCat
|
||||
Debug("成功恢复分类: %s (ID: %d)", categoryName, cat.ID)
|
||||
} else {
|
||||
Debug("分类 %s 不存在: %v", categoryName, err)
|
||||
}
|
||||
}
|
||||
if cat != nil {
|
||||
Debug("找到分类: %s (ID: %d)", categoryName, cat.ID)
|
||||
return &cat.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 没有分类,尝试用标签反查
|
||||
if len(tagIDs) == 0 {
|
||||
Debug("没有标签,无法通过标签反查分类")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Debug("尝试通过标签反查分类")
|
||||
for _, tagID := range tagIDs {
|
||||
Debug("查找标签ID: %d", tagID)
|
||||
tag, err := s.tagRepo.GetByID(tagID)
|
||||
if err != nil {
|
||||
Debug("查找标签ID %d 失败: %v", tagID, err)
|
||||
continue
|
||||
}
|
||||
if tag != nil && tag.CategoryID != nil {
|
||||
Debug("通过标签 %s (ID: %d) 找到分类ID: %d", tag.Name, tagID, *tag.CategoryID)
|
||||
return tag.CategoryID, nil
|
||||
}
|
||||
}
|
||||
|
||||
Debug("未找到分类,返回nil")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 工具函数,解引用string指针
|
||||
func derefString(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
55
utils/timezone.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InitTimezone 初始化时区设置
|
||||
func InitTimezone() {
|
||||
// 从环境变量获取时区配置
|
||||
timezone := os.Getenv("TIMEZONE")
|
||||
if timezone == "" {
|
||||
// 默认使用上海时间
|
||||
timezone = "Asia/Shanghai"
|
||||
Info("未配置时区,使用默认时区: %s", timezone)
|
||||
} else {
|
||||
Info("使用配置的时区: %s", timezone)
|
||||
}
|
||||
|
||||
// 设置时区
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
Error("加载时区失败: %v,使用系统默认时区", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置全局时区
|
||||
time.Local = loc
|
||||
Info("时区设置成功: %s", timezone)
|
||||
}
|
||||
|
||||
// GetCurrentTime 获取当前时间(使用配置的时区)
|
||||
func GetCurrentTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// GetCurrentTimeString 获取当前时间字符串(使用配置的时区)
|
||||
func GetCurrentTimeString() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// GetCurrentTimeRFC3339 获取当前时间RFC3339格式(使用配置的时区)
|
||||
func GetCurrentTimeRFC3339() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// ParseTime 解析时间字符串(使用配置的时区)
|
||||
func ParseTime(timeStr string) (time.Time, error) {
|
||||
return time.Parse("2006-01-02 15:04:05", timeStr)
|
||||
}
|
||||
|
||||
// FormatTime 格式化时间(使用配置的时区)
|
||||
func FormatTime(t time.Time, layout string) string {
|
||||
return t.Format(layout)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ type VersionInfo struct {
|
||||
// 编译时注入的版本信息
|
||||
var (
|
||||
Version = getVersionFromFile()
|
||||
BuildTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
BuildTime = GetCurrentTimeString()
|
||||
GitCommit = "unknown"
|
||||
GitBranch = "unknown"
|
||||
)
|
||||
@@ -40,7 +40,7 @@ func getVersionFromFile() string {
|
||||
|
||||
// GetVersionInfo 获取版本信息
|
||||
func GetVersionInfo() *VersionInfo {
|
||||
buildTime, _ := time.Parse("2006-01-02 15:04:05", BuildTime)
|
||||
buildTime, _ := ParseTime(BuildTime)
|
||||
|
||||
return &VersionInfo{
|
||||
Version: Version,
|
||||
@@ -72,7 +72,7 @@ func GetFullVersionInfo() string {
|
||||
Node版本: %s
|
||||
平台: %s/%s`,
|
||||
info.Version,
|
||||
info.BuildTime.Format("2006-01-02 15:04:05"),
|
||||
FormatTime(info.BuildTime, "2006-01-02 15:04:05"),
|
||||
info.GitCommit,
|
||||
info.GitBranch,
|
||||
info.GoVersion,
|
||||
|
||||
2
web/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
NUXT_PUBLIC_API_CLIENT=http://localhost:8080/api
|
||||
NUXT_PUBLIC_API_SERVER=http://localhost:8080/api
|
||||
BIN
web/assets/images/3dian.png
Normal file
|
After Width: | Height: | Size: 481 B |
BIN
web/assets/images/android.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
web/assets/images/banner.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
web/assets/images/banner.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/images/footer-banner.webp
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web/assets/images/iphone.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
6
web/components.d.ts
vendored
@@ -11,10 +11,12 @@ declare module 'vue' {
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 text-center relative">
|
||||
<div class="header-container bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 text-center relative">
|
||||
<!-- 页面标题和面包屑 -->
|
||||
<div class="mb-4">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-2">
|
||||
@@ -150,4 +150,12 @@ const logout = async () => {
|
||||
|
||||
<style scoped>
|
||||
/* 确保样式与首页完全一致 */
|
||||
.header-container {
|
||||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
|
||||
<footer class="footer-container mt-auto py-6 border-t border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div class="max-w-7xl mx-auto text-center text-gray-400 text-sm px-3 sm:px-5">
|
||||
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||
<p class="flex items-center justify-center gap-2">
|
||||
<span>{{ systemConfig?.copyright || '© 2025 老九网盘资源数据库 By 老九' }}</span>
|
||||
@@ -35,4 +35,15 @@ const systemConfig = computed(() => (systemConfigData.value as any) || { copyrig
|
||||
onMounted(() => {
|
||||
fetchVersionInfo()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.footer-container{
|
||||
background: url(/assets/images/footer-banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -4,10 +4,11 @@
|
||||
<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>
|
||||
<p class="mb-2"><strong>格式要求:</strong>标题和URL为一组,标题必填, 同一标题URL支持多行</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
电影标题1
|
||||
电影1
|
||||
https://pan.baidu.com/s/123456
|
||||
https://pan.quark.com/s/123456
|
||||
电影标题2
|
||||
https://pan.baidu.com/s/789012
|
||||
电视剧标题3
|
||||
@@ -20,9 +21,9 @@ https://pan.quark.cn/s/345678</pre>
|
||||
</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>
|
||||
<n-input v-model="batchInput" type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
placeholder="请输入资源内容,格式:标题和URL为一组..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,30 +60,11 @@ const validateInput = () => {
|
||||
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)
|
||||
} catch {
|
||||
throw new Error(`第${i + 2}行URL格式无效: ${url}`)
|
||||
}
|
||||
// 首行必须为标题
|
||||
if (/^https?:\/\//i.test(lines[0])) {
|
||||
// 你可以用 alert、ElMessage 或其它方式提示
|
||||
alert('首行必须为标题,不能为链接!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,14 +78,30 @@ const handleSubmit = async () => {
|
||||
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]
|
||||
let currentTitle = ''
|
||||
let currentUrls = []
|
||||
|
||||
for (const line of lines) {
|
||||
// 判断是否为 url(以 http/https 开头)
|
||||
if (/^https?:\/\//i.test(line)) {
|
||||
currentUrls.push(line)
|
||||
} else {
|
||||
// 新标题,先保存上一个
|
||||
if (currentTitle && currentUrls.length) {
|
||||
resources.push({
|
||||
title: currentTitle,
|
||||
url: currentUrls.slice()
|
||||
})
|
||||
}
|
||||
currentTitle = line
|
||||
currentUrls = []
|
||||
}
|
||||
}
|
||||
// 处理最后一组
|
||||
if (currentTitle && currentUrls.length) {
|
||||
resources.push({
|
||||
title: title,
|
||||
url: url,
|
||||
source: '批量添加'
|
||||
title: currentTitle,
|
||||
url: currentUrls.slice()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div v-if="show" class="fixed top-4 right-4 z-50">
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg max-w-sm">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-exclamation-circle text-red-400"></i>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h3 class="text-sm font-medium text-red-800">错误</h3>
|
||||
<div class="mt-1 text-sm text-red-700">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<button
|
||||
@click="close"
|
||||
class="inline-flex text-red-400 hover:text-red-600 focus:outline-none"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
message: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 5000
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const show = ref(false)
|
||||
|
||||
const close = () => {
|
||||
show.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
show.value = true
|
||||
if (props.duration > 0) {
|
||||
setTimeout(() => {
|
||||
close()
|
||||
}, props.duration)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
121
web/components/ForbiddenPage.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="forbidden-page">
|
||||
<div class="top-bar-guidance">
|
||||
<p class="top-bar-guidance-text">请按提示在手机 浏览器 打开<img src="/assets/images/3dian.png" class="icon-safari"></p>
|
||||
<p class="top-bar-guidance-text">苹果设备<img src="/assets/images/iphone.png" class="icon-safari">↗↗↗</p>
|
||||
<p class="top-bar-guidance-text">安卓设备<img src="/assets/images/android.png" class="icon-safari">↗↗↗</p>
|
||||
</div>
|
||||
|
||||
<div id="contens">
|
||||
<p><br/><br/></p>
|
||||
<p>1.本站不支持 微信,QQ等APP 内访问</p>
|
||||
<p><br/></p>
|
||||
<p>2.请按提示在手机 浏览器 打开</p>
|
||||
<p id="device-tip"><br/>3.请在浏览器中打开</p>
|
||||
</div>
|
||||
|
||||
<p><br/><br/></p>
|
||||
<div class="app-download-tip">
|
||||
<span class="guidance-desc" id="current-url"></span>
|
||||
</div>
|
||||
<p><br/></p>
|
||||
<div class="app-download-tip">
|
||||
<span class="guidance-desc">点击右上角···图标 or 复制网址自行打开</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
// 组件属性
|
||||
interface Props {
|
||||
currentUrl?: string
|
||||
isIOS?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
currentUrl: '',
|
||||
isIOS: false
|
||||
})
|
||||
|
||||
// 在组件挂载时设置内容
|
||||
onMounted(() => {
|
||||
const currentUrlElement = document.getElementById('current-url')
|
||||
const deviceTipElement = document.getElementById('device-tip')
|
||||
|
||||
if (currentUrlElement) {
|
||||
currentUrlElement.textContent = props.currentUrl || window.location.href
|
||||
}
|
||||
|
||||
if (deviceTipElement) {
|
||||
const deviceText = props.isIOS ? '苹果设备请在Safari浏览器中打开' : '安卓设备请在Chrome或其他浏览器中打开'
|
||||
deviceTipElement.innerHTML = `<br/>3.${deviceText}`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forbidden-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.top-bar-guidance {
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
height: 70%;
|
||||
line-height: 1.2;
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
background: url('/assets/images/banner.png') center top/cover no-repeat;
|
||||
}
|
||||
|
||||
.top-bar-guidance p {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.top-bar-guidance .icon-safari {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
margin: 0 .2em;
|
||||
}
|
||||
|
||||
.top-bar-guidance-text {
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
word-wrap: nowrap;
|
||||
}
|
||||
|
||||
.top-bar-guidance-text img {
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
margin: 0 .2em;
|
||||
}
|
||||
|
||||
#contens {
|
||||
font-weight: bold;
|
||||
color: #2466f4;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 125px;
|
||||
}
|
||||
|
||||
.app-download-tip {
|
||||
margin: 0 auto;
|
||||
width: 290px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #2466f4;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcAQMAAACak0ePAAAABlBMVEUAAAAdYfh+GakkAAAAAXRSTlMAQObYZgAAAA5JREFUCNdjwA8acEkAAAy4AIE4hQq/AAAAAElFTkSuQmCC) left center/auto 15px repeat-x;
|
||||
}
|
||||
|
||||
.app-download-tip .guidance-desc {
|
||||
background-color: #fff;
|
||||
padding: 0 5px;
|
||||
}
|
||||
</style>
|
||||
51
web/components/LoadingState.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">{{ message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 组件属性
|
||||
interface Props {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
message: '正在检测访问环境...'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: url('/assets/images/banner.webp') center / cover no-repeat;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 4px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -96,14 +96,16 @@ import QRCode from 'qrcode'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
url: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
url: ''
|
||||
})
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const qrCanvas = ref<HTMLCanvasElement>()
|
||||
@@ -168,7 +170,9 @@ const copyUrl = async () => {
|
||||
|
||||
// 跳转到链接
|
||||
const openLink = () => {
|
||||
window.open(props.url, '_blank')
|
||||
if (process.client) {
|
||||
window.open(props.url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载二维码
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full mx-4" style="height:600px;">
|
||||
<div class="p-6 h-full flex flex-col text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
添加资源
|
||||
</h2>
|
||||
<button @click="$emit('close')" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex mb-6 border-b flex-shrink-0">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:class="['px-4 py-2 -mb-px border-b-2', mode === tab.value ? 'border-blue-500 text-blue-600 font-bold' : 'border-transparent text-gray-500']"
|
||||
@click="mode = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- 批量添加 -->
|
||||
<div v-if="mode === 'batch'">
|
||||
<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>格式1:</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</pre>
|
||||
<p class="mt-2 mb-2"><strong>格式2:</strong>只有URL,系统自动判断</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
https://pan.baidu.com/s/123456
|
||||
https://pan.baidu.com/s/789012
|
||||
https://pan.baidu.com/s/345678</pre>
|
||||
</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="请输入资源内容,支持两种格式..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 单个添加 -->
|
||||
<div v-else-if="mode === 'single'" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标题</label>
|
||||
<input v-model="form.title" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入标题" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述</label>
|
||||
<textarea v-model="form.description" rows="3" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入资源描述"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">类型</label>
|
||||
<select v-model="form.file_type" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700">
|
||||
<option value="">选择类型</option>
|
||||
<option value="pan">网盘</option>
|
||||
<option value="link">直链</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标签</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
<span v-for="tag in form.tags" :key="tag" class="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs flex items-center">
|
||||
{{ tag }}
|
||||
<button type="button" class="ml-1 text-xs" @click="removeTag(tag)">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<input v-model="newTag" @keyup.enter.prevent="addTag" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="输入标签后回车添加" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">链接(可多行,每行一个链接)</label>
|
||||
<textarea v-model="form.url" rows="3" class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700" placeholder="https://a.com https://b.com"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API说明 -->
|
||||
<div v-else class="space-y-4">
|
||||
<div class="text-gray-700 dark:text-gray-300 text-sm">
|
||||
<p>你可以通过API批量添加资源:</p>
|
||||
<pre class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs overflow-x-auto mt-2">
|
||||
POST /api/resources/batch
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
[
|
||||
{ "title": "资源A", "url": "https://a.com", "file_type": "pan", ... },
|
||||
{ "title": "资源B", "url": "https://b.com", ... }
|
||||
]
|
||||
</pre>
|
||||
<p>参数说明:<br/>
|
||||
title: 标题<br/>
|
||||
url: 资源链接<br/>
|
||||
file_type: 类型(pan/link/other)<br/>
|
||||
tags: 标签数组(可选)<br/>
|
||||
description: 描述(可选)<br/>
|
||||
... 其他字段参考文档
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功/失败提示 -->
|
||||
<SuccessToast v-if="showSuccess" :message="successMsg" @close="showSuccess = false" />
|
||||
<ErrorToast v-if="showError" :message="errorMsg" @close="showError = false" />
|
||||
</div>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<div class="flex-shrink-0 pt-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/90 sticky bottom-0 left-0 w-full flex justify-end space-x-3 z-10 backdrop-blur">
|
||||
<template v-if="mode === 'batch'">
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">取消</button>
|
||||
<button type="button" @click="handleBatchSubmit" class="btn-primary" :disabled="loading">
|
||||
{{ loading ? '保存中...' : '批量添加' }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="mode === 'single'">
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">取消</button>
|
||||
<button type="button" @click="handleSingleSubmit" class="btn-primary" :disabled="loading">
|
||||
{{ loading ? '保存中...' : '添加' }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button type="button" @click="$emit('close')" class="btn-secondary">关闭</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useResourceStore } from '~/stores/resource'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import SuccessToast from './SuccessToast.vue'
|
||||
import ErrorToast from './ErrorToast.vue'
|
||||
import { useReadyResourceApi } from '~/composables/useApi'
|
||||
|
||||
const store = useResourceStore()
|
||||
const { categories } = storeToRefs(store)
|
||||
|
||||
const props = defineProps<{ resource?: any }>()
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const loading = ref(false)
|
||||
const newTag = ref('')
|
||||
const showSuccess = ref(false)
|
||||
const showError = ref(false)
|
||||
const successMsg = ref('')
|
||||
const errorMsg = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ label: '批量添加', value: 'batch' },
|
||||
{ label: '单个添加', value: 'single' },
|
||||
{ label: 'API说明', value: 'api' },
|
||||
]
|
||||
const mode = ref('batch')
|
||||
|
||||
// 批量添加
|
||||
const batchInput = ref('')
|
||||
|
||||
// 单个添加表单
|
||||
const form = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
url: '', // 多行
|
||||
category_id: '',
|
||||
tags: [] as string[],
|
||||
file_path: '',
|
||||
file_type: '',
|
||||
file_size: 0,
|
||||
is_public: true,
|
||||
})
|
||||
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.resource) {
|
||||
form.value = {
|
||||
title: props.resource.title || '',
|
||||
description: props.resource.description || '',
|
||||
url: props.resource.url || '',
|
||||
category_id: props.resource.category_id || '',
|
||||
tags: [...(props.resource.tags || [])],
|
||||
file_path: props.resource.file_path || '',
|
||||
file_type: props.resource.file_type || '',
|
||||
file_size: props.resource.file_size || 0,
|
||||
is_public: props.resource.is_public !== false,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const addTag = () => {
|
||||
const tag = newTag.value.trim()
|
||||
if (tag && !form.value.tags.includes(tag)) {
|
||||
form.value.tags.push(tag)
|
||||
newTag.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
const index = form.value.tags.indexOf(tag)
|
||||
if (index > -1) {
|
||||
form.value.tags.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量添加提交
|
||||
const handleBatchSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!batchInput.value.trim()) throw new Error('请输入资源内容')
|
||||
const res: any = await readyResourceApi.createReadyResourcesFromText(batchInput.value)
|
||||
showSuccess.value = true
|
||||
successMsg.value = `成功添加 ${res.count || 0} 个资源,资源已进入待处理列表,处理完成后会自动入库`
|
||||
batchInput.value = ''
|
||||
} catch (e: any) {
|
||||
showError.value = true
|
||||
errorMsg.value = e.message || '批量添加失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 单个添加提交
|
||||
const handleSingleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 多行链接
|
||||
const urls = form.value.url.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
|
||||
if (!urls.length) throw new Error('请输入至少一个链接')
|
||||
for (const url of urls) {
|
||||
await store.createResource({
|
||||
...form.value,
|
||||
url,
|
||||
tags: [...form.value.tags],
|
||||
})
|
||||
}
|
||||
showSuccess.value = true
|
||||
successMsg.value = '资源已进入待处理列表,处理完成后会自动入库'
|
||||
// 清空表单
|
||||
form.value.title = ''
|
||||
form.value.description = ''
|
||||
form.value.url = ''
|
||||
form.value.tags = []
|
||||
form.value.file_type = ''
|
||||
} catch (e: any) {
|
||||
showError.value = true
|
||||
errorMsg.value = e.message || '添加失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
</style>
|
||||
@@ -5,9 +5,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
标题 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.title"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="输入资源标题(必填)"
|
||||
required
|
||||
/>
|
||||
@@ -18,12 +17,11 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
描述 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="输入资源描述,如:剧情简介、文件大小、清晰度等"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
@@ -31,15 +29,14 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
URL <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.url"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="请输入资源链接,支持多行,每行一个链接"
|
||||
required
|
||||
></textarea>
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
支持百度网盘、阿里云盘、夸克网盘等链接
|
||||
支持百度网盘、阿里云盘、夸克网盘等链接,每行一个链接
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -48,9 +45,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
分类 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.category"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="如:电影、电视剧、动漫、音乐等"
|
||||
/>
|
||||
</div>
|
||||
@@ -76,10 +72,9 @@
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
<n-input
|
||||
v-model="newTag"
|
||||
@keyup.enter.prevent="addTag"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="输入标签后回车添加,多个标签用逗号分隔"
|
||||
/>
|
||||
</div>
|
||||
@@ -89,9 +84,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
封面图片 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.img"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="封面图片链接"
|
||||
/>
|
||||
</div>
|
||||
@@ -101,9 +95,8 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
数据来源 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
<n-input
|
||||
v-model="form.source"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
placeholder="如:手动添加、API导入、爬虫等"
|
||||
/>
|
||||
</div>
|
||||
@@ -113,12 +106,11 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
额外数据 <span class="text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<textarea
|
||||
<n-input
|
||||
v-model="form.extra"
|
||||
rows="3"
|
||||
class="input-field dark:bg-gray-900 dark:text-gray-100 dark:border-gray-700"
|
||||
type="textarea"
|
||||
placeholder="JSON格式的额外数据,如:{'size': '2GB', 'quality': '1080p'}"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
@@ -205,7 +197,7 @@ const clearForm = () => {
|
||||
newTag.value = ''
|
||||
}
|
||||
|
||||
// 单个添加提交
|
||||
// 单个添加提交 - 更新为使用批量添加方法
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -214,23 +206,23 @@ const handleSubmit = async () => {
|
||||
// 多行链接处理
|
||||
const urls = form.value.url.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
|
||||
|
||||
// 为每个URL创建一个资源
|
||||
for (const url of urls) {
|
||||
const resourceData = {
|
||||
title: form.value.title, // 标题必填
|
||||
description: form.value.description || undefined, // 添加描述
|
||||
url: url,
|
||||
// 使用批量添加方法,将多个URL作为一个资源的多个链接
|
||||
const resourceData = {
|
||||
resources: [{
|
||||
title: form.value.title || undefined, // 后端期望 *string 类型
|
||||
description: form.value.description || '',
|
||||
url: urls, // 现在 url 是一个数组
|
||||
category: form.value.category || '',
|
||||
tags: form.value.tags.join(','), // 转换为逗号分隔的字符串
|
||||
img: form.value.img || '',
|
||||
source: form.value.source || '手动添加',
|
||||
extra: form.value.extra || '',
|
||||
}
|
||||
|
||||
await readyResourceApi.createReadyResource(resourceData)
|
||||
}]
|
||||
}
|
||||
|
||||
emit('success', `成功添加 ${urls.length} 个资源到待处理列表`)
|
||||
const response = await readyResourceApi.batchCreateReadyResources(resourceData)
|
||||
|
||||
emit('success', `成功添加资源,包含 ${urls.length} 个链接`)
|
||||
clearForm()
|
||||
} catch (e: any) {
|
||||
emit('error', e.message || '添加失败')
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div v-if="show" class="fixed top-4 right-4 z-50">
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg max-w-sm">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-check-circle text-green-400"></i>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h3 class="text-sm font-medium text-green-800">成功</h3>
|
||||
<div class="mt-1 text-sm text-green-700">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0">
|
||||
<button
|
||||
@click="close"
|
||||
class="inline-flex text-green-400 hover:text-green-600 focus:outline-none"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
message: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const show = ref(false)
|
||||
|
||||
const close = () => {
|
||||
show.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
show.value = true
|
||||
if (props.duration > 0) {
|
||||
setTimeout(() => {
|
||||
close()
|
||||
}, props.duration)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -18,6 +18,11 @@ export const parseApiResponse = <T>(response: any): T => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是包含items字段的响应格式(如分类接口)
|
||||
if (response && typeof response === 'object' && 'items' in response) {
|
||||
return response
|
||||
}
|
||||
|
||||
// 检查是否是包含success字段的响应格式(如登录接口)
|
||||
if (response && typeof response === 'object' && 'success' in response && 'data' in response) {
|
||||
if (response.success) {
|
||||
@@ -66,7 +71,7 @@ export const useAuthApi = () => {
|
||||
}
|
||||
|
||||
export const useCategoryApi = () => {
|
||||
const getCategories = () => useApiFetch('/categories').then(parseApiResponse)
|
||||
const getCategories = (params?: any) => useApiFetch('/categories', { params }).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)
|
||||
@@ -93,7 +98,7 @@ export const useCksApi = () => {
|
||||
}
|
||||
|
||||
export const useTagApi = () => {
|
||||
const getTags = () => useApiFetch('/tags').then(parseApiResponse)
|
||||
const getTags = (params?: any) => useApiFetch('/tags', { params }).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)
|
||||
@@ -105,6 +110,7 @@ export const useTagApi = () => {
|
||||
|
||||
export const useReadyResourceApi = () => {
|
||||
const getReadyResources = (params?: any) => useApiFetch('/ready-resources', { params }).then(parseApiResponse)
|
||||
const getFailedResources = (params?: any) => useApiFetch('/ready-resources/errors', { 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) => {
|
||||
@@ -114,7 +120,19 @@ export const useReadyResourceApi = () => {
|
||||
}
|
||||
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 }
|
||||
const clearErrorMsg = (id: number) => useApiFetch(`/ready-resources/${id}/clear-error`, { method: 'POST' }).then(parseApiResponse)
|
||||
const retryFailedResources = () => useApiFetch('/ready-resources/retry-failed', { method: 'POST' }).then(parseApiResponse)
|
||||
return {
|
||||
getReadyResources,
|
||||
getFailedResources,
|
||||
createReadyResource,
|
||||
batchCreateReadyResources,
|
||||
createReadyResourcesFromText,
|
||||
deleteReadyResource,
|
||||
clearReadyResources,
|
||||
clearErrorMsg,
|
||||
retryFailedResources
|
||||
}
|
||||
}
|
||||
|
||||
export const useStatsApi = () => {
|
||||
@@ -125,7 +143,8 @@ export const useStatsApi = () => {
|
||||
export const useSystemConfigApi = () => {
|
||||
const getSystemConfig = () => useApiFetch('/system/config').then(parseApiResponse)
|
||||
const updateSystemConfig = (data: any) => useApiFetch('/system/config', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
return { getSystemConfig, updateSystemConfig }
|
||||
const toggleAutoProcess = (enabled: boolean) => useApiFetch('/system/config/toggle-auto-process', { method: 'POST', body: { auto_process_ready_resources: enabled } }).then(parseApiResponse)
|
||||
return { getSystemConfig, updateSystemConfig, toggleAutoProcess }
|
||||
}
|
||||
|
||||
export const useHotDramaApi = () => {
|
||||
|
||||
@@ -22,12 +22,27 @@ export function useApiFetch<T = any>(
|
||||
...options,
|
||||
headers,
|
||||
onResponse({ response }) {
|
||||
if (response.status === 401 ||
|
||||
(response._data && (response._data.code === 401 || response._data.error === '无效的令牌'))
|
||||
) {
|
||||
userStore.logout()
|
||||
if (process.client) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
// 触发 onResponseError 逻辑
|
||||
throw Object.assign(new Error('登录已过期,请重新登录'), {
|
||||
data: response._data,
|
||||
status: response.status,
|
||||
})
|
||||
}
|
||||
|
||||
// 统一处理 code/message
|
||||
if (response._data && response._data.code && response._data.code !== 200) {
|
||||
throw new Error(response._data.message || '请求失败')
|
||||
}
|
||||
},
|
||||
onResponseError({ error }: { error: any }) {
|
||||
console.log('error', error)
|
||||
// 检查是否为"无效的令牌"错误
|
||||
if (error?.data?.error === '无效的令牌') {
|
||||
// 清除用户状态
|
||||
|
||||
@@ -18,7 +18,7 @@ interface VersionResponse {
|
||||
|
||||
export const useVersion = () => {
|
||||
const versionInfo = ref<VersionInfo>({
|
||||
version: '1.0.8',
|
||||
version: '1.0.10',
|
||||
build_time: '',
|
||||
git_commit: 'unknown',
|
||||
git_branch: 'unknown',
|
||||
|
||||
@@ -18,12 +18,17 @@
|
||||
<AdminHeader :title="pageTitle" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 页面内容插槽 -->
|
||||
<slot />
|
||||
<ClientOnly>
|
||||
<n-notification-provider>
|
||||
<n-dialog-provider>
|
||||
<!-- 页面内容插槽 -->
|
||||
<slot />
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
27
web/layouts/single.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="single-layout">
|
||||
<n-notification-provider>
|
||||
<slot />
|
||||
</n-notification-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.single-layout {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
132
web/middleware/ua.global.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { defineNuxtRouteMiddleware, useRequestEvent } from 'nuxt/app'
|
||||
import { getHeader, getRequestURL, setResponseStatus, setResponseHeader, send } from 'h3'
|
||||
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// 只在服务端执行
|
||||
if (!process.server) return
|
||||
|
||||
const event = useRequestEvent()
|
||||
if (!event) return
|
||||
|
||||
const userAgent = getHeader(event, 'user-agent') || ''
|
||||
const isForbiddenApp = ['QQ/', 'MicroMessenger', 'WeiBo', 'DingTalk', 'Mail'].some(it => userAgent.includes(it))
|
||||
|
||||
if (isForbiddenApp) {
|
||||
// 获取当前 URL
|
||||
const currentUrl = getRequestURL(event).href
|
||||
|
||||
// 设置响应头
|
||||
setResponseStatus(event, 200)
|
||||
setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8')
|
||||
|
||||
// 直接返回 HTML 响应
|
||||
return send(event, generateForbiddenPage(currentUrl, userAgent))
|
||||
}
|
||||
})
|
||||
|
||||
// 生成禁止访问页面的函数
|
||||
function generateForbiddenPage(url: string, userAgent: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>请在浏览器中打开</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.forbidden-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.top-bar-guidance {
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
height: 70%;
|
||||
line-height: 1.2;
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
background: url('/assets/images/banner.png') center right/cover no-repeat;
|
||||
}
|
||||
.top-bar-guidance p {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.top-bar-guidance .icon-safari {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
margin: 0 .2em;
|
||||
}
|
||||
.top-bar-guidance-text {
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
word-wrap: nowrap;
|
||||
}
|
||||
.top-bar-guidance-text img {
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
margin: 0 .2em;
|
||||
}
|
||||
#contens {
|
||||
font-weight: bold;
|
||||
color: #2466f4;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 125px;
|
||||
}
|
||||
.app-download-tip {
|
||||
margin: 0 auto;
|
||||
width: 290px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #2466f4;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcAQMAAACak0ePAAAABlBMVEUAAAAdYfh+GakkAAAAAXRSTlMAQObYZgAAAA5JREFUCNdjwA8acEkAAAy4AIE4hQq/AAAAAElFTkSuQmCC) left center/auto 15px repeat-x;
|
||||
}
|
||||
.app-download-tip .guidance-desc {
|
||||
background-color: #fff;
|
||||
padding: 0 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="forbidden-page">
|
||||
<div class="top-bar-guidance">
|
||||
<p class="top-bar-guidance-text">请按提示在手机 浏览器 打开<img src="/assets/images/3dian.png" class="icon-safari"></p>
|
||||
<p class="top-bar-guidance-text">苹果设备<img src="/assets/images/iphone.png" class="icon-safari">↗↗↗</p>
|
||||
<p class="top-bar-guidance-text">安卓设备<img src="/assets/images/android.png" class="icon-safari">↗↗↗</p>
|
||||
</div>
|
||||
|
||||
<div id="contens">
|
||||
<p><br/><br/></p>
|
||||
<p>1.本站不支持 微信,QQ等APP 内访问</p>
|
||||
<p><br/></p>
|
||||
<p>2.请按提示在手机 浏览器 打开</p>
|
||||
<p id="device-tip"><br/>3.请在浏览器中打开</p>
|
||||
</div>
|
||||
|
||||
<p><br/><br/></p>
|
||||
<div class="app-download-tip">
|
||||
<span class="guidance-desc" id="current-url">${url}</span>
|
||||
</div>
|
||||
<p><br/></p>
|
||||
<div class="app-download-tip">
|
||||
<span class="guidance-desc">点击右上角···图标 or 复制网址自行打开</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineNuxtConfig({
|
||||
})
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: ['naive-ui', 'vueuc', 'date-fns'],
|
||||
include: ['vueuc', 'date-fns'],
|
||||
exclude: ["oxc-parser"] // 强制使用 WASM 版本
|
||||
}
|
||||
},
|
||||
@@ -51,8 +51,10 @@ export default defineNuxtConfig({
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: process.env.NUXT_PUBLIC_API_CLIENT || 'http://localhost:8080/api',
|
||||
apiServer: process.env.NUXT_PUBLIC_API_SERVER || 'http://localhost:8080/api'
|
||||
// 开发环境:直接访问后端,生产环境:通过 Nginx 反代
|
||||
apiBase: process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8080/api',
|
||||
// 服务端:开发环境直接访问,生产环境容器内访问
|
||||
apiServer: process.env.NODE_ENV === 'production' ? 'http://backend:8080/api' : 'http://localhost:8080/api'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "res-db-web",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -43,38 +43,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功/失败提示 -->
|
||||
<SuccessToast v-if="showSuccess" :message="successMsg" @close="showSuccess = false" />
|
||||
<ErrorToast v-if="showError" :message="errorMsg" @close="showError = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BatchAddResource from '~/components/BatchAddResource.vue'
|
||||
import SingleAddResource from '~/components/SingleAddResource.vue'
|
||||
import ApiDocumentation from '~/components/ApiDocumentation.vue'
|
||||
import SuccessToast from '~/components/SuccessToast.vue'
|
||||
import ErrorToast from '~/components/ErrorToast.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const showSuccess = ref(false)
|
||||
const successMsg = ref('')
|
||||
const showError = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ label: '批量添加', value: 'batch' },
|
||||
{ label: '单个添加', value: 'single' },
|
||||
]
|
||||
const mode = ref('batch')
|
||||
const notification = useNotification()
|
||||
|
||||
// 检查用户权限
|
||||
onMounted(() => {
|
||||
@@ -87,13 +78,17 @@ onMounted(() => {
|
||||
|
||||
// 事件处理
|
||||
const handleSuccess = (message: string) => {
|
||||
successMsg.value = message
|
||||
showSuccess.value = true
|
||||
notification.success({
|
||||
content: message,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
|
||||
const handleError = (message: string) => {
|
||||
errorMsg.value = message
|
||||
showError.value = true
|
||||
notification.error({
|
||||
content: message,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
|
||||
@@ -3,24 +3,21 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button @click="showAddModal = true"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-white text-sm flex items-center gap-2">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</button>
|
||||
<n-button @click="showAddModal = true" type="success">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative">
|
||||
<input v-model="searchQuery" @keyup="debounceSearch" type="text"
|
||||
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
|
||||
<n-input v-model:value="searchQuery" @input="debounceSearch" type="text"
|
||||
placeholder="搜索分类名称..." />
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2">
|
||||
<n-button @click="refreshData" type="tertiary">
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,10 +51,9 @@
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无分类</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加分类"按钮创建新分类</div>
|
||||
<button @click="showAddModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2">
|
||||
<n-button @click="showAddModal = true" type="primary">
|
||||
<i class="fas fa-plus"></i> 添加分类
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -85,16 +81,12 @@
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="editCategory(category)"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="编辑分类">
|
||||
<n-button @click="editCategory(category)" type="info" size="small">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button @click="deleteCategory(category.id)"
|
||||
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="删除分类">
|
||||
</n-button>
|
||||
<n-button @click="deleteCategory(category.id)" type="error" size="small">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -152,36 +144,31 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ editingCategory ? '编辑分类' : '添加分类' }}
|
||||
</h3>
|
||||
<button @click="closeModal"
|
||||
class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<n-button @click="closeModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">分类名称:</label>
|
||||
<input v-model="formData.name" type="text" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
<n-input v-model:value="formData.name" type="text" required
|
||||
placeholder="请输入分类名称" />
|
||||
</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="formData.description" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
placeholder="请输入分类描述(可选)"></textarea>
|
||||
<n-input v-model:value="formData.description" type="textarea"
|
||||
placeholder="请输入分类描述(可选)" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" @click="closeModal"
|
||||
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
|
||||
<n-button type="tertiary" @click="closeModal">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="submitting"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
</n-button>
|
||||
<n-button type="primary" :disabled="submitting" @click="handleSubmit">
|
||||
{{ submitting ? '提交中...' : (editingCategory ? '更新' : '添加') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -192,7 +179,8 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
@@ -220,6 +208,7 @@ let searchTimeout: NodeJS.Timeout | null = null
|
||||
const showAddModal = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingCategory = ref(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
@@ -259,12 +248,37 @@ const fetchCategories = async () => {
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value
|
||||
}
|
||||
console.log('获取分类列表参数:', params)
|
||||
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)
|
||||
console.log('分类接口响应:', response)
|
||||
console.log('响应类型:', typeof response)
|
||||
console.log('响应是否为数组:', Array.isArray(response))
|
||||
|
||||
// 适配后端API响应格式
|
||||
if (response && response.items) {
|
||||
console.log('使用 items 格式:', response.items)
|
||||
categories.value = response.items
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} else if (Array.isArray(response)) {
|
||||
console.log('使用数组格式:', response)
|
||||
// 兼容旧格式
|
||||
categories.value = response
|
||||
totalCount.value = response.length
|
||||
totalPages.value = 1
|
||||
} else {
|
||||
console.log('使用默认格式:', response)
|
||||
categories.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
}
|
||||
console.log('最终分类数据:', categories.value)
|
||||
console.log('分类数据长度:', categories.value.length)
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
categories.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -294,36 +308,61 @@ const goToPage = (page: number) => {
|
||||
|
||||
// 编辑分类
|
||||
const editCategory = (category: any) => {
|
||||
console.log('编辑分类:', category)
|
||||
editingCategory.value = category
|
||||
formData.value = {
|
||||
name: category.name,
|
||||
description: category.description || ''
|
||||
}
|
||||
console.log('设置表单数据:', formData.value)
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
const deleteCategory = async (categoryId: number) => {
|
||||
if (!confirm(`确定要删除分类吗?`)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await categoryApi.deleteCategory(categoryId)
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除分类吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await categoryApi.deleteCategory(categoryId)
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
let response: any
|
||||
if (editingCategory.value) {
|
||||
await categoryApi.updateCategory(editingCategory.value.id, formData.value)
|
||||
response = await categoryApi.updateCategory(editingCategory.value.id, formData.value)
|
||||
} else {
|
||||
await categoryApi.createCategory(formData.value)
|
||||
response = await categoryApi.createCategory(formData.value)
|
||||
}
|
||||
console.log('分类操作响应:', response)
|
||||
|
||||
// 检查是否是恢复操作
|
||||
if (response && response.message && response.message.includes('恢复成功')) {
|
||||
console.log('检测到分类恢复操作,延迟刷新数据')
|
||||
console.log('恢复的分类信息:', response.category)
|
||||
closeModal()
|
||||
// 延迟一点时间再刷新,确保数据库状态已更新
|
||||
setTimeout(async () => {
|
||||
console.log('开始刷新分类数据...')
|
||||
await fetchCategories()
|
||||
console.log('分类数据刷新完成')
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
closeModal()
|
||||
await fetchCategories()
|
||||
} catch (error) {
|
||||
|
||||
@@ -19,23 +19,23 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
type="success"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加账号
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-40">
|
||||
<n-select v-model:value="platform" :options="platformOptions" />
|
||||
<n-select v-model:value="platform" :options="platformOptions" @update:value="onPlatformChange" />
|
||||
</div>
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,12 +71,12 @@
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无账号</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加账号"按钮创建新账号</div>
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加账号
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -228,8 +228,8 @@
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
>
|
||||
<option value="">请选择平台</option>
|
||||
<option v-for="pan in platforms" :key="pan.id" :value="pan.id">
|
||||
{{ pan.name }}
|
||||
<option v-for="pan in platforms.filter(pan => pan.name === 'quark')" :key="pan.id" :value="pan.id">
|
||||
{{ pan.remark }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="showEditModal" class="mt-1 text-xs text-gray-500 dark:text-gray-400">编辑时不允许修改平台类型</p>
|
||||
@@ -242,49 +242,44 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Cookie <span class="text-red-500">*</span></label>
|
||||
<textarea
|
||||
v-model="form.ck"
|
||||
<n-input
|
||||
v-model:value="form.ck"
|
||||
required
|
||||
rows="4"
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
type="textarea"
|
||||
placeholder="请输入Cookie内容,系统将自动识别容量"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">备注</label>
|
||||
<input
|
||||
v-model="form.remark"
|
||||
<n-input
|
||||
v-model:value="form.remark"
|
||||
type="text"
|
||||
class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-gray-100"
|
||||
placeholder="可选,备注信息"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showEditModal">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.is_valid"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600"
|
||||
<n-checkbox
|
||||
v-model:checked="form.is_valid"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">账号有效</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="tertiary"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="submitting"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '处理中...' : (showEditModal ? '更新' : '创建') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -295,7 +290,8 @@
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
@@ -322,6 +318,7 @@ const loading = ref(true)
|
||||
const pageLoading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const platform = ref(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
import { useCksApi, usePanApi } from '~/composables/useApi'
|
||||
const cksApi = useCksApi()
|
||||
@@ -333,10 +330,18 @@ const pans = computed(() => {
|
||||
return Array.isArray(pansData.value) ? pansData.value : (pansData.value?.list || [])
|
||||
})
|
||||
const platformOptions = computed(() => {
|
||||
return pans.value.map(pan => ({
|
||||
label: pan.remark,
|
||||
value: pan.id
|
||||
}))
|
||||
const options = [
|
||||
{ label: '全部平台', value: null }
|
||||
]
|
||||
|
||||
pans.value.forEach(pan => {
|
||||
options.push({
|
||||
label: pan.remark || pan.name || `平台${pan.id}`,
|
||||
value: pan.id
|
||||
})
|
||||
})
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// 检查认证
|
||||
@@ -382,8 +387,11 @@ const createCks = async () => {
|
||||
await fetchCks()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('创建账号失败:', error)
|
||||
alert('创建账号失败: ' + (error.message || '未知错误'))
|
||||
dialog.error({
|
||||
title: '错误',
|
||||
content: '创建账号失败: ' + (error.message || '未知错误'),
|
||||
positiveText: '确定'
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -406,47 +414,68 @@ const updateCks = async () => {
|
||||
|
||||
// 删除账号
|
||||
const deleteCks = async (id) => {
|
||||
if (!confirm('确定要删除这个账号吗?')) return
|
||||
|
||||
try {
|
||||
await cksApi.deleteCks(id)
|
||||
await fetchCks()
|
||||
} catch (error) {
|
||||
console.error('删除账号失败:', error)
|
||||
alert('删除账号失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个账号吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await cksApi.deleteCks(id)
|
||||
await fetchCks()
|
||||
} catch (error) {
|
||||
console.error('删除账号失败:', error)
|
||||
alert('删除账号失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新容量
|
||||
const refreshCapacity = async (id) => {
|
||||
if (!confirm('确定要刷新此账号的容量信息吗?')) return
|
||||
|
||||
try {
|
||||
await cksApi.refreshCapacity(id)
|
||||
await fetchCks()
|
||||
alert('容量信息已刷新!')
|
||||
} catch (error) {
|
||||
console.error('刷新容量失败:', error)
|
||||
alert('刷新容量失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要刷新此账号的容量信息吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await cksApi.refreshCapacity(id)
|
||||
await fetchCks()
|
||||
alert('容量信息已刷新!')
|
||||
} catch (error) {
|
||||
console.error('刷新容量失败:', error)
|
||||
alert('刷新容量失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换账号状态
|
||||
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 || '未知错误'}`)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: `确定要${cks.is_valid ? '禁用' : '启用'}此账号吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
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 || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑账号
|
||||
@@ -531,13 +560,24 @@ const formatFileSize = (bytes) => {
|
||||
// 过滤和分页计算
|
||||
const filteredCksList = computed(() => {
|
||||
let filtered = cksList.value
|
||||
console.log('原始账号数量:', filtered.length)
|
||||
|
||||
// 平台过滤
|
||||
if (platform.value !== null && platform.value !== undefined) {
|
||||
filtered = filtered.filter(cks => cks.pan_id === platform.value)
|
||||
console.log('平台过滤后数量:', filtered.length, '平台ID:', platform.value)
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(cks =>
|
||||
cks.pan?.name?.toLowerCase().includes(query) ||
|
||||
cks.remark?.toLowerCase().includes(query)
|
||||
)
|
||||
console.log('搜索过滤后数量:', filtered.length, '搜索词:', searchQuery.value)
|
||||
}
|
||||
|
||||
totalPages.value = Math.ceil(filtered.length / itemsPerPage.value)
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value
|
||||
const end = start + itemsPerPage.value
|
||||
@@ -555,9 +595,17 @@ const debounceSearch = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 平台变化处理
|
||||
const onPlatformChange = () => {
|
||||
currentPage.value = 1
|
||||
console.log('平台过滤条件变化:', platform.value)
|
||||
console.log('当前过滤后的账号数量:', filteredCksList.value.length)
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
currentPage.value = 1
|
||||
// 保持当前的过滤条件,只刷新数据
|
||||
fetchCks()
|
||||
fetchPlatforms()
|
||||
}
|
||||
|
||||
550
web/pages/admin/failed-resources.vue
Normal file
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||
<!-- 全局加载状态 -->
|
||||
<div v-if="pageLoading" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-red-600"></div>
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候,正在加载失败资源列表</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">失败资源列表</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">显示处理失败的资源,包含错误信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="retryAllFailed"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-redo"></i> 重试所有失败
|
||||
</button>
|
||||
<button
|
||||
@click="clearAllErrors"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-broom"></i> 清除所有错误
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误统计 -->
|
||||
<div v-if="errorStats && Object.keys(errorStats).length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">错误类型统计</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
<div
|
||||
v-for="(count, type) in errorStats"
|
||||
:key="type"
|
||||
class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3 text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold text-red-600 dark:text-red-400">{{ count }}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">{{ getErrorTypeName(type) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 失败资源列表 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-red-800 dark:bg-red-900 text-white dark:text-gray-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">标题</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">URL</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">错误信息</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">创建时间</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">IP地址</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
|
||||
<tr v-if="loading" class="text-center py-8">
|
||||
<td colspan="7" class="text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="failedResources.length === 0">
|
||||
<td colspan="7">
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
||||
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
|
||||
<path d="M16 24h16M24 16v16" stroke-width="3" stroke-linecap="round" />
|
||||
</svg>
|
||||
<div class="text-lg font-semibold text-gray-400 dark:text-gray-500 mb-2">暂无失败资源</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600">所有资源处理成功</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="resource in failedResources"
|
||||
:key="resource.id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ resource.id }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||
<span v-if="resource.title" :title="resource.title">{{ escapeHtml(resource.title) }}</span>
|
||||
<span v-else class="text-gray-400 dark:text-gray-500 italic">未设置</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<a
|
||||
:href="checkUrlSafety(resource.url)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline break-all"
|
||||
:title="resource.url"
|
||||
>
|
||||
{{ escapeHtml(resource.url) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="max-w-xs">
|
||||
<span
|
||||
class="text-red-600 dark:text-red-400 text-xs bg-red-50 dark:bg-red-900/20 px-2 py-1 rounded"
|
||||
:title="resource.error_msg"
|
||||
>
|
||||
{{ truncateError(resource.error_msg) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ formatTime(resource.create_time) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ escapeHtml(resource.ip || '-') }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="retryResource(resource.id)"
|
||||
class="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
title="重试此资源"
|
||||
>
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="clearError(resource.id)"
|
||||
class="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-300 transition-colors"
|
||||
title="清除错误信息"
|
||||
>
|
||||
<i class="fas fa-broom"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteResource(resource.id)"
|
||||
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="删除此资源"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div v-if="totalPages > 1" class="mt-6 flex justify-center">
|
||||
<div class="flex items-center space-x-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4">
|
||||
<!-- 总资源数 -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
||||
</div>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600"></div>
|
||||
|
||||
<!-- 上一页 -->
|
||||
<button
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<span>上一页</span>
|
||||
</button>
|
||||
|
||||
<!-- 页码 -->
|
||||
<template v-for="page in visiblePages" :key="page">
|
||||
<button
|
||||
v-if="typeof page === 'number'"
|
||||
@click="goToPage(page)"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 min-w-[40px]',
|
||||
page === currentPage
|
||||
? 'bg-red-600 text-white shadow-md'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
]"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
<span v-else class="px-3 py-2 text-sm text-gray-500">...</span>
|
||||
</template>
|
||||
|
||||
<!-- 下一页 -->
|
||||
<button
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<span>下一页</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div v-if="totalPages <= 1" class="mt-4 text-center">
|
||||
<div class="inline-flex items-center bg-white dark:bg-gray-800 rounded-lg shadow px-6 py-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个失败资源
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
interface FailedResource {
|
||||
id: number
|
||||
title?: string
|
||||
url: string
|
||||
error_msg: string
|
||||
create_time: string
|
||||
ip?: string
|
||||
}
|
||||
|
||||
const failedResources = ref<FailedResource[]>([])
|
||||
const loading = ref(false)
|
||||
const pageLoading = ref(true)
|
||||
|
||||
// 分页相关状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(100)
|
||||
const totalCount = ref(0)
|
||||
const totalPages = ref(0)
|
||||
|
||||
// 错误统计
|
||||
const errorStats = ref<Record<string, number>>({})
|
||||
const dialog = useDialog()
|
||||
|
||||
// 获取失败资源API
|
||||
import { useReadyResourceApi } from '~/composables/useApi'
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await readyResourceApi.getFailedResources({
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value
|
||||
}) as any
|
||||
|
||||
if (response && response.data) {
|
||||
failedResources.value = response.data
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil((response.total || 0) / pageSize.value)
|
||||
errorStats.value = response.error_stats || {}
|
||||
} else {
|
||||
failedResources.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
errorStats.value = {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取失败资源失败:', error)
|
||||
failedResources.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
errorStats.value = {}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到指定页面
|
||||
const goToPage = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
currentPage.value = page
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可见的页码
|
||||
const visiblePages = computed(() => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5
|
||||
|
||||
if (totalPages.value <= maxVisible) {
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
if (currentPage.value <= 3) {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
pages.push('...')
|
||||
pages.push(totalPages.value)
|
||||
} else if (currentPage.value >= totalPages.value - 2) {
|
||||
pages.push(1)
|
||||
pages.push('...')
|
||||
for (let i = totalPages.value - 3; i <= totalPages.value; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
pages.push(1)
|
||||
pages.push('...')
|
||||
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
pages.push('...')
|
||||
pages.push(totalPages.value)
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重试单个资源
|
||||
const retryResource = async (id: number) => {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要重试这个资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除,资源将在下次调度时重新处理')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清除单个资源错误
|
||||
const clearError = async (id: number) => {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清除这个资源的错误信息吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.clearErrorMsg(id)
|
||||
alert('错误信息已清除')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('清除错误失败:', error)
|
||||
alert('清除错误失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个失败资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
if (failedResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重试所有失败资源
|
||||
const retryAllFailed = async () => {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要重试所有可重试的失败资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const response = await readyResourceApi.retryFailedResources() as any
|
||||
alert(`重试操作完成:\n总数量:${response.total_count}\n已清除:${response.cleared_count}\n跳过:${response.skipped_count}`)
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('重试所有失败资源失败:', error)
|
||||
alert('重试失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清除所有错误
|
||||
const clearAllErrors = async () => {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清除所有失败资源的错误信息吗?此操作不可恢复!',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
// 这里需要实现批量清除错误的API
|
||||
alert('批量清除错误功能待实现')
|
||||
} catch (error) {
|
||||
console.error('清除所有错误失败:', error)
|
||||
alert('清除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString: string) => {
|
||||
const date = new Date(timeString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 转义HTML防止XSS
|
||||
const escapeHtml = (text: string) => {
|
||||
if (!text) return text
|
||||
const div = document.createElement('div')
|
||||
div.textContent = text
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
// 验证URL安全性
|
||||
const checkUrlSafety = (url: string) => {
|
||||
if (!url) return '#'
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
||||
return '#'
|
||||
}
|
||||
return url
|
||||
} catch {
|
||||
return '#'
|
||||
}
|
||||
}
|
||||
|
||||
// 截断错误信息
|
||||
const truncateError = (errorMsg: string) => {
|
||||
if (!errorMsg) return ''
|
||||
return errorMsg.length > 50 ? errorMsg.substring(0, 50) + '...' : errorMsg
|
||||
}
|
||||
|
||||
// 获取错误类型名称
|
||||
const getErrorTypeName = (type: string) => {
|
||||
const typeNames: Record<string, string> = {
|
||||
'NO_ACCOUNT': '无账号',
|
||||
'NO_VALID_ACCOUNT': '无有效账号',
|
||||
'TRANSFER_FAILED': '转存失败',
|
||||
'LINK_CHECK_FAILED': '链接检查失败',
|
||||
'UNSUPPORTED_LINK': '不支持的链接',
|
||||
'INVALID_LINK': '无效链接',
|
||||
'SERVICE_CREATION_FAILED': '服务创建失败',
|
||||
'TAG_PROCESSING_FAILED': '标签处理失败',
|
||||
'CATEGORY_PROCESSING_FAILED': '分类处理失败',
|
||||
'RESOURCE_SAVE_FAILED': '资源保存失败',
|
||||
'PLATFORM_NOT_FOUND': '平台未找到',
|
||||
'UNKNOWN': '未知错误'
|
||||
}
|
||||
return typeNames[type] || type
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('页面初始化失败:', error)
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '失败资源列表 - 老九网盘资源数据库'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 表格滚动样式 */
|
||||
.overflow-x-auto {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 表格头部固定 */
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 分页按钮悬停效果 */
|
||||
.pagination-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 当前页码按钮效果 */
|
||||
.current-page {
|
||||
box-shadow: 0 4px 6px -1px rgba(220, 38, 38, 0.3), 0 2px 4px -1px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
/* 表格行悬停效果 */
|
||||
tbody tr:hover {
|
||||
background-color: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
/* 暗黑模式下的表格行悬停 */
|
||||
.dark tbody tr:hover {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -175,10 +175,10 @@
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/ready-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
|
||||
<NuxtLink to="/admin/failed-resources" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors block">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">批量处理</span>
|
||||
<i class="fas fa-tasks text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">失败列表</span>
|
||||
<i class="fas fa-exclamation-triangle text-red-400"></i>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -237,7 +237,8 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
// 用户状态管理
|
||||
|
||||
@@ -78,83 +78,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量添加模态框 -->
|
||||
<div v-if="showAddModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto text-gray-900 dark:text-gray-100">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold">批量添加待处理资源</h3>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-800">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 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>格式1:</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</pre>
|
||||
<p class="mt-2 mb-2"><strong>格式2:</strong>只有URL,系统自动判断</p>
|
||||
<pre class="bg-white dark:bg-gray-800 p-2 rounded border text-xs">
|
||||
https://pan.baidu.com/s/123456
|
||||
https://pan.baidu.com/s/789012
|
||||
https://pan.baidu.com/s/345678</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">资源内容:</label>
|
||||
<textarea
|
||||
v-model="resourceText"
|
||||
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 dark:placeholder-gray-500"
|
||||
placeholder="请输入资源内容,支持两种格式..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button @click="closeModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
取消
|
||||
</button>
|
||||
<button @click="handleBatchAdd" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
批量添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
to="/admin/add-resource"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量添加
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="clearAll"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 flex items-center gap-2"
|
||||
type="error"
|
||||
>
|
||||
<i class="fas fa-trash"></i> 清空全部
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,17 +137,11 @@ https://pan.baidu.com/s/345678</pre>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加资源"按钮快速导入资源</div>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
to="/admin/add-resource"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -310,7 +252,8 @@ https://pan.baidu.com/s/345678</pre>
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
interface ReadyResource {
|
||||
@@ -323,8 +266,6 @@ interface ReadyResource {
|
||||
|
||||
const readyResources = ref<ReadyResource[]>([])
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
const resourceText = ref('')
|
||||
const pageLoading = ref(true) // 添加页面加载状态
|
||||
|
||||
// 分页相关状态
|
||||
@@ -335,16 +276,21 @@ const totalPages = ref(0)
|
||||
|
||||
// 获取待处理资源API
|
||||
import { useReadyResourceApi, useSystemConfigApi } from '~/composables/useApi'
|
||||
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||
const readyResourceApi = useReadyResourceApi()
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
|
||||
// 获取系统配置
|
||||
const systemConfig = ref<any>(null)
|
||||
const updatingConfig = ref(false) // 添加配置更新状态
|
||||
const dialog = useDialog()
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const response = await systemConfigApi.getSystemConfig()
|
||||
systemConfig.value = response
|
||||
// 同时更新 Pinia store
|
||||
systemConfigStore.setConfig(response)
|
||||
} catch (error) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
}
|
||||
@@ -444,66 +390,55 @@ const refreshConfig = () => {
|
||||
fetchSystemConfig()
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
resourceText.value = ''
|
||||
}
|
||||
|
||||
// 批量添加
|
||||
const handleBatchAdd = async () => {
|
||||
if (!resourceText.value.trim()) {
|
||||
alert('请输入资源内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await readyResourceApi.createReadyResourcesFromText(resourceText.value) as any
|
||||
console.log('批量添加成功:', response)
|
||||
closeModal()
|
||||
fetchData()
|
||||
alert(`成功添加 ${response.data.count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('批量添加失败:', error)
|
||||
alert('批量添加失败,请检查输入格式')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
if (!confirm('确定要删除这个待处理资源吗?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (readyResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个待处理资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await readyResourceApi.deleteReadyResource(id)
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
if (readyResources.value.length === 1 && currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清空全部
|
||||
const clearAll = async () => {
|
||||
if (!confirm('确定要清空所有待处理资源吗?此操作不可恢复!')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await readyResourceApi.clearReadyResources() as any
|
||||
console.log('清空成功:', response)
|
||||
currentPage.value = 1 // 清空后回到第一页
|
||||
fetchData()
|
||||
alert(`成功清空 ${response.data.deleted_count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
alert('清空失败')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要清空所有待处理资源吗?此操作不可恢复!',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const response = await readyResourceApi.clearReadyResources() as any
|
||||
console.log('清空成功:', response)
|
||||
currentPage.value = 1 // 清空后回到第一页
|
||||
fetchData()
|
||||
alert(`成功清空 ${response.data.deleted_count} 个资源`)
|
||||
} catch (error) {
|
||||
console.error('清空失败:', error)
|
||||
alert('清空失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
@@ -543,32 +478,18 @@ const toggleAutoProcess = async () => {
|
||||
updatingConfig.value = true
|
||||
try {
|
||||
const newValue = !systemConfig.value?.auto_process_ready_resources
|
||||
console.log('当前配置:', systemConfig.value)
|
||||
console.log('新值:', newValue)
|
||||
console.log('切换自动处理配置:', newValue)
|
||||
|
||||
// 先获取当前配置,然后只更新需要的字段
|
||||
const currentConfig = await systemConfigApi.getSystemConfig() as any
|
||||
console.log('获取到的当前配置:', currentConfig)
|
||||
|
||||
const updateData = {
|
||||
site_title: currentConfig.site_title || '',
|
||||
site_description: currentConfig.site_description || '',
|
||||
keywords: currentConfig.keywords || '',
|
||||
author: currentConfig.author || '',
|
||||
copyright: currentConfig.copyright || '',
|
||||
auto_process_ready_resources: newValue,
|
||||
auto_process_interval: currentConfig.auto_process_interval || 30,
|
||||
auto_transfer_enabled: currentConfig.auto_transfer_enabled || false,
|
||||
auto_fetch_hot_drama_enabled: currentConfig.auto_fetch_hot_drama_enabled || false,
|
||||
page_size: currentConfig.page_size || 100,
|
||||
maintenance_mode: currentConfig.maintenance_mode || false
|
||||
}
|
||||
|
||||
console.log('更新数据:', updateData)
|
||||
const response = await systemConfigApi.updateSystemConfig(updateData)
|
||||
console.log('更新响应:', response)
|
||||
// 使用专门的切换API
|
||||
const response = await systemConfigApi.toggleAutoProcess(newValue)
|
||||
console.log('切换响应:', response)
|
||||
|
||||
// 更新本地配置状态
|
||||
systemConfig.value = response
|
||||
|
||||
// 同时更新 Pinia store 中的系统配置
|
||||
systemConfigStore.setConfig(response)
|
||||
|
||||
alert(`自动处理配置已${newValue ? '开启' : '关闭'}`)
|
||||
} catch (error: any) {
|
||||
console.error('切换自动处理配置失败:', error)
|
||||
|
||||
@@ -22,11 +22,10 @@
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">搜索资源</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
@keyup.enter="handleSearch"
|
||||
type="text"
|
||||
class="w-full px-4 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 dark:placeholder-gray-500"
|
||||
placeholder="输入文件名或链接进行搜索..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
@@ -69,18 +68,18 @@
|
||||
<!-- 搜索按钮 -->
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="handleSearch"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="clearFilters"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-times"></i> 清除筛选
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
共找到 <span class="font-semibold text-gray-900 dark:text-gray-100">{{ totalCount }}</span> 个资源
|
||||
@@ -91,26 +90,26 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showBatchModal = true"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||
type="primary"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量操作
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 flex items-center gap-2"
|
||||
type="tertiary"
|
||||
>
|
||||
<i class="fas fa-refresh"></i> 刷新
|
||||
</button>
|
||||
<button
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="exportData"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 flex items-center gap-2"
|
||||
type="info"
|
||||
>
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,9 +118,9 @@
|
||||
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl p-6 max-w-2xl w-full mx-4 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold">批量操作</h3>
|
||||
<button @click="closeBatchModal" class="text-gray-500 hover:text-gray-800">
|
||||
<n-button @click="closeBatchModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -155,11 +154,19 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">选择标签:</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="tag in tags" :key="tag.id" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
<n-checkbox
|
||||
:value="tag.id"
|
||||
v-model="batchTags"
|
||||
class="mr-2"
|
||||
:checked="batchTags.includes(tag.id)"
|
||||
@update:checked="(checked) => {
|
||||
if (checked) {
|
||||
batchTags.push(tag.id)
|
||||
} else {
|
||||
const index = batchTags.indexOf(tag.id)
|
||||
if (index > -1) {
|
||||
batchTags.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<span class="text-sm">{{ tag.name }}</span>
|
||||
</div>
|
||||
@@ -168,12 +175,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<button @click="closeBatchModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
<n-button @click="closeBatchModal" type="tertiary">
|
||||
取消
|
||||
</button>
|
||||
<button @click="handleBatchAction" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
||||
</n-button>
|
||||
<n-button @click="handleBatchAction" type="primary">
|
||||
执行操作
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,11 +192,9 @@
|
||||
<thead class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectAll"
|
||||
@change="toggleSelectAll"
|
||||
class="mr-2"
|
||||
<n-checkbox
|
||||
v-model:checked="selectAll"
|
||||
@update:checked="toggleSelectAll"
|
||||
/>
|
||||
ID
|
||||
</th>
|
||||
@@ -219,17 +224,11 @@
|
||||
<div class="text-sm text-gray-400 dark:text-gray-600 mb-4">你可以点击上方"添加资源"按钮快速导入资源</div>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/add-resource"
|
||||
to="/admin/add-resource"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-plus"></i> 添加资源
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showBatchModal = true"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors text-sm flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-list"></i> 批量操作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -240,11 +239,19 @@
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
<n-checkbox
|
||||
:value="resource.id"
|
||||
v-model="selectedResources"
|
||||
class="mr-2"
|
||||
:checked="selectedResources.includes(resource.id)"
|
||||
@update:checked="(checked) => {
|
||||
if (checked) {
|
||||
selectedResources.push(resource.id)
|
||||
} else {
|
||||
const index = selectedResources.indexOf(resource.id)
|
||||
if (index > -1) {
|
||||
selectedResources.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
{{ resource.id }}
|
||||
</td>
|
||||
@@ -364,7 +371,8 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
interface Resource {
|
||||
@@ -418,6 +426,7 @@ const batchCategory = ref('')
|
||||
const batchTags = ref<number[]>([])
|
||||
const selectedResources = ref<number[]>([])
|
||||
const selectAll = ref(false)
|
||||
const dialog = useDialog()
|
||||
|
||||
// API
|
||||
import { useResourceApi, usePanApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
||||
@@ -576,8 +585,8 @@ const visiblePages = computed(() => {
|
||||
})
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = () => {
|
||||
if (selectAll.value) {
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
selectedResources.value = resources.value.map(r => r.id)
|
||||
} else {
|
||||
selectedResources.value = []
|
||||
@@ -599,10 +608,18 @@ const handleBatchAction = async () => {
|
||||
try {
|
||||
switch (batchAction.value) {
|
||||
case 'delete':
|
||||
if (confirm(`确定要删除选中的 ${selectedResources.value.length} 个资源吗?`)) {
|
||||
await resourceApi.batchDeleteResources(selectedResources.value)
|
||||
alert('批量删除成功')
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: `确定要删除选中的 ${selectedResources.value.length} 个资源吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
await resourceApi.batchDeleteResources(selectedResources.value)
|
||||
alert('批量删除成功')
|
||||
}
|
||||
})
|
||||
return
|
||||
break
|
||||
case 'update_category':
|
||||
if (!batchCategory.value) {
|
||||
@@ -648,16 +665,23 @@ const editResource = (resource: Resource) => {
|
||||
|
||||
// 删除资源
|
||||
const deleteResource = async (id: number) => {
|
||||
if (confirm('确定要删除这个资源吗?')) {
|
||||
try {
|
||||
await resourceApi.deleteResource(id)
|
||||
alert('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个资源吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await resourceApi.deleteResource(id)
|
||||
alert('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
<script setup>
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
|
||||
@@ -20,31 +20,29 @@
|
||||
<form @submit.prevent="saveConfig" class="space-y-6">
|
||||
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane name="SEO 配置" tab="SEO 配置">
|
||||
<n-tab-pane name="站点配置" tab="站点配置">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 网站标题 -->
|
||||
<div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
网站标题 *
|
||||
</label>
|
||||
<input
|
||||
v-model="config.siteTitle"
|
||||
<n-input
|
||||
v-model:value="config.siteTitle"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="老九网盘资源数据库"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 网站描述 -->
|
||||
<div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
网站描述
|
||||
</label>
|
||||
<input
|
||||
v-model="config.siteDescription"
|
||||
<n-input
|
||||
v-model:value="config.siteDescription"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="专业的老九网盘资源数据库"
|
||||
/>
|
||||
</div>
|
||||
@@ -54,42 +52,73 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
关键词 (用逗号分隔)
|
||||
</label>
|
||||
<input
|
||||
v-model="config.keywords"
|
||||
<n-input
|
||||
v-model:value="config.keywords"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="网盘,资源管理,文件分享"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 作者 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
作者
|
||||
</label>
|
||||
<input
|
||||
v-model="config.author"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="系统管理员"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
版权信息
|
||||
</label>
|
||||
<input
|
||||
v-model="config.copyright"
|
||||
<n-input
|
||||
v-model:value="config.copyright"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="© 2024 老九网盘资源数据库"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 禁止词 -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
违禁词
|
||||
</label>
|
||||
<n-input
|
||||
v-model:value="config.forbiddenWords"
|
||||
type="textarea"
|
||||
placeholder=""
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
每页显示数量
|
||||
</label>
|
||||
<select
|
||||
v-model.number="config.pageSize"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="20">20 条</option>
|
||||
<option value="50">50 条</option>
|
||||
<option value="100">100 条</option>
|
||||
<option value="200">200 条</option>
|
||||
</select>
|
||||
</div> -->
|
||||
|
||||
<!-- 系统维护模式 -->
|
||||
<div class="md:col-span-2 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">
|
||||
<n-switch v-model:value="config.maintenanceMode" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="自动处理配置" tab="自动处理配置">
|
||||
<n-tab-pane name="功能配置" tab="功能配置">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- 待处理资源自动处理 -->
|
||||
@@ -104,12 +133,7 @@
|
||||
</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>
|
||||
<n-switch v-model:value="config.autoProcessReadyResources" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,12 +141,9 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
自动处理间隔 (分钟)
|
||||
</label>
|
||||
<input
|
||||
v-model.number="config.autoProcessInterval"
|
||||
<n-input
|
||||
v-model:value="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">
|
||||
@@ -146,12 +167,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="config.autoTransferEnabled"
|
||||
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>
|
||||
<n-switch v-model:value="config.autoTransferEnabled" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,12 +179,9 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
自动转存限制(n天内资源)
|
||||
</label>
|
||||
<input
|
||||
v-model.number="config.autoTransferLimitDays"
|
||||
<n-input
|
||||
v-model:value="config.autoTransferLimitDays"
|
||||
type="number"
|
||||
min="0"
|
||||
max="365"
|
||||
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">
|
||||
@@ -181,12 +194,9 @@
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
最小存储空间(GB)
|
||||
</label>
|
||||
<input
|
||||
v-model.number="config.autoTransferMinSpace"
|
||||
<n-input
|
||||
v-model:value="config.autoTransferMinSpace"
|
||||
type="number"
|
||||
min="100"
|
||||
max="1024"
|
||||
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="500"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
@@ -207,12 +217,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="config.autoFetchHotDramaEnabled"
|
||||
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>
|
||||
<n-switch v-model:value="config.autoFetchHotDramaEnabled" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,47 +226,6 @@
|
||||
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="其他配置" tab="其他配置">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 每页显示数量 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
每页显示数量
|
||||
</label>
|
||||
<select
|
||||
v-model.number="config.pageSize"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="20">20 条</option>
|
||||
<option value="50">50 条</option>
|
||||
<option value="100">100 条</option>
|
||||
<option value="200">200 条</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 系统维护模式 -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
维护模式
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
开启后,普通用户无法访问系统
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="config.maintenanceMode"
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-red-300 dark:peer-focus:ring-red-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-red-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="API配置" tab="API配置">
|
||||
<div class="space-y-4">
|
||||
<!-- API Token -->
|
||||
@@ -270,19 +234,17 @@
|
||||
公开API访问令牌
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="config.apiToken"
|
||||
<n-input
|
||||
v-model:value="config.apiToken"
|
||||
type="text"
|
||||
class="flex-1 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="输入API Token,用于公开API访问认证"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="info"
|
||||
@click="generateApiToken"
|
||||
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
生成
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
用于公开API的访问认证,建议使用随机字符串
|
||||
@@ -296,12 +258,9 @@
|
||||
API使用说明
|
||||
</h3>
|
||||
<div class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<p>• 单个添加资源: POST /api/public/resources/add</p>
|
||||
<p>• 批量添加资源: POST /api/public/resources/batch-add</p>
|
||||
<p>• 资源搜索: GET /api/public/resources/search</p>
|
||||
<p>• 热门剧: GET /api/public/hot-dramas</p>
|
||||
<p>• 认证方式: 在请求头中添加 X-API-Token 或在查询参数中添加 api_token</p>
|
||||
<p>• Swagger文档: <a href="/swagger/index.html" target="_blank" class="underline">查看完整API文档</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,21 +269,20 @@
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="flex justify-end space-x-4 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="tertiary"
|
||||
@click="resetForm"
|
||||
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="saving"
|
||||
class="px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="saveConfig"
|
||||
>
|
||||
<i v-if="saving" class="fas fa-spinner fa-spin mr-2"></i>
|
||||
{{ saving ? '保存中...' : '保存配置' }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -336,7 +294,8 @@
|
||||
<script setup>
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
@@ -346,6 +305,7 @@ const systemConfigStore = useSystemConfigStore()
|
||||
|
||||
// API
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
const notification = useNotification()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -373,6 +333,7 @@ const config = ref({
|
||||
|
||||
// 系统配置状态(用于SEO)
|
||||
const systemConfig = ref(null)
|
||||
const originalConfig = ref(null)
|
||||
|
||||
// 页面元数据 - 移到变量声明之后
|
||||
useHead({
|
||||
@@ -402,7 +363,7 @@ const loadConfig = async () => {
|
||||
|
||||
// 使用新的统一响应格式,直接使用response
|
||||
if (response) {
|
||||
config.value = {
|
||||
const newConfig = {
|
||||
siteTitle: response.site_title || '老九网盘资源数据库',
|
||||
siteDescription: response.site_description || '专业的老九网盘资源数据库',
|
||||
keywords: response.keywords || '网盘,资源管理,文件分享',
|
||||
@@ -414,10 +375,13 @@ const loadConfig = async () => {
|
||||
autoTransferLimitDays: response.auto_transfer_limit_days || 30, // 新增:自动转存限制天数
|
||||
autoTransferMinSpace: response.auto_transfer_min_space || 500, // 新增:最小存储空间(GB)
|
||||
autoFetchHotDramaEnabled: response.auto_fetch_hot_drama_enabled || false, // 新增
|
||||
forbiddenWords: response.forbidden_words || '',
|
||||
pageSize: response.page_size || 100,
|
||||
maintenanceMode: response.maintenance_mode || false,
|
||||
apiToken: response.api_token || '' // 加载API Token
|
||||
}
|
||||
config.value = newConfig
|
||||
originalConfig.value = JSON.parse(JSON.stringify(newConfig)) // 深拷贝保存原始数据
|
||||
systemConfig.value = response // 更新系统配置状态
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -432,37 +396,87 @@ const loadConfig = async () => {
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
const changes = {}
|
||||
const currentConfig = config.value
|
||||
const original = originalConfig.value
|
||||
|
||||
const requestData = {
|
||||
site_title: config.value.siteTitle,
|
||||
site_description: config.value.siteDescription,
|
||||
keywords: config.value.keywords,
|
||||
author: config.value.author,
|
||||
copyright: config.value.copyright,
|
||||
auto_process_ready_resources: config.value.autoProcessReadyResources,
|
||||
auto_process_interval: config.value.autoProcessInterval,
|
||||
auto_transfer_enabled: config.value.autoTransferEnabled, // 新增
|
||||
auto_transfer_limit_days: config.value.autoTransferLimitDays, // 新增:自动转存限制天数
|
||||
auto_transfer_min_space: config.value.autoTransferMinSpace, // 新增:最小存储空间(GB)
|
||||
auto_fetch_hot_drama_enabled: config.value.autoFetchHotDramaEnabled, // 新增
|
||||
page_size: config.value.pageSize,
|
||||
maintenance_mode: config.value.maintenanceMode,
|
||||
api_token: config.value.apiToken // 保存API Token
|
||||
// 检查每个字段是否有变化
|
||||
if (currentConfig.siteTitle !== original.siteTitle) {
|
||||
changes.site_title = currentConfig.siteTitle
|
||||
}
|
||||
if (currentConfig.siteDescription !== original.siteDescription) {
|
||||
changes.site_description = currentConfig.siteDescription
|
||||
}
|
||||
if (currentConfig.keywords !== original.keywords) {
|
||||
changes.keywords = currentConfig.keywords
|
||||
}
|
||||
if (currentConfig.author !== original.author) {
|
||||
changes.author = currentConfig.author
|
||||
}
|
||||
if (currentConfig.copyright !== original.copyright) {
|
||||
changes.copyright = currentConfig.copyright
|
||||
}
|
||||
if (currentConfig.autoProcessReadyResources !== original.autoProcessReadyResources) {
|
||||
changes.auto_process_ready_resources = currentConfig.autoProcessReadyResources
|
||||
}
|
||||
if (currentConfig.autoProcessInterval !== original.autoProcessInterval) {
|
||||
changes.auto_process_interval = currentConfig.autoProcessInterval
|
||||
}
|
||||
if (currentConfig.autoTransferEnabled !== original.autoTransferEnabled) {
|
||||
changes.auto_transfer_enabled = currentConfig.autoTransferEnabled
|
||||
}
|
||||
if (currentConfig.autoTransferLimitDays !== original.autoTransferLimitDays) {
|
||||
changes.auto_transfer_limit_days = currentConfig.autoTransferLimitDays
|
||||
}
|
||||
if (currentConfig.autoTransferMinSpace !== original.autoTransferMinSpace) {
|
||||
changes.auto_transfer_min_space = currentConfig.autoTransferMinSpace
|
||||
}
|
||||
if (currentConfig.autoFetchHotDramaEnabled !== original.autoFetchHotDramaEnabled) {
|
||||
changes.auto_fetch_hot_drama_enabled = currentConfig.autoFetchHotDramaEnabled
|
||||
}
|
||||
if (currentConfig.forbiddenWords !== original.forbiddenWords) {
|
||||
changes.forbidden_words = currentConfig.forbiddenWords
|
||||
}
|
||||
if (currentConfig.pageSize !== original.pageSize) {
|
||||
changes.page_size = currentConfig.pageSize
|
||||
}
|
||||
if (currentConfig.maintenanceMode !== original.maintenanceMode) {
|
||||
changes.maintenance_mode = currentConfig.maintenanceMode
|
||||
}
|
||||
if (currentConfig.apiToken !== original.apiToken) {
|
||||
changes.api_token = currentConfig.apiToken
|
||||
}
|
||||
|
||||
const response = await systemConfigApi.updateSystemConfig(requestData)
|
||||
console.log('检测到的变化:', changes)
|
||||
if (Object.keys(changes).length === 0) {
|
||||
notification.warning({
|
||||
content: '没有需要保存的配置',
|
||||
duration: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
const response = await systemConfigApi.updateSystemConfig(changes)
|
||||
// 使用新的统一响应格式,直接检查response是否存在
|
||||
if (response) {
|
||||
alert('配置保存成功!')
|
||||
notification.success({
|
||||
content: '配置保存成功!',
|
||||
duration: 3000
|
||||
})
|
||||
await loadConfig()
|
||||
// 自动更新 systemConfig store(强制刷新)
|
||||
await systemConfigStore.initConfig(true)
|
||||
} else {
|
||||
alert('保存配置失败:未知错误')
|
||||
notification.error({
|
||||
content: '保存配置失败:未知错误',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
alert('保存配置失败:' + (error.message || '未知错误'))
|
||||
notification.error({
|
||||
content: '保存配置失败:' + (error.message || '未知错误'),
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -470,16 +484,24 @@ const saveConfig = async () => {
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (confirm('确定要重置所有配置吗?')) {
|
||||
loadConfig()
|
||||
}
|
||||
notification.confirm({
|
||||
title: '确定要重置所有配置吗?',
|
||||
content: '重置后,所有配置将恢复为修改前配置',
|
||||
duration: 3000,
|
||||
onOk: () => {
|
||||
loadConfig()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成API Token
|
||||
const generateApiToken = () => {
|
||||
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
config.value.apiToken = newToken;
|
||||
alert('新API Token已生成: ' + newToken);
|
||||
notification.success({
|
||||
content: '新API Token已生成: ' + newToken,
|
||||
duration: 3000
|
||||
})
|
||||
};
|
||||
|
||||
// 页面加载时获取配置
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup="debounceSearch"
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
@input="debounceSearch"
|
||||
type="text"
|
||||
class="w-64 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all text-sm"
|
||||
placeholder="搜索标签名称..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
@@ -203,19 +202,18 @@
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ editingTag ? '编辑标签' : '添加标签' }}
|
||||
</h3>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<n-button @click="closeModal" type="tertiary" size="small">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">标签名称:</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
<n-input
|
||||
v-model:value="formData.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
placeholder="请输入标签名称"
|
||||
/>
|
||||
</div>
|
||||
@@ -235,29 +233,27 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">描述:</label>
|
||||
<textarea
|
||||
v-model="formData.description"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
<n-input
|
||||
v-model:value="formData.description"
|
||||
type="textarea"
|
||||
placeholder="请输入标签描述(可选)"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<n-button
|
||||
type="tertiary"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="submitting"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '提交中...' : (editingTag ? '更新' : '添加') }}
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -267,15 +263,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// API 导入
|
||||
import { useTagApi, useCategoryApi } from '~/composables/useApi'
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const tagApi = useTagApi()
|
||||
const categoryApi = useCategoryApi()
|
||||
|
||||
// 调试信息
|
||||
console.log('tagApi:', tagApi)
|
||||
console.log('categoryApi:', categoryApi)
|
||||
|
||||
// 页面状态
|
||||
const pageLoading = ref(true)
|
||||
const loading = ref(false)
|
||||
@@ -297,6 +304,7 @@ let searchTimeout: NodeJS.Timeout | null = null
|
||||
const showAddModal = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingTag = ref<any>(null)
|
||||
const dialog = useDialog()
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
@@ -331,10 +339,25 @@ const checkAuth = () => {
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
console.log('获取分类列表...')
|
||||
const response = await categoryApi.getCategories()
|
||||
categories.value = Array.isArray(response) ? response : []
|
||||
console.log('分类接口响应:', response)
|
||||
|
||||
// 适配后端API响应格式
|
||||
if (response && response.items) {
|
||||
console.log('使用 items 格式:', response.items)
|
||||
categories.value = response.items
|
||||
} else if (Array.isArray(response)) {
|
||||
console.log('使用数组格式:', response)
|
||||
categories.value = response
|
||||
} else {
|
||||
console.log('使用默认格式:', response)
|
||||
categories.value = []
|
||||
}
|
||||
console.log('最终分类数据:', categories.value)
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
categories.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,17 +370,41 @@ const fetchTags = async () => {
|
||||
page_size: pageSize.value,
|
||||
search: searchQuery.value
|
||||
}
|
||||
console.log('获取标签列表参数:', params)
|
||||
console.log('搜索查询值:', searchQuery.value)
|
||||
console.log('搜索查询类型:', typeof searchQuery.value)
|
||||
|
||||
let response: any
|
||||
if (selectedCategory.value) {
|
||||
response = await tagApi.getTagsByCategory(selectedCategory.value, params)
|
||||
response = await tagApi.getTagsByCategory(parseInt(selectedCategory.value), params)
|
||||
} else {
|
||||
response = await tagApi.getTags(params)
|
||||
}
|
||||
tags.value = response.items || []
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
console.log('标签接口响应:', response)
|
||||
|
||||
// 适配后端API响应格式
|
||||
if (response && response.items) {
|
||||
console.log('使用 items 格式:', response.items)
|
||||
tags.value = response.items
|
||||
totalCount.value = response.total || 0
|
||||
totalPages.value = Math.ceil(totalCount.value / pageSize.value)
|
||||
} else if (Array.isArray(response)) {
|
||||
console.log('使用数组格式:', response)
|
||||
tags.value = response
|
||||
totalCount.value = response.length
|
||||
totalPages.value = 1
|
||||
} else {
|
||||
console.log('使用默认格式:', response)
|
||||
tags.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
}
|
||||
console.log('最终标签数据:', tags.value)
|
||||
} catch (error) {
|
||||
console.error('获取标签列表失败:', error)
|
||||
tags.value = []
|
||||
totalCount.value = 0
|
||||
totalPages.value = 1
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -371,10 +418,12 @@ const onCategoryChange = () => {
|
||||
|
||||
// 搜索防抖
|
||||
const debounceSearch = () => {
|
||||
console.log('搜索防抖触发,当前搜索值:', searchQuery.value)
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
searchTimeout = setTimeout(() => {
|
||||
console.log('执行搜索,搜索值:', searchQuery.value)
|
||||
currentPage.value = 1
|
||||
fetchTags()
|
||||
}, 300)
|
||||
@@ -404,16 +453,21 @@ const editTag = (tag: any) => {
|
||||
|
||||
// 删除标签
|
||||
const deleteTag = async (tagId: number) => {
|
||||
if (!confirm(`确定要删除标签吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await tagApi.deleteTag(tagId)
|
||||
await fetchTags()
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除标签吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await tagApi.deleteTag(tagId)
|
||||
await fetchTags()
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -433,10 +487,24 @@ const handleSubmit = async () => {
|
||||
category_id: categoryId
|
||||
}
|
||||
|
||||
let response: any
|
||||
if (editingTag.value) {
|
||||
await tagApi.updateTag(editingTag.value.id, submitData)
|
||||
response = await tagApi.updateTag(editingTag.value.id, submitData)
|
||||
} else {
|
||||
await tagApi.createTag(submitData)
|
||||
response = await tagApi.createTag(submitData)
|
||||
}
|
||||
|
||||
console.log('标签操作响应:', response)
|
||||
|
||||
// 检查是否是恢复操作
|
||||
if (response && response.message && response.message.includes('恢复成功')) {
|
||||
console.log('检测到标签恢复操作,延迟刷新数据')
|
||||
closeModal()
|
||||
// 延迟一点时间再刷新,确保数据库状态已更新
|
||||
setTimeout(async () => {
|
||||
await fetchTags()
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
|
||||
closeModal()
|
||||
@@ -480,15 +548,25 @@ const handleLogout = () => {
|
||||
// 页面加载
|
||||
onMounted(async () => {
|
||||
try {
|
||||
console.log('页面开始加载...')
|
||||
checkAuth()
|
||||
console.log('认证检查完成')
|
||||
|
||||
console.log('开始获取分类列表...')
|
||||
await fetchCategories()
|
||||
console.log('分类列表获取完成')
|
||||
|
||||
console.log('开始获取标签列表...')
|
||||
await fetchTags()
|
||||
console.log('标签列表获取完成')
|
||||
|
||||
// 检查URL参数,如果action=add则自动打开新增弹窗
|
||||
const route = useRoute()
|
||||
if (route.query.action === 'add') {
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
console.log('页面加载完成')
|
||||
} catch (error) {
|
||||
console.error('标签管理页面初始化失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">用户管理</h2>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<n-button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
type="primary"
|
||||
>
|
||||
添加用户
|
||||
</button>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,9 +52,11 @@
|
||||
{{ user.last_login ? formatDate(user.last_login) : '从未登录' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button @click="editUser(user)" class="text-indigo-600 hover:text-indigo-900 mr-3">编辑</button>
|
||||
<button @click="showChangePasswordModal(user)" class="text-yellow-600 hover:text-yellow-900 mr-3">修改密码</button>
|
||||
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-900">删除</button>
|
||||
<n-button @click="editUser(user)" type="info" size="small" class="mr-3" :title="user.username === 'admin' ? '管理员用户信息不可修改' : '编辑用户'">
|
||||
编辑{{ user.username === 'admin' ? ' (只读)' : '' }}
|
||||
</n-button>
|
||||
<n-button @click="showChangePasswordModal(user)" type="warning" size="small" class="mr-3">修改密码</n-button>
|
||||
<n-button @click="deleteUser(user.id)" type="error" size="small">删除</n-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -69,33 +71,44 @@
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{{ showEditModal ? '编辑用户' : '创建用户' }}
|
||||
</h3>
|
||||
<div v-if="showEditModal && editingUser?.username === 'admin'" class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
管理员用户信息不可修改,只能通过修改密码功能来更新密码。
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="showEditModal && editingUser?.username !== 'admin'" class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p class="text-sm text-blue-800">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
编辑模式:用户名和邮箱不可修改,只能修改角色和激活状态。
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">用户名</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
<n-input
|
||||
v-model:value="form.username"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">邮箱</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
<n-input
|
||||
v-model:value="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showCreateModal">
|
||||
<label class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -103,20 +116,19 @@
|
||||
<select
|
||||
v-model="form.role"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
>
|
||||
<option value="user">用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.is_active"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">激活状态</span>
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">激活状态</label>
|
||||
<n-switch
|
||||
v-model:value="form.is_active"
|
||||
size="medium"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
@@ -129,7 +141,8 @@
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="showEditModal && editingUser?.username === 'admin'"
|
||||
>
|
||||
{{ showEditModal ? '更新' : '创建' }}
|
||||
</button>
|
||||
@@ -153,22 +166,20 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">新密码</label>
|
||||
<input
|
||||
v-model="passwordForm.newPassword"
|
||||
<n-input
|
||||
v-model:value="passwordForm.newPassword"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="请输入新密码(至少6位)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">确认新密码</label>
|
||||
<input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
<n-input
|
||||
v-model:value="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</div>
|
||||
@@ -197,18 +208,20 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const users = ref([])
|
||||
const users = ref<any[]>([])
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const showPasswordModal = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const changingPasswordUser = ref(null)
|
||||
const dialog = useDialog()
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
@@ -270,16 +283,23 @@ const updateUser = async () => {
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (id) => {
|
||||
if (!confirm('确定要删除这个用户吗?')) return
|
||||
|
||||
try {
|
||||
const { useUserApi } = await import('~/composables/useApi')
|
||||
const userApi = useUserApi()
|
||||
await userApi.deleteUser(id)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这个用户吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const { useUserApi } = await import('~/composables/useApi')
|
||||
const userApi = useUserApi()
|
||||
await userApi.deleteUser(id)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示修改密码模态框
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
<script setup lang="ts">
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
// 页面元数据
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex-1 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<div class="header-container bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||
老九网盘资源数据库 - API文档
|
||||
@@ -46,57 +46,6 @@
|
||||
|
||||
<!-- API接口列表 -->
|
||||
<div class="space-y-8">
|
||||
<!-- 单个添加资源 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="bg-green-600 text-white px-6 py-4">
|
||||
<h3 class="text-xl font-semibold flex items-center">
|
||||
<i class="fas fa-plus-circle mr-2"></i>
|
||||
单个添加资源
|
||||
</h3>
|
||||
<p class="text-green-100 mt-1">添加单个资源到待处理列表</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求信息</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong>方法:</strong><span class="bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 px-2 py-1 rounded">POST</span></p>
|
||||
<p><strong>路径:</strong><code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">/api/public/resources/add</code></p>
|
||||
<p><strong>认证:</strong><span class="text-red-600 dark:text-red-400">必需</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求参数</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"title": "资源标题",
|
||||
"description": "资源描述",
|
||||
"url": "资源链接",
|
||||
"category": "分类名称",
|
||||
"tags": "标签1,标签2",
|
||||
"img": "封面图片链接",
|
||||
"source": "数据来源",
|
||||
"extra": "额外信息"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">响应示例</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"success": true,
|
||||
"message": "资源添加成功,已进入待处理列表",
|
||||
"data": {
|
||||
"id": 123
|
||||
},
|
||||
"code": 200
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量添加资源 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="bg-purple-600 text-white px-6 py-4">
|
||||
@@ -104,7 +53,7 @@
|
||||
<i class="fas fa-layer-group mr-2"></i>
|
||||
批量添加资源
|
||||
</h3>
|
||||
<p class="text-purple-100 mt-1">批量添加多个资源到待处理列表</p>
|
||||
<p class="text-purple-100 mt-1">批量添加多个资源到待处理列表,每个资源可包含多个链接(url为数组),标题和url为必填项</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
@@ -113,22 +62,28 @@
|
||||
<div class="space-y-2 text-sm">
|
||||
<p><strong>方法:</strong><span class="bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200 px-2 py-1 rounded">POST</span></p>
|
||||
<p><strong>路径:</strong><code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">/api/public/resources/batch-add</code></p>
|
||||
<p><strong>认证:</strong><span class="text-red-600 dark:text-red-400">必需</span></p>
|
||||
<p><strong>认证:</strong><span class="text-red-600 dark:text-red-400">必需</span>(X-API-Token)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-3">请求参数</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">title 和 url 是必填项,其他字段均为选填</p>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"resources": [
|
||||
{
|
||||
"title": "资源1",
|
||||
"url": "链接1",
|
||||
"description": "描述1"
|
||||
"description": "描述1",
|
||||
"url": ["链接1", "链接2"],
|
||||
"category": "分类",
|
||||
"tags": "标签1,标签2",
|
||||
"img": "图片链接",
|
||||
"source": "数据来源",
|
||||
"extra": "额外信息"
|
||||
},
|
||||
{
|
||||
"title": "资源2",
|
||||
"url": "链接2",
|
||||
"title": "资源2",
|
||||
"url": ["链接3"],
|
||||
"description": "描述2"
|
||||
}
|
||||
]
|
||||
@@ -141,7 +96,7 @@
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded p-4">
|
||||
<pre class="text-sm overflow-x-auto"><code>{
|
||||
"success": true,
|
||||
"message": "批量添加成功,共添加 2 个资源",
|
||||
"message": "批量添加成功",
|
||||
"data": {
|
||||
"created_count": 2,
|
||||
"created_ids": [123, 124]
|
||||
@@ -323,14 +278,15 @@
|
||||
<pre class="text-sm overflow-x-auto"><code># 设置API Token
|
||||
API_TOKEN="your_api_token_here"
|
||||
|
||||
# 单个添加资源
|
||||
curl -X POST "http://localhost:8080/api/public/resources/add" \
|
||||
# 批量添加资源
|
||||
curl -X POST "http://localhost:8080/api/public/resources/batch-add" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: $API_TOKEN" \
|
||||
-d '{
|
||||
"title": "测试资源",
|
||||
"url": "https://example.com/resource",
|
||||
"description": "测试描述"
|
||||
"resources": [
|
||||
{ "title": "测试资源1", "url": ["https://example.com/resource1"], "description": "描述1" },
|
||||
{ "title": "测试资源2", "url": ["https://example.com/resource2", "https://example.com/resource3"], "description": "描述2" }
|
||||
]
|
||||
}'
|
||||
|
||||
# 搜索资源
|
||||
@@ -355,16 +311,21 @@ fetch('/api/public/resources/search?q=测试', { headers: { 'X-API-Token': 'your
|
||||
alert(res.message)
|
||||
}
|
||||
})
|
||||
// 单个添加资源
|
||||
fetch('/api/public/resources/add', {
|
||||
// 批量添加资源
|
||||
fetch('/api/public/resources/batch-add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Token': 'your_token' },
|
||||
body: JSON.stringify({ title: 'xxx', url: 'xxx' })
|
||||
body: JSON.stringify({
|
||||
resources: [
|
||||
{ title: 'xxx', url: ['xxx'], description: 'xxx' },
|
||||
{ title: 'yyy', url: ['yyy', 'zzz'], description: 'yyy' }
|
||||
]
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
alert('添加成功,ID:' + res.data.id)
|
||||
alert('添加成功,ID: ' + res.data.created_ids.join(', '))
|
||||
} else {
|
||||
alert(res.message)
|
||||
}
|
||||
@@ -406,4 +367,12 @@ pre {
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
.header-container {
|
||||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex-1 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<div class="header-container bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||
热播剧榜单
|
||||
@@ -336,4 +336,12 @@ watch(dramas, (newDramas) => {
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header-container{
|
||||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="flex-1 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<div class="header-container bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||
{{ systemConfig?.site_title || '老九网盘资源数据库' }}
|
||||
@@ -55,21 +55,9 @@
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
|
||||
<!-- <div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@keyup="handleSearch"
|
||||
type="text"
|
||||
class="w-full px-4 py-3 rounded-full border-2 border-gray-300 dark:border-gray-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 transition-all"
|
||||
placeholder="输入文件名或链接进行搜索..."
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</div>
|
||||
</div> -->
|
||||
<ClientOnly>
|
||||
<div class="relative">
|
||||
<n-input round placeholder="搜索" v-model="searchQuery" @blur="handleSearch" @keyup.enter="handleSearch">
|
||||
<n-input round placeholder="搜索" v-model:value="searchQuery" @blur="handleSearch" @keyup.enter="handleSearch">
|
||||
<template #suffix>
|
||||
<i class="fas fa-search text-gray-400"></i>
|
||||
</template>
|
||||
@@ -295,11 +283,14 @@ const pageSize = ref(200)
|
||||
const selectedPlatform = ref(route.query.platform as string || '')
|
||||
const showLinkModal = ref(false)
|
||||
const selectedResource = ref<any>(null)
|
||||
const authInitialized = ref(false)
|
||||
const authInitialized = ref(true) // 在app.vue中已经初始化,这里直接设为true
|
||||
const isLoadingMore = ref(false)
|
||||
const hasMoreData = ref(true)
|
||||
const pageLoading = ref(false)
|
||||
|
||||
// 用户状态管理
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 使用 useAsyncData 获取资源数据
|
||||
const { data: resourcesData, pending, refresh } = await useAsyncData(
|
||||
() => `resources-${currentPage.value}-${searchQuery.value}-${selectedPlatform.value}`,
|
||||
@@ -333,13 +324,8 @@ const totalPages = computed(() => {
|
||||
return Math.ceil(total / pageSize.value)
|
||||
})
|
||||
|
||||
// 用户状态管理
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 初始化认证状态
|
||||
onMounted(() => {
|
||||
userStore.initAuth()
|
||||
authInitialized.value = true
|
||||
animateCounters()
|
||||
})
|
||||
|
||||
@@ -356,8 +342,8 @@ const handleSearch = async (e?: any) => {
|
||||
|
||||
// 更新URL参数
|
||||
const query = { ...route.query }
|
||||
if (searchQuery.value.trim()) {
|
||||
query.search = searchQuery.value.trim()
|
||||
if ((searchQuery.value as string).trim()) {
|
||||
query.search = (searchQuery.value as string).trim()
|
||||
} else {
|
||||
delete query.search
|
||||
}
|
||||
@@ -407,7 +393,9 @@ const openLink = async (url: string, resourceId: number) => {
|
||||
try {
|
||||
await fetch(`/api/resources/${resourceId}/view`, { method: 'POST' })
|
||||
} catch (e) {}
|
||||
window.open(url, '_blank')
|
||||
if (process.client) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换链接显示
|
||||
@@ -423,15 +411,17 @@ const toggleLink = async (resource: any) => {
|
||||
const copyToClipboard = async (text: any) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
const button = document.querySelector('.show-link-btn')
|
||||
if (button) {
|
||||
const originalText = button.innerHTML
|
||||
button.innerHTML = '<i class="fas fa-check"></i> 已复制'
|
||||
button.classList.add('bg-green-600')
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText
|
||||
button.classList.remove('bg-green-600')
|
||||
}, 2000)
|
||||
if (process.client) {
|
||||
const button = document.querySelector('.show-link-btn')
|
||||
if (button) {
|
||||
const originalText = button.innerHTML
|
||||
button.innerHTML = '<i class="fas fa-check"></i> 已复制'
|
||||
button.classList.add('bg-green-600')
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText
|
||||
button.classList.remove('bg-green-600')
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
@@ -483,6 +473,8 @@ const isUpdatedToday = (dateString: string) => {
|
||||
|
||||
// 数字动画效果
|
||||
const animateCounters = () => {
|
||||
if (!process.client) return
|
||||
|
||||
const counters = document.querySelectorAll('.count-up')
|
||||
const speed = 200
|
||||
|
||||
@@ -518,11 +510,13 @@ const goToPage = async (page: number) => {
|
||||
// 刷新数据
|
||||
await refresh()
|
||||
|
||||
// 滚动到顶部
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
// 滚动到顶部(只在客户端执行)
|
||||
if (process.client) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -590,4 +584,12 @@ const loadMore = async () => {
|
||||
}
|
||||
.animate-blink.delay-200 { animation-delay: 0.2s; }
|
||||
.animate-blink.delay-400 { animation-delay: 0.4s; }
|
||||
.header-container{
|
||||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -5,38 +5,32 @@
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">管理员登录</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">请输入管理员账号密码</p>
|
||||
<div class="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
默认管理员账户:admin / password
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div class="mt-3 p-3 bg-blue-50 rounded-lg">
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
<n-input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.username }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.username" class="mt-1 text-sm text-red-600">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.password }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.password" class="mt-1 text-sm text-red-600">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
@@ -67,19 +61,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<ErrorToast
|
||||
v-if="showErrorToast"
|
||||
:message="errorToastMessage"
|
||||
@close="showErrorToast = false"
|
||||
/>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<SuccessToast
|
||||
v-if="showSuccessToast"
|
||||
:message="successToastMessage"
|
||||
@close="showSuccessToast = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,6 +70,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const notification = useNotification()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
@@ -100,12 +82,6 @@ const errors = reactive({
|
||||
password: ''
|
||||
})
|
||||
|
||||
const showErrorToast = ref(false)
|
||||
const errorToastMessage = ref('')
|
||||
const showSuccessToast = ref(false)
|
||||
const successToastMessage = ref('')
|
||||
|
||||
|
||||
|
||||
const validateForm = () => {
|
||||
errors.username = ''
|
||||
@@ -133,6 +109,10 @@ const handleLogin = async () => {
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
notification.success({
|
||||
content: '登录成功',
|
||||
duration: 3000
|
||||
})
|
||||
await router.push('/admin')
|
||||
} else {
|
||||
// 根据错误类型提供更友好的提示
|
||||
@@ -148,11 +128,18 @@ const handleLogin = async () => {
|
||||
message = result.message
|
||||
}
|
||||
}
|
||||
errorToastMessage.value = message
|
||||
showErrorToast.value = true
|
||||
notification.error({
|
||||
content: message,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
layout: 'single',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '管理员登录 - 老九网盘资源数据库'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex-1 p-3 sm:p-5">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<div class="header-container bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||
系统性能监控
|
||||
@@ -47,10 +47,8 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">自动刷新:</label>
|
||||
<input
|
||||
v-model="autoRefresh"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
<n-checkbox
|
||||
v-model:checked="autoRefresh"
|
||||
/>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ autoRefreshInterval }}秒</span>
|
||||
</div>
|
||||
@@ -375,4 +373,12 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
.header-container{
|
||||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.1) 0%,
|
||||
rgba(0,0,0,0.25) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -10,53 +10,49 @@
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-100">用户名</label>
|
||||
<input
|
||||
<n-input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.username }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.username" class="mt-1 text-sm text-red-600">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-100">邮箱</label>
|
||||
<input
|
||||
<n-input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.email }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.email" class="mt-1 text-sm text-red-600">{{ errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-100">密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.password }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.password" class="mt-1 text-sm text-red-600">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-100">确认密码</label>
|
||||
<input
|
||||
<n-input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
v-model="form.confirmPassword"
|
||||
required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
:class="{ 'border-red-500': errors.confirmPassword }"
|
||||
>
|
||||
/>
|
||||
<p v-if="errors.confirmPassword" class="mt-1 text-sm text-red-600">{{ errors.confirmPassword }}</p>
|
||||
</div>
|
||||
|
||||
@@ -86,20 +82,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<ErrorToast
|
||||
v-if="showErrorToast"
|
||||
:message="errorToastMessage"
|
||||
@close="showErrorToast = false"
|
||||
/>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<SuccessToast
|
||||
v-if="showSuccessToast"
|
||||
:message="successToastMessage"
|
||||
@close="showSuccessToast = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,7 +91,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const notification = useNotification()
|
||||
const form = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
@@ -124,11 +106,6 @@ const errors = reactive({
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const showErrorToast = ref(false)
|
||||
const errorToastMessage = ref('')
|
||||
const showSuccessToast = ref(false)
|
||||
const successToastMessage = ref('')
|
||||
|
||||
const validateForm = () => {
|
||||
errors.username = ''
|
||||
errors.email = ''
|
||||
@@ -184,8 +161,10 @@ const handleRegister = async () => {
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
successToastMessage.value = '注册成功!请登录'
|
||||
showSuccessToast.value = true
|
||||
notification.success({
|
||||
content: '注册成功!请登录',
|
||||
duration: 3000
|
||||
})
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 2000)
|
||||
@@ -203,11 +182,18 @@ const handleRegister = async () => {
|
||||
errorMessage = result.message
|
||||
}
|
||||
}
|
||||
errorToastMessage.value = errorMessage
|
||||
showErrorToast.value = true
|
||||
notification.error({
|
||||
content: errorMessage,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
layout: 'single',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
// 设置页面标题
|
||||
useHead({
|
||||
title: '用户注册 - 老九网盘资源数据库'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 在客户端初始化时恢复用户状态
|
||||
userStore.initAuth()
|
||||
// 只在客户端执行
|
||||
if (process.client) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 在应用启动时初始化用户认证状态
|
||||
// 使用 nextTick 确保在 DOM 更新后执行
|
||||
nextTick(() => {
|
||||
userStore.initAuth()
|
||||
})
|
||||
}
|
||||
})
|
||||