mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
update: 后台界面优化
This commit is contained in:
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// AlipanService 阿里云盘服务
|
||||
@@ -428,7 +430,7 @@ func (a *AlipanService) manageAccessToken() (string, error) {
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
if time.Now().After(tokenInfo.ExpiresAt) {
|
||||
if utils.GetCurrentTime().After(tokenInfo.ExpiresAt) {
|
||||
return a.getNewAccessToken()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// QuarkPanService 夸克网盘服务
|
||||
@@ -406,7 +408,7 @@ func (q *QuarkPanService) getShareSave(shareID, stoken string, fidList, fidToken
|
||||
|
||||
// 生成指定长度的时间戳
|
||||
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)
|
||||
if len(timestampStr) > length {
|
||||
timestampStr = timestampStr[:length]
|
||||
|
||||
@@ -219,8 +219,8 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
|
||||
// 设置时间戳(使用第一个配置的时间)
|
||||
if len(configs) > 0 {
|
||||
response[entity.ConfigResponseFieldCreatedAt] = configs[0].CreatedAt.Format("2006-01-02 15:04:05")
|
||||
response[entity.ConfigResponseFieldUpdatedAt] = configs[0].UpdatedAt.Format("2006-01-02 15:04:05")
|
||||
response[entity.ConfigResponseFieldCreatedAt] = configs[0].CreatedAt.Format(utils.TimeFormatDateTime)
|
||||
response[entity.ConfigResponseFieldUpdatedAt] = configs[0].UpdatedAt.Format(utils.TimeFormatDateTime)
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -212,7 +212,7 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
var resources []entity.Resource
|
||||
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 {
|
||||
@@ -258,6 +258,31 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
if isPublic, ok := value.(bool); ok {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func (r *ResourceViewRepositoryImpl) RecordView(resourceID uint, ipAddress, user
|
||||
|
||||
// GetTodayViews 获取今日访问量
|
||||
func (r *ResourceViewRepositoryImpl) GetTodayViews() (int64, error) {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
today := utils.GetTodayString()
|
||||
var count int64
|
||||
err := r.db.Model(&entity.ResourceView{}).
|
||||
Where("DATE(created_at) = ?", today).
|
||||
@@ -62,8 +62,8 @@ func (r *ResourceViewRepositoryImpl) GetViewsTrend(days int) ([]map[string]inter
|
||||
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")
|
||||
date := utils.GetCurrentTime().AddDate(0, 0, -i)
|
||||
dateStr := date.Format(utils.TimeFormatDate)
|
||||
|
||||
count, err := r.GetViewsByDate(dateStr)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,9 +2,9 @@ package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -37,7 +37,7 @@ func (r *SearchStatRepositoryImpl) RecordSearch(keyword, ip, userAgent string) e
|
||||
stat := entity.SearchStat{
|
||||
Keyword: keyword,
|
||||
Count: 1,
|
||||
Date: time.Now(), // 可保留 date 字段,实际用 created_at 统计
|
||||
Date: utils.GetCurrentTime(), // 可保留 date 字段,实际用 created_at 统计
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
@@ -124,9 +124,9 @@ func (r *SearchStatRepositoryImpl) GetKeywordTrend(keyword string, days int) ([]
|
||||
// GetSummary 获取搜索统计汇总
|
||||
func (r *SearchStatRepositoryImpl) GetSummary() (map[string]int64, error) {
|
||||
var total, today, week, month, keywords int64
|
||||
now := time.Now()
|
||||
todayStr := now.Format("2006-01-02")
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format("2006-01-02") // 周一
|
||||
now := utils.GetCurrentTime()
|
||||
todayStr := now.Format(utils.TimeFormatDate)
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format(utils.TimeFormatDate) // 周一
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
// 总搜索次数
|
||||
|
||||
@@ -44,6 +44,21 @@ func GetResources(c *gin.Context) {
|
||||
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)
|
||||
|
||||
|
||||
@@ -23,12 +23,16 @@ func GetStats(c *gin.Context) {
|
||||
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
|
||||
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
|
||||
todayViews, err := repoManager.ResourceViewRepository.GetTodayViews()
|
||||
@@ -44,8 +48,8 @@ func GetStats(c *gin.Context) {
|
||||
// 添加调试日志
|
||||
utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d",
|
||||
totalResources, totalCategories, totalTags, totalViews)
|
||||
utils.Info("今日数据 - 新增资源: %d, 今日浏览量: %d, 今日搜索: %d",
|
||||
todayResources, todayViews, todaySearches)
|
||||
utils.Info("今日数据 - 新增资源: %d, 今日更新: %d, 今日浏览量: %d, 今日搜索: %d",
|
||||
todayResources, todayUpdates, todayViews, todaySearches)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"total_resources": totalResources,
|
||||
@@ -53,6 +57,7 @@ func GetStats(c *gin.Context) {
|
||||
"total_tags": totalTags,
|
||||
"total_views": totalViews,
|
||||
"today_resources": todayResources,
|
||||
"today_updates": todayUpdates,
|
||||
"today_views": todayViews,
|
||||
"today_searches": todaySearches,
|
||||
})
|
||||
@@ -111,7 +116,7 @@ func GetPerformanceStats(c *gin.Context) {
|
||||
func GetSystemInfo(c *gin.Context) {
|
||||
SuccessResponse(c, gin.H{
|
||||
"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,
|
||||
"environment": gin.H{
|
||||
"gin_mode": gin.Mode(),
|
||||
@@ -146,7 +151,7 @@ func GetSearchesTrend(c *gin.Context) {
|
||||
// 生成最近7天的日期
|
||||
for i := 6; i >= 0; i-- {
|
||||
date := utils.GetCurrentTime().AddDate(0, 0, -i)
|
||||
dateStr := date.Format("2006-01-02")
|
||||
dateStr := date.Format(utils.TimeFormatDate)
|
||||
|
||||
// 查询该日期的搜索量(从搜索统计表)
|
||||
var searches int64
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
@@ -58,8 +57,8 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
|
||||
Type: "transfer",
|
||||
Status: "pending",
|
||||
TotalItems: len(req.Resources),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: utils.GetCurrentTime(),
|
||||
UpdatedAt: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
err := h.repoMgr.TaskRepository.Create(newTask)
|
||||
@@ -85,8 +84,8 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
|
||||
TaskID: newTask.ID,
|
||||
Status: "pending",
|
||||
InputData: string(inputJSON),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: utils.GetCurrentTime(),
|
||||
UpdatedAt: utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
err = h.repoMgr.TaskItemRepository.Create(taskItem)
|
||||
|
||||
@@ -257,7 +257,7 @@ func (a *AutoTransferScheduler) processAutoTransfer() {
|
||||
utils.Error(fmt.Sprintf("转存资源失败 (ID: %d): %v", res.ID, err))
|
||||
} else {
|
||||
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
|
||||
time.Sleep(time.Duration(sleepSec) * time.Second)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func (a *AutoTransferScheduler) getQuarkPanID() (uint, error) {
|
||||
// getResourcesForTransfer 获取需要转存的资源
|
||||
func (a *AutoTransferScheduler) getResourcesForTransfer(quarkPanID uint, limit int) ([]*entity.Resource, error) {
|
||||
// 获取最近24小时内的资源
|
||||
sinceTime := time.Now().Add(-24 * time.Hour)
|
||||
sinceTime := utils.GetCurrentTime().Add(-24 * time.Hour)
|
||||
|
||||
// 使用资源仓库的方法获取需要转存的资源
|
||||
repoImpl, ok := a.resourceRepo.(*repo.ResourceRepositoryImpl)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"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{}{
|
||||
"error": err.Error(),
|
||||
"time": time.Now(),
|
||||
"time": utils.GetCurrentTime(),
|
||||
}
|
||||
outputJSON, _ := json.Marshal(outputData)
|
||||
|
||||
@@ -289,7 +288,7 @@ func (tm *TaskManager) processTaskItem(ctx context.Context, taskID uint, item *e
|
||||
// 处理成功
|
||||
outputData := map[string]interface{}{
|
||||
"success": true,
|
||||
"time": time.Now(),
|
||||
"time": utils.GetCurrentTime(),
|
||||
}
|
||||
outputJSON, _ := json.Marshal(outputData)
|
||||
|
||||
@@ -315,7 +314,7 @@ func (tm *TaskManager) updateTaskProgress(taskID uint, progress float64, process
|
||||
"processed": processed,
|
||||
"success": success,
|
||||
"failed": failed,
|
||||
"time": time.Now(),
|
||||
"time": utils.GetCurrentTime(),
|
||||
}
|
||||
|
||||
progressJSON, _ := json.Marshal(progressData)
|
||||
|
||||
@@ -80,7 +80,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
|
||||
ResourceID: existingResource.ID,
|
||||
SaveURL: existingResource.SaveURL,
|
||||
Success: true,
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Time: utils.GetCurrentTimeString(),
|
||||
}
|
||||
|
||||
outputJSON, _ := json.Marshal(output)
|
||||
@@ -98,7 +98,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
|
||||
output := TransferOutput{
|
||||
Error: err.Error(),
|
||||
Success: false,
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Time: utils.GetCurrentTimeString(),
|
||||
}
|
||||
|
||||
outputJSON, _ := json.Marshal(output)
|
||||
@@ -113,7 +113,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
|
||||
output := TransferOutput{
|
||||
Error: "转存成功但未获取到分享链接",
|
||||
Success: false,
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Time: utils.GetCurrentTimeString(),
|
||||
}
|
||||
|
||||
outputJSON, _ := json.Marshal(output)
|
||||
@@ -128,7 +128,7 @@ func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *ent
|
||||
ResourceID: resourceID,
|
||||
SaveURL: saveURL,
|
||||
Success: true,
|
||||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Time: utils.GetCurrentTimeString(),
|
||||
}
|
||||
|
||||
outputJSON, _ := json.Marshal(output)
|
||||
|
||||
@@ -5,6 +5,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 时间格式常量
|
||||
const (
|
||||
TimeFormatDate = "2006-01-02"
|
||||
TimeFormatDateTime = "2006-01-02 15:04:05"
|
||||
TimeFormatRFC3339 = time.RFC3339
|
||||
)
|
||||
|
||||
// InitTimezone 初始化时区设置
|
||||
func InitTimezone() {
|
||||
// 从环境变量获取时区配置
|
||||
@@ -36,20 +43,35 @@ func GetCurrentTime() time.Time {
|
||||
|
||||
// GetCurrentTimeString 获取当前时间字符串(使用配置的时区)
|
||||
func GetCurrentTimeString() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
return time.Now().Format(TimeFormatDateTime)
|
||||
}
|
||||
|
||||
// GetCurrentTimeRFC3339 获取当前时间RFC3339格式(使用配置的时区)
|
||||
func GetCurrentTimeRFC3339() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
return time.Now().Format(TimeFormatRFC3339)
|
||||
}
|
||||
|
||||
// ParseTime 解析时间字符串(使用配置的时区)
|
||||
func ParseTime(timeStr string) (time.Time, error) {
|
||||
return time.Parse("2006-01-02 15:04:05", timeStr)
|
||||
return time.Parse(TimeFormatDateTime, timeStr)
|
||||
}
|
||||
|
||||
// FormatTime 格式化时间(使用配置的时区)
|
||||
func FormatTime(t time.Time, layout string) string {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func GetFullVersionInfo() string {
|
||||
Node版本: %s
|
||||
平台: %s/%s`,
|
||||
info.Version,
|
||||
FormatTime(info.BuildTime, "2006-01-02 15:04:05"),
|
||||
FormatTime(info.BuildTime, TimeFormatDateTime),
|
||||
info.GitCommit,
|
||||
info.GitBranch,
|
||||
info.GoVersion,
|
||||
|
||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -24,6 +24,7 @@ declare module 'vue' {
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
|
||||
@@ -136,7 +136,7 @@ const systemConfigStore = useSystemConfigStore()
|
||||
const systemConfig = computed(() => systemConfigStore.config)
|
||||
|
||||
onMounted(() => {
|
||||
systemConfigStore.initConfig()
|
||||
systemConfigStore.initConfig(false, true)
|
||||
})
|
||||
|
||||
// 退出登录
|
||||
|
||||
@@ -8,19 +8,16 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
资源信息 <span class="text-red-500">*</span>
|
||||
资源内容 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<n-input
|
||||
v-model:value="resourceText"
|
||||
type="textarea"
|
||||
placeholder="请输入资源信息,每行格式:标题|链接地址 例如: 电影名称1|https://pan.quark.cn/s/xxx 电影名称2|https://pan.baidu.com/s/xxx"
|
||||
:rows="12"
|
||||
placeholder="请输入资源内容,格式:标题和URL为一组..."
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
show-count
|
||||
:maxlength="10000"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
每行一个资源,格式:标题|链接地址(用竖线分隔)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +70,7 @@
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
清空输入
|
||||
清空内容
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -286,7 +283,7 @@ const handleBatchTransfer = async () => {
|
||||
const resourceList = parseResourceText(resourceText.value)
|
||||
|
||||
if (resourceList.length === 0) {
|
||||
message.warning('没有找到有效的资源信息,请按照"标题"和"链接"分行输入')
|
||||
message.warning('没有找到有效的资源信息,请按照格式要求输入:标题和URL为一组,标题必填')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,24 +330,51 @@ const handleBatchTransfer = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 解析资源文本,按照 标题\n链接 的格式
|
||||
// 解析资源文本,按照 标题\n链接 的格式(支持同一标题多个URL)
|
||||
const parseResourceText = (text: string) => {
|
||||
const lines = text.split('\n').filter((line: string) => line.trim())
|
||||
const resourceList = []
|
||||
|
||||
for (let i = 0; i < lines.length; i += 2) {
|
||||
const title = lines[i]?.trim()
|
||||
const url = lines[i + 1]?.trim()
|
||||
let currentTitle = ''
|
||||
let currentUrls = []
|
||||
|
||||
if (title && url && isValidUrl(url)) {
|
||||
for (const line of lines) {
|
||||
// 判断是否为 url(以 http/https 开头)
|
||||
if (/^https?:\/\//i.test(line)) {
|
||||
currentUrls.push(line.trim())
|
||||
} else {
|
||||
// 新标题,先保存上一个
|
||||
if (currentTitle && currentUrls.length > 0) {
|
||||
// 为每个URL创建一个资源项
|
||||
for (const url of currentUrls) {
|
||||
if (isValidUrl(url)) {
|
||||
resourceList.push({
|
||||
title,
|
||||
url,
|
||||
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 || []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resourceList
|
||||
}
|
||||
|
||||
@@ -92,14 +92,14 @@ const autoTransferEnabled = ref(false)
|
||||
// 获取系统配置状态
|
||||
const fetchSystemStatus = async () => {
|
||||
try {
|
||||
await systemConfigStore.initConfig()
|
||||
await systemConfigStore.initConfig(false, true)
|
||||
|
||||
// 从系统配置中获取自动处理和自动转存状态
|
||||
const config = systemConfigStore.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
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 调试信息 -->
|
||||
<div class="text-sm text-gray-500 mb-2">
|
||||
数据数量: {{ resources.length }}, 总数: {{ total }}, 加载状态: {{ loading }}
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
@@ -51,10 +56,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { useResourceApi } from '~/composables/useApi'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
// 消息提示
|
||||
const $message = useMessage()
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const resources = ref([])
|
||||
const resources = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
@@ -79,12 +88,12 @@ const pagination = reactive({
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
const columns: any[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
fixed: 'left'
|
||||
fixed: 'left' as const
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
@@ -101,12 +110,15 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform_name',
|
||||
key: 'pan_name',
|
||||
width: 80,
|
||||
render: (row: any) => {
|
||||
if (row.pan_id) {
|
||||
const platform = platformOptions.value.find((p: any) => p.value === row.pan_id)
|
||||
return platform?.label || '未知'
|
||||
}
|
||||
return '未知'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '转存链接',
|
||||
@@ -137,31 +149,39 @@ const columns = [
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
render: (row: any) => {
|
||||
return [
|
||||
return h('div', { class: 'flex space-x-2' }, [
|
||||
h('n-button', {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => viewResource(row)
|
||||
}, '查看'),
|
||||
}, { default: () => '查看' }),
|
||||
h('n-button', {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
style: { marginLeft: '8px' },
|
||||
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 () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const params: any = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
has_save_url: true // 筛选有转存链接的资源
|
||||
@@ -174,17 +194,36 @@ const fetchTransferredResources = async () => {
|
||||
params.category_id = selectedCategory.value
|
||||
}
|
||||
|
||||
console.log('请求参数:', params)
|
||||
const result = await resourceApi.getResources(params) as any
|
||||
console.log('已转存资源结果:', result)
|
||||
console.log('结果类型:', typeof result)
|
||||
console.log('结果结构:', Object.keys(result || {}))
|
||||
|
||||
if (result && result.resources) {
|
||||
resources.value = result.resources
|
||||
if (result && result.data) {
|
||||
console.log('使用 resources 格式,数量:', result.data.length)
|
||||
resources.value = result.data
|
||||
total.value = result.total || 0
|
||||
pagination.itemCount = result.total || 0
|
||||
} else if (Array.isArray(result)) {
|
||||
console.log('使用数组格式,数量:', result.length)
|
||||
resources.value = result
|
||||
total.value = 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) {
|
||||
console.error('获取已转存资源失败:', error)
|
||||
|
||||
@@ -327,8 +327,8 @@ const fetchUntransferredResources = async () => {
|
||||
const result = await resourceApi.getResources(params) as any
|
||||
console.log('未转存资源结果:', result)
|
||||
|
||||
if (result && result.resources) {
|
||||
resources.value = result.resources
|
||||
if (result && result.data) {
|
||||
resources.value = result.data
|
||||
total.value = result.total || 0
|
||||
} else if (Array.isArray(result)) {
|
||||
resources.value = result
|
||||
|
||||
@@ -26,6 +26,15 @@ export const parseApiResponse = <T>(response: any): T => {
|
||||
// 检查是否是包含success字段的响应格式(如登录接口)
|
||||
if (response && typeof response === 'object' && 'success' in response && 'data' in response) {
|
||||
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格式
|
||||
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
return {
|
||||
|
||||
82
web/composables/useTimeFormat.ts
Normal file
82
web/composables/useTimeFormat.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -304,8 +304,8 @@ const systemConfigStore = useSystemConfigStore()
|
||||
// 任务状态管理
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
// 初始化系统配置
|
||||
await systemConfigStore.initConfig()
|
||||
// 初始化系统配置(管理员页面使用管理员API)
|
||||
await systemConfigStore.initConfig(false, true)
|
||||
|
||||
// 版本信息
|
||||
const versionInfo = ref({
|
||||
@@ -495,10 +495,10 @@ const operationItems = ref([
|
||||
active: (route: any) => route.path.startsWith('/admin/data-push')
|
||||
},
|
||||
{
|
||||
to: '/admin/auto-reply',
|
||||
label: '自动回复',
|
||||
icon: 'fas fa-comments',
|
||||
active: (route: any) => route.path.startsWith('/admin/auto-reply')
|
||||
to: '/admin/bot',
|
||||
label: '机器人',
|
||||
icon: 'fas fa-robot',
|
||||
active: (route: any) => route.path.startsWith('/admin/bot')
|
||||
},
|
||||
{
|
||||
to: '/admin/seo',
|
||||
@@ -533,7 +533,7 @@ const autoExpandCurrentGroup = () => {
|
||||
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')) {
|
||||
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
|
||||
} else if (currentPath.startsWith('/admin/search-stats') || currentPath.startsWith('/admin/third-party-stats')) {
|
||||
expandedGroups.value.statistics = true
|
||||
@@ -555,7 +555,7 @@ watch(() => useRoute().path, (newPath) => {
|
||||
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')) {
|
||||
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
|
||||
} else if (newPath.startsWith('/admin/search-stats') || newPath.startsWith('/admin/third-party-stats')) {
|
||||
expandedGroups.value.statistics = true
|
||||
|
||||
@@ -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>
|
||||
@@ -1,26 +1,255 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<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 mt-2">机器人配置与管理</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理各平台的机器人配置和自动回复功能</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<!-- 配置表单 -->
|
||||
<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">
|
||||
<!-- 步骤1:Astrobot 安装指南 -->
|
||||
<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">
|
||||
<div class="text-gray-400 dark:text-gray-500 mb-4">
|
||||
<i class="fas fa-robot text-4xl"></i>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 机器人管理页面
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: ['auth']
|
||||
ssr: false
|
||||
})
|
||||
|
||||
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>
|
||||
@@ -127,6 +127,11 @@ const saveConfig = async () => {
|
||||
content: '开发配置保存成功',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新系统配置状态,确保顶部导航同步更新
|
||||
const { useSystemConfigStore } = await import('~/stores/systemConfig')
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
await systemConfigStore.initConfig(true, true) // 强制刷新,使用管理员API
|
||||
} catch (error) {
|
||||
console.error('保存开发配置失败:', error)
|
||||
notification.error({
|
||||
|
||||
@@ -229,6 +229,11 @@ const saveConfig = async () => {
|
||||
content: '功能配置保存成功',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新系统配置状态,确保顶部导航同步更新
|
||||
const { useSystemConfigStore } = await import('~/stores/systemConfig')
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
await systemConfigStore.initConfig(true, true) // 强制刷新,使用管理员API
|
||||
} catch (error) {
|
||||
console.error('保存功能配置失败:', error)
|
||||
notification.error({
|
||||
|
||||
@@ -414,54 +414,54 @@ const linkColumns = [
|
||||
// 提交到百度
|
||||
const submitToBaidu = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.baidu = new Date().toLocaleString()
|
||||
lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到百度')
|
||||
}
|
||||
|
||||
// 提交到谷歌
|
||||
const submitToGoogle = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.google = new Date().toLocaleString()
|
||||
lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到谷歌')
|
||||
}
|
||||
|
||||
// 提交到必应
|
||||
const submitToBing = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.bing = new Date().toLocaleString()
|
||||
lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到必应')
|
||||
}
|
||||
|
||||
// 提交到搜狗
|
||||
const submitToSogou = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.sogou = new Date().toLocaleString()
|
||||
lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到搜狗')
|
||||
}
|
||||
|
||||
// 提交到神马搜索
|
||||
const submitToShenma = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.shenma = new Date().toLocaleString()
|
||||
lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到神马搜索')
|
||||
}
|
||||
|
||||
// 提交到360搜索
|
||||
const submitTo360 = () => {
|
||||
// 模拟提交
|
||||
lastSubmitTime.value.so360 = new Date().toLocaleString()
|
||||
lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
|
||||
message.success('已提交到360搜索')
|
||||
}
|
||||
|
||||
// 批量提交
|
||||
const submitToAll = () => {
|
||||
// 模拟批量提交
|
||||
lastSubmitTime.value.baidu = new Date().toLocaleString()
|
||||
lastSubmitTime.value.google = new Date().toLocaleString()
|
||||
lastSubmitTime.value.bing = new Date().toLocaleString()
|
||||
lastSubmitTime.value.sogou = new Date().toLocaleString()
|
||||
lastSubmitTime.value.shenma = new Date().toLocaleString()
|
||||
lastSubmitTime.value.so360 = new Date().toLocaleString()
|
||||
lastSubmitTime.value.baidu = new Date().toLocaleString('zh-CN')
|
||||
lastSubmitTime.value.google = new Date().toLocaleString('zh-CN')
|
||||
lastSubmitTime.value.bing = new Date().toLocaleString('zh-CN')
|
||||
lastSubmitTime.value.sogou = new Date().toLocaleString('zh-CN')
|
||||
lastSubmitTime.value.shenma = new Date().toLocaleString('zh-CN')
|
||||
lastSubmitTime.value.so360 = new Date().toLocaleString('zh-CN')
|
||||
message.success('已批量提交到所有搜索引擎')
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,11 @@ const saveConfig = async () => {
|
||||
content: '站点配置保存成功',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新系统配置状态,确保顶部导航同步更新
|
||||
const { useSystemConfigStore } = await import('~/stores/systemConfig')
|
||||
const systemConfigStore = useSystemConfigStore()
|
||||
await systemConfigStore.initConfig(true, true) // 强制刷新,使用管理员API
|
||||
} catch (error) {
|
||||
console.error('保存站点配置失败:', error)
|
||||
notification.error({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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 items-center space-x-4">
|
||||
<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-3">
|
||||
<n-button
|
||||
quaternary
|
||||
size="small"
|
||||
@@ -11,12 +11,11 @@
|
||||
<template #icon>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</template>
|
||||
<span class="hidden sm:inline">返回任务列表</span>
|
||||
<span class="sm:hidden">返回</span>
|
||||
<span class="hidden sm:inline">返回</span>
|
||||
</n-button>
|
||||
<div>
|
||||
<h1 class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">任务详情</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">查看任务的详细信息和执行状态</p>
|
||||
<h1 class="text-lg md:text-xl font-bold text-gray-900 dark:text-white">任务详情</h1>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">查看任务的详细信息和执行状态</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,102 +74,96 @@
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<n-spin size="large" />
|
||||
<div v-if="loading" class="flex justify-center py-4">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div v-else-if="task" class="space-y-6">
|
||||
<!-- 基本信息卡片 -->
|
||||
<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-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务ID</label>
|
||||
<p class="text-sm text-gray-900 dark:text-white mt-1">{{ task.id }}</p>
|
||||
</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 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">
|
||||
<div v-else-if="task" class="space-y-4">
|
||||
<!-- 整合的任务信息卡片 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-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-1">
|
||||
<div class="flex items-center space-x-4 mb-3">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ task.title }}</h2>
|
||||
<n-tag :type="getTaskStatusColor(task.status)" size="small">
|
||||
{{ getTaskStatusText(task.status) }}
|
||||
</n-tag>
|
||||
<n-tag :type="getTaskTypeColor(task.task_type)" size="small">
|
||||
{{ getTaskTypeText(task.task_type) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">任务状态</label>
|
||||
<div class="mt-1">
|
||||
<n-tag :type="getTaskStatusColor(task.status)" size="small">
|
||||
{{ getTaskStatusText(task.status) }}
|
||||
</n-tag>
|
||||
</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>
|
||||
<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.updated_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="task.description" class="mt-4">
|
||||
<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 break-words">{{ 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 class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<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>
|
||||
<span class="text-gray-500 dark:text-gray-400">ID:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-white">{{ task.id }}</span>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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-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">
|
||||
<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="8"
|
||||
:height="6"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务项列表 -->
|
||||
<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">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">任务项列表</h2>
|
||||
<!-- 右侧:统计信息 -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="text-center">
|
||||
<div class="text-lg font-bold text-green-600 dark:text-green-400">{{ task.success_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-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 class="p-4 md:p-6 overflow-x-auto">
|
||||
<!-- 任务描述(如果有) -->
|
||||
<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-600 dark:text-gray-400">{{ task.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务项列表 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
|
||||
<div class="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<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 class="overflow-x-auto">
|
||||
<n-data-table
|
||||
:columns="taskItemColumns"
|
||||
:data="taskItems"
|
||||
@@ -178,13 +171,14 @@
|
||||
:pagination="itemsPaginationConfig"
|
||||
size="small"
|
||||
:scroll-x="600"
|
||||
:bordered="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else class="text-center py-8">
|
||||
<div v-else class="text-center py-4">
|
||||
<n-empty description="任务不存在或已被删除" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,26 +231,30 @@ const taskItemColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
minWidth: 80
|
||||
width: 60,
|
||||
minWidth: 60
|
||||
},
|
||||
{
|
||||
title: '输入数据',
|
||||
key: 'input',
|
||||
minWidth: 200,
|
||||
minWidth: 250,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render: (row: any) => {
|
||||
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: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
minWidth: 100,
|
||||
width: 80,
|
||||
minWidth: 80,
|
||||
render: (row: any) => {
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
pending: { text: '待处理', color: 'warning' },
|
||||
@@ -269,7 +267,7 @@ const taskItemColumns = [
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '输出数据',
|
||||
title: '结果',
|
||||
key: 'output',
|
||||
minWidth: 200,
|
||||
ellipsis: {
|
||||
@@ -277,16 +275,28 @@ const taskItemColumns = [
|
||||
},
|
||||
render: (row: any) => {
|
||||
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',
|
||||
width: 160,
|
||||
minWidth: 160,
|
||||
width: 120,
|
||||
minWidth: 120,
|
||||
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'))
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
486
web/pages/admin/untransferred-resources.vue
Normal file
486
web/pages/admin/untransferred-resources.vue
Normal 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>
|
||||
@@ -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 items-center">
|
||||
<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 class="flex items-center">
|
||||
<i class="fas fa-database text-blue-600 mr-1"></i>
|
||||
@@ -303,7 +303,7 @@ watch(systemConfigError, (error) => {
|
||||
|
||||
// 从 SSR 数据中获取值
|
||||
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 systemConfig = computed(() => (systemConfigData.value as any).data || { site_title: '老九网盘资源数据库' })
|
||||
const safeLoading = computed(() => pending.value)
|
||||
|
||||
@@ -7,11 +7,12 @@ export const useSystemConfigStore = defineStore('systemConfig', {
|
||||
initialized: false
|
||||
}),
|
||||
actions: {
|
||||
async initConfig(force = false) {
|
||||
async initConfig(force = false, useAdminApi = false) {
|
||||
if (this.initialized && !force) return
|
||||
try {
|
||||
// 使用公开的系统配置API,不需要管理员权限
|
||||
const response = await useApiFetch('/public/system-config')
|
||||
// 根据上下文选择API:管理员页面使用管理员API,其他页面使用公开API
|
||||
const apiUrl = useAdminApi ? '/system/config' : '/public/system-config'
|
||||
const response = await useApiFetch(apiUrl)
|
||||
console.log('Store API响应:', response) // 调试信息
|
||||
|
||||
// 正确处理API响应结构
|
||||
|
||||
Reference in New Issue
Block a user