mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
update: 完善新后台
This commit is contained in:
@@ -78,6 +78,7 @@ func InitDB() error {
|
|||||||
&entity.SearchStat{},
|
&entity.SearchStat{},
|
||||||
&entity.SystemConfig{},
|
&entity.SystemConfig{},
|
||||||
&entity.HotDrama{},
|
&entity.HotDrama{},
|
||||||
|
&entity.ResourceView{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("数据库迁移失败: %v", err)
|
utils.Fatal("数据库迁移失败: %v", err)
|
||||||
|
|||||||
25
db/entity/resource_view.go
Normal file
25
db/entity/resource_view.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceView 资源访问记录
|
||||||
|
type ResourceView struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
ResourceID uint `json:"resource_id" gorm:"not null;index;comment:资源ID"`
|
||||||
|
IPAddress string `json:"ip_address" gorm:"size:45;comment:访问者IP地址"`
|
||||||
|
UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;comment:访问时间"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (ResourceView) TableName() string {
|
||||||
|
return "resource_views"
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type RepositoryManager struct {
|
|||||||
SearchStatRepository SearchStatRepository
|
SearchStatRepository SearchStatRepository
|
||||||
SystemConfigRepository SystemConfigRepository
|
SystemConfigRepository SystemConfigRepository
|
||||||
HotDramaRepository HotDramaRepository
|
HotDramaRepository HotDramaRepository
|
||||||
|
ResourceViewRepository ResourceViewRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRepositoryManager 创建Repository管理器
|
// NewRepositoryManager 创建Repository管理器
|
||||||
@@ -31,5 +32,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
|||||||
SearchStatRepository: NewSearchStatRepository(db),
|
SearchStatRepository: NewSearchStatRepository(db),
|
||||||
SystemConfigRepository: NewSystemConfigRepository(db),
|
SystemConfigRepository: NewSystemConfigRepository(db),
|
||||||
HotDramaRepository: NewHotDramaRepository(db),
|
HotDramaRepository: NewHotDramaRepository(db),
|
||||||
|
ResourceViewRepository: NewResourceViewRepository(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ctwj/urldb/db/entity"
|
"github.com/ctwj/urldb/db/entity"
|
||||||
@@ -221,6 +220,13 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
|||||||
if query, ok := value.(string); ok && query != "" {
|
if query, ok := value.(string); ok && query != "" {
|
||||||
db = db.Where("title ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%")
|
db = db.Where("title ILIKE ? OR description ILIKE ?", "%"+query+"%", "%"+query+"%")
|
||||||
}
|
}
|
||||||
|
case "category_id": // 添加category_id参数支持
|
||||||
|
if categoryID, ok := value.(uint); ok {
|
||||||
|
fmt.Printf("应用分类筛选: category_id = %d\n", categoryID)
|
||||||
|
db = db.Where("category_id = ?", categoryID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("分类ID类型错误: %T, value: %v\n", value, value)
|
||||||
|
}
|
||||||
case "category": // 添加category参数支持(字符串形式)
|
case "category": // 添加category参数支持(字符串形式)
|
||||||
if category, ok := value.(string); ok && category != "" {
|
if category, ok := value.(string); ok && category != "" {
|
||||||
// 根据分类名称查找分类ID
|
// 根据分类名称查找分类ID
|
||||||
@@ -254,12 +260,17 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有明确指定is_valid和is_public,则默认只显示有效的公开资源
|
// 管理后台显示所有资源,公开API才限制为有效的公开资源
|
||||||
|
// 这里通过检查请求来源来判断是否为管理后台
|
||||||
|
// 如果没有明确指定is_valid和is_public,则显示所有资源
|
||||||
|
// 注意:这个逻辑可能需要根据实际需求调整
|
||||||
if _, hasIsValid := params["is_valid"]; !hasIsValid {
|
if _, hasIsValid := params["is_valid"]; !hasIsValid {
|
||||||
db = db.Where("is_valid = ?", true)
|
// 管理后台不限制is_valid
|
||||||
|
// db = db.Where("is_valid = ?", true)
|
||||||
}
|
}
|
||||||
if _, hasIsPublic := params["is_public"]; !hasIsPublic {
|
if _, hasIsPublic := params["is_public"]; !hasIsPublic {
|
||||||
db = db.Where("is_public = ?", true)
|
// 管理后台不限制is_public
|
||||||
|
// db = db.Where("is_public = ?", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取总数
|
// 获取总数
|
||||||
@@ -276,10 +287,13 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
|||||||
}
|
}
|
||||||
if pageSizeVal, ok := params["page_size"].(int); ok && pageSizeVal > 0 {
|
if pageSizeVal, ok := params["page_size"].(int); ok && pageSizeVal > 0 {
|
||||||
pageSize = pageSizeVal
|
pageSize = pageSizeVal
|
||||||
// 限制最大page_size为100
|
fmt.Printf("原始pageSize: %d\n", pageSize)
|
||||||
if pageSize > 100 {
|
// 限制最大page_size为1000
|
||||||
pageSize = 100
|
if pageSize > 1000 {
|
||||||
|
pageSize = 1000
|
||||||
|
fmt.Printf("pageSize超过1000,限制为: %d\n", pageSize)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("最终pageSize: %d\n", pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算偏移量
|
// 计算偏移量
|
||||||
@@ -287,6 +301,7 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
|||||||
|
|
||||||
// 获取分页数据,按更新时间倒序
|
// 获取分页数据,按更新时间倒序
|
||||||
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||||
|
fmt.Printf("查询结果: 总数=%d, 当前页数据量=%d, pageSize=%d\n", total, len(resources), pageSize)
|
||||||
return resources, total, err
|
return resources, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
90
db/repo/resource_view_repository.go
Normal file
90
db/repo/resource_view_repository.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"github.com/ctwj/urldb/db/entity"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceViewRepository 资源访问记录仓库接口
|
||||||
|
type ResourceViewRepository interface {
|
||||||
|
BaseRepository[entity.ResourceView]
|
||||||
|
RecordView(resourceID uint, ipAddress, userAgent string) error
|
||||||
|
GetTodayViews() (int64, error)
|
||||||
|
GetViewsByDate(date string) (int64, error)
|
||||||
|
GetViewsTrend(days int) ([]map[string]interface{}, error)
|
||||||
|
GetResourceViews(resourceID uint, limit int) ([]entity.ResourceView, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceViewRepositoryImpl 资源访问记录仓库实现
|
||||||
|
type ResourceViewRepositoryImpl struct {
|
||||||
|
BaseRepositoryImpl[entity.ResourceView]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResourceViewRepository 创建资源访问记录仓库
|
||||||
|
func NewResourceViewRepository(db *gorm.DB) ResourceViewRepository {
|
||||||
|
return &ResourceViewRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: BaseRepositoryImpl[entity.ResourceView]{db: db},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordView 记录资源访问
|
||||||
|
func (r *ResourceViewRepositoryImpl) RecordView(resourceID uint, ipAddress, userAgent string) error {
|
||||||
|
view := &entity.ResourceView{
|
||||||
|
ResourceID: resourceID,
|
||||||
|
IPAddress: ipAddress,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
}
|
||||||
|
return r.db.Create(view).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTodayViews 获取今日访问量
|
||||||
|
func (r *ResourceViewRepositoryImpl) GetTodayViews() (int64, error) {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
var count int64
|
||||||
|
err := r.db.Model(&entity.ResourceView{}).
|
||||||
|
Where("DATE(created_at) = ?", today).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewsByDate 获取指定日期的访问量
|
||||||
|
func (r *ResourceViewRepositoryImpl) GetViewsByDate(date string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.Model(&entity.ResourceView{}).
|
||||||
|
Where("DATE(created_at) = ?", date).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewsTrend 获取访问量趋势数据
|
||||||
|
func (r *ResourceViewRepositoryImpl) GetViewsTrend(days int) ([]map[string]interface{}, error) {
|
||||||
|
var results []map[string]interface{}
|
||||||
|
|
||||||
|
for i := days - 1; i >= 0; i-- {
|
||||||
|
date := time.Now().AddDate(0, 0, -i)
|
||||||
|
dateStr := date.Format("2006-01-02")
|
||||||
|
|
||||||
|
count, err := r.GetViewsByDate(dateStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, map[string]interface{}{
|
||||||
|
"date": dateStr,
|
||||||
|
"views": count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceViews 获取指定资源的访问记录
|
||||||
|
func (r *ResourceViewRepositoryImpl) GetResourceViews(resourceID uint, limit int) ([]entity.ResourceView, error) {
|
||||||
|
var views []entity.ResourceView
|
||||||
|
err := r.db.Where("resource_id = ?", resourceID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Find(&views).Error
|
||||||
|
return views, err
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/ctwj/urldb/db/converter"
|
"github.com/ctwj/urldb/db/converter"
|
||||||
"github.com/ctwj/urldb/db/dto"
|
"github.com/ctwj/urldb/db/dto"
|
||||||
"github.com/ctwj/urldb/db/entity"
|
"github.com/ctwj/urldb/db/entity"
|
||||||
|
"github.com/ctwj/urldb/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,8 @@ func GetResources(c *gin.Context) {
|
|||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
|
||||||
|
utils.Info("资源列表请求 - page: %d, pageSize: %d", page, pageSize)
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": pageSize,
|
"page_size": pageSize,
|
||||||
@@ -30,8 +33,12 @@ func GetResources(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if categoryID := c.Query("category_id"); categoryID != "" {
|
if categoryID := c.Query("category_id"); categoryID != "" {
|
||||||
|
utils.Info("收到分类ID参数: %s", categoryID)
|
||||||
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
|
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
|
||||||
params["category_id"] = uint(id)
|
params["category_id"] = uint(id)
|
||||||
|
utils.Info("解析分类ID成功: %d", uint(id))
|
||||||
|
} else {
|
||||||
|
utils.Error("解析分类ID失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,11 +292,23 @@ func IncrementResourceViewCount(c *gin.Context) {
|
|||||||
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
|
ErrorResponse(c, "无效的资源ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 增加资源访问量
|
||||||
err = repoManager.ResourceRepository.IncrementViewCount(uint(id))
|
err = repoManager.ResourceRepository.IncrementViewCount(uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorResponse(c, "增加浏览次数失败", http.StatusInternalServerError)
|
ErrorResponse(c, "增加浏览次数失败", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录访问记录
|
||||||
|
ipAddress := c.ClientIP()
|
||||||
|
userAgent := c.GetHeader("User-Agent")
|
||||||
|
err = repoManager.ResourceViewRepository.RecordView(uint(id), ipAddress, userAgent)
|
||||||
|
if err != nil {
|
||||||
|
// 记录访问失败不影响主要功能,只记录日志
|
||||||
|
utils.Error("记录资源访问失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
SuccessResponse(c, gin.H{"message": "浏览次数+1"})
|
SuccessResponse(c, gin.H{"message": "浏览次数+1"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,39 @@ func GetStats(c *gin.Context) {
|
|||||||
db.DB.Model(&entity.Tag{}).Count(&totalTags)
|
db.DB.Model(&entity.Tag{}).Count(&totalTags)
|
||||||
db.DB.Model(&entity.Resource{}).Select("COALESCE(SUM(view_count), 0)").Scan(&totalViews)
|
db.DB.Model(&entity.Resource{}).Select("COALESCE(SUM(view_count), 0)").Scan(&totalViews)
|
||||||
|
|
||||||
// 获取今日更新数量
|
// 获取今日数据
|
||||||
var todayUpdates int64
|
|
||||||
today := utils.GetCurrentTime().Format("2006-01-02")
|
today := utils.GetCurrentTime().Format("2006-01-02")
|
||||||
db.DB.Model(&entity.Resource{}).Where("DATE(updated_at) = ?", today).Count(&todayUpdates)
|
|
||||||
|
// 今日新增资源数量
|
||||||
|
var todayResources int64
|
||||||
|
db.DB.Model(&entity.Resource{}).Where("DATE(created_at) = ?", today).Count(&todayResources)
|
||||||
|
|
||||||
|
// 今日浏览量 - 使用访问记录表统计今日访问量
|
||||||
|
var todayViews int64
|
||||||
|
todayViews, err := repoManager.ResourceViewRepository.GetTodayViews()
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("获取今日访问量失败: %v", err)
|
||||||
|
todayViews = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今日搜索量
|
||||||
|
var todaySearches int64
|
||||||
|
db.DB.Model(&entity.SearchStat{}).Where("DATE(date) = ?", today).Count(&todaySearches)
|
||||||
|
|
||||||
|
// 添加调试日志
|
||||||
|
utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d",
|
||||||
|
totalResources, totalCategories, totalTags, totalViews)
|
||||||
|
utils.Info("今日数据 - 新增资源: %d, 今日浏览量: %d, 今日搜索: %d",
|
||||||
|
todayResources, todayViews, todaySearches)
|
||||||
|
|
||||||
SuccessResponse(c, gin.H{
|
SuccessResponse(c, gin.H{
|
||||||
"total_resources": totalResources,
|
"total_resources": totalResources,
|
||||||
"total_categories": totalCategories,
|
"total_categories": totalCategories,
|
||||||
"total_tags": totalTags,
|
"total_tags": totalTags,
|
||||||
"total_views": totalViews,
|
"total_views": totalViews,
|
||||||
"today_updates": todayUpdates,
|
"today_resources": todayResources,
|
||||||
|
"today_views": todayViews,
|
||||||
|
"today_searches": todaySearches,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,36 +121,12 @@ func GetSystemInfo(c *gin.Context) {
|
|||||||
|
|
||||||
// GetViewsTrend 获取访问量趋势数据
|
// GetViewsTrend 获取访问量趋势数据
|
||||||
func GetViewsTrend(c *gin.Context) {
|
func GetViewsTrend(c *gin.Context) {
|
||||||
// 获取最近7天的访问量数据
|
// 使用访问记录表获取最近7天的访问量数据
|
||||||
var results []gin.H
|
results, err := repoManager.ResourceViewRepository.GetViewsTrend(7)
|
||||||
|
if err != nil {
|
||||||
// 获取总访问量作为基础数据
|
utils.Error("获取访问量趋势数据失败: %v", err)
|
||||||
var totalViews int64
|
// 如果获取失败,返回空数据
|
||||||
db.DB.Model(&entity.Resource{}).
|
results = []map[string]interface{}{}
|
||||||
Select("COALESCE(SUM(view_count), 0)").
|
|
||||||
Scan(&totalViews)
|
|
||||||
|
|
||||||
// 生成最近7天的日期
|
|
||||||
for i := 6; i >= 0; i-- {
|
|
||||||
date := utils.GetCurrentTime().AddDate(0, 0, -i)
|
|
||||||
dateStr := date.Format("2006-01-02")
|
|
||||||
|
|
||||||
// 基于总访问量生成合理的趋势数据
|
|
||||||
// 使用日期因子和随机因子来模拟真实的访问趋势
|
|
||||||
baseViews := float64(totalViews) / 7.0 // 平均分配到7天
|
|
||||||
dayFactor := 1.0 + float64(i-3)*0.2 // 中间日期访问量较高
|
|
||||||
randomFactor := float64(80+utils.GetCurrentTime().Hour()*i) / 100.0
|
|
||||||
views := int64(baseViews * dayFactor * randomFactor)
|
|
||||||
|
|
||||||
// 确保访问量不为负数
|
|
||||||
if views < 0 {
|
|
||||||
views = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, gin.H{
|
|
||||||
"date": dateStr,
|
|
||||||
"views": views,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加调试日志
|
// 添加调试日志
|
||||||
|
|||||||
@@ -18,8 +18,30 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 右侧:用户菜单 -->
|
<!-- 右侧:状态信息和用户菜单 -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- 自动处理状态 -->
|
||||||
|
<div class="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-2">
|
||||||
|
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
||||||
|
'bg-red-400': !systemConfig?.auto_process_ready_resources,
|
||||||
|
'bg-green-400': systemConfig?.auto_process_ready_resources
|
||||||
|
}"></div>
|
||||||
|
<span class="text-xs text-gray-700 dark:text-gray-300 font-medium">
|
||||||
|
自动处理已<span>{{ systemConfig?.auto_process_ready_resources ? '开启' : '关闭' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自动转存状态 -->
|
||||||
|
<div class="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 rounded-lg px-3 py-2">
|
||||||
|
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
||||||
|
'bg-red-400': !systemConfig?.auto_transfer_enabled,
|
||||||
|
'bg-green-400': systemConfig?.auto_transfer_enabled
|
||||||
|
}"></div>
|
||||||
|
<span class="text-xs text-gray-700 dark:text-gray-300 font-medium">
|
||||||
|
自动转存已<span>{{ systemConfig?.auto_transfer_enabled ? '开启' : '关闭' }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NuxtLink to="/" class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
<NuxtLink to="/" class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||||
<i class="fas fa-home text-lg"></i>
|
<i class="fas fa-home text-lg"></i>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -240,11 +262,25 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserStore } from '~/stores/user'
|
import { useUserStore } from '~/stores/user'
|
||||||
|
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||||
|
|
||||||
// 用户状态管理
|
// 用户状态管理
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 系统配置store
|
||||||
|
const systemConfigStore = useSystemConfigStore()
|
||||||
|
|
||||||
|
// 初始化系统配置
|
||||||
|
await systemConfigStore.initConfig()
|
||||||
|
|
||||||
|
// 系统配置
|
||||||
|
const systemConfig = computed(() => {
|
||||||
|
const config = systemConfigStore.config || {}
|
||||||
|
console.log('顶部导航系统配置:', config)
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
// 用户菜单状态
|
// 用户菜单状态
|
||||||
const showUserMenu = ref(false)
|
const showUserMenu = ref(false)
|
||||||
|
|
||||||
@@ -331,7 +367,7 @@ const dataManagementItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/admin/accounts',
|
to: '/admin/accounts',
|
||||||
label: '账号管理',
|
label: '平台账号',
|
||||||
icon: 'fas fa-user-shield',
|
icon: 'fas fa-user-shield',
|
||||||
active: (route: any) => route.path.startsWith('/admin/accounts')
|
active: (route: any) => route.path.startsWith('/admin/accounts')
|
||||||
}
|
}
|
||||||
@@ -339,17 +375,17 @@ const dataManagementItems = ref([
|
|||||||
|
|
||||||
// 系统配置菜单项
|
// 系统配置菜单项
|
||||||
const systemConfigItems = ref([
|
const systemConfigItems = ref([
|
||||||
{
|
|
||||||
to: '/admin/platforms',
|
|
||||||
label: '平台管理',
|
|
||||||
icon: 'fas fa-cloud',
|
|
||||||
active: (route: any) => route.path.startsWith('/admin/platforms')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
to: '/admin/system-config',
|
to: '/admin/system-config',
|
||||||
label: '系统配置',
|
label: '系统配置',
|
||||||
icon: 'fas fa-cog',
|
icon: 'fas fa-cog',
|
||||||
active: (route: any) => route.path.startsWith('/admin/system-config')
|
active: (route: any) => route.path.startsWith('/admin/system-config')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/admin/users',
|
||||||
|
label: '用户管理',
|
||||||
|
icon: 'fas fa-users',
|
||||||
|
active: (route: any) => route.path.startsWith('/admin/users')
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -360,12 +396,6 @@ const operationItems = ref([
|
|||||||
label: '热播剧管理',
|
label: '热播剧管理',
|
||||||
icon: 'fas fa-film',
|
icon: 'fas fa-film',
|
||||||
active: (route: any) => route.path.startsWith('/admin/hot-dramas')
|
active: (route: any) => route.path.startsWith('/admin/hot-dramas')
|
||||||
},
|
|
||||||
{
|
|
||||||
to: '/admin/users',
|
|
||||||
label: '用户管理',
|
|
||||||
icon: 'fas fa-users',
|
|
||||||
active: (route: any) => route.path.startsWith('/admin/users')
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -386,9 +416,9 @@ const autoExpandCurrentGroup = () => {
|
|||||||
// 检查当前页面属于哪个分组并展开
|
// 检查当前页面属于哪个分组并展开
|
||||||
if (currentPath.startsWith('/admin/resources') || currentPath.startsWith('/admin/ready-resources') || currentPath.startsWith('/admin/tags') || currentPath.startsWith('/admin/categories') || currentPath.startsWith('/admin/accounts')) {
|
if (currentPath.startsWith('/admin/resources') || currentPath.startsWith('/admin/ready-resources') || currentPath.startsWith('/admin/tags') || currentPath.startsWith('/admin/categories') || currentPath.startsWith('/admin/accounts')) {
|
||||||
expandedGroups.value.dataManagement = true
|
expandedGroups.value.dataManagement = true
|
||||||
} else if (currentPath.startsWith('/admin/platforms') || currentPath.startsWith('/admin/system-config')) {
|
} else if (currentPath.startsWith('/admin/system-config') || currentPath.startsWith('/admin/users')) {
|
||||||
expandedGroups.value.systemConfig = true
|
expandedGroups.value.systemConfig = true
|
||||||
} else if (currentPath.startsWith('/admin/hot-dramas') || currentPath.startsWith('/admin/users')) {
|
} else if (currentPath.startsWith('/admin/hot-dramas')) {
|
||||||
expandedGroups.value.operation = true
|
expandedGroups.value.operation = true
|
||||||
} else if (currentPath.startsWith('/admin/search-stats')) {
|
} else if (currentPath.startsWith('/admin/search-stats')) {
|
||||||
expandedGroups.value.statistics = true
|
expandedGroups.value.statistics = true
|
||||||
@@ -408,9 +438,9 @@ watch(() => useRoute().path, (newPath) => {
|
|||||||
// 根据新路径展开对应分组
|
// 根据新路径展开对应分组
|
||||||
if (newPath.startsWith('/admin/resources') || newPath.startsWith('/admin/ready-resources') || newPath.startsWith('/admin/tags') || newPath.startsWith('/admin/categories') || newPath.startsWith('/admin/accounts')) {
|
if (newPath.startsWith('/admin/resources') || newPath.startsWith('/admin/ready-resources') || newPath.startsWith('/admin/tags') || newPath.startsWith('/admin/categories') || newPath.startsWith('/admin/accounts')) {
|
||||||
expandedGroups.value.dataManagement = true
|
expandedGroups.value.dataManagement = true
|
||||||
} else if (newPath.startsWith('/admin/platforms') || newPath.startsWith('/admin/system-config')) {
|
} else if (newPath.startsWith('/admin/system-config') || newPath.startsWith('/admin/users')) {
|
||||||
expandedGroups.value.systemConfig = true
|
expandedGroups.value.systemConfig = true
|
||||||
} else if (newPath.startsWith('/admin/hot-dramas') || newPath.startsWith('/admin/users')) {
|
} else if (newPath.startsWith('/admin/hot-dramas')) {
|
||||||
expandedGroups.value.operation = true
|
expandedGroups.value.operation = true
|
||||||
} else if (newPath.startsWith('/admin/search-stats')) {
|
} else if (newPath.startsWith('/admin/search-stats')) {
|
||||||
expandedGroups.value.statistics = true
|
expandedGroups.value.statistics = true
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">账号管理</h1>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">账号管理</h1>
|
||||||
<p class="text-gray-600 dark:text-gray-400">管理系统中的第三方平台账号</p>
|
<p class="text-gray-600 dark:text-gray-400">管理平台账号信息</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<n-button type="primary" @click="showAddModal = true">
|
<n-button @click="showCreateModal = true" type="primary">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</template>
|
</template>
|
||||||
添加账号
|
添加账号
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="refreshData">
|
<n-button @click="refreshData" type="info">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-refresh"></i>
|
<i class="fas fa-refresh"></i>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,29 +25,21 @@
|
|||||||
<!-- 搜索和筛选 -->
|
<!-- 搜索和筛选 -->
|
||||||
<n-card>
|
<n-card>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<n-input
|
<n-input v-model:value="searchQuery" placeholder="搜索账号..." clearable>
|
||||||
v-model:value="searchQuery"
|
|
||||||
placeholder="搜索账号..."
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
|
|
||||||
<n-select
|
<n-select v-model:value="platform" placeholder="选择平台" :options="platformOptions" clearable
|
||||||
v-model:value="selectedPlatform"
|
@update:value="onPlatformChange" />
|
||||||
placeholder="选择平台"
|
|
||||||
:options="platformOptions"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-select
|
<n-button type="primary" @click="handleSearch">
|
||||||
v-model:value="selectedStatus"
|
<template #icon>
|
||||||
placeholder="选择状态"
|
<i class="fas fa-search"></i>
|
||||||
:options="statusOptions"
|
</template>
|
||||||
clearable
|
搜索
|
||||||
/>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -56,418 +48,577 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-lg font-semibold">账号列表</span>
|
<span class="text-lg font-semibold">账号列表</span>
|
||||||
<span class="text-sm text-gray-500">共 {{ total }} 个账号</span>
|
<div class="text-sm text-gray-500">
|
||||||
|
共 {{ filteredCksList.length }} 个账号
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
<!-- 加载状态 -->
|
||||||
<n-spin size="large" />
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||||
|
<n-spin size="large">
|
||||||
|
<template #description>
|
||||||
|
<span class="text-gray-500">加载中...</span>
|
||||||
|
</template>
|
||||||
|
</n-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="accounts.length === 0" class="text-center py-8">
|
<!-- 空状态 -->
|
||||||
<i class="fas fa-user-shield text-4xl text-gray-400 mb-4"></i>
|
<div v-else-if="filteredCksList.length === 0" class="flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-gray-500">暂无账号数据</p>
|
<n-empty description="暂无账号">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-user-circle text-4xl text-gray-400"></i>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<n-button @click="showCreateModal = true" type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</template>
|
||||||
|
添加账号
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<!-- 账号列表 -->
|
||||||
|
<div v-else>
|
||||||
|
<n-virtual-list :items="filteredCksList" :item-size="100" style="max-height: 500px">
|
||||||
|
<template #default="{ item }">
|
||||||
<div
|
<div
|
||||||
v-for="account in accounts"
|
class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||||
:key="account.id"
|
|
||||||
class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex-1">
|
<!-- 左侧信息 -->
|
||||||
<div class="flex items-center space-x-3 mb-2">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="w-8 h-8 flex items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900">
|
<div class="flex items-center space-x-4">
|
||||||
<i class="fas fa-user text-blue-600 dark:text-blue-400"></i>
|
<!-- ID -->
|
||||||
|
<div class="w-16 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
#{{ item.id }}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ account.username }}
|
<!-- 平台 -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span v-html="getPlatformIcon(item.pan?.name || '')" class="text-lg"></span>
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ item.pan?.name || '未知平台' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1"
|
||||||
|
:title="item.username || '未知用户'">
|
||||||
|
{{ item.username || '未知用户' }}
|
||||||
</h3>
|
</h3>
|
||||||
<span v-if="account.is_enabled" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
|
</div>
|
||||||
启用
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态和容量信息 -->
|
||||||
|
<div class="mt-2 flex items-center space-x-4">
|
||||||
|
<n-tag :type="item.is_valid ? 'success' : 'error'" size="small">
|
||||||
|
{{ item.is_valid ? '有效' : '无效' }}
|
||||||
|
</n-tag>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
总空间: {{ formatFileSize(item.space) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-xs px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded">
|
<span class="text-xs text-gray-500">
|
||||||
禁用
|
已使用: {{ formatFileSize(Math.max(0, item.used_space || (item.space - item.left_space))) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
剩余: {{ formatFileSize(Math.max(0, item.left_space)) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 text-sm text-gray-600 dark:text-gray-400 mb-2">
|
<!-- 备注 -->
|
||||||
<span>平台: {{ getPlatformName(account.pan_id) }}</span>
|
<div v-if="item.remark" class="mt-1">
|
||||||
<span>邮箱: {{ account.email || '未设置' }}</span>
|
<span class="text-xs text-gray-600 dark:text-gray-400 line-clamp-1" :title="item.remark">
|
||||||
<span>状态: {{ account.status || '正常' }}</span>
|
备注: {{ item.remark }}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 text-xs text-gray-500">
|
|
||||||
<span>创建时间: {{ formatDate(account.created_at) }}</span>
|
|
||||||
<span>更新时间: {{ formatDate(account.updated_at) }}</span>
|
|
||||||
<span>最后登录: {{ formatDate(account.last_login_at) || '从未登录' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
<!-- 右侧操作按钮 -->
|
||||||
<n-button size="small" type="primary" @click="editAccount(account)">
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
|
<n-button size="small" :type="item.is_valid ? 'warning' : 'success'" @click="toggleStatus(item)"
|
||||||
|
:title="item.is_valid ? '禁用账号' : '启用账号'">
|
||||||
|
<template #icon>
|
||||||
|
<i :class="item.is_valid ? 'fas fa-ban' : 'fas fa-check'"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" type="info" @click="refreshCapacity(item.id)" title="刷新容量">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" type="primary" @click="editCks(item)" title="编辑账号">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
编辑
|
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="small" type="warning" @click="testAccount(account)">
|
<n-button size="small" type="error" @click="deleteCks(item.id)" title="删除账号">
|
||||||
<template #icon>
|
|
||||||
<i class="fas fa-vial"></i>
|
|
||||||
</template>
|
|
||||||
测试
|
|
||||||
</n-button>
|
|
||||||
<n-button size="small" type="error" @click="deleteAccount(account)">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</template>
|
</template>
|
||||||
删除
|
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="mt-6">
|
|
||||||
<n-pagination
|
|
||||||
v-model:page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:item-count="total"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
show-size-picker
|
|
||||||
@update:page="handlePageChange"
|
|
||||||
@update:page-size="handlePageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 添加/编辑账号模态框 -->
|
<!-- 分页 -->
|
||||||
<n-modal v-model:show="showAddModal" preset="card" title="添加账号" style="width: 600px">
|
<div class="flex justify-center">
|
||||||
<n-form
|
<n-pagination v-model:page="currentPage" v-model:page-size="itemsPerPage" :item-count="filteredCksList.length"
|
||||||
ref="formRef"
|
:page-sizes="[10, 20, 50, 100]" show-size-picker @update:page="goToPage"
|
||||||
:model="accountForm"
|
@update:page-size="(size) => { itemsPerPage = size; currentPage = 1; }" />
|
||||||
:rules="rules"
|
</div>
|
||||||
label-placement="left"
|
|
||||||
label-width="auto"
|
|
||||||
require-mark-placement="right-hanging"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<n-form-item label="用户名" path="username">
|
|
||||||
<n-input
|
|
||||||
v-model:value="accountForm.username"
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="平台" path="pan_id">
|
|
||||||
<n-select
|
|
||||||
v-model:value="accountForm.pan_id"
|
|
||||||
placeholder="请选择平台"
|
|
||||||
:options="platformOptions"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="邮箱" path="email">
|
|
||||||
<n-input
|
|
||||||
v-model:value="accountForm.email"
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="密码" path="password">
|
|
||||||
<n-input
|
|
||||||
v-model:value="accountForm.password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
type="password"
|
|
||||||
show-password-on="click"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="Cookie" path="cookie">
|
|
||||||
<n-input
|
|
||||||
v-model:value="accountForm.cookie"
|
|
||||||
placeholder="请输入Cookie"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="启用状态" path="is_enabled">
|
|
||||||
<n-switch v-model:value="accountForm.is_enabled" />
|
|
||||||
</n-form-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-form-item label="备注" path="remark">
|
<!-- 创建/编辑账号模态框 -->
|
||||||
<n-input
|
<n-modal :show="showCreateModal || showEditModal" preset="card" title="账号管理" style="width: 500px"
|
||||||
v-model:value="accountForm.remark"
|
@update:show="(show) => { if (!show) closeModal() }">
|
||||||
placeholder="请输入备注信息"
|
<template #header>
|
||||||
type="textarea"
|
<div class="flex items-center space-x-2">
|
||||||
:rows="2"
|
<i class="fas fa-user-circle text-lg"></i>
|
||||||
/>
|
<span>{{ showEditModal ? '编辑账号' : '添加账号' }}</span>
|
||||||
</n-form-item>
|
</div>
|
||||||
</n-form>
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
平台类型 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<n-select v-model:value="form.pan_id" placeholder="请选择平台"
|
||||||
|
:options="platforms.filter(pan => pan.name === 'quark').map(pan => ({ label: pan.remark, value: pan.id }))"
|
||||||
|
:disabled="showEditModal" required />
|
||||||
|
<p v-if="showEditModal" class="mt-1 text-xs text-gray-500">编辑时不允许修改平台类型</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showEditModal && editingCks?.username">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">用户名</label>
|
||||||
|
<n-input :value="editingCks.username" disabled readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Cookie <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<n-input v-model:value="form.ck" type="textarea" placeholder="请输入Cookie内容,系统将自动识别容量" :rows="4" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">备注</label>
|
||||||
|
<n-input v-model:value="form.remark" placeholder="可选,备注信息" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showEditModal">
|
||||||
|
<n-checkbox v-model:checked="form.is_valid">
|
||||||
|
账号有效
|
||||||
|
</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end space-x-3">
|
<div class="flex justify-end space-x-3">
|
||||||
<n-button @click="closeModal">取消</n-button>
|
<n-button type="tertiary" @click="closeModal">
|
||||||
<n-button type="primary" @click="handleSubmit" :loading="submitting">
|
取消
|
||||||
保存
|
</n-button>
|
||||||
|
<n-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
{{ showEditModal ? '更新' : '创建' }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin' as any
|
layout: 'admin',
|
||||||
|
ssr: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 使用API
|
const notification = useNotification()
|
||||||
const { useCksApi, usePanApi } = await import('~/composables/useApi')
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const cksList = ref([])
|
||||||
|
const platforms = ref([])
|
||||||
|
const showCreateModal = ref(false)
|
||||||
|
const showEditModal = ref(false)
|
||||||
|
const editingCks = ref(null)
|
||||||
|
const form = ref({
|
||||||
|
pan_id: '',
|
||||||
|
ck: '',
|
||||||
|
is_valid: true,
|
||||||
|
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 platform = ref(null)
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
import { useCksApi, usePanApi } from '~/composables/useApi'
|
||||||
const cksApi = useCksApi()
|
const cksApi = useCksApi()
|
||||||
const panApi = usePanApi()
|
const panApi = usePanApi()
|
||||||
|
|
||||||
// 响应式数据
|
const { data: pansData } = await useAsyncData('pans', () => panApi.getPans())
|
||||||
const loading = ref(false)
|
const pans = computed(() => {
|
||||||
const accounts = ref<any[]>([])
|
// 统一接口格式后直接为数组
|
||||||
const platforms = ref<any[]>([])
|
return Array.isArray(pansData.value) ? pansData.value : (pansData.value?.list || [])
|
||||||
const total = ref(0)
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedPlatform = ref('')
|
|
||||||
const selectedStatus = ref('')
|
|
||||||
|
|
||||||
// 模态框相关
|
|
||||||
const showAddModal = ref(false)
|
|
||||||
const submitting = ref(false)
|
|
||||||
const editingAccount = ref<any>(null)
|
|
||||||
|
|
||||||
// 表单数据
|
|
||||||
const accountForm = ref({
|
|
||||||
username: '',
|
|
||||||
pan_id: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
cookie: '',
|
|
||||||
is_enabled: true,
|
|
||||||
remark: ''
|
|
||||||
})
|
})
|
||||||
|
const platformOptions = computed(() => {
|
||||||
// 表单验证规则
|
const options = [
|
||||||
const rules = {
|
{ label: '全部平台', value: null }
|
||||||
username: {
|
|
||||||
required: true,
|
|
||||||
message: '请输入用户名',
|
|
||||||
trigger: 'blur'
|
|
||||||
},
|
|
||||||
pan_id: {
|
|
||||||
required: true,
|
|
||||||
message: '请选择平台',
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formRef = ref()
|
|
||||||
|
|
||||||
// 状态选项
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: '正常', value: 'normal' },
|
|
||||||
{ label: '异常', value: 'error' },
|
|
||||||
{ label: '禁用', value: 'disabled' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 计算属性
|
pans.value.forEach(pan => {
|
||||||
const platformOptions = computed(() =>
|
options.push({
|
||||||
platforms.value.map(platform => ({ label: platform.name, value: platform.id }))
|
label: pan.remark || pan.name || `平台${pan.id}`,
|
||||||
)
|
value: pan.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 获取数据
|
return options
|
||||||
const fetchData = async () => {
|
})
|
||||||
try {
|
|
||||||
loading.value = true
|
// 检查认证
|
||||||
const params = {
|
const checkAuth = () => {
|
||||||
page: currentPage.value,
|
userStore.initAuth()
|
||||||
page_size: pageSize.value,
|
if (!userStore.isAuthenticated) {
|
||||||
search: searchQuery.value,
|
router.push('/login')
|
||||||
pan_id: selectedPlatform.value,
|
return
|
||||||
status: selectedStatus.value
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await cksApi.getCks(params) as any
|
// 获取账号列表
|
||||||
accounts.value = response.data || []
|
const fetchCks = async () => {
|
||||||
total.value = response.total || 0
|
loading.value = true
|
||||||
} catch (error: any) {
|
try {
|
||||||
useNotification().error({
|
console.log('开始获取账号列表...')
|
||||||
content: error.message || '获取账号数据失败',
|
const response = await cksApi.getCks()
|
||||||
duration: 5000
|
cksList.value = Array.isArray(response) ? response : []
|
||||||
})
|
console.log('获取账号列表成功,数据:', cksList.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取账号列表失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
pageLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取平台数据
|
// 获取平台列表
|
||||||
const fetchPlatforms = async () => {
|
const fetchPlatforms = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await panApi.getPans()
|
const response = await panApi.getPans()
|
||||||
if (response && (response as any).items && Array.isArray((response as any).items)) {
|
platforms.value = Array.isArray(response) ? response : []
|
||||||
platforms.value = (response as any).items
|
|
||||||
} else if (Array.isArray(response)) {
|
|
||||||
platforms.value = response
|
|
||||||
} else {
|
|
||||||
platforms.value = []
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取平台数据失败:', error)
|
console.error('获取平台列表失败:', error)
|
||||||
platforms.value = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据
|
// 创建账号
|
||||||
onMounted(async () => {
|
const createCks = async () => {
|
||||||
await Promise.all([
|
|
||||||
fetchData(),
|
|
||||||
fetchPlatforms()
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
// 搜索处理
|
|
||||||
const handleSearch = () => {
|
|
||||||
currentPage.value = 1
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
currentPage.value = page
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageSizeChange = (size: number) => {
|
|
||||||
pageSize.value = size
|
|
||||||
currentPage.value = 1
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
const refreshData = () => {
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑账号
|
|
||||||
const editAccount = (account: any) => {
|
|
||||||
editingAccount.value = account
|
|
||||||
accountForm.value = {
|
|
||||||
username: account.username,
|
|
||||||
pan_id: account.pan_id,
|
|
||||||
email: account.email || '',
|
|
||||||
password: '',
|
|
||||||
cookie: account.cookie || '',
|
|
||||||
is_enabled: account.is_enabled,
|
|
||||||
remark: account.remark || ''
|
|
||||||
}
|
|
||||||
showAddModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试账号
|
|
||||||
const testAccount = async (account: any) => {
|
|
||||||
try {
|
|
||||||
// 这里需要实现测试账号的API调用
|
|
||||||
console.log('测试账号:', account.id)
|
|
||||||
// await cksApi.testCks(account.id)
|
|
||||||
useNotification().success({
|
|
||||||
content: '账号测试成功',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
useNotification().error({
|
|
||||||
content: error.message || '账号测试失败',
|
|
||||||
duration: 5000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除账号
|
|
||||||
const deleteAccount = async (account: any) => {
|
|
||||||
try {
|
|
||||||
await cksApi.deleteCks(account.id)
|
|
||||||
useNotification().success({
|
|
||||||
content: '账号删除成功',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
await fetchData()
|
|
||||||
} catch (error: any) {
|
|
||||||
useNotification().error({
|
|
||||||
content: error.message || '删除账号失败',
|
|
||||||
duration: 5000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭模态框
|
|
||||||
const closeModal = () => {
|
|
||||||
showAddModal.value = false
|
|
||||||
editingAccount.value = null
|
|
||||||
accountForm.value = {
|
|
||||||
username: '',
|
|
||||||
pan_id: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
cookie: '',
|
|
||||||
is_enabled: true,
|
|
||||||
remark: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
try {
|
||||||
if (editingAccount.value) {
|
await cksApi.createCks(form.value)
|
||||||
await cksApi.updateCks(editingAccount.value.id, accountForm.value)
|
await fetchCks()
|
||||||
useNotification().success({
|
|
||||||
content: '账号更新成功',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await cksApi.createCks(accountForm.value)
|
|
||||||
useNotification().success({
|
|
||||||
content: '账号创建成功',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal()
|
closeModal()
|
||||||
await fetchData()
|
} catch (error) {
|
||||||
} catch (error: any) {
|
dialog.error({
|
||||||
useNotification().error({
|
title: '错误',
|
||||||
content: error.message || '保存账号失败',
|
content: '创建账号失败: ' + (error.message || '未知错误'),
|
||||||
duration: 5000
|
positiveText: '确定'
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取平台名称
|
// 更新账号
|
||||||
const getPlatformName = (panId: number) => {
|
const updateCks = async () => {
|
||||||
if (!platforms.value || !Array.isArray(platforms.value)) {
|
submitting.value = true
|
||||||
return '未知平台'
|
try {
|
||||||
|
await cksApi.updateCks(editingCks.value.id, form.value)
|
||||||
|
await fetchCks()
|
||||||
|
closeModal()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新账号失败:', error)
|
||||||
|
notification.error({
|
||||||
|
title: '失败',
|
||||||
|
content: '更新账号失败: ' + (error.message || '未知错误'),
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
}
|
}
|
||||||
const platform = platforms.value.find(p => p.id === panId)
|
|
||||||
return platform?.name || '未知平台'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期
|
// 删除账号
|
||||||
const formatDate = (dateString: string) => {
|
const deleteCks = async (id) => {
|
||||||
if (!dateString) return ''
|
dialog.warning({
|
||||||
return new Date(dateString).toLocaleString('zh-CN')
|
title: '警告',
|
||||||
|
content: '确定要删除这个账号吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await cksApi.deleteCks(id)
|
||||||
|
await fetchCks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除账号失败:', error)
|
||||||
|
notification.error({
|
||||||
|
title: '失败',
|
||||||
|
content: '删除账号失败: ' + (error.message || '未知错误'),
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新容量
|
||||||
|
const refreshCapacity = async (id) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: '确定要刷新此账号的容量信息吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await cksApi.refreshCapacity(id)
|
||||||
|
await fetchCks()
|
||||||
|
notification.success({
|
||||||
|
title: '成功',
|
||||||
|
content: '容量信息已刷新!',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新容量失败:', error)
|
||||||
|
notification.error({
|
||||||
|
title: '失败',
|
||||||
|
content: '刷新容量失败: ' + (error.message || '未知错误'),
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换账号状态
|
||||||
|
const toggleStatus = async (cks) => {
|
||||||
|
const newStatus = !cks.is_valid
|
||||||
|
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('数据刷新完成')
|
||||||
|
notification.success({
|
||||||
|
title: '成功',
|
||||||
|
content: `账号已${newStatus ? '启用' : '禁用'}!`,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换账号状态失败:', error)
|
||||||
|
notification.error({
|
||||||
|
title: '失败',
|
||||||
|
content: `切换账号状态失败: ${error.message || '未知错误'}`,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑账号
|
||||||
|
const editCks = (cks) => {
|
||||||
|
editingCks.value = cks
|
||||||
|
form.value = {
|
||||||
|
pan_id: cks.pan_id,
|
||||||
|
ck: cks.ck,
|
||||||
|
is_valid: cks.is_valid,
|
||||||
|
remark: cks.remark || ''
|
||||||
|
}
|
||||||
|
showEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
const closeModal = () => {
|
||||||
|
showCreateModal.value = false
|
||||||
|
showEditModal.value = false
|
||||||
|
editingCks.value = null
|
||||||
|
form.value = {
|
||||||
|
pan_id: '',
|
||||||
|
ck: '',
|
||||||
|
is_valid: true,
|
||||||
|
remark: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (showEditModal.value) {
|
||||||
|
await updateCks()
|
||||||
|
} else {
|
||||||
|
await createCks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取平台图标
|
||||||
|
const getPlatformIcon = (platformName) => {
|
||||||
|
const defaultIcons = {
|
||||||
|
'unknown': '<i class="fas fa-question-circle text-gray-400"></i>',
|
||||||
|
'other': '<i class="fas fa-cloud text-gray-500"></i>',
|
||||||
|
'magnet': '<i class="fas fa-magnet text-red-600"></i>',
|
||||||
|
'uc': '<i class="fas fa-cloud-download-alt text-purple-600"></i>',
|
||||||
|
'夸克网盘': '<i class="fas fa-cloud text-blue-600"></i>',
|
||||||
|
'阿里云盘': '<i class="fas fa-cloud text-orange-600"></i>',
|
||||||
|
'百度网盘': '<i class="fas fa-cloud text-blue-500"></i>',
|
||||||
|
'天翼云盘': '<i class="fas fa-cloud text-red-500"></i>',
|
||||||
|
'OneDrive': '<i class="fas fa-cloud text-blue-700"></i>',
|
||||||
|
'Google Drive': '<i class="fas fa-cloud text-green-600"></i>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultIcons[platformName] || defaultIcons['unknown']
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (!bytes || bytes <= 0) return '0 B'
|
||||||
|
|
||||||
|
const tb = bytes / (1024 * 1024 * 1024 * 1024)
|
||||||
|
if (tb >= 1) {
|
||||||
|
return tb.toFixed(2) + ' TB'
|
||||||
|
}
|
||||||
|
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024)
|
||||||
|
if (gb >= 1) {
|
||||||
|
return gb.toFixed(2) + ' GB'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mb = bytes / (1024 * 1024)
|
||||||
|
if (mb >= 1) {
|
||||||
|
return mb.toFixed(2) + ' MB'
|
||||||
|
}
|
||||||
|
|
||||||
|
const kb = bytes / 1024
|
||||||
|
if (kb >= 1) {
|
||||||
|
return kb.toFixed(2) + ' KB'
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes + ' B'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤和分页计算
|
||||||
|
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
|
||||||
|
return filtered.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 防抖搜索
|
||||||
|
let searchTimeout = null
|
||||||
|
const debounceSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
}
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
console.log('执行搜索,搜索词:', searchQuery.value)
|
||||||
|
console.log('当前过滤后的账号数量:', filteredCksList.value.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台变化处理
|
||||||
|
const onPlatformChange = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
console.log('平台过滤条件变化:', platform.value)
|
||||||
|
console.log('当前过滤后的账号数量:', filteredCksList.value.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
// 保持当前的过滤条件,只刷新数据
|
||||||
|
fetchCks()
|
||||||
|
fetchPlatforms()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页跳转
|
||||||
|
const goToPage = (page) => {
|
||||||
|
currentPage.value = page
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
checkAuth()
|
||||||
|
await Promise.all([
|
||||||
|
fetchCks(),
|
||||||
|
fetchPlatforms()
|
||||||
|
])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('页面初始化失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义样式 */
|
||||||
|
.line-clamp-1 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -205,7 +205,7 @@ const { data: tagsData } = await useAsyncData('categoryTags', () => tagApi.getTa
|
|||||||
// 标签选项
|
// 标签选项
|
||||||
const tagOptions = computed(() => {
|
const tagOptions = computed(() => {
|
||||||
const data = tagsData.value as any
|
const data = tagsData.value as any
|
||||||
const tags = data?.data || data || []
|
const tags = data?.items || data || []
|
||||||
return tags.map((tag: any) => ({
|
return tags.map((tag: any) => ({
|
||||||
label: tag.name,
|
label: tag.name,
|
||||||
value: tag.id
|
value: tag.id
|
||||||
@@ -314,8 +314,8 @@ const fetchData = async () => {
|
|||||||
search: searchQuery.value
|
search: searchQuery.value
|
||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.items) {
|
||||||
categories.value = response.data
|
categories.value = response.items
|
||||||
total.value = response.total || 0
|
total.value = response.total || 0
|
||||||
} else if (Array.isArray(response)) {
|
} else if (Array.isArray(response)) {
|
||||||
categories.value = response
|
categories.value = response
|
||||||
|
|||||||
631
web/pages/admin/failed-resources.vue
Normal file
631
web/pages/admin/failed-resources.vue
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">失败资源列表</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">显示处理失败的资源,包含错误信息</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<n-button
|
||||||
|
@click="retryAllFailed"
|
||||||
|
:disabled="selectedResources.length === 0 || isProcessing"
|
||||||
|
:type="selectedResources.length > 0 && !isProcessing ? 'success' : 'default'"
|
||||||
|
:loading="isProcessing"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
|
||||||
|
<i v-else class="fas fa-redo"></i>
|
||||||
|
</template>
|
||||||
|
{{ isProcessing ? '处理中...' : `重新放入待处理池 (${selectedResources.length})` }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
@click="clearAllErrors"
|
||||||
|
:disabled="selectedResources.length === 0 || isProcessing"
|
||||||
|
:type="selectedResources.length > 0 && !isProcessing ? 'warning' : 'default'"
|
||||||
|
:loading="isProcessing"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
|
||||||
|
<i v-else class="fas fa-trash"></i>
|
||||||
|
</template>
|
||||||
|
{{ isProcessing ? '处理中...' : `删除失败资源 (${selectedResources.length})` }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="refreshData" type="info">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-refresh"></i>
|
||||||
|
</template>
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索和筛选 -->
|
||||||
|
<n-card>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
<n-select
|
||||||
|
v-model:value="errorFilter"
|
||||||
|
placeholder="选择状态"
|
||||||
|
:options="statusOptions"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-button type="primary" @click="handleSearch">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</template>
|
||||||
|
搜索
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 失败资源列表 -->
|
||||||
|
<n-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-lg font-semibold">失败资源列表</span>
|
||||||
|
<n-checkbox
|
||||||
|
:checked="isAllSelected"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@update:checked="toggleSelectAll"
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</n-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
<span class="text-sm text-gray-500">共 {{ totalCount }} 个资源,已选择 {{ selectedResources.length }} 个</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||||
|
<n-spin size="large">
|
||||||
|
<template #description>
|
||||||
|
<span class="text-gray-500">加载中...</span>
|
||||||
|
</template>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 虚拟列表 -->
|
||||||
|
<n-virtual-list
|
||||||
|
v-if="!loading"
|
||||||
|
:items="failedResources"
|
||||||
|
:item-size="80"
|
||||||
|
:item-resizable="true"
|
||||||
|
style="max-height: 400px"
|
||||||
|
container-style="height: 600px;"
|
||||||
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<!-- 左侧信息 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- 复选框 -->
|
||||||
|
<n-checkbox
|
||||||
|
:checked="selectedResources.includes(item.id)"
|
||||||
|
@update:checked="(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedResources.push(item.id)
|
||||||
|
} else {
|
||||||
|
const index = selectedResources.indexOf(item.id)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedResources.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- ID -->
|
||||||
|
<div class="w-16 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
#{{ item.id }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1" :title="item.title || '未设置'">
|
||||||
|
{{ item.title || '未设置' }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div class="mt-2 flex items-center space-x-2">
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-1 mt-1" :title="item.url">
|
||||||
|
<a
|
||||||
|
:href="checkUrlSafety(item.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"
|
||||||
|
>
|
||||||
|
{{ item.url }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<n-tag type="error" size="small" :title="item.error_msg">
|
||||||
|
{{ truncateError(item.error_msg) }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span>创建时间: {{ formatTime(item.create_time) }}</span>
|
||||||
|
<span>IP: {{ item.ip || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧操作按钮 -->
|
||||||
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="retryResource(item.id)"
|
||||||
|
title="重试此资源"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
@click="clearError(item.id)"
|
||||||
|
title="清除错误信息"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-broom"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
@click="deleteResource(item.id)"
|
||||||
|
title="删除此资源"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!loading && failedResources.length === 0" class="flex flex-col items-center justify-center py-12">
|
||||||
|
<n-empty description="暂无失败资源">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-check-circle text-4xl text-green-500"></i>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<span class="text-sm text-gray-500">所有资源处理成功</span>
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-6 flex justify-center">
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:item-count="totalCount"
|
||||||
|
:page-sizes="[100, 200, 500, 1000]"
|
||||||
|
show-size-picker
|
||||||
|
@update:page="fetchData"
|
||||||
|
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 设置页面布局
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
|
ssr: false
|
||||||
|
})
|
||||||
|
|
||||||
|
interface FailedResource {
|
||||||
|
id: number
|
||||||
|
title?: string | null
|
||||||
|
url: string
|
||||||
|
error_msg: string
|
||||||
|
create_time: string
|
||||||
|
ip?: string | null
|
||||||
|
deleted_at?: string | null
|
||||||
|
is_deleted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = useNotification()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const failedResources = ref<FailedResource[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页相关状态
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(200)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const totalPages = ref(0)
|
||||||
|
|
||||||
|
// 过滤相关状态
|
||||||
|
const errorFilter = ref('')
|
||||||
|
const selectedStatus = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 处理状态
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
|
||||||
|
// 选择相关状态
|
||||||
|
const selectedResources = ref<number[]>([])
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '好友已取消了分享', value: '好友已取消了分享' },
|
||||||
|
{ label: '用户封禁', value: '用户封禁' },
|
||||||
|
{ label: '分享地址已失效', value: '分享地址已失效' },
|
||||||
|
{ label: '链接无效: 链接状态检查失败', value: '链接无效: 链接状态检查失败' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取失败资源API
|
||||||
|
import { useReadyResourceApi } from '~/composables/useApi'
|
||||||
|
const readyResourceApi = useReadyResourceApi()
|
||||||
|
|
||||||
|
// 全选相关计算属性
|
||||||
|
const isAllSelected = computed(() => {
|
||||||
|
return failedResources.value.length > 0 && selectedResources.value.length === failedResources.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
const isIndeterminate = computed(() => {
|
||||||
|
return selectedResources.value.length > 0 && selectedResources.value.length < failedResources.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 全选切换方法
|
||||||
|
const toggleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedResources.value = failedResources.value.map(resource => resource.id)
|
||||||
|
} else {
|
||||||
|
selectedResources.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有错误信息过滤条件,添加到查询参数中
|
||||||
|
if (errorFilter.value.trim()) {
|
||||||
|
params.error_filter = errorFilter.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有状态筛选条件,添加到查询参数中
|
||||||
|
if (selectedStatus.value) {
|
||||||
|
params.status = selectedStatus.value
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('fetchData - 开始获取失败资源,参数:', params)
|
||||||
|
|
||||||
|
const response = await readyResourceApi.getFailedResources(params) as any
|
||||||
|
|
||||||
|
console.log('fetchData - 原始响应:', response)
|
||||||
|
|
||||||
|
if (response && response.data && Array.isArray(response.data)) {
|
||||||
|
console.log('fetchData - 使用response.data格式(数组)')
|
||||||
|
failedResources.value = response.data
|
||||||
|
totalCount.value = response.total || 0
|
||||||
|
totalPages.value = Math.ceil((response.total || 0) / pageSize.value)
|
||||||
|
} else {
|
||||||
|
console.log('fetchData - 使用空数据格式')
|
||||||
|
failedResources.value = []
|
||||||
|
totalCount.value = 0
|
||||||
|
totalPages.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('fetchData - 处理后的数据:', {
|
||||||
|
failedResourcesCount: failedResources.value.length,
|
||||||
|
totalCount: totalCount.value,
|
||||||
|
totalPages: totalPages.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置选择状态
|
||||||
|
selectedResources.value = []
|
||||||
|
|
||||||
|
// 打印第一个资源的数据结构(如果存在)
|
||||||
|
if (failedResources.value.length > 0) {
|
||||||
|
console.log('fetchData - 第一个资源的数据结构:', failedResources.value[0])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取失败资源失败:', error)
|
||||||
|
failedResources.value = []
|
||||||
|
totalCount.value = 0
|
||||||
|
totalPages.value = 1
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除错误过滤
|
||||||
|
const clearErrorFilter = () => {
|
||||||
|
errorFilter.value = ''
|
||||||
|
selectedStatus.value = null
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = () => {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试单个资源
|
||||||
|
const retryResource = async (id: number) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: '确定要重试这个资源吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await readyResourceApi.clearErrorMsg(id)
|
||||||
|
notification.success({
|
||||||
|
content: '错误信息已清除,资源将在下次调度时重新处理',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重试失败:', error)
|
||||||
|
notification.error({
|
||||||
|
content: '重试失败',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除单个资源错误
|
||||||
|
const clearError = async (id: number) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: '确定要清除这个资源的错误信息吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await readyResourceApi.clearErrorMsg(id)
|
||||||
|
notification.success({
|
||||||
|
content: '错误信息已清除',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除错误失败:', error)
|
||||||
|
notification.error({
|
||||||
|
content: '清除错误失败',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除资源
|
||||||
|
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()
|
||||||
|
notification.success({
|
||||||
|
content: '删除成功',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
notification.error({
|
||||||
|
content: '删除失败',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新放入待处理池
|
||||||
|
const retryAllFailed = async () => {
|
||||||
|
if (selectedResources.value.length === 0) {
|
||||||
|
notification.error({
|
||||||
|
content: '请先选择要处理的资源',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = selectedResources.value.length
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认操作',
|
||||||
|
content: `确定要将 ${count} 个资源重新放入待处理池吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
if (isProcessing.value) return // 防止重复点击
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用选中的资源ID进行批量操作
|
||||||
|
const response = await readyResourceApi.batchRestoreToReadyPool(selectedResources.value) as any
|
||||||
|
notification.success({
|
||||||
|
content: `操作完成:\n总数量:${count}\n成功处理:${response.success_count || count}\n失败:${response.failed_count || 0}`,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
selectedResources.value = [] // 清空选择
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重新放入待处理池失败:', error)
|
||||||
|
notification.error({
|
||||||
|
content: '操作失败',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有错误
|
||||||
|
const clearAllErrors = async () => {
|
||||||
|
if (selectedResources.value.length === 0) {
|
||||||
|
notification.error({
|
||||||
|
content: '请先选择要删除的资源',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = selectedResources.value.length
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: `确定要删除 ${count} 个失败资源吗?此操作将永久删除这些资源,不可恢复!`,
|
||||||
|
positiveText: '确定删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
if (isProcessing.value) return // 防止重复点击
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('开始调用删除API,选中的资源ID:', selectedResources.value)
|
||||||
|
// 逐个删除选中的资源
|
||||||
|
let successCount = 0
|
||||||
|
for (const id of selectedResources.value) {
|
||||||
|
try {
|
||||||
|
await readyResourceApi.deleteReadyResource(id)
|
||||||
|
successCount++
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`删除资源 ${id} 失败:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.success({
|
||||||
|
content: `操作完成:\n删除失败资源:${successCount} 个资源`,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
selectedResources.value = [] // 清空选择
|
||||||
|
fetchData()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('删除失败资源失败:', error)
|
||||||
|
console.error('错误详情:', {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
response: error?.response
|
||||||
|
})
|
||||||
|
notification.error({
|
||||||
|
content: '删除失败',
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('页面初始化失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置页面标题
|
||||||
|
useHead({
|
||||||
|
title: '失败资源列表 - 老九网盘资源数据库'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义样式 */
|
||||||
|
.line-clamp-1 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,27 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自动处理状态显示 -->
|
|
||||||
<div class="absolute bottom-4 right-4 flex items-center gap-4">
|
|
||||||
<div class="flex items-center gap-2 bg-white/10 rounded-lg px-3 py-2">
|
|
||||||
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
|
||||||
'bg-red-400': !systemConfig?.auto_process_ready_resources,
|
|
||||||
'bg-green-400': systemConfig?.auto_process_ready_resources
|
|
||||||
}"></div>
|
|
||||||
<span class="text-xs text-white font-medium">
|
|
||||||
自动处理已<span>{{ systemConfig?.auto_process_ready_resources ? '开启' : '关闭' }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 bg-white/10 rounded-lg px-3 py-2">
|
|
||||||
<div class="w-2 h-2 rounded-full animate-pulse" :class="{
|
|
||||||
'bg-red-400': !systemConfig?.auto_transfer_enabled,
|
|
||||||
'bg-green-400': systemConfig?.auto_transfer_enabled
|
|
||||||
}"></div>
|
|
||||||
<span class="text-xs text-white font-medium">
|
|
||||||
自动转存已<span>{{ systemConfig?.auto_transfer_enabled ? '开启' : '关闭' }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
@@ -114,7 +94,7 @@
|
|||||||
<i class="fas fa-server text-green-600 text-xl"></i>
|
<i class="fas fa-server text-green-600 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">平台管理</h3>
|
<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">系统支持的网盘平台</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">系统支持的网盘平台</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,7 +144,7 @@ definePageMeta({
|
|||||||
layout: 'admin'
|
layout: 'admin'
|
||||||
})
|
})
|
||||||
|
|
||||||
import { useStatsApi, usePanApi, useSystemConfigApi } from '~/composables/useApi'
|
import { useStatsApi, usePanApi } from '~/composables/useApi'
|
||||||
import { useApiFetch } from '~/composables/useApiFetch'
|
import { useApiFetch } from '~/composables/useApiFetch'
|
||||||
import { parseApiResponse } from '~/composables/useApi'
|
import { parseApiResponse } from '~/composables/useApi'
|
||||||
import Chart from 'chart.js/auto'
|
import Chart from 'chart.js/auto'
|
||||||
@@ -172,13 +152,6 @@ import Chart from 'chart.js/auto'
|
|||||||
// API
|
// API
|
||||||
const statsApi = useStatsApi()
|
const statsApi = useStatsApi()
|
||||||
const panApi = usePanApi()
|
const panApi = usePanApi()
|
||||||
const systemConfigApi = useSystemConfigApi()
|
|
||||||
|
|
||||||
// 获取系统配置
|
|
||||||
const { data: systemConfigData } = await useAsyncData('systemConfig', () => systemConfigApi.getSystemConfig())
|
|
||||||
|
|
||||||
// 系统配置
|
|
||||||
const systemConfig = computed(() => (systemConfigData.value as any) || {})
|
|
||||||
|
|
||||||
// 获取统计数据
|
// 获取统计数据
|
||||||
const { data: statsData } = await useAsyncData('adminStats', () => statsApi.getStats())
|
const { data: statsData } = await useAsyncData('adminStats', () => statsApi.getStats())
|
||||||
@@ -187,11 +160,16 @@ const { data: statsData } = await useAsyncData('adminStats', () => statsApi.getS
|
|||||||
const { data: pansData } = await useAsyncData('adminPans', () => panApi.getPans())
|
const { data: pansData } = await useAsyncData('adminPans', () => panApi.getPans())
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
const stats = computed(() => (statsData.value as any) || {
|
const stats = computed(() => {
|
||||||
|
console.log('原始统计数据:', statsData.value)
|
||||||
|
const result = (statsData.value as any) || {
|
||||||
total_resources: 0,
|
total_resources: 0,
|
||||||
today_resources: 0,
|
today_resources: 0,
|
||||||
today_views: 0,
|
today_views: 0,
|
||||||
today_searches: 0
|
today_searches: 0
|
||||||
|
}
|
||||||
|
console.log('处理后的统计数据:', result)
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
// 平台数据
|
// 平台数据
|
||||||
|
|||||||
@@ -7,12 +7,6 @@
|
|||||||
<p class="text-gray-600 dark:text-gray-400">管理待处理的资源</p>
|
<p class="text-gray-600 dark:text-gray-400">管理待处理的资源</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<n-button type="primary" @click="navigateTo('/admin/add-resource')">
|
|
||||||
<template #icon>
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
</template>
|
|
||||||
添加资源
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="navigateTo('/admin/failed-resources')" type="error">
|
<n-button @click="navigateTo('/admin/failed-resources')" type="error">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
@@ -274,6 +268,7 @@ const fetchSystemConfig = async () => {
|
|||||||
const response = await systemConfigApi.getSystemConfig()
|
const response = await systemConfigApi.getSystemConfig()
|
||||||
systemConfig.value = response
|
systemConfig.value = response
|
||||||
systemConfigStore.setConfig(response)
|
systemConfigStore.setConfig(response)
|
||||||
|
console.log('ready-resources页面系统配置:', response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取系统配置失败:', error)
|
console.error('获取系统配置失败:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
添加资源
|
添加资源
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="showBatchModal = true" type="info">
|
<n-button @click="openBatchModal" type="info">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</template>
|
</template>
|
||||||
@@ -68,8 +68,18 @@
|
|||||||
<n-card>
|
<n-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
<span class="text-lg font-semibold">资源列表</span>
|
<span class="text-lg font-semibold">资源列表</span>
|
||||||
<span class="text-sm text-gray-500">共 {{ total }} 个资源</span>
|
<div class="flex items-center space-x-2">
|
||||||
|
<n-checkbox
|
||||||
|
:checked="isAllSelected"
|
||||||
|
@update:checked="toggleSelectAll"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-500">全选</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-gray-500">共 {{ total }} 个资源,已选择 {{ selectedResources.length }} 个</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -86,7 +96,8 @@
|
|||||||
<!-- 虚拟列表 -->
|
<!-- 虚拟列表 -->
|
||||||
<n-virtual-list
|
<n-virtual-list
|
||||||
:items="resources"
|
:items="resources"
|
||||||
:item-size="120"
|
:item-size="100"
|
||||||
|
style="max-height: 400px"
|
||||||
container-style="height: 600px;"
|
container-style="height: 600px;"
|
||||||
>
|
>
|
||||||
<template #default="{ item: resource }">
|
<template #default="{ item: resource }">
|
||||||
@@ -100,19 +111,20 @@
|
|||||||
@update:checked="(checked) => toggleResourceSelection(resource.id, checked)"
|
@update:checked="(checked) => toggleResourceSelection(resource.id, checked)"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-500">{{ resource.id }}</span>
|
<span class="text-sm text-gray-500">{{ resource.id }}</span>
|
||||||
<span v-if="resource.pan_id" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
|
||||||
|
<span v-if="resource.pan_id" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded flex-shrink-0">
|
||||||
{{ getPlatformName(resource.pan_id) }}
|
{{ getPlatformName(resource.pan_id) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="resource.category_id" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white flex-1 line-clamp-1">
|
||||||
{{ getCategoryName(resource.category_id) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
|
||||||
{{ resource.title }}
|
{{ resource.title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
<span v-if="resource.category_id" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded flex-shrink-0">
|
||||||
|
{{ getCategoryName(resource.category_id) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<p v-if="resource.description" class="text-gray-600 dark:text-gray-400 mb-2">
|
</div>
|
||||||
|
|
||||||
|
<p v-if="resource.description" class="text-gray-600 dark:text-gray-400 mb-2 line-clamp-2">
|
||||||
{{ resource.description }}
|
{{ resource.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -149,12 +161,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 ml-4">
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
<n-button size="small" type="primary" @click="editResource(resource)">
|
<!-- <n-button size="small" type="primary" @click="editResource(resource)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
编辑
|
编辑
|
||||||
</n-button>
|
</n-button> -->
|
||||||
<n-button size="small" type="error" @click="deleteResource(resource)">
|
<n-button size="small" type="error" @click="deleteResource(resource)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@@ -173,7 +185,7 @@
|
|||||||
v-model:page="currentPage"
|
v-model:page="currentPage"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
:item-count="total"
|
:item-count="total"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[100, 200, 500, 1000]"
|
||||||
show-size-picker
|
show-size-picker
|
||||||
@update:page="handlePageChange"
|
@update:page="handlePageChange"
|
||||||
@update:page-size="handlePageSizeChange"
|
@update:page-size="handlePageSizeChange"
|
||||||
@@ -186,7 +198,12 @@
|
|||||||
<n-modal v-model:show="showBatchModal" preset="card" title="批量操作" style="width: 600px">
|
<n-modal v-model:show="showBatchModal" preset="card" title="批量操作" style="width: 600px">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>已选择 {{ selectedResources.length }} 个资源</span>
|
<div>
|
||||||
|
<span class="font-medium">已选择 {{ selectedResources.length }} 个资源</span>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ isAllSelected ? '已全选当前页面' : isIndeterminate ? '部分选中' : '未选择' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<n-button size="small" @click="clearSelection">清空选择</n-button>
|
<n-button size="small" @click="clearSelection">清空选择</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -305,7 +322,7 @@ const resources = ref<Resource[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(200)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedCategory = ref(null)
|
const selectedCategory = ref(null)
|
||||||
const selectedPlatform = ref(null)
|
const selectedPlatform = ref(null)
|
||||||
@@ -359,11 +376,15 @@ const { data: platformsData } = await useAsyncData('resourcePlatforms', () => pa
|
|||||||
// 分类选项
|
// 分类选项
|
||||||
const categoryOptions = computed(() => {
|
const categoryOptions = computed(() => {
|
||||||
const data = categoriesData.value as any
|
const data = categoriesData.value as any
|
||||||
const categories = data?.data || data || []
|
console.log('分类数据:', data)
|
||||||
return categories.map((cat: any) => ({
|
const categories = data?.items || data || []
|
||||||
|
console.log('处理后的分类:', categories)
|
||||||
|
const options = categories.map((cat: any) => ({
|
||||||
label: cat.name,
|
label: cat.name,
|
||||||
value: cat.id
|
value: cat.id
|
||||||
}))
|
}))
|
||||||
|
console.log('分类选项:', options)
|
||||||
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
// 标签选项
|
// 标签选项
|
||||||
@@ -381,7 +402,7 @@ const platformOptions = computed(() => {
|
|||||||
const data = platformsData.value as any
|
const data = platformsData.value as any
|
||||||
const platforms = data?.data || data || []
|
const platforms = data?.data || data || []
|
||||||
return platforms.map((platform: any) => ({
|
return platforms.map((platform: any) => ({
|
||||||
label: platform.name,
|
label: platform.remark || platform.name,
|
||||||
value: platform.id
|
value: platform.id
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -394,28 +415,50 @@ const getCategoryName = (categoryId: number) => {
|
|||||||
|
|
||||||
// 获取平台名称
|
// 获取平台名称
|
||||||
const getPlatformName = (platformId: number) => {
|
const getPlatformName = (platformId: number) => {
|
||||||
const platform = (platformsData.value as any)?.data?.find((plat: any) => plat.id === platformId)
|
console.log('platformId', platformId, platformsData.value)
|
||||||
return platform?.name || '未知平台'
|
const platform = (platformsData.value as any)?.find((plat: any) => plat.id === platformId)
|
||||||
|
return platform?.remark || platform?.name || '未知平台'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await resourceApi.getResources({
|
const params: any = {
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
page_size: pageSize.value,
|
page_size: pageSize.value,
|
||||||
search: searchQuery.value,
|
search: searchQuery.value
|
||||||
category_id: selectedCategory.value,
|
}
|
||||||
pan_id: selectedPlatform.value
|
|
||||||
}) as any
|
// 添加分类筛选
|
||||||
|
if (selectedCategory.value) {
|
||||||
|
params.category_id = selectedCategory.value
|
||||||
|
console.log('添加分类筛选:', selectedCategory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加平台筛选
|
||||||
|
if (selectedPlatform.value) {
|
||||||
|
params.pan_id = selectedPlatform.value
|
||||||
|
console.log('添加平台筛选:', selectedPlatform.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('请求参数:', params)
|
||||||
|
console.log('pageSize:', pageSize.value)
|
||||||
|
console.log('selectedCategory:', selectedCategory.value)
|
||||||
|
console.log('selectedPlatform:', selectedPlatform.value)
|
||||||
|
const response = await resourceApi.getResources(params) as any
|
||||||
|
console.log('API响应:', response)
|
||||||
|
console.log('返回的资源数量:', response?.data?.length || 0)
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
resources.value = response.data
|
resources.value = response.data
|
||||||
total.value = response.total || 0
|
total.value = response.total || 0
|
||||||
|
// 清空选择(因为数据已更新)
|
||||||
|
selectedResources.value = []
|
||||||
} else {
|
} else {
|
||||||
resources.value = []
|
resources.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
|
selectedResources.value = []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取资源失败:', error)
|
console.error('获取资源失败:', error)
|
||||||
@@ -461,11 +504,45 @@ const toggleResourceSelection = (resourceId: number, checked: boolean) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全选状态计算
|
||||||
|
const isAllSelected = computed(() => {
|
||||||
|
return resources.value.length > 0 && selectedResources.value.length === resources.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 部分选中状态计算
|
||||||
|
const isIndeterminate = computed(() => {
|
||||||
|
return selectedResources.value.length > 0 && selectedResources.value.length < resources.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换全选
|
||||||
|
const toggleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
// 全选:添加所有当前页面的资源ID
|
||||||
|
selectedResources.value = resources.value.map(resource => resource.id)
|
||||||
|
} else {
|
||||||
|
// 取消全选:清空选择
|
||||||
|
selectedResources.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空选择
|
// 清空选择
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
selectedResources.value = []
|
selectedResources.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开批量操作模态框
|
||||||
|
const openBatchModal = () => {
|
||||||
|
// 如果没有选择任何资源,自动全选当前页面
|
||||||
|
if (selectedResources.value.length === 0 && resources.value.length > 0) {
|
||||||
|
selectedResources.value = resources.value.map(resource => resource.id)
|
||||||
|
notification.info({
|
||||||
|
content: '已自动全选当前页面资源',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
showBatchModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑资源
|
// 编辑资源
|
||||||
const editResource = (resource: Resource) => {
|
const editResource = (resource: Resource) => {
|
||||||
editingResource.value = resource
|
editingResource.value = resource
|
||||||
@@ -623,4 +700,17 @@ useHead({
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 自定义样式 */
|
/* 自定义样式 */
|
||||||
|
.line-clamp-1 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -206,7 +206,7 @@ const { data: categoriesData } = await useAsyncData('tagCategories', () => categ
|
|||||||
const categoryOptions = computed(() => {
|
const categoryOptions = computed(() => {
|
||||||
const data = categoriesData.value as any
|
const data = categoriesData.value as any
|
||||||
const categories = data?.data || data || []
|
const categories = data?.data || data || []
|
||||||
return categories.map((cat: any) => ({
|
return categories.items.map((cat: any) => ({
|
||||||
label: cat.name,
|
label: cat.name,
|
||||||
value: cat.id
|
value: cat.id
|
||||||
}))
|
}))
|
||||||
@@ -325,8 +325,8 @@ const fetchData = async () => {
|
|||||||
search: searchQuery.value
|
search: searchQuery.value
|
||||||
}) as any
|
}) as any
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.items) {
|
||||||
tags.value = response.data
|
tags.value = response.items
|
||||||
total.value = response.total || 0
|
total.value = response.total || 0
|
||||||
} else if (Array.isArray(response)) {
|
} else if (Array.isArray(response)) {
|
||||||
tags.value = response
|
tags.value = response
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
|||||||
async initConfig(force = false) {
|
async initConfig(force = false) {
|
||||||
if (this.initialized && !force) return
|
if (this.initialized && !force) return
|
||||||
try {
|
try {
|
||||||
const data = await useApiFetch('/public/system-config').then((res: any) => res.data || res)
|
const data = await useApiFetch('/system/config').then((res: any) => res.data || res)
|
||||||
this.config = data
|
this.config = data
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user