mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
fix site import dialog
This commit is contained in:
@@ -15,18 +15,19 @@ const display = useDisplay()
|
|||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注册事件
|
// 注册事件
|
||||||
const emit = defineEmits(['update:modelValue', 'import-success'])
|
const emit = defineEmits(['update:modelValue', 'import-success'])
|
||||||
|
|
||||||
|
// 界面阶段枚举
|
||||||
|
enum ImportStage {
|
||||||
|
SELECT_FILE = 'select_file', // 选择文件阶段
|
||||||
|
PREVIEW_FILE = 'preview_file', // 文件预览阶段
|
||||||
|
IMPORTING = 'importing', // 正在导入阶段
|
||||||
|
IMPORT_COMPLETE = 'import_complete', // 导入完成阶段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前阶段
|
||||||
|
const currentStage = ref<ImportStage>(ImportStage.SELECT_FILE)
|
||||||
|
|
||||||
// 是否拖拽中
|
// 是否拖拽中
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
@@ -37,19 +38,20 @@ const importData = ref<Site[]>([])
|
|||||||
// 导入进度
|
// 导入进度
|
||||||
const importProgress = ref(0)
|
const importProgress = ref(0)
|
||||||
|
|
||||||
// 是否正在导入
|
|
||||||
const isImporting = ref(false)
|
|
||||||
|
|
||||||
// 预览数据
|
// 预览数据
|
||||||
const previewData = ref<Site[]>([])
|
const previewData = ref<Site[]>([])
|
||||||
|
|
||||||
// 是否显示预览
|
|
||||||
const showPreview = ref(false)
|
|
||||||
|
|
||||||
// 选中的文件
|
// 选中的文件
|
||||||
const selectedFile = ref<File | null>(null)
|
const selectedFile = ref<File | null>(null)
|
||||||
|
|
||||||
|
// 导入错误信息
|
||||||
|
const importErrors = ref<Array<{ site: Site; error: string }>>([])
|
||||||
|
|
||||||
|
// 导入成功的站点
|
||||||
|
const importSuccesses = ref<Site[]>([])
|
||||||
|
|
||||||
|
// 是否显示错误详情
|
||||||
|
const showErrorDetails = ref(false)
|
||||||
|
|
||||||
// 处理拖拽事件
|
// 处理拖拽事件
|
||||||
function handleDragOver(event: DragEvent) {
|
function handleDragOver(event: DragEvent) {
|
||||||
@@ -65,7 +67,7 @@ function handleDragLeave(event: DragEvent) {
|
|||||||
async function handleDrop(event: DragEvent) {
|
async function handleDrop(event: DragEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
|
||||||
const files = event.dataTransfer?.files
|
const files = event.dataTransfer?.files
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
@@ -83,11 +85,11 @@ async function processFile(file: File) {
|
|||||||
try {
|
try {
|
||||||
const text = await file.text()
|
const text = await file.text()
|
||||||
const data = JSON.parse(text)
|
const data = JSON.parse(text)
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
importData.value = data
|
importData.value = data
|
||||||
previewData.value = data.slice(0, 5) // 只显示前5个站点作为预览
|
previewData.value = data.slice(0, 5) // 只显示前5个站点作为预览
|
||||||
showPreview.value = true
|
currentStage.value = ImportStage.PREVIEW_FILE
|
||||||
} else {
|
} else {
|
||||||
$toast.error(t('site.messages.invalidFileFormat'))
|
$toast.error(t('site.messages.invalidFileFormat'))
|
||||||
}
|
}
|
||||||
@@ -121,74 +123,98 @@ async function importSites() {
|
|||||||
$toast.warning(t('site.messages.someInvalidData', { valid: validSites.length, total: importData.value.length }))
|
$toast.warning(t('site.messages.someInvalidData', { valid: validSites.length, total: importData.value.length }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入导入阶段
|
||||||
|
currentStage.value = ImportStage.IMPORTING
|
||||||
startNProgress()
|
startNProgress()
|
||||||
isImporting.value = true
|
|
||||||
importProgress.value = 0
|
importProgress.value = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let successCount = 0
|
let successCount = 0
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
|
importErrors.value = [] // 清空之前的错误信息
|
||||||
|
importSuccesses.value = [] // 清空之前的成功信息
|
||||||
|
|
||||||
for (let i = 0; i < validSites.length; i++) {
|
for (let i = 0; i < validSites.length; i++) {
|
||||||
const site = validSites[i]
|
const site = validSites[i]
|
||||||
try {
|
try {
|
||||||
// 移除id字段,避免冲突
|
// 移除id字段,避免冲突
|
||||||
const { id, ...siteData } = site
|
const { id, ...siteData } = site
|
||||||
const result = await api.post('site/', siteData)
|
const result: { success: boolean; message?: string } = await api.post('site/', siteData)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
// 记录成功的站点
|
||||||
successCount++
|
successCount++
|
||||||
|
importSuccesses.value.push(site)
|
||||||
} else {
|
} else {
|
||||||
failCount++
|
failCount++
|
||||||
|
// 记录失败信息
|
||||||
|
importErrors.value.push({
|
||||||
|
site,
|
||||||
|
error: result.message || t('site.messages.importFailed'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Import site ${site.name} failed:`, error)
|
console.error(`Import site ${site.name} failed:`, error)
|
||||||
failCount++
|
failCount++
|
||||||
|
// 记录错误信息
|
||||||
|
importErrors.value.push({
|
||||||
|
site,
|
||||||
|
error: error instanceof Error ? error.message : t('site.messages.importFailed'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
// 更新进度
|
||||||
importProgress.value = Math.round(((i + 1) / validSites.length) * 100)
|
importProgress.value = Math.round(((i + 1) / validSites.length) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入完成阶段
|
||||||
|
currentStage.value = ImportStage.IMPORT_COMPLETE
|
||||||
|
|
||||||
// 显示导入结果
|
// 显示导入结果
|
||||||
if (successCount > 0) {
|
if (failCount === 0 && successCount > 0) {
|
||||||
|
// 全部成功,直接关闭对话框
|
||||||
$toast.success(t('site.messages.importSuccess', { count: successCount }))
|
$toast.success(t('site.messages.importSuccess', { count: successCount }))
|
||||||
emit('import-success')
|
closeDialog(true)
|
||||||
closeDialog()
|
} else if (successCount === 0 && failCount > 0) {
|
||||||
}
|
// 全部失败的情况
|
||||||
|
$toast.error(t('site.messages.importAllFailed', { count: failCount }))
|
||||||
if (failCount > 0) {
|
showErrorDetails.value = true
|
||||||
|
} else {
|
||||||
|
// 部分成功部分失败的情况
|
||||||
$toast.error(t('site.messages.importPartialFailed', { success: successCount, failed: failCount }))
|
$toast.error(t('site.messages.importPartialFailed', { success: successCount, failed: failCount }))
|
||||||
|
showErrorDetails.value = true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Import sites failed:', error)
|
console.error('Import sites failed:', error)
|
||||||
$toast.error(t('site.messages.importFailed'))
|
$toast.error(t('site.messages.importFailed'))
|
||||||
|
// 出错时回到预览阶段
|
||||||
|
currentStage.value = ImportStage.PREVIEW_FILE
|
||||||
} finally {
|
} finally {
|
||||||
isImporting.value = false
|
|
||||||
doneNProgress()
|
doneNProgress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭对话框
|
// 重置到文件选择阶段
|
||||||
function closeDialog() {
|
function resetToFileSelection() {
|
||||||
emit('update:modelValue', false)
|
currentStage.value = ImportStage.SELECT_FILE
|
||||||
// 重置状态
|
|
||||||
importData.value = []
|
importData.value = []
|
||||||
previewData.value = []
|
previewData.value = []
|
||||||
showPreview.value = false
|
|
||||||
importProgress.value = 0
|
importProgress.value = 0
|
||||||
isImporting.value = false
|
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
|
importErrors.value = []
|
||||||
|
importSuccesses.value = []
|
||||||
|
showErrorDetails.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听对话框状态
|
// 关闭对话框
|
||||||
watch(() => props.modelValue, (newVal) => {
|
function closeDialog(success: boolean = false) {
|
||||||
if (!newVal) {
|
if (success) {
|
||||||
closeDialog()
|
emit('import-success')
|
||||||
}
|
}
|
||||||
})
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
// 监听文件选择
|
// 监听文件选择
|
||||||
watch(selectedFile, async (newFile) => {
|
watch(selectedFile, async newFile => {
|
||||||
if (newFile) {
|
if (newFile) {
|
||||||
await processFile(newFile)
|
await processFile(newFile)
|
||||||
}
|
}
|
||||||
@@ -196,9 +222,9 @@ watch(selectedFile, async (newFile) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogWrapper scrollable :close-on-back="false" eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
<DialogWrapper scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard>
|
<VCard>
|
||||||
<VCardItem class="py-3">
|
<VCardItem class="py-2">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-upload" class="me-2" />
|
<VIcon icon="mdi-upload" class="me-2" />
|
||||||
</template>
|
</template>
|
||||||
@@ -208,43 +234,46 @@ watch(selectedFile, async (newFile) => {
|
|||||||
<VDialogCloseBtn @click="closeDialog" />
|
<VDialogCloseBtn @click="closeDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<!-- 文件上传区域 -->
|
<!-- 阶段1:选择文件阶段 -->
|
||||||
<div
|
<div v-if="currentStage === ImportStage.SELECT_FILE" class="upload-area">
|
||||||
v-if="!showPreview"
|
<div
|
||||||
class="upload-area"
|
class="upload-zone"
|
||||||
:class="{ 'dragging': isDragging }"
|
:class="{ 'dragging': isDragging }"
|
||||||
@dragover="handleDragOver"
|
@dragover="handleDragOver"
|
||||||
@dragleave="handleDragLeave"
|
@dragleave="handleDragLeave"
|
||||||
@drop="handleDrop"
|
@drop="handleDrop"
|
||||||
>
|
>
|
||||||
<VFileInput
|
<VFileInput
|
||||||
v-model="selectedFile"
|
v-model="selectedFile"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
:label="t('site.fields.selectFile')"
|
:label="t('site.fields.selectFile')"
|
||||||
:hint="t('site.hints.selectFile')"
|
:hint="t('site.hints.selectFile')"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
prepend-icon="mdi-file-upload"
|
prepend-icon="mdi-file-upload"
|
||||||
/>
|
/>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<VIcon icon="mdi-cloud-upload" size="48" color="primary" />
|
<VIcon icon="mdi-cloud-upload" size="48" color="primary" />
|
||||||
<p class="text-body-1 mt-2">{{ t('site.hints.dragDropFile') }}</p>
|
<p class="text-body-1 mt-2">{{ t('site.hints.dragDropFile') }}</p>
|
||||||
<p class="text-caption text-medium-emphasis">{{ t('site.hints.supportedFormat') }}</p>
|
<p class="text-caption text-medium-emphasis">{{ t('site.hints.supportedFormat') }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 预览区域 -->
|
<!-- 阶段2:文件预览阶段 -->
|
||||||
<div v-if="showPreview" class="preview-area">
|
<div v-if="currentStage === ImportStage.PREVIEW_FILE" class="preview-area">
|
||||||
<VAlert
|
<VAlert
|
||||||
type="info"
|
type="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:text="t('site.messages.previewData', { count: importData.length })"
|
:text="t('site.messages.previewData', { count: importData.length })"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 预览列表 -->
|
<!-- 预览列表 -->
|
||||||
<VCard variant="outlined" class="mb-4">
|
<VCard variant="outlined" class="mb-4">
|
||||||
<VCardTitle class="text-subtitle-1">
|
<VCardTitle class="text-subtitle-1">
|
||||||
{{ t('site.preview.title') }} ({{ t('site.preview.showing', { count: previewData.length, total: importData.length }) }})
|
{{ t('site.preview.title') }} ({{
|
||||||
|
t('site.preview.showing', { count: previewData.length, total: importData.length })
|
||||||
|
}})
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VList>
|
<VList>
|
||||||
@@ -262,12 +291,7 @@ watch(selectedFile, async (newFile) => {
|
|||||||
<VListItemTitle>{{ site.name || t('site.preview.unnamed') }}</VListItemTitle>
|
<VListItemTitle>{{ site.name || t('site.preview.unnamed') }}</VListItemTitle>
|
||||||
<VListItemSubtitle>{{ site.url || t('site.preview.noUrl') }}</VListItemSubtitle>
|
<VListItemSubtitle>{{ site.url || t('site.preview.noUrl') }}</VListItemSubtitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<VChip
|
<VChip v-if="!validateSiteData(site)" size="small" color="error" variant="tonal">
|
||||||
v-if="!validateSiteData(site)"
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
{{ t('site.preview.invalid') }}
|
{{ t('site.preview.invalid') }}
|
||||||
</VChip>
|
</VChip>
|
||||||
</template>
|
</template>
|
||||||
@@ -276,34 +300,84 @@ watch(selectedFile, async (newFile) => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="d-flex justify-end gap-2">
|
||||||
|
<VBtn variant="text" @click="resetToFileSelection">
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn color="primary" @click="importSites" :disabled="importData.length === 0">
|
||||||
|
{{ t('site.actions.startImport') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 阶段3:正在导入阶段 -->
|
||||||
|
<div v-if="currentStage === ImportStage.IMPORTING" class="importing-area">
|
||||||
|
<VAlert
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-4"
|
||||||
|
:text="t('site.messages.importing', { progress: importProgress })"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 导入进度 -->
|
<!-- 导入进度 -->
|
||||||
<div v-if="isImporting" class="import-progress">
|
<VCard variant="outlined" class="mb-4">
|
||||||
<VProgressLinear
|
<VCardTitle class="text-subtitle-1">
|
||||||
v-model="importProgress"
|
{{ t('site.messages.importing', { progress: importProgress }) }}
|
||||||
color="primary"
|
</VCardTitle>
|
||||||
height="8"
|
<VCardText>
|
||||||
rounded
|
<VProgressLinear v-model="importProgress" color="primary" height="8" rounded class="mb-2" />
|
||||||
class="mb-2"
|
<p class="text-caption text-center">{{ importProgress }}%</p>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 阶段4:导入完成阶段 -->
|
||||||
|
<div v-if="currentStage === ImportStage.IMPORT_COMPLETE" class="result-area">
|
||||||
|
<!-- 成功导入的站点 -->
|
||||||
|
<div v-if="importSuccesses.length > 0" class="success-sites mb-4">
|
||||||
|
<VAlert
|
||||||
|
type="success"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-4"
|
||||||
|
:text="t('site.messages.importSuccess', { count: importSuccesses.length })"
|
||||||
/>
|
/>
|
||||||
<p class="text-caption text-center">{{ t('site.messages.importing', { progress: importProgress }) }}</p>
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误详情 -->
|
||||||
|
<div v-if="showErrorDetails && importErrors.length > 0" class="error-details">
|
||||||
|
<VAlert
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-4"
|
||||||
|
:text="t('site.messages.importErrors', { count: importErrors.length })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VCard variant="outlined" class="mb-4">
|
||||||
|
<VCardTitle class="text-subtitle-1 d-flex align-center justify-space-between">
|
||||||
|
{{ t('site.errors.title') }}
|
||||||
|
</VCardTitle>
|
||||||
|
<!-- 错误信息详情 -->
|
||||||
|
<VExpansionPanels class="mt-4">
|
||||||
|
<VExpansionPanel v-for="(error, index) in importErrors" :key="index">
|
||||||
|
<VExpansionPanelTitle>
|
||||||
|
{{ error.site.name || t('site.preview.unnamed') }} - {{ t('site.errors.details') }}
|
||||||
|
</VExpansionPanelTitle>
|
||||||
|
<VExpansionPanelText>
|
||||||
|
<VAlert type="error" variant="text" :text="error.error" class="mb-0" />
|
||||||
|
</VExpansionPanelText>
|
||||||
|
</VExpansionPanel>
|
||||||
|
</VExpansionPanels>
|
||||||
|
</VCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="d-flex justify-end gap-2">
|
<div class="d-flex justify-end gap-2">
|
||||||
<VBtn
|
<VBtn variant="text" @click="resetToFileSelection">
|
||||||
variant="text"
|
{{ t('common.reset') }}
|
||||||
@click="showPreview = false"
|
|
||||||
:disabled="isImporting"
|
|
||||||
>
|
|
||||||
{{ t('common.back') }}
|
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
<VBtn color="primary" @click="closeDialog(false)">
|
||||||
color="primary"
|
{{ t('common.close') }}
|
||||||
@click="importSites"
|
|
||||||
:loading="isImporting"
|
|
||||||
:disabled="importData.length === 0"
|
|
||||||
>
|
|
||||||
{{ t('site.actions.startImport') }}
|
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,24 +388,36 @@ watch(selectedFile, async (newFile) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.upload-area {
|
.upload-area {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone {
|
||||||
|
padding: 2rem;
|
||||||
border: 2px dashed #ccc;
|
border: 2px dashed #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-area.dragging {
|
.upload-zone.dragging {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-area {
|
.error-details {
|
||||||
max-height: 60vh;
|
margin-block: 1rem;
|
||||||
overflow-y: auto;
|
margin-inline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-progress {
|
.error-details .v-expansion-panels {
|
||||||
margin: 1rem 0;
|
background: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.border-success {
|
||||||
|
border-inline-start: 4px solid rgb(var(--v-theme-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-error {
|
||||||
|
border-inline-start: 4px solid rgb(var(--v-theme-error));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+11
-2
@@ -1033,8 +1033,8 @@ export default {
|
|||||||
actions: {
|
actions: {
|
||||||
add: 'Add Site',
|
add: 'Add Site',
|
||||||
edit: 'Edit Site',
|
edit: 'Edit Site',
|
||||||
import: 'Batch Import',
|
import: 'Import',
|
||||||
export: 'Batch Export',
|
export: 'Export',
|
||||||
startImport: 'Start Import',
|
startImport: 'Start Import',
|
||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
@@ -1047,6 +1047,7 @@ export default {
|
|||||||
importSuccess: 'Successfully imported {count} sites',
|
importSuccess: 'Successfully imported {count} sites',
|
||||||
importFailed: 'Failed to import sites',
|
importFailed: 'Failed to import sites',
|
||||||
importPartialFailed: 'Import completed, {success} successful, {failed} failed',
|
importPartialFailed: 'Import completed, {success} successful, {failed} failed',
|
||||||
|
importAllFailed: 'Import failed, all {count} sites failed to import',
|
||||||
noDataToImport: 'No data to import',
|
noDataToImport: 'No data to import',
|
||||||
noValidData: 'No valid data',
|
noValidData: 'No valid data',
|
||||||
someInvalidData: 'Some data is invalid, valid data: {valid}/{total}',
|
someInvalidData: 'Some data is invalid, valid data: {valid}/{total}',
|
||||||
@@ -1055,9 +1056,17 @@ export default {
|
|||||||
parseFileError: 'Failed to parse file, please check file format',
|
parseFileError: 'Failed to parse file, please check file format',
|
||||||
previewData: 'Preview data ({count} sites)',
|
previewData: 'Preview data ({count} sites)',
|
||||||
importing: 'Importing... ({progress}%)',
|
importing: 'Importing... ({progress}%)',
|
||||||
|
importErrors: 'Import encountered {count} errors',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
loadDownloader: 'Failed to load downloader settings',
|
loadDownloader: 'Failed to load downloader settings',
|
||||||
|
title: 'Import Error Details',
|
||||||
|
failed: 'Import Failed',
|
||||||
|
details: 'Error Details',
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
successTitle: 'Successfully Imported Sites',
|
||||||
|
success: 'Import Success',
|
||||||
},
|
},
|
||||||
testConnectivity: 'Test Connectivity',
|
testConnectivity: 'Test Connectivity',
|
||||||
testing: 'Testing ...',
|
testing: 'Testing ...',
|
||||||
|
|||||||
+11
-2
@@ -1029,8 +1029,8 @@ export default {
|
|||||||
actions: {
|
actions: {
|
||||||
add: '新增站点',
|
add: '新增站点',
|
||||||
edit: '编辑站点',
|
edit: '编辑站点',
|
||||||
import: '批量导入',
|
import: '导入',
|
||||||
export: '批量导出',
|
export: '导出',
|
||||||
startImport: '开始导入',
|
startImport: '开始导入',
|
||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
@@ -1043,6 +1043,7 @@ export default {
|
|||||||
importSuccess: '成功导入 {count} 个站点',
|
importSuccess: '成功导入 {count} 个站点',
|
||||||
importFailed: '站点导入失败',
|
importFailed: '站点导入失败',
|
||||||
importPartialFailed: '导入完成,成功 {success} 个,失败 {failed} 个',
|
importPartialFailed: '导入完成,成功 {success} 个,失败 {failed} 个',
|
||||||
|
importAllFailed: '导入失败,{count} 个站点全部导入失败',
|
||||||
noDataToImport: '没有数据可导入',
|
noDataToImport: '没有数据可导入',
|
||||||
noValidData: '没有有效的数据',
|
noValidData: '没有有效的数据',
|
||||||
someInvalidData: '部分数据无效,有效数据 {valid}/{total} 个',
|
someInvalidData: '部分数据无效,有效数据 {valid}/{total} 个',
|
||||||
@@ -1051,9 +1052,17 @@ export default {
|
|||||||
parseFileError: '文件解析失败,请检查文件格式',
|
parseFileError: '文件解析失败,请检查文件格式',
|
||||||
previewData: '预览数据 ({count} 个站点)',
|
previewData: '预览数据 ({count} 个站点)',
|
||||||
importing: '正在导入... ({progress}%)',
|
importing: '正在导入... ({progress}%)',
|
||||||
|
importErrors: '导入过程中出现 {count} 个错误',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
loadDownloader: '加载下载器设置失败',
|
loadDownloader: '加载下载器设置失败',
|
||||||
|
title: '导入错误详情',
|
||||||
|
failed: '导入失败',
|
||||||
|
details: '错误详情',
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
successTitle: '成功导入的站点',
|
||||||
|
success: '导入成功',
|
||||||
},
|
},
|
||||||
testConnectivity: '测试连通性',
|
testConnectivity: '测试连通性',
|
||||||
testing: '测试中 ...',
|
testing: '测试中 ...',
|
||||||
|
|||||||
+11
-2
@@ -1028,8 +1028,8 @@ export default {
|
|||||||
actions: {
|
actions: {
|
||||||
add: '新增站點',
|
add: '新增站點',
|
||||||
edit: '編輯站點',
|
edit: '編輯站點',
|
||||||
import: '批量導入',
|
import: '導入',
|
||||||
export: '批量導出',
|
export: '導出',
|
||||||
startImport: '開始導入',
|
startImport: '開始導入',
|
||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
@@ -1042,6 +1042,7 @@ export default {
|
|||||||
importSuccess: '成功導入 {count} 個站點',
|
importSuccess: '成功導入 {count} 個站點',
|
||||||
importFailed: '站點導入失敗',
|
importFailed: '站點導入失敗',
|
||||||
importPartialFailed: '導入完成,成功 {success} 個,失敗 {failed} 個',
|
importPartialFailed: '導入完成,成功 {success} 個,失敗 {failed} 個',
|
||||||
|
importAllFailed: '導入失敗,{count} 個站點全部導入失敗',
|
||||||
noDataToImport: '沒有數據可導入',
|
noDataToImport: '沒有數據可導入',
|
||||||
noValidData: '沒有有效的數據',
|
noValidData: '沒有有效的數據',
|
||||||
someInvalidData: '部分數據無效,有效數據 {valid}/{total} 個',
|
someInvalidData: '部分數據無效,有效數據 {valid}/{total} 個',
|
||||||
@@ -1050,9 +1051,17 @@ export default {
|
|||||||
parseFileError: '文件解析失敗,請檢查文件格式',
|
parseFileError: '文件解析失敗,請檢查文件格式',
|
||||||
previewData: '預覽數據 ({count} 個站點)',
|
previewData: '預覽數據 ({count} 個站點)',
|
||||||
importing: '正在導入... ({progress}%)',
|
importing: '正在導入... ({progress}%)',
|
||||||
|
importErrors: '導入過程中出現 {count} 個錯誤',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
loadDownloader: '加載下載器設置失敗',
|
loadDownloader: '加載下載器設置失敗',
|
||||||
|
title: '導入錯誤詳情',
|
||||||
|
failed: '導入失敗',
|
||||||
|
details: '錯誤詳情',
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
successTitle: '成功導入的站點',
|
||||||
|
success: '導入成功',
|
||||||
},
|
},
|
||||||
testConnectivity: '測試連通性',
|
testConnectivity: '測試連通性',
|
||||||
testing: '測試中 ...',
|
testing: '測試中 ...',
|
||||||
|
|||||||
@@ -224,8 +224,8 @@ function selectFilter(value: string) {
|
|||||||
async function exportSites() {
|
async function exportSites() {
|
||||||
try {
|
try {
|
||||||
// 获取所有站点数据
|
// 获取所有站点数据
|
||||||
const sites = await api.get('site/')
|
const sites: Site[] = await api.get('site/')
|
||||||
|
|
||||||
// 创建导出数据,只包含必要的字段
|
// 创建导出数据,只包含必要的字段
|
||||||
const exportData = sites.map((site: Site) => ({
|
const exportData = sites.map((site: Site) => ({
|
||||||
name: site.name,
|
name: site.name,
|
||||||
@@ -247,12 +247,12 @@ async function exportSites() {
|
|||||||
limit_count: site.limit_count,
|
limit_count: site.limit_count,
|
||||||
limit_seconds: site.limit_seconds,
|
limit_seconds: site.limit_seconds,
|
||||||
is_active: site.is_active,
|
is_active: site.is_active,
|
||||||
pri: site.pri
|
pri: site.pri,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 创建Blob对象
|
// 创建Blob对象
|
||||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||||
|
|
||||||
// 创建下载链接
|
// 创建下载链接
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
@@ -262,7 +262,7 @@ async function exportSites() {
|
|||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
// 显示成功提示
|
// 显示成功提示
|
||||||
$toast.success(t('site.messages.exportSuccess'))
|
$toast.success(t('site.messages.exportSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -302,14 +302,14 @@ useDynamicButton({
|
|||||||
<div class="d-flex align-center gap-2">
|
<div class="d-flex align-center gap-2">
|
||||||
<!-- 导入按钮 -->
|
<!-- 导入按钮 -->
|
||||||
<VBtn :icon="display.smAndDown.value" variant="text" color="success" @click="siteImportDialog = true">
|
<VBtn :icon="display.smAndDown.value" variant="text" color="success" @click="siteImportDialog = true">
|
||||||
<VIcon icon="mdi-upload" />
|
<VIcon icon="mdi-import" />
|
||||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||||
{{ t('site.actions.import') }}
|
{{ t('site.actions.import') }}
|
||||||
</span>
|
</span>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<!-- 导出按钮 -->
|
<!-- 导出按钮 -->
|
||||||
<VBtn :icon="display.smAndDown.value" variant="text" color="primary" @click="exportSites">
|
<VBtn :icon="display.smAndDown.value" variant="text" color="warning" @click="exportSites">
|
||||||
<VIcon icon="mdi-download" />
|
<VIcon icon="mdi-export" />
|
||||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||||
{{ t('site.actions.export') }}
|
{{ t('site.actions.export') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -416,7 +416,7 @@ useDynamicButton({
|
|||||||
|
|
||||||
<!-- 统计信息弹窗 -->
|
<!-- 统计信息弹窗 -->
|
||||||
<SiteStatisticsDialog v-if="siteStatsDialog" v-model="siteStatsDialog" :sites="siteList" />
|
<SiteStatisticsDialog v-if="siteStatsDialog" v-model="siteStatsDialog" :sites="siteList" />
|
||||||
|
|
||||||
<!-- 导入站点弹窗 -->
|
<!-- 导入站点弹窗 -->
|
||||||
<SiteImportDialog v-if="siteImportDialog" v-model="siteImportDialog" @import-success="fetchData" />
|
<SiteImportDialog v-if="siteImportDialog" v-model="siteImportDialog" @import-success="fetchData" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user