first commit
This commit is contained in:
602
resources/js/Pages/Metriks/Components/MetrikaForm.vue
Normal file
602
resources/js/Pages/Metriks/Components/MetrikaForm.vue
Normal file
@@ -0,0 +1,602 @@
|
||||
<script setup>
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import { Head, router, useForm } from '@inertiajs/vue3'
|
||||
import {
|
||||
NForm, NFormItem, NInput, NInputNumber, NSelect,
|
||||
NSwitch, NButton, NSpace, NCard, NH1, NH3,
|
||||
NText, NTag, NProgress, NDivider, NGrid, NGi,
|
||||
NAlert, NStatistic, NIcon, NTooltip, NRadioGroup,
|
||||
NRadio, NCheckbox, NDatePicker, NTimePicker
|
||||
} from 'naive-ui'
|
||||
import {useNotification} from "../../../Composables/useNotification.js";
|
||||
|
||||
const props = defineProps({
|
||||
groupId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const { errorApi } = useNotification()
|
||||
|
||||
// Состояние формы
|
||||
const formRef = ref(null)
|
||||
const form = ref({
|
||||
group_id: props.groupId,
|
||||
comment: '',
|
||||
// Динамические поля будут добавлены позже
|
||||
})
|
||||
const formProcessing = ref(false)
|
||||
const formSections = ref([])
|
||||
const formRules = ref({})
|
||||
const formTitle = ref('')
|
||||
const formDescription = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const isSavingDraft = ref(false)
|
||||
const lastSavedTime = ref(null)
|
||||
|
||||
// Используем useForm из Inertia
|
||||
// const { data: form, defaults, post, processing, errors: formErrors, reset } = useForm()
|
||||
|
||||
|
||||
// Загрузка формы
|
||||
onMounted(async () => {
|
||||
await loadFormData()
|
||||
await loadExistingData()
|
||||
})
|
||||
|
||||
// Загрузка структуры формы
|
||||
const loadFormData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/metric-forms/${props.groupId}`)
|
||||
const formData = response.data
|
||||
|
||||
console.log(formData)
|
||||
|
||||
formTitle.value = formData.group.name
|
||||
formDescription.value = formData.group.description
|
||||
formSections.value = formData.form.sections
|
||||
|
||||
// Инициализируем значения формы
|
||||
const initialValues = { group_id: props.groupId, comment: '' }
|
||||
for (let fieldName in formData.form.default) {
|
||||
initialValues[fieldName] = formData.form.default[fieldName]
|
||||
}
|
||||
|
||||
form.value = {
|
||||
...form.value,
|
||||
...initialValues
|
||||
}
|
||||
|
||||
// Настраиваем правила валидации
|
||||
formRules.value = formData.form.validation
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки формы:', error)
|
||||
// errorApi()
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка существующих данных
|
||||
const loadExistingData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/metric-forms/${props.groupId}/existing`)
|
||||
if (response.data.existing_data) {
|
||||
const existing = response.data.existing_data
|
||||
|
||||
// Заполняем форму существующими данными
|
||||
Object.keys(existing.data).forEach(key => {
|
||||
form[key] = existing.data[key]
|
||||
})
|
||||
|
||||
form.comment = existing.comment || ''
|
||||
lastSavedTime.value = new Date(existing.submitted_at).toLocaleTimeString('ru-RU')
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки существующих данных:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Вычисляемые свойства
|
||||
const currentDate = computed(() => {
|
||||
return new Date().toLocaleDateString('ru-RU', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
const totalFieldsCount = computed(() => {
|
||||
return formSections.value.reduce((total, section) => total + section.fields.length, 0)
|
||||
})
|
||||
|
||||
const filledFieldsCount = computed(() => {
|
||||
return Object.keys(form.value).filter(key =>
|
||||
key.startsWith('metrika_item_') &&
|
||||
form.value[key] !== null &&
|
||||
form.value[key] !== '' &&
|
||||
form.value[key] !== undefined
|
||||
).length
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
if (totalFieldsCount.value === 0) return 0
|
||||
return Math.round((filledFieldsCount.value / totalFieldsCount.value) * 100)
|
||||
})
|
||||
|
||||
const requiredFieldsCount = computed(() => {
|
||||
return formSections.value.reduce((total, section) => {
|
||||
return total + section.fields.filter(f => f.is_required).length
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const filledRequiredFieldsCount = computed(() => {
|
||||
let count = 0
|
||||
formSections.value.forEach(section => {
|
||||
section.fields.forEach(field => {
|
||||
if (field.is_required) {
|
||||
const fieldName = `metrika_item_${field.metrika_item_id}`
|
||||
if (form.value[fieldName] !== null && form.value[fieldName] !== '' && form.value[fieldName] !== undefined) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return count
|
||||
})
|
||||
|
||||
const requiredFieldsStatus = computed(() => {
|
||||
const filled = filledRequiredFieldsCount.value
|
||||
const total = requiredFieldsCount.value
|
||||
|
||||
if (filled === 0) return { type: 'error', text: '0%' }
|
||||
if (filled === total) return { type: 'success', text: '100%' }
|
||||
|
||||
const percent = Math.round((filled / total) * 100)
|
||||
return {
|
||||
type: percent > 50 ? 'warning' : 'error',
|
||||
text: `${percent}%`
|
||||
}
|
||||
})
|
||||
|
||||
const formStatus = computed(() => {
|
||||
if (filledFieldsCount.value === 0) return 'empty'
|
||||
if (filledRequiredFieldsCount.value < requiredFieldsCount.value) return 'incomplete'
|
||||
// if (Object.keys(formErrors).length > 0) return 'invalid'
|
||||
return 'ready'
|
||||
})
|
||||
|
||||
const formStatusText = computed(() => {
|
||||
const texts = {
|
||||
'empty': 'Не заполнено',
|
||||
'incomplete': 'Не все обязательные поля',
|
||||
'invalid': 'Есть ошибки',
|
||||
'ready': 'Готово к отправке'
|
||||
}
|
||||
return texts[formStatus.value] || 'Неизвестно'
|
||||
})
|
||||
|
||||
const formStatusType = computed(() => {
|
||||
const types = {
|
||||
'empty': 'default',
|
||||
'incomplete': 'warning',
|
||||
'invalid': 'error',
|
||||
'ready': 'success'
|
||||
}
|
||||
return types[formStatus.value] || 'default'
|
||||
})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return formStatus.value === 'ready' && !formProcessing.value
|
||||
})
|
||||
|
||||
const hasData = computed(() => {
|
||||
return filledFieldsCount.value > 0
|
||||
})
|
||||
|
||||
const showStatistics = computed(() => {
|
||||
return totalFieldsCount.value > 0
|
||||
})
|
||||
|
||||
// Методы
|
||||
const getFieldComponent = (dataType) => {
|
||||
const components = {
|
||||
'integer': NInputNumber,
|
||||
'float': NInputNumber,
|
||||
'string': NInput,
|
||||
'text': NInput,
|
||||
'textarea': NInput,
|
||||
'boolean': NSwitch,
|
||||
'select': NSelect,
|
||||
'radio': NRadioGroup,
|
||||
'checkbox': NCheckbox,
|
||||
'date': NDatePicker,
|
||||
'time': NTimePicker
|
||||
}
|
||||
return components[dataType] || NInput
|
||||
}
|
||||
|
||||
const getFieldOptions = (field) => {
|
||||
if (field.options && Array.isArray(field.options)) {
|
||||
return field.options.map(opt => ({
|
||||
label: opt.label || opt,
|
||||
value: opt.value || opt
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getFieldName = (fieldKey) => {
|
||||
const itemId = fieldKey.replace('metrika_item_', '')
|
||||
for (const section of formSections.value) {
|
||||
for (const field of section.fields) {
|
||||
if (field.metrika_item_id.toString() === itemId) {
|
||||
return field.metrika_item_name
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldKey
|
||||
}
|
||||
|
||||
const getStatusTagType = (status) => {
|
||||
const types = {
|
||||
'empty': 'default',
|
||||
'incomplete': 'warning',
|
||||
'invalid': 'error',
|
||||
'ready': 'success'
|
||||
}
|
||||
return types[status] || 'default'
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// Валидация формы
|
||||
await formRef.value.validate()
|
||||
|
||||
// if (Object.keys(formErrors).length > 0) {
|
||||
// // notification.warning({
|
||||
// // title: 'Ошибки в форме',
|
||||
// // content: 'Исправьте ошибки перед отправкой',
|
||||
// // duration: 4000
|
||||
// // })
|
||||
// return
|
||||
// }
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
// const response = await post(route('metric-forms.save', { group: props.groupId }), {
|
||||
// onSuccess: () => {
|
||||
// notification.success({
|
||||
// title: 'Успешно!',
|
||||
// content: 'Отчет сохранен',
|
||||
// duration: 5000
|
||||
// })
|
||||
// lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
|
||||
// },
|
||||
// onError: (errors) => {
|
||||
// notification.error({
|
||||
// title: 'Ошибка сохранения',
|
||||
// content: 'Проверьте правильность заполнения данных',
|
||||
// duration: 5000
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
const response = await axios.post(`/api/metric-forms/${props.groupId}/save`, formRef.value)
|
||||
.then(res => {
|
||||
// notification.success({
|
||||
// title: 'Успешно!',
|
||||
// content: 'Отчет сохранен',
|
||||
// duration: 5000
|
||||
// })
|
||||
router.visit('/')
|
||||
lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
|
||||
})
|
||||
.catch(e => {
|
||||
// notification.error({
|
||||
// title: 'Ошибка сохранения',
|
||||
// content: 'Проверьте правильность заполнения данных',
|
||||
// duration: 5000
|
||||
// })
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения:', error)
|
||||
// notification.error({
|
||||
// title: 'Ошибка',
|
||||
// content: error.message || 'Произошла ошибка при сохранении',
|
||||
// duration: 5000
|
||||
// })
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveDraft = async () => {
|
||||
isSavingDraft.value = true
|
||||
|
||||
try {
|
||||
// Сохраняем в localStorage
|
||||
const draftData = {
|
||||
form: form.value,
|
||||
timestamp: new Date().toISOString(),
|
||||
groupId: props.groupId
|
||||
}
|
||||
|
||||
localStorage.setItem(`metric_form_draft_${props.groupId}`, JSON.stringify(draftData))
|
||||
|
||||
lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
|
||||
// message.success('Черновик сохранен')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения черновика:', error)
|
||||
// message.error('Не удалось сохранить черновик')
|
||||
} finally {
|
||||
isSavingDraft.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
if (confirm('Вы уверены, что хотите сбросить форму? Все несохраненные данные будут потеряны.')) {
|
||||
reset()
|
||||
loadFormData()
|
||||
// message.info('Форма сброшена')
|
||||
}
|
||||
}
|
||||
|
||||
const exportFormData = () => {
|
||||
const exportData = {
|
||||
group: formTitle.value,
|
||||
date: currentDate.value,
|
||||
data: {},
|
||||
comment: form.comment
|
||||
}
|
||||
|
||||
// Собираем данные полей
|
||||
formSections.value.forEach(section => {
|
||||
section.fields.forEach(field => {
|
||||
const fieldName = `metrika_item_${field.metrika_item_id}`
|
||||
exportData.data[field.metrika_item_name] = form.value[fieldName]
|
||||
})
|
||||
})
|
||||
|
||||
// Создаем и скачиваем файл
|
||||
const dataStr = JSON.stringify(exportData, null, 2)
|
||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', dataUri)
|
||||
link.setAttribute('download', `metric_report_${formTitle.value}_${new Date().toISOString().split('T')[0]}.json`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Head :title="`Ввод данных: ${formTitle}`" />
|
||||
|
||||
<!-- Заголовок формы -->
|
||||
<div class="max-w-6xl mx-auto mt-4 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<n-h1 class="!mb-2">{{ formTitle }}</n-h1>
|
||||
<n-text depth="3" v-if="formDescription">
|
||||
{{ formDescription }}
|
||||
</n-text>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<n-tag :type="getStatusTagType(formStatus)" size="small" round>
|
||||
{{ formStatusText }}
|
||||
</n-tag>
|
||||
<n-text depth="3" class="text-sm">
|
||||
{{ currentDate }}
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <n-space>-->
|
||||
<!-- <n-button @click="$inertia.visit(route('dashboard'))" secondary>-->
|
||||
<!-- Назад-->
|
||||
<!-- </n-button>-->
|
||||
<!-- <n-button @click="exportFormData" :disabled="!hasData">-->
|
||||
<!-- Экспорт-->
|
||||
<!-- </n-button>-->
|
||||
<!-- </n-space>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Прогресс заполнения -->
|
||||
<div class="max-w-6xl mx-auto mb-2">
|
||||
<n-card size="small">
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-text>Прогресс заполнения</n-text>
|
||||
<n-text strong>{{ progressPercentage }}%</n-text>
|
||||
</n-space>
|
||||
<n-progress
|
||||
:percentage="progressPercentage"
|
||||
:height="6"
|
||||
:border-radius="0"
|
||||
:show-indicator="false"
|
||||
class="mt-2"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- Основная форма -->
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<n-card>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
@submit.prevent="submitForm"
|
||||
>
|
||||
<!-- Секции формы -->
|
||||
<div v-for="(section, sectionIndex) in formSections" :key="sectionIndex">
|
||||
<n-divider v-if="sectionIndex > 0" />
|
||||
|
||||
<div class="mb-6">
|
||||
<n-h3>{{ section.name }}</n-h3>
|
||||
<n-text depth="3" class="text-sm">
|
||||
Заполните данные по секции
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
<n-grid :cols="section.fields.length > 5 ? 2 : 1" :x-gap="24" :y-gap="16">
|
||||
<n-gi
|
||||
v-for="field in section.fields"
|
||||
:key="field.metrika_item_id"
|
||||
:span="field.span || 1"
|
||||
>
|
||||
<n-form-item
|
||||
:label="field.metrika_item_name"
|
||||
:path="`metrika_item_${field.metrika_item_id}`"
|
||||
:required="field.is_required"
|
||||
>
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center gap-2">-->
|
||||
<!-- <span>{{ field.metrika_item_name }}</span>-->
|
||||
<!-- <n-tooltip v-if="field.metrika_item_description" trigger="hover">-->
|
||||
<!-- <template #trigger>-->
|
||||
<!-- <n-icon size="14" class="cursor-help">-->
|
||||
<!-- <InfoCircle />-->
|
||||
<!-- </n-icon>-->
|
||||
<!-- </template>-->
|
||||
<!-- {{ field.metrika_item_description }}-->
|
||||
<!-- </n-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
|
||||
<!-- Динамические поля ввода -->
|
||||
<component
|
||||
:is="getFieldComponent(field.metrika_item_data_type)"
|
||||
v-model:value="form[`metrika_item_${field.metrika_item_id}`]"
|
||||
:placeholder="field.placeholder || `Введите ${field.metrika_item_name.toLowerCase()}`"
|
||||
:options="getFieldOptions(field)"
|
||||
:min="field.validation_rules?.min"
|
||||
:max="field.validation_rules?.max"
|
||||
:step="field.metrika_item_data_type === 'float' ? 0.1 : 1"
|
||||
:rows="3"
|
||||
clearable
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<!-- <template #feedback>-->
|
||||
<!-- <div v-if="formErrors[`metric_${field.metrika_item_id}`]" class="text-red-500 text-xs mt-1">-->
|
||||
<!-- {{ formErrors[`metric_${field.metrika_item_id}`][0] }}-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
|
||||
<!-- Комментарий -->
|
||||
<n-form-item label="Комментарий" path="comment">
|
||||
<n-input
|
||||
v-model:value="form.comment"
|
||||
type="textarea"
|
||||
placeholder="Дополнительные заметки и комментарии..."
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- Валидационные ошибки -->
|
||||
<!-- <n-alert-->
|
||||
<!-- v-if="Object.keys(formErrors).length > 0"-->
|
||||
<!-- title="Ошибки заполнения"-->
|
||||
<!-- type="error"-->
|
||||
<!-- class="mb-4"-->
|
||||
<!-- >-->
|
||||
<!-- <ul class="list-disc pl-5">-->
|
||||
<!-- <li v-for="(errors, field) in formErrors" :key="field">-->
|
||||
<!-- {{ getFieldName(field) }}: {{ errors[0] }}-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </n-alert>-->
|
||||
|
||||
<!-- Статистика -->
|
||||
<!-- <n-card v-if="showStatistics" title="Статистика" size="small" class="mb-4">-->
|
||||
<!-- <n-grid :cols="4" responsive="screen">-->
|
||||
<!-- <n-gi>-->
|
||||
<!-- <n-statistic label="Заполнено полей">-->
|
||||
<!-- {{ filledFieldsCount }} / {{ totalFieldsCount }}-->
|
||||
<!-- </n-statistic>-->
|
||||
<!-- </n-gi>-->
|
||||
<!-- <n-gi>-->
|
||||
<!-- <n-statistic label="Обязательные поля">-->
|
||||
<!-- <n-tag :type="requiredFieldsStatus.type" size="small">-->
|
||||
<!-- {{ requiredFieldsStatus.text }}-->
|
||||
<!-- </n-tag>-->
|
||||
<!-- </n-statistic>-->
|
||||
<!-- </n-gi>-->
|
||||
<!-- <n-gi>-->
|
||||
<!-- <n-statistic label="Последнее сохранение">-->
|
||||
<!-- {{ lastSavedTime || 'Не сохранено' }}-->
|
||||
<!-- </n-statistic>-->
|
||||
<!-- </n-gi>-->
|
||||
<!-- <n-gi>-->
|
||||
<!-- <n-statistic label="Статус">-->
|
||||
<!-- <n-tag :type="formStatusType" size="small">-->
|
||||
<!-- {{ formStatusText }}-->
|
||||
<!-- </n-tag>-->
|
||||
<!-- </n-statistic>-->
|
||||
<!-- </n-gi>-->
|
||||
<!-- </n-grid>-->
|
||||
<!-- </n-card>-->
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<n-space justify="end">
|
||||
<!-- <n-button @click="resetForm" :disabled="isSubmitting">-->
|
||||
<!-- Сбросить-->
|
||||
<!-- </n-button>-->
|
||||
<!-- <n-button-->
|
||||
<!-- @click="saveDraft"-->
|
||||
<!-- :loading="isSavingDraft"-->
|
||||
<!-- secondary-->
|
||||
<!-- >-->
|
||||
<!-- Сохранить черновик-->
|
||||
<!-- </n-button>-->
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="submitForm"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
Сохранить отчет
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-form-item :deep(.n-form-item-label__text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.n-divider {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.n-statistic :deep(.n-statistic-label) {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.n-statistic :deep(.n-statistic-value) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user