first commit

This commit is contained in:
brusnitsyn
2025-10-31 16:48:05 +09:00
commit 8b650558e2
143 changed files with 24664 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
// composables/useSmartApiForm.js
import { ref, reactive, watch } from 'vue'
import axios from 'axios'
export function useApiForm(initialData = {}) {
const loading = ref(false)
const errors = ref({})
const progress = ref(0)
// Для обычных полей формы
const formData = ref({ ...initialData })
// Для файлов
const files = ref({})
const setFile = (fieldName, file) => {
files.value[fieldName] = file
// Автоматически очищаем ошибку для этого поля
clearError(fieldName)
}
const clearError = (field) => {
if (errors.value[field]) {
const newErrors = { ...errors.value }
delete newErrors[field]
errors.value = newErrors
}
}
const clearAllErrors = () => {
errors.value = {}
}
const submit = async (url, method = 'post', config = {}) => {
loading.value = true
progress.value = 0
clearAllErrors()
try {
// Создаем FormData
const formDataToSend = new FormData()
// Добавляем обычные поля формы
Object.keys(formData.value).forEach(key => {
if (formData.value[key] !== null && formData.value[key] !== undefined) {
formDataToSend.append(key, formData.value[key])
}
})
// Добавляем файлы
Object.keys(files.value).forEach(key => {
if (files.value[key]) {
formDataToSend.append(key, files.value[key])
}
})
const response = await axios({
method,
url,
data: formDataToSend,
headers: {
'Content-Type': 'multipart/form-data',
...config.headers
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
progress.value = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
}
},
...config
})
return response.data
} catch (error) {
if (error.response?.status === 422) {
errors.value = error.response.data.errors || {}
}
throw error
} finally {
loading.value = false
}
}
const reset = () => {
// Сбрасываем обычные поля
Object.keys(initialData).forEach(key => {
formData.value[key] = initialData[key]
})
// Сбрасываем файлы
files.value = {}
clearAllErrors()
}
return {
formData,
files,
errors,
loading,
progress,
submit,
setFile,
clearError,
clearAllErrors,
reset
}
}

View File

@@ -0,0 +1,113 @@
import {
ref,
reactive,
onMounted,
onUpdated,
nextTick
} from "vue"
import {useElementSize} from "@vueuse/core";
export function useDynamicA4Layout() {
const A4_HEIGHT = 933
const A4_WIDTH = 623
const a4Pages = reactive([])
const isCalculating = ref(false)
const componentHeights = ref(new Map())
const waitForComponentRender = (componentRef, itemType) => {
return new Promise((resolve) => {
if (!componentRef) {
resolve(0)
return
}
const checkRender = () => {
requestAnimationFrame(() => {
if (componentRef.offsetHeight > 0) {
resolve(getComponentHeight(componentRef))
} else {
resolve(0)
// setTimeout(checkRender, 1000)
}
})
}
checkRender()
})
}
const getComponentHeight = (element) => {
if (!element) return 0
const styles = getComputedStyle(element)
return element.offsetHeight
+ parseInt(styles.marginTop)
+ parseInt(styles.marginBottom)
+ parseInt(styles.borderTopWidth)
+ parseInt(styles.borderBottomWidth)
}
const calculateDynamicLayout = async(items, getComponentRefs) => {
if (isCalculating.value) return
isCalculating.value = true
try {
await nextTick()
const componentRefs = getComponentRefs()
a4Pages.splice(0, a4Pages.length)
const currentPage = {items: [], totalHeight: 0, id: Date.now()}
a4Pages.push(currentPage)
const heightPromises = items.map(async (item, index) => {
const componentRef = componentRefs.get(item.id)
const height = await waitForComponentRender(componentRef, item.type)
componentHeights.value.set(item.id || index, height)
return height
})
const heights = await Promise.all(heightPromises)
let currentHeight = 0
const pageMargin = 0
items.forEach((item, index) => {
// console.log(item)
const itemHeight = heights[index]
const availableHeight = A4_HEIGHT - (pageMargin * 2) - currentHeight
console.log(currentHeight > 0 && availableHeight < itemHeight)
if (currentHeight > 0 && availableHeight < itemHeight) {
const newPage = {
items: [item],
totalHeight: itemHeight,
id: Date.now() + index
}
a4Pages.push(newPage)
currentHeight = itemHeight
} else {
const page = a4Pages[a4Pages.length - 1]
page.items.push(item)
page.totalHeight += itemHeight
currentHeight += itemHeight
}
})
} catch(error) {
console.error('Ошибка вычисления размера')
} finally {
isCalculating.value = false
}
}
return {
a4Pages,
A4_HEIGHT,
A4_WIDTH,
calculateDynamicLayout,
isCalculating,
componentHeights
}
}

View File

@@ -0,0 +1,66 @@
// composables/useFileDownload.js
export const useFileDownload = () => {
const downloadFile = async (url, data, filename = 'file', method = 'post') => {
try {
const response = await axios({
method,
url,
data: method === 'post' ? data : null,
params: method === 'get' ? data : null,
responseType: 'blob'
});
// Проверяем, что это действительно файл, а не ошибка
const contentType = response.headers['content-type'];
if (contentType.includes('application/json')) {
// Это JSON ошибка, а не файл
const errorData = JSON.parse(await response.data.text());
throw new Error(errorData.error || 'Download failed');
}
// Создаем blob URL
const blob = new Blob([response.data], {
type: response.headers['content-type']
});
const downloadUrl = window.URL.createObjectURL(blob);
// Создаем временную ссылку для скачивания
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
// Имитируем клик для скачивания
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Очищаем URL
window.URL.revokeObjectURL(downloadUrl);
return true;
} catch (error) {
console.error('Download error:', error);
throw error;
}
};
const getFileNameFromResponse = (response) => {
const contentDisposition = response.headers['content-disposition'];
let fileName = 'document.docx';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch && filenameMatch[1]) {
fileName = filenameMatch[1].replace(/"/g, '');
}
}
return fileName;
};
return {
downloadFile
};
};