feat: add proxy import flow
This commit is contained in:
@@ -18,15 +18,26 @@
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.dataImportFile') }}</label>
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-dark-600 dark:bg-dark-800"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm text-gray-700 dark:text-dark-200">
|
||||
{{ fileName || t('admin.accounts.dataImportSelectFile') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-dark-400">JSON (.json)</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary shrink-0" @click="openFilePicker">
|
||||
{{ t('common.chooseFile') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="input"
|
||||
class="hidden"
|
||||
accept="application/json,.json"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<p v-if="fileName" class="mt-2 text-xs text-gray-500 dark:text-dark-400">
|
||||
{{ fileName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -100,6 +111,7 @@ const importing = ref(false)
|
||||
const file = ref<File | null>(null)
|
||||
const result = ref<AdminDataImportResult | null>(null)
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const fileName = computed(() => file.value?.name || '')
|
||||
|
||||
const errorItems = computed(() => result.value?.errors || [])
|
||||
@@ -110,10 +122,17 @@ watch(
|
||||
if (open) {
|
||||
file.value = null
|
||||
result.value = null
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
file.value = target.files?.[0] || null
|
||||
|
||||
183
frontend/src/components/admin/proxy/ImportDataModal.vue
Normal file
183
frontend/src/components/admin/proxy/ImportDataModal.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="t('admin.proxies.dataImportTitle')"
|
||||
width="normal"
|
||||
close-on-click-outside
|
||||
@close="handleClose"
|
||||
>
|
||||
<form id="import-proxy-data-form" class="space-y-4" @submit.prevent="handleImport">
|
||||
<div class="text-sm text-gray-600 dark:text-dark-300">
|
||||
{{ t('admin.proxies.dataImportHint') }}
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-600 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
{{ t('admin.proxies.dataImportWarning') }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.proxies.dataImportFile') }}</label>
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-dark-600 dark:bg-dark-800"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm text-gray-700 dark:text-dark-200">
|
||||
{{ fileName || t('admin.proxies.dataImportSelectFile') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-dark-400">JSON (.json)</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary shrink-0" @click="openFilePicker">
|
||||
{{ t('common.chooseFile') }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
accept="application/json,.json"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="result"
|
||||
class="space-y-2 rounded-xl border border-gray-200 p-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.proxies.dataImportResult') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 dark:text-dark-300">
|
||||
{{ t('admin.proxies.dataImportResultSummary', result) }}
|
||||
</div>
|
||||
|
||||
<div v-if="errorItems.length" class="mt-2">
|
||||
<div class="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
{{ t('admin.proxies.dataImportErrors') }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 max-h-48 overflow-auto rounded-lg bg-gray-50 p-3 font-mono text-xs dark:bg-dark-800"
|
||||
>
|
||||
<div v-for="(item, idx) in errorItems" :key="idx" class="whitespace-pre-wrap">
|
||||
{{ item.kind }} {{ item.name || item.proxy_key || '-' }} — {{ item.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-secondary" type="button" :disabled="importing" @click="handleClose">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
form="import-proxy-data-form"
|
||||
:disabled="importing"
|
||||
>
|
||||
{{ importing ? t('admin.proxies.dataImporting') : t('admin.proxies.dataImportButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import type { AdminDataImportResult } from '@/types'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
(e: 'imported'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const importing = ref(false)
|
||||
const file = ref<File | null>(null)
|
||||
const result = ref<AdminDataImportResult | null>(null)
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const fileName = computed(() => file.value?.name || '')
|
||||
|
||||
const errorItems = computed(() => result.value?.errors || [])
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(open) => {
|
||||
if (open) {
|
||||
file.value = null
|
||||
result.value = null
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
file.value = target.files?.[0] || null
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (importing.value) return
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file.value) {
|
||||
appStore.showError(t('admin.proxies.dataImportSelectFile'))
|
||||
return
|
||||
}
|
||||
|
||||
importing.value = true
|
||||
try {
|
||||
const text = await file.value.text()
|
||||
const dataPayload = JSON.parse(text)
|
||||
|
||||
const res = await adminAPI.proxies.importData({ data: dataPayload })
|
||||
|
||||
result.value = res
|
||||
|
||||
const msgParams: Record<string, unknown> = {
|
||||
proxy_created: res.proxy_created,
|
||||
proxy_reused: res.proxy_reused,
|
||||
proxy_failed: res.proxy_failed
|
||||
}
|
||||
|
||||
if (res.proxy_failed > 0) {
|
||||
appStore.showError(t('admin.proxies.dataImportCompletedWithErrors', msgParams))
|
||||
} else {
|
||||
appStore.showSuccess(t('admin.proxies.dataImportSuccess', msgParams))
|
||||
emit('imported')
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error instanceof SyntaxError) {
|
||||
appStore.showError(t('admin.proxies.dataImportParseFailed'))
|
||||
} else {
|
||||
appStore.showError(error?.message || t('admin.proxies.dataImportFailed'))
|
||||
}
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user