feat: add progress bar and ws client for async import
This commit is contained in:
@@ -49,11 +49,21 @@
|
||||
<input id="sourceUrl" v-model="form.sourceUrl" type="url" :required="form.importMethod === 'url'" />
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="successMessage" class="success-message">{{ successMessage }}</div>
|
||||
<div v-if="importJob.jobId" class="job-status">
|
||||
<h3>Статус импорта</h3>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" :style="{ width: importJob.progress + '%' }"></div>
|
||||
</div>
|
||||
<p>{{ importJob.message }} ({{ importJob.progress }}%)</p>
|
||||
<div v-if="importJob.status === 'failed'" class="error-message">Ошибка: {{ importJob.message }}</div>
|
||||
<div v-if="importJob.status === 'completed'" class="success-message">Импорт успешно завершен!</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="isLoading">
|
||||
{{ isLoading ? "Импорт..." : "Импортировать" }}
|
||||
<div v-if="error && !importJob.jobId" class="error-message">{{ error }}</div>
|
||||
<!-- <div v-if="successMessage" class="success-message">{{ successMessage }}</div> -->
|
||||
|
||||
<button type="submit" :disabled="isLoading || (importJob.jobId !== null && importJob.status !== 'completed' && importJob.status !== 'failed')">
|
||||
{{ isLoading ? "Запуск..." : "Импортировать" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -61,8 +71,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ref, reactive, watch, onUnmounted } from "vue";
|
||||
import apiClient from "@/api/axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
@@ -78,6 +91,16 @@ const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const successMessage = ref<string | null>(null);
|
||||
|
||||
// Состояние задачи импорта
|
||||
const importJob = reactive({
|
||||
jobId: null as number | null,
|
||||
status: "" as string,
|
||||
progress: 0,
|
||||
message: "" as string,
|
||||
});
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
watch(
|
||||
() => form.importerType,
|
||||
(newType) => {
|
||||
@@ -94,6 +117,57 @@ const onFileSelected = (event: Event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const connectWebSocket = () => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const host = window.location.host;
|
||||
const url = `${protocol}//${host}/api/admin/ws/jobs?token=${authStore.token}`;
|
||||
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.job_id === importJob.jobId) {
|
||||
importJob.status = data.status;
|
||||
importJob.progress = data.progress;
|
||||
importJob.message = data.error_message || getStatusMessage(data.status);
|
||||
|
||||
if (data.status === 'completed') {
|
||||
successMessage.value = "Импорт завершен!";
|
||||
isLoading.value = false;
|
||||
} else if (data.status === 'failed') {
|
||||
error.value = data.error_message;
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("WS parse error", e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WS closed");
|
||||
if (importJob.status !== 'completed' && importJob.status !== 'failed' && importJob.jobId) {
|
||||
// Reconnect logic could be here
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getStatusMessage = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'Ожидание очереди...';
|
||||
case 'downloading': return 'Скачивание файлов...';
|
||||
case 'processing': return 'Обработка...';
|
||||
case 'completed': return 'Готово';
|
||||
case 'failed': return 'Ошибка';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (form.importMethod === "file" && !form.file) {
|
||||
error.value = "Пожалуйста, выберите файл для импорта.";
|
||||
@@ -107,6 +181,12 @@ const handleImport = async () => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
successMessage.value = null;
|
||||
|
||||
// Сброс состояния задачи
|
||||
importJob.jobId = null;
|
||||
importJob.status = "";
|
||||
importJob.progress = 0;
|
||||
importJob.message = "";
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("name", form.name);
|
||||
@@ -127,13 +207,28 @@ const handleImport = async () => {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
successMessage.value = response.data;
|
||||
|
||||
// Обработка асинхронного ответа
|
||||
if (response.status === 202 && response.data.job_id) {
|
||||
importJob.jobId = response.data.job_id;
|
||||
importJob.status = 'pending';
|
||||
importJob.message = 'Задача создана...';
|
||||
connectWebSocket();
|
||||
} else {
|
||||
// Fallback for sync behavior (old) or other codes
|
||||
successMessage.value = response.data;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data || "Произошла ошибка при импорте.";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) ws.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user