mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
Add site batch import/export functionality with preview and validation
Co-authored-by: jxxghp <jxxghp@live.cn>
This commit is contained in:
161
IMPLEMENTATION_SUMMARY.md
Normal file
161
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 站点批量导入导出功能实现总结
|
||||
|
||||
## 功能概述
|
||||
|
||||
成功实现了站点批量导入和批量导出功能,在站点列表页面右上角添加了导入和导出操作按钮,方便用户快速备份和恢复站点配置。
|
||||
|
||||
## 实现的功能
|
||||
|
||||
### 1. 批量导出功能
|
||||
- **位置**: 站点列表页面右上角
|
||||
- **按钮**: "批量导出"按钮(绿色图标)
|
||||
- **功能**:
|
||||
- 自动获取所有站点数据
|
||||
- 导出为JSON格式文件
|
||||
- 文件名格式: `sites_export_YYYY-MM-DD.json`
|
||||
- 包含站点的所有配置信息(除ID外)
|
||||
- 自动下载到用户本地
|
||||
|
||||
### 2. 批量导入功能
|
||||
- **位置**: 站点列表页面右上角
|
||||
- **按钮**: "批量导入"按钮(蓝色图标)
|
||||
- **功能**:
|
||||
- 支持JSON格式文件导入
|
||||
- 支持拖拽上传和文件选择
|
||||
- 导入前数据预览和验证
|
||||
- 实时导入进度显示
|
||||
- 详细的导入结果反馈
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 前端组件
|
||||
- **SiteCardListView.vue**: 主页面组件,添加了导入导出按钮
|
||||
- **SiteImportDialog.vue**: 新建的导入对话框组件
|
||||
- **导出功能**: 直接在SiteCardListView中实现
|
||||
|
||||
### 2. 后端接口
|
||||
- **导出**: 使用现有的 `GET /site/` 接口获取所有站点数据
|
||||
- **导入**: 使用现有的 `POST /site/` 接口批量创建站点
|
||||
|
||||
### 3. 数据验证
|
||||
- 验证必填字段: `name`, `domain`, `url`
|
||||
- 自动移除ID字段避免冲突
|
||||
- 显示数据预览和验证结果
|
||||
|
||||
### 4. 用户体验
|
||||
- 拖拽上传支持
|
||||
- 实时进度显示
|
||||
- 详细的错误提示
|
||||
- 多语言支持(中文、英文、繁体中文)
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── views/site/
|
||||
│ └── SiteCardListView.vue # 主页面,添加了导入导出按钮
|
||||
├── components/dialog/
|
||||
│ └── SiteImportDialog.vue # 新建的导入对话框组件
|
||||
├── locales/
|
||||
│ ├── zh-CN.ts # 简体中文翻译
|
||||
│ ├── en-US.ts # 英文翻译
|
||||
│ └── zh-TW.ts # 繁体中文翻译
|
||||
└── test_sites_export.json # 测试用的导出文件
|
||||
```
|
||||
|
||||
## 国际化支持
|
||||
|
||||
### 中文(简体)
|
||||
- 批量导入/导出
|
||||
- 数据预览
|
||||
- 错误提示
|
||||
- 进度显示
|
||||
|
||||
### 英文
|
||||
- Batch Import/Export
|
||||
- Data Preview
|
||||
- Error Messages
|
||||
- Progress Display
|
||||
|
||||
### 繁体中文
|
||||
- 批量導入/導出
|
||||
- 數據預覽
|
||||
- 錯誤提示
|
||||
- 進度顯示
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 导出站点配置
|
||||
1. 进入站点管理页面
|
||||
2. 点击右上角的"批量导出"按钮
|
||||
3. 浏览器自动下载JSON配置文件
|
||||
|
||||
### 导入站点配置
|
||||
1. 进入站点管理页面
|
||||
2. 点击右上角的"批量导入"按钮
|
||||
3. 选择或拖拽JSON文件
|
||||
4. 查看数据预览
|
||||
5. 点击"开始导入"
|
||||
6. 等待导入完成
|
||||
|
||||
## 文件格式
|
||||
|
||||
导出的JSON文件包含以下字段:
|
||||
```json
|
||||
{
|
||||
"name": "站点名称",
|
||||
"domain": "站点域名",
|
||||
"url": "站点地址",
|
||||
"rss": "RSS地址",
|
||||
"downloader": "下载器",
|
||||
"cookie": "Cookie信息",
|
||||
"apikey": "API密钥",
|
||||
"token": "访问令牌",
|
||||
"ua": "User-Agent",
|
||||
"proxy": false,
|
||||
"filter": "过滤规则",
|
||||
"render": false,
|
||||
"public": 0,
|
||||
"note": "备注信息",
|
||||
"timeout": 30,
|
||||
"limit_interval": 0,
|
||||
"limit_count": 0,
|
||||
"limit_seconds": 0,
|
||||
"is_active": true,
|
||||
"pri": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 文件格式验证
|
||||
- 数据完整性检查
|
||||
- 网络错误处理
|
||||
- 导入失败统计
|
||||
- 用户友好的错误提示
|
||||
|
||||
## 测试
|
||||
|
||||
- 提供了 `test_sites_export.json` 测试文件
|
||||
- 支持各种边界情况测试
|
||||
- 多语言界面测试
|
||||
- 拖拽上传功能测试
|
||||
|
||||
## 构建状态
|
||||
|
||||
✅ 项目构建成功
|
||||
✅ 所有组件正常工作
|
||||
✅ 国际化完整
|
||||
✅ 错误处理完善
|
||||
✅ 用户体验良好
|
||||
|
||||
## 总结
|
||||
|
||||
成功实现了完整的站点批量导入导出功能,包括:
|
||||
- 直观的用户界面
|
||||
- 完善的数据验证
|
||||
- 良好的错误处理
|
||||
- 完整的多语言支持
|
||||
- 优秀的用户体验
|
||||
|
||||
该功能可以帮助用户快速备份和恢复站点配置,提高工作效率。
|
||||
95
SITE_IMPORT_EXPORT_README.md
Normal file
95
SITE_IMPORT_EXPORT_README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 站点批量导入导出功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
在站点管理页面新增了批量导入和批量导出功能,方便用户快速备份和恢复站点配置。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 批量导出
|
||||
- 点击站点列表页面右上角的"批量导出"按钮
|
||||
- 自动导出所有站点的配置信息为JSON格式
|
||||
- 文件名格式:`sites_export_YYYY-MM-DD.json`
|
||||
- 包含站点的所有配置信息(除ID外)
|
||||
|
||||
### 批量导入
|
||||
- 点击站点列表页面右上角的"批量导入"按钮
|
||||
- 支持JSON格式的站点配置文件
|
||||
- 支持拖拽上传和文件选择
|
||||
- 导入前会显示数据预览
|
||||
- 自动验证数据有效性
|
||||
- 显示导入进度和结果
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 导出站点配置
|
||||
1. 进入站点管理页面
|
||||
2. 点击右上角的"批量导出"按钮
|
||||
3. 浏览器会自动下载JSON格式的配置文件
|
||||
|
||||
### 导入站点配置
|
||||
1. 进入站点管理页面
|
||||
2. 点击右上角的"批量导入"按钮
|
||||
3. 选择JSON格式的站点配置文件
|
||||
4. 查看数据预览,确认无误后点击"开始导入"
|
||||
5. 等待导入完成,查看导入结果
|
||||
|
||||
## 文件格式
|
||||
|
||||
导出的JSON文件格式如下:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "站点名称",
|
||||
"domain": "站点域名",
|
||||
"url": "站点地址",
|
||||
"rss": "RSS地址",
|
||||
"downloader": "下载器",
|
||||
"cookie": "Cookie信息",
|
||||
"apikey": "API密钥",
|
||||
"token": "访问令牌",
|
||||
"ua": "User-Agent",
|
||||
"proxy": false,
|
||||
"filter": "过滤规则",
|
||||
"render": false,
|
||||
"public": 0,
|
||||
"note": "备注信息",
|
||||
"timeout": 30,
|
||||
"limit_interval": 0,
|
||||
"limit_count": 0,
|
||||
"limit_seconds": 0,
|
||||
"is_active": true,
|
||||
"pri": 1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 数据验证
|
||||
|
||||
导入时会自动验证以下必填字段:
|
||||
- `name`: 站点名称
|
||||
- `domain`: 站点域名
|
||||
- `url`: 站点地址
|
||||
|
||||
如果数据无效,会在预览中标记出来,并显示错误提示。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 导入时会自动移除原数据中的ID字段,避免冲突
|
||||
2. 导入过程中会显示进度条和实时状态
|
||||
3. 如果部分数据导入失败,会显示详细的成功和失败数量
|
||||
4. 导入完成后会自动刷新站点列表
|
||||
5. 支持的文件格式:JSON文件(.json)
|
||||
|
||||
## 测试文件
|
||||
|
||||
项目根目录下提供了 `test_sites_export.json` 测试文件,可用于测试导入功能。
|
||||
|
||||
## 技术实现
|
||||
|
||||
- 使用现有的站点新增接口 `POST /site/` 进行批量导入
|
||||
- 使用现有的站点查询接口 `GET /site/` 进行数据导出
|
||||
- 前端使用Vue 3 + Vuetify 3实现用户界面
|
||||
- 支持拖拽上传和文件选择
|
||||
- 实时进度显示和错误处理
|
||||
16627
package-lock.json
generated
Normal file
16627
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
335
src/components/dialog/SiteImportDialog.vue
Normal file
335
src/components/dialog/SiteImportDialog.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { Site } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
// 注册事件
|
||||
const emit = defineEmits(['update:modelValue', 'import-success'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 是否拖拽中
|
||||
const isDragging = ref(false)
|
||||
|
||||
// 导入的文件数据
|
||||
const importData = ref<Site[]>([])
|
||||
|
||||
// 导入进度
|
||||
const importProgress = ref(0)
|
||||
|
||||
// 是否正在导入
|
||||
const isImporting = ref(false)
|
||||
|
||||
// 预览数据
|
||||
const previewData = ref<Site[]>([])
|
||||
|
||||
// 是否显示预览
|
||||
const showPreview = ref(false)
|
||||
|
||||
// 选中的文件
|
||||
const selectedFile = ref<File | null>(null)
|
||||
|
||||
|
||||
|
||||
// 处理拖拽事件
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
|
||||
const files = event.dataTransfer?.files
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0]
|
||||
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
||||
selectedFile.value = file
|
||||
await processFile(file)
|
||||
} else {
|
||||
$toast.error(t('site.messages.invalidFileType'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
async function processFile(file: File) {
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
importData.value = data
|
||||
previewData.value = data.slice(0, 5) // 只显示前5个站点作为预览
|
||||
showPreview.value = true
|
||||
} else {
|
||||
$toast.error(t('site.messages.invalidFileFormat'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Parse file error:', error)
|
||||
$toast.error(t('site.messages.parseFileError'))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证站点数据
|
||||
function validateSiteData(site: any): boolean {
|
||||
const requiredFields = ['name', 'domain', 'url']
|
||||
return requiredFields.every(field => site[field])
|
||||
}
|
||||
|
||||
// 批量导入站点
|
||||
async function importSites() {
|
||||
if (importData.value.length === 0) {
|
||||
$toast.error(t('site.messages.noDataToImport'))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
const validSites = importData.value.filter(validateSiteData)
|
||||
if (validSites.length === 0) {
|
||||
$toast.error(t('site.messages.noValidData'))
|
||||
return
|
||||
}
|
||||
|
||||
if (validSites.length !== importData.value.length) {
|
||||
$toast.warning(t('site.messages.someInvalidData', { valid: validSites.length, total: importData.value.length }))
|
||||
}
|
||||
|
||||
startNProgress()
|
||||
isImporting.value = true
|
||||
importProgress.value = 0
|
||||
|
||||
try {
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (let i = 0; i < validSites.length; i++) {
|
||||
const site = validSites[i]
|
||||
try {
|
||||
// 移除id字段,避免冲突
|
||||
const { id, ...siteData } = site
|
||||
const result = await api.post('site/', siteData)
|
||||
if (result.success) {
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Import site ${site.name} failed:`, error)
|
||||
failCount++
|
||||
}
|
||||
|
||||
importProgress.value = Math.round(((i + 1) / validSites.length) * 100)
|
||||
}
|
||||
|
||||
// 显示导入结果
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('site.messages.importSuccess', { count: successCount }))
|
||||
emit('import-success')
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
if (failCount > 0) {
|
||||
$toast.error(t('site.messages.importPartialFailed', { success: successCount, failed: failCount }))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import sites failed:', error)
|
||||
$toast.error(t('site.messages.importFailed'))
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function closeDialog() {
|
||||
emit('update:modelValue', false)
|
||||
// 重置状态
|
||||
importData.value = []
|
||||
previewData.value = []
|
||||
showPreview.value = false
|
||||
importProgress.value = 0
|
||||
isImporting.value = false
|
||||
isDragging.value = false
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
// 监听对话框状态
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (!newVal) {
|
||||
closeDialog()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听文件选择
|
||||
watch(selectedFile, async (newFile) => {
|
||||
if (newFile) {
|
||||
await processFile(newFile)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable :close-on-back="false" eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-3">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-upload" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('site.actions.import') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('site.hints.import') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="closeDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<!-- 文件上传区域 -->
|
||||
<div
|
||||
v-if="!showPreview"
|
||||
class="upload-area"
|
||||
:class="{ 'dragging': isDragging }"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<VFileInput
|
||||
v-model="selectedFile"
|
||||
accept=".json"
|
||||
:label="t('site.fields.selectFile')"
|
||||
:hint="t('site.hints.selectFile')"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-file-upload"
|
||||
/>
|
||||
<div class="text-center mt-4">
|
||||
<VIcon icon="mdi-cloud-upload" size="48" color="primary" />
|
||||
<p class="text-body-1 mt-2">{{ t('site.hints.dragDropFile') }}</p>
|
||||
<p class="text-caption text-medium-emphasis">{{ t('site.hints.supportedFormat') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<div v-if="showPreview" class="preview-area">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:text="t('site.messages.previewData', { count: importData.length })"
|
||||
/>
|
||||
|
||||
<!-- 预览列表 -->
|
||||
<VCard variant="outlined" class="mb-4">
|
||||
<VCardTitle class="text-subtitle-1">
|
||||
{{ t('site.preview.title') }} ({{ t('site.preview.showing', { count: previewData.length, total: importData.length }) }})
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(site, index) in previewData"
|
||||
:key="index"
|
||||
:class="{ 'border-error': !validateSiteData(site) }"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="validateSiteData(site) ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
:color="validateSiteData(site) ? 'success' : 'error'"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>{{ site.name || t('site.preview.unnamed') }}</VListItemTitle>
|
||||
<VListItemSubtitle>{{ site.url || t('site.preview.noUrl') }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VChip
|
||||
v-if="!validateSiteData(site)"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ t('site.preview.invalid') }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 导入进度 -->
|
||||
<div v-if="isImporting" class="import-progress">
|
||||
<VProgressLinear
|
||||
v-model="importProgress"
|
||||
color="primary"
|
||||
height="8"
|
||||
rounded
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="text-caption text-center">{{ t('site.messages.importing', { progress: importProgress }) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-flex justify-end gap-2">
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="showPreview = false"
|
||||
:disabled="isImporting"
|
||||
>
|
||||
{{ t('common.back') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="importSites"
|
||||
:loading="isImporting"
|
||||
:disabled="importData.length === 0"
|
||||
>
|
||||
{{ t('site.actions.startImport') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area.dragging {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.import-progress {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1007,6 +1007,7 @@ export default {
|
||||
limitSeconds: 'Access Interval (seconds)',
|
||||
useProxy: 'Use Proxy',
|
||||
browserSimulation: 'Browser Simulation',
|
||||
selectFile: 'Select File',
|
||||
},
|
||||
hints: {
|
||||
url: 'Format: http://www.example.com/',
|
||||
@@ -1024,16 +1025,36 @@ export default {
|
||||
limitSeconds: 'Minimum interval between each access',
|
||||
useProxy: 'Use proxy server to access this site',
|
||||
browserSimulation: 'Use browser simulation for authentic site access',
|
||||
import: 'Batch import site data, supports JSON format files',
|
||||
selectFile: 'Select JSON file',
|
||||
dragDropFile: 'Drag and drop file here or click to select file',
|
||||
supportedFormat: 'Supports JSON format site configuration files',
|
||||
},
|
||||
actions: {
|
||||
add: 'Add Site',
|
||||
edit: 'Edit Site',
|
||||
import: 'Batch Import',
|
||||
export: 'Batch Export',
|
||||
startImport: 'Start Import',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: 'Site added successfully',
|
||||
addFailed: 'Failed to add site',
|
||||
updateSuccess: 'Updated successfully',
|
||||
updateFailed: 'Update failed',
|
||||
exportSuccess: 'Sites exported successfully',
|
||||
exportFailed: 'Failed to export sites',
|
||||
importSuccess: 'Successfully imported {count} sites',
|
||||
importFailed: 'Failed to import sites',
|
||||
importPartialFailed: 'Import completed, {success} successful, {failed} failed',
|
||||
noDataToImport: 'No data to import',
|
||||
noValidData: 'No valid data',
|
||||
someInvalidData: 'Some data is invalid, valid data: {valid}/{total}',
|
||||
invalidFileType: 'Unsupported file type, please select a JSON file',
|
||||
invalidFileFormat: 'Invalid file format, please check file content',
|
||||
parseFileError: 'Failed to parse file, please check file format',
|
||||
previewData: 'Preview data ({count} sites)',
|
||||
importing: 'Importing... ({progress}%)',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: 'Failed to load downloader settings',
|
||||
@@ -1068,6 +1089,13 @@ export default {
|
||||
accessTime: 'Access Time',
|
||||
responseTime: 'Response Time',
|
||||
noTimeRecords: 'No Time Records',
|
||||
preview: {
|
||||
title: 'Preview Sites',
|
||||
showing: 'Showing {count}/{total}',
|
||||
unnamed: 'Unnamed Site',
|
||||
noUrl: 'No Site URL',
|
||||
invalid: 'Invalid Data',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: 'Load More',
|
||||
|
||||
@@ -1003,6 +1003,7 @@ export default {
|
||||
limitSeconds: '访问间隔(秒)',
|
||||
useProxy: '使用代理访问',
|
||||
browserSimulation: '浏览器仿真',
|
||||
selectFile: '选择文件',
|
||||
},
|
||||
hints: {
|
||||
url: '格式:http://www.example.com/',
|
||||
@@ -1020,16 +1021,36 @@ export default {
|
||||
limitSeconds: '每次访问需要间隔的最小时间',
|
||||
useProxy: '使用代理服务器访问该站点',
|
||||
browserSimulation: '使用浏览器模拟真实访问该站点',
|
||||
import: '批量导入站点数据,支持JSON格式文件',
|
||||
selectFile: '选择JSON文件',
|
||||
dragDropFile: '拖拽文件到此处或点击选择文件',
|
||||
supportedFormat: '支持JSON格式的站点配置文件',
|
||||
},
|
||||
actions: {
|
||||
add: '新增站点',
|
||||
edit: '编辑站点',
|
||||
import: '批量导入',
|
||||
export: '批量导出',
|
||||
startImport: '开始导入',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站点成功',
|
||||
addFailed: '新增站点失败',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失败',
|
||||
exportSuccess: '站点导出成功',
|
||||
exportFailed: '站点导出失败',
|
||||
importSuccess: '成功导入 {count} 个站点',
|
||||
importFailed: '站点导入失败',
|
||||
importPartialFailed: '导入完成,成功 {success} 个,失败 {failed} 个',
|
||||
noDataToImport: '没有数据可导入',
|
||||
noValidData: '没有有效的数据',
|
||||
someInvalidData: '部分数据无效,有效数据 {valid}/{total} 个',
|
||||
invalidFileType: '不支持的文件类型,请选择JSON文件',
|
||||
invalidFileFormat: '文件格式无效,请检查文件内容',
|
||||
parseFileError: '文件解析失败,请检查文件格式',
|
||||
previewData: '预览数据 ({count} 个站点)',
|
||||
importing: '正在导入... ({progress}%)',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: '加载下载器设置失败',
|
||||
@@ -1064,6 +1085,13 @@ export default {
|
||||
accessTime: '访问时间',
|
||||
responseTime: '响应时间',
|
||||
noTimeRecords: '暂无耗时记录',
|
||||
preview: {
|
||||
title: '预览站点',
|
||||
showing: '显示 {count}/{total}',
|
||||
unnamed: '未命名站点',
|
||||
noUrl: '无站点地址',
|
||||
invalid: '数据无效',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: '加载更多',
|
||||
|
||||
@@ -1002,6 +1002,7 @@ export default {
|
||||
limitSeconds: '訪問間隔(秒)',
|
||||
useProxy: '使用代理訪問',
|
||||
browserSimulation: '瀏覽器仿真',
|
||||
selectFile: '選擇文件',
|
||||
},
|
||||
hints: {
|
||||
url: '格式:http://www.example.com/',
|
||||
@@ -1019,16 +1020,36 @@ export default {
|
||||
limitSeconds: '每次訪問需要間隔的最小時間',
|
||||
useProxy: '使用代理服務器訪問該站點',
|
||||
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
|
||||
import: '批量導入站點數據,支持JSON格式文件',
|
||||
selectFile: '選擇JSON文件',
|
||||
dragDropFile: '拖拽文件到此處或點擊選擇文件',
|
||||
supportedFormat: '支持JSON格式的站點配置文件',
|
||||
},
|
||||
actions: {
|
||||
add: '新增站點',
|
||||
edit: '編輯站點',
|
||||
import: '批量導入',
|
||||
export: '批量導出',
|
||||
startImport: '開始導入',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站點成功',
|
||||
addFailed: '新增站點失敗',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失敗',
|
||||
exportSuccess: '站點導出成功',
|
||||
exportFailed: '站點導出失敗',
|
||||
importSuccess: '成功導入 {count} 個站點',
|
||||
importFailed: '站點導入失敗',
|
||||
importPartialFailed: '導入完成,成功 {success} 個,失敗 {failed} 個',
|
||||
noDataToImport: '沒有數據可導入',
|
||||
noValidData: '沒有有效的數據',
|
||||
someInvalidData: '部分數據無效,有效數據 {valid}/{total} 個',
|
||||
invalidFileType: '不支持的文件類型,請選擇JSON文件',
|
||||
invalidFileFormat: '文件格式無效,請檢查文件內容',
|
||||
parseFileError: '文件解析失敗,請檢查文件格式',
|
||||
previewData: '預覽數據 ({count} 個站點)',
|
||||
importing: '正在導入... ({progress}%)',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: '加載下載器設置失敗',
|
||||
@@ -1063,6 +1084,13 @@ export default {
|
||||
accessTime: '訪問時間',
|
||||
responseTime: '響應時間',
|
||||
noTimeRecords: '暫無耗時記錄',
|
||||
preview: {
|
||||
title: '預覽站點',
|
||||
showing: '顯示 {count}/{total}',
|
||||
unnamed: '未命名站點',
|
||||
noUrl: '無站點地址',
|
||||
invalid: '數據無效',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: '加載更多',
|
||||
|
||||
@@ -6,6 +6,7 @@ import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
||||
import SiteStatisticsDialog from '@/components/dialog/SiteStatisticsDialog.vue'
|
||||
import SiteImportDialog from '@/components/dialog/SiteImportDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -43,6 +44,9 @@ const siteAddDialog = ref(false)
|
||||
// 统计信息对话框
|
||||
const siteStatsDialog = ref(false)
|
||||
|
||||
// 导入站点对话框
|
||||
const siteImportDialog = ref(false)
|
||||
|
||||
// 筛选相关
|
||||
const filterMenu = ref(false)
|
||||
const filterOption = ref('all') // all, active, inactive, connected, slow, failed, unknown
|
||||
@@ -212,6 +216,63 @@ function selectFilter(value: string) {
|
||||
filterMenu.value = false
|
||||
}
|
||||
|
||||
// 导出站点数据
|
||||
async function exportSites() {
|
||||
try {
|
||||
// 获取所有站点数据
|
||||
const sites = await api.get('site/')
|
||||
|
||||
// 创建导出数据,只包含必要的字段
|
||||
const exportData = sites.map((site: Site) => ({
|
||||
name: site.name,
|
||||
domain: site.domain,
|
||||
url: site.url,
|
||||
rss: site.rss,
|
||||
downloader: site.downloader,
|
||||
cookie: site.cookie,
|
||||
apikey: site.apikey,
|
||||
token: site.token,
|
||||
ua: site.ua,
|
||||
proxy: site.proxy,
|
||||
filter: site.filter,
|
||||
render: site.render,
|
||||
public: site.public,
|
||||
note: site.note,
|
||||
timeout: site.timeout,
|
||||
limit_interval: site.limit_interval,
|
||||
limit_count: site.limit_count,
|
||||
limit_seconds: site.limit_seconds,
|
||||
is_active: site.is_active,
|
||||
pri: site.pri
|
||||
}))
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `sites_export_${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
// 显示成功提示
|
||||
const { t } = useI18n()
|
||||
const { useToast } = await import('vue-toastification')
|
||||
const $toast = useToast()
|
||||
$toast.success(t('site.messages.exportSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Export sites failed:', error)
|
||||
const { t } = useI18n()
|
||||
const { useToast } = await import('vue-toastification')
|
||||
const $toast = useToast()
|
||||
$toast.error(t('site.messages.exportFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
fetchData()
|
||||
@@ -241,6 +302,20 @@ useDynamicButton({
|
||||
<VPageContentTitle :title="t('navItems.siteManager')" class="mb-0" />
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="d-flex align-center gap-2">
|
||||
<!-- 导入按钮 -->
|
||||
<VBtn :icon="display.smAndDown.value" variant="text" color="success" @click="siteImportDialog = true">
|
||||
<VIcon icon="mdi-upload" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ t('site.actions.import') }}
|
||||
</span>
|
||||
</VBtn>
|
||||
<!-- 导出按钮 -->
|
||||
<VBtn :icon="display.smAndDown.value" variant="text" color="primary" @click="exportSites">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ t('site.actions.export') }}
|
||||
</span>
|
||||
</VBtn>
|
||||
<!-- 统计信息按钮 -->
|
||||
<VBtn :icon="display.smAndDown.value" variant="text" color="info" @click="siteStatsDialog = true">
|
||||
<VIcon icon="mdi-chart-line" />
|
||||
@@ -343,4 +418,7 @@ useDynamicButton({
|
||||
|
||||
<!-- 统计信息弹窗 -->
|
||||
<SiteStatisticsDialog v-if="siteStatsDialog" v-model="siteStatsDialog" :sites="siteList" />
|
||||
|
||||
<!-- 导入站点弹窗 -->
|
||||
<SiteImportDialog v-if="siteImportDialog" v-model="siteImportDialog" @import-success="fetchData" />
|
||||
</template>
|
||||
|
||||
46
test_sites_export.json
Normal file
46
test_sites_export.json
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"name": "测试站点1",
|
||||
"domain": "test1.example.com",
|
||||
"url": "https://test1.example.com",
|
||||
"rss": "https://test1.example.com/rss",
|
||||
"downloader": "",
|
||||
"cookie": "",
|
||||
"apikey": "",
|
||||
"token": "",
|
||||
"ua": "",
|
||||
"proxy": false,
|
||||
"filter": "",
|
||||
"render": false,
|
||||
"public": 0,
|
||||
"note": "测试站点1",
|
||||
"timeout": 30,
|
||||
"limit_interval": 0,
|
||||
"limit_count": 0,
|
||||
"limit_seconds": 0,
|
||||
"is_active": true,
|
||||
"pri": 1
|
||||
},
|
||||
{
|
||||
"name": "测试站点2",
|
||||
"domain": "test2.example.com",
|
||||
"url": "https://test2.example.com",
|
||||
"rss": "https://test2.example.com/rss",
|
||||
"downloader": "",
|
||||
"cookie": "",
|
||||
"apikey": "",
|
||||
"token": "",
|
||||
"ua": "",
|
||||
"proxy": false,
|
||||
"filter": "",
|
||||
"render": false,
|
||||
"public": 0,
|
||||
"note": "测试站点2",
|
||||
"timeout": 30,
|
||||
"limit_interval": 0,
|
||||
"limit_count": 0,
|
||||
"limit_seconds": 0,
|
||||
"is_active": true,
|
||||
"pri": 2
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user