Files
documenter-mono/resources/js/Pages/ContractGenerator.vue
2025-11-06 16:04:36 +09:00

450 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import {computed, nextTick, onMounted, ref, useTemplateRef, watch} from "vue"
import {useDateFormat, useDebounceFn} from "@vueuse/core"
import Sections from "../Layouts/Sections.vue";
import Input from '../Components/Input/Input.vue'
import Select from "../Components/Select/Select.vue";
import Card from "../Components/Card/Card.vue";
import Button from "../Components/Button/Button.vue";
import ListStrate from "../Components/List/ListStrate.vue";
import CardBack from "../Components/Card/CardBack.vue";
import {Link, router} from "@inertiajs/vue3";
import Editor from "../Components/Editor.vue";
import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed'
import {useFileDownload} from "../Composables/useFileDownload.js";
import PriceInput from "../Components/Document/InputVariable/PriceInput.vue";
import TextArea from "../Components/Input/TextArea.vue";
import Calendar from "../Components/Calendar/Calendar.vue";
import Collapsible from "../Components/Collapsible/Collapsible.vue";
import Accordion from "../Components/Accordion/Accordion.vue";
const { downloadFile } = useFileDownload()
const props = defineProps({
template: Object,
})
const editorRef = ref(null)
const content = ref(props.template.content ?? [])
const formData = ref([])
const documentStructure = ref(props.template.content || [])
const prepareVariables = (variables) => {
for (const variable of variables) {
formData.value.push(variable)
}
}
// Форматируем ключ переменной в читаемый name
const formatLabel = (key) => {
return key
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const viewer = useTemplateRef('viewer')
onMounted(async() => {
await preview()
prepareVariables(props.template.variables)
})
const previewLoading = ref(true)
const previewUrl = ref()
const preview = async () => {
previewLoading.value = true
await axios.post(`/contract-generator/${props.template.id}/preview`, {
variables: formData.value
}, {
responseType: 'blob'
}).then(res => {
previewUrl.value = URL.createObjectURL(res.data)
})
}
const updatePreview = async () => {
await preview()
}
const onChangeVariableTextValue = (variableId, value) => {
console.log(variableId, value)
changeVariableValue(variableId, value)
}
const onChangeVariableSelectValue = (variableId, option) => {
changeVariableValue(variableId, option.value)
}
const changeVariableValue = (variableId, value) => {
if (content.value && Array.isArray(content.value)) {
const updatedContent = content.value.map(htmlString => {
return htmlString.replace(
new RegExp(`(<span[^>]*brs-element-id="${variableId}"[^>]*>)[^<]*(</span>)`, 'g'),
`$1${value}$2`
)
})
content.value = updatedContent
}
}
const onPrint = () => {
if (viewer.value)
viewer.value.print(200, props.template.name, true)
}
const onDownloadDocx = async () => {
try {
await downloadFile(
`/contract-generator/${props.template.id}/download`,
{ variables: formData.value },
`${props.template.name}.docx`
)
} catch (e) {
console.error('Ошибка при скачивании docx файла: ', e.message)
}
}
// function scrollToElementByText(text, options = {}) {
// const {
// behavior = 'smooth',
// block = 'start',
// inline = 'nearest',
// partialMatch = false,
// caseSensitive = false
// } = options
//
// // Ищем все элементы, содержащие текст
// const elements = Array.from(document.querySelectorAll('*')).filter(element => {
// const elementText = caseSensitive
// ? element.textContent
// : element.textContent.toLowerCase()
// const searchText = caseSensitive
// ? text
// : text.toLowerCase()
//
// return partialMatch
// ? elementText.includes(searchText)
// : elementText.trim() === searchText
// })
//
// if (elements.length > 0) {
// elements[0].scrollIntoView({
// behavior,
// block,
// inline
// })
// return elements[0]
// }
//
// return null
// }
const searchAndScroll = (targetText) => {
if (!targetText.trim()) return
const result = scrollToElementByText(targetText)
if (result) {
console.log(result)
highlightElement(result.element)
}
}
const scrollToElementByText = (targetText) => {
const elementContainers = document.querySelectorAll('.textLayer')
if (!elementContainers) return null
const elementsOfContainers = []
for (const container of elementContainers) {
elementsOfContainers.push(...container.children)
}
const allElements = Array.from(elementsOfContainers)
.filter(el => el.textContent && el.textContent.trim())
.filter(el => {
const style = window.getComputedStyle(el)
return style.display !== 'none' && style.visibility !== 'hidden'
})
// Сначала ищем точное совпадение
for (const el of allElements) {
if (el.textContent.trim() === targetText) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
return { element: el, foundText: targetText, isComposite: false }
}
}
// Ищем составной текст в соседних элементах
for (let i = 0; i < allElements.length - 1; i++) {
const current = allElements[i]
const next = allElements[i + 1]
const combined = current.textContent.trim() + next.textContent.trim()
if (combined === targetText) {
current.scrollIntoView({ behavior: 'smooth', block: 'center' })
return {
element: [current, next],
foundText: targetText,
isComposite: true,
parts: [current.textContent.trim(), next.textContent.trim()]
}
}
}
return null
}
const highlightElement = (element) => {
// Убираем предыдущую подсветку
document.querySelectorAll('.search-highlight').forEach(el => {
el.classList.remove('search-highlight')
})
if (Array.isArray(element)) {
for (const el of element) {
el.classList.add('search-highlight')
setTimeout(() => {
el.classList.remove('search-highlight')
}, 3000)
}
} else {
element.classList.add('search-highlight')
setTimeout(() => {
element.classList.remove('search-highlight')
}, 3000)
}
}
</script>
<template>
<Sections>
<template #leftbar>
<Card header="Информация о документе">
<div>
<ListStrate header="Наименование">
<span class="block text-sm">
{{ template.name }}
</span>
</ListStrate>
<ListStrate header="Дата обновления">
<span class="text-sm">
{{ useDateFormat(template.updated_at, 'DD.MM.YYYY HH:mm:ss') }}
</span>
</ListStrate>
<ListStrate header="Дата создания">
<span class="text-sm">
{{ useDateFormat(template.created_at, 'DD.MM.YYYY HH:mm:ss') }}
</span>
</ListStrate>
</div>
<template #footer>
<div class="flex flex-col gap-y-1">
<Button block @click="onPrint" :loading="previewLoading" icon-left>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 17h2a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h2"></path><path d="M17 9V5a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v4"></path><rect x="7" y="13" width="10" height="8" rx="2"></rect></g></svg>
</template>
Печать документа
</Button>
<Button block @click="onDownloadDocx" :loading="previewLoading" icon-left>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"></path><path d="M12 11v6"></path><path d="M9 14l3 3l3-3"></path></g></svg>
</template>
Скачать docx
</Button>
<CardBack :tag="Link" href="/" class="mt-2" />
</div>
</template>
</Card>
</template>
<Card header="Предпросмотр" :content-scroll="!previewLoading" :content-relative>
<div class="flex flex-col items-center justify-center">
<VuePdfEmbed width="793.701" text-layer ref="viewer" :source="previewUrl" @rendered="previewLoading = false" />
<div v-if="previewLoading" class="absolute inset-0 backdrop-blur-xs h-full flex items-center justify-center z-10">
<div class="text-center space-y-4">
<div class="flex items-center justify-center">
<div class="relative">
<div class="w-8 h-8 border-3 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
<div class="absolute inset-0 w-8 h-8 border-3 border-transparent border-r-blue-300 rounded-full animate-spin" style="animation-duration: 1.5s"></div>
</div>
</div>
</div>
</div>
</div>
</Card>
<template #rightbar>
<Card header="Свойства документа" :content-relative>
<div v-if="previewLoading" class="absolute inset-0 flex items-center justify-center">
<div class="flex items-center justify-center">
<div class="relative">
<div class="w-8 h-8 border-3 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
<div class="absolute inset-0 w-8 h-8 border-3 border-transparent border-r-blue-300 rounded-full animate-spin" style="animation-duration: 1.5s"></div>
</div>
</div>
</div>
<div class="flex flex-col gap-y-1">
<template v-for="(data, key) in formData" :key="key">
<div v-if="data.type === 'group'">
<Collapsible :header="data.label">
<ListStrate v-for="(variable, key) in data.children" :key="key" :header="variable.label">
<Input
v-if="variable.type === 'text'"
:id="key"
@focus="searchAndScroll(variable.name)"
v-model:value="variable.value"
@update:value="value => onChangeVariableTextValue(key, value)"
:placeholder="`Введите ${formatLabel(variable.label)}`"
/>
<TextArea
v-if="variable.type === 'textarea'"
:rows="8"
:resize="false"
:id="key"
@focus="searchAndScroll(variable.name)"
v-model:value="variable.value"
@update:value="value => onChangeVariableTextValue(key, value)"
:placeholder="`Введите ${formatLabel(variable.label)}`"
/>
<!-- Select поле -->
<Select
v-else-if="variable.type === 'select'"
:id="key"
@focus="searchAndScroll(variable.name)"
@change="value => onChangeVariableSelectValue(key, value)"
v-model:value="variable.value"
:options="variable.options"
/>
<!-- Radio кнопки -->
<div v-else-if="variable.type === 'radio'" class="space-y-2">
<label
v-for="(optionLabel, optionValue) in variable.options"
:key="optionValue"
class="flex items-center"
>
<input
type="radio"
:name="key"
:value="optionValue"
v-model="formData[key]"
@change="updatePreview"
class="mr-2"
>
{{ optionLabel }}
</label>
</div>
<PriceInput v-else-if="variable.type === 'price-input'"
v-model:number="variable.number"
v-model:text="variable.value"
@focus="searchAndScroll(variable.name)"
/>
<Calendar v-else-if="variable.type === 'calendar'"
v-model="variable.value"
:format="variable.format"
block
@focus="searchAndScroll(variable.name)"
/>
</ListStrate>
</Collapsible>
</div>
<ListStrate v-else :key="key" :header="data.label">
<Input
v-if="data.type === 'text'"
:id="key"
@focus="searchAndScroll(data.name)"
v-model:value="data.value"
@update:value="value => onChangeVariableTextValue(key, value)"
:placeholder="`Введите ${formatLabel(data.label)}`"
/>
<TextArea
v-if="data.type === 'textarea'"
:rows="8"
:resize="false"
:id="key"
@focus="searchAndScroll(data.name)"
v-model:value="data.value"
@update:value="value => onChangeVariableTextValue(key, value)"
:placeholder="`Введите ${formatLabel(data.label)}`"
/>
<!-- Select поле -->
<Select
v-else-if="data.type === 'select'"
:id="key"
@focus="searchAndScroll(data.name)"
@change="value => onChangeVariableSelectValue(key, value)"
v-model:value="data.value"
:options="data.options"
/>
<!-- Radio кнопки -->
<div v-else-if="data.type === 'radio'" class="space-y-2">
<label
v-for="(optionLabel, optionValue) in data.options"
:key="optionValue"
class="flex items-center"
>
<input
type="radio"
:name="key"
:value="optionValue"
v-model="formData[key]"
@change="updatePreview"
class="mr-2"
>
{{ optionLabel }}
</label>
</div>
<PriceInput v-else-if="data.type === 'price-input'"
v-model:number="data.number"
v-model:text="data.value"
@focus="searchAndScroll(data.name)"
/>
<Calendar v-else-if="data.type === 'calendar'"
v-model="data.value"
:format="data.format"
block
@focus="searchAndScroll(data.name)"
/>
</ListStrate>
</template>
</div>
<template #footer>
<Button :loading="previewLoading" block @click="updatePreview">
Обновить предпросмотр
</Button>
</template>
</Card>
</template>
</Sections>
</template>
<style scoped>
@reference "tailwindcss";
:deep(.search-highlight) {
@apply border border-dashed border-yellow-500 bg-yellow-200 text-black;
}
:deep(.vue-pdf-embed) {
margin: 0 auto;
}
:deep(.vue-pdf-embed .vue-pdf-embed__page) {
margin-bottom: 20px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: hidden;
}
</style>