first commit
This commit is contained in:
45
resources/js/Components/Document/A4.vue
Normal file
45
resources/js/Components/Document/A4.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
cover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const defaultClasses = [
|
||||
'w-[21cm] h-[29.7cm] bg-white text-black rounded-sm outline-none'
|
||||
]
|
||||
|
||||
const defaultCoverClasses = [
|
||||
'p-[2.5cm]'
|
||||
]
|
||||
|
||||
const coverClasses = [
|
||||
'pt-[2cm] pl-[3cm] pb-[2cm] pr-[1.5cm]'
|
||||
]
|
||||
|
||||
const classes = computed(() => {
|
||||
let base = [...defaultClasses]
|
||||
|
||||
if (props.cover)
|
||||
base = base.concat(coverClasses)
|
||||
else
|
||||
base = base.concat(defaultCoverClasses)
|
||||
|
||||
return base
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="min-h-[calc(29.7cm-5cm)] relative">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
61
resources/js/Components/Document/DocumentElement.vue
Normal file
61
resources/js/Components/Document/DocumentElement.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import {computed, onMounted} from "vue"
|
||||
import DocumentWarning from "./DocumentWarning.vue"
|
||||
import DocumentHtml from "./DocumentHtml.vue"
|
||||
import DocumentHeading from "./DocumentHeading.vue"
|
||||
import DocumentParagraph from "./DocumentParagraph.vue";
|
||||
import DocumentTextRun from "./DocumentTextRun.vue";
|
||||
import DocumentLineBreak from "./DocumentLineBreak.vue";
|
||||
import DocumentTable from "./DocumentTable.vue";
|
||||
|
||||
const props = defineProps({
|
||||
element: Object
|
||||
})
|
||||
|
||||
const emit = defineEmits(['variable-click', 'is-mounted'])
|
||||
|
||||
const componentMap = {
|
||||
'heading': DocumentHeading,
|
||||
'html': DocumentHtml,
|
||||
// 'warning': DocumentWarning,
|
||||
'paragraph': DocumentParagraph,
|
||||
'text_run': DocumentTextRun,
|
||||
'line_break': DocumentLineBreak,
|
||||
'table': DocumentTable
|
||||
}
|
||||
|
||||
const componentType = computed(() => {
|
||||
return componentMap[props.element.type] || 'div'
|
||||
})
|
||||
|
||||
const elementClass = computed(() => {
|
||||
return `element-${props.element.type}`
|
||||
})
|
||||
|
||||
const compiledContent = computed(() => {
|
||||
return props.element.compiledContent || props.element.content
|
||||
})
|
||||
|
||||
const handleVariableClick = (variableName) => {
|
||||
emit('variable-click', variableName)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emit('is-mounted', true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-element" :class="elementClass">
|
||||
<component
|
||||
:is="componentType"
|
||||
:content="compiledContent"
|
||||
:element="element"
|
||||
@variable-click="handleVariableClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
54
resources/js/Components/Document/DocumentHeading.vue
Normal file
54
resources/js/Components/Document/DocumentHeading.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
content: String,
|
||||
element: Object
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
const styles = {}
|
||||
const style = props.element.style || {}
|
||||
|
||||
// Выравнивание
|
||||
if (style.align) {
|
||||
styles.textAlign = style.align
|
||||
}
|
||||
|
||||
// Междустрочный интервал
|
||||
if (style.lineHeight) {
|
||||
styles.lineHeight = style.lineHeight
|
||||
}
|
||||
|
||||
// Отступы
|
||||
if (style.spaceBefore) {
|
||||
styles.marginTop = `${style.spaceBefore}pt`
|
||||
}
|
||||
|
||||
if (style.spaceAfter) {
|
||||
styles.marginBottom = `${style.spaceAfter}pt`
|
||||
}
|
||||
|
||||
if (style.indent) {
|
||||
if (style.indent.left) {
|
||||
styles.marginLeft = `${style.indent.left}pt`
|
||||
}
|
||||
if (style.indent.right) {
|
||||
styles.marginRight = `${style.indent.right}pt`
|
||||
}
|
||||
if (style.indent.firstLine) {
|
||||
styles.textIndent = `${style.indent.firstLine}pt`
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 :data-element-id="element.id" :style="styles" class="font-bold" v-html="content"></h2>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
35
resources/js/Components/Document/DocumentHtml.vue
Normal file
35
resources/js/Components/Document/DocumentHtml.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
content: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="html-content mb-4" v-html="content"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.html-content {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.html-content ::v-deep(.placeholder) {
|
||||
background-color: #fffacd;
|
||||
border-bottom: 1px dashed #ccc;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.html-content ::v-deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.html-content ::v-deep(table, th, td) {
|
||||
border: 1px solid black;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
17
resources/js/Components/Document/DocumentLineBreak.vue
Normal file
17
resources/js/Components/Document/DocumentLineBreak.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
element: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<br :data-element-id="element.id" class="block content-[\'\'] m-0 p-0 h-0" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
139
resources/js/Components/Document/DocumentParagraph.vue
Normal file
139
resources/js/Components/Document/DocumentParagraph.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import DocumentText from './DocumentText.vue';
|
||||
// import DocumentVariable from './DocumentVariable.vue';
|
||||
import DocumentTextRun from './DocumentTextRun.vue';
|
||||
import DocumentLineBreak from './DocumentLineBreak.vue';
|
||||
|
||||
const props = defineProps({
|
||||
element: Object,
|
||||
formData: Object
|
||||
});
|
||||
|
||||
defineEmits(['variable-click']);
|
||||
|
||||
const elementComponents = {
|
||||
text: DocumentText,
|
||||
// variable: DocumentVariable,
|
||||
text_run: DocumentTextRun,
|
||||
line_break: DocumentLineBreak
|
||||
};
|
||||
|
||||
const paragraphStyles = computed(() => {
|
||||
const styles = {};
|
||||
const paragraphStyle = props.element.style || {};
|
||||
|
||||
// Выравнивание
|
||||
if (paragraphStyle.align) {
|
||||
styles.textAlign = paragraphStyle.align;
|
||||
}
|
||||
|
||||
// Междустрочный интервал
|
||||
if (paragraphStyle.lineHeight) {
|
||||
styles.lineHeight = paragraphStyle.lineHeight;
|
||||
}
|
||||
|
||||
// Отступы
|
||||
if (paragraphStyle.spaceBefore) {
|
||||
styles.marginTop = `${paragraphStyle.spaceBefore}pt`;
|
||||
}
|
||||
|
||||
if (paragraphStyle.spaceAfter) {
|
||||
styles.marginBottom = `${paragraphStyle.spaceAfter}pt`;
|
||||
}
|
||||
|
||||
if (paragraphStyle.indent) {
|
||||
if (paragraphStyle.indent.left) {
|
||||
styles.marginLeft = `${paragraphStyle.indent.left}pt`;
|
||||
}
|
||||
if (paragraphStyle.indent.right) {
|
||||
styles.marginRight = `${paragraphStyle.indent.right}pt`;
|
||||
}
|
||||
if (paragraphStyle.indent.firstLine) {
|
||||
styles.textIndent = `${paragraphStyle.indent.firstLine}pt`;
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
});
|
||||
|
||||
const paragraphClasses = computed(() => {
|
||||
const classes = ['paragraph-element'];
|
||||
|
||||
if (props.element.style?.align) {
|
||||
classes.push(`align-${props.element.style.align}`);
|
||||
}
|
||||
|
||||
return classes;
|
||||
});
|
||||
|
||||
const getComponentForElement = (element) => {
|
||||
return elementComponents[element.type] || 'span';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
class="document-paragraph"
|
||||
:style="paragraphStyles"
|
||||
:class="paragraphClasses"
|
||||
>
|
||||
<template v-if="element.elements" v-for="element in element.elements" :key="element.id">
|
||||
<component
|
||||
:is="getComponentForElement(element)"
|
||||
:element="element"
|
||||
:form-data="formData"
|
||||
:is-inline="true"
|
||||
@variable-click="$emit('variable-click', $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ element.content }}
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.document-paragraph {
|
||||
margin: 0.5em 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.document-paragraph.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.document-paragraph.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.document-paragraph.align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.document-paragraph.align-distribute {
|
||||
text-align: justify;
|
||||
text-justify: distribute;
|
||||
}
|
||||
|
||||
/* Наследование стилей для вложенных элементов */
|
||||
.document-paragraph ::v-deep(.text-element) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.document-paragraph ::v-deep(.variable-element) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.document-paragraph ::v-deep(.text-run) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Стили для печати */
|
||||
@media print {
|
||||
.document-paragraph {
|
||||
text-align: justify;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
resources/js/Components/Document/DocumentRenderer.vue
Normal file
107
resources/js/Components/Document/DocumentRenderer.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import DocumentElement from "./DocumentElement.vue"
|
||||
import A4 from "./A4.vue";
|
||||
import {computed, getCurrentInstance, nextTick, onMounted, ref, watch, watchEffect} from "vue";
|
||||
import {useDynamicA4Layout} from "../../Composables/useDynamicA4Layout.js";
|
||||
import {waitForAllComponentsMounted} from "../../Utils/heightCalculator.js";
|
||||
|
||||
const props = defineProps({
|
||||
structure: Array,
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['variable-click'])
|
||||
|
||||
const {calculateDynamicLayout, componentHeights, a4Pages} = useDynamicA4Layout()
|
||||
|
||||
const allElements = computed(() => props.structure.elements)
|
||||
const componentRefs = ref(new Map())
|
||||
|
||||
const handleVariableClick = (variableName) => {
|
||||
emit('variable-click', variableName)
|
||||
}
|
||||
|
||||
const elementRefs = ref(new Map())
|
||||
const addToRefs = (el, id) => {
|
||||
if (el)
|
||||
elementRefs.value.set(id, el.$el)
|
||||
}
|
||||
|
||||
const hasAllMounted = computed(() => {
|
||||
// Array.from(elementRefs.value.values())
|
||||
// .every(ref => ref.value?.$.vnode.el?.isConnected)
|
||||
|
||||
|
||||
})
|
||||
|
||||
// watch(() => hasAllMounted, async (newSize, oldSize) => {
|
||||
// const expectedCount = props.structure?.elements?.length || 0
|
||||
//
|
||||
// if (newSize === expectedCount && newSize > 0 && newSize !== oldSize) {
|
||||
// // Даём время на полное монтирование
|
||||
// await nextTick()
|
||||
// await nextTick() // Двойной nextTick для надёжности
|
||||
//
|
||||
// console.log(hasAllMounted.value)
|
||||
//
|
||||
// // try {
|
||||
// // await waitForAllComponentsMounted(getCurrentInstance())
|
||||
// // console.log('Все компоненты смонтированы!')
|
||||
// // await calculateDynamicLayout(props.structure.elements, () => elementRefs.value)
|
||||
// // } catch (error) {
|
||||
// // console.error('Ошибка при ожидании монтирования:', error)
|
||||
// // }
|
||||
// }
|
||||
// })
|
||||
onMounted(() => {
|
||||
// console.log(Array.from(elementRefs.value.values())[0].)
|
||||
})
|
||||
watch(() => elementRefs.value.size, async () => {
|
||||
const expectedCount = props.structure?.elements?.length || 0
|
||||
const currentSize = elementRefs.value.size
|
||||
|
||||
if (currentSize > 0 && currentSize === expectedCount) {
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
// console.log('Все компоненты смонтированы!')
|
||||
await calculateDynamicLayout(props.structure.elements, () => elementRefs.value)
|
||||
}
|
||||
}, {
|
||||
flush: 'post'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<A4 class="absolute -left-[9999px] -top-[9999px] section collapse document-content overflow-hidden">
|
||||
<DocumentElement
|
||||
v-for="element in allElements"
|
||||
:ref="el => addToRefs(el, element.id)"
|
||||
:element="element"
|
||||
/>
|
||||
</A4>
|
||||
<A4 v-for="paginatedElement in a4Pages" cover class="section document-content my-2 overflow-hidden">
|
||||
<template v-for="item in paginatedElement.items" :key="item.id">
|
||||
<DocumentElement
|
||||
:contenteditable="editable"
|
||||
:class="editable ? 'overflow-hidden outline-none' : ''"
|
||||
:element="item"
|
||||
@variable-click="handleVariableClick"
|
||||
/>
|
||||
</template>
|
||||
</A4>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.document-content {
|
||||
line-height: 1.2;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.section {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
40
resources/js/Components/Document/DocumentTable.vue
Normal file
40
resources/js/Components/Document/DocumentTable.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import DocumentElement from "./DocumentElement.vue";
|
||||
import DocumentTableRow from "./DocumentTableRow.vue";
|
||||
|
||||
const props = defineProps({
|
||||
content: String,
|
||||
element: Object,
|
||||
})
|
||||
|
||||
const rows = computed(() => props.element.rows || [])
|
||||
const cols = computed(() => props.element.cols || 0)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table :data-element-id="element.id" class="table-element">
|
||||
<tbody>
|
||||
<DocumentTableRow @is-mounted="" :rows="rows" />
|
||||
<!-- <tr-->
|
||||
<!-- v-for="(row, rowIndex) in rows"-->
|
||||
<!-- :key="`row-${rowIndex}`"-->
|
||||
<!-- >-->
|
||||
<!-- <td-->
|
||||
<!-- v-for="(cell, colIndex) in row.cells"-->
|
||||
<!-- :key="`td-${rowIndex}-${colIndex}`"-->
|
||||
<!-- :style="`width: ${cell.width}px`"-->
|
||||
<!-- >-->
|
||||
<!-- <DocumentElement v-for="element in cell.elements" :element="element" :key="element.id" />-->
|
||||
<!-- </td>-->
|
||||
<!-- </tr>-->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
13
resources/js/Components/Document/DocumentTableCell.vue
Normal file
13
resources/js/Components/Document/DocumentTableCell.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
element: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
29
resources/js/Components/Document/DocumentTableRow.vue
Normal file
29
resources/js/Components/Document/DocumentTableRow.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import DocumentElement from "./DocumentElement.vue";
|
||||
import {onMounted} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
rows: Array
|
||||
})
|
||||
|
||||
const emits = defineEmits(['is-mounted'])
|
||||
|
||||
onMounted(() => {
|
||||
emits('is-mounted', true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
v-for="(row, rowIndex) in rows"
|
||||
:key="`row-${rowIndex}`"
|
||||
>
|
||||
<td v-for="cell in row.cells" :style="`width: ${cell.width}px`">
|
||||
<DocumentElement v-for="element in cell.elements" :element="element" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
93
resources/js/Components/Document/DocumentText.vue
Normal file
93
resources/js/Components/Document/DocumentText.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
parentElement: Object,
|
||||
element: Object,
|
||||
formData: Object,
|
||||
isInline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const displayText = computed(() => {
|
||||
return props.element.content || '';
|
||||
});
|
||||
|
||||
const textStyles = computed(() => {
|
||||
const styles = {};
|
||||
const formatting = props.element.formatting || {};
|
||||
|
||||
if (formatting.fontSize) {
|
||||
styles.fontSize = formatting.fontSize;
|
||||
}
|
||||
|
||||
if (formatting.fontFamily) {
|
||||
styles.fontFamily = formatting.fontFamily;
|
||||
}
|
||||
|
||||
if (formatting.fontColor) {
|
||||
styles.color = formatting.fontColor;
|
||||
}
|
||||
|
||||
return styles;
|
||||
});
|
||||
|
||||
const textClasses = computed(() => {
|
||||
const classes = [];
|
||||
const formatting = props.element.formatting || {};
|
||||
|
||||
if (formatting.bold) {
|
||||
classes.push('text-bold');
|
||||
}
|
||||
|
||||
if (formatting.italic) {
|
||||
classes.push('text-italic');
|
||||
}
|
||||
|
||||
if (formatting.underline !== 'none') {
|
||||
classes.push('text-underline');
|
||||
}
|
||||
|
||||
return classes;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:data-element-id="element.id"
|
||||
class="text-element"
|
||||
:style="textStyles"
|
||||
:class="textClasses"
|
||||
>
|
||||
{{ displayText }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-element {
|
||||
display: inline;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Стили для печати */
|
||||
@media print {
|
||||
.text-element {
|
||||
color: black !important;
|
||||
font-family: 'Times New Roman', serif !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
resources/js/Components/Document/DocumentTextRun.vue
Normal file
141
resources/js/Components/Document/DocumentTextRun.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import {computed, ref} from 'vue';
|
||||
import DocumentText from './DocumentText.vue';
|
||||
// import DocumentVariable from './DocumentVariable.vue';
|
||||
import DocumentLineBreak from './DocumentLineBreak.vue';
|
||||
|
||||
const props = defineProps({
|
||||
element: Object,
|
||||
formData: Object
|
||||
});
|
||||
|
||||
defineEmits(['variable-click']);
|
||||
|
||||
const elementComponents = {
|
||||
text: DocumentText,
|
||||
// variable: DocumentVariable,
|
||||
line_break: DocumentLineBreak
|
||||
}
|
||||
|
||||
const textRunRef = ref()
|
||||
|
||||
const combinedStyles = computed(() => {
|
||||
const styles = {}
|
||||
const formatting = props.element.formatting || {}
|
||||
const paragraphStyle = props.element.style || {}
|
||||
|
||||
// Стили шрифта
|
||||
if (formatting.fontSize) {
|
||||
styles.fontSize = formatting.fontSize;
|
||||
}
|
||||
|
||||
if (formatting.fontFamily) {
|
||||
styles.fontFamily = formatting.fontFamily;
|
||||
}
|
||||
|
||||
if (formatting.fontColor) {
|
||||
styles.color = formatting.fontColor;
|
||||
}
|
||||
|
||||
if (formatting.backgroundColor) {
|
||||
styles.backgroundColor = formatting.backgroundColor;
|
||||
}
|
||||
|
||||
// Стили параграфа (если есть)
|
||||
if (paragraphStyle.align) {
|
||||
styles.textAlign = paragraphStyle.align;
|
||||
}
|
||||
|
||||
if (paragraphStyle.lineHeight) {
|
||||
styles.lineHeight = paragraphStyle.lineHeight;
|
||||
}
|
||||
|
||||
if (paragraphStyle.indent) {
|
||||
// console.log(paragraphStyle.indent)
|
||||
if (paragraphStyle.indent.firstLine)
|
||||
styles['--first-line-indent'] = `${paragraphStyle.indent.firstLine}px`
|
||||
}
|
||||
|
||||
return styles;
|
||||
});
|
||||
|
||||
const textRunClasses = computed(() => {
|
||||
// const classes = []
|
||||
// const style = props.element.style
|
||||
// if (style && style.indent) {
|
||||
// if (style.indent.firstLine)
|
||||
// classes.push(`first:pl-[${style.indent.firstLine}px]`)
|
||||
// }
|
||||
//
|
||||
// return classes
|
||||
// const classes = ['text-run-element']
|
||||
//
|
||||
// const formatting = props.element.formatting || {}
|
||||
// if (formatting.bold) classes.push('text-bold')
|
||||
// if (formatting.italic) classes.push('text-italic')
|
||||
// if (formatting.underline !== 'none') classes.push('text-underline')
|
||||
// if (formatting.strikethrough) classes.push('text-strikethrough')
|
||||
//
|
||||
// return classes;
|
||||
});
|
||||
|
||||
const getComponentForElement = (element) => {
|
||||
return elementComponents[element.type] || 'span';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="textRunRef" :data-element-id="element.id" :style="combinedStyles" >
|
||||
<template v-for="children in element.elements" :key="children.id" >
|
||||
<DocumentText
|
||||
class="first:pl-[var(--first-line-indent)]"
|
||||
:element="children"
|
||||
:form-data="formData"
|
||||
@variable-click="$emit('variable-click', $event)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.text-run {
|
||||
display: inline;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.text-run-element {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.text-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Наследование стилей для вложенных элементов */
|
||||
.text-run ::v-deep(.text-element) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.text-run ::v-deep(.variable-element) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.text-run ::v-deep(.line-break) {
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
24
resources/js/Components/Document/DocumentWarning.vue
Normal file
24
resources/js/Components/Document/DocumentWarning.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
content: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="warning-note bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700" v-html="content"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
136
resources/js/Components/Document/InputVariable/PriceInput.vue
Normal file
136
resources/js/Components/Document/InputVariable/PriceInput.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import Input from "../../Input/Input.vue";
|
||||
import {computed, watch} from "vue";
|
||||
|
||||
const numberModel = defineModel('number')
|
||||
const textModel = defineModel('text')
|
||||
|
||||
// Функция для преобразования числа в текст
|
||||
const numberToWords = (num) => {
|
||||
if (num === 0) return 'ноль рублей'
|
||||
if (num < 0) return 'минус ' + numberToWords(Math.abs(num))
|
||||
|
||||
const units = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
|
||||
const teens = ['десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать']
|
||||
const tens = ['', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто']
|
||||
const hundreds = ['', 'сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот']
|
||||
|
||||
const thousands = ['', 'одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
|
||||
const millions = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
|
||||
const billions = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять']
|
||||
|
||||
// Функция для преобразования трехзначного числа
|
||||
const convertThreeDigit = (n, isFemale = false) => {
|
||||
if (n === 0) return ''
|
||||
|
||||
let result = ''
|
||||
const hundred = Math.floor(n / 100)
|
||||
const remainder = n % 100
|
||||
|
||||
if (hundred > 0) {
|
||||
result += hundreds[hundred] + ' '
|
||||
}
|
||||
|
||||
if (remainder >= 20) {
|
||||
const ten = Math.floor(remainder / 10)
|
||||
const unit = remainder % 10
|
||||
result += tens[ten] + ' '
|
||||
if (unit > 0) {
|
||||
if (isFemale && unit <= 2) {
|
||||
result += (unit === 1 ? 'одна' : 'две') + ' '
|
||||
} else {
|
||||
result += units[unit] + ' '
|
||||
}
|
||||
}
|
||||
} else if (remainder >= 10) {
|
||||
result += teens[remainder - 10] + ' '
|
||||
} else if (remainder > 0) {
|
||||
if (isFemale && remainder <= 2) {
|
||||
result += (remainder === 1 ? 'одна' : 'две') + ' '
|
||||
} else {
|
||||
result += units[remainder] + ' '
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
// Функция для получения правильного окончания
|
||||
const getEnding = (num, forms) => {
|
||||
const lastDigit = num % 10
|
||||
const lastTwoDigits = num % 100
|
||||
|
||||
if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
|
||||
return forms[2]
|
||||
}
|
||||
if (lastDigit === 1) {
|
||||
return forms[0]
|
||||
}
|
||||
if (lastDigit >= 2 && lastDigit <= 4) {
|
||||
return forms[1]
|
||||
}
|
||||
return forms[2]
|
||||
}
|
||||
|
||||
let result = ''
|
||||
let remaining = num
|
||||
|
||||
// Миллиарды
|
||||
const billionsPart = Math.floor(remaining / 1000000000)
|
||||
if (billionsPart > 0) {
|
||||
result += convertThreeDigit(billionsPart) + ' '
|
||||
result += getEnding(billionsPart, ['миллиард', 'миллиарда', 'миллиардов']) + ' '
|
||||
remaining %= 1000000000
|
||||
}
|
||||
|
||||
// Миллионы
|
||||
const millionsPart = Math.floor(remaining / 1000000)
|
||||
if (millionsPart > 0) {
|
||||
result += convertThreeDigit(millionsPart) + ' '
|
||||
result += getEnding(millionsPart, ['миллион', 'миллиона', 'миллионов']) + ' '
|
||||
remaining %= 1000000
|
||||
}
|
||||
|
||||
// Тысячи
|
||||
const thousandsPart = Math.floor(remaining / 1000)
|
||||
if (thousandsPart > 0) {
|
||||
result += convertThreeDigit(thousandsPart, true) + ' '
|
||||
result += getEnding(thousandsPart, ['тысяча', 'тысячи', 'тысяч']) + ' '
|
||||
remaining %= 1000
|
||||
}
|
||||
|
||||
// Сотни, десятки, единицы
|
||||
if (remaining > 0) {
|
||||
result += convertThreeDigit(remaining) + ' '
|
||||
}
|
||||
|
||||
// Добавляем рубли с правильным окончанием
|
||||
result += getEnding(num, ['рубль', 'рубля', 'рублей'])
|
||||
|
||||
return result.trim().replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
// Вычисляемое свойство для текстового представления
|
||||
const amountInWords = computed(() => {
|
||||
const num = Number(numberModel.value) || 0
|
||||
return numberToWords(num)
|
||||
})
|
||||
|
||||
// Следим за изменениями и обновляем модель
|
||||
watch(amountInWords, (newValue) => {
|
||||
textModel.value = `${numberModel.value} руб. (${newValue})`
|
||||
}, { immediate: true })
|
||||
|
||||
// Обработчик изменения ввода
|
||||
const handleInput = (value) => {
|
||||
numberModel.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input v-model:value="numberModel" @update:value="handleInput" placeholder="Введите сумму" type="number" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user