feat: add progress bar and ws client for async import

This commit is contained in:
2026-01-05 18:07:02 +03:00
parent 4dfecf7fcf
commit 8ba21f915f

View File

@@ -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>