mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
update: ui更新
This commit is contained in:
@@ -17,7 +17,9 @@ type TagRepository interface {
|
||||
GetResourceCount(tagID uint) (int64, error)
|
||||
FindByResourceID(resourceID uint) ([]entity.Tag, error)
|
||||
FindWithPagination(page, pageSize int) ([]entity.Tag, int64, error)
|
||||
FindWithPaginationOrderByResourceCount(page, pageSize int) ([]entity.Tag, int64, error)
|
||||
Search(query string, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
SearchOrderByResourceCount(query string, page, pageSize int) ([]entity.Tag, int64, error)
|
||||
UpdateWithNulls(tag *entity.Tag) error
|
||||
GetByID(id uint) (*entity.Tag, error)
|
||||
RestoreDeletedTag(id uint) error
|
||||
@@ -172,3 +174,71 @@ func (r *TagRepositoryImpl) GetByID(id uint) (*entity.Tag, error) {
|
||||
func (r *TagRepositoryImpl) RestoreDeletedTag(id uint) error {
|
||||
return r.db.Unscoped().Model(&entity.Tag{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// FindWithPaginationOrderByResourceCount 按资源数量排序的分页查询
|
||||
func (r *TagRepositoryImpl) FindWithPaginationOrderByResourceCount(page, pageSize int) ([]entity.Tag, int64, error) {
|
||||
var tags []entity.Tag
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
err := r.db.Model(&entity.Tag{}).Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 使用子查询统计每个标签的资源数量并排序
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.Preload("Category").
|
||||
Select("tags.*, COALESCE(resource_counts.count, 0) as resource_count").
|
||||
Joins(`LEFT JOIN (
|
||||
SELECT rt.tag_id, COUNT(rt.resource_id) as count
|
||||
FROM resource_tags rt
|
||||
INNER JOIN resources r ON rt.resource_id = r.id AND r.deleted_at IS NULL
|
||||
GROUP BY rt.tag_id
|
||||
) as resource_counts ON tags.id = resource_counts.tag_id`).
|
||||
Order("COALESCE(resource_counts.count, 0) DESC, tags.created_at DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&tags).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return tags, total, nil
|
||||
}
|
||||
|
||||
// SearchOrderByResourceCount 按资源数量排序的搜索
|
||||
func (r *TagRepositoryImpl) SearchOrderByResourceCount(query string, page, pageSize int) ([]entity.Tag, int64, error) {
|
||||
var tags []entity.Tag
|
||||
var total int64
|
||||
|
||||
// 构建搜索条件
|
||||
searchQuery := "%" + query + "%"
|
||||
|
||||
// 获取总数
|
||||
err := r.db.Model(&entity.Tag{}).Where("name ILIKE ? OR description ILIKE ?", searchQuery, searchQuery).Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 使用子查询统计每个标签的资源数量并排序
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.Preload("Category").
|
||||
Select("tags.*, COALESCE(resource_counts.count, 0) as resource_count").
|
||||
Joins(`LEFT JOIN (
|
||||
SELECT rt.tag_id, COUNT(rt.resource_id) as count
|
||||
FROM resource_tags rt
|
||||
INNER JOIN resources r ON rt.resource_id = r.id AND r.deleted_at IS NULL
|
||||
GROUP BY rt.tag_id
|
||||
) as resource_counts ON tags.id = resource_counts.tag_id`).
|
||||
Where("tags.name ILIKE ? OR tags.description ILIKE ?", searchQuery, searchQuery).
|
||||
Order("COALESCE(resource_counts.count, 0) DESC, tags.created_at DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&tags).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return tags, total, nil
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ func GetTags(c *gin.Context) {
|
||||
var err error
|
||||
|
||||
if search != "" {
|
||||
// 搜索标签
|
||||
tags, total, err = repoManager.TagRepository.Search(search, page, pageSize)
|
||||
// 搜索标签(按资源数量排序)
|
||||
tags, total, err = repoManager.TagRepository.SearchOrderByResourceCount(search, page, pageSize)
|
||||
} else {
|
||||
// 分页查询
|
||||
tags, total, err = repoManager.TagRepository.FindWithPagination(page, pageSize)
|
||||
// 分页查询(按资源数量排序)
|
||||
tags, total, err = repoManager.TagRepository.FindWithPaginationOrderByResourceCount(page, pageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
|
||||
410
web/components/Admin/ManualBatchTransfer.vue
Normal file
410
web/components/Admin/ManualBatchTransfer.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 说明信息 -->
|
||||
<n-alert type="info" show-icon>
|
||||
<template #icon>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</template>
|
||||
批量转存功能:支持批量输入资源URL进行转存操作。每行一个链接,系统将自动处理转存任务。
|
||||
</n-alert>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<n-card title="批量转存配置">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 左侧:URL输入 -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
资源链接 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<n-input
|
||||
v-model:value="resourceText"
|
||||
type="textarea"
|
||||
placeholder="请输入资源链接,每行一个..."
|
||||
:rows="12"
|
||||
show-count
|
||||
:maxlength="10000"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
支持的格式:夸克网盘、百度网盘等分享链接
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ totalLines }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">总行数</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-green-600">{{ validUrls }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">有效链接</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<div class="text-2xl font-bold text-red-600">{{ invalidUrls }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">无效链接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:配置选项 -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
默认分类
|
||||
</label>
|
||||
<n-select
|
||||
v-model:value="selectedCategory"
|
||||
placeholder="选择分类"
|
||||
:options="categoryOptions"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
标签
|
||||
</label>
|
||||
<n-select
|
||||
v-model:value="selectedTags"
|
||||
placeholder="选择标签"
|
||||
:options="tagOptions"
|
||||
multiple
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
转存平台
|
||||
</label>
|
||||
<n-select
|
||||
v-model:value="selectedPlatform"
|
||||
placeholder="选择转存平台"
|
||||
:options="platformOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
处理选项
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<n-checkbox v-model:checked="autoValidate">
|
||||
自动验证链接有效性
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="skipExisting">
|
||||
跳过已存在的资源
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="autoTransfer">
|
||||
添加后立即开始转存
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="space-y-3 pt-4">
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
:loading="processing"
|
||||
:disabled="!resourceText.trim() || processing"
|
||||
@click="handleBatchTransfer"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-upload"></i>
|
||||
</template>
|
||||
开始批量转存 ({{ validUrls }} 个)
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
block
|
||||
@click="clearInput"
|
||||
:disabled="processing"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
清空输入
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<n-card v-if="results.length > 0" title="转存结果">
|
||||
<div class="space-y-4">
|
||||
<!-- 结果统计 -->
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-blue-600">{{ results.length }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">总处理数</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-green-600">{{ successCount }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">成功</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-red-600">{{ failedCount }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">失败</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-yellow-600">{{ processingCount }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">处理中</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果列表 -->
|
||||
<n-data-table
|
||||
:columns="resultColumns"
|
||||
:data="results"
|
||||
:pagination="false"
|
||||
max-height="300"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import { useCategoryApi, useTagApi, usePanApi } from '~/composables/useApi'
|
||||
|
||||
// 数据状态
|
||||
const resourceText = ref('')
|
||||
const processing = ref(false)
|
||||
const results = ref([])
|
||||
|
||||
// 配置选项
|
||||
const selectedCategory = ref(null)
|
||||
const selectedTags = ref([])
|
||||
const selectedPlatform = ref(null)
|
||||
const autoValidate = ref(true)
|
||||
const skipExisting = ref(true)
|
||||
const autoTransfer = ref(false)
|
||||
|
||||
// 选项数据
|
||||
const categoryOptions = ref([])
|
||||
const tagOptions = ref([])
|
||||
const platformOptions = ref([])
|
||||
|
||||
// API实例
|
||||
const categoryApi = useCategoryApi()
|
||||
const tagApi = useTagApi()
|
||||
const panApi = usePanApi()
|
||||
|
||||
// 计算属性
|
||||
const totalLines = computed(() => {
|
||||
return resourceText.value ? resourceText.value.split('\n').filter(line => line.trim()).length : 0
|
||||
})
|
||||
|
||||
const validUrls = computed(() => {
|
||||
if (!resourceText.value) return 0
|
||||
const lines = resourceText.value.split('\n').filter(line => line.trim())
|
||||
return lines.filter(line => isValidUrl(line.trim())).length
|
||||
})
|
||||
|
||||
const invalidUrls = computed(() => {
|
||||
return totalLines.value - validUrls.value
|
||||
})
|
||||
|
||||
const successCount = computed(() => {
|
||||
return results.value.filter((r: any) => r.status === 'success').length
|
||||
})
|
||||
|
||||
const failedCount = computed(() => {
|
||||
return results.value.filter((r: any) => r.status === 'failed').length
|
||||
})
|
||||
|
||||
const processingCount = computed(() => {
|
||||
return results.value.filter((r: any) => r.status === 'processing').length
|
||||
})
|
||||
|
||||
// 结果表格列
|
||||
const resultColumns = [
|
||||
{
|
||||
title: '链接',
|
||||
key: 'url',
|
||||
width: 300,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (row: any) => {
|
||||
const statusMap = {
|
||||
success: { color: 'success', text: '成功', icon: 'fas fa-check' },
|
||||
failed: { color: 'error', text: '失败', icon: 'fas fa-times' },
|
||||
processing: { color: 'info', text: '处理中', icon: 'fas fa-spinner fa-spin' }
|
||||
}
|
||||
const status = statusMap[row.status] || statusMap.failed
|
||||
return h('n-tag', { type: status.color }, {
|
||||
icon: () => h('i', { class: status.icon }),
|
||||
default: () => status.text
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '消息',
|
||||
key: 'message',
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '转存链接',
|
||||
key: 'saveUrl',
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render: (row: any) => {
|
||||
if (row.saveUrl) {
|
||||
return h('a', {
|
||||
href: row.saveUrl,
|
||||
target: '_blank',
|
||||
class: 'text-blue-500 hover:text-blue-700'
|
||||
}, '查看')
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// URL验证
|
||||
const isValidUrl = (url: string) => {
|
||||
try {
|
||||
new URL(url)
|
||||
// 简单检查是否包含常见网盘域名
|
||||
const diskDomains = ['quark.cn', 'pan.baidu.com', 'aliyundrive.com']
|
||||
return diskDomains.some(domain => url.includes(domain))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类选项
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const result = await categoryApi.getCategories() as any
|
||||
if (result && result.items) {
|
||||
categoryOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签选项
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const result = await tagApi.getTags() as any
|
||||
if (result && result.items) {
|
||||
tagOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取平台选项
|
||||
const fetchPlatforms = async () => {
|
||||
try {
|
||||
const result = await panApi.getPans() as any
|
||||
if (result && Array.isArray(result)) {
|
||||
platformOptions.value = result.map((item: any) => ({
|
||||
label: item.remark || item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取平台失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理批量转存
|
||||
const handleBatchTransfer = async () => {
|
||||
if (!resourceText.value.trim()) {
|
||||
$message.warning('请输入资源链接')
|
||||
return
|
||||
}
|
||||
|
||||
processing.value = true
|
||||
results.value = []
|
||||
|
||||
try {
|
||||
const lines = resourceText.value.split('\n').filter(line => line.trim())
|
||||
const validLines = lines.filter(line => isValidUrl(line.trim()))
|
||||
|
||||
if (validLines.length === 0) {
|
||||
$message.warning('没有找到有效的资源链接')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化结果
|
||||
results.value = validLines.map(url => ({
|
||||
url: url.trim(),
|
||||
status: 'processing',
|
||||
message: '准备处理...',
|
||||
saveUrl: null
|
||||
}))
|
||||
|
||||
// 这里应该调用实际的批量转存API
|
||||
// 由于只是UI展示,这里模拟处理过程
|
||||
for (let i = 0; i < results.value.length; i++) {
|
||||
const result = results.value[i]
|
||||
|
||||
// 模拟处理延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟随机成功/失败
|
||||
const isSuccess = Math.random() > 0.3
|
||||
|
||||
if (isSuccess) {
|
||||
result.status = 'success'
|
||||
result.message = '转存成功'
|
||||
result.saveUrl = `https://pan.quark.cn/s/mock${Date.now()}`
|
||||
} else {
|
||||
result.status = 'failed'
|
||||
result.message = '转存失败:网络错误'
|
||||
}
|
||||
|
||||
// 触发响应式更新
|
||||
results.value = [...results.value]
|
||||
}
|
||||
|
||||
$message.success(`批量转存完成,成功 ${successCount.value} 个,失败 ${failedCount.value} 个`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量转存失败:', error)
|
||||
$message.error('批量转存失败')
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
resourceText.value = ''
|
||||
results.value = []
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
fetchTags()
|
||||
fetchPlatforms()
|
||||
})
|
||||
</script>
|
||||
285
web/components/Admin/TransferredList.vue
Normal file
285
web/components/Admin/TransferredList.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索已转存资源..."
|
||||
@keyup.enter="handleSearch"
|
||||
clearable
|
||||
>
|
||||
<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="selectedTag"
|
||||
placeholder="选择标签"
|
||||
:options="tagOptions"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<i class="fas fa-search"></i>
|
||||
</template>
|
||||
搜索
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="resources"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:remote="true"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
:row-key="(row: any) => row.id"
|
||||
max-height="500"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { useResourceApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const resources = ref([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// 搜索条件
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref(null)
|
||||
const selectedTag = ref(null)
|
||||
|
||||
// 选项数据
|
||||
const categoryOptions = ref([])
|
||||
const tagOptions = ref([])
|
||||
|
||||
// API实例
|
||||
const resourceApi = useResourceApi()
|
||||
const categoryApi = useCategoryApi()
|
||||
const tagApi = useTagApi()
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
prefix: ({ itemCount }: any) => `共 ${itemCount} 条`
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
key: 'category_name',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
key: 'platform_name',
|
||||
width: 80,
|
||||
render: (row: any) => {
|
||||
const platform = platformOptions.value.find((p: any) => p.value === row.pan_id)
|
||||
return platform?.label || '未知'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '转存链接',
|
||||
key: 'save_url',
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render: (row: any) => {
|
||||
return h('a', {
|
||||
href: row.save_url,
|
||||
target: '_blank',
|
||||
class: 'text-green-500 hover:text-green-700'
|
||||
}, row.save_url.length > 30 ? row.save_url.substring(0, 30) + '...' : row.save_url)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '转存时间',
|
||||
key: 'updated_at',
|
||||
width: 130,
|
||||
render: (row: any) => {
|
||||
return new Date(row.updated_at).toLocaleDateString()
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
render: (row: any) => {
|
||||
return [
|
||||
h('n-button', {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => viewResource(row)
|
||||
}, '查看'),
|
||||
h('n-button', {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
style: { marginLeft: '8px' },
|
||||
onClick: () => copyLink(row.save_url)
|
||||
}, '复制')
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 平台选项
|
||||
const platformOptions = ref([])
|
||||
|
||||
// 获取已转存资源
|
||||
const fetchTransferredResources = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
has_save_url: true // 筛选有转存链接的资源
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
params.search = searchQuery.value
|
||||
}
|
||||
if (selectedCategory.value) {
|
||||
params.category_id = selectedCategory.value
|
||||
}
|
||||
|
||||
const result = await resourceApi.getResources(params) as any
|
||||
console.log('已转存资源结果:', result)
|
||||
|
||||
if (result && result.resources) {
|
||||
resources.value = result.resources
|
||||
total.value = result.total || 0
|
||||
pagination.itemCount = result.total || 0
|
||||
} else if (Array.isArray(result)) {
|
||||
resources.value = result
|
||||
total.value = result.length
|
||||
pagination.itemCount = result.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取已转存资源失败:', error)
|
||||
resources.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类选项
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const result = await categoryApi.getCategories() as any
|
||||
console.log('分类结果:', result)
|
||||
|
||||
if (result && result.items) {
|
||||
categoryOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签选项
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const result = await tagApi.getTags() as any
|
||||
console.log('标签结果:', result)
|
||||
|
||||
if (result && result.items) {
|
||||
tagOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
pagination.page = 1
|
||||
fetchTransferredResources()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
pagination.page = page
|
||||
fetchTransferredResources()
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
pageSize.value = size
|
||||
pagination.pageSize = size
|
||||
currentPage.value = 1
|
||||
pagination.page = 1
|
||||
fetchTransferredResources()
|
||||
}
|
||||
|
||||
// 查看资源
|
||||
const viewResource = (resource: any) => {
|
||||
// 这里可以打开资源详情模态框
|
||||
console.log('查看资源:', resource)
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
const copyLink = async (url: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
$message.success('链接已复制到剪贴板')
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
$message.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
fetchTags()
|
||||
fetchTransferredResources()
|
||||
})
|
||||
</script>
|
||||
541
web/components/Admin/UntransferredList.vue
Normal file
541
web/components/Admin/UntransferredList.vue
Normal file
@@ -0,0 +1,541 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索未转存资源..."
|
||||
@keyup.enter="handleSearch"
|
||||
clearable
|
||||
>
|
||||
<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="selectedTag"
|
||||
placeholder="选择标签"
|
||||
:options="tagOptions"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-model:value="selectedStatus"
|
||||
placeholder="资源状态"
|
||||
:options="statusOptions"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<i class="fas fa-search"></i>
|
||||
</template>
|
||||
搜索
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<n-card>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<n-checkbox
|
||||
:checked="isAllSelected"
|
||||
@update:checked="toggleSelectAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">全选</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">
|
||||
共 {{ total }} 个资源,已选择 {{ selectedResources.length }} 个
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="selectedResources.length === 0"
|
||||
:loading="batchTransferring"
|
||||
@click="handleBatchTransfer"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</template>
|
||||
批量转存 ({{ selectedResources.length }})
|
||||
</n-button>
|
||||
|
||||
<n-button @click="refreshData">
|
||||
<template #icon>
|
||||
<i class="fas fa-refresh"></i>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<n-card>
|
||||
<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-inbox text-4xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-500">暂无未转存的夸克资源</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 虚拟列表 -->
|
||||
<n-virtual-list
|
||||
:items="resources"
|
||||
:item-size="120"
|
||||
style="max-height: 500px"
|
||||
container-style="height: 500px;"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 选择框 -->
|
||||
<div class="pt-2">
|
||||
<n-checkbox
|
||||
:checked="selectedResources.includes(item.id)"
|
||||
@update:checked="(checked) => toggleResourceSelection(item.id, checked)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 资源信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 标题和状态 -->
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white line-clamp-1">
|
||||
{{ item.title || '未命名资源' }}
|
||||
</h3>
|
||||
<n-tag :type="getStatusType(item)" size="small">
|
||||
{{ getStatusText(item) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm line-clamp-2 mb-2">
|
||||
{{ item.description || '暂无描述' }}
|
||||
</p>
|
||||
|
||||
<!-- 元信息 -->
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-folder mr-1"></i>
|
||||
{{ item.category_name || '未分类' }}
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-cloud mr-1"></i>
|
||||
夸克网盘
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-eye mr-1"></i>
|
||||
{{ item.view_count || 0 }} 次浏览
|
||||
</span>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
{{ formatDate(item.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 原始链接 -->
|
||||
<div class="mt-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-400">原始链接:</span>
|
||||
<a
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
class="text-xs text-blue-500 hover:text-blue-700 truncate max-w-xs"
|
||||
>
|
||||
{{ item.url }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex flex-col space-y-2 ml-4">
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="item.transferring"
|
||||
@click="handleSingleTransfer(item)"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</template>
|
||||
{{ item.transferring ? '转存中' : '立即转存' }}
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
size="small"
|
||||
@click="viewResource(item)"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-eye"></i>
|
||||
</template>
|
||||
查看详情
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-4 flex justify-center">
|
||||
<n-pagination
|
||||
v-model:page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:item-count="total"
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 转存结果模态框 -->
|
||||
<n-modal v-model:show="showTransferResult" preset="card" title="转存结果" style="width: 600px">
|
||||
<div v-if="transferResults.length > 0" class="space-y-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-green-600">{{ transferSuccessCount }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">成功</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-red-600">{{ transferFailedCount }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">失败</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="text-xl font-bold text-blue-600">{{ transferResults.length }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">总计</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-300 overflow-y-auto">
|
||||
<div v-for="result in transferResults" :key="result.id" class="p-3 border rounded mb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium truncate">{{ result.title }}</div>
|
||||
<div class="text-xs text-gray-500 truncate">{{ result.url }}</div>
|
||||
</div>
|
||||
<n-tag :type="result.success ? 'success' : 'error'" size="small">
|
||||
{{ result.success ? '成功' : '失败' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
<div v-if="result.message" class="text-xs text-gray-600 mt-1">
|
||||
{{ result.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useResourceApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const resources = ref([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// 搜索条件
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref(null)
|
||||
const selectedTag = ref(null)
|
||||
const selectedStatus = ref(null)
|
||||
|
||||
// 选择状态
|
||||
const selectedResources = ref([])
|
||||
|
||||
// 批量操作状态
|
||||
const batchTransferring = ref(false)
|
||||
const showTransferResult = ref(false)
|
||||
const transferResults = ref([])
|
||||
|
||||
// 选项数据
|
||||
const categoryOptions = ref([])
|
||||
const tagOptions = ref([])
|
||||
const statusOptions = [
|
||||
{ label: '有效', value: 'valid' },
|
||||
{ label: '无效', value: 'invalid' },
|
||||
{ label: '待验证', value: 'pending' }
|
||||
]
|
||||
|
||||
// API实例
|
||||
const resourceApi = useResourceApi()
|
||||
const categoryApi = useCategoryApi()
|
||||
const tagApi = useTagApi()
|
||||
|
||||
// 计算属性
|
||||
const isAllSelected = computed(() => {
|
||||
return resources.value.length > 0 && selectedResources.value.length === resources.value.length
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
return selectedResources.value.length > 0 && selectedResources.value.length < resources.value.length
|
||||
})
|
||||
|
||||
const transferSuccessCount = computed(() => {
|
||||
return transferResults.value.filter(r => r.success).length
|
||||
})
|
||||
|
||||
const transferFailedCount = computed(() => {
|
||||
return transferResults.value.filter(r => !r.success).length
|
||||
})
|
||||
|
||||
// 获取未转存资源(夸克网盘且无save_url)
|
||||
const fetchUntransferredResources = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
no_save_url: true, // 筛选没有转存链接的资源
|
||||
pan_name: 'quark' // 仅夸克网盘资源
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
params.search = searchQuery.value
|
||||
}
|
||||
if (selectedCategory.value) {
|
||||
params.category_id = selectedCategory.value
|
||||
}
|
||||
|
||||
const result = await resourceApi.getResources(params) as any
|
||||
console.log('未转存资源结果:', result)
|
||||
|
||||
if (result && result.resources) {
|
||||
resources.value = result.resources
|
||||
total.value = result.total || 0
|
||||
} else if (Array.isArray(result)) {
|
||||
resources.value = result
|
||||
total.value = result.length
|
||||
}
|
||||
|
||||
// 清空选择
|
||||
selectedResources.value = []
|
||||
} catch (error) {
|
||||
console.error('获取未转存资源失败:', error)
|
||||
resources.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类选项
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const result = await categoryApi.getCategories() as any
|
||||
if (result && result.items) {
|
||||
categoryOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签选项
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const result = await tagApi.getTags() as any
|
||||
if (result && result.items) {
|
||||
tagOptions.value = result.items.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取标签失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
fetchUntransferredResources()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
fetchUntransferredResources()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
fetchUntransferredResources()
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchUntransferredResources()
|
||||
}
|
||||
|
||||
// 选择处理
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
selectedResources.value = resources.value.map(r => r.id)
|
||||
} else {
|
||||
selectedResources.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const toggleResourceSelection = (id: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!selectedResources.value.includes(id)) {
|
||||
selectedResources.value.push(id)
|
||||
}
|
||||
} else {
|
||||
const index = selectedResources.value.indexOf(id)
|
||||
if (index > -1) {
|
||||
selectedResources.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单个转存
|
||||
const handleSingleTransfer = async (resource: any) => {
|
||||
resource.transferring = true
|
||||
|
||||
try {
|
||||
// 这里应该调用实际的转存API
|
||||
// 由于只是UI展示,模拟转存过程
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 模拟随机成功/失败
|
||||
const isSuccess = Math.random() > 0.3
|
||||
|
||||
if (isSuccess) {
|
||||
$message.success(`${resource.title} 转存成功`)
|
||||
// 刷新列表
|
||||
refreshData()
|
||||
} else {
|
||||
$message.error(`${resource.title} 转存失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('转存失败:', error)
|
||||
$message.error('转存失败')
|
||||
} finally {
|
||||
resource.transferring = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量转存
|
||||
const handleBatchTransfer = async () => {
|
||||
if (selectedResources.value.length === 0) {
|
||||
$message.warning('请选择要转存的资源')
|
||||
return
|
||||
}
|
||||
|
||||
batchTransferring.value = true
|
||||
transferResults.value = []
|
||||
|
||||
try {
|
||||
const selectedItems = resources.value.filter(r => selectedResources.value.includes(r.id))
|
||||
|
||||
// 这里应该调用实际的批量转存API
|
||||
// 由于只是UI展示,模拟批量转存过程
|
||||
for (const item of selectedItems) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const isSuccess = Math.random() > 0.3
|
||||
transferResults.value.push({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
success: isSuccess,
|
||||
message: isSuccess ? '转存成功' : '转存失败:网络错误'
|
||||
})
|
||||
}
|
||||
|
||||
showTransferResult.value = true
|
||||
|
||||
// 刷新列表
|
||||
refreshData()
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量转存失败:', error)
|
||||
$message.error('批量转存失败')
|
||||
} finally {
|
||||
batchTransferring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看资源详情
|
||||
const viewResource = (resource: any) => {
|
||||
console.log('查看资源详情:', resource)
|
||||
// 这里可以打开资源详情模态框
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (resource: any) => {
|
||||
if (resource.is_valid === false) return 'error'
|
||||
if (resource.is_valid === true) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (resource: any) => {
|
||||
if (resource.is_valid === false) return '无效'
|
||||
if (resource.is_valid === true) return '有效'
|
||||
return '待验证'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
fetchTags()
|
||||
fetchUntransferredResources()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,46 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<!-- 页面标题 -->
|
||||
<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 class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-400 dark:text-gray-500 mb-4">
|
||||
<i class="fas fa-exchange-alt text-4xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能开发中</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">数据转存管理功能正在开发中,敬请期待...</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主要内容 -->
|
||||
<n-card>
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<!-- 手动批量转存 -->
|
||||
<n-tab-pane name="manual" tab="手动批量转存">
|
||||
<AdminManualBatchTransfer />
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 已转存列表 -->
|
||||
<n-tab-pane name="transferred" tab="已转存列表">
|
||||
<AdminTransferredList ref="transferredListRef" />
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 未转存列表 -->
|
||||
<n-tab-pane name="untransferred" tab="未转存列表">
|
||||
<AdminUntransferredList ref="untransferredListRef" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 数据转存管理页面
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 页面配置
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: ['auth']
|
||||
})
|
||||
|
||||
// 活动标签
|
||||
const activeTab = ref('manual')
|
||||
|
||||
// 组件引用
|
||||
const transferredListRef = ref(null)
|
||||
const untransferredListRef = ref(null)
|
||||
</script>
|
||||
Reference in New Issue
Block a user