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"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,8 @@ import (
|
|||||||
func GetResources(c *gin.Context) {
|
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,
|
||||||
@@ -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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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(() => {
|
||||||
total_resources: 0,
|
console.log('原始统计数据:', statsData.value)
|
||||||
today_resources: 0,
|
const result = (statsData.value as any) || {
|
||||||
today_views: 0,
|
total_resources: 0,
|
||||||
today_searches: 0
|
today_resources: 0,
|
||||||
|
today_views: 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">
|
||||||
<span class="text-lg font-semibold">资源列表</span>
|
<div class="flex items-center space-x-4">
|
||||||
<span class="text-sm text-gray-500">共 {{ total }} 个资源</span>
|
<span class="text-lg font-semibold">资源列表</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">
|
||||||
|
{{ resource.title }}
|
||||||
|
</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) }}
|
{{ getCategoryName(resource.category_id) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
<p v-if="resource.description" class="text-gray-600 dark:text-gray-400 mb-2 line-clamp-2">
|
||||||
{{ resource.title }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p v-if="resource.description" class="text-gray-600 dark:text-gray-400 mb-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