update: ui

This commit is contained in:
ctwj
2025-09-14 10:26:58 +08:00
parent 9690a63646
commit d23a6b26e4
24 changed files with 1887 additions and 1713 deletions

View File

@@ -392,8 +392,9 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
// CreateExpansionTask 创建扩容任务
func (h *TaskHandler) CreateExpansionTask(c *gin.Context) {
var req struct {
PanAccountID uint `json:"pan_account_id" binding:"required"`
Description string `json:"description"`
PanAccountID uint `json:"pan_account_id" binding:"required"`
Description string `json:"description"`
DataSource map[string]interface{} `json:"dataSource"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -420,10 +421,14 @@ func (h *TaskHandler) CreateExpansionTask(c *gin.Context) {
accountName = fmt.Sprintf("账号%d", cks.ID)
}
// 构建任务配置存储账号ID
// 构建任务配置存储账号ID和数据源
taskConfig := map[string]interface{}{
"pan_account_id": req.PanAccountID,
}
// 如果有数据源配置添加到taskConfig中
if req.DataSource != nil && len(req.DataSource) > 0 {
taskConfig["data_source"] = req.DataSource
}
configJSON, _ := json.Marshal(taskConfig)
// 创建任务标题,包含账号名称
@@ -451,6 +456,10 @@ func (h *TaskHandler) CreateExpansionTask(c *gin.Context) {
expansionInput := task.ExpansionInput{
PanAccountID: req.PanAccountID,
}
// 如果有数据源配置,添加到输入数据中
if req.DataSource != nil && len(req.DataSource) > 0 {
expansionInput.DataSource = req.DataSource
}
inputJSON, _ := json.Marshal(expansionInput)

View File

@@ -29,7 +29,8 @@ func (ep *ExpansionProcessor) GetTaskType() string {
// ExpansionInput 扩容任务输入数据结构
type ExpansionInput struct {
PanAccountID uint `json:"pan_account_id"`
PanAccountID uint `json:"pan_account_id"`
DataSource map[string]interface{} `json:"data_source,omitempty"`
}
// ExpansionOutput 扩容任务输出数据结构
@@ -93,8 +94,8 @@ func (ep *ExpansionProcessor) Process(ctx context.Context, taskID uint, item *en
return err
}
// 执行扩容操作(这里留空,直接返回成功
if err := ep.performExpansion(ctx, input.PanAccountID); err != nil {
// 执行扩容操作(传入数据源
if err := ep.performExpansion(ctx, input.PanAccountID, input.DataSource); err != nil {
output := ExpansionOutput{
Success: false,
Message: "扩容失败",
@@ -177,11 +178,11 @@ func (ep *ExpansionProcessor) checkAccountType(panAccountID uint) error {
}
// performExpansion 执行扩容操作
func (ep *ExpansionProcessor) performExpansion(ctx context.Context, panAccountID uint) error {
func (ep *ExpansionProcessor) performExpansion(ctx context.Context, panAccountID uint, dataSource map[string]interface{}) error {
// 扩容逻辑暂时留空,直接返回成功
// TODO: 实现具体的扩容逻辑
utils.Info("执行扩容操作账号ID: %d", panAccountID)
utils.Info("执行扩容操作账号ID: %d, 数据源: %v", panAccountID, dataSource)
// 模拟扩容操作延迟
// time.Sleep(2 * time.Second)

View File

@@ -1,112 +1,110 @@
<template>
<div class="space-y-6">
<div class="space-y-6 h-full">
<!-- 输入区域 -->
<n-card title="批量转存资源列表">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 左侧资源输入 -->
<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="请输入资源内容格式标题和URL为一组..."
:autosize="{ minRows: 10, maxRows: 15 }"
show-count
:maxlength="100000"
/>
</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>
<CategorySelector
v-model="selectedCategory"
placeholder="选择分类"
clearable
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
标签
</label>
<TagSelector
v-model="selectedTags"
placeholder="选择标签"
multiple
clearable
/>
</div>
<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-select
v-model:value="selectedAccounts"
:options="accountOptions"
placeholder="选择网盘账号"
multiple
filterable
:loading="accountsLoading"
@update:value="handleAccountChange"
>
<template #option="{ option: accountOption }">
<div class="flex items-center justify-between w-full">
<div class="flex items-center space-x-2">
<span class="text-sm">{{ accountOption.label }}</span>
<n-tag v-if="accountOption.is_valid" type="success" size="small">有效</n-tag>
<n-tag v-else type="error" size="small">无效</n-tag>
</div>
<div class="text-xs text-gray-500">
{{ formatSpace(accountOption.left_space) }}
</div>
</div>
</template>
</n-select>
<div class="text-xs text-gray-500 mt-1">
请选择要使用的网盘账号系统将使用选中的账号进行转存操作
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-3 pt-4">
<n-button
type="primary"
block
size="large"
:loading="processing"
:disabled="!resourceText.trim() || !selectedAccounts.length || processing"
@click="handleBatchTransfer"
>
<template #icon>
<i class="fas fa-upload"></i>
</template>
开始批量转存
</n-button>
<n-button
block
@click="clearInput"
:disabled="processing"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
清空内容
</n-button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 左侧资源输入 -->
<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="请输入资源内容格式标题和URL为一组..."
:autosize="{ minRows: 10, maxRows: 15 }"
show-count
:maxlength="100000"
/>
</div>
</div>
</n-card>
<!-- 右侧配置选项 -->
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
默认分类
</label>
<CategorySelector
v-model="selectedCategory"
placeholder="选择分类"
clearable
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
标签
</label>
<TagSelector
v-model="selectedTags"
placeholder="选择标签"
multiple
clearable
/>
</div>
<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-select
v-model:value="selectedAccounts"
:options="accountOptions"
placeholder="选择网盘账号"
multiple
filterable
:loading="accountsLoading"
@update:value="handleAccountChange"
>
<template #option="scope">
<div class="flex items-center justify-between w-full" v-if="scope && scope.option">
<div class="flex items-center space-x-2">
<span class="text-sm">{{ scope.option.label || '未知账号' }}</span>
<n-tag v-if="scope.option.is_valid" type="success" size="small">有效</n-tag>
<n-tag v-else type="error" size="small">无效</n-tag>
</div>
<div class="text-xs text-gray-500">
{{ formatSpace(scope.option.left_space || 0) }}
</div>
</div>
</template>
</n-select>
<div class="text-xs text-gray-500 mt-1">
请选择要使用的网盘账号系统将使用选中的账号进行转存操作
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-3 pt-4">
<n-button
type="primary"
block
size="large"
:loading="processing"
:disabled="!resourceText.trim() || !selectedAccounts.length || processing"
@click="handleBatchTransfer"
>
<template #icon>
<i class="fas fa-upload"></i>
</template>
开始批量转存
</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 v-if="results.length > 0" title="转存结果">
@@ -202,15 +200,15 @@ const invalidUrls = computed(() => {
})
const successCount = computed(() => {
return results.value.filter((r: any) => r.status === 'success').length
return results.value ? results.value.filter((r: any) => r && r.status === 'success').length : 0
})
const failedCount = computed(() => {
return results.value.filter((r: any) => r.status === 'failed').length
return results.value ? results.value.filter((r: any) => r && r.status === 'failed').length : 0
})
const processingCount = computed(() => {
return results.value.filter((r: any) => r.status === 'processing').length
return results.value ? results.value.filter((r: any) => r && r.status === 'processing').length : 0
})
// 结果表格列
@@ -243,9 +241,10 @@ const resultColumns = [
pending: { color: 'warning', text: '等待中', icon: 'fas fa-clock' }
}
const status = statusMap[row.status as keyof typeof statusMap] || statusMap.failed
return h('n-tag', { type: status.color }, {
icon: () => h('i', { class: status.icon }),
default: () => status.text
const safeStatus = status || statusMap.failed
return h('n-tag', { type: safeStatus.color }, {
icon: () => h('i', { class: safeStatus.icon }),
default: () => safeStatus.text
})
}
},
@@ -264,7 +263,7 @@ const resultColumns = [
tooltip: true
},
render: (row: any) => {
if (row.saveUrl) {
if (row && row.saveUrl) {
return h('a', {
href: row.saveUrl,
target: '_blank',
@@ -354,7 +353,11 @@ const handleBatchTransfer = async () => {
// 第三步:创建任务
const taskResponse = await taskApi.createBatchTransferTask(taskData) as any
console.log('任务创建响应:', taskResponse)
if (!taskResponse || !taskResponse.task_id) {
throw new Error('创建任务失败:响应数据无效')
}
currentTaskId.value = taskResponse.task_id
// 第四步:启动任务
@@ -523,14 +526,17 @@ const getAccountOptions = async () => {
const response = await cksApi.getCks() as any
const accounts = Array.isArray(response) ? response : []
accountOptions.value = accounts.map((account: any) => ({
label: `${account.username || '未知用户'} (${account.pan?.name || '未知平台'})`,
value: account.id,
is_valid: account.is_valid,
left_space: account.left_space,
username: account.username,
pan_name: account.pan?.name || '未知平台'
}))
accountOptions.value = accounts.map((account: any) => {
if (!account) return null
return {
label: `${account.username || '未知用户'} (${account.pan?.name || '未知平台'})`,
value: account.id,
is_valid: account.is_valid || false,
left_space: account.left_space || 0,
username: account.username || '未知用户',
pan_name: account.pan?.name || '未知平台'
}
}).filter(option => option !== null) as any[]
} catch (error) {
console.error('获取网盘账号选项失败:', error)
message.error('获取网盘账号失败')

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-4">
<div class="flex flex-col gap-2 h-full">
<!-- 搜索和筛选 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="flex-0 grid grid-cols-1 md:grid-cols-4 gap-4">
<n-input
v-model:value="searchQuery"
placeholder="搜索已转存资源..."
@@ -34,28 +34,111 @@
</div>
<!-- 调试信息 -->
<div class="text-sm text-gray-500 mb-2">
<div class="flex-0 text-sm text-gray-500">
数据数量: {{ resources.length }}, 总数: {{ total }}, 加载状态: {{ loading }}
</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"
virtual-scroll
max-height="500"
/>
<!-- 资源列表 -->
<div class="flex-1 h-1">
<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 class="h-full">
<!-- 虚拟列表 -->
<n-virtual-list
:items="resources"
:item-size="120"
class="h-full"
>
<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="flex-1 min-w-0">
<div class="flex items-center space-x-2 mb-2">
<!-- ID -->
<span class="text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 px-2 py-1 rounded">
ID: {{ item.id }}
</span>
<!-- 标题 -->
<h3 class="text-lg font-medium text-gray-900 dark:text-white line-clamp-1 flex-1">
{{ item.title || '未命名资源' }}
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm mt-3">
<!-- 分类 -->
<div class="flex items-center">
<i class="fas fa-folder mr-1 text-gray-400"></i>
<span class="text-gray-600 dark:text-gray-400">分类:</span>
<span class="ml-2">{{ item.category_name || '未分类' }}</span>
</div>
<!-- 转存时间 -->
<div class="flex items-center">
<i class="fas fa-calendar mr-1 text-gray-400"></i>
<span class="text-gray-600 dark:text-gray-400">转存时间:</span>
<span class="ml-2">{{ formatDate(item.updated_at) }}</span>
</div>
<!-- 浏览数 -->
<div class="flex items-center">
<i class="fas fa-eye mr-1 text-gray-400"></i>
<span class="text-gray-600 dark:text-gray-400">浏览数:</span>
<span class="ml-2">{{ item.view_count || 0 }}</span>
</div>
</div>
<!-- 转存链接 -->
<div class="mt-3">
<div class="flex items-start space-x-2">
<span class="text-xs text-gray-400">转存链接:</span>
<a
v-if="item.save_url"
:href="item.save_url"
target="_blank"
class="text-xs text-green-500 hover:text-green-700 break-all"
>
{{ item.save_url.length > 60 ? item.save_url.substring(0, 60) + '...' : item.save_url }}
</a>
<span v-else class="text-xs text-gray-500">暂无转存链接</span>
</div>
</div>
</div>
</div>
</div>
</template>
</n-virtual-list>
</div>
</div>
<!-- 分页 -->
<div class="flex-0">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="total"
:page-sizes="[10000, 20000, 50000, 100000]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useResourceApi, usePanApi } from '~/composables/useApi'
import { useMessage } from 'naive-ui'
@@ -81,78 +164,17 @@ const panApi = usePanApi()
// 获取平台数据
const { data: platformsData } = await useAsyncData('transferredPlatforms', () => panApi.getPans())
// 平台选项
const platformOptions = computed(() => {
const data = platformsData.value as any
const platforms = data?.data || data || []
return platforms.map((platform: any) => ({
label: platform.remark || platform.name,
value: platform.id
}))
})
// 获取平台名称
const getPlatformName = (platformId: number) => {
const platform = (platformsData.value as any)?.data?.find((plat: any) => plat.id === platformId)
return platform?.remark || platform?.name || '未知平台'
}
// 分页配置
const pagination = reactive({
page: 1,
pageSize: 10000,
itemCount: 0,
pageSizes: [10000, 20000, 50000, 100000],
showSizePicker: true,
showQuickJumper: true,
prefix: ({ itemCount }: any) => `${itemCount}`
})
// 表格列配置
const columns: any[] = [
{
title: 'ID',
key: 'id',
width: 60,
fixed: 'left' as const
},
{
title: '标题',
key: 'title',
width: 200,
ellipsis: {
tooltip: true
}
},
{
title: '分类',
key: 'category_name',
width: 80
},
{
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()
}
}
]
// 格式化日期
const formatDate = (dateString: string) => {
if (!dateString) return '未知时间'
return new Date(dateString).toLocaleDateString()
}
// 获取已转存资源
const fetchTransferredResources = async () => {
@@ -183,24 +205,20 @@ const fetchTransferredResources = async () => {
console.log('使用嵌套data格式数量:', result.data.data.length)
resources.value = result.data.data
total.value = result.data.total || 0
pagination.itemCount = result.data.total || 0
} else {
// 处理直接的data结构{data: [...], total: ...}
console.log('使用直接data格式数量:', 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)
@@ -223,22 +241,18 @@ const fetchTransferredResources = async () => {
// 搜索处理
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()
}
@@ -246,4 +260,13 @@ const handlePageSizeChange = (size: number) => {
onMounted(() => {
fetchTransferredResources()
})
</script>
</script>
<style scoped>
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-4">
<div class="h-full flex flex-col gap-2">
<!-- 搜索和筛选 -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="flex-0 grid grid-cols-1 md:grid-cols-5 gap-4">
<n-input
v-model:value="searchQuery"
placeholder="搜索未转存资源..."
@@ -41,47 +41,45 @@
</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 class="flex-0 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>
</n-card>
<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>
<div class="flex-1 h-1">
<div v-if="loading" class="flex items-center justify-center py-8">
<n-spin size="large" />
</div>
@@ -91,13 +89,12 @@
<p class="text-gray-500">暂无未转存的夸克资源</p>
</div>
<div v-else>
<div v-else class="h-full">
<!-- 虚拟列表 -->
<n-virtual-list
:items="resources"
:item-size="120"
style="max-height: 500px"
container-style="height: 500px;"
class="h-full"
>
<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">
@@ -167,22 +164,23 @@
</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="[10000, 20000, 50000, 100000]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
</n-card>
</div>
<div class="flex-0">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="total"
:page-sizes="[10000, 20000, 50000, 100000]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
<!-- 网盘账号选择模态框 -->
<n-modal v-model:show="showAccountSelectionModal" preset="card" title="选择网盘账号" style="width: 600px">

View File

@@ -0,0 +1,80 @@
<template>
<div class="h-full flex flex-col gap-3">
<!-- 顶部标题和按钮区域 -->
<div class="flex-0 w-full flex">
<div v-if="isSubPage" class="flex-0 mr-4 flex items-center">
<n-button @click="goBack" type="text" size="small">
<template #icon>
<i class="fas fa-arrow-left"></i>
</template>
</n-button>
</div>
<!-- 页面头部内容 -->
<div class="flex-1 w-1 flex items-center justify-between">
<slot name="page-header"></slot>
</div>
</div>
<!-- 通知提示区域 -->
<div v-if="hasNoticeSection" class="flex-shrink-0">
<slot name="notice-section"></slot>
</div>
<!-- 过滤栏区域 -->
<div v-if="hasFilterBar" class="flex-shrink-0">
<slot name="filter-bar"></slot>
</div>
<!-- 内容区 - 自适应剩余高度 -->
<div class="flex-1 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
<!-- 内容区header -->
<div v-if="hasContentHeader" class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-700 whitespace-nowrap">
<slot name="content-header"></slot>
</div>
<!-- 内容区content - 自适应剩余高度 -->
<div class="flex-1 h-1 content-slot overflow-hidden">
<slot name="content"></slot>
</div>
<!-- 内容区footer -->
<div v-if="hasContentFooter" class="flex-shrink-0 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-700">
<slot name="content-footer"></slot>
</div>
</div>
</div>
</template>
<script setup>
const router = useRouter()
const $slots = useSlots()
const hasNoticeSection = computed(() => $slots['notice-section'] !== undefined)
const hasFilterBar = computed(() => $slots['filter-bar'] !== undefined)
const hasContentHeader = computed(() => $slots['content-header'] !== undefined)
const hasContentFooter = computed(() => $slots['content-footer'] !== undefined)
const goBack = () => {
try {
router.back()
} catch (error) {
navigateTo('/admin')
}
}
defineProps({
minHeight: {
type: String,
default: '400px'
},
isSubPage: {
type: Boolean,
default: false,
}
})
</script>
<style scoped>
:deep(.content-slot) {
min-height: 0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<div class="min-h-screen max-h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
<!-- 设置通用title -->
<Head>
<title>管理后台 - 老九网盘资源数据库</title>
@@ -129,9 +129,9 @@
</header>
<!-- 侧边栏和主内容区域 -->
<div class="flex">
<div class="flex main-content">
<!-- 侧边栏 -->
<aside class="w-64 bg-white dark:bg-gray-800 shadow-sm border-r border-gray-200 dark:border-gray-700 min-h-screen">
<aside class="w-64 bg-white dark:bg-gray-800 shadow-sm border-r border-gray-200 dark:border-gray-700 h-full overflow-y-auto">
<nav class="mt-8">
<div class="px-4 space-y-6">
<!-- 仪表盘 -->
@@ -273,7 +273,7 @@
</aside>
<!-- 主内容区域 -->
<main class="flex-1 p-8">
<main class="flex-1 p-4 h-full overflow-y-auto">
<ClientOnly>
<n-message-provider>
<n-notification-provider>
@@ -304,9 +304,7 @@ const systemConfigStore = useSystemConfigStore()
// 任务状态管理
const taskStore = useTaskStore()
// 初始化系统配置管理员页面使用管理员API
// 在setup阶段初始化确保数据可用
await systemConfigStore.initConfig(false, true)
systemConfigStore.initConfig(false, true).catch(console.error)
// 版本信息
const versionInfo = ref({
@@ -328,10 +326,18 @@ const fetchVersionInfo = async () => {
// 初始化版本信息和任务状态管理
onMounted(() => {
fetchVersionInfo()
// 启动任务状态自动更新
taskStore.startAutoUpdate()
console.log('Admin layout: 任务状态自动更新已启动')
// 确保在客户端配置被正确载入防止SSR水合问题
setTimeout(async () => {
try {
await systemConfigStore.initConfig(true, true) // 强制刷新防止SSR水合问题
} catch (error) {
console.error('Admin layout: onMounted 配置刷新失败', error)
}
}, 100) // 延迟100ms确保组件渲染完成
})
// 组件销毁时清理任务状态管理
@@ -344,9 +350,6 @@ onBeforeUnmount(() => {
// 系统配置
const systemConfig = computed(() => {
const config = systemConfigStore.config || {}
console.log('顶部导航系统配置:', config)
console.log('自动处理状态:', config.auto_process_ready_resources)
console.log('自动转存状态:', config.auto_transfer_enabled)
return config
})
@@ -605,4 +608,7 @@ const navigateToTasks = () => {
font-family: 'Font Awesome 6 Free';
font-weight: 900;
}
.main-content {
height: calc(100vh - 85px);
}
</style>

View File

@@ -1,76 +1,176 @@
<template>
<div class="max-w-7xl mx-auto space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<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-alert type="info" :show-icon="false">
<div class="flex items-center space-x-2">
<i class="fas fa-info-circle text-blue-500"></i>
<span class="text-sm">
<strong>20T扩容说明</strong>建议 <span @click="showImageModal = true" style="color:red" class="cursor-pointer">蜂小推</span> quark 账号扩容<span @click="drawActive = true" style="color:blue" class="cursor-pointer">什么推荐蜂小推</span><br>
1. 20T扩容 只支持新号等到蜂小推首次 6T 奖励 到账后进行扩容<br>
2. 账号需要处于关闭状态 开启状态可能会被用于自动转存等任务存咋影响<br>
3. <strong><n-text style='font-size:16px' type="error">扩容完成后并不直接获得容量</n-text>账号将存储大量热门资源<n-text style='font-size:16px' type="error">需要手动推广</n-text></strong><br>
4. 注意 推广获得20T容量删除所有资源 热门资源比较敏感不建议长期推广仅用于扩容
</span>
<AdminPageLayout :is-sub-page="true">
<!-- 页面头部 - 标题 -->
<template #page-header>
<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-alert>
</template>
<n-drawer v-model:show="drawActive" :width="502" closable placement="right">
<n-drawer-content title="扩容说明">
<div class="space-y-6 p-4">
<div class="mb-4">
<p class="text-gray-700 text-large dark:text-gray-300 leading-relaxed">
扩容是网盘公司提供给推广用户的<n-text type="success">特权</n-text>需要先注册推广平台并<n-text type="success">达标</n-text>即可获得权益
</p>
</div>
<!-- 通知提示区域 - 扩容说明Alert -->
<template #notice-section>
<n-alert type="info" :show-icon="false">
<div class="flex items-center space-x-2">
<i class="fas fa-info-circle text-blue-500"></i>
<span class="text-sm">
<strong>20T扩容说明</strong>建议 <span @click="showImageModal = true" style="color:red" class="cursor-pointer">蜂小推</span> quark 账号扩容<span @click="drawActive = true" style="color:blue" class="cursor-pointer">什么推荐蜂小推</span><br>
1. 20T扩容 只支持新号等到蜂小推首次 6T 奖励 到账后进行扩容<br>
2. 账号需要处于关闭状态 开启状态可能会被用于自动转存等任务存咋影响<br>
3. <strong><n-text style='font-size:16px' type="error">扩容完成后并不直接获得容量</n-text>账号将存储大量热门资源<n-text style='font-size:16px' type="error">需要手动推广</n-text></strong><br>
4. 注意 推广获得20T容量删除所有资源 热门资源比较敏感不建议长期推广仅用于扩容
</span>
</div>
</n-alert>
</template>
<n-collapse arrow-placement="right">
<n-collapse-item title="达标要求" name="0">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-list-check text-blue-500 mr-2"></i>
达标要求以蜂小推为例
</h3>
<span>首次账号累计7天转存 > 10 拉新 > 5</span>
</n-collapse-item>
<n-collapse-item title="注意事项" name="1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-exclamation-triangle text-orange-500 mr-2"></i>
注意事项
</h3>
<span>每个人的转存只有当日第一次转存且通过手机转存才算有效转存</span>
</n-collapse-item>
<n-collapse-item title="扩容原理" name="2">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-question-circle text-purple-500 mr-2"></i>
扩容原理
</h3>
<span>大量转存热播资源这样才能尽可能快的达标</span>
</n-collapse-item>
<n-collapse-item title="为什么推荐蜂小推" name="3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-thumbs-up text-green-500 mr-2"></i>
为什么推荐蜂小推
</h3>
<p class="text-gray-600 dark:text-gray-400 leading-relaxed">
登记后第二天即会发送 <strong class="text-blue-600">6T 空间</strong>满足大量存储资源的前提条件
</p>
</n-collapse-item>
<n-collapse-item title="蜂小推怎么注册" name="3">
<p class="text-gray-600 dark:text-gray-400">
请扫描下方二维码进行注册
</p>
<div class="mt-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-center">
<n-qr-code :value="qrCode" />
<!-- 内容区header - 账号列表标题 -->
<template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">支持扩容的账号列表</span>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ expansionAccounts.length }} 个账号
</div>
</div>
</template>
<!-- 内容区content - 账号列表详情 -->
<template #content>
<div class="flex-1 h-full overflow-hidden">
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large">
<template #description>
<span class="text-gray-500">加载中...</span>
</template>
</n-spin>
</div>
<!-- 空状态 -->
<div v-else-if="expansionAccounts.length === 0" class="flex flex-col items-center justify-center py-12">
<n-empty description="暂无可扩容的账号,请先添加有效的 quark 账号">
<template #icon>
<i class="fas fa-user-circle text-4xl text-gray-400"></i>
</template>
</n-empty>
</div>
<!-- 账号列表内容的虚拟滚动列表 -->
<div v-else class="h-full">
<n-virtual-list class="h-full" :items="expansionAccounts" :item-size="80">
<template #default="{ item }">
<div class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center justify-between">
<!-- 左侧信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-4">
<!-- 平台图标 -->
<span v-html="getPlatformIcon(item.service_type === 'quark' ? '夸克网盘' : '其他')" class="text-lg"></span>
<!-- 账号信息 -->
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1">
{{ item.name }}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ item.service_type === 'quark' ? '夸克网盘' : '其他平台' }}
</p>
</div>
<!-- 扩容状态 -->
<div class="flex items-center space-x-2">
<n-tag v-if="item.expanded" type="success" size="small">
已扩容
</n-tag>
<n-tag v-else type="warning" size="small">
可扩容
</n-tag>
</div>
</div>
<!-- 创建时间 -->
<div class="mt-2">
<span class="text-xs text-gray-600 dark:text-gray-400">
创建时间: {{ formatDate(item.created_at) }}
</span>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center space-x-2 ml-4">
<n-button
size="small"
type="primary"
:disabled="item.expanded"
:loading="expandingAccountId === item.id"
@click="handleExpansion(item)"
>
<template #icon>
<i class="fas fa-expand"></i>
</template>
{{ item.expanded ? '已扩容' : '扩容' }}
</n-button>
</div>
</div>
</n-collapse-item>
</n-collapse>
</div>
</template>
</n-virtual-list>
</div>
</div>
</template>
</AdminPageLayout>
<!-- 抽屉和模态框 - 移动到AdminPageLayout外部 -->
<n-drawer v-model:show="drawActive" :width="502" closable placement="right">
<n-drawer-content title="扩容说明">
<div class="space-y-6 p-4">
<div class="mb-4">
<p class="text-gray-700 text-large dark:text-gray-300 leading-relaxed">
扩容是网盘公司提供给推广用户的<n-text type="success">特权</n-text>需要先注册推广平台并<n-text type="success">达标</n-text>即可获得权益
</p>
</div>
</n-drawer-content>
<n-collapse arrow-placement="right">
<n-collapse-item title="达标要求" name="0">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-list-check text-blue-500 mr-2"></i>
达标要求以蜂小推为例
</h3>
<span>首次账号累计7天转存 > 10 拉新 > 5</span>
</n-collapse-item>
<n-collapse-item title="注意事项" name="1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-exclamation-triangle text-orange-500 mr-2"></i>
注意事项
</h3>
<span>每个人的转存只有当日第一次转存且通过手机转存才算有效转存</span>
</n-collapse-item>
<n-collapse-item title="扩容原理" name="2">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-question-circle text-purple-500 mr-2"></i>
扩容原理
</h3>
<span>大量转存热播资源这样才能尽可能快的达标</span>
</n-collapse-item>
<n-collapse-item title="为什么推荐蜂小推" name="3">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3 flex items-center">
<i class="fas fa-thumbs-up text-green-500 mr-2"></i>
为什么推荐蜂小推
</h3>
<p class="text-gray-600 dark:text-gray-400 leading-relaxed">
登记后第二天即会发送 <strong class="text-blue-600">6T 空间</strong>满足大量存储资源的前提条件
</p>
</n-collapse-item>
<n-collapse-item title="蜂小推怎么注册" name="3">
<p class="text-gray-600 dark:text-gray-400">
请扫描下方二维码进行注册
</p>
<div class="mt-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg text-center">
<n-qr-code :value="qrCode" />
</div>
</n-collapse-item>
</n-collapse>
</div>
</n-drawer-content>
</n-drawer>
<!-- 图片模态框 -->
@@ -115,110 +215,6 @@
</div>
</n-card>
</n-modal>
<!-- 账号列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">支持扩容的账号列表</span>
<div class="text-sm text-gray-500">
{{ expansionAccounts.length }} 个账号
</div>
</div>
</template>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large">
<template #description>
<span class="text-gray-500">加载中...</span>
</template>
</n-spin>
</div>
<!-- 空状态 -->
<div v-else-if="expansionAccounts.length === 0" class="flex flex-col items-center justify-center py-12">
<n-empty description="暂无可扩容的账号,请先添加有效的 quark 账号">
<template #icon>
<i class="fas fa-user-circle text-4xl text-gray-400"></i>
</template>
</n-empty>
</div>
<!-- 账号列表 -->
<div v-else>
<n-virtual-list :items="expansionAccounts" :item-size="80" style="max-height: 500px">
<template #default="{ item }">
<div class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center justify-between">
<!-- 左侧信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-4">
<!-- 平台图标 -->
<span v-html="getPlatformIcon(item.service_type === 'quark' ? '夸克网盘' : '其他')" class="text-lg"></span>
<!-- 账号信息 -->
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1">
{{ item.name }}
</h3>
<p class="text-xs text-gray-500">
{{ item.service_type === 'quark' ? '夸克网盘' : '其他平台' }}
</p>
</div>
<!-- 扩容状态 -->
<div class="flex items-center space-x-2">
<n-tag v-if="item.expanded" type="success" size="small">
已扩容
</n-tag>
<n-tag v-else type="warning" size="small">
可扩容
</n-tag>
</div>
</div>
<!-- 创建时间 -->
<div class="mt-2">
<span class="text-xs text-gray-600 dark:text-gray-400">
创建时间: {{ formatDate(item.created_at) }}
</span>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center space-x-2 ml-4">
<n-button
size="small"
type="primary"
:disabled="item.expanded"
:loading="expandingAccountId === item.id"
@click="handleExpansion(item)"
>
<template #icon>
<i class="fas fa-expand"></i>
</template>
{{ item.expanded ? '已扩容' : '扩容' }}
</n-button>
</div>
</div>
</div>
</template>
</n-virtual-list>
</div>
</n-card>
<!-- 扩容任务列表 -->
<n-card v-if="expansionTasks.length > 0" title="扩容任务列表">
<n-data-table
:columns="taskColumns"
:data="expansionTasks"
:pagination="false"
max-height="400"
size="small"
/>
</n-card>
</div>
</template>
<script setup lang="ts">
@@ -233,7 +229,6 @@ import { useNotification, useDialog } from 'naive-ui'
// 响应式数据
const expansionAccounts = ref([])
const expansionTasks = ref([])
const loading = ref(true)
const expandingAccountId = ref(null)
const drawActive = ref(false) // 侧边栏激活
@@ -248,62 +243,6 @@ const pendingAccount = ref<any>(null) // 待处理的账号
const taskApi = useTaskApi()
const notification = useNotification()
// 表格列配置
const taskColumns = [
{
title: '任务ID',
key: 'id',
width: 80
},
{
title: '标题',
key: 'title',
ellipsis: {
tooltip: true
}
},
{
title: '状态',
key: 'status',
width: 100,
render: (row: any) => {
const statusMap = {
pending: { color: 'warning', text: '等待中', icon: 'fas fa-clock' },
running: { color: 'info', text: '运行中', icon: 'fas fa-spinner fa-spin' },
completed: { color: 'success', text: '已完成', icon: 'fas fa-check' },
failed: { color: 'error', text: '失败', icon: 'fas fa-times' }
}
const status = statusMap[row.status as keyof typeof statusMap] || statusMap.failed
return h('n-tag', { type: status.color }, {
icon: () => h('i', { class: status.icon }),
default: () => status.text
})
}
},
{
title: '创建时间',
key: 'created_at',
width: 150,
render: (row: any) => formatDate(row.created_at)
},
{
title: '操作',
key: 'actions',
width: 150,
render: (row: any) => h('div', { class: 'flex space-x-2' }, [
h('n-button', {
size: 'small',
type: 'primary',
onClick: () => viewTaskDetails(row.id)
}, '详情'),
row.status === 'running' ? h('n-button', {
size: 'small',
type: 'warning',
onClick: () => stopTask(row.id)
}, '停止') : null
].filter(Boolean))
}
]
// 获取支持扩容的账号列表
const fetchExpansionAccounts = async () => {
@@ -323,15 +262,6 @@ const fetchExpansionAccounts = async () => {
}
}
// 获取扩容任务列表
const fetchExpansionTasks = async () => {
try {
const response = await taskApi.getTasks({ taskType: 'expansion' })
expansionTasks.value = response.tasks || []
} catch (error) {
console.error('获取扩容任务列表失败:', error)
}
}
// 处理扩容操作
const handleExpansion = async (account) => {
@@ -366,11 +296,12 @@ const confirmDataSourceSelection = async () => {
duration: 3000
})
navigateTo('/admin/tasks')
// 刷新数据
await Promise.all([
fetchExpansionAccounts(),
fetchExpansionTasks()
])
// await Promise.all([
// fetchExpansionAccounts(),
// fetchExpansionTasks()
// ])
} catch (error) {
console.error('创建扩容任务失败:', error)
notification.error({
@@ -384,52 +315,7 @@ const confirmDataSourceSelection = async () => {
}
}
// 查看任务详情
const viewTaskDetails = async (taskId) => {
try {
const status = await taskApi.getTaskStatus(taskId)
console.log('任务详情:', status)
// 这里可以展示任务详情的模态框
notification.info({
title: '任务详情',
content: `任务状态: ${status.status}, 总项目数: ${status.total_items}`,
duration: 5000
})
} catch (error) {
console.error('获取任务详情失败:', error)
}
}
// 停止任务
const stopTask = async (taskId) => {
const dialog = useDialog()
dialog.warning({
title: '确认停止',
content: '确定要停止这个扩容任务吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await taskApi.stopTask(taskId)
notification.success({
title: '成功',
content: '任务已停止',
duration: 3000
})
await fetchExpansionTasks()
} catch (error) {
console.error('停止任务失败:', error)
notification.error({
title: '失败',
content: '停止任务失败',
duration: 3000
})
}
}
})
}
// 获取平台图标
const getPlatformIcon = (platformName) => {
@@ -455,10 +341,7 @@ const formatDate = (dateString) => {
// 页面加载
onMounted(async () => {
await Promise.all([
fetchExpansionAccounts(),
fetchExpansionTasks()
])
await fetchExpansionAccounts()
})
</script>
@@ -469,4 +352,4 @@ onMounted(async () => {
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">账号管理</h1>
<p class="text-gray-600 dark:text-gray-400">管理平台账号信息</p>
@@ -26,41 +26,43 @@
刷新
</n-button>
</div>
</div>
</template>
<!-- 搜索和筛选 -->
<n-card>
<div class="flex flex-col md:flex-row gap-4">
<n-input v-model:value="searchQuery" placeholder="搜索账号..." clearable class="flex-1">
<template #prefix>
<i class="fas fa-search"></i>
</template>
</n-input>
<!-- 过滤栏 - 搜索和筛选 -->
<template #filter-bar>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<div class="flex flex-col md:flex-row gap-4">
<n-input v-model:value="searchQuery" placeholder="搜索账号..." clearable class="flex-1">
<template #prefix>
<i class="fas fa-search"></i>
</template>
</n-input>
<n-select v-model:value="platform" placeholder="选择平台" :options="platformOptions" clearable
@update:value="onPlatformChange" class="w-full md:w-48" />
<n-select v-model:value="platform" placeholder="选择平台" :options="platformOptions" clearable
@update:value="onPlatformChange" class="w-full md:w-48" />
<n-button type="primary" @click="handleSearch" class="w-full md:w-auto md:min-w-[100px]">
<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">
<span class="text-lg font-semibold">账号列表</span>
<div class="text-sm text-gray-500">
{{ filteredCksList.length }} 个账号
</div>
<n-button type="primary" @click="handleSearch" class="w-full md:w-auto md:min-w-[100px]">
<template #icon>
<i class="fas fa-search"></i>
</template>
搜索
</n-button>
</div>
</template>
</div>
</template>
<!-- 加载状态 -->
<!-- 内容区header - 账号列表头部 -->
<template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold text-gray-900 dark:text-white">账号列表</span>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ filteredCksList.length }} 个账号
</div>
</div>
</template>
<!-- 内容区content - 账号列表表格 -->
<template #content>
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large">
<template #description>
@@ -69,7 +71,6 @@
</n-spin>
</div>
<!-- 空状态 -->
<div v-else-if="filteredCksList.length === 0" class="flex flex-col items-center justify-center py-12">
<n-empty description="暂无账号">
<template #icon>
@@ -86,9 +87,9 @@
</n-empty>
</div>
<!-- 账号列表 -->
<div v-else>
<n-virtual-list :items="filteredCksList" :item-size="100" style="max-height: 500px">
<!-- 账号列表和分页 -->
<div v-else class="flex flex-col flex-1 h-full overflow-hidden">
<n-virtual-list :items="filteredCksList" :item-size="100" class="h-full">
<template #default="{ item }">
<div
class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
@@ -123,13 +124,13 @@
<n-tag :type="item.is_valid ? 'success' : 'error'" size="small">
{{ item.is_valid ? '有效' : '无效' }}
</n-tag>
<span class="text-xs text-gray-500">
<span class="text-xs text-gray-500 dark:text-gray-400">
总空间: {{ formatFileSize(item.space) }}
</span>
<span class="text-xs text-gray-500">
<span class="text-xs text-gray-500 dark:text-gray-400">
已使用: {{ formatFileSize(Math.max(0, item.used_space || (item.space - item.left_space))) }}
</span>
<span class="text-xs text-gray-500">
<span class="text-xs text-gray-500 dark:text-gray-400">
剩余: {{ formatFileSize(Math.max(0, item.left_space)) }}
</span>
</div>
@@ -171,15 +172,19 @@
</template>
</n-virtual-list>
</div>
</n-card>
</template>
<!-- 分页 -->
<div class="flex justify-center">
<n-pagination v-model:page="currentPage" v-model:page-size="itemsPerPage" :item-count="filteredCksList.length"
:page-sizes="[10, 20, 50, 100]" show-size-picker @update:page="goToPage"
@update:page-size="(size) => { itemsPerPage = size; currentPage = 1; }" />
</div>
</div>
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination v-model:page="currentPage" v-model:page-size="itemsPerPage" :item-count="filteredCksList.length"
:page-sizes="[10, 20, 50, 100]" show-size-picker @update:page="goToPage"
@update:page-size="(size) => { itemsPerPage = size; currentPage = 1; }" />
</div>
</div>
</template>
</AdminPageLayout>
<!-- 创建/编辑账号模态框 -->
<n-modal :show="showCreateModal || showEditModal" preset="card" title="账号管理" style="width: 500px"

View File

@@ -1,24 +1,26 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题 -->
<template #page-header>
<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>
</template>
<!-- 配置表单 -->
<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">
<!-- 内容区 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="mb-6"
>
<n-tab-pane name="qq" tab="QQ机器人">
<div class="tab-content-container">
<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">
@@ -87,10 +89,8 @@
自动格式化搜索结果<br>
支持超时时间配置<br><br>
<strong>安装步骤</strong><br>
1. 下载插件文件<br>
2. 将插件放入 Astrobot plugins 目录<br>
3. 重启 Astrobot<br>
4. 在配置文件中添加插件配置
1. Astrbot 插件市场 搜索 urldb 安装<br>
2. 根据下面的配置信息配置插件
</p>
</div>
</div>
@@ -157,38 +157,47 @@
</div>
</div>
</div>
</div>
</div>
</n-tab-pane>
<n-tab-pane name="wechat" tab="微信公众号">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">微信公众号机器人功能正在开发中敬请期待</p>
<div class="tab-content-container">
<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>
</div>
</n-tab-pane>
<n-tab-pane name="telegram" tab="Telegram机器人">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">Telegram机器人功能正在开发中敬请期待</p>
<div class="tab-content-container">
<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>
</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 class="tab-content-container">
<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>
</div>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</n-tabs>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import AdminPageLayout from '~/components/AdminPageLayout.vue'
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
// 设置页面布局
@@ -278,5 +287,21 @@ onMounted(() => {
</script>
<style scoped>
/* 自定义样式 */
</style>
/* 机器人管理页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
/* tab内容容器 - 个别内容滚动 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>

View File

@@ -1,32 +1,22 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<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 type="primary" @click="showAddModal = true">
<template #icon>
<i class="fas fa-plus"></i>
</template>
添加分类
</n-button>
<n-button @click="refreshData">
<template #icon>
<i class="fas fa-refresh"></i>
</template>
刷新
</n-button>
</div>
</div>
</template>
<!-- 提示信息 -->
<n-alert title="分类用于对资源进行分类管理,可以关联多个标签" type="info" />
<!-- 提示信息区域 -->
<template #notice-section>
<n-alert title="分类用于对资源进行分类管理,可以关联多个标签" type="info" />
</template>
<!-- 搜索和操作 -->
<n-card>
<!-- 过滤栏 - 搜索和操作 -->
<template #filter-bar>
<div class="flex justify-between items-center">
<div class="flex gap-2">
<n-button @click="showAddModal = true" type="success">
@@ -38,9 +28,9 @@
</div>
<div class="flex gap-2">
<div class="relative">
<n-input
v-model:value="searchQuery"
@input="debounceSearch"
<n-input
v-model:value="searchQuery"
@input="debounceSearch"
type="text"
placeholder="搜索分类名称..."
clearable
@@ -58,21 +48,24 @@
</n-button>
</div>
</div>
</n-card>
</template>
<!-- 分类列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">分类列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个分类</span>
</div>
</template>
<!-- 内容区header - 分类列表标题 -->
<!-- <template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">分类列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个分类</span>
</div>
</template> -->
<div v-if="loading" class="flex items-center justify-center py-8">
<!-- 内容区 - 分类数据 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex h-full items-center justify-center py-8">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="categories.length === 0" class="text-center py-8">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
@@ -88,20 +81,38 @@
</n-button>
</div>
<div v-else>
<n-data-table
:columns="columns"
:data="categories"
:pagination="pagination"
:bordered="false"
:single-line="false"
:loading="loading"
@update:page="handlePageChange"
/>
</div>
</n-card>
<!-- 数据表格 -->
<div v-else class="flex flex-col h-full min-h-[600px]">
<!-- 数据表格容器自适应填充剩余高度 -->
<div class="flex-1 overflow-hidden">
<n-data-table
:columns="columns"
:data="categories"
:pagination="false"
:bordered="false"
:single-line="false"
:loading="loading"
:scroll-x="800"
class="h-full"
/>
</div>
<!-- 添加/编辑分类模态框 -->
<!-- 分页组件外部显示 -->
<div class="mt-4 flex justify-center border-t pt-4">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="total"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
@update:page="handlePageChange"
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
/>
</div>
</div>
</template>
</AdminPageLayout>
<!-- 添加/编辑分类模态框 -->
<n-modal v-model:show="showAddModal" preset="card" :title="editingCategory ? '编辑分类' : '添加分类'" style="width: 500px">
<n-form
ref="formRef"
@@ -147,7 +158,6 @@
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
@@ -288,23 +298,7 @@ const columns = [
}
]
// 分页配置
const pagination = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
itemCount: total.value,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page: number) => {
currentPage.value = page
fetchData()
},
onUpdatePageSize: (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchData()
}
}))
// 分页配置已经移到模板中外部显示
// 获取数据
const fetchData = async () => {

View File

@@ -1,11 +1,15 @@
<template>
<div class="p-6">
<div class="mb-6">
<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>
</div>
<AdminPageLayout>
<!-- 页面头部 - 标题 -->
<template #page-header>
<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>
</template>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<!-- 内容区 -->
<template #content>
<div class="text-center py-12">
<div class="text-gray-400 dark:text-gray-500 mb-4">
<i class="fas fa-upload text-4xl"></i>
@@ -13,11 +17,13 @@
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能开发中</h3>
<p class="text-gray-500 dark:text-gray-400">数据推送功能正在开发中敬请期待...</p>
</div>
</div>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// 数据推送管理页面
definePageMeta({
layout: 'admin',

View File

@@ -1,35 +1,52 @@
<template>
<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">管理资源转存任务和状态</p>
</div>
<AdminPageLayout>
<!-- 页面头部 - 标题 -->
<template #page-header>
<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>
</template>
<!-- 主要内容 -->
<n-card>
<n-tabs v-model:value="activeTab" type="line" animated>
<!-- 手动批量转存 -->
<n-tab-pane name="manual" tab="手动批量转存">
<AdminManualBatchTransfer />
</n-tab-pane>
<!-- 内容 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="mb-6"
>
<!-- 手动批量转存 -->
<n-tab-pane name="manual" tab="手动批量转存">
<div class="tab-content-container">
<AdminManualBatchTransfer />
</div>
</n-tab-pane>
<!-- 已转存列表 -->
<n-tab-pane name="transferred" tab="已转存列表">
<AdminTransferredList ref="transferredListRef" />
</n-tab-pane>
<!-- 已转存列表 -->
<n-tab-pane name="transferred" tab="已转存列表">
<div class="tab-content-container">
<AdminTransferredList ref="transferredListRef" />
</div>
</n-tab-pane>
<!-- 未转存列表 -->
<n-tab-pane name="untransferred" tab="未转存列表">
<AdminUntransferredList ref="untransferredListRef" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
<!-- 未转存列表 -->
<n-tab-pane name="untransferred" tab="未转存列表">
<div class="tab-content-container">
<AdminUntransferredList ref="untransferredListRef" />
</div>
</n-tab-pane>
</n-tabs>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// 页面配置
definePageMeta({
@@ -43,4 +60,24 @@ const activeTab = ref('manual')
// 组件引用
const transferredListRef = ref(null)
const untransferredListRef = ref(null)
</script>
</script>
<style scoped>
/* 数据转存管理页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
/* tab内容容器 - 个别内容滚动 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和保存按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">开发配置</h1>
<p class="text-gray-600 dark:text-gray-400">管理API和开发相关配置</p>
@@ -12,11 +12,11 @@
</template>
保存配置
</n-button>
</div>
</template>
<!-- 配置表单 -->
<n-card>
<div class="space-y-6">
<!-- 内容区 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- API Token -->
<div>
<n-form-item label="公开API访问令牌" path="api_token">
@@ -71,12 +71,13 @@
</n-button>
</div>
</div>
</n-card>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// 设置页面布局
definePageMeta({
@@ -332,4 +333,13 @@ onMounted(() => {
<style scoped>
/* 自定义样式 */
</style>
.config-content {
padding: 1rem;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
</style>

View File

@@ -1,36 +1,36 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout :is-sub-page="true">
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">失败资源列表</h1>
<p class="text-gray-600 dark:text-gray-400">显示处理失败的资源包含错误信息</p>
</div>
<div class="flex space-x-3">
<n-button
@click="retryAllFailed"
:disabled="selectedResources.length === 0 || isProcessing"
:type="selectedResources.length > 0 && !isProcessing ? 'success' : 'default'"
:loading="isProcessing"
>
<template #icon>
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
<i v-else class="fas fa-redo"></i>
</template>
{{ isProcessing ? '处理中...' : `重新放入待处理池 (${selectedResources.length})` }}
</n-button>
<n-button
@click="clearAllErrors"
:disabled="selectedResources.length === 0 || isProcessing"
:type="selectedResources.length > 0 && !isProcessing ? 'warning' : 'default'"
:loading="isProcessing"
>
<template #icon>
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
<i v-else class="fas fa-trash"></i>
</template>
{{ isProcessing ? '处理中...' : `删除失败资源 (${selectedResources.length})` }}
</n-button>
<n-button
@click="retryAllFailed"
:disabled="selectedResources.length === 0 || isProcessing"
:type="selectedResources.length > 0 && !isProcessing ? 'success' : 'default'"
:loading="isProcessing"
>
<template #icon>
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
<i v-else class="fas fa-redo"></i>
</template>
{{ isProcessing ? '处理中...' : `重新放入待处理池 (${selectedResources.length})` }}
</n-button>
<n-button
@click="clearAllErrors"
:disabled="selectedResources.length === 0 || isProcessing"
:type="selectedResources.length > 0 && !isProcessing ? 'warning' : 'default'"
:loading="isProcessing"
>
<template #icon>
<i v-if="isProcessing" class="fas fa-spinner fa-spin"></i>
<i v-else class="fas fa-trash"></i>
</template>
{{ isProcessing ? '处理中...' : `删除失败资源 (${selectedResources.length})` }}
</n-button>
<n-button @click="refreshData" type="info">
<template #icon>
<i class="fas fa-refresh"></i>
@@ -38,189 +38,194 @@
刷新
</n-button>
</div>
</div>
</template>
<!-- 搜索和筛选 -->
<n-card>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<n-select
v-model:value="errorFilter"
placeholder="选择状态"
:options="statusOptions"
clearable
/>
<n-button type="primary" @click="handleSearch" class="w-full md:w-auto md:min-w-[100px]">
<template #icon>
<i class="fas fa-search"></i>
<!-- 过滤栏 - 搜索和筛选 -->
<template #filter-bar>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<div class="flex flex-col md:flex-row gap-4">
<n-select
v-model:value="errorFilter"
placeholder="选择状态"
:options="statusOptions"
clearable
/>
<n-button type="primary" @click="handleSearch" class="w-full md:w-auto md:min-w-[100px]">
<template #icon>
<i class="fas fa-search"></i>
</template>
搜索
</n-button>
</div>
</div>
</template>
<!-- 内容区header - 失败资源列表头部 -->
<template #content-header>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-lg font-semibold">失败资源列表</span>
<n-checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@update:checked="toggleSelectAll"
>
全选
</n-checkbox>
</div>
<div class="text-sm text-gray-500">
<span> {{ totalCount }} 个资源已选择 {{ selectedResources.length }} </span>
</div>
</div>
</template>
<!-- 内容区content - 失败资源列表 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large">
<template #description>
<span class="text-gray-500">加载中...</span>
</template>
搜索
</n-button>
</div>
</n-card>
</n-spin>
</div>
<!-- 失败资源列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-lg font-semibold">失败资源列表</span>
<n-checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@update:checked="toggleSelectAll"
>
全选
</n-checkbox>
</div>
<div class="text-sm text-gray-500">
<span class="text-sm text-gray-500"> {{ totalCount }} 个资源已选择 {{ selectedResources.length }} </span>
</div>
</div>
</template>
<!-- 虚拟列表 -->
<div v-else-if="failedResources.length > 0">
<n-virtual-list
:items="failedResources"
:item-size="120"
:item-resizable="true"
style="max-height: 600px"
>
<template #default="{ item }">
<div class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center justify-between">
<!-- 左侧信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-4">
<!-- 复选框 -->
<n-checkbox
:checked="selectedResources.includes(item.id)"
@update:checked="(checked) => {
if (checked) {
selectedResources.push(item.id)
} else {
const index = selectedResources.indexOf(item.id)
if (index > -1) {
selectedResources.splice(index, 1)
}
}
}"
/>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large">
<template #description>
<span class="text-gray-500">加载中...</span>
</template>
</n-spin>
</div>
<!-- ID -->
<div class="w-16 text-sm font-medium text-gray-900 dark:text-gray-100">
#{{ item.id }}
</div>
<!-- 虚拟列表 -->
<n-virtual-list
v-if="!loading"
:items="failedResources"
:item-size="80"
:item-resizable="true"
style="max-height: 400px"
container-style="height: 600px;"
>
<template #default="{ item }">
<div class="border-b border-gray-200 dark:border-gray-700 p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-center justify-between">
<!-- 左侧信息 -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-4">
<!-- 复选框 -->
<n-checkbox
:checked="selectedResources.includes(item.id)"
@update:checked="(checked) => {
if (checked) {
selectedResources.push(item.id)
} else {
const index = selectedResources.indexOf(item.id)
if (index > -1) {
selectedResources.splice(index, 1)
}
}
}"
/>
<!-- ID -->
<div class="w-16 text-sm font-medium text-gray-900 dark:text-gray-100">
#{{ item.id }}
</div>
<!-- 标题 -->
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1" :title="item.title || '未设置'">
{{ item.title || '未设置' }}
</h3>
</div>
</div>
<!-- 错误信息 -->
<div class="mt-2 flex items-center space-x-2">
<!-- 标题 -->
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1" :title="item.title || '未设置'">
{{ item.title || '未设置' }}
</h3>
</div>
</div>
<!-- 错误信息 -->
<div class="mt-2 flex items-center space-x-2">
<p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-1 mt-1" :title="item.url">
<a
:href="checkUrlSafety(item.url)"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
>
{{ item.url }}
</a>
</p>
<n-tag type="error" size="small" :title="item.error_msg">
{{ truncateError(item.error_msg) }}
</n-tag>
</div>
<!-- 底部信息 -->
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
<span>创建时间: {{ formatTime(item.create_time) }}</span>
<span>IP: {{ item.ip || '-' }}</span>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center space-x-2 ml-4">
<n-button
size="small"
type="success"
@click="retryResource(item.id)"
title="重试此资源"
>
<template #icon>
<i class="fas fa-redo"></i>
</template>
</n-button>
<n-button
size="small"
type="warning"
@click="clearError(item.id)"
title="清除错误信息"
>
<template #icon>
<i class="fas fa-broom"></i>
</template>
</n-button>
<n-button
size="small"
type="error"
@click="deleteResource(item.id)"
title="删除此资源"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
</div>
</template>
</n-virtual-list>
<a
:href="checkUrlSafety(item.url)"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline"
>
{{ item.url }}
</a>
</p>
<n-tag type="error" size="small" :title="item.error_msg">
{{ truncateError(item.error_msg) }}
</n-tag>
</div>
<!-- 空状态 -->
<div v-if="!loading && failedResources.length === 0" class="flex flex-col items-center justify-center py-12">
<n-empty description="暂无失败资源">
<template #icon>
<i class="fas fa-check-circle text-4xl text-green-500"></i>
</template>
<template #extra>
<span class="text-sm text-gray-500">所有资源处理成功</span>
</template>
</n-empty>
</div>
<!-- 底部信息 -->
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
<span>创建时间: {{ formatTime(item.create_time) }}</span>
<span>IP: {{ item.ip || '-' }}</span>
</div>
</div>
<!-- 分页 -->
<div class="mt-6 flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="totalCount"
:page-sizes="[100, 200, 500, 1000]"
show-size-picker
@update:page="fetchData"
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
/>
</div>
</n-card>
</div>
<!-- 右侧操作按钮 -->
<div class="flex items-center space-x-2 ml-4">
<n-button
size="small"
type="success"
@click="retryResource(item.id)"
title="重试此资源"
>
<template #icon>
<i class="fas fa-redo"></i>
</template>
</n-button>
<n-button
size="small"
type="warning"
@click="clearError(item.id)"
title="清除错误信息"
>
<template #icon>
<i class="fas fa-broom"></i>
</template>
</n-button>
<n-button
size="small"
type="error"
@click="deleteResource(item.id)"
title="删除此资源"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
</div>
</template>
</n-virtual-list>
</div>
<!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-12">
<n-empty description="暂无失败资源">
<template #icon>
<i class="fas fa-check-circle text-4xl text-green-500"></i>
</template>
<template #extra>
<span class="text-sm text-gray-500">所有资源处理成功</span>
</template>
</n-empty>
</div>
</template>
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="totalCount"
:page-sizes="[100, 200, 500, 1000]"
show-size-picker
@update:page="fetchData"
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
/>
</div>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和保存按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">功能配置</h1>
<p class="text-gray-600 dark:text-gray-400">管理系统功能开关和参数设置</p>
@@ -12,10 +12,11 @@
</template>
保存配置
</n-button>
</div>
</template>
<!-- 配置表单 -->
<n-card>
<!-- 内容区 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
@@ -24,15 +25,15 @@
class="mb-6"
>
<n-tab-pane name="resource" tab="资源处理">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="tab-content-container">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="space-y-8">
<!-- 自动处理配置组 -->
<div class="space-y-4">
@@ -166,19 +167,20 @@
</div>
</div>
</div>
</n-form>
</n-form>
</div>
</n-tab-pane>
<n-tab-pane name="transfer" tab="转存配置">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="tab-content-container">
<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">
@@ -243,19 +245,20 @@
/>
</div>
</div>
</n-form>
</n-form>
</div>
</n-tab-pane>
<n-tab-pane name="drama" tab="热播剧">
<n-form
ref="formRef"
:model="configForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<div class="tab-content-container">
<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">
@@ -266,17 +269,20 @@
<n-switch v-model:value="configForm.hot_drama_auto_fetch" />
</div>
</div>
</n-form>
</n-form>
</div>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</n-tabs>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useNotification } from 'naive-ui'
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// 设置页面布局
definePageMeta({
@@ -537,4 +543,20 @@ onMounted(() => {
<style scoped>
/* 自定义样式 */
</style>
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
/* tab内容容器 - 个别内容滚动 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">文件管理</h1>
<p class="text-gray-600 dark:text-gray-400">管理系统中的上传文件</p>
@@ -20,13 +20,15 @@
刷新
</n-button>
</div>
</div>
</template>
<!-- 提示信息 -->
<n-alert title="支持图片格式文件最大文件大小5MB" type="info" />
<!-- 提示信息区域 -->
<template #notice-section>
<n-alert title="支持图片格式文件最大文件大小5MB" type="info" />
</template>
<!-- 搜索和筛选 -->
<n-card>
<!-- 过滤栏 - 搜索功能 -->
<template #filter-bar>
<div class="flex gap-4">
<n-input
v-model:value="searchKeyword"
@@ -39,7 +41,7 @@
<i class="fas fa-search"></i>
</template>
</n-input>
<n-button type="primary" @click="handleSearch" class="w-20">
<template #icon>
<i class="fas fa-search"></i>
@@ -47,21 +49,24 @@
搜索
</n-button>
</div>
</n-card>
</template>
<!-- 文件列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">文件列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个文件</span>
</div>
</template>
<!-- 内容区header - 文件列表标题 -->
<!-- <template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">文件列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个文件</span>
</div>
</template> -->
<div v-if="loading" class="flex items-center justify-center py-8">
<!-- 内容区 - 文件列表 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex h-full items-center justify-center py-8">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="fileList.length === 0" class="text-center py-8">
<i class="fas fa-file-upload text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-500">暂无文件数据</p>
@@ -73,99 +78,106 @@
</n-button>
</div>
<div v-else>
<!-- 图片预览区域 -->
<div class="image-preview-container">
<n-image-group>
<div class="image-grid">
<div
v-for="file in fileList"
:key="file.id"
class="image-item"
:class="{ 'is-image': isImageFile(file) }"
>
<!-- 图片文件显示预览 -->
<div v-if="isImageFile(file)" class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600">
<div class="image-preview relative">
<n-image
:src="getImageUrl(file.access_url)"
:alt="file.original_name"
:lazy="false"
object-fit="cover"
class="preview-image rounded"
@error="handleImageError"
@load="handleImageLoad"
/>
<div class="delete-button">
<n-button
size="small"
type="error"
circle
@click="confirmDelete(file)"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
<div class="image-info mt-2">
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ file.original_name }}
</div>
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(file.file_size) }}
</div>
</div>
</div>
<!-- 非图片文件显示图标 -->
<div v-else class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600 relative">
<div class="file-icon">
<i :class="getFileIconClass(file.file_type)"></i>
</div>
<div class="file-info">
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ file.original_name }}
</div>
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(file.file_size) }}
</div>
</div>
<div class="delete-button">
<n-button
size="small"
type="error"
circle
@click="confirmDelete(file)"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
<!-- 文件网格和分页容器 -->
<div v-else class="flex flex-col h-full">
<!-- 文件网格区域 - 自适应高度 -->
<div class="flex-1 overflow-auto">
<div class="file-list-container">
<n-image-group>
<div class="image-grid">
<div
v-for="file in fileList"
:key="file.id"
class="image-item"
:class="{ 'is-image': isImageFile(file) }"
>
<!-- 图片文件显示预览 -->
<div v-if="isImageFile(file)" class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600">
<div class="image-preview relative">
<n-image
:src="getImageUrl(file.access_url)"
:alt="file.original_name"
:lazy="false"
object-fit="cover"
class="preview-image rounded"
@error="handleImageError"
@load="handleImageLoad"
/>
<div class="delete-button">
<n-button
size="small"
type="error"
circle
@click="confirmDelete(file)"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
<div class="image-info mt-2">
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ file.original_name }}
</div>
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(file.file_size) }}
</div>
</div>
</div>
<!-- 非图片文件显示图标 -->
<div v-else class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600 relative">
<div class="file-icon">
<i :class="getFileIconClass(file.file_type)"></i>
</div>
<div class="file-info">
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ file.original_name }}
</div>
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
{{ formatFileSize(file.file_size) }}
</div>
</div>
<div class="delete-button">
<n-button
size="small"
type="error"
circle
@click="confirmDelete(file)"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
</n-button>
</div>
</div>
</div>
</div>
</div>
</n-image-group>
<!-- 分页 -->
<div class="pagination-wrapper">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-count="Math.ceil(pagination.total / pagination.pageSize)"
:page-sizes="pagination.pageSizes"
show-size-picker
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</n-image-group>
</div>
</div>
</div>
</n-card>
</template>
<!-- 上传模态框 -->
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.total"
:page-sizes="[100, 200, 500, 1000]"
show-size-picker
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
</template>
</AdminPageLayout>
<!-- 上传模态框 -->
<n-modal v-model:show="showUploadModal" preset="card" title="上传文件" style="width: 800px" @update:show="handleModalClose">
<FileUpload ref="fileUploadRef" :key="uploadModalKey" />
<template #footer>
@@ -191,7 +203,6 @@
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
@@ -479,12 +490,22 @@ onMounted(() => {
<style scoped>
/* 文件管理页面样式 */
.file-list-container {
/* 容器样式将替换原来的n-card背景 */
padding: 1rem;
background-color: var(--color-white, #ffffff);
}
/* 暗色主题支持 */
.dark .file-list-container {
background-color: var(--color-dark-bg, #1f2937);
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
max-height: 400px;
height: 100%;
overflow-y: auto;
}
@@ -568,27 +589,26 @@ onMounted(() => {
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 1rem;
/* 由于分页已移到外部,这里的样式不再需要 */
/* 分页现在直接使用 AdminPageLayout 的 content-footer */
}
/* 滚动条样式 */
.image-preview-container::-webkit-scrollbar {
/* 滚动条样式 - 更新为新的容器类名 */
.image-grid::-webkit-scrollbar {
width: 6px;
}
.image-preview-container::-webkit-scrollbar-track {
.image-grid::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.image-preview-container::-webkit-scrollbar-thumb {
.image-grid::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.image-preview-container::-webkit-scrollbar-thumb:hover {
.image-grid::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>

View File

@@ -1,13 +1,13 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<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="navigateTo('/admin/failed-resources')" type="error">
<n-button @click="navigateTo('/admin/failed-resources')" type="tertiary">
<template #icon>
<i class="fas fa-exclamation-triangle"></i>
</template>
@@ -19,98 +19,48 @@
</template>
刷新
</n-button>
</div>
</div>
<!-- 自动处理配置状态 -->
<n-card>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<i class="fas fa-cog text-gray-600 dark:text-gray-400"></i>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">自动处理配置</span>
</div>
<div class="flex items-center space-x-2">
<div
:class="[
'w-3 h-3 rounded-full',
systemConfig?.auto_process_ready_resources
? 'bg-green-500 animate-pulse'
: 'bg-red-500'
]"
></div>
<span
:class="[
'text-sm font-medium',
systemConfig?.auto_process_ready_resources
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
]"
>
{{ systemConfig?.auto_process_ready_resources ? '已开启' : '已关闭' }}
</span>
</div>
</div>
<div class="flex items-center space-x-3">
<div class="text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1"></i>
{{ systemConfig?.auto_process_ready_resources
? '系统会自动处理待处理资源并入库'
: '需要手动处理待处理资源'
}}
</div>
<!-- <n-button
@click="refreshConfig"
:disabled="updatingConfig"
size="small"
type="tertiary"
title="刷新配置"
>
<template #icon>
<i class="fas fa-sync-alt"></i>
</template>
</n-button> -->
<n-button
<n-button
@click="toggleAutoProcess"
:disabled="updatingConfig"
:type="systemConfig?.auto_process_ready_resources ? 'error' : 'success'"
size="small"
>
<template #icon>
<i v-if="updatingConfig" class="fas fa-spinner fa-spin"></i>
<i v-else :class="systemConfig?.auto_process_ready_resources ? 'fas fa-pause' : 'fas fa-play'"></i>
</template>
{{ systemConfig?.auto_process_ready_resources ? '关闭' : '开启' }}
{{ systemConfig?.auto_process_ready_resources ? '关闭自动处理' : '开启自动处理' }}
</n-button>
</div>
</template>
<!-- 内容区header - 资源列表头部 -->
<template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">待处理资源列表</span>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500"> {{ totalCount }} 个待处理资源</span>
<n-button
@click="clearAll"
type="error"
size="small"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
清空全部
</n-button>
</div>
</div>
</n-card>
<!-- 资源列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">待处理资源列表</span>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500"> {{ totalCount }} 个待处理资源</span>
<n-button
@click="clearAll"
type="error"
size="small"
>
<template #icon>
<i class="fas fa-trash"></i>
</template>
清空全部
</n-button>
</div>
</div>
</template>
</template>
<!-- 内容区content - 资源列表 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-8">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="readyResources.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>
@@ -125,6 +75,7 @@
</div>
</div>
<!-- 数据表格 -->
<div v-else>
<n-data-table
:columns="columns"
@@ -136,8 +87,8 @@
@update:page="handlePageChange"
/>
</div>
</n-card>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">资源管理</h1>
<p class="text-gray-600 dark:text-gray-400">管理系统中的所有资源</p>
@@ -26,93 +26,97 @@
刷新
</n-button>
</div>
</div>
</template>
<!-- 搜索和筛选 -->
<n-card>
<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="selectedPlatform"
placeholder="选择平台"
:options="platformOptions"
clearable
/>
<n-button type="primary" @click="handleSearch" class="w-20">
<template #icon>
<i class="fas fa-search"></i>
</template>
搜索
</n-button>
</div>
</n-card>
<!-- 过滤栏 - 搜索和筛选 -->
<template #filter-bar>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-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-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>
<n-select
v-model:value="selectedCategory"
placeholder="选择分类"
:options="categoryOptions"
clearable
/>
<n-select
v-model:value="selectedPlatform"
placeholder="选择平台"
:options="platformOptions"
clearable
/>
<n-button type="primary" @click="handleSearch" class="w-20">
<template #icon>
<i class="fas fa-search"></i>
</template>
搜索
</n-button>
</div>
</template>
</div>
</template>
<div v-if="loading" class="flex items-center justify-center py-8">
<!-- 内容区header - 资源列表头部 -->
<template #content-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 dark:text-gray-400">全选</span>
</div>
</div>
<span class="text-sm text-gray-500 dark:text-gray-400"> {{ total }} 个资源已选择 {{ selectedResources.length }} </span>
</div>
</template>
<!-- 内容区content - 资源列表 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large" />
</div>
<div v-else-if="resources.length === 0" class="text-center py-8">
<!-- 空状态 -->
<div v-else-if="resources.length === 0" class="flex flex-col items-center justify-center py-12">
<i class="fas fa-inbox text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-500">暂无资源数据</p>
<p class="text-gray-500 dark:text-gray-400">暂无资源数据</p>
</div>
<div v-else>
<!-- 虚拟列表 -->
<!-- 虚拟列表容器 -->
<div v-else class="flex-1 h-full overflow-hidden">
<n-virtual-list
:items="resources"
:item-size="100"
style="max-height: 400px"
container-style="height: 600px;"
class="h-full"
>
<template #default="{ item: resource }">
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors mb-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center space-x-2 mb-2">
<n-checkbox
:value="resource.id"
<n-checkbox
:value="resource.id"
:checked="selectedResources.includes(resource.id)"
@update:checked="(checked) => toggleResourceSelection(resource.id, checked)"
/>
<span class="text-sm text-gray-500">{{ resource.id }}</span>
<span class="text-sm text-gray-500 dark:text-gray-400">{{ resource.id }}</span>
<span v-if="resource.pan_id" class="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded flex-shrink-0">
{{ getPlatformName(resource.pan_id) }}
</span>
@@ -122,14 +126,14 @@
<span v-if="resource.category_id" class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded flex-shrink-0">
{{ getCategoryName(resource.category_id) }}
</span>
</div>
<p v-if="resource.description" class="text-gray-600 dark:text-gray-400 mb-2 line-clamp-2">
{{ resource.description }}
</p>
<div class="flex items-center space-x-4 text-sm text-gray-500">
<div class="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>
<i class="fas fa-link mr-1"></i>
{{ resource.url }}
@@ -150,9 +154,9 @@
<div v-if="resource.tags && resource.tags.length > 0" class="mt-2">
<div class="flex flex-wrap gap-1">
<span
v-for="tag in resource.tags"
:key="tag.id"
<span
v-for="tag in resource.tags"
:key="tag.id"
class="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded"
>
{{ tag.name }}
@@ -160,7 +164,7 @@
</div>
</div>
</div>
<div class="flex items-center space-x-2 ml-4">
<!-- <n-button size="small" type="primary" @click="editResource(resource)">
<template #icon>
@@ -179,9 +183,13 @@
</div>
</template>
</n-virtual-list>
</div>
</template>
<!-- 分页 -->
<div class="mt-6">
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
@@ -193,104 +201,107 @@
/>
</div>
</div>
</n-card>
</template>
</AdminPageLayout>
<!-- 批量操作模态框 -->
<n-modal v-model:show="showBatchModal" preset="card" title="批量操作" style="width: 600px">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<span class="font-medium">已选择 {{ selectedResources.length }} 个资源</span>
<p class="text-sm text-gray-500 mt-1">
{{ isAllSelected ? '已全选当前页面' : isIndeterminate ? '部分选中' : '未选择' }}
</p>
</div>
<n-button size="small" @click="clearSelection">清空选择</n-button>
</div>
<div class="grid grid-cols-2 gap-4">
<n-button type="error" @click="batchDelete" :disabled="selectedResources.length === 0">
<template #icon>
<i class="fas fa-trash"></i>
</template>
批量删除
</n-button>
<n-button type="warning" @click="batchUpdate" :disabled="selectedResources.length === 0">
<template #icon>
<i class="fas fa-edit"></i>
</template>
批量更新
</n-button>
<!-- 模态框 - 在AdminPageLayout外部 -->
<!-- 批量操作模态框 -->
<n-modal v-model:show="showBatchModal" preset="card" title="批量操作" style="width: 600px">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<span class="font-medium">已选择 {{ selectedResources.length }} 个资源</span>
<p class="text-sm text-gray-500 mt-1">
{{ isAllSelected ? '已全选当前页面' : isIndeterminate ? '部分选中' : '未选择' }}
</p>
</div>
<n-button size="small" @click="clearSelection">清空选择</n-button>
</div>
</n-modal>
<!-- 编辑资源模态框 -->
<n-modal v-model:show="showEditModal" preset="card" title="编辑资源" style="width: 600px">
<n-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<n-form-item label="标题" path="title">
<n-input v-model:value="editForm.title" placeholder="请输入资源标题" />
</n-form-item>
<div class="grid grid-cols-2 gap-4">
<n-button type="error" @click="batchDelete" :disabled="selectedResources.length === 0">
<template #icon>
<i class="fas fa-trash"></i>
</template>
批量删除
</n-button>
<n-button type="warning" @click="batchUpdate" :disabled="selectedResources.length === 0">
<template #icon>
<i class="fas fa-edit"></i>
</template>
批量更新
</n-button>
</div>
</div>
</n-modal>
<n-form-item label="描述" path="description">
<n-input
v-model:value="editForm.description"
type="textarea"
placeholder="请输入资源描述"
:rows="3"
/>
</n-form-item>
<!-- 编辑资源模态框 -->
<n-modal v-model:show="showEditModal" preset="card" title="编辑资源" style="width: 600px">
<n-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<n-form-item label="标题" path="title">
<n-input v-model:value="editForm.title" placeholder="请输入资源标题" />
</n-form-item>
<n-form-item label="URL" path="url">
<n-input v-model:value="editForm.url" placeholder="请输入资源链接" />
</n-form-item>
<n-form-item label="描述" path="description">
<n-input
v-model:value="editForm.description"
type="textarea"
placeholder="请输入资源描述"
:rows="3"
/>
</n-form-item>
<n-form-item label="分类" path="category_id">
<n-select
v-model:value="editForm.category_id"
:options="categoryOptions"
placeholder="请选择分类"
clearable
/>
</n-form-item>
<n-form-item label="URL" path="url">
<n-input v-model:value="editForm.url" placeholder="请输入资源链接" />
</n-form-item>
<n-form-item label="平台" path="pan_id">
<n-select
v-model:value="editForm.pan_id"
:options="platformOptions"
placeholder="请选择平台"
clearable
/>
</n-form-item>
<n-form-item label="分类" path="category_id">
<n-select
v-model:value="editForm.category_id"
:options="categoryOptions"
placeholder="请选择分类"
clearable
/>
</n-form-item>
<n-form-item label="标签" path="tag_ids">
<n-select
v-model:value="editForm.tag_ids"
:options="tagOptions"
placeholder="请选择标签"
multiple
clearable
/>
</n-form-item>
</n-form>
<n-form-item label="平台" path="pan_id">
<n-select
v-model:value="editForm.pan_id"
:options="platformOptions"
placeholder="请选择平台"
clearable
/>
</n-form-item>
<n-form-item label="标签" path="tag_ids">
<n-select
v-model:value="editForm.tag_ids"
:options="tagOptions"
placeholder="请选择标签"
multiple
clearable
/>
</n-form-item>
</n-form>
<template #footer>
<div class="flex justify-end space-x-3">
<n-button @click="showEditModal = false">取消</n-button>
<n-button type="primary" @click="handleEditSubmit" :loading="editing">
保存
</n-button>
</div>
</template>
</n-modal>
<template #footer>
<div class="flex justify-end space-x-3">
<n-button @click="showEditModal = false">取消</n-button>
<n-button type="primary" @click="handleEditSubmit" :loading="editing">
保存
</n-button>
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,14 +1,21 @@
<template>
<div class="p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">SEO管理</h1>
<p class="text-gray-600 dark:text-gray-400 mt-2">搜索引擎优化管理</p>
</div>
<AdminPageLayout>
<!-- 页面头部 - 标题 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">SEO管理</h1>
<p class="text-gray-600 dark:text-gray-400">搜索引擎优化管理</p>
</div>
</template>
<!-- Tab导航 -->
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="site-submit" tab="站点提交">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<!-- 内容区 -->
<template #content>
<div class="config-content h-full">
<!-- Tab导航 -->
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="site-submit" tab="站点提交">
<div class="tab-content-container">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">站点提交待开发</h3>
<p class="text-gray-600 dark:text-gray-400">向各大搜索引擎提交站点信息</p>
@@ -181,94 +188,101 @@
</div>
</div>
</div>
</n-tab-pane>
</div>
</n-tab-pane>
<n-tab-pane name="link-building" tab="外链建设">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">外链建设待开发</h3>
<p class="text-gray-600 dark:text-gray-400">管理和监控外部链接建设情况</p>
</div>
<n-tab-pane name="link-building" tab="外链建设">
<div class="tab-content-container">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">外链建设待开发</h3>
<p class="text-gray-600 dark:text-gray-400">管理和监控外部链接建设情况</p>
</div>
<!-- 外链统计 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<i class="fas fa-link text-blue-600 dark:text-blue-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">总外链数</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.total }}</p>
</div>
<!-- 外链统计 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<i class="fas fa-link text-blue-600 dark:text-blue-400"></i>
</div>
</div>
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
<i class="fas fa-check text-green-600 dark:text-green-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">有效外链</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.valid }}</p>
</div>
</div>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<i class="fas fa-clock text-yellow-600 dark:text-yellow-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">待审核</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.pending }}</p>
</div>
</div>
</div>
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
<i class="fas fa-times text-red-600 dark:text-red-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">失效外链</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.invalid }}</p>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">总外链数</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.total }}</p>
</div>
</div>
</div>
<!-- 外链列表 -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">外链列表</h4>
<n-button type="primary" @click="addNewLink">
<template #icon>
<i class="fas fa-plus"></i>
</template>
添加外链
</n-button>
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
<i class="fas fa-check text-green-600 dark:text-green-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">有效外链</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.valid }}</p>
</div>
</div>
</div>
<n-data-table
:columns="linkColumns"
:data="linkList"
:pagination="linkPagination"
:loading="linkLoading"
:bordered="false"
striped
/>
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<i class="fas fa-clock text-yellow-600 dark:text-yellow-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">待审核</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.pending }}</p>
</div>
</div>
</div>
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<div class="flex items-center">
<div class="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
<i class="fas fa-times text-red-600 dark:text-red-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600 dark:text-gray-400">失效外链</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ linkStats.invalid }}</p>
</div>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
<!-- 外链列表 -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h4 class="text-lg font-medium text-gray-900 dark:text-white">外链列表</h4>
<n-button type="primary" @click="addNewLink">
<template #icon>
<i class="fas fa-plus"></i>
</template>
添加外链
</n-button>
</div>
<n-data-table
:columns="linkColumns"
:data="linkList"
:pagination="linkPagination"
:loading="linkLoading"
:bordered="false"
striped
/>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// SEO管理页面
definePageMeta({
layout: 'admin'
@@ -493,4 +507,23 @@ const deleteLink = (row: any) => {
onMounted(() => {
loadLinkList()
})
</script>
</script>
<style scoped>
/* SEO管理页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和保存按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">站点配置</h1>
<p class="text-gray-600 dark:text-gray-400">管理网站基本信息和设置</p>
@@ -12,172 +12,174 @@
</template>
保存配置
</n-button>
</div>
</template>
<!-- 配置表单 -->
<n-card>
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="mb-6"
>
<n-tab-pane name="basic" 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-input
v-model:value="configForm.site_title"
placeholder="请输入网站标题"
/>
</div>
<!-- 网站描述 -->
<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">网站的简要介绍用于SEO和社交媒体分享</span>
</div>
<n-input
v-model:value="configForm.site_description"
placeholder="请输入网站描述"
/>
</div>
<!-- 关键词 -->
<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">用于SEO优化多个关键词用逗号分隔</span>
</div>
<n-input
v-model:value="configForm.keywords"
placeholder="请输入关键词,用逗号分隔"
/>
</div>
<!-- 网站Logo -->
<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">网站Logo</label>
<span class="text-xs text-gray-500 dark:text-gray-400">选择网站Logo图片建议使用正方形图片</span>
</div>
<div class="flex items-center space-x-4">
<div v-if="configForm.site_logo" class="flex-shrink-0">
<n-image
:src="getImageUrl(configForm.site_logo)"
alt="网站Logo"
width="80"
height="80"
object-fit="cover"
class="rounded-lg border"
/>
</div>
<div class="flex-1">
<n-button type="primary" @click="openLogoSelector">
<template #icon>
<i class="fas fa-image"></i>
</template>
{{ configForm.site_logo ? '更换Logo' : '选择Logo' }}
</n-button>
<n-button v-if="configForm.site_logo" @click="clearLogo" class="ml-2">
<template #icon>
<i class="fas fa-times"></i>
</template>
清除
</n-button>
</div>
</div>
</div>
<!-- 版权信息 -->
<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-input
v-model:value="configForm.copyright"
placeholder="请输入版权信息"
/>
</div>
</div>
</n-form>
</n-tab-pane>
<n-tab-pane name="security" 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.maintenance_mode" />
</div>
<!-- 违禁词 -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<!-- 内容区 - 配置表单 -->
<template #content>
<div class="config-content h-full">
<!-- 顶部Tabs -->
<n-tabs
v-model:value="activeTab"
type="line"
animated
>
<n-tab-pane name="basic" tab="基本信息">
<div class="tab-content-container">
<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>
<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>
<a
href="https://raw.githubusercontent.com/ctwj/urldb/refs/heads/main/db/forbidden.txt"
target="_blank"
class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"
>
开源违禁词
</a>
<n-input
v-model:value="configForm.site_title"
placeholder="请输入网站标题"
/>
</div>
<n-input
v-model:value="configForm.forbidden_words"
placeholder="请输入违禁词,用逗号分隔"
type="textarea"
:rows="4"
/>
</div>
<!-- 开启注册 -->
<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 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">网站的简要介绍用于SEO和社交媒体分享</span>
</div>
<n-input
v-model:value="configForm.site_description"
placeholder="请输入网站描述"
/>
</div>
<!-- 关键词 -->
<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">用于SEO优化多个关键词用逗号分隔</span>
</div>
<n-input
v-model:value="configForm.keywords"
placeholder="请输入关键词,用逗号分隔"
/>
</div>
<!-- 网站Logo -->
<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">网站Logo</label>
<span class="text-xs text-gray-500 dark:text-gray-400">选择网站Logo图片建议使用正方形图片</span>
</div>
<div class="flex items-center space-x-4">
<div v-if="configForm.site_logo" class="flex-shrink-0">
<n-image
:src="getImageUrl(configForm.site_logo)"
alt="网站Logo"
width="80"
height="80"
object-fit="cover"
class="rounded-lg border"
/>
</div>
<div class="flex-1">
<n-button type="primary" @click="openLogoSelector">
<template #icon>
<i class="fas fa-image"></i>
</template>
{{ configForm.site_logo ? '更换Logo' : '选择Logo' }}
</n-button>
<n-button v-if="configForm.site_logo" @click="clearLogo" class="ml-2">
<template #icon>
<i class="fas fa-times"></i>
</template>
清除
</n-button>
</div>
</div>
</div>
<!-- 版权信息 -->
<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-input
v-model:value="configForm.copyright"
placeholder="请输入版权信息"
/>
</div>
<n-switch v-model:value="configForm.enable_register" />
</div>
</n-form>
</div>
</n-form>
</n-tab-pane>
</n-tabs>
</n-card>
</n-tab-pane>
<n-tab-pane name="security" tab="安全设置">
<div class="tab-content-container">
<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.maintenance_mode" />
</div>
<!-- 违禁词 -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<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>
<a
href="https://raw.githubusercontent.com/ctwj/urldb/refs/heads/main/db/forbidden.txt"
target="_blank"
class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"
>
开源违禁词
</a>
</div>
<n-input
v-model:value="configForm.forbidden_words"
placeholder="请输入违禁词,用逗号分隔"
type="textarea"
:rows="4"
/>
</div>
<!-- 开启注册 -->
<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.enable_register" />
</div>
</div>
</n-form>
</div>
</n-tab-pane>
</n-tabs>
</div>
</template>
</AdminPageLayout>
<!-- Logo选择模态框 -->
<n-modal v-model:show="showLogoSelector" preset="card" title="选择Logo图片" style="width: 90vw; max-width: 1200px; max-height: 80vh;">
<div class="space-y-4">
@@ -271,7 +273,6 @@
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
@@ -584,7 +585,31 @@ onMounted(() => {
</script>
<style scoped>
/* 自定义样式 */
/* 站点配置页面样式 */
.config-content {
padding: 8px;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
/* 配置标签容器 - 支持滚动 */
.config-tabs-container {
height: calc(100vh - 200px);
overflow-y: auto;
padding: 0.5rem 0;
}
/* tab内容容器 - 个别内容滚动 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -611,11 +636,9 @@ onMounted(() => {
border-radius: 4px;
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 1rem;
}
</style>
</style>

View File

@@ -1,32 +1,21 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<template #page-header>
<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 type="primary" @click="showAddModal = true">
<template #icon>
<i class="fas fa-plus"></i>
</template>
添加标签
</n-button>
<n-button @click="refreshData">
<template #icon>
<i class="fas fa-refresh"></i>
</template>
刷新
</n-button>
</div>
</div>
</template>
<!-- 提示信息 -->
<n-alert title="提交的数据中,如果包含标签,数据添加成功,会自动添加标签" type="info" />
<!-- 提示信息区域 -->
<template #notice-section>
<n-alert title="提交的数据中,如果包含标签,数据添加成功,会自动添加标签" type="info" />
</template>
<!-- 搜索和操作 -->
<n-card>
<!-- 过滤栏 - 搜索和操作 -->
<template #filter-bar>
<div class="flex justify-between items-center">
<div class="flex gap-2">
<n-button @click="showAddModal = true" type="success">
@@ -38,9 +27,9 @@
</div>
<div class="flex gap-2">
<div class="relative">
<n-input
v-model:value="searchQuery"
@input="debounceSearch"
<n-input
v-model:value="searchQuery"
@input="debounceSearch"
type="text"
placeholder="搜索标签名称..."
clearable
@@ -58,21 +47,24 @@
</n-button>
</div>
</div>
</n-card>
</template>
<!-- 标签列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">标签列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个标签</span>
</div>
</template>
<!-- 内容区header - 标签列表标题 -->
<!-- <template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">标签列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个标签</span>
</div>
</template> -->
<div v-if="loading" class="flex items-center justify-center py-8">
<!-- 内容区 - 标签数据 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex h-full items-center justify-center py-8">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="tags.length === 0" class="text-center py-8">
<svg class="w-16 h-16 text-gray-300 dark:text-gray-600 mb-4 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" stroke-width="3" stroke-dasharray="6 6" />
@@ -87,21 +79,40 @@
添加标签
</n-button>
</div>
<div v-else>
<!-- 数据表格 - 自适应高度 -->
<div v-else class="flex flex-col h-full overflow-auto">
<n-data-table
:columns="columns"
:data="tags"
:pagination="pagination"
:bordered="false"
:single-line="false"
:loading="loading"
@update:page="handlePageChange"
/>
:columns="columns"
:data="tags"
:pagination="false"
:bordered="false"
:single-line="false"
:loading="loading"
:scroll-x="800"
class="h-full"
/>
</div>
</n-card>
</template>
<!-- 添加/编辑标签模态框 -->
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="total"
:page-sizes="[100, 200, 500, 1000]"
show-size-picker
@update:page="fetchData"
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
/>
</div>
</div>
</template>
</AdminPageLayout>
<!-- 添加/编辑标签模态框 -->
<n-modal v-model:show="showAddModal" preset="card" :title="editingTag ? '编辑标签' : '添加标签'" style="width: 500px">
<n-form
ref="formRef"
@@ -146,7 +157,6 @@
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
@@ -291,23 +301,7 @@ const columns = [
}
]
// 分页配置
const pagination = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
itemCount: total.value,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page: number) => {
currentPage.value = page
fetchData()
},
onUpdatePageSize: (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchData()
}
}))
// 分页配置已经被移到模板中处理
// 获取数据
const fetchData = async () => {

View File

@@ -1,7 +1,7 @@
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<AdminPageLayout>
<!-- 页面头部 - 标题和操作按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h1>
<p class="text-gray-600 dark:text-gray-400">管理系统中的用户账户</p>
@@ -20,21 +20,25 @@
刷新
</n-button>
</div>
</div>
</template>
<!-- 提示信息 -->
<n-alert title="用户管理功能,可以创建、编辑、删除用户,以及修改用户密码" type="info" />
<!-- 通知区域 -->
<template #notice-section>
<n-alert title="用户管理功能,可以创建、编辑、删除用户,以及修改用户密码" type="info" />
</template>
<!-- 用户列表 -->
<n-card>
<template #header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">用户列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个用户</span>
</div>
</template>
<!-- 内容区header -->
<template #content-header>
<div class="flex items-center justify-between">
<span class="text-lg font-semibold">用户列表</span>
<span class="text-sm text-gray-500"> {{ total }} 个用户</span>
</div>
</template>
<div v-if="loading" class="flex items-center justify-center py-8">
<!-- 内容区 - 用户列表 -->
<template #content>
<div v-if="loading" class="flex items-center justify-center py-8">
<n-spin size="large" />
</div>
@@ -53,20 +57,38 @@
</n-button>
</div>
<div v-else>
<div v-else class="h-full">
<n-data-table
:columns="columns"
:data="users"
:pagination="pagination"
:bordered="false"
:single-line="false"
:loading="loading"
@update:page="handlePageChange"
/>
</div>
</n-card>
</template>
<!-- 创建/编辑用户模态框 -->
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="currentPage"
v-model:page-size="pageSize"
:item-count="total"
:page-sizes="[100, 200, 500, 1000]"
show-size-picker
@update:page="fetchData"
@update:page-size="(size) => { pageSize = size; currentPage = 1; fetchData() }"
/>
</div>
</div>
</template>
</AdminPageLayout>
<!-- 创建/编辑用户模态框 -->
<n-modal v-model:show="showModal" preset="card" :title="showEditModal ? '编辑用户' : '创建用户'" style="width: 500px">
<div v-if="showEditModal && editingUser?.username === 'admin'" class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p class="text-sm text-yellow-800">
@@ -138,7 +160,7 @@
</template>
</n-modal>
<!-- 修改密码模态框 -->
<!-- 修改密码模态框 -->
<n-modal v-model:show="showChangePasswordModal" preset="card" title="修改密码" style="width: 400px">
<n-form
ref="passwordFormRef"
@@ -176,10 +198,11 @@
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import AdminPageLayout from '~/components/AdminPageLayout.vue'
// 设置页面布局
definePageMeta({
layout: 'admin'
@@ -594,4 +617,13 @@ const showModal = computed({
<style scoped>
/* 自定义样式 */
</style>
.config-content {
padding: 1rem;
background-color: var(--color-white, #ffffff);
}
.dark .config-content {
background-color: var(--color-dark-bg, #1f2937);
}
</style>

View File

@@ -65,7 +65,7 @@ export const useTaskStore = defineStore('task', () => {
// 获取任务统计信息
const fetchTaskStats = async () => {
try {
const response = await taskApi.getTasks({status: 'running'}) as any
const response = await taskApi.getTasks() as any
// console.log('原始任务API响应:', response)
// 处理API响应格式