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"
"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()
}

View File

@@ -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]

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -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 {

View File

@@ -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"
// 总搜索次数

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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
View File

@@ -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']

View File

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

View File

@@ -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="请输入资源信息,每行格式:标题|链接地址&#10;例如:&#10;电影名称1|https://pan.quark.cn/s/xxx&#10;电影名称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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

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()
// 初始化系统配置
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

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>
<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">
<!-- 步骤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">
<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>

View File

@@ -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({

View File

@@ -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({

View File

@@ -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('已批量提交到所有搜索引擎')
}

View File

@@ -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({

View File

@@ -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'))
])
}
}
]

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 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)

View File

@@ -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响应结构