This commit is contained in:
2026-05-07 18:05:17 +03:00
parent 59e283945c
commit 1a5b10b129

View File

@@ -24,6 +24,11 @@
Импорт JSON Импорт JSON
<input type="file" accept=".json" @change="importConfigFromJson" class="hidden"> <input type="file" accept=".json" @change="importConfigFromJson" class="hidden">
</label> </label>
<button @click="openExitModal" class="btn-danger flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div> </div>
</div> </div>
@@ -494,6 +499,30 @@
</div> </div>
</div> </div>
</Transition> </Transition>
<!-- Модалка подтверждения выхода без сохранения -->
<Transition name="fade">
<div v-if="exitConfirmModal.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="exitConfirmModal.show = false">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full">
<div class="p-6 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-amber-100 mb-4">
<svg class="h-6 w-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Несохранённые изменения</h3>
<p class="text-sm text-gray-500 mb-6">Вы уверены, что хотите выйти? Все несохранённые данные будут потеряны.</p>
<div class="flex justify-center space-x-3">
<button @click="exitConfirmModal.show = false" class="btn-secondary">Остаться</button>
<button @click="confirmExit" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">Выйти</button>
</div>
</div>
</div>
</div>
</div>
</Transition>
</AppLayout> </AppLayout>
</template> </template>
@@ -585,7 +614,9 @@ const active = ref(false)
const searchQuery = ref('') const searchQuery = ref('')
const activeTab = ref<'table' | 'sql'>('table') const activeTab = ref<'table' | 'sql'>('table')
const collapsed = ref({ number: false, category: false, filter: false }) const collapsed = ref({ number: false, category: false, filter: false })
const isLoadingQuery = ref(false)
const isDirty = ref(false)
const isReady = ref(false)
// Поля для сохранения запроса // Поля для сохранения запроса
const queryId = ref<number | null>(null) const queryId = ref<number | null>(null)
const queryName = ref('') const queryName = ref('')
@@ -615,8 +646,13 @@ let dragReorderType: string | null = null
let dragReorderFromIdx: number | null = null let dragReorderFromIdx: number | null = null
const resetModal = ref({ show: false }) const resetModal = ref({ show: false })
const exitConfirmModal = ref({ show: false })
// ---------- Вспомогательные функции ---------- // ---------- Вспомогательные функции ----------
function markDirty() {
isDirty.value = true
}
const buildAvailableFields = (): AvailableField[] => { const buildAvailableFields = (): AvailableField[] => {
if (!columnsData.value.length) return [] if (!columnsData.value.length) return []
const selected = reportType.value const selected = reportType.value
@@ -757,9 +793,29 @@ function triggerSqlUpdate() {
} }
// Наблюдение за изменениями для перегенерации SQL // Наблюдение за изменениями для перегенерации SQL
watch([rowFields, columnFields, valueFields, filterFields, reportType, buildSummary, dateTo, daysBack, tableName, selectedDbConnection], () => { watch(
triggerSqlUpdate() [
}, { deep: true }) rowFields,
columnFields,
valueFields,
filterFields,
reportType,
buildSummary,
dateTo,
daysBack,
tableName,
active,
queryName,
selectedRestaurants,
selectedDbConnection
],
() => {
if (isReady.value && !isLoadingQuery.value) {
isDirty.value = true
}
},
{ deep: true }
)
// ---------- Экспорт / импорт JSON (iikoконфиг) ---------- // ---------- Экспорт / импорт JSON (iikoконфиг) ----------
async function exportConfigAsJson() { async function exportConfigAsJson() {
@@ -855,6 +911,7 @@ function importConfigFromJson(event: Event) {
} }
}) })
} }
isDirty.value = true
showNotification('Конфигурация iiko загружена', 'success') showNotification('Конфигурация iiko загружена', 'success')
} catch (err: any) { } catch (err: any) {
showNotification('Ошибка при загрузке JSON: ' + err.message, 'error') showNotification('Ошибка при загрузке JSON: ' + err.message, 'error')
@@ -866,6 +923,7 @@ function importConfigFromJson(event: Event) {
// ---------- Работа с сохранёнными запросами (CRUD через API) ---------- // ---------- Работа с сохранёнными запросами (CRUD через API) ----------
async function loadQuery(id: number) { async function loadQuery(id: number) {
isLoadingQuery.value = true
try { try {
const res = await fetch(`/api/olap/queries/${id}`) const res = await fetch(`/api/olap/queries/${id}`)
if (!res.ok) throw new Error() if (!res.ok) throw new Error()
@@ -873,8 +931,7 @@ async function loadQuery(id: number) {
// 1. Базовые поля запроса // 1. Базовые поля запроса
queryName.value = data.name queryName.value = data.name
active.value = data.active ?? true
active.value = data.active ?? true;
// 2. Подключение к БД // 2. Подключение к БД
if (data.dbConnectionId) { if (data.dbConnectionId) {
@@ -931,7 +988,7 @@ async function loadQuery(id: number) {
valueFields.value.push({ valueFields.value.push({
...(field as NumberField), ...(field as NumberField),
id: Date.now() + Math.random().toString(), id: Date.now() + Math.random().toString(),
aggregation: 'sum' // по умолчанию, можно расширить если в конфиге будет aggregation: 'sum'
}) })
} }
} }
@@ -956,10 +1013,12 @@ async function loadQuery(id: number) {
} }
} }
// 10. Обновляем SQL (вызовется автоматически через watch)
triggerSqlUpdate() triggerSqlUpdate()
isDirty.value = false
} catch (e: any) { } catch (e: any) {
showNotification('Ошибка загрузки запроса: ' + e.message, 'error') showNotification('Ошибка загрузки запроса: ' + e.message, 'error')
} finally {
isLoadingQuery.value = false
} }
} }
@@ -998,7 +1057,7 @@ async function saveQuery() {
}) })
if (!res.ok) throw new Error(await res.text()) if (!res.ok) throw new Error(await res.text())
showNotification('Запрос сохранён', 'success') showNotification('Запрос сохранён', 'success')
router.push('/olap/queries') isDirty.value = false
} catch (e: any) { } catch (e: any) {
showNotification('Ошибка сохранения: ' + e.message, 'error') showNotification('Ошибка сохранения: ' + e.message, 'error')
} }
@@ -1209,6 +1268,7 @@ const confirmReset = () => {
selectedRestaurants.value = [] selectedRestaurants.value = []
selectedDbConnection.value = null selectedDbConnection.value = null
sqlScript.value = '-- Выберите подключение к БД для генерации SQL' sqlScript.value = '-- Выберите подключение к БД для генерации SQL'
isDirty.value = true
showNotification('Все настройки сброшены', 'success') showNotification('Все настройки сброшены', 'success')
} }
const toggleSection = (section: 'number' | 'category' | 'filter') => { const toggleSection = (section: 'number' | 'category' | 'filter') => {
@@ -1265,17 +1325,36 @@ const filteredDbConnectionsList = computed(() => {
return dbConnectionsList.value.filter(c => c.name.toLowerCase().includes(lower)) return dbConnectionsList.value.filter(c => c.name.toLowerCase().includes(lower))
}) })
function openExitModal() {
if (isDirty.value) {
exitConfirmModal.value.show = true
} else {
router.push('/olap/queries')
}
}
function confirmExit() {
exitConfirmModal.value.show = false
router.push('/olap/queries')
}
watch(reportType, () => { watch(reportType, () => {
refreshFieldsAndReset() if (isReady.value) refreshFieldsAndReset()
}) })
onMounted(async () => { onMounted(async () => {
isLoadingQuery.value = true
await fetchColumns() await fetchColumns()
isLoadingQuery.value = false
if (route.params.id) { if (route.params.id) {
queryId.value = parseInt(route.params.id as string) queryId.value = parseInt(route.params.id as string)
await loadQuery(queryId.value) await loadQuery(queryId.value)
} else {
isDirty.value = false
} }
triggerSqlUpdate() triggerSqlUpdate()
isReady.value = true // <- Страница полностью загружена
}) })
</script> </script>
@@ -1299,4 +1378,7 @@ onMounted(async () => {
background: #cbd5e1; background: #cbd5e1;
border-radius: 4px; border-radius: 4px;
} }
.btn-danger {
@apply bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition;
}
</style> </style>