This commit is contained in:
2026-05-07 00:01:28 +03:00
parent ec6c52c79d
commit 8386be18ba

View File

@@ -19,35 +19,23 @@
</div>
</div>
<div class="flex flex-col lg:flex-row gap-6">
<!-- Левая панель - поля (с прокруткой и сворачиванием) -->
<div class="lg:w-72 card p-5 flex flex-col max-h-[calc(100vh-12rem)] lg:sticky lg:top-4 self-start">
<h3 class="text-xl font-bold mb-4">Поля</h3>
<div class="mb-3">
<input type="text" v-model="searchQuery" placeholder="Поиск по названию или тегам" class="input-field">
<div class="flex flex-row gap-6">
<!-- Левая панель - поля (sticky, на всю высоту) -->
<div class="w-72 shrink-0 card p-4 flex flex-col lg:sticky lg:top-4 h-[calc(100vh-12rem)]">
<h3 class="text-lg font-bold mb-3">Поля</h3>
<div class="mb-2">
<input type="text" v-model="searchQuery" placeholder="Поиск по названию или тегам" class="input-field py-1.5 text-sm">
<div class="text-xs text-gray-500 mt-1" v-if="searchQuery">Найдено: {{ filteredAvailableFields.length }} / {{ availableFields.length }}</div>
</div>
<div v-if="loading" class="text-center py-8">
<svg class="animate-spin h-8 w-8 text-primary-600 mx-auto" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="mt-2 text-gray-500">Загрузка полей...</p>
</div>
<div v-else-if="error" class="bg-red-50 text-red-700 p-3 rounded-xl text-sm border border-red-200">
{{ error }}
</div>
<div v-else-if="availableFields.length === 0" class="text-center py-8 text-gray-500">
Нет доступных полей. Возможно, требуется инициализация структуры.
</div>
<div v-else class="flex-1 overflow-y-auto space-y-4 pr-1">
<div v-if="loading" class="text-center py-8">...</div>
<div v-else-if="error" class="bg-red-50 text-red-700 p-3 rounded-xl text-sm">...</div>
<div v-else-if="availableFields.length === 0" class="text-center py-8 text-gray-500">...</div>
<div v-else class="flex-1 overflow-y-auto space-y-3 pr-1">
<!-- Числовые -->
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('number')">
<h4 class="text-gray-600 text-sm font-bold">Числовые VALUES</h4>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">{{ filteredNumberFields.length }}</span>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.number" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -57,13 +45,10 @@
</svg>
</button>
</div>
</div>
<div v-show="!collapsed.number" class="mt-2">
<div v-show="!collapsed.number" class="mt-2 space-y-1.5">
<div v-for="field in filteredNumberFields" :key="field.id"
class="bg-purple-600 text-white p-3 rounded-xl mb-2 cursor-grab active:cursor-grabbing"
draggable="true"
@dragstart="dragStart($event, field)"
@dragend="dragEnd">
class="bg-purple-600 text-white p-2 rounded-lg cursor-grab active:cursor-grabbing text-sm"
draggable="true" @dragstart="dragStart($event, field)" @dragend="dragEnd">
{{ field.name }}
</div>
</div>
@@ -73,8 +58,6 @@
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('category')">
<h4 class="text-gray-600 text-sm font-bold">Категории ROW / COLUMN</h4>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">{{ filteredCategoryFields.length }}</span>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.category" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -84,13 +67,10 @@
</svg>
</button>
</div>
</div>
<div v-show="!collapsed.category" class="mt-2">
<div v-show="!collapsed.category" class="mt-2 space-y-1.5">
<div v-for="field in filteredCategoryFields" :key="field.id"
class="bg-pink-500 text-white p-3 rounded-xl mb-2 cursor-grab active:cursor-grabbing"
draggable="true"
@dragstart="dragStart($event, field)"
@dragend="dragEnd">
class="bg-pink-500 text-white p-2 rounded-lg cursor-grab active:cursor-grabbing text-sm"
draggable="true" @dragstart="dragStart($event, field)" @dragend="dragEnd">
{{ field.name }}
</div>
</div>
@@ -100,8 +80,6 @@
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('filter')">
<h4 class="text-gray-600 text-sm font-bold">Фильтры</h4>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-400">{{ filteredFilterFields.length }}</span>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.filter" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -111,64 +89,61 @@
</svg>
</button>
</div>
</div>
<div v-show="!collapsed.filter" class="mt-2">
<div v-show="!collapsed.filter" class="mt-2 space-y-1.5">
<div v-for="field in filteredFilterFields" :key="field.id"
class="bg-cyan-600 text-white p-3 rounded-xl mb-2 cursor-grab active:cursor-grabbing"
draggable="true"
@dragstart="dragStart($event, field)"
@dragend="dragEnd">
class="bg-cyan-600 text-white p-2 rounded-lg cursor-grab active:cursor-grabbing text-sm"
draggable="true" @dragstart="dragStart($event, field)" @dragend="dragEnd">
{{ field.name }}
</div>
</div>
</div>
</div>
<div class="mt-5">
<button @click="openResetModal" class="btn-secondary w-full">Сбросить всё</button>
<div class="mt-3">
<button @click="openResetModal" class="btn-secondary w-full py-1.5 text-sm">Сбросить всё</button>
</div>
</div>
<!-- Правая часть -->
<div class="flex-1">
<div class="flex-1 overflow-x-auto">
<!-- Верхняя панель настроек -->
<div class="card p-4 mb-4 flex flex-wrap items-center gap-4 justify-between">
<div class="flex items-center gap-3">
<span class="font-bold text-gray-700">Тип отчета:</span>
<select v-model="reportType" class="input-field w-auto">
<div class="card p-3 mb-4 flex flex-wrap items-center gap-3 justify-between">
<div class="flex items-center gap-2">
<span class="font-bold text-gray-700 text-sm">Тип отчета:</span>
<select v-model="reportType" class="input-field w-auto py-1 text-sm">
<option value="SALES">SALES</option>
<option value="DELIVERIES">DELIVERIES</option>
<option value="TRANSACTIONS">TRANSACTIONS</option>
</select>
</div>
<label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-1.5 cursor-pointer text-sm">
<input type="checkbox" v-model="buildSummary" class="w-4 h-4 rounded border-gray-300 text-primary-600">
<span class="font-bold">buildSummary</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-1.5 cursor-pointer text-sm">
<input type="checkbox" v-model="active" class="w-4 h-4 rounded border-gray-300 text-primary-600">
<span class="font-bold">Активно (UI)</span>
</label>
</div>
<!-- Имя таблицы -->
<div class="card p-4 mb-4">
<label class="font-medium text-gray-700">Имя таблицы ClickHouse</label>
<div class="card p-3 mb-4">
<label class="font-medium text-gray-700 text-sm">Имя таблицы ClickHouse</label>
<input type="text" v-model="tableName" @input="validateTableName"
class="input-field mt-1"
class="input-field mt-1 py-1.5 text-sm"
:class="{'border-green-500 bg-green-50': tableNameValid && tableName, 'border-red-500 bg-red-50': tableNameTouched && !tableNameValid && tableName}">
<div v-if="tableNameTouched && tableName && !tableNameValid" class="text-red-500 text-sm mt-1">Должно начинаться с буквы</div>
<div v-if="tableNameTouched && tableName && !tableNameValid" class="text-red-500 text-xs mt-1">Должно начинаться с буквы</div>
</div>
<!-- Настройки даты -->
<div class="card p-4 mb-4 grid grid-cols-2 gap-4">
<div class="card p-3 mb-4 grid grid-cols-2 gap-3">
<div>
<label class="font-medium text-gray-700">Дата до (конец дня)</label>
<input type="date" v-model="dateTo" class="input-field">
<label class="font-medium text-gray-700 text-sm">Дата до (конец дня)</label>
<input type="date" v-model="dateTo" class="input-field py-1.5 text-sm">
</div>
<div>
<label class="font-medium text-gray-700">Дней назад (1)</label>
<input type="number" v-model.number="daysBack" min="1" step="1" class="input-field">
<label class="font-medium text-gray-700 text-sm">Дней назад (1)</label>
<input type="number" v-model.number="daysBack" min="1" step="1" class="input-field py-1.5 text-sm">
</div>
</div>
@@ -176,68 +151,68 @@
<div class="card overflow-hidden">
<div class="flex border-b border-gray-200">
<button @click="activeTab='table'"
class="px-4 py-2 text-sm font-medium transition-colors"
class="px-3 py-1.5 text-sm font-medium transition-colors"
:class="activeTab==='table' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
Таблица
</button>
<button @click="activeTab='sql'"
class="px-4 py-2 text-sm font-medium transition-colors"
class="px-3 py-1.5 text-sm font-medium transition-colors"
:class="activeTab==='sql' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
SQL скрипт
</button>
</div>
<div class="p-4">
<div class="p-3">
<div v-if="activeTab==='table'">
<!-- Фильтры -->
<div class="mb-4"
<div class="mb-3"
@dragover.prevent="dragOverZone = 'filter'"
@dragleave="dragOverZone = null"
@drop="dropOnZone('filter', $event)"
:class="{'ring-2 ring-primary-400 bg-primary-50 rounded-lg': dragOverZone === 'filter'}">
<h3 class="font-bold text-gray-800 mb-2">Пользовательские фильтры</h3>
<div class="flex flex-wrap gap-3">
<h3 class="font-bold text-gray-800 text-sm mb-1.5">Пользовательские фильтры</h3>
<div class="flex flex-wrap gap-2">
<div v-for="(f, idx) in filterFields" :key="f.id"
class="inline-flex items-center gap-2 bg-cyan-100 text-cyan-800 px-3 py-1 rounded-full text-sm">
class="inline-flex items-center gap-1.5 bg-cyan-100 text-cyan-800 px-2 py-1 rounded-full text-xs">
{{ f.name }}
<div class="filter-control inline-flex gap-1 ml-1">
<select v-model="f.filterType" class="text-xs bg-transparent border rounded px-1">
<select v-model="f.filterType" class="text-xs bg-transparent border rounded px-1 py-0">
<option value="IncludeValues">IncludeValues</option>
<option value="ExcludeValues">ExcludeValues</option>
<option value="EnumValue">EnumValue</option>
<option value="StringValue">StringValue</option>
</select>
<template v-if="f.filterType === 'IncludeValues' || f.filterType === 'ExcludeValues'">
<input type="text" v-model="f.valuesString" placeholder="знач1,знач2" class="text-xs border rounded px-1 w-28" @input="parseValues(f)">
<input type="text" v-model="f.valuesString" placeholder="знач1,знач2" class="text-xs border rounded px-1 w-24 py-0" @input="parseValues(f)">
</template>
<template v-else-if="f.filterType === 'EnumValue'">
<input v-model="f.enumKey" placeholder="enumKey" class="text-xs border rounded px-1 w-20">
<input v-model="f.enumValue" placeholder="значение" class="text-xs border rounded px-1 w-24">
<input v-model="f.enumKey" placeholder="enumKey" class="text-xs border rounded px-1 w-16 py-0">
<input v-model="f.enumValue" placeholder="значение" class="text-xs border rounded px-1 w-20 py-0">
</template>
<template v-else-if="f.filterType === 'StringValue'">
<input v-model="f.value" placeholder="значение" class="text-xs border rounded px-1 w-24">
<input v-model="f.value" placeholder="значение" class="text-xs border rounded px-1 w-20 py-0">
</template>
</div>
<button @click="removeFilter(idx)" class="text-cyan-600 hover:text-cyan-800 ml-1"></button>
<button @click="removeFilter(idx)" class="text-cyan-600 hover:text-cyan-800 ml-0.5"></button>
</div>
<div v-if="!filterFields.length" class="text-gray-400 text-sm">Перетащите поле фильтра</div>
<div v-if="!filterFields.length" class="text-gray-400 text-xs">Перетащите поле фильтра</div>
</div>
</div>
<!-- Таблица-сетка -->
<div class="overflow-x-auto border rounded-xl">
<table class="min-w-full divide-y divide-gray-200">
<div class="overflow-x-auto border rounded-lg">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead>
<tr>
<th class="bg-gray-100 p-2 text-center border-r border-gray-200"
<th class="bg-gray-100 p-1.5 text-center border-r border-gray-200"
:colspan="rowFields.length || 1"
@dragover.prevent="dragOverZone = 'row'"
@dragleave="dragOverZone = null"
@drop="dropOnZone('row', $event)"
:class="{'bg-primary-50': dragOverZone === 'row'}">
<div class="text-sm font-bold text-gray-600">ROW</div>
<div class="text-xs font-bold text-gray-600">ROW</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in rowFields" :key="f.id"
class="inline-flex items-center gap-1 bg-pink-100 text-pink-800 px-2 py-0.5 rounded-full text-xs cursor-move"
class="inline-flex items-center gap-1 bg-pink-100 text-pink-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
draggable="true"
@dragstart="dragStartReorder($event, f, 'row', idx)"
@dragend="dragEndReorder"
@@ -250,16 +225,16 @@
<span v-if="!rowFields.length" class="text-gray-400 text-xs">Перетащите категорию</span>
</div>
</th>
<th class="bg-gray-100 p-2 text-center"
<th class="bg-gray-100 p-1.5 text-center"
:colspan="columnFields.length + (valueFields.length || 1)"
@dragover.prevent="dragOverZone = 'column'"
@dragleave="dragOverZone = null"
@drop="dropOnZone('column', $event)"
:class="{'bg-primary-50': dragOverZone === 'column'}">
<div class="text-sm font-bold text-gray-600">COLUMN</div>
<div class="text-xs font-bold text-gray-600">COLUMN</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in columnFields" :key="f.id"
class="inline-flex items-center gap-1 bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full text-xs cursor-move"
class="inline-flex items-center gap-1 bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
draggable="true"
@dragstart="dragStartReorder($event, f, 'column', idx)"
@dragend="dragEndReorder"
@@ -274,18 +249,18 @@
</th>
</tr>
<tr>
<th v-for="rf in rowFields" :key="'rh'+rf.id" class="bg-gray-50 p-2 text-left text-xs font-medium text-gray-500 border-r">{{ rf.name }}</th>
<th v-for="cf in columnFields" :key="'ch'+cf.id" class="bg-gray-50 p-2 text-left text-xs font-medium text-gray-500">{{ cf.name }}</th>
<th class="bg-gray-100 p-2 text-center"
<th v-for="rf in rowFields" :key="'rh'+rf.id" class="bg-gray-50 p-1.5 text-left text-xs font-medium text-gray-500 border-r">{{ rf.name }}</th>
<th v-for="cf in columnFields" :key="'ch'+cf.id" class="bg-gray-50 p-1.5 text-left text-xs font-medium text-gray-500">{{ cf.name }}</th>
<th class="bg-gray-100 p-1.5 text-center"
:colspan="valueFields.length || 1"
@dragover.prevent="dragOverZone = 'value'"
@dragleave="dragOverZone = null"
@drop="dropOnZone('value', $event)"
:class="{'bg-primary-50': dragOverZone === 'value'}">
<div class="text-sm font-bold text-gray-600">VALUES</div>
<div class="text-xs font-bold text-gray-600">VALUES</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in valueFields" :key="f.id"
class="inline-flex items-center gap-1 bg-amber-100 text-amber-800 px-2 py-0.5 rounded-full text-xs cursor-move"
class="inline-flex items-center gap-1 bg-amber-100 text-amber-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
draggable="true"
@dragstart="dragStartReorder($event, f, 'value', idx)"
@dragend="dragEndReorder"
@@ -293,7 +268,7 @@
@dragleave="dragLeaveReorder"
@drop="dropReorder($event, 'value', idx)">
{{ f.name }}
<select v-model="f.aggregation" class="text-xs bg-transparent border rounded px-1" @click.stop>
<select v-model="f.aggregation" class="text-xs bg-transparent border rounded px-0.5 py-0" @click.stop>
<option value="sum">SUM</option>
<option value="avg">AVG</option>
<option value="count">COUNT</option>
@@ -307,9 +282,9 @@
</thead>
<tbody>
<tr v-for="i in 3" :key="i" class="border-t">
<td v-for="rf in rowFields" :key="'rv'+rf.id" class="p-2 text-sm text-gray-400 border-r"></td>
<td v-for="cf in columnFields" :key="'cv'+cf.id" class="p-2 text-sm text-gray-400"></td>
<td v-for="vf in valueFields" :key="'vv'+vf.id" class="p-2 text-sm font-bold text-purple-700"></td>
<td v-for="rf in rowFields" :key="'rv'+rf.id" class="p-1.5 text-xs text-gray-400 border-r"></td>
<td v-for="cf in columnFields" :key="'cv'+cf.id" class="p-1.5 text-xs text-gray-400"></td>
<td v-for="vf in valueFields" :key="'vv'+vf.id" class="p-1.5 text-xs font-bold text-purple-700"></td>
</tr>
</tbody>
</table>
@@ -317,11 +292,11 @@
</div>
<div v-if="activeTab==='sql'">
<div class="flex justify-between items-center mb-3">
<h3 class="font-bold text-gray-800">Скрипт для ClickHouse</h3>
<button @click="copySQL" class="btn-secondary text-sm">Копировать SQL</button>
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-gray-800 text-sm">Скрипт для ClickHouse</h3>
<button @click="copySQL" class="btn-secondary text-xs py-1 px-2">Копировать SQL</button>
</div>
<div class="bg-gray-900 text-gray-200 p-4 rounded-xl overflow-x-auto font-mono text-sm whitespace-pre-wrap">
<div class="bg-gray-900 text-gray-200 p-3 rounded-lg overflow-x-auto font-mono text-xs whitespace-pre-wrap">
<pre>{{ sqlScript }}</pre>
</div>
</div>
@@ -330,7 +305,7 @@
</div>
</div>
<!-- Модалка подтверждения сброса -->
<!-- Модалка подтверждения сброса (без изменений) -->
<Transition name="fade">
<div v-if="resetModal.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="resetModal.show = false">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
@@ -363,7 +338,7 @@ import { useNotification } from '@/composables/useNotification'
const { showNotification } = useNotification()
// Типы
// Типы (без изменений)
interface ApiColumn {
fieldKey: string
fieldKeyNormal: string
@@ -461,7 +436,7 @@ const toggleSection = (section: 'number' | 'category' | 'filter') => {
// Модалка сброса
const resetModal = ref({ show: false })
// Drag & drop state
// Drag & drop state (без изменений)
let draggedItem: AvailableField | null = null
let draggedFromSidebar = true
const dragOverZone = ref<string | null>(null)
@@ -469,7 +444,7 @@ let dragReorderItem: CategoryField | NumberField | null = null
let dragReorderType: string | null = null
let dragReorderFromIdx: number | null = null
// Функции работы с полями
// Функции (без изменений, кроме удаления collapsed)
const buildAvailableFields = (): AvailableField[] => {
if (!columnsData.value.length) return []
const selected = reportType.value
@@ -752,7 +727,7 @@ const confirmReset = () => {
tableName.value = ''
tableNameValid.value = true
tableNameTouched.value = false
buildSummary.value = false
buildSummary.value = true
searchQuery.value = ''
showNotification('Все настройки сброшены', 'success')
}