update: 后台界面优化

This commit is contained in:
ctwj
2025-08-11 01:34:07 +08:00
parent 1b0fc06bf7
commit b567531a7d
33 changed files with 1186 additions and 419 deletions

View File

@@ -7,6 +7,8 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"time" "time"
"github.com/ctwj/urldb/utils"
) )
// AlipanService 阿里云盘服务 // AlipanService 阿里云盘服务
@@ -428,7 +430,7 @@ func (a *AlipanService) manageAccessToken() (string, error) {
} }
// 检查token是否过期 // 检查token是否过期
if time.Now().After(tokenInfo.ExpiresAt) { if utils.GetCurrentTime().After(tokenInfo.ExpiresAt) {
return a.getNewAccessToken() return a.getNewAccessToken()
} }

View File

@@ -8,6 +8,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/ctwj/urldb/utils"
) )
// QuarkPanService 夸克网盘服务 // QuarkPanService 夸克网盘服务
@@ -406,7 +408,7 @@ func (q *QuarkPanService) getShareSave(shareID, stoken string, fidList, fidToken
// 生成指定长度的时间戳 // 生成指定长度的时间戳
func (q *QuarkPanService) generateTimestamp(length int) int64 { func (q *QuarkPanService) generateTimestamp(length int) int64 {
timestamp := time.Now().UnixNano() / int64(time.Millisecond) timestamp := utils.GetCurrentTime().UnixNano() / int64(time.Millisecond)
timestampStr := strconv.FormatInt(timestamp, 10) timestampStr := strconv.FormatInt(timestamp, 10)
if len(timestampStr) > length { if len(timestampStr) > length {
timestampStr = timestampStr[:length] timestampStr = timestampStr[:length]

View File

@@ -219,8 +219,8 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
// 设置时间戳(使用第一个配置的时间) // 设置时间戳(使用第一个配置的时间)
if len(configs) > 0 { if len(configs) > 0 {
response[entity.ConfigResponseFieldCreatedAt] = configs[0].CreatedAt.Format("2006-01-02 15:04:05") response[entity.ConfigResponseFieldCreatedAt] = configs[0].CreatedAt.Format(utils.TimeFormatDateTime)
response[entity.ConfigResponseFieldUpdatedAt] = configs[0].UpdatedAt.Format("2006-01-02 15:04:05") response[entity.ConfigResponseFieldUpdatedAt] = configs[0].UpdatedAt.Format(utils.TimeFormatDateTime)
} }
return response return response

View File

@@ -212,7 +212,7 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
var resources []entity.Resource var resources []entity.Resource
var total int64 var total int64
db := r.db.Model(&entity.Resource{}) db := r.db.Model(&entity.Resource{}).Preload("Category").Preload("Pan").Preload("Tags")
// 处理参数 // 处理参数
for key, value := range params { for key, value := range params {
@@ -258,6 +258,31 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
if isPublic, ok := value.(bool); ok { if isPublic, ok := value.(bool); ok {
db = db.Where("is_public = ?", isPublic) db = db.Where("is_public = ?", isPublic)
} }
case "has_save_url": // 添加has_save_url参数支持
if hasSaveURL, ok := value.(bool); ok {
fmt.Printf("处理 has_save_url 参数: %v\n", hasSaveURL)
if hasSaveURL {
// 有转存链接save_url不为空且不为空格
db = db.Where("save_url IS NOT NULL AND save_url != '' AND TRIM(save_url) != ''")
fmt.Printf("应用 has_save_url=true 条件: save_url IS NOT NULL AND save_url != '' AND TRIM(save_url) != ''\n")
} else {
// 没有转存链接save_url为空、NULL或只有空格
db = db.Where("(save_url IS NULL OR save_url = '' OR TRIM(save_url) = '')")
fmt.Printf("应用 has_save_url=false 条件: (save_url IS NULL OR save_url = '' OR TRIM(save_url) = '')\n")
}
}
case "no_save_url": // 添加no_save_url参数支持与has_save_url=false相同
if noSaveURL, ok := value.(bool); ok && noSaveURL {
db = db.Where("(save_url IS NULL OR save_url = '' OR TRIM(save_url) = '')")
}
case "pan_name": // 添加pan_name参数支持
if panName, ok := value.(string); ok && panName != "" {
// 根据平台名称查找平台ID
var panEntity entity.Pan
if err := r.db.Where("name ILIKE ?", "%"+panName+"%").First(&panEntity).Error; err == nil {
db = db.Where("pan_id = ?", panEntity.ID)
}
}
} }
} }

View File

@@ -1,8 +1,8 @@
package repo package repo
import ( import (
"time"
"github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -40,7 +40,7 @@ func (r *ResourceViewRepositoryImpl) RecordView(resourceID uint, ipAddress, user
// GetTodayViews 获取今日访问量 // GetTodayViews 获取今日访问量
func (r *ResourceViewRepositoryImpl) GetTodayViews() (int64, error) { func (r *ResourceViewRepositoryImpl) GetTodayViews() (int64, error) {
today := time.Now().Format("2006-01-02") today := utils.GetTodayString()
var count int64 var count int64
err := r.db.Model(&entity.ResourceView{}). err := r.db.Model(&entity.ResourceView{}).
Where("DATE(created_at) = ?", today). Where("DATE(created_at) = ?", today).
@@ -60,22 +60,22 @@ func (r *ResourceViewRepositoryImpl) GetViewsByDate(date string) (int64, error)
// GetViewsTrend 获取访问量趋势数据 // GetViewsTrend 获取访问量趋势数据
func (r *ResourceViewRepositoryImpl) GetViewsTrend(days int) ([]map[string]interface{}, error) { func (r *ResourceViewRepositoryImpl) GetViewsTrend(days int) ([]map[string]interface{}, error) {
var results []map[string]interface{} var results []map[string]interface{}
for i := days - 1; i >= 0; i-- { for i := days - 1; i >= 0; i-- {
date := time.Now().AddDate(0, 0, -i) date := utils.GetCurrentTime().AddDate(0, 0, -i)
dateStr := date.Format("2006-01-02") dateStr := date.Format(utils.TimeFormatDate)
count, err := r.GetViewsByDate(dateStr) count, err := r.GetViewsByDate(dateStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
results = append(results, map[string]interface{}{ results = append(results, map[string]interface{}{
"date": dateStr, "date": dateStr,
"views": count, "views": count,
}) })
} }
return results, nil return results, nil
} }
@@ -87,4 +87,4 @@ func (r *ResourceViewRepositoryImpl) GetResourceViews(resourceID uint, limit int
Limit(limit). Limit(limit).
Find(&views).Error Find(&views).Error
return views, err return views, err
} }

View File

@@ -2,9 +2,9 @@ package repo
import ( import (
"fmt" "fmt"
"time"
"github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -37,7 +37,7 @@ func (r *SearchStatRepositoryImpl) RecordSearch(keyword, ip, userAgent string) e
stat := entity.SearchStat{ stat := entity.SearchStat{
Keyword: keyword, Keyword: keyword,
Count: 1, Count: 1,
Date: time.Now(), // 可保留 date 字段,实际用 created_at 统计 Date: utils.GetCurrentTime(), // 可保留 date 字段,实际用 created_at 统计
IP: ip, IP: ip,
UserAgent: userAgent, UserAgent: userAgent,
} }
@@ -124,9 +124,9 @@ func (r *SearchStatRepositoryImpl) GetKeywordTrend(keyword string, days int) ([]
// GetSummary 获取搜索统计汇总 // GetSummary 获取搜索统计汇总
func (r *SearchStatRepositoryImpl) GetSummary() (map[string]int64, error) { func (r *SearchStatRepositoryImpl) GetSummary() (map[string]int64, error) {
var total, today, week, month, keywords int64 var total, today, week, month, keywords int64
now := time.Now() now := utils.GetCurrentTime()
todayStr := now.Format("2006-01-02") todayStr := now.Format(utils.TimeFormatDate)
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format("2006-01-02") // 周一 weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format(utils.TimeFormatDate) // 周一
monthStart := now.Format("2006-01") + "-01" monthStart := now.Format("2006-01") + "-01"
// 总搜索次数 // 总搜索次数

View File

@@ -44,6 +44,21 @@ func GetResources(c *gin.Context) {
utils.Error("解析分类ID失败: %v", err) utils.Error("解析分类ID失败: %v", err)
} }
} }
if hasSaveURL := c.Query("has_save_url"); hasSaveURL != "" {
if hasSaveURL == "true" {
params["has_save_url"] = true
} else if hasSaveURL == "false" {
params["has_save_url"] = false
}
}
if noSaveURL := c.Query("no_save_url"); noSaveURL != "" {
if noSaveURL == "true" {
params["no_save_url"] = true
}
}
if panName := c.Query("pan_name"); panName != "" {
params["pan_name"] = panName
}
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params) resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)

View File

@@ -23,12 +23,16 @@ func GetStats(c *gin.Context) {
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)
// 获取今日数据 // 获取今日数据
today := utils.GetCurrentTime().Format("2006-01-02") today := utils.GetTodayString()
// 今日新增资源数量 // 今日新增资源数量
var todayResources int64 var todayResources int64
db.DB.Model(&entity.Resource{}).Where("DATE(created_at) = ?", today).Count(&todayResources) db.DB.Model(&entity.Resource{}).Where("DATE(created_at) = ?", today).Count(&todayResources)
// 今日更新资源数量(包括新增和修改)
var todayUpdates int64
db.DB.Model(&entity.Resource{}).Where("DATE(updated_at) = ?", today).Count(&todayUpdates)
// 今日浏览量 - 使用访问记录表统计今日访问量 // 今日浏览量 - 使用访问记录表统计今日访问量
var todayViews int64 var todayViews int64
todayViews, err := repoManager.ResourceViewRepository.GetTodayViews() todayViews, err := repoManager.ResourceViewRepository.GetTodayViews()
@@ -44,8 +48,8 @@ func GetStats(c *gin.Context) {
// 添加调试日志 // 添加调试日志
utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d", utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d",
totalResources, totalCategories, totalTags, totalViews) totalResources, totalCategories, totalTags, totalViews)
utils.Info("今日数据 - 新增资源: %d, 今日浏览量: %d, 今日搜索: %d", utils.Info("今日数据 - 新增资源: %d, 今日更新: %d, 今日浏览量: %d, 今日搜索: %d",
todayResources, todayViews, todaySearches) todayResources, todayUpdates, todayViews, todaySearches)
SuccessResponse(c, gin.H{ SuccessResponse(c, gin.H{
"total_resources": totalResources, "total_resources": totalResources,
@@ -53,6 +57,7 @@ func GetStats(c *gin.Context) {
"total_tags": totalTags, "total_tags": totalTags,
"total_views": totalViews, "total_views": totalViews,
"today_resources": todayResources, "today_resources": todayResources,
"today_updates": todayUpdates,
"today_views": todayViews, "today_views": todayViews,
"today_searches": todaySearches, "today_searches": todaySearches,
}) })
@@ -111,7 +116,7 @@ func GetPerformanceStats(c *gin.Context) {
func GetSystemInfo(c *gin.Context) { func GetSystemInfo(c *gin.Context) {
SuccessResponse(c, gin.H{ SuccessResponse(c, gin.H{
"uptime": time.Since(startTime).String(), "uptime": time.Since(startTime).String(),
"start_time": utils.FormatTime(startTime, "2006-01-02 15:04:05"), "start_time": utils.FormatTime(startTime, utils.TimeFormatDateTime),
"version": utils.Version, "version": utils.Version,
"environment": gin.H{ "environment": gin.H{
"gin_mode": gin.Mode(), "gin_mode": gin.Mode(),
@@ -146,7 +151,7 @@ func GetSearchesTrend(c *gin.Context) {
// 生成最近7天的日期 // 生成最近7天的日期
for i := 6; i >= 0; i-- { for i := 6; i >= 0; i-- {
date := utils.GetCurrentTime().AddDate(0, 0, -i) date := utils.GetCurrentTime().AddDate(0, 0, -i)
dateStr := date.Format("2006-01-02") dateStr := date.Format(utils.TimeFormatDate)
// 查询该日期的搜索量(从搜索统计表) // 查询该日期的搜索量(从搜索统计表)
var searches int64 var searches int64

View File

@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo" "github.com/ctwj/urldb/db/repo"
@@ -58,8 +57,8 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
Type: "transfer", Type: "transfer",
Status: "pending", Status: "pending",
TotalItems: len(req.Resources), TotalItems: len(req.Resources),
CreatedAt: time.Now(), CreatedAt: utils.GetCurrentTime(),
UpdatedAt: time.Now(), UpdatedAt: utils.GetCurrentTime(),
} }
err := h.repoMgr.TaskRepository.Create(newTask) err := h.repoMgr.TaskRepository.Create(newTask)
@@ -85,8 +84,8 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
TaskID: newTask.ID, TaskID: newTask.ID,
Status: "pending", Status: "pending",
InputData: string(inputJSON), InputData: string(inputJSON),
CreatedAt: time.Now(), CreatedAt: utils.GetCurrentTime(),
UpdatedAt: time.Now(), UpdatedAt: utils.GetCurrentTime(),
} }
err = h.repoMgr.TaskItemRepository.Create(taskItem) err = h.repoMgr.TaskItemRepository.Create(taskItem)

View File

@@ -257,7 +257,7 @@ func (a *AutoTransferScheduler) processAutoTransfer() {
utils.Error(fmt.Sprintf("转存资源失败 (ID: %d): %v", res.ID, err)) utils.Error(fmt.Sprintf("转存资源失败 (ID: %d): %v", res.ID, err))
} else { } else {
utils.Info(fmt.Sprintf("成功转存资源: %s", res.Title)) utils.Info(fmt.Sprintf("成功转存资源: %s", res.Title))
rand.Seed(time.Now().UnixNano()) rand.Seed(utils.GetCurrentTime().UnixNano())
sleepSec := rand.Intn(3) + 1 // 1,2,3 sleepSec := rand.Intn(3) + 1 // 1,2,3
time.Sleep(time.Duration(sleepSec) * time.Second) time.Sleep(time.Duration(sleepSec) * time.Second)
} }
@@ -289,7 +289,7 @@ func (a *AutoTransferScheduler) getQuarkPanID() (uint, error) {
// getResourcesForTransfer 获取需要转存的资源 // getResourcesForTransfer 获取需要转存的资源
func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint, limit int) ([]*entity.Resource, error) { func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint, limit int) ([]*entity.Resource, error) {
// 获取最近24小时内的资源 // 获取最近24小时内的资源
sinceTime := time.Now().Add(-24 * time.Hour) sinceTime := utils.GetCurrentTime().Add(-24 * time.Hour)
// 使用资源仓库的方法获取需要转存的资源 // 使用资源仓库的方法获取需要转存的资源
repoImpl, ok := a.resourceRepo.(*repo.ResourceRepositoryImpl) repoImpl, ok := a.resourceRepo.(*repo.ResourceRepositoryImpl)

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sync" "sync"
"time"
"github.com/ctwj/urldb/db/entity" "github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo" "github.com/ctwj/urldb/db/repo"
@@ -275,7 +274,7 @@ func (tm *TaskManager) processTaskItem(ctx context.Context, taskID uint, item *e
// 处理失败 // 处理失败
outputData := map[string]interface{}{ outputData := map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"time": time.Now(), "time": utils.GetCurrentTime(),
} }
outputJSON, _ := json.Marshal(outputData) outputJSON, _ := json.Marshal(outputData)
@@ -289,7 +288,7 @@ func (tm *TaskManager) processTaskItem(ctx context.Context, taskID uint, item *e
// 处理成功 // 处理成功
outputData := map[string]interface{}{ outputData := map[string]interface{}{
"success": true, "success": true,
"time": time.Now(), "time": utils.GetCurrentTime(),
} }
outputJSON, _ := json.Marshal(outputData) outputJSON, _ := json.Marshal(outputData)
@@ -315,7 +314,7 @@ func (tm *TaskManager) updateTaskProgress(taskID uint, progress float64, process
"processed": processed, "processed": processed,
"success": success, "success": success,
"failed": failed, "failed": failed,
"time": time.Now(), "time": utils.GetCurrentTime(),
} }
progressJSON, _ := json.Marshal(progressData) progressJSON, _ := json.Marshal(progressData)

View File

@@ -80,7 +80,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
ResourceID: existingResource.ID, ResourceID: existingResource.ID,
SaveURL: existingResource.SaveURL, SaveURL: existingResource.SaveURL,
Success: true, Success: true,
Time: time.Now().Format("2006-01-02 15:04:05"), Time: utils.GetCurrentTimeString(),
} }
outputJSON, _ := json.Marshal(output) outputJSON, _ := json.Marshal(output)
@@ -98,7 +98,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
output := TransferOutput{ output := TransferOutput{
Error: err.Error(), Error: err.Error(),
Success: false, Success: false,
Time: time.Now().Format("2006-01-02 15:04:05"), Time: utils.GetCurrentTimeString(),
} }
outputJSON, _ := json.Marshal(output) outputJSON, _ := json.Marshal(output)
@@ -113,7 +113,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
output := TransferOutput{ output := TransferOutput{
Error: "转存成功但未获取到分享链接", Error: "转存成功但未获取到分享链接",
Success: false, Success: false,
Time: time.Now().Format("2006-01-02 15:04:05"), Time: utils.GetCurrentTimeString(),
} }
outputJSON, _ := json.Marshal(output) outputJSON, _ := json.Marshal(output)
@@ -128,7 +128,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
ResourceID: resourceID, ResourceID: resourceID,
SaveURL: saveURL, SaveURL: saveURL,
Success: true, Success: true,
Time: time.Now().Format("2006-01-02 15:04:05"), Time: utils.GetCurrentTimeString(),
} }
outputJSON, _ := json.Marshal(output) outputJSON, _ := json.Marshal(output)

View File

@@ -5,6 +5,13 @@ import (
"time" "time"
) )
// 时间格式常量
const (
TimeFormatDate = "2006-01-02"
TimeFormatDateTime = "2006-01-02 15:04:05"
TimeFormatRFC3339 = time.RFC3339
)
// InitTimezone 初始化时区设置 // InitTimezone 初始化时区设置
func InitTimezone() { func InitTimezone() {
// 从环境变量获取时区配置 // 从环境变量获取时区配置
@@ -36,20 +43,35 @@ func GetCurrentTime() time.Time {
// GetCurrentTimeString 获取当前时间字符串(使用配置的时区) // GetCurrentTimeString 获取当前时间字符串(使用配置的时区)
func GetCurrentTimeString() string { func GetCurrentTimeString() string {
return time.Now().Format("2006-01-02 15:04:05") return time.Now().Format(TimeFormatDateTime)
} }
// GetCurrentTimeRFC3339 获取当前时间RFC3339格式使用配置的时区 // GetCurrentTimeRFC3339 获取当前时间RFC3339格式使用配置的时区
func GetCurrentTimeRFC3339() string { func GetCurrentTimeRFC3339() string {
return time.Now().Format(time.RFC3339) return time.Now().Format(TimeFormatRFC3339)
} }
// ParseTime 解析时间字符串(使用配置的时区) // ParseTime 解析时间字符串(使用配置的时区)
func ParseTime(timeStr string) (time.Time, error) { func ParseTime(timeStr string) (time.Time, error) {
return time.Parse("2006-01-02 15:04:05", timeStr) return time.Parse(TimeFormatDateTime, timeStr)
} }
// FormatTime 格式化时间(使用配置的时区) // FormatTime 格式化时间(使用配置的时区)
func FormatTime(t time.Time, layout string) string { func FormatTime(t time.Time, layout string) string {
return t.Format(layout) return t.Format(layout)
} }
// GetTodayString 获取今日日期字符串
func GetTodayString() string {
return time.Now().Format(TimeFormatDate)
}
// GetCurrentTimestamp 获取当前时间戳
func GetCurrentTimestamp() int64 {
return time.Now().Unix()
}
// GetCurrentTimestampNano 获取当前纳秒时间戳
func GetCurrentTimestampNano() int64 {
return time.Now().UnixNano()
}

View File

@@ -72,7 +72,7 @@ func GetFullVersionInfo() string {
Node版本: %s Node版本: %s
平台: %s/%s`, 平台: %s/%s`,
info.Version, info.Version,
FormatTime(info.BuildTime, "2006-01-02 15:04:05"), FormatTime(info.BuildTime, TimeFormatDateTime),
info.GitCommit, info.GitCommit,
info.GitBranch, info.GitBranch,
info.GoVersion, info.GoVersion,

1
web/components.d.ts vendored
View File

@@ -24,6 +24,7 @@ declare module 'vue' {
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NList: typeof import('naive-ui')['NList'] NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem'] NListItem: typeof import('naive-ui')['NListItem']
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']

View File

@@ -136,7 +136,7 @@ const systemConfigStore = useSystemConfigStore()
const systemConfig = computed(() => systemConfigStore.config) const systemConfig = computed(() => systemConfigStore.config)
onMounted(() => { onMounted(() => {
systemConfigStore.initConfig() systemConfigStore.initConfig(false, true)
}) })
// 退出登录 // 退出登录

View File

@@ -8,19 +8,16 @@
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
资源信息 <span class="text-red-500">*</span> 资源内容 <span class="text-red-500">*</span>
</label> </label>
<n-input <n-input
v-model:value="resourceText" v-model:value="resourceText"
type="textarea" type="textarea"
placeholder="请输入资源信息,每行格式:标题|链接地址&#10;例如:&#10;电影名称1|https://pan.quark.cn/s/xxx&#10;电影名称2|https://pan.baidu.com/s/xxx" placeholder="请输入资源内容格式标题和URL为一组..."
:rows="12" :autosize="{ minRows: 10, maxRows: 15 }"
show-count show-count
:maxlength="10000" :maxlength="10000"
/> />
<p class="text-xs text-gray-500 mt-1">
每行一个资源格式标题|链接地址用竖线分隔
</p>
</div> </div>
</div> </div>
@@ -73,7 +70,7 @@
<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>
@@ -286,7 +283,7 @@ const handleBatchTransfer = async () => {
const resourceList = parseResourceText(resourceText.value) const resourceList = parseResourceText(resourceText.value)
if (resourceList.length === 0) { if (resourceList.length === 0) {
message.warning('没有找到有效的资源信息,请按照"标题"和"链接"分行输入') message.warning('没有找到有效的资源信息,请按照格式要求输入标题和URL为一组标题必填')
return return
} }
@@ -333,22 +330,49 @@ const handleBatchTransfer = async () => {
} }
} }
// 解析资源文本,按照 标题\n链接 的格式 // 解析资源文本,按照 标题\n链接 的格式支持同一标题多个URL
const parseResourceText = (text: string) => { const parseResourceText = (text: string) => {
const lines = text.split('\n').filter((line: string) => line.trim()) const lines = text.split('\n').filter((line: string) => line.trim())
const resourceList = [] const resourceList = []
for (let i = 0; i < lines.length; i += 2) { let currentTitle = ''
const title = lines[i]?.trim() let currentUrls = []
const url = lines[i + 1]?.trim()
for (const line of lines) {
if (title && url && isValidUrl(url)) { // 判断是否为 url以 http/https 开头)
resourceList.push({ if (/^https?:\/\//i.test(line)) {
title, currentUrls.push(line.trim())
url, } else {
category_id: selectedCategory.value || 0, // 新标题,先保存上一个
tags: selectedTags.value || [] if (currentTitle && currentUrls.length > 0) {
}) // 为每个URL创建一个资源项
for (const url of currentUrls) {
if (isValidUrl(url)) {
resourceList.push({
title: currentTitle,
url: url,
category_id: selectedCategory.value || 0,
tags: selectedTags.value || []
})
}
}
}
currentTitle = line.trim()
currentUrls = []
}
}
// 处理最后一组
if (currentTitle && currentUrls.length > 0) {
for (const url of currentUrls) {
if (isValidUrl(url)) {
resourceList.push({
title: currentTitle,
url: url,
category_id: selectedCategory.value || 0,
tags: selectedTags.value || []
})
}
} }
} }

View File

@@ -92,14 +92,14 @@ const autoTransferEnabled = ref(false)
// 获取系统配置状态 // 获取系统配置状态
const fetchSystemStatus = async () => { const fetchSystemStatus = async () => {
try { try {
await systemConfigStore.initConfig() await systemConfigStore.initConfig(false, true)
// 从系统配置中获取自动处理和自动转存状态 // 从系统配置中获取自动处理和自动转存状态
const config = systemConfigStore.config const config = systemConfigStore.config
if (config) { if (config) {
// 检查自动处理状态 // 检查自动处理状态
autoProcessEnabled.value = config.auto_process_enabled === '1' || config.auto_process_enabled === true autoProcessEnabled.value = config.auto_process_ready_resources === '1' || config.auto_process_ready_resources === true
// 检查自动转存状态 // 检查自动转存状态
autoTransferEnabled.value = config.auto_transfer_enabled === '1' || config.auto_transfer_enabled === true autoTransferEnabled.value = config.auto_transfer_enabled === '1' || config.auto_transfer_enabled === true

View File

@@ -33,6 +33,11 @@
</n-button> </n-button>
</div> </div>
<!-- 调试信息 -->
<div class="text-sm text-gray-500 mb-2">
数据数量: {{ resources.length }}, 总数: {{ total }}, 加载状态: {{ loading }}
</div>
<!-- 数据表格 --> <!-- 数据表格 -->
<n-data-table <n-data-table
:columns="columns" :columns="columns"
@@ -51,10 +56,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue' import { ref, reactive, computed, onMounted, h } from 'vue'
import { useResourceApi } from '~/composables/useApi' import { useResourceApi } from '~/composables/useApi'
import { useMessage } from 'naive-ui'
// 消息提示
const $message = useMessage()
// 数据状态 // 数据状态
const loading = ref(false) const loading = ref(false)
const resources = ref([]) const resources = ref<any[]>([])
const total = ref(0) const total = ref(0)
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
@@ -79,12 +88,12 @@ const pagination = reactive({
}) })
// 表格列配置 // 表格列配置
const columns = [ const columns: any[] = [
{ {
title: 'ID', title: 'ID',
key: 'id', key: 'id',
width: 60, width: 60,
fixed: 'left' fixed: 'left' as const
}, },
{ {
title: '标题', title: '标题',
@@ -101,11 +110,14 @@ const columns = [
}, },
{ {
title: '平台', title: '平台',
key: 'platform_name', key: 'pan_name',
width: 80, width: 80,
render: (row: any) => { render: (row: any) => {
const platform = platformOptions.value.find((p: any) => p.value === row.pan_id) if (row.pan_id) {
return platform?.label || '未知' const platform = platformOptions.value.find((p: any) => p.value === row.pan_id)
return platform?.label || '未知'
}
return '未知'
} }
}, },
{ {
@@ -137,31 +149,39 @@ const columns = [
width: 120, width: 120,
fixed: 'right', fixed: 'right',
render: (row: any) => { render: (row: any) => {
return [ return h('div', { class: 'flex space-x-2' }, [
h('n-button', { h('n-button', {
size: 'small', size: 'small',
type: 'primary', type: 'primary',
onClick: () => viewResource(row) onClick: () => viewResource(row)
}, '查看'), }, { default: () => '查看' }),
h('n-button', { h('n-button', {
size: 'small', size: 'small',
type: 'info', type: 'info',
style: { marginLeft: '8px' },
onClick: () => copyLink(row.save_url) onClick: () => copyLink(row.save_url)
}, '复制') }, { default: () => '复制' })
] ])
} }
} }
] ]
// 平台选项 // 平台选项
const platformOptions = ref([]) const platformOptions = ref([
{ label: '夸克网盘', value: 1 },
{ label: '百度网盘', value: 2 },
{ label: '阿里云盘', value: 3 },
{ label: '天翼云盘', value: 4 },
{ label: '迅雷云盘', value: 5 },
{ label: '123云盘', value: 6 },
{ label: '115网盘', value: 7 },
{ label: 'UC网盘', value: 8 }
])
// 获取已转存资源 // 获取已转存资源
const fetchTransferredResources = async () => { const fetchTransferredResources = async () => {
loading.value = true loading.value = true
try { try {
const params = { const params: any = {
page: currentPage.value, page: currentPage.value,
page_size: pageSize.value, page_size: pageSize.value,
has_save_url: true // 筛选有转存链接的资源 has_save_url: true // 筛选有转存链接的资源
@@ -174,17 +194,36 @@ const fetchTransferredResources = async () => {
params.category_id = selectedCategory.value params.category_id = selectedCategory.value
} }
console.log('请求参数:', params)
const result = await resourceApi.getResources(params) as any const result = await resourceApi.getResources(params) as any
console.log('已转存资源结果:', result) console.log('已转存资源结果:', result)
console.log('结果类型:', typeof result)
console.log('结果结构:', Object.keys(result || {}))
if (result && result.resources) { if (result && result.data) {
resources.value = result.resources console.log('使用 resources 格式,数量:', result.data.length)
resources.value = result.data
total.value = result.total || 0 total.value = result.total || 0
pagination.itemCount = result.total || 0 pagination.itemCount = result.total || 0
} else if (Array.isArray(result)) { } else if (Array.isArray(result)) {
console.log('使用数组格式,数量:', result.length)
resources.value = result resources.value = result
total.value = result.length total.value = result.length
pagination.itemCount = result.length pagination.itemCount = result.length
} else {
console.log('未知格式,设置空数组')
resources.value = []
total.value = 0
pagination.itemCount = 0
}
console.log('最终 resources.value:', resources.value)
console.log('最终 total.value:', total.value)
// 检查是否有资源没有 save_url
const resourcesWithoutSaveUrl = resources.value.filter((r: any) => !r.save_url || r.save_url.trim() === '')
if (resourcesWithoutSaveUrl.length > 0) {
console.warn('发现没有 save_url 的资源:', resourcesWithoutSaveUrl.map((r: any) => ({ id: r.id, title: r.title, save_url: r.save_url })))
} }
} catch (error) { } catch (error) {
console.error('获取已转存资源失败:', error) console.error('获取已转存资源失败:', error)

View File

@@ -327,8 +327,8 @@ const fetchUntransferredResources = async () => {
const result = await resourceApi.getResources(params) as any const result = await resourceApi.getResources(params) as any
console.log('未转存资源结果:', result) console.log('未转存资源结果:', result)
if (result && result.resources) { if (result && result.data) {
resources.value = result.resources resources.value = result.data
total.value = result.total || 0 total.value = result.total || 0
} else if (Array.isArray(result)) { } else if (Array.isArray(result)) {
resources.value = result resources.value = result

View File

@@ -26,6 +26,15 @@ export const parseApiResponse = <T>(response: any): T => {
// 检查是否是包含success字段的响应格式如登录接口 // 检查是否是包含success字段的响应格式如登录接口
if (response && typeof response === 'object' && 'success' in response && 'data' in response) { if (response && typeof response === 'object' && 'success' in response && 'data' in response) {
if (response.success) { if (response.success) {
// 特殊处理资源接口返回的data格式转换为resources格式
if (response.data && Array.isArray(response.data) && response.total !== undefined) {
return {
resources: response.data,
total: response.total,
page: response.page,
page_size: response.page_size
} as T
}
// 特殊处理资源接口返回的data.list格式转换为resources格式 // 特殊处理资源接口返回的data.list格式转换为resources格式
if (response.data && response.data.list && Array.isArray(response.data.list)) { if (response.data && response.data.list && Array.isArray(response.data.list)) {
return { return {

View File

@@ -0,0 +1,82 @@
// 统一的时间格式化工具函数
export const useTimeFormat = () => {
// 格式化日期时间(标准格式)
const formatDateTime = (dateString: string | Date) => {
if (!dateString) return '-'
const date = dateString instanceof Date ? dateString : new Date(dateString)
return date.toLocaleString('zh-CN')
}
// 格式化日期(仅日期)
const formatDate = (dateString: string | Date) => {
if (!dateString) return '-'
const date = dateString instanceof Date ? dateString : new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
// 格式化时间(仅时间)
const formatTime = (dateString: string | Date) => {
if (!dateString) return '-'
const date = dateString instanceof Date ? dateString : new Date(dateString)
return date.toLocaleTimeString('zh-CN')
}
// 格式化相对时间
const formatRelativeTime = (dateString: string | Date) => {
if (!dateString) return '-'
const date = dateString instanceof Date ? dateString : new Date(dateString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffSec = Math.floor(diffMs / 1000)
const diffMin = Math.floor(diffSec / 60)
const diffHour = Math.floor(diffMin / 60)
const diffDay = Math.floor(diffHour / 24)
const diffWeek = Math.floor(diffDay / 7)
const diffMonth = Math.floor(diffDay / 30)
const diffYear = Math.floor(diffDay / 365)
const isToday = date.toDateString() === now.toDateString()
if (isToday) {
if (diffMin < 1) {
return '刚刚'
} else if (diffHour < 1) {
return `${diffMin}分钟前`
} else {
return `${diffHour}小时前`
}
} else if (diffDay < 1) {
return `${diffHour}小时前`
} else if (diffDay < 7) {
return `${diffDay}天前`
} else if (diffWeek < 4) {
return `${diffWeek}周前`
} else if (diffMonth < 12) {
return `${diffMonth}个月前`
} else {
return `${diffYear}年前`
}
}
// 获取当前时间字符串
const getCurrentTimeString = () => {
return new Date().toLocaleString('zh-CN')
}
// 检查是否为今天
const isToday = (dateString: string | Date) => {
if (!dateString) return false
const date = dateString instanceof Date ? dateString : new Date(dateString)
const now = new Date()
return date.toDateString() === now.toDateString()
}
return {
formatDateTime,
formatDate,
formatTime,
formatRelativeTime,
getCurrentTimeString,
isToday
}
}

View File

@@ -304,8 +304,8 @@ const systemConfigStore = useSystemConfigStore()
// 任务状态管理 // 任务状态管理
const taskStore = useTaskStore() const taskStore = useTaskStore()
// 初始化系统配置 // 初始化系统配置管理员页面使用管理员API
await systemConfigStore.initConfig() await systemConfigStore.initConfig(false, true)
// 版本信息 // 版本信息
const versionInfo = ref({ const versionInfo = ref({
@@ -495,10 +495,10 @@ const operationItems = ref([
active: (route: any) => route.path.startsWith('/admin/data-push') active: (route: any) => route.path.startsWith('/admin/data-push')
}, },
{ {
to: '/admin/auto-reply', to: '/admin/bot',
label: '自动回复', label: '机器人',
icon: 'fas fa-comments', icon: 'fas fa-robot',
active: (route: any) => route.path.startsWith('/admin/auto-reply') active: (route: any) => route.path.startsWith('/admin/bot')
}, },
{ {
to: '/admin/seo', to: '/admin/seo',
@@ -533,7 +533,7 @@ const autoExpandCurrentGroup = () => {
expandedGroups.value.dataManagement = true expandedGroups.value.dataManagement = true
} else if (currentPath.startsWith('/admin/site-config') || currentPath.startsWith('/admin/feature-config') || currentPath.startsWith('/admin/dev-config') || currentPath.startsWith('/admin/users') || currentPath.startsWith('/admin/version')) { } else if (currentPath.startsWith('/admin/site-config') || currentPath.startsWith('/admin/feature-config') || currentPath.startsWith('/admin/dev-config') || currentPath.startsWith('/admin/users') || currentPath.startsWith('/admin/version')) {
expandedGroups.value.systemConfig = true expandedGroups.value.systemConfig = true
} else if (currentPath.startsWith('/admin/data-transfer') || currentPath.startsWith('/admin/seo') || currentPath.startsWith('/admin/data-push') || currentPath.startsWith('/admin/auto-reply')) { } else if (currentPath.startsWith('/admin/data-transfer') || currentPath.startsWith('/admin/seo') || currentPath.startsWith('/admin/data-push') || currentPath.startsWith('/admin/bot')) {
expandedGroups.value.operation = true expandedGroups.value.operation = true
} else if (currentPath.startsWith('/admin/search-stats') || currentPath.startsWith('/admin/third-party-stats')) { } else if (currentPath.startsWith('/admin/search-stats') || currentPath.startsWith('/admin/third-party-stats')) {
expandedGroups.value.statistics = true expandedGroups.value.statistics = true
@@ -555,7 +555,7 @@ watch(() => useRoute().path, (newPath) => {
expandedGroups.value.dataManagement = true expandedGroups.value.dataManagement = true
} else if (newPath.startsWith('/admin/site-config') || newPath.startsWith('/admin/feature-config') || newPath.startsWith('/admin/dev-config') || newPath.startsWith('/admin/users') || newPath.startsWith('/admin/version')) { } else if (newPath.startsWith('/admin/site-config') || newPath.startsWith('/admin/feature-config') || newPath.startsWith('/admin/dev-config') || newPath.startsWith('/admin/users') || newPath.startsWith('/admin/version')) {
expandedGroups.value.systemConfig = true expandedGroups.value.systemConfig = true
} else if (newPath.startsWith('/admin/data-transfer') || newPath.startsWith('/admin/seo') || newPath.startsWith('/admin/data-push') || newPath.startsWith('/admin/auto-reply')) { } else if (newPath.startsWith('/admin/data-transfer') || newPath.startsWith('/admin/seo') || newPath.startsWith('/admin/data-push') || newPath.startsWith('/admin/bot')) {
expandedGroups.value.operation = true expandedGroups.value.operation = true
} else if (newPath.startsWith('/admin/search-stats') || newPath.startsWith('/admin/third-party-stats')) { } else if (newPath.startsWith('/admin/search-stats') || newPath.startsWith('/admin/third-party-stats')) {
expandedGroups.value.statistics = true expandedGroups.value.statistics = true

View File

@@ -1,198 +0,0 @@
<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>
<n-button type="primary" @click="saveConfig" :loading="saving">
<template #icon>
<i class="fas fa-save"></i>
</template>
保存配置
</n-button>
</div>
<!-- 配置表单 -->
<n-card>
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="mb-6"
>
<n-tab-pane name="qq" tab="QQ机器人">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- QQ机器人配置占位符 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">QQ机器人开关</label>
<span class="text-xs text-gray-500 dark:text-gray-400">开启QQ机器人自动回复功能</span>
</div>
<n-switch v-model:value="configForm.qq_bot_enabled" />
</div>
<!-- 占位符内容 -->
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<i class="fas fa-cog text-4xl mb-4"></i>
<p class="text-lg font-medium mb-2">QQ机器人配置</p>
<p class="text-sm">QQ机器人自动回复功能配置区域</p>
<p class="text-xs mt-2">具体配置项待开发...</p>
</div>
</div>
</n-form>
</n-tab-pane>
<n-tab-pane name="wechat" tab="微信公众号">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- 微信公众号配置占位符 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">微信公众号开关</label>
<span class="text-xs text-gray-500 dark:text-gray-400">开启微信公众号自动回复功能</span>
</div>
<n-switch v-model:value="configForm.wechat_mp_enabled" />
</div>
<!-- 占位符内容 -->
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<i class="fas fa-comment-dots text-4xl mb-4"></i>
<p class="text-lg font-medium mb-2">微信公众号配置</p>
<p class="text-sm">微信公众号自动回复功能配置区域</p>
<p class="text-xs mt-2">具体配置项待开发...</p>
</div>
</div>
</n-form>
</n-tab-pane>
<n-tab-pane name="wechat_open" tab="微信对话开放平台">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-6">
<!-- 微信对话开放平台配置占位符 -->
<div class="space-y-2">
<div class="flex items-center space-x-2">
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">微信对话开放平台开关</label>
<span class="text-xs text-gray-500 dark:text-gray-400">开启微信对话开放平台自动回复功能</span>
</div>
<n-switch v-model:value="configForm.wechat_open_enabled" />
</div>
<!-- 占位符内容 -->
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<i class="fas fa-comments text-4xl mb-4"></i>
<p class="text-lg font-medium mb-2">微信对话开放平台配置</p>
<p class="text-sm">微信对话开放平台自动回复功能配置区域</p>
<p class="text-xs mt-2">具体配置项待开发...</p>
</div>
</div>
</n-form>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin',
ssr: false
})
const notification = useNotification()
const formRef = ref()
const saving = ref(false)
const activeTab = ref('qq')
// 配置表单数据
const configForm = ref({
qq_bot_enabled: false,
wechat_mp_enabled: false,
wechat_open_enabled: false
})
// 表单验证规则
const rules = {
// 暂时为空,后续添加验证规则
}
// 获取配置
const fetchConfig = async () => {
try {
// 暂时使用模拟数据
configForm.value = {
qq_bot_enabled: false,
wechat_mp_enabled: false,
wechat_open_enabled: false
}
} catch (error) {
console.error('获取自动回复配置失败:', error)
notification.error({
content: '获取自动回复配置失败',
duration: 3000
})
}
}
// 保存配置
const saveConfig = async () => {
try {
saving.value = true
// 暂时使用模拟保存
await new Promise(resolve => setTimeout(resolve, 1000))
notification.success({
content: '自动回复配置保存成功',
duration: 3000
})
} catch (error) {
console.error('保存自动回复配置失败:', error)
notification.error({
content: '保存自动回复配置失败',
duration: 3000
})
} finally {
saving.value = false
}
}
// 页面加载时获取配置
onMounted(() => {
fetchConfig()
})
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -1,26 +1,255 @@
<template> <template>
<div class="p-6"> <div class="space-y-6">
<div class="mb-6"> <!-- 页面标题 -->
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">机器人管理</h1> <div class="flex items-center justify-between">
<p class="text-gray-600 dark:text-gray-400 mt-2">机器人配置与管理</p> <div>
</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 class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-center py-12">
<div class="text-gray-400 dark:text-gray-500 mb-4">
<i class="fas fa-robot text-4xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能开发中</h3>
<p class="text-gray-500 dark:text-gray-400">机器人管理功能正在开发中敬请期待...</p>
</div> </div>
</div> </div>
<!-- 配置表单 -->
<n-card>
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="mb-6"
>
<n-tab-pane name="qq" tab="QQ机器人">
<div class="space-y-8">
<!-- 步骤1Astrobot 安装指南 -->
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-6">
<div class="flex items-center mb-4">
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">1</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">安装 Astrobot</h3>
</div>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<i class="fas fa-github text-gray-600 dark:text-gray-400 mt-1"></i>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white mb-1">开源地址</p>
<a
href="https://github.com/Astrian/astrobot"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
https://github.com/Astrian/astrobot
</a>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-book text-gray-600 dark:text-gray-400 mt-1"></i>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white mb-1">安装教程</p>
<a
href="https://github.com/Astrian/astrobot/wiki"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
https://github.com/Astrian/astrobot/wiki
</a>
</div>
</div>
</div>
</div>
<!-- 步骤2插件安装 -->
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-6">
<div class="flex items-center mb-4">
<div class="w-8 h-8 bg-green-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">2</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">安装插件</h3>
</div>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<i class="fas fa-puzzle-piece text-gray-600 dark:text-gray-400 mt-1"></i>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white mb-1">插件地址</p>
<a
href="https://github.com/ctwj/astrbot_plugin_urldb"
target="_blank"
class="text-green-600 dark:text-green-400 hover:underline text-sm"
>
https://github.com/ctwj/astrbot_plugin_urldb
</a>
</div>
</div>
<div class="bg-gray-100 dark:bg-gray-800 rounded p-3">
<p class="text-sm text-gray-700 dark:text-gray-300">
<strong>插件特性</strong><br>
支持@机器人搜索功能<br>
可配置API域名和密钥<br>
自动格式化搜索结果<br>
支持超时时间配置<br><br>
<strong>安装步骤</strong><br>
1. 下载插件文件<br>
2. 将插件放入 Astrobot plugins 目录<br>
3. 重启 Astrobot<br>
4. 在配置文件中添加插件配置
</p>
</div>
</div>
</div>
<!-- 步骤3配置信息 -->
<div class="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-6">
<div class="flex items-center mb-4">
<div class="w-8 h-8 bg-purple-600 text-white rounded-full flex items-center justify-center mr-3">
<span class="text-sm font-bold">3</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">配置信息</h3>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">网站域名</label>
<div class="flex items-center space-x-2">
<n-input
:value="siteDomain"
readonly
class="flex-1"
/>
<n-button
size="small"
@click="copyToClipboard(siteDomain)"
type="primary"
>
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">API Token</label>
<div class="flex items-center space-x-2">
<n-input
:value="apiToken"
readonly
type="password"
show-password-on="click"
class="flex-1"
/>
<n-button
size="small"
@click="copyToClipboard(apiToken)"
type="primary"
>
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
</div>
<div class="bg-gray-100 dark:bg-gray-800 rounded p-3">
<p class="text-sm text-gray-700 dark:text-gray-300">
<strong>配置说明</strong><br>
将上述信息配置到 Astrobot 的插件配置文件中插件将自动连接到本系统进行资源搜索
</p>
</div>
</div>
</div>
</div>
</n-tab-pane>
<n-tab-pane name="wechat" tab="微信公众号">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">微信公众号机器人功能正在开发中敬请期待</p>
</div>
</n-tab-pane>
<n-tab-pane name="telegram" tab="Telegram机器人">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">Telegram机器人功能正在开发中敬请期待</p>
</div>
</n-tab-pane>
<n-tab-pane name="wechat_open" tab="微信开放平台">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">微信开放平台机器人功能正在开发中敬请期待</p>
</div>
</n-tab-pane>
</n-tabs>
</n-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 机器人管理页面 // 设置页面布局
definePageMeta({ definePageMeta({
layout: 'admin', layout: 'admin',
middleware: ['auth'] ssr: false
}) })
</script>
const notification = useNotification()
const activeTab = ref('qq')
// 获取网站域名和API Token
const siteDomain = computed(() => {
if (process.client) {
return window.location.origin
}
return 'https://yourdomain.com'
})
const apiToken = ref('')
// 获取API Token
const fetchApiToken = async () => {
try {
const { useSystemConfigApi } = await import('~/composables/useApi')
const systemConfigApi = useSystemConfigApi()
const response = await systemConfigApi.getSystemConfig()
if (response && (response as any).api_token) {
apiToken.value = (response as any).api_token
} else {
apiToken.value = '未配置API Token'
}
} catch (error) {
console.error('获取API Token失败:', error)
apiToken.value = '获取失败'
}
}
// 复制到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
notification.success({
content: '已复制到剪贴板',
duration: 2000
})
} catch (error) {
console.error('复制失败:', error)
notification.error({
content: '复制失败',
duration: 2000
})
}
}
// 页面加载时获取配置
onMounted(() => {
fetchApiToken()
console.log('机器人管理页面已加载')
})
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -127,6 +127,11 @@ const saveConfig = async () => {
content: '开发配置保存成功', content: '开发配置保存成功',
duration: 3000 duration: 3000
}) })
// 刷新系统配置状态,确保顶部导航同步更新
const { useSystemConfigStore } = await import('~/stores/systemConfig')
const systemConfigStore = useSystemConfigStore()
await systemConfigStore.initConfig(true, true) // 强制刷新使用管理员API
} catch (error) { } catch (error) {
console.error('保存开发配置失败:', error) console.error('保存开发配置失败:', error)
notification.error({ notification.error({

View File

@@ -229,6 +229,11 @@ const saveConfig = async () => {
content: '功能配置保存成功', content: '功能配置保存成功',
duration: 3000 duration: 3000
}) })
// 刷新系统配置状态,确保顶部导航同步更新
const { useSystemConfigStore } = await import('~/stores/systemConfig')
const systemConfigStore = useSystemConfigStore()
await systemConfigStore.initConfig(true, true) // 强制刷新使用管理员API
} catch (error) { } catch (error) {
console.error('保存功能配置失败:', error) console.error('保存功能配置失败:', error)
notification.error({ notification.error({

View File

@@ -414,54 +414,54 @@ const linkColumns = [
// 提交到百度 // 提交到百度
const submitToBaidu = () => { const submitToBaidu = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.baidu = new Date().toLocaleString() lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
message.success('已提交到百度') message.success('已提交到百度')
} }
// 提交到谷歌 // 提交到谷歌
const submitToGoogle = () => { const submitToGoogle = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.google = new Date().toLocaleString() lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
message.success('已提交到谷歌') message.success('已提交到谷歌')
} }
// 提交到必应 // 提交到必应
const submitToBing = () => { const submitToBing = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.bing = new Date().toLocaleString() lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
message.success('已提交到必应') message.success('已提交到必应')
} }
// 提交到搜狗 // 提交到搜狗
const submitToSogou = () => { const submitToSogou = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.sogou = new Date().toLocaleString() lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
message.success('已提交到搜狗') message.success('已提交到搜狗')
} }
// 提交到神马搜索 // 提交到神马搜索
const submitToShenma = () => { const submitToShenma = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.shenma = new Date().toLocaleString() lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
message.success('已提交到神马搜索') message.success('已提交到神马搜索')
} }
// 提交到360搜索 // 提交到360搜索
const submitTo360 = () => { const submitTo360 = () => {
// 模拟提交 // 模拟提交
lastSubmitTime.value.so360 = new Date().toLocaleString() lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
message.success('已提交到360搜索') message.success('已提交到360搜索')
} }
// 批量提交 // 批量提交
const submitToAll = () => { const submitToAll = () => {
// 模拟批量提交 // 模拟批量提交
lastSubmitTime.value.baidu = new Date().toLocaleString() lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.google = new Date().toLocaleString() lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.bing = new Date().toLocaleString() lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.sogou = new Date().toLocaleString() lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.shenma = new Date().toLocaleString() lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
lastSubmitTime.value.so360 = new Date().toLocaleString() lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
message.success('已批量提交到所有搜索引擎') message.success('已批量提交到所有搜索引擎')
} }

View File

@@ -242,6 +242,11 @@ const saveConfig = async () => {
content: '站点配置保存成功', content: '站点配置保存成功',
duration: 3000 duration: 3000
}) })
// 刷新系统配置状态,确保顶部导航同步更新
const { useSystemConfigStore } = await import('~/stores/systemConfig')
const systemConfigStore = useSystemConfigStore()
await systemConfigStore.initConfig(true, true) // 强制刷新使用管理员API
} catch (error) { } catch (error) {
console.error('保存站点配置失败:', error) console.error('保存站点配置失败:', error)
notification.error({ notification.error({

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="p-6 space-y-6"> <div class="p-4 space-y-4">
<!-- 页面标题和返回按钮 --> <!-- 页面标题和返回按钮 -->
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0"> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-3 lg:space-y-0">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-3">
<n-button <n-button
quaternary quaternary
size="small" size="small"
@@ -11,12 +11,11 @@
<template #icon> <template #icon>
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</template> </template>
<span class="hidden sm:inline">返回任务列表</span> <span class="hidden sm:inline">返回</span>
<span class="sm:hidden">返回</span>
</n-button> </n-button>
<div> <div>
<h1 class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">任务详情</h1> <h1 class="text-lg md:text-xl font-bold text-gray-900 dark:text-white">任务详情</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">查看任务的详细信息和执行状态</p> <p class="text-xs text-gray-600 dark:text-gray-400">查看任务的详细信息和执行状态</p>
</div> </div>
</div> </div>
@@ -75,102 +74,96 @@
</div> </div>
<!-- 加载状态 --> <!-- 加载状态 -->
<div v-if="loading" class="flex justify-center py-8"> <div v-if="loading" class="flex justify-center py-4">
<n-spin size="large" /> <n-spin size="medium" />
</div> </div>
<!-- 任务详情 --> <!-- 任务详情 -->
<div v-else-if="task" class="space-y-6"> <div v-else-if="task" class="space-y-4">
<!-- 基本信息卡片 --> <!-- 整合的任务信息卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 md:p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">基本信息</h2> <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <!-- 左侧基本信息 -->
<div> <div class="flex-1">
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务ID</label> <div class="flex items-center space-x-4 mb-3">
<p class="text-sm text-gray-900 dark:text-white mt-1">{{ task.id }}</p> <h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ task.title }}</h2>
</div> <n-tag :type="getTaskStatusColor(task.status)" size="small">
<div> {{ getTaskStatusText(task.status) }}
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务标题</label> </n-tag>
<p class="text-sm text-gray-900 dark:text-white mt-1 break-words">{{ task.title }}</p>
</div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务类型</label>
<div class="mt-1">
<n-tag :type="getTaskTypeColor(task.task_type)" size="small"> <n-tag :type="getTaskTypeColor(task.task_type)" size="small">
{{ getTaskTypeText(task.task_type) }} {{ getTaskTypeText(task.task_type) }}
</n-tag> </n-tag>
</div> </div>
</div>
<div> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务状态</label> <div>
<div class="mt-1"> <span class="text-gray-500 dark:text-gray-400">ID:</span>
<n-tag :type="getTaskStatusColor(task.status)" size="small"> <span class="ml-1 text-gray-900 dark:text-white">{{ task.id }}</span>
{{ getTaskStatusText(task.status) }} </div>
</n-tag> <div>
<span class="text-gray-500 dark:text-gray-400">总项目:</span>
<span class="ml-1 text-gray-900 dark:text-white">{{ task.total_items || 0 }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">已处理:</span>
<span class="ml-1 text-blue-600 dark:text-blue-400 font-medium">{{ task.processed_items || 0 }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">成功率:</span>
<span class="ml-1 text-green-600 dark:text-green-400 font-medium">
{{ task.total_items > 0 ? Math.round((task.success_items / task.total_items) * 100) : 0 }}%
</span>
</div>
</div>
<!-- 进度条 -->
<div class="mt-3" v-if="task.total_items > 0">
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-gray-500 dark:text-gray-400">进度</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ Math.round((task.processed_items / task.total_items) * 100) }}%
</span>
</div>
<n-progress
type="line"
:percentage="Math.round((task.processed_items / task.total_items) * 100)"
:height="6"
:show-indicator="false"
/>
</div> </div>
</div> </div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">创建时间</label> <!-- 右侧统计信息 -->
<p class="text-sm text-gray-900 dark:text-white mt-1">{{ formatDate(task.created_at) }}</p> <div class="flex items-center space-x-6">
</div> <div class="text-center">
<div> <div class="text-lg font-bold text-green-600 dark:text-green-400">{{ task.success_items || 0 }}</div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">更新时间</label> <div class="text-xs text-gray-500 dark:text-gray-400">成功</div>
<p class="text-sm text-gray-900 dark:text-white mt-1">{{ formatDate(task.updated_at) }}</p> </div>
<div class="text-center">
<div class="text-lg font-bold text-red-600 dark:text-red-400">{{ task.failed_items || 0 }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">失败</div>
</div>
<div class="text-center">
<div class="text-lg font-bold text-gray-600 dark:text-gray-400">{{ task.total_items - task.processed_items || 0 }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">待处理</div>
</div>
</div> </div>
</div> </div>
<div v-if="task.description" class="mt-4"> <!-- 任务描述如果有 -->
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务描述</label> <div v-if="task.description" class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-900 dark:text-white mt-1 break-words">{{ task.description }}</p> <p class="text-sm text-gray-600 dark:text-gray-400">{{ task.description }}</p>
</div>
</div>
<!-- 进度信息卡片 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 md:p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">进度信息</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">总项目数</label>
<p class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mt-1">{{ task.total_items || 0 }}</p>
</div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">已处理</label>
<p class="text-xl md:text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">{{ task.processed_items || 0 }}</p>
</div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">成功</label>
<p class="text-xl md:text-2xl font-bold text-green-600 dark:text-green-400 mt-1">{{ task.success_items || 0 }}</p>
</div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">失败</label>
<p class="text-xl md:text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{{ task.failed_items || 0 }}</p>
</div>
</div>
<!-- 进度条 -->
<div class="mt-4" v-if="task.total_items > 0">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总体进度</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ Math.round((task.processed_items / task.total_items) * 100) }}%
</span>
</div>
<n-progress
type="line"
:percentage="Math.round((task.processed_items / task.total_items) * 100)"
:height="8"
:show-indicator="false"
/>
</div> </div>
</div> </div>
<!-- 任务项列表 --> <!-- 任务项列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
<div class="p-4 md:p-6 border-b border-gray-200 dark:border-gray-700"> <div class="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">任务项列表</h2> <h2 class="text-base font-semibold text-gray-900 dark:text-white">任务项列表</h2>
<span class="text-sm text-gray-500 dark:text-gray-400"> {{ total }} </span>
</div> </div>
<div class="p-4 md:p-6 overflow-x-auto"> <div class="overflow-x-auto">
<n-data-table <n-data-table
:columns="taskItemColumns" :columns="taskItemColumns"
:data="taskItems" :data="taskItems"
@@ -178,13 +171,14 @@
:pagination="itemsPaginationConfig" :pagination="itemsPaginationConfig"
size="small" size="small"
:scroll-x="600" :scroll-x="600"
:bordered="false"
/> />
</div> </div>
</div> </div>
</div> </div>
<!-- 错误状态 --> <!-- 错误状态 -->
<div v-else class="text-center py-8"> <div v-else class="text-center py-4">
<n-empty description="任务不存在或已被删除" /> <n-empty description="任务不存在或已被删除" />
</div> </div>
</div> </div>
@@ -237,26 +231,30 @@ const taskItemColumns = [
{ {
title: 'ID', title: 'ID',
key: 'id', key: 'id',
width: 80, width: 60,
minWidth: 80 minWidth: 60
}, },
{ {
title: '输入数据', title: '输入数据',
key: 'input', key: 'input',
minWidth: 200, minWidth: 250,
ellipsis: { ellipsis: {
tooltip: true tooltip: true
}, },
render: (row: any) => { render: (row: any) => {
if (!row.input) return h('span', { class: 'text-sm text-gray-500' }, '无输入数据') if (!row.input) return h('span', { class: 'text-sm text-gray-500' }, '无输入数据')
return h('span', { class: 'text-sm' }, row.input.title || row.input.url || '无标题') const title = row.input.title || row.input.url || '无标题'
return h('div', { class: 'text-sm' }, [
h('div', { class: 'font-medium' }, title),
h('div', { class: 'text-xs text-gray-500 mt-1' }, row.input.url || '')
])
} }
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
width: 100, width: 80,
minWidth: 100, minWidth: 80,
render: (row: any) => { render: (row: any) => {
const statusMap: Record<string, { text: string; color: string }> = { const statusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待处理', color: 'warning' }, pending: { text: '待处理', color: 'warning' },
@@ -269,7 +267,7 @@ const taskItemColumns = [
} }
}, },
{ {
title: '输出数据', title: '结果',
key: 'output', key: 'output',
minWidth: 200, minWidth: 200,
ellipsis: { ellipsis: {
@@ -277,16 +275,28 @@ const taskItemColumns = [
}, },
render: (row: any) => { render: (row: any) => {
if (!row.output) return h('span', { class: 'text-sm text-gray-500' }, '无输出') if (!row.output) return h('span', { class: 'text-sm text-gray-500' }, '无输出')
return h('span', { class: 'text-sm' }, row.output.error || row.output.save_url || '处理完成') if (row.output.error) {
return h('div', { class: 'text-sm' }, [
h('div', { class: 'text-red-600 font-medium' }, '失败'),
h('div', { class: 'text-xs text-gray-500 mt-1' }, row.output.error)
])
}
return h('div', { class: 'text-sm' }, [
h('div', { class: 'text-green-600 font-medium' }, '成功'),
h('div', { class: 'text-xs text-gray-500 mt-1' }, row.output.save_url || '处理完成')
])
} }
}, },
{ {
title: '创建时间', title: '时间',
key: 'created_at', key: 'created_at',
width: 160, width: 120,
minWidth: 160, minWidth: 120,
render: (row: any) => { render: (row: any) => {
return new Date(row.created_at).toLocaleString('zh-CN') return h('div', { class: 'text-sm' }, [
h('div', new Date(row.created_at).toLocaleDateString('zh-CN')),
h('div', { class: 'text-xs text-gray-500' }, new Date(row.created_at).toLocaleTimeString('zh-CN'))
])
} }
} }
] ]

View File

@@ -0,0 +1,486 @@
<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="refreshData" type="info">
<template #icon>
<i class="fas fa-refresh"></i>
</template>
刷新
</n-button>
<n-button @click="batchTransfer" type="primary" :disabled="selectedResources.length === 0">
<template #icon>
<i class="fas fa-cloud-upload-alt"></i>
</template>
批量转存 ({{ selectedResources.length }})
</n-button>
</div>
</div>
<!-- 统计信息 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-clock text-orange-500 text-2xl"></i>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">待转存总数</div>
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ total }}</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-check-circle text-green-500 text-2xl"></i>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已选择</div>
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ selectedResources.length }}</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-quark text-blue-500 text-2xl"></i>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">夸克网盘</div>
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ total }}</div>
</div>
</div>
</div>
</div>
<!-- 搜索和筛选 -->
<n-card>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<n-input
v-model:value="searchQuery"
placeholder="搜索资源标题..."
@keyup.enter="handleSearch"
>
<template #prefix>
<i class="fas fa-search"></i>
</template>
</n-input>
<n-select
v-model:value="selectedCategory"
placeholder="选择分类"
:options="categoryOptions"
clearable
/>
<n-select
v-model:value="sortBy"
placeholder="排序方式"
:options="sortOptions"
/>
<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-4">
<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>
</template>
<div v-if="loading" class="flex items-center justify-center py-8">
<n-spin size="large" />
</div>
<div v-else-if="resources.length === 0" class="text-center py-8">
<i class="fas fa-check-circle text-4xl text-green-400 mb-4"></i>
<p class="text-gray-500">暂无未转存的资源</p>
</div>
<div v-else>
<n-data-table
:columns="columns"
:data="resources"
:pagination="paginationConfig"
:bordered="false"
size="small"
:scroll-x="800"
@update:checked-row-keys="handleSelectionChange"
/>
</div>
</n-card>
<!-- 批量转存确认对话框 -->
<n-modal v-model:show="showBatchTransferModal" preset="dialog" title="确认批量转存">
<div class="space-y-4">
<p>确定要将选中的 <strong>{{ selectedResources.length }}</strong> 个资源进行批量转存吗</p>
<div class="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded border border-yellow-200 dark:border-yellow-800">
<div class="flex items-start space-x-2">
<i class="fas fa-exclamation-triangle text-yellow-500 mt-0.5"></i>
<div class="text-sm text-yellow-800 dark:text-yellow-200">
<p> 转存过程可能需要较长时间</p>
<p> 请确保夸克网盘账号有足够的存储空间</p>
<p> 转存完成后可在"已转存列表"中查看结果</p>
</div>
</div>
</div>
</div>
<template #action>
<div class="flex space-x-2">
<n-button @click="showBatchTransferModal = false">取消</n-button>
<n-button type="primary" @click="confirmBatchTransfer" :loading="transferring">
{{ transferring ? '转存中...' : '确认转存' }}
</n-button>
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin'
})
import { ref, computed, onMounted } from 'vue'
import { useResourceApi, useCategoryApi, useTaskApi } from '~/composables/useApi'
// API实例
const resourceApi = useResourceApi()
const categoryApi = useCategoryApi()
const taskApi = useTaskApi()
// 数据状态
const resources = ref([])
const categories = ref([])
const loading = ref(false)
const transferring = ref(false)
const total = ref(0)
const selectedResourceIds = ref([])
// 搜索和筛选
const searchQuery = ref('')
const selectedCategory = ref(null)
const sortBy = ref('created_at')
// 分页
const currentPage = ref(1)
const pageSize = ref(20)
// 模态框
const showBatchTransferModal = ref(false)
// 排序选项
const sortOptions = [
{ label: '创建时间 (最新)', value: 'created_at' },
{ label: '创建时间 (最早)', value: 'created_at_asc' },
{ label: '更新时间 (最新)', value: 'updated_at' },
{ label: '标题 (A-Z)', value: 'title' },
{ label: '标题 (Z-A)', value: 'title_desc' }
]
// 分类选项
const categoryOptions = computed(() => {
return categories.value.map(cat => ({
label: cat.name,
value: cat.id
}))
})
// 分页配置
const paginationConfig = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
itemCount: total.value,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page: number) => {
currentPage.value = page
fetchResources()
},
onUpdatePageSize: (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchResources()
}
}))
// 选择状态
const selectedResources = computed(() => {
return resources.value.filter(r => selectedResourceIds.value.includes(r.id))
})
const isAllSelected = computed(() => {
return resources.value.length > 0 && selectedResourceIds.value.length === resources.value.length
})
const isIndeterminate = computed(() => {
return selectedResourceIds.value.length > 0 && selectedResourceIds.value.length < resources.value.length
})
// 表格列定义
const columns = [
{
type: 'selection',
width: 50
},
{
title: 'ID',
key: 'id',
width: 80,
minWidth: 80
},
{
title: '标题',
key: 'title',
minWidth: 200,
ellipsis: {
tooltip: true
}
},
{
title: '分类',
key: 'category',
width: 120,
render: (row: any) => {
if (!row.category) return '-'
return h('n-tag', { type: 'info', size: 'small' }, { default: () => row.category.name })
}
},
{
title: '原始链接',
key: 'url',
minWidth: 200,
ellipsis: {
tooltip: true
},
render: (row: any) => {
return h('a', {
href: row.url,
target: '_blank',
class: 'text-blue-600 hover:text-blue-800 text-xs break-all'
}, row.url)
}
},
{
title: '创建时间',
key: 'created_at',
width: 160,
render: (row: any) => {
return new Date(row.created_at).toLocaleString('zh-CN')
}
},
{
title: '更新时间',
key: 'updated_at',
width: 160,
render: (row: any) => {
return new Date(row.updated_at).toLocaleString('zh-CN')
}
},
{
title: '操作',
key: 'actions',
width: 120,
fixed: 'right',
render: (row: any) => {
return h('div', { class: 'flex space-x-1' }, [
h('n-button', {
size: 'small',
type: 'primary',
onClick: () => singleTransfer(row)
}, { default: () => '转存' }),
h('n-button', {
size: 'small',
type: 'default',
onClick: () => viewResource(row.id)
}, { default: () => '查看' })
])
}
}
]
// 获取未转存资源
const fetchResources = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
page_size: pageSize.value,
pan_name: 'quark', // 只获取夸克网盘的资源
has_save_url: false, // 只获取没有转存链接的资源
search: searchQuery.value,
category_id: selectedCategory.value,
sort_by: sortBy.value
}
const response = await resourceApi.getResources(params)
resources.value = response.resources || []
total.value = response.total || 0
} catch (error) {
console.error('获取未转存资源失败:', error)
notification.error({
content: '获取未转存资源失败',
duration: 3000
})
} finally {
loading.value = false
}
}
// 获取分类列表
const fetchCategories = async () => {
try {
const response = await categoryApi.getCategories()
categories.value = response || []
} catch (error) {
console.error('获取分类失败:', error)
}
}
// 搜索处理
const handleSearch = () => {
currentPage.value = 1
fetchResources()
}
// 刷新数据
const refreshData = () => {
fetchResources()
}
// 选择处理
const handleSelectionChange = (keys: any[]) => {
selectedResourceIds.value = keys
}
// 全选/取消全选
const toggleSelectAll = (checked: boolean) => {
if (checked) {
selectedResourceIds.value = resources.value.map(r => r.id)
} else {
selectedResourceIds.value = []
}
}
// 单个转存
const singleTransfer = async (resource: any) => {
try {
const taskData = {
title: `转存资源: ${resource.title}`,
description: `转存单个资源: ${resource.title}`,
resources: [{
title: resource.title,
url: resource.url,
category_id: resource.category_id || 0
}]
}
const response = await taskApi.createBatchTransferTask(taskData)
notification.success({
content: '转存任务已创建',
duration: 3000
})
// 跳转到任务详情页
navigateTo(`/admin/tasks/${response.id}`)
} catch (error) {
console.error('创建转存任务失败:', error)
notification.error({
content: '创建转存任务失败',
duration: 3000
})
}
}
// 批量转存
const batchTransfer = () => {
if (selectedResources.value.length === 0) {
notification.warning({
content: '请先选择要转存的资源',
duration: 3000
})
return
}
showBatchTransferModal.value = true
}
// 确认批量转存
const confirmBatchTransfer = async () => {
transferring.value = true
try {
const taskData = {
title: `批量转存 ${selectedResources.value.length} 个资源`,
description: `批量转存 ${selectedResources.value.length} 个夸克网盘资源`,
resources: selectedResources.value.map(r => ({
title: r.title,
url: r.url,
category_id: r.category_id || 0
}))
}
const response = await taskApi.createBatchTransferTask(taskData)
notification.success({
content: `批量转存任务已创建,共 ${selectedResources.value.length} 个资源`,
duration: 3000
})
// 跳转到任务详情页
navigateTo(`/admin/tasks/${response.id}`)
} catch (error) {
console.error('创建批量转存任务失败:', error)
notification.error({
content: '创建批量转存任务失败',
duration: 3000
})
} finally {
transferring.value = false
showBatchTransferModal.value = false
}
}
// 查看资源详情
const viewResource = (id: number) => {
navigateTo(`/admin/resources/${id}`)
}
// 页面加载
onMounted(async () => {
await Promise.all([
fetchCategories(),
fetchResources()
])
})
</script>
<style scoped>
/* 自定义样式 */
</style>

View File

@@ -94,7 +94,7 @@
<div class="flex justify-between mt-3 text-sm text-gray-600 dark:text-gray-300 px-2"> <div class="flex justify-between mt-3 text-sm text-gray-600 dark:text-gray-300 px-2">
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-calendar-day text-pink-600 mr-1"></i> <i class="fas fa-calendar-day text-pink-600 mr-1"></i>
今日更新: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="safeStats?.today_updates || 0">0</span> 今日资源: <span class="font-medium text-pink-600 ml-1 count-up" :data-target="safeStats?.today_resources || 0">0</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-database text-blue-600 mr-1"></i> <i class="fas fa-database text-blue-600 mr-1"></i>
@@ -303,7 +303,7 @@ watch(systemConfigError, (error) => {
// 从 SSR 数据中获取值 // 从 SSR 数据中获取值
const safeResources = computed(() => (resourcesData.value as any)?.data || []) const safeResources = computed(() => (resourcesData.value as any)?.data || [])
const safeStats = computed(() => (statsData.value as any) || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0, today_updates: 0 }) const safeStats = computed(() => (statsData.value as any) || { total_resources: 0, total_categories: 0, total_tags: 0, total_views: 0, today_resources: 0 })
const platforms = computed(() => (platformsData.value as any) || []) const platforms = computed(() => (platformsData.value as any) || [])
const systemConfig = computed(() => (systemConfigData.value as any).data || { site_title: '老九网盘资源数据库' }) const systemConfig = computed(() => (systemConfigData.value as any).data || { site_title: '老九网盘资源数据库' })
const safeLoading = computed(() => pending.value) const safeLoading = computed(() => pending.value)

View File

@@ -7,11 +7,12 @@ export const useSystemConfigStore = defineStore('systemConfig', {
initialized: false initialized: false
}), }),
actions: { actions: {
async initConfig(force = false) { async initConfig(force = false, useAdminApi = false) {
if (this.initialized && !force) return if (this.initialized && !force) return
try { try {
// 使用公开的系统配置API不需要管理员权限 // 根据上下文选择API管理员页面使用管理员API其他页面使用公开API
const response = await useApiFetch('/public/system-config') const apiUrl = useAdminApi ? '/system/config' : '/public/system-config'
const response = await useApiFetch(apiUrl)
console.log('Store API响应:', response) // 调试信息 console.log('Store API响应:', response) // 调试信息
// 正确处理API响应结构 // 正确处理API响应结构