UI коструктора отчетов
This commit is contained in:
188
resources/js/Pages/Analytics/Index.vue
Normal file
188
resources/js/Pages/Analytics/Index.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup>
|
||||
import { ref, h, computed } from 'vue'
|
||||
import { router, Link } from '@inertiajs/vue3'
|
||||
import {
|
||||
NButton, NText, NDataTable, NEmpty, NDropdown, NIcon, NScrollbar,
|
||||
NTag, useDialog, useMessage,
|
||||
} from 'naive-ui'
|
||||
import { TbReportMedical, TbPlus, TbDotsVertical, TbLayoutGrid } from 'vue-icons-plus/tb'
|
||||
import AppLayout from '../../Layouts/AppLayout.vue'
|
||||
import AppContainer from '../../Components/AppContainer.vue'
|
||||
import SectionCard from '../../Components/SectionCard.vue'
|
||||
import PageBanner from '../../Components/PageBanner.vue'
|
||||
import PresetCard from './Components/PresetCard.vue'
|
||||
import TemplatePickerModal from './Components/TemplatePickerModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
documents: { type: Array, default: () => [] },
|
||||
presets: { type: Array, default: () => [] },
|
||||
categories: { type: Array, default: () => [] },
|
||||
canManage: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const dialog = useDialog()
|
||||
const message = useMessage()
|
||||
const showPicker = ref(false)
|
||||
|
||||
// «Отчёт с нуля» + первые шаблоны в ленте создания.
|
||||
const blankPreset = computed(() => props.presets.find((p) => p.key === 'blank'))
|
||||
const featured = computed(() => props.presets.filter((p) => p.key !== 'blank').slice(0, 5))
|
||||
|
||||
const openPreset = (preset) => {
|
||||
router.get(preset.key === 'blank' ? '/reports/new' : `/reports/new?preset=${preset.key}`)
|
||||
}
|
||||
|
||||
const openDocument = (id) => router.get(`/reports/${id}`)
|
||||
|
||||
const rowProps = (row) => ({ style: 'cursor: pointer;', onClick: () => openDocument(row.id) })
|
||||
|
||||
const duplicate = (row) => router.post(`/reports/${row.id}/duplicate`)
|
||||
|
||||
const remove = (row) => {
|
||||
dialog.warning({
|
||||
title: 'Удалить отчёт',
|
||||
content: `Удалить «${row.name}»? Действие необратимо.`,
|
||||
positiveText: 'Удалить',
|
||||
negativeText: 'Отмена',
|
||||
onPositiveClick: () => router.delete(`/reports/${row.id}`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => message.success('Отчёт удалён'),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const rowMenu = (row) => [
|
||||
{ key: 'open', label: 'Открыть' },
|
||||
{ key: 'duplicate', label: 'Дублировать' },
|
||||
{ key: 'delete', label: 'Удалить', props: { style: 'color: var(--error-color);' } },
|
||||
]
|
||||
|
||||
const onMenuSelect = (key, row) => {
|
||||
if (key === 'open') openDocument(row.id)
|
||||
if (key === 'duplicate') duplicate(row)
|
||||
if (key === 'delete') remove(row)
|
||||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{
|
||||
title: 'Название',
|
||||
key: 'name',
|
||||
render: (row) => h('div', {}, [
|
||||
h('div', { style: 'font-weight: 600;' }, row.name),
|
||||
row.description ? h(NText, { depth: 3, style: 'font-size: 12px;' }, () => row.description) : null,
|
||||
]),
|
||||
},
|
||||
{ title: 'Источник', key: 'datasetLabel', width: 200, render: (row) => h(NTag, { size: 'small', bordered: false, round: true }, () => row.datasetLabel) },
|
||||
{ title: 'Автор', key: 'author', width: 180, render: (row) => row.author ?? '—' },
|
||||
{ title: 'Изменено', key: 'updatedAt', width: 130, render: (row) => row.updatedAt ?? '—' },
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 56,
|
||||
render: (row) => props.canManage
|
||||
? h(NDropdown, {
|
||||
trigger: 'click',
|
||||
options: rowMenu(row),
|
||||
onSelect: (key) => onMenuSelect(key, row),
|
||||
}, () => h(NButton, {
|
||||
text: true,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
}, () => h(NIcon, null, () => h(TbDotsVertical)))) : null,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout>
|
||||
<AppContainer>
|
||||
<PageBanner
|
||||
title="Отчёты"
|
||||
:icon="TbReportMedical"
|
||||
:breadcrumbs="[{ label: 'Главная', href: '/', tag: Link }]"
|
||||
>
|
||||
<template #meta>
|
||||
<NText depth="3" style="font-size: 13px;">Создавайте отчёты с нуля или по готовым шаблонам</NText>
|
||||
</template>
|
||||
</PageBanner>
|
||||
|
||||
<SectionCard v-if="canManage" title="Создание отчёта">
|
||||
<template #header-extra>
|
||||
<NButton text type="primary" @click="showPicker = true">
|
||||
<template #icon><TbLayoutGrid /></template>
|
||||
Открыть все шаблоны
|
||||
</NButton>
|
||||
</template>
|
||||
<NScrollbar x-scrollable class="pb-3">
|
||||
<div class="create-row">
|
||||
<div class="create-card" @click="openPreset({ key: 'blank' })">
|
||||
<div class="blank-tile"><NIcon :size="28"><TbPlus /></NIcon></div>
|
||||
<NText class="blank-title">Отчёт с нуля</NText>
|
||||
</div>
|
||||
<div v-for="preset in featured" :key="preset.key" class="create-card" @click="openPreset(preset)">
|
||||
<PresetCard :preset="preset" />
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Отчёты" no-padding>
|
||||
<NEmpty v-if="!documents.length" description="Пока нет сохранённых отчётов" style="padding: 32px;" />
|
||||
<NDataTable
|
||||
v-else
|
||||
min-height="calc(100vh - 570px)"
|
||||
:columns="columns"
|
||||
:data="documents"
|
||||
:row-props="rowProps"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<TemplatePickerModal
|
||||
v-model:show="showPicker"
|
||||
:presets="presets"
|
||||
:categories="categories"
|
||||
@select="openPreset"
|
||||
/>
|
||||
</AppContainer>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-data-table-th) { background: transparent !important; }
|
||||
:deep(.n-data-table) { background: transparent; }
|
||||
:deep(.n-data-table-wrapper) { border-radius: 0; }
|
||||
:deep(.n-data-table-th .n-data-table-th__title) { font-size: 12px; }
|
||||
:deep(.n-data-table-td) { font-size: 13px; }
|
||||
|
||||
.create-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.create-card {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.blank-tile {
|
||||
height: 84px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.create-card .blank-tile { border: 1px dashed color-mix(in srgb, var(--primary-color) 40%, transparent); }
|
||||
.create-card:first-child {
|
||||
border: 1px solid var(--n-border-color, rgba(255,255,255,.1));
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s, transform .15s;
|
||||
background: var(--n-card-color);
|
||||
}
|
||||
.create-card:first-child:hover { border-color: var(--primary-color); transform: translateY(-2px); }
|
||||
.blank-title { display: block; font-weight: 600; font-size: 14px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user