feat: подробное отображение ошибок синхронизации
This commit is contained in:
@@ -166,6 +166,7 @@ export interface SyncResultDto {
|
||||
updated: number
|
||||
skipped: number
|
||||
error?: string | null
|
||||
details?: string[] | null
|
||||
}
|
||||
|
||||
export interface UserAchievementDto {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { coursesApi, lecturesApi, locationsApi, syncApi, tagsApi } from '@/api'
|
||||
import { ApiError } from '@/api/client'
|
||||
import type {
|
||||
ApiScheduleTypeId,
|
||||
CourseDto,
|
||||
@@ -30,6 +31,7 @@ const loading = ref(false)
|
||||
const syncingSchedule = ref(false)
|
||||
const syncingRooms = ref(false)
|
||||
const syncError = ref('')
|
||||
const syncErrorDetails = ref<string[]>([])
|
||||
const syncStatus = ref<SyncStatusDto | null>(null)
|
||||
const syncResult = ref<SyncResultDto | null>(null)
|
||||
const addToast = inject('addToast') as ((message: string, type?: 'success' | 'error' | 'info') => void) | undefined
|
||||
@@ -66,6 +68,10 @@ const syncMeta = computed(() => {
|
||||
})
|
||||
|
||||
const visibleSyncResult = computed(() => syncResult.value ?? syncStatus.value?.lastResult ?? null)
|
||||
const visibleSyncDetails = computed(() => {
|
||||
if (syncErrorDetails.value.length) return syncErrorDetails.value
|
||||
return visibleSyncResult.value?.details ?? []
|
||||
})
|
||||
|
||||
const tabConfig: Record<TabKey, TabConfig> = {
|
||||
lectures: {
|
||||
@@ -182,6 +188,7 @@ async function refreshSyncStatus() {
|
||||
async function runScheduleSync() {
|
||||
syncingSchedule.value = true
|
||||
syncError.value = ''
|
||||
syncErrorDetails.value = []
|
||||
syncResult.value = null
|
||||
try {
|
||||
syncResult.value = await syncApi.schedule({
|
||||
@@ -193,6 +200,7 @@ async function runScheduleSync() {
|
||||
|
||||
if (syncResult.value.error) {
|
||||
syncError.value = syncResult.value.error
|
||||
syncErrorDetails.value = syncResult.value.details ?? []
|
||||
addToast?.('Синхронизация завершилась с ошибкой.', 'error')
|
||||
} else {
|
||||
addToast?.('Расписание синхронизировано.', 'success')
|
||||
@@ -201,6 +209,7 @@ async function runScheduleSync() {
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
syncError.value = err instanceof Error ? err.message : 'Не удалось синхронизировать расписание.'
|
||||
syncErrorDetails.value = extractApiErrorDetails(err)
|
||||
addToast?.(syncError.value, 'error')
|
||||
await refreshSyncStatus()
|
||||
} finally {
|
||||
@@ -211,19 +220,41 @@ async function runScheduleSync() {
|
||||
async function runRoomsSync() {
|
||||
syncingRooms.value = true
|
||||
syncError.value = ''
|
||||
syncErrorDetails.value = []
|
||||
syncResult.value = null
|
||||
try {
|
||||
syncResult.value = await syncApi.rooms()
|
||||
addToast?.('Аудитории синхронизированы.', 'success')
|
||||
if (syncResult.value.error) {
|
||||
syncError.value = syncResult.value.error
|
||||
syncErrorDetails.value = syncResult.value.details ?? []
|
||||
addToast?.('Синхронизация аудиторий завершилась с ошибкой.', 'error')
|
||||
} else {
|
||||
addToast?.('Аудитории синхронизированы.', 'success')
|
||||
}
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
syncError.value = err instanceof Error ? err.message : 'Не удалось синхронизировать аудитории.'
|
||||
syncErrorDetails.value = extractApiErrorDetails(err)
|
||||
addToast?.(syncError.value, 'error')
|
||||
} finally {
|
||||
syncingRooms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function extractApiErrorDetails(err: unknown) {
|
||||
if (!(err instanceof ApiError)) return []
|
||||
const details = err.details
|
||||
if (!details || typeof details !== 'object') return []
|
||||
|
||||
const problem = details as { title?: unknown; detail?: unknown; traceId?: unknown; status?: unknown }
|
||||
return [
|
||||
typeof problem.title === 'string' ? `title=${problem.title}` : '',
|
||||
typeof problem.status === 'number' ? `status=${problem.status}` : '',
|
||||
typeof problem.detail === 'string' ? `detail=${problem.detail}` : '',
|
||||
typeof problem.traceId === 'string' ? `traceId=${problem.traceId}` : '',
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadData()
|
||||
})
|
||||
@@ -282,6 +313,12 @@ onMounted(() => {
|
||||
<div v-if="syncError || visibleSyncResult?.error" class="sync-error">
|
||||
{{ syncError || visibleSyncResult?.error }}
|
||||
</div>
|
||||
<details v-if="visibleSyncDetails.length" class="sync-details">
|
||||
<summary>Подробности ошибки</summary>
|
||||
<ul>
|
||||
<li v-for="detail in visibleSyncDetails" :key="detail">{{ detail }}</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn-primary" type="submit" :disabled="syncingSchedule">
|
||||
@@ -326,6 +363,17 @@ onMounted(() => {
|
||||
.sync-meta { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; }
|
||||
.sync-result { font-size: 13px; color: var(--color-text-secondary); }
|
||||
.sync-error { font-size: 13px; color: var(--color-error); }
|
||||
.sync-details {
|
||||
border: 1px solid rgba(239,68,68,0.24);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 10px;
|
||||
background: rgba(254,242,242,0.68);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.sync-details summary { cursor: pointer; color: var(--color-error); font-weight: 600; }
|
||||
.sync-details ul { margin: 8px 0 0; padding-left: 18px; overflow-wrap: anywhere; }
|
||||
.sync-details li + li { margin-top: 4px; }
|
||||
.sync-status {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--color-border-glass);
|
||||
|
||||
Reference in New Issue
Block a user