mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
update: cks
This commit is contained in:
@@ -1,153 +0,0 @@
|
|||||||
# PanFactory 单例模式分析
|
|
||||||
|
|
||||||
## 为什么需要单例模式?
|
|
||||||
|
|
||||||
### 1. 当前使用场景分析
|
|
||||||
|
|
||||||
在 `utils/scheduler.go` 中的使用:
|
|
||||||
```go
|
|
||||||
func (s *Scheduler) processReadyResources() {
|
|
||||||
// 每次处理待处理资源时都创建新实例
|
|
||||||
factory := panutils.GetInstance() // 使用单例模式
|
|
||||||
|
|
||||||
for _, readyResource := range readyResources {
|
|
||||||
// 处理每个资源
|
|
||||||
s.convertReadyResourceToResource(readyResource, factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 性能开销分析
|
|
||||||
|
|
||||||
#### 工厂实例本身的开销
|
|
||||||
```go
|
|
||||||
type PanFactory struct{} // 空结构体,几乎无开销
|
|
||||||
```
|
|
||||||
- 内存占用:几乎为0
|
|
||||||
- 创建时间:纳秒级别
|
|
||||||
|
|
||||||
#### 实际开销分析
|
|
||||||
真正的开销在于每次调用 `CreatePanService` 时创建的具体服务实例:
|
|
||||||
|
|
||||||
1. **HTTP客户端创建**
|
|
||||||
```go
|
|
||||||
httpClient: &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **请求头设置**
|
|
||||||
```go
|
|
||||||
headers: make(map[string]string)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **配置对象创建**
|
|
||||||
```go
|
|
||||||
config := &PanConfig{...}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 单例模式的优势
|
|
||||||
|
|
||||||
#### 内存优化
|
|
||||||
- **避免重复创建**:工厂实例只创建一次
|
|
||||||
- **减少GC压力**:减少对象创建和销毁
|
|
||||||
- **内存占用稳定**:不会因为频繁创建导致内存波动
|
|
||||||
|
|
||||||
#### 性能优化
|
|
||||||
- **减少初始化开销**:避免重复的初始化操作
|
|
||||||
- **提高响应速度**:后续调用直接使用已创建的实例
|
|
||||||
- **减少锁竞争**:单例模式使用 `sync.Once`,性能更好
|
|
||||||
|
|
||||||
#### 资源管理
|
|
||||||
- **统一管理**:所有地方使用同一个工厂实例
|
|
||||||
- **状态一致性**:避免多个实例状态不一致的问题
|
|
||||||
- **配置共享**:可以在工厂中维护全局配置
|
|
||||||
|
|
||||||
### 4. 实现细节
|
|
||||||
|
|
||||||
#### 线程安全的单例实现
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
instance *PanFactory
|
|
||||||
once sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewPanFactory() *PanFactory {
|
|
||||||
once.Do(func() {
|
|
||||||
instance = &PanFactory{}
|
|
||||||
})
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetInstance() *PanFactory {
|
|
||||||
return NewPanFactory()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 优势
|
|
||||||
1. **线程安全**:使用 `sync.Once` 保证线程安全
|
|
||||||
2. **延迟初始化**:只有在第一次调用时才创建实例
|
|
||||||
3. **性能优秀**:后续调用直接返回已创建的实例
|
|
||||||
|
|
||||||
### 5. 性能测试结果
|
|
||||||
|
|
||||||
#### 单例模式 vs 普通创建
|
|
||||||
```bash
|
|
||||||
# 单例模式性能测试
|
|
||||||
BenchmarkSingletonCreation-8 100000000 11.2 ns/op
|
|
||||||
|
|
||||||
# 普通创建性能测试(如果每次都创建新实例)
|
|
||||||
BenchmarkNewPanFactory-8 100000000 12.1 ns/op
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 内存使用对比
|
|
||||||
- **单例模式**:固定内存占用,无波动
|
|
||||||
- **普通创建**:每次创建新实例,增加内存占用
|
|
||||||
|
|
||||||
### 6. 使用建议
|
|
||||||
|
|
||||||
#### 推荐使用方式
|
|
||||||
```go
|
|
||||||
// ✅ 推荐:使用单例模式
|
|
||||||
factory := panutils.GetInstance()
|
|
||||||
|
|
||||||
// ❌ 不推荐:每次都创建新实例
|
|
||||||
factory := panutils.NewPanFactory()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 在定时任务中的使用
|
|
||||||
```go
|
|
||||||
func (s *Scheduler) processReadyResources() {
|
|
||||||
// 获取单例实例
|
|
||||||
factory := panutils.GetInstance()
|
|
||||||
|
|
||||||
for _, readyResource := range readyResources {
|
|
||||||
// 复用同一个工厂实例
|
|
||||||
s.convertReadyResourceToResource(readyResource, factory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 扩展性考虑
|
|
||||||
|
|
||||||
#### 未来可能的扩展
|
|
||||||
1. **配置缓存**:在工厂中缓存常用配置
|
|
||||||
2. **连接池管理**:管理HTTP连接池
|
|
||||||
3. **服务实例缓存**:缓存已创建的服务实例
|
|
||||||
4. **监控和统计**:在工厂中添加使用统计
|
|
||||||
|
|
||||||
#### 单例模式的适用性
|
|
||||||
- ✅ **适合**:工厂类、配置管理、连接池
|
|
||||||
- ❌ **不适合**:有状态的对象、需要隔离的实例
|
|
||||||
|
|
||||||
### 8. 总结
|
|
||||||
|
|
||||||
对于 `PanFactory` 使用单例模式是**推荐的做法**,原因:
|
|
||||||
|
|
||||||
1. **性能优化**:减少不必要的对象创建
|
|
||||||
2. **内存优化**:降低内存占用和GC压力
|
|
||||||
3. **资源管理**:统一管理工厂实例
|
|
||||||
4. **线程安全**:保证并发环境下的正确性
|
|
||||||
5. **扩展性好**:为未来功能扩展提供基础
|
|
||||||
|
|
||||||
虽然工厂实例本身开销不大,但在高频调用的场景下(如定时任务),单例模式能带来明显的性能提升和资源优化。
|
|
||||||
@@ -253,6 +253,71 @@ func (a *AlipanService) DeleteFiles(fileList []string) (*TransferResult, error)
|
|||||||
return SuccessResult("删除成功", nil), nil
|
return SuccessResult("删除成功", nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
func (a *AlipanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||||
|
// 设置Cookie
|
||||||
|
a.SetHeader("Cookie", cookie)
|
||||||
|
|
||||||
|
// 获取access token
|
||||||
|
accessToken, err := a.manageAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取access_token失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Authorization头
|
||||||
|
a.SetHeader("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
// 调用阿里云盘用户信息API
|
||||||
|
userInfoURL := "https://api.alipan.com/v2/user/get"
|
||||||
|
resp, err := a.HTTPGet(userInfoURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
NickName string `json:"nick_name"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
DriveInfo struct {
|
||||||
|
TotalSize string `json:"total_size"`
|
||||||
|
UsedSize string `json:"used_size"`
|
||||||
|
} `json:"drive_info"`
|
||||||
|
VipInfo struct {
|
||||||
|
VipStatus string `json:"vip_status"`
|
||||||
|
} `json:"vip_info"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.ParseJSONResponse(resp, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != "" {
|
||||||
|
return nil, fmt.Errorf("API返回错误: %s", result.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换VIP状态
|
||||||
|
vipStatus := result.Data.VipInfo.VipStatus == "vip"
|
||||||
|
|
||||||
|
// 转换容量字符串为字节数
|
||||||
|
totalSizeStr := result.Data.DriveInfo.TotalSize
|
||||||
|
usedSizeStr := result.Data.DriveInfo.UsedSize
|
||||||
|
|
||||||
|
// 解析容量字符串
|
||||||
|
totalSizeBytes := ParseCapacityString(totalSizeStr)
|
||||||
|
usedSizeBytes := ParseCapacityString(usedSizeStr)
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
Username: result.Data.NickName,
|
||||||
|
VIPStatus: vipStatus,
|
||||||
|
UsedSpace: usedSizeBytes,
|
||||||
|
TotalSpace: totalSizeBytes,
|
||||||
|
ServiceType: "alipan",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getAlipan1 通过分享id获取file_id
|
// getAlipan1 通过分享id获取file_id
|
||||||
func (a *AlipanService) getAlipan1(shareID string) (*AlipanShareInfo, error) {
|
func (a *AlipanService) getAlipan1(shareID string) (*AlipanShareInfo, error) {
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package pan
|
package pan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// BaiduPanService 百度网盘服务
|
// BaiduPanService 百度网盘服务
|
||||||
type BaiduPanService struct {
|
type BaiduPanService struct {
|
||||||
*BasePanService
|
*BasePanService
|
||||||
@@ -44,3 +48,56 @@ func (b *BaiduPanService) DeleteFiles(fileList []string) (*TransferResult, error
|
|||||||
// TODO: 实现百度网盘文件删除
|
// TODO: 实现百度网盘文件删除
|
||||||
return ErrorResult("百度网盘文件删除功能暂未实现"), nil
|
return ErrorResult("百度网盘文件删除功能暂未实现"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
func (b *BaiduPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||||
|
// 设置Cookie
|
||||||
|
b.SetHeader("Cookie", cookie)
|
||||||
|
|
||||||
|
// 调用百度网盘用户信息API
|
||||||
|
userInfoURL := "https://pan.baidu.com/api/gettemplatevariable"
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"fields": "['username','uk','vip_type','vip_endtime','total_capacity','used_capacity']",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := b.HTTPPost(userInfoURL, data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result struct {
|
||||||
|
Errno int `json:"errno"`
|
||||||
|
Data struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Uk string `json:"uk"`
|
||||||
|
VipType int `json:"vip_type"`
|
||||||
|
VipEndtime string `json:"vip_endtime"`
|
||||||
|
TotalCapacity string `json:"total_capacity"`
|
||||||
|
UsedCapacity string `json:"used_capacity"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.ParseJSONResponse(resp, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Errno != 0 {
|
||||||
|
return nil, fmt.Errorf("API返回错误: %d", result.Errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换VIP状态
|
||||||
|
vipStatus := result.Data.VipType > 0
|
||||||
|
|
||||||
|
// 解析容量字符串
|
||||||
|
totalCapacityBytes := ParseCapacityString(result.Data.TotalCapacity)
|
||||||
|
usedCapacityBytes := ParseCapacityString(result.Data.UsedCapacity)
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
Username: result.Data.Username,
|
||||||
|
VIPStatus: vipStatus,
|
||||||
|
UsedSpace: usedCapacityBytes,
|
||||||
|
TotalSpace: totalCapacityBytes,
|
||||||
|
ServiceType: "baidu",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package pan
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -53,6 +54,15 @@ type TransferResult struct {
|
|||||||
Fid string `json:"fid,omitempty"`
|
Fid string `json:"fid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserInfo 用户信息结构体
|
||||||
|
type UserInfo struct {
|
||||||
|
Username string `json:"username"` // 用户名
|
||||||
|
VIPStatus bool `json:"vipStatus"` // VIP状态
|
||||||
|
UsedSpace int64 `json:"usedSpace"` // 已使用空间
|
||||||
|
TotalSpace int64 `json:"totalSpace"` // 总空间
|
||||||
|
ServiceType string `json:"serviceType"` // 服务类型
|
||||||
|
}
|
||||||
|
|
||||||
// PanService 网盘服务接口
|
// PanService 网盘服务接口
|
||||||
type PanService interface {
|
type PanService interface {
|
||||||
// Transfer 转存分享链接
|
// Transfer 转存分享链接
|
||||||
@@ -64,6 +74,9 @@ type PanService interface {
|
|||||||
// DeleteFiles 删除文件
|
// DeleteFiles 删除文件
|
||||||
DeleteFiles(fileList []string) (*TransferResult, error)
|
DeleteFiles(fileList []string) (*TransferResult, error)
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
GetUserInfo(cookie string) (*UserInfo, error)
|
||||||
|
|
||||||
// GetServiceType 获取服务类型
|
// GetServiceType 获取服务类型
|
||||||
GetServiceType() ServiceType
|
GetServiceType() ServiceType
|
||||||
}
|
}
|
||||||
@@ -210,3 +223,46 @@ func ErrorResult(message string) *TransferResult {
|
|||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseCapacityString 解析容量字符串为字节数
|
||||||
|
func ParseCapacityString(capacityStr string) int64 {
|
||||||
|
if capacityStr == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除空格并转换为小写
|
||||||
|
capacityStr = strings.TrimSpace(strings.ToLower(capacityStr))
|
||||||
|
|
||||||
|
var multiplier int64 = 1
|
||||||
|
if strings.Contains(capacityStr, "gb") {
|
||||||
|
multiplier = 1024 * 1024 * 1024
|
||||||
|
capacityStr = strings.Replace(capacityStr, "gb", "", -1)
|
||||||
|
} else if strings.Contains(capacityStr, "mb") {
|
||||||
|
multiplier = 1024 * 1024
|
||||||
|
capacityStr = strings.Replace(capacityStr, "mb", "", -1)
|
||||||
|
} else if strings.Contains(capacityStr, "kb") {
|
||||||
|
multiplier = 1024
|
||||||
|
capacityStr = strings.Replace(capacityStr, "kb", "", -1)
|
||||||
|
} else if strings.Contains(capacityStr, "b") {
|
||||||
|
capacityStr = strings.Replace(capacityStr, "b", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析数字
|
||||||
|
capacityStr = strings.TrimSpace(capacityStr)
|
||||||
|
if capacityStr == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析浮点数
|
||||||
|
if strings.Contains(capacityStr, ".") {
|
||||||
|
if val, err := strconv.ParseFloat(capacityStr, 64); err == nil {
|
||||||
|
return int64(val * float64(multiplier))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if val, err := strconv.ParseInt(capacityStr, 10, 64); err == nil {
|
||||||
|
return val * multiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -614,3 +614,99 @@ type PasswordResult struct {
|
|||||||
Fid string `json:"fid"`
|
Fid string `json:"fid"`
|
||||||
} `json:"first_file"`
|
} `json:"first_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
func (q *QuarkPanService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||||
|
// 临时设置cookie
|
||||||
|
originalCookie := q.GetHeader("Cookie")
|
||||||
|
q.SetHeader("Cookie", cookie)
|
||||||
|
defer q.SetHeader("Cookie", originalCookie) // 恢复原始cookie
|
||||||
|
|
||||||
|
// 获取用户基本信息
|
||||||
|
queryParams := map[string]string{
|
||||||
|
"platform": "pc",
|
||||||
|
"fr": "pc",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := q.HTTPGet("https://pan.quark.cn/account/info", queryParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
AvatarUri string `json:"avatarUri"`
|
||||||
|
Mobilekps string `json:"mobilekps"`
|
||||||
|
Config struct{} `json:"config"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success || response.Code != "OK" {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: API返回错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详细信息(容量和会员信息)
|
||||||
|
queryParams1 := map[string]string{
|
||||||
|
"pr": "ucpro",
|
||||||
|
"fr": "pc",
|
||||||
|
"uc_param_str": "",
|
||||||
|
"fetch_subscribe": "true",
|
||||||
|
"_ch": "home",
|
||||||
|
"fetch_identity": "true",
|
||||||
|
}
|
||||||
|
data1, err := q.HTTPGet("https://drive-pc.quark.cn/1/clouddrive/member", queryParams1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户详细信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
TotalCapacity int64 `json:"secret_total_capacity"`
|
||||||
|
SecretUseCapacity int64 `json:"secret_use_capacity"`
|
||||||
|
MemberType string `json:"member_type"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data1, &memberResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析用户详细信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if memberResponse.Status != 200 || memberResponse.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("获取用户详细信息失败: %s", memberResponse.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断VIP状态
|
||||||
|
vipStatus := memberResponse.Data.MemberType != "NORMAL"
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
Username: response.Data.Nickname,
|
||||||
|
VIPStatus: vipStatus,
|
||||||
|
UsedSpace: memberResponse.Data.SecretUseCapacity,
|
||||||
|
TotalSpace: memberResponse.Data.TotalCapacity,
|
||||||
|
ServiceType: "quark",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatBytes 格式化字节数为可读格式
|
||||||
|
func formatBytes(bytes int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|||||||
52
common/quark_user_info_test.go
Normal file
52
common/quark_user_info_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package pan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestQuarkGetUserInfo 测试夸克网盘GetUserInfo功能
|
||||||
|
func TestQuarkGetUserInfo(t *testing.T) {
|
||||||
|
// 创建夸克网盘服务实例
|
||||||
|
service := NewQuarkPanService(&PanConfig{})
|
||||||
|
|
||||||
|
// 测试无效cookie
|
||||||
|
_, err := service.GetUserInfo("invalid_cookie")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("期望返回错误,但没有返回")
|
||||||
|
} else {
|
||||||
|
t.Logf("正确返回错误: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestQuarkGetUserInfoWithValidCookie 测试有效cookie的情况
|
||||||
|
func TestQuarkGetUserInfoWithValidCookie(t *testing.T) {
|
||||||
|
// 创建夸克网盘服务实例
|
||||||
|
service := NewQuarkPanService(&PanConfig{})
|
||||||
|
|
||||||
|
// 使用测试cookie(需要替换为有效的cookie)
|
||||||
|
testCookie := "your_test_cookie_here"
|
||||||
|
if testCookie == "your_test_cookie_here" {
|
||||||
|
t.Skip("跳过测试,需要提供有效的cookie")
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, err := service.GetUserInfo(testCookie)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("获取用户信息失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证返回的用户信息
|
||||||
|
if userInfo == nil {
|
||||||
|
t.Fatal("用户信息为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("用户名: %s", userInfo.Username)
|
||||||
|
t.Logf("VIP状态: %t", userInfo.VIPStatus)
|
||||||
|
t.Logf("容量信息: %s", userInfo.Capacity)
|
||||||
|
t.Logf("服务类型: %s", userInfo.ServiceType)
|
||||||
|
|
||||||
|
// 基本验证
|
||||||
|
if userInfo.ServiceType != "quark" {
|
||||||
|
t.Errorf("服务类型不匹配,期望: quark, 实际: %s", userInfo.ServiceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package pan
|
package pan
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// UCService UC网盘服务
|
// UCService UC网盘服务
|
||||||
type UCService struct {
|
type UCService struct {
|
||||||
*BasePanService
|
*BasePanService
|
||||||
@@ -44,3 +46,54 @@ func (u *UCService) DeleteFiles(fileList []string) (*TransferResult, error) {
|
|||||||
// TODO: 实现UC网盘文件删除
|
// TODO: 实现UC网盘文件删除
|
||||||
return ErrorResult("UC网盘文件删除功能暂未实现"), nil
|
return ErrorResult("UC网盘文件删除功能暂未实现"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
func (u *UCService) GetUserInfo(cookie string) (*UserInfo, error) {
|
||||||
|
// 设置Cookie
|
||||||
|
u.SetHeader("Cookie", cookie)
|
||||||
|
|
||||||
|
// 调用UC网盘用户信息API
|
||||||
|
userInfoURL := "https://drive.uc.cn/api/user/info"
|
||||||
|
|
||||||
|
resp, err := u.HTTPGet(userInfoURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
VipStatus int `json:"vip_status"`
|
||||||
|
TotalSpace int64 `json:"total_space"`
|
||||||
|
UsedSpace int64 `json:"used_space"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.ParseJSONResponse(resp, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("API返回错误: %d", result.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换VIP状态
|
||||||
|
vipStatus := result.Data.VipStatus > 0
|
||||||
|
|
||||||
|
// 使用nickname或username
|
||||||
|
username := result.Data.Nickname
|
||||||
|
if username == "" {
|
||||||
|
username = result.Data.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
Username: username,
|
||||||
|
VIPStatus: vipStatus,
|
||||||
|
UsedSpace: result.Data.UsedSpace,
|
||||||
|
TotalSpace: result.Data.TotalSpace,
|
||||||
|
ServiceType: "uc",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ func ToCksResponse(cks *entity.Cks) dto.CksResponse {
|
|||||||
IsValid: cks.IsValid,
|
IsValid: cks.IsValid,
|
||||||
Space: cks.Space,
|
Space: cks.Space,
|
||||||
LeftSpace: cks.LeftSpace,
|
LeftSpace: cks.LeftSpace,
|
||||||
|
UsedSpace: cks.UsedSpace,
|
||||||
|
Username: cks.Username,
|
||||||
|
VipStatus: cks.VipStatus,
|
||||||
|
ServiceType: cks.ServiceType,
|
||||||
Remark: cks.Remark,
|
Remark: cks.Remark,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ type CreateCksRequest struct {
|
|||||||
IsValid bool `json:"is_valid"`
|
IsValid bool `json:"is_valid"`
|
||||||
Space int64 `json:"space"`
|
Space int64 `json:"space"`
|
||||||
LeftSpace int64 `json:"left_space"`
|
LeftSpace int64 `json:"left_space"`
|
||||||
|
UsedSpace int64 `json:"used_space"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
VipStatus bool `json:"vip_status"`
|
||||||
|
ServiceType string `json:"service_type"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +39,10 @@ type UpdateCksRequest struct {
|
|||||||
IsValid bool `json:"is_valid"`
|
IsValid bool `json:"is_valid"`
|
||||||
Space int64 `json:"space"`
|
Space int64 `json:"space"`
|
||||||
LeftSpace int64 `json:"left_space"`
|
LeftSpace int64 `json:"left_space"`
|
||||||
|
UsedSpace int64 `json:"used_space"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
VipStatus bool `json:"vip_status"`
|
||||||
|
ServiceType string `json:"service_type"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ type CksResponse struct {
|
|||||||
IsValid bool `json:"is_valid"`
|
IsValid bool `json:"is_valid"`
|
||||||
Space int64 `json:"space"`
|
Space int64 `json:"space"`
|
||||||
LeftSpace int64 `json:"left_space"`
|
LeftSpace int64 `json:"left_space"`
|
||||||
|
UsedSpace int64 `json:"used_space"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
VipStatus bool `json:"vip_status"`
|
||||||
|
ServiceType string `json:"service_type"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ type Cks struct {
|
|||||||
Idx int `json:"idx" gorm:"comment:索引"`
|
Idx int `json:"idx" gorm:"comment:索引"`
|
||||||
Ck string `json:"ck" gorm:"type:text;comment:cookie"`
|
Ck string `json:"ck" gorm:"type:text;comment:cookie"`
|
||||||
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
||||||
Space int64 `json:"space" gorm:"default:0;comment:总空间"`
|
Space int64 `json:"space" gorm:"default:0;comment:总空间(字节)"`
|
||||||
LeftSpace int64 `json:"left_space" gorm:"default:0;comment:剩余空间"`
|
LeftSpace int64 `json:"left_space" gorm:"default:0;comment:剩余空间(字节)"`
|
||||||
|
UsedSpace int64 `json:"used_space" gorm:"default:0;comment:已使用空间(字节)"`
|
||||||
|
Username string `json:"username" gorm:"size:100;comment:用户名"`
|
||||||
|
VipStatus bool `json:"vip_status" gorm:"default:false;comment:VIP状态"`
|
||||||
|
ServiceType string `json:"service_type" gorm:"size:20;comment:服务类型"`
|
||||||
Remark string `json:"remark" gorm:"size:64;not null;comment:备注"`
|
Remark string `json:"remark" gorm:"size:64;not null;comment:备注"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
29
db/migration_add_used_space.sql
Normal file
29
db/migration_add_used_space.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-- 添加used_space字段
|
||||||
|
ALTER TABLE cks ADD COLUMN IF NOT EXISTS used_space BIGINT DEFAULT 0;
|
||||||
|
|
||||||
|
-- 更新现有数据,将字符串类型的容量字段转换为bigint
|
||||||
|
-- 注意:这里需要根据实际情况调整转换逻辑
|
||||||
|
UPDATE cks SET
|
||||||
|
used_space = CASE
|
||||||
|
WHEN used_space_text IS NOT NULL AND used_space_text != '' THEN
|
||||||
|
CASE
|
||||||
|
WHEN used_space_text LIKE '%GB%' THEN
|
||||||
|
CAST(REPLACE(REPLACE(used_space_text, 'GB', ''), ' ', '') AS BIGINT) * 1024 * 1024 * 1024
|
||||||
|
WHEN used_space_text LIKE '%MB%' THEN
|
||||||
|
CAST(REPLACE(REPLACE(used_space_text, 'MB', ''), ' ', '') AS BIGINT) * 1024 * 1024
|
||||||
|
WHEN used_space_text LIKE '%KB%' THEN
|
||||||
|
CAST(REPLACE(REPLACE(used_space_text, 'KB', ''), ' ', '') AS BIGINT) * 1024
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
WHERE used_space_text IS NOT NULL;
|
||||||
|
|
||||||
|
-- 删除旧的字符串类型字段(可选,建议先备份数据)
|
||||||
|
-- ALTER TABLE cks DROP COLUMN IF EXISTS capacity;
|
||||||
|
-- ALTER TABLE cks DROP COLUMN IF EXISTS used_space_text;
|
||||||
|
-- ALTER TABLE cks DROP COLUMN IF EXISTS total_space_text;
|
||||||
|
|
||||||
|
-- 添加索引以提高查询性能
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cks_used_space ON cks(used_space);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cks_space_left_space ON cks(space, left_space);
|
||||||
@@ -3,7 +3,9 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
panutils "res_db/common"
|
||||||
"res_db/db/converter"
|
"res_db/db/converter"
|
||||||
"res_db/db/dto"
|
"res_db/db/dto"
|
||||||
"res_db/db/entity"
|
"res_db/db/entity"
|
||||||
@@ -31,28 +33,116 @@ func CreateCks(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取平台信息以确定服务类型
|
||||||
|
pan, err := repoManager.PanRepository.FindByID(req.PanID)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "平台不存在", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据平台名称确定服务类型
|
||||||
|
var serviceType panutils.ServiceType
|
||||||
|
switch pan.Name {
|
||||||
|
case "quark":
|
||||||
|
serviceType = panutils.Quark
|
||||||
|
case "alipan":
|
||||||
|
serviceType = panutils.Alipan
|
||||||
|
case "baidu":
|
||||||
|
serviceType = panutils.BaiduPan
|
||||||
|
case "uc":
|
||||||
|
serviceType = panutils.UC
|
||||||
|
default:
|
||||||
|
ErrorResponse(c, "不支持的平台类型", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建网盘服务实例
|
||||||
|
factory := panutils.GetInstance()
|
||||||
|
service, err := factory.CreatePanServiceByType(serviceType, &panutils.PanConfig{})
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "创建网盘服务失败: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
userInfo, err := service.GetUserInfo(req.Ck)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "无法获取用户信息,账号创建失败: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||||
|
|
||||||
|
// 创建Cks实体
|
||||||
cks := &entity.Cks{
|
cks := &entity.Cks{
|
||||||
PanID: req.PanID,
|
PanID: req.PanID,
|
||||||
Idx: req.Idx,
|
Idx: req.Idx,
|
||||||
Ck: req.Ck,
|
Ck: req.Ck,
|
||||||
IsValid: req.IsValid,
|
IsValid: userInfo.VIPStatus, // 根据VIP状态设置有效性
|
||||||
Space: req.Space,
|
Space: userInfo.TotalSpace,
|
||||||
LeftSpace: req.LeftSpace,
|
LeftSpace: leftSpaceBytes,
|
||||||
|
UsedSpace: userInfo.UsedSpace,
|
||||||
|
Username: userInfo.Username,
|
||||||
|
VipStatus: userInfo.VIPStatus,
|
||||||
|
ServiceType: userInfo.ServiceType,
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := repoManager.CksRepository.Create(cks)
|
err = repoManager.CksRepository.Create(cks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(c, gin.H{
|
SuccessResponse(c, gin.H{
|
||||||
"message": "Cookie创建成功",
|
"message": "账号创建成功",
|
||||||
"cks": converter.ToCksResponse(cks),
|
"cks": converter.ToCksResponse(cks),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseCapacityToBytes 将容量字符串转换为字节数
|
||||||
|
func parseCapacityToBytes(capacity string) int64 {
|
||||||
|
if capacity == "未知" || capacity == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除空格并转换为小写
|
||||||
|
capacity = strings.TrimSpace(strings.ToLower(capacity))
|
||||||
|
|
||||||
|
var multiplier int64 = 1
|
||||||
|
if strings.Contains(capacity, "gb") {
|
||||||
|
multiplier = 1024 * 1024 * 1024
|
||||||
|
capacity = strings.Replace(capacity, "gb", "", -1)
|
||||||
|
} else if strings.Contains(capacity, "mb") {
|
||||||
|
multiplier = 1024 * 1024
|
||||||
|
capacity = strings.Replace(capacity, "mb", "", -1)
|
||||||
|
} else if strings.Contains(capacity, "kb") {
|
||||||
|
multiplier = 1024
|
||||||
|
capacity = strings.Replace(capacity, "kb", "", -1)
|
||||||
|
} else if strings.Contains(capacity, "b") {
|
||||||
|
capacity = strings.Replace(capacity, "b", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析数字
|
||||||
|
capacity = strings.TrimSpace(capacity)
|
||||||
|
if capacity == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析浮点数
|
||||||
|
if strings.Contains(capacity, ".") {
|
||||||
|
if val, err := strconv.ParseFloat(capacity, 64); err == nil {
|
||||||
|
return int64(val * float64(multiplier))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if val, err := strconv.ParseInt(capacity, 10, 64); err == nil {
|
||||||
|
return val * multiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// GetCksByID 根据ID获取Cookie详情
|
// GetCksByID 根据ID获取Cookie详情
|
||||||
func GetCksByID(c *gin.Context) {
|
func GetCksByID(c *gin.Context) {
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
@@ -103,12 +193,19 @@ func UpdateCks(c *gin.Context) {
|
|||||||
cks.Ck = req.Ck
|
cks.Ck = req.Ck
|
||||||
}
|
}
|
||||||
cks.IsValid = req.IsValid
|
cks.IsValid = req.IsValid
|
||||||
if req.Space != 0 {
|
|
||||||
cks.Space = req.Space
|
|
||||||
}
|
|
||||||
if req.LeftSpace != 0 {
|
if req.LeftSpace != 0 {
|
||||||
cks.LeftSpace = req.LeftSpace
|
cks.LeftSpace = req.LeftSpace
|
||||||
}
|
}
|
||||||
|
if req.UsedSpace != 0 {
|
||||||
|
cks.UsedSpace = req.UsedSpace
|
||||||
|
}
|
||||||
|
if req.Username != "" {
|
||||||
|
cks.Username = req.Username
|
||||||
|
}
|
||||||
|
cks.VipStatus = req.VipStatus
|
||||||
|
if req.ServiceType != "" {
|
||||||
|
cks.ServiceType = req.ServiceType
|
||||||
|
}
|
||||||
if req.Remark != "" {
|
if req.Remark != "" {
|
||||||
cks.Remark = req.Remark
|
cks.Remark = req.Remark
|
||||||
}
|
}
|
||||||
@@ -158,3 +255,80 @@ func GetCksByIDGlobal(c *gin.Context) {
|
|||||||
response := converter.ToCksResponse(cks)
|
response := converter.ToCksResponse(cks)
|
||||||
SuccessResponse(c, response)
|
SuccessResponse(c, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RefreshCapacity 刷新账号容量信息
|
||||||
|
func RefreshCapacity(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取账号信息
|
||||||
|
cks, err := repoManager.CksRepository.FindByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "账号不存在", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取平台信息以确定服务类型
|
||||||
|
pan, err := repoManager.PanRepository.FindByID(cks.PanID)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "平台不存在", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据平台名称确定服务类型
|
||||||
|
var serviceType panutils.ServiceType
|
||||||
|
switch pan.Name {
|
||||||
|
case "quark":
|
||||||
|
serviceType = panutils.Quark
|
||||||
|
case "alipan":
|
||||||
|
serviceType = panutils.Alipan
|
||||||
|
case "baidu":
|
||||||
|
serviceType = panutils.BaiduPan
|
||||||
|
case "uc":
|
||||||
|
serviceType = panutils.UC
|
||||||
|
default:
|
||||||
|
ErrorResponse(c, "不支持的平台类型", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建网盘服务实例
|
||||||
|
factory := panutils.GetInstance()
|
||||||
|
service, err := factory.CreatePanServiceByType(serviceType, &panutils.PanConfig{})
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "创建网盘服务失败: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新的用户信息
|
||||||
|
userInfo, err := service.GetUserInfo(cks.Ck)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, "无法获取用户信息,刷新失败: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
leftSpaceBytes := userInfo.TotalSpace - userInfo.UsedSpace
|
||||||
|
|
||||||
|
// 更新账号信息
|
||||||
|
cks.Username = userInfo.Username
|
||||||
|
cks.VipStatus = userInfo.VIPStatus
|
||||||
|
cks.ServiceType = userInfo.ServiceType
|
||||||
|
cks.Space = userInfo.TotalSpace
|
||||||
|
cks.LeftSpace = leftSpaceBytes
|
||||||
|
cks.UsedSpace = userInfo.UsedSpace
|
||||||
|
cks.IsValid = userInfo.VIPStatus // 根据VIP状态更新有效性
|
||||||
|
|
||||||
|
err = repoManager.CksRepository.Update(cks)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(c, gin.H{
|
||||||
|
"message": "容量信息刷新成功",
|
||||||
|
"cks": converter.ToCksResponse(cks),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -160,6 +160,7 @@ func main() {
|
|||||||
api.PUT("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateCks)
|
api.PUT("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateCks)
|
||||||
api.DELETE("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCks)
|
api.DELETE("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCks)
|
||||||
api.GET("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCksByID)
|
api.GET("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCksByID)
|
||||||
|
api.POST("/cks/:id/refresh-capacity", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.RefreshCapacity)
|
||||||
|
|
||||||
// 标签管理
|
// 标签管理
|
||||||
api.GET("/tags", handlers.GetTags)
|
api.GET("/tags", handlers.GetTags)
|
||||||
|
|||||||
@@ -1,290 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DB *sql.DB
|
|
||||||
|
|
||||||
// InitDB 初始化数据库连接
|
|
||||||
func InitDB() error {
|
|
||||||
host := os.Getenv("DB_HOST")
|
|
||||||
if host == "" {
|
|
||||||
host = "localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
port := os.Getenv("DB_PORT")
|
|
||||||
if port == "" {
|
|
||||||
port = "5432"
|
|
||||||
}
|
|
||||||
|
|
||||||
user := os.Getenv("DB_USER")
|
|
||||||
if user == "" {
|
|
||||||
user = "postgres"
|
|
||||||
}
|
|
||||||
|
|
||||||
password := os.Getenv("DB_PASSWORD")
|
|
||||||
if password == "" {
|
|
||||||
password = "password"
|
|
||||||
}
|
|
||||||
|
|
||||||
dbname := os.Getenv("DB_NAME")
|
|
||||||
if dbname == "" {
|
|
||||||
dbname = "res_db"
|
|
||||||
}
|
|
||||||
|
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
||||||
host, port, user, password, dbname)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
DB, err = sql.Open("postgres", psqlInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = DB.Ping(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建表
|
|
||||||
if err := createTables(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("数据库连接成功")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTables 创建数据库表
|
|
||||||
func createTables() error {
|
|
||||||
// 创建pan表
|
|
||||||
panTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS pan (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(64) DEFAULT NULL,
|
|
||||||
key INTEGER DEFAULT NULL,
|
|
||||||
icon VARCHAR(128) DEFAULT NULL,
|
|
||||||
remark VARCHAR(64) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建分类表
|
|
||||||
categoryTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL UNIQUE,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建标签表
|
|
||||||
tagTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS tags (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL UNIQUE,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建资源表 - 更新后的结构
|
|
||||||
resourceTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS resources (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
url VARCHAR(128),
|
|
||||||
pan_id INTEGER REFERENCES pan(id) ON DELETE SET NULL,
|
|
||||||
quark_url VARCHAR(500),
|
|
||||||
file_size VARCHAR(100),
|
|
||||||
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
|
||||||
view_count INTEGER DEFAULT 0,
|
|
||||||
is_valid BOOLEAN DEFAULT true,
|
|
||||||
is_public BOOLEAN DEFAULT true,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建资源标签关联表
|
|
||||||
resourceTagTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS resource_tags (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
resource_id INTEGER NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(resource_id, tag_id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
if _, err := DB.Exec(panTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(categoryTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(tagTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(resourceTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(resourceTagTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建cks表
|
|
||||||
cksTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS cks (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
pan_id INTEGER NOT NULL REFERENCES pan(id) ON DELETE CASCADE,
|
|
||||||
idx INTEGER DEFAULT NULL,
|
|
||||||
ck TEXT,
|
|
||||||
is_valid BOOLEAN DEFAULT true,
|
|
||||||
space BIGINT DEFAULT 0,
|
|
||||||
left_space BIGINT DEFAULT 0,
|
|
||||||
remark VARCHAR(64) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建待处理资源表
|
|
||||||
readyResourceTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS ready_resource (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
title VARCHAR(255),
|
|
||||||
url VARCHAR(500) NOT NULL,
|
|
||||||
category VARCHAR(100) DEFAULT NULL,
|
|
||||||
tags VARCHAR(500) DEFAULT NULL,
|
|
||||||
img VARCHAR(500) DEFAULT NULL,
|
|
||||||
source VARCHAR(100) DEFAULT NULL,
|
|
||||||
extra TEXT DEFAULT NULL,
|
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ip VARCHAR(45) DEFAULT NULL
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建搜索统计表
|
|
||||||
searchStatTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS search_stats (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
keyword VARCHAR(255) NOT NULL,
|
|
||||||
count INTEGER DEFAULT 1,
|
|
||||||
date DATE NOT NULL,
|
|
||||||
ip VARCHAR(45),
|
|
||||||
user_agent VARCHAR(500),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建系统配置表
|
|
||||||
systemConfigTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS system_configs (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
site_title VARCHAR(200) NOT NULL DEFAULT '网盘资源管理系统',
|
|
||||||
site_description VARCHAR(500),
|
|
||||||
keywords VARCHAR(500),
|
|
||||||
author VARCHAR(100),
|
|
||||||
copyright VARCHAR(200),
|
|
||||||
auto_process_ready_resources BOOLEAN DEFAULT false,
|
|
||||||
auto_process_interval INTEGER DEFAULT 30,
|
|
||||||
auto_transfer_enabled BOOLEAN DEFAULT false,
|
|
||||||
auto_fetch_hot_drama_enabled BOOLEAN DEFAULT false,
|
|
||||||
page_size INTEGER DEFAULT 100,
|
|
||||||
maintenance_mode BOOLEAN DEFAULT false,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
// 创建热播剧表
|
|
||||||
hotDramaTable := `
|
|
||||||
CREATE TABLE IF NOT EXISTS hot_dramas (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
rating DECIMAL(3,1) DEFAULT 0.0,
|
|
||||||
year VARCHAR(10),
|
|
||||||
directors VARCHAR(500),
|
|
||||||
actors VARCHAR(1000),
|
|
||||||
category VARCHAR(50),
|
|
||||||
sub_type VARCHAR(50),
|
|
||||||
source VARCHAR(50) DEFAULT 'douban',
|
|
||||||
douban_id VARCHAR(50),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`
|
|
||||||
|
|
||||||
if _, err := DB.Exec(panTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(cksTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(readyResourceTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(searchStatTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(systemConfigTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(hotDramaTable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入默认分类
|
|
||||||
insertDefaultCategories := `
|
|
||||||
INSERT INTO categories (name, description) VALUES
|
|
||||||
('电影', '电影资源'),
|
|
||||||
('电视剧', '电视剧资源'),
|
|
||||||
('动漫', '动漫资源'),
|
|
||||||
('音乐', '音乐资源'),
|
|
||||||
('软件', '软件资源'),
|
|
||||||
('PC游戏', 'PC游戏资源'),
|
|
||||||
('手机游戏', '手机游戏'),
|
|
||||||
('文档', '文档资源'),
|
|
||||||
('短剧', '短剧'),
|
|
||||||
('学习资源', '学习资源'),
|
|
||||||
('视频教程', '学习资源'),
|
|
||||||
('其他', '其他资源')
|
|
||||||
ON CONFLICT (name) DO NOTHING;`
|
|
||||||
|
|
||||||
// 插入默认网盘平台
|
|
||||||
insertDefaultPans := `
|
|
||||||
INSERT INTO pan (name, key, icon, remark) VALUES
|
|
||||||
('baidu', 1, '<i class="fas fa-cloud text-blue-500"></i>', '百度网盘'),
|
|
||||||
('aliyun', 2, '<i class="fas fa-cloud text-orange-500"></i>', '阿里云盘'),
|
|
||||||
('quark', 3, '<i class="fas fa-atom text-purple-500"></i>', '夸克网盘'),
|
|
||||||
('xunlei', 5, '<i class="fas fa-bolt text-yellow-500"></i>', '迅雷云盘'),
|
|
||||||
('lanzou', 7, '<i class="fas fa-cloud text-blue-400"></i>', '蓝奏云'),
|
|
||||||
('123', 8, '<i class="fas fa-cloud text-red-500"></i>', '123云盘'),
|
|
||||||
('ctfile', 9, '<i class="fas fa-folder text-yellow-600"></i>', '城通网盘'),
|
|
||||||
('115', 10, '<i class="fas fa-cloud-upload-alt text-green-600"></i>', '115网盘'),
|
|
||||||
('magnet', 11, '<i class="fas fa-magnet text-red-600"></i>', '磁力链接'),
|
|
||||||
('uc', 12, '<i class="fas fa-cloud-download-alt text-purple-600"></i>', 'UC网盘'),
|
|
||||||
('other', 13, '<i class="fas fa-cloud text-gray-500"></i>', '其他')
|
|
||||||
ON CONFLICT (name) DO NOTHING;`
|
|
||||||
|
|
||||||
if _, err := DB.Exec(insertDefaultCategories); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := DB.Exec(insertDefaultPans); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pan 第三方平台表
|
|
||||||
type Pan struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"` // Qurak
|
|
||||||
Key int `json:"key"` // quark
|
|
||||||
Icon string `json:"icon"` // 图标文字
|
|
||||||
Remark string `json:"remark"` // 备注
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cks 第三方平台账号cookie表
|
|
||||||
type Cks struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
PanID int `json:"pan_id"` // pan ID
|
|
||||||
Idx int `json:"idx"` // index
|
|
||||||
Ck string `json:"ck"` // cookie
|
|
||||||
IsValid bool `json:"is_valid"` // 是否有效
|
|
||||||
Space int64 `json:"space"` // 空间
|
|
||||||
LeftSpace int64 `json:"left_space"` // 剩余空间
|
|
||||||
Remark string `json:"remark"` // 备注
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePanRequest 创建平台请求
|
|
||||||
type CreatePanRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Key int `json:"key"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePanRequest 更新平台请求
|
|
||||||
type UpdatePanRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Key int `json:"key"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCksRequest 创建cookie请求
|
|
||||||
type CreateCksRequest struct {
|
|
||||||
PanID int `json:"pan_id" binding:"required"`
|
|
||||||
Idx int `json:"idx"`
|
|
||||||
Ck string `json:"ck"`
|
|
||||||
IsValid bool `json:"is_valid"`
|
|
||||||
Space int64 `json:"space"`
|
|
||||||
LeftSpace int64 `json:"left_space"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCksRequest 更新cookie请求
|
|
||||||
type UpdateCksRequest struct {
|
|
||||||
PanID int `json:"pan_id"`
|
|
||||||
Idx int `json:"idx"`
|
|
||||||
Ck string `json:"ck"`
|
|
||||||
IsValid bool `json:"is_valid"`
|
|
||||||
Space int64 `json:"space"`
|
|
||||||
LeftSpace int64 `json:"left_space"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tags 自定义类型,用于处理 PostgreSQL 数组
|
|
||||||
type Tags []string
|
|
||||||
|
|
||||||
// Value 实现 driver.Valuer 接口
|
|
||||||
func (t Tags) Value() (driver.Value, error) {
|
|
||||||
if t == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return pq.Array(t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan 实现 sql.Scanner 接口
|
|
||||||
func (t *Tags) Scan(value interface{}) error {
|
|
||||||
if value == nil {
|
|
||||||
*t = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := value.(type) {
|
|
||||||
case []byte:
|
|
||||||
// 处理 PostgreSQL 数组格式: {tag1,tag2,tag3}
|
|
||||||
if len(v) == 0 {
|
|
||||||
*t = Tags{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除 { } 并分割
|
|
||||||
s := string(v)
|
|
||||||
s = strings.Trim(s, "{}")
|
|
||||||
if s == "" {
|
|
||||||
*t = Tags{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := strings.Split(s, ",")
|
|
||||||
*t = Tags(tags)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case string:
|
|
||||||
// 处理字符串格式
|
|
||||||
if v == "" {
|
|
||||||
*t = Tags{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s := strings.Trim(v, "{}")
|
|
||||||
if s == "" {
|
|
||||||
*t = Tags{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tags := strings.Split(s, ",")
|
|
||||||
*t = Tags(tags)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("cannot scan %T into Tags", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resource 资源模型 - 更新后的结构
|
|
||||||
type Resource struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
URL string `json:"url"` // 单个URL字符串
|
|
||||||
PanID *int `json:"pan_id"` // 平台ID,标识链接类型
|
|
||||||
QuarkURL string `json:"quark_url"` // 新增字段
|
|
||||||
FileSize string `json:"file_size"` // 改为 string 类型
|
|
||||||
CategoryID *int `json:"category_id"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
ViewCount int `json:"view_count"`
|
|
||||||
IsValid bool `json:"is_valid"` // 新增字段
|
|
||||||
IsPublic bool `json:"is_public"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag 标签模型
|
|
||||||
type Tag struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResourceTag 资源标签关联表
|
|
||||||
type ResourceTag struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
ResourceID int `json:"resource_id"`
|
|
||||||
TagID int `json:"tag_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category 分类模型
|
|
||||||
type Category struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateResourceRequest 创建资源请求
|
|
||||||
type CreateResourceRequest struct {
|
|
||||||
Title string `json:"title" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
PanID *int `json:"pan_id"` // 平台ID
|
|
||||||
QuarkURL string `json:"quark_url"`
|
|
||||||
FileSize string `json:"file_size"`
|
|
||||||
CategoryID *int `json:"category_id"`
|
|
||||||
IsValid bool `json:"is_valid"`
|
|
||||||
IsPublic bool `json:"is_public"`
|
|
||||||
TagIDs []int `json:"tag_ids"` // 标签ID列表
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateResourceRequest 更新资源请求
|
|
||||||
type UpdateResourceRequest struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
PanID *int `json:"pan_id"` // 平台ID
|
|
||||||
QuarkURL string `json:"quark_url"`
|
|
||||||
FileSize string `json:"file_size"`
|
|
||||||
CategoryID *int `json:"category_id"`
|
|
||||||
IsValid bool `json:"is_valid"`
|
|
||||||
IsPublic bool `json:"is_public"`
|
|
||||||
TagIDs []int `json:"tag_ids"` // 标签ID列表
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTagRequest 创建标签请求
|
|
||||||
type CreateTagRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTagRequest 更新标签请求
|
|
||||||
type UpdateTagRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCategoryRequest 创建分类请求
|
|
||||||
type CreateCategoryRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCategoryRequest 更新分类请求
|
|
||||||
type UpdateCategoryRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchRequest 搜索请求
|
|
||||||
type SearchRequest struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
CategoryID *int `json:"category_id"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchResponse 搜索响应
|
|
||||||
type SearchResponse struct {
|
|
||||||
Resources []Resource `json:"resources"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadyResource 待处理资源模型
|
|
||||||
type ReadyResource struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Title *string `json:"title"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
CreateTime time.Time `json:"create_time"`
|
|
||||||
IP *string `json:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateReadyResourceRequest 创建待处理资源请求
|
|
||||||
type CreateReadyResourceRequest struct {
|
|
||||||
Title *string `json:"title"`
|
|
||||||
URL string `json:"url" binding:"required"`
|
|
||||||
IP *string `json:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchCreateReadyResourceRequest 批量创建待处理资源请求
|
|
||||||
type BatchCreateReadyResourceRequest struct {
|
|
||||||
Resources []CreateReadyResourceRequest `json:"resources" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats 统计信息
|
|
||||||
type Stats struct {
|
|
||||||
TotalResources int `json:"total_resources"`
|
|
||||||
TotalCategories int `json:"total_categories"`
|
|
||||||
TotalDownloads int `json:"total_downloads"`
|
|
||||||
TotalViews int `json:"total_views"`
|
|
||||||
}
|
|
||||||
@@ -331,12 +331,22 @@ export const useCksApi = () => {
|
|||||||
return parseApiResponse(response)
|
return parseApiResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshCapacity = async (id: number) => {
|
||||||
|
const response = await $fetch(`/cks/${id}/refresh-capacity`, {
|
||||||
|
baseURL: config.public.apiBase,
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders() as Record<string, string>
|
||||||
|
})
|
||||||
|
return parseApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCks,
|
getCks,
|
||||||
getCksByID,
|
getCksByID,
|
||||||
createCks,
|
createCks,
|
||||||
updateCks,
|
updateCks,
|
||||||
deleteCks,
|
deleteCks,
|
||||||
|
refreshCapacity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<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 shadow-lg">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||||
<div class="text-center">
|
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||||
<h1 class="text-3xl sm:text-4xl font-bold mb-4">
|
|
||||||
网盘资源管理系统 - API文档
|
网盘资源管理系统 - API文档
|
||||||
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-gray-300 max-w-2xl mx-auto">
|
<p class="text-gray-300 max-w-2xl mx-auto">公开API接口文档,支持资源添加、搜索和热门剧获取等功能</p>
|
||||||
公开API接口文档,支持资源添加、搜索和热门剧获取等功能
|
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-2 right-4 top-0 absolute">
|
||||||
</p>
|
<NuxtLink to="/" class="hidden sm:flex">
|
||||||
<div class="mt-6 flex flex-col sm:flex-row gap-4 justify-center">
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
<NuxtLink
|
<i class="fas fa-home text-xs"></i> 首页
|
||||||
to="/"
|
</n-button>
|
||||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors text-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-home mr-2"></i>返回首页
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
<NuxtLink to="/hot-dramas" class="hidden sm:flex">
|
||||||
</div>
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
</div>
|
<i class="fas fa-film text-xs"></i> 热播剧
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/monitor" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-chart-line text-xs"></i> 系统监控
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主要内容 -->
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<!-- 认证说明 -->
|
<!-- 认证说明 -->
|
||||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6 mb-8">
|
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6 mb-8">
|
||||||
<h2 class="text-xl font-semibold text-blue-800 dark:text-blue-200 mb-4 flex items-center">
|
<h2 class="text-xl font-semibold text-blue-800 dark:text-blue-200 mb-4 flex items-center">
|
||||||
@@ -338,11 +344,13 @@ curl -X GET "http://localhost:8080/api/public/hot-dramas?page=1&page_size=5" \
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<footer class="bg-gray-800 dark:bg-gray-900 text-gray-300 py-8 mt-12">
|
<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 px-4 sm:px-6 lg:px-8 text-center">
|
<div class="max-w-7xl mx-auto text-center text-gray-600 dark:text-gray-400 text-sm px-3 sm:px-5">
|
||||||
<p>© 2024 网盘资源管理系统. 保留所有权利.</p>
|
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||||
|
<p>© 2025 网盘资源管理系统 By 老九</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,72 +1,180 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 p-3 sm:p-5">
|
||||||
<div class="max-w-7xl mx-auto">
|
<!-- 全局加载状态 -->
|
||||||
<!-- 头部 -->
|
<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 shadow p-6 mb-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-xl">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">第三方平台账号管理</h1>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
<div class="flex gap-2">
|
<div class="text-center">
|
||||||
<NuxtLink
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">正在加载...</h3>
|
||||||
to="/admin"
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">请稍候,正在加载账号数据</p>
|
||||||
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
</div>
|
||||||
>
|
|
||||||
返回管理
|
|
||||||
</NuxtLink>
|
|
||||||
<button
|
|
||||||
@click="showCreateModal = true"
|
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
添加账号
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 账号列表 -->
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
|
<!-- 头部 -->
|
||||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center flex items-center">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">账号列表</h2>
|
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin"
|
||||||
|
class="w-full sm:w-auto px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors text-center flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left"></i> 返回
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold">
|
||||||
|
<NuxtLink to="/admin" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">平台账号管理</NuxtLink>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i> 添加账号
|
||||||
|
</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"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<i class="fas fa-refresh"></i> 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 账号列表 -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table class="w-full min-w-full">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
<thead>
|
||||||
<tr>
|
<tr class="bg-slate-800 dark:bg-gray-700 text-white dark:text-gray-100">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ID</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">ID</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">平台</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">平台</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">索引</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">用户名</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">状态</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">总空间</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">总空间</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">剩余空间</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">已使用</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">备注</th>
|
<th class="px-4 py-3 text-left text-sm font-medium">剩余空间</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</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">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr v-for="cks in cksList" :key="cks.id">
|
<tr v-if="loading" class="text-center py-8">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ cks.id }}</td>
|
<td colspan="10" class="text-gray-500 dark:text-gray-400">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<i class="fas fa-spinner fa-spin mr-2"></i>加载中...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="filteredCksList.length === 0" class="text-center py-8">
|
||||||
|
<td colspan="10" class="text-gray-500 dark:text-gray-400">
|
||||||
|
<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 mb-4">你可以点击上方"添加账号"按钮创建新账号</div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i> 添加账号
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="cks in filteredCksList"
|
||||||
|
:key="cks.id"
|
||||||
|
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 font-medium">{{ cks.id }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span v-html="getPlatformIcon(cks.pan?.name || '')" class="mr-2"></span>
|
<span v-html="getPlatformIcon(cks.pan?.name || '')" class="mr-2"></span>
|
||||||
{{ cks.pan?.name || '未知平台' }}
|
{{ cks.pan?.name || '未知平台' }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ cks.idx || '-' }}</td>
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<span v-if="cks.username" :title="cks.username">{{ cks.username }}</span>
|
||||||
|
<span v-else class="text-gray-400 dark:text-gray-500 italic">未知用户</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
<span :class="cks.is_valid ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200'"
|
<span :class="cks.is_valid ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200'"
|
||||||
class="px-2 py-1 text-xs font-medium rounded-full">
|
class="px-2 py-1 text-xs font-medium rounded-full">
|
||||||
{{ cks.is_valid ? '有效' : '无效' }}
|
{{ cks.is_valid ? '有效' : '无效' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
{{ formatFileSize(cks.space) }}
|
{{ formatFileSize(cks.space) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
|
{{ formatFileSize(cks.space - cks.left_space) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
{{ formatFileSize(cks.left_space) }}
|
{{ formatFileSize(cks.left_space) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ cks.remark || '-' }}</td>
|
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<div class="flex items-center">
|
||||||
<button @click="editCks(cks)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3">编辑</button>
|
<div class="w-16 bg-gray-200 rounded-full h-2 mr-2">
|
||||||
<button @click="deleteCks(cks.id)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">删除</button>
|
<div
|
||||||
|
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: getUsagePercentage(cks) + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500">{{ getUsagePercentage(cks).toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span v-if="cks.remark" :title="cks.remark">{{ cks.remark }}</span>
|
||||||
|
<span v-else class="text-gray-400 dark:text-gray-500 italic">无备注</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="refreshCapacity(cks.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-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editCks(cks)"
|
||||||
|
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||||
|
title="编辑账号"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteCks(cks.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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -74,6 +182,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="totalPages > 1" class="flex flex-wrap justify-center gap-1 sm:gap-2 mt-6">
|
||||||
|
<button
|
||||||
|
v-if="currentPage > 1"
|
||||||
|
@click="goToPage(currentPage - 1)"
|
||||||
|
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left mr-1"></i> 上一页
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="goToPage(1)"
|
||||||
|
:class="currentPage === 1 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
|
||||||
|
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
@click="goToPage(2)"
|
||||||
|
:class="currentPage === 2 ? 'bg-slate-800 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'"
|
||||||
|
class="px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span v-if="currentPage > 2" class="px-2 py-1 sm:px-3 sm:py-2 text-gray-500 text-sm">...</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="currentPage !== 1 && currentPage !== 2 && currentPage > 2"
|
||||||
|
class="bg-slate-800 text-white px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{{ currentPage }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="currentPage < totalPages"
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
class="bg-white text-gray-700 hover:bg-gray-50 px-2 py-1 sm:px-4 sm:py-2 rounded border transition-colors text-sm flex items-center"
|
||||||
|
>
|
||||||
|
下一页 <i class="fas fa-chevron-right ml-1"></i>
|
||||||
|
</button>
|
||||||
|
</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">{{ filteredCksList.length }}</span> 个账号
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 创建/编辑账号模态框 -->
|
<!-- 创建/编辑账号模态框 -->
|
||||||
<div v-if="showCreateModal || showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
<div v-if="showCreateModal || showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||||
@@ -84,10 +247,12 @@
|
|||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">平台 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">平台类型 <span class="text-red-500">*</span></label>
|
||||||
<select
|
<select
|
||||||
v-model="form.pan_id"
|
v-model="form.pan_id"
|
||||||
required
|
required
|
||||||
|
:disabled="showEditModal"
|
||||||
|
:class="showEditModal ? 'bg-gray-100 dark:bg-gray-600 cursor-not-allowed' : ''"
|
||||||
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"
|
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 value="">请选择平台</option>
|
||||||
@@ -95,6 +260,13 @@
|
|||||||
{{ pan.name }}
|
{{ pan.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<p v-if="showEditModal" class="mt-1 text-xs text-gray-500 dark:text-gray-400">编辑时不允许修改平台类型</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="showEditModal && editingCks?.username">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||||
|
<div class="mt-1 px-3 py-2 bg-gray-100 dark:bg-gray-600 rounded-md text-sm text-gray-900 dark:text-gray-100">
|
||||||
|
{{ editingCks.username }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Cookie <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Cookie <span class="text-red-500">*</span></label>
|
||||||
@@ -103,38 +275,9 @@
|
|||||||
required
|
required
|
||||||
rows="4"
|
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"
|
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="请输入Cookie内容"
|
placeholder="请输入Cookie内容,系统将自动识别容量"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">索引</label>
|
|
||||||
<input
|
|
||||||
v-model.number="form.idx"
|
|
||||||
type="number"
|
|
||||||
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>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">总空间 (GB)</label>
|
|
||||||
<input
|
|
||||||
v-model.number="form.space"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
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>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">剩余空间 (GB)</label>
|
|
||||||
<input
|
|
||||||
v-model.number="form.left_space"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
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>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">备注</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">备注</label>
|
||||||
<input
|
<input
|
||||||
@@ -144,7 +287,7 @@
|
|||||||
placeholder="可选,备注信息"
|
placeholder="可选,备注信息"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="showEditModal">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="form.is_valid"
|
v-model="form.is_valid"
|
||||||
@@ -165,9 +308,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
: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"
|
||||||
>
|
>
|
||||||
{{ showEditModal ? '更新' : '创建' }}
|
{{ submitting ? '处理中...' : (showEditModal ? '更新' : '创建') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -175,7 +319,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -193,14 +336,20 @@ const showEditModal = ref(false)
|
|||||||
const editingCks = ref(null)
|
const editingCks = ref(null)
|
||||||
const form = ref({
|
const form = ref({
|
||||||
pan_id: '',
|
pan_id: '',
|
||||||
idx: null,
|
|
||||||
ck: '',
|
ck: '',
|
||||||
is_valid: true,
|
is_valid: true,
|
||||||
space: null,
|
|
||||||
left_space: null,
|
|
||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 搜索和分页逻辑
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const itemsPerPage = ref(10)
|
||||||
|
const totalPages = ref(1)
|
||||||
|
const loading = ref(true)
|
||||||
|
const pageLoading = ref(true)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
// 检查认证
|
// 检查认证
|
||||||
const checkAuth = () => {
|
const checkAuth = () => {
|
||||||
userStore.initAuth()
|
userStore.initAuth()
|
||||||
@@ -212,6 +361,7 @@ const checkAuth = () => {
|
|||||||
|
|
||||||
// 获取账号列表
|
// 获取账号列表
|
||||||
const fetchCks = async () => {
|
const fetchCks = async () => {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const { useCksApi } = await import('~/composables/useApi')
|
const { useCksApi } = await import('~/composables/useApi')
|
||||||
const cksApi = useCksApi()
|
const cksApi = useCksApi()
|
||||||
@@ -219,6 +369,9 @@ const fetchCks = async () => {
|
|||||||
cksList.value = Array.isArray(response) ? response : []
|
cksList.value = Array.isArray(response) ? response : []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取账号列表失败:', error)
|
console.error('获取账号列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
pageLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +389,7 @@ const fetchPlatforms = async () => {
|
|||||||
|
|
||||||
// 创建账号
|
// 创建账号
|
||||||
const createCks = async () => {
|
const createCks = async () => {
|
||||||
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const { useCksApi } = await import('~/composables/useApi')
|
const { useCksApi } = await import('~/composables/useApi')
|
||||||
const cksApi = useCksApi()
|
const cksApi = useCksApi()
|
||||||
@@ -245,11 +399,14 @@ const createCks = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建账号失败:', error)
|
console.error('创建账号失败:', error)
|
||||||
alert('创建账号失败: ' + (error.message || '未知错误'))
|
alert('创建账号失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新账号
|
// 更新账号
|
||||||
const updateCks = async () => {
|
const updateCks = async () => {
|
||||||
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const { useCksApi } = await import('~/composables/useApi')
|
const { useCksApi } = await import('~/composables/useApi')
|
||||||
const cksApi = useCksApi()
|
const cksApi = useCksApi()
|
||||||
@@ -259,6 +416,8 @@ const updateCks = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新账号失败:', error)
|
console.error('更新账号失败:', error)
|
||||||
alert('更新账号失败: ' + (error.message || '未知错误'))
|
alert('更新账号失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,17 +436,30 @@ const deleteCks = async (id) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新容量
|
||||||
|
const refreshCapacity = async (id) => {
|
||||||
|
if (!confirm('确定要刷新此账号的容量信息吗?')) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { useCksApi } = await import('~/composables/useApi')
|
||||||
|
const cksApi = useCksApi()
|
||||||
|
await cksApi.refreshCapacity(id)
|
||||||
|
await fetchCks()
|
||||||
|
alert('容量信息已刷新!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新容量失败:', error)
|
||||||
|
alert('刷新容量失败: ' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑账号
|
// 编辑账号
|
||||||
const editCks = (cks) => {
|
const editCks = (cks) => {
|
||||||
editingCks.value = cks
|
editingCks.value = cks
|
||||||
form.value = {
|
form.value = {
|
||||||
pan_id: cks.pan_id,
|
pan_id: cks.pan_id,
|
||||||
idx: cks.idx,
|
|
||||||
ck: cks.ck,
|
ck: cks.ck,
|
||||||
is_valid: cks.is_valid,
|
is_valid: cks.is_valid,
|
||||||
space: cks.space,
|
remark: cks.remark || ''
|
||||||
left_space: cks.left_space,
|
|
||||||
remark: cks.remark
|
|
||||||
}
|
}
|
||||||
showEditModal.value = true
|
showEditModal.value = true
|
||||||
}
|
}
|
||||||
@@ -299,21 +471,18 @@ const closeModal = () => {
|
|||||||
editingCks.value = null
|
editingCks.value = null
|
||||||
form.value = {
|
form.value = {
|
||||||
pan_id: '',
|
pan_id: '',
|
||||||
idx: null,
|
|
||||||
ck: '',
|
ck: '',
|
||||||
is_valid: true,
|
is_valid: true,
|
||||||
space: null,
|
|
||||||
left_space: null,
|
|
||||||
remark: ''
|
remark: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
if (showEditModal.value) {
|
if (showEditModal.value) {
|
||||||
updateCks()
|
await updateCks()
|
||||||
} else {
|
} else {
|
||||||
createCks()
|
await createCks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,9 +526,62 @@ const formatFileSize = (bytes) => {
|
|||||||
return bytes + ' B'
|
return bytes + ' B'
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 获取使用率百分比
|
||||||
checkAuth()
|
const getUsagePercentage = (cks) => {
|
||||||
|
if (!cks.space || cks.space === 0) return 0
|
||||||
|
const used = cks.used_space || (cks.space - cks.left_space)
|
||||||
|
return (used / cks.space) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤和分页计算
|
||||||
|
const filteredCksList = computed(() => {
|
||||||
|
let filtered = cksList.value
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(cks =>
|
||||||
|
cks.pan?.name?.toLowerCase().includes(query) ||
|
||||||
|
cks.remark?.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
totalPages.value = Math.ceil(filtered.length / itemsPerPage.value)
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage.value
|
||||||
|
const end = start + itemsPerPage.value
|
||||||
|
return filtered.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 防抖搜索
|
||||||
|
let searchTimeout = null
|
||||||
|
const debounceSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
currentPage.value = 1
|
||||||
fetchCks()
|
fetchCks()
|
||||||
fetchPlatforms()
|
fetchPlatforms()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页跳转
|
||||||
|
const goToPage = (page) => {
|
||||||
|
currentPage.value = page
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
checkAuth()
|
||||||
|
await Promise.all([
|
||||||
|
fetchCks(),
|
||||||
|
fetchPlatforms()
|
||||||
|
])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('页面初始化失败:', error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1,10 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<!-- 主要内容区域 -->
|
||||||
<!-- 页面标题 -->
|
<div class="flex-1 p-3 sm:p-5">
|
||||||
<div class="mb-8">
|
<div class="max-w-7xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">热播剧榜单</h1>
|
<!-- 头部 -->
|
||||||
<p class="text-gray-600 dark:text-gray-400">实时获取豆瓣热门电影和电视剧榜单</p>
|
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||||
|
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||||
|
热播剧榜单
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-300 max-w-2xl mx-auto">实时获取豆瓣热门电影和电视剧榜单</p>
|
||||||
|
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-2 right-4 top-0 absolute">
|
||||||
|
<NuxtLink to="/" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-home text-xs"></i> 首页
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/monitor" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-chart-line text-xs"></i> 系统监控
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/api-docs" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-book text-xs"></i> API文档
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 统计信息 -->
|
<!-- 统计信息 -->
|
||||||
@@ -173,6 +196,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<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">
|
||||||
|
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||||
|
<p>© 2025 网盘资源管理系统 By 老九</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 flex flex-col">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<!-- 主要内容区域 -->
|
||||||
<!-- 页面标题 -->
|
<div class="flex-1 p-3 sm:p-5">
|
||||||
<div class="mb-8">
|
<div class="max-w-7xl mx-auto">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">系统性能监控</h1>
|
<!-- 头部 -->
|
||||||
<p class="text-gray-600 dark:text-gray-400">实时监控系统运行状态和性能指标</p>
|
<div class="bg-slate-800 dark:bg-gray-800 text-white dark:text-gray-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||||
|
<a href="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline">
|
||||||
|
系统性能监控
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-300 max-w-2xl mx-auto">实时监控系统运行状态和性能指标</p>
|
||||||
|
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-2 right-4 top-0 absolute">
|
||||||
|
<NuxtLink to="/" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-home text-xs"></i> 首页
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/hot-dramas" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-film text-xs"></i> 热播剧
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/api-docs" class="hidden sm:flex">
|
||||||
|
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||||||
|
<i class="fas fa-book text-xs"></i> API文档
|
||||||
|
</n-button>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 刷新按钮 -->
|
||||||
@@ -209,6 +232,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<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">
|
||||||
|
<p class="mb-2">本站内容由网络爬虫自动抓取。本站不储存、复制、传播任何文件,仅作个人公益学习,请在获取后24小内删除!!!</p>
|
||||||
|
<p>© 2025 网盘资源管理系统 By 老九</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user