File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 103 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#FB2500" stroke="None" d="M94 384c-10.69 10.9-19.59 23.73-29.75 35.25C57.4 429.31 33.45 446.87 54 455h65c61.67-2.91 126.65 7.03 185.33-7.67 117.65-27.97 185.84-172.81 132.59-281.25C390.54 54.32 238.47 9.09 139.7 77.7 34.91 139.96 15.23 294.73 94 384m236-226v177c-26.16-6.68-51.76-15.48-78-22 8.51-24.46 20.5-47.86 29.98-72.02 5.09-16.89-18.53-19.16-25.73-6.73-18.43 15.59-37.17 30.71-55.5 46.5-9.63 13.08-22.66.79-34.43-2.07-12.21-5.3-25.45-9.3-37.32-14.68 66.54-35.63 133.64-72.07 201-106Z"/></svg>
|
||||
|
After Width: | Height: | Size: 573 B |
@@ -0,0 +1,53 @@
|
||||
import { calculateFontSizeForText } from '@/utils/calculateFontSizeForText'
|
||||
|
||||
/**
|
||||
* Добавление текста в SVG, прибитого к нижнему краю.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param label Надпись для добавления.
|
||||
* @param color Цвет текста.
|
||||
* @param marginMultiplier Множитель относительного отступа.
|
||||
* @returns SVG с добавленным текстом.
|
||||
*/
|
||||
export function addLabelToSvg(
|
||||
svgText: string,
|
||||
label: string,
|
||||
color: string,
|
||||
marginMultiplier: number = 0.1,
|
||||
): string {
|
||||
if (!label) {
|
||||
return svgText
|
||||
}
|
||||
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml')
|
||||
const svgElement = svgDoc.querySelector('svg')
|
||||
|
||||
if (svgElement) {
|
||||
// Увеличиваем viewBox для текста
|
||||
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number)
|
||||
if (viewBox) {
|
||||
const fontSize = calculateFontSizeForText(label, viewBox[2])
|
||||
const textMargin = Math.min(fontSize, viewBox[3] * marginMultiplier)
|
||||
|
||||
viewBox[3] += fontSize + textMargin // Увеличиваем высоту
|
||||
svgElement.setAttribute('viewBox', viewBox.join(' '))
|
||||
|
||||
// Добавляем текст
|
||||
const textElement = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'text')
|
||||
textElement.setAttribute('x', '50%')
|
||||
textElement.setAttribute('y', '100%') // Устанавливаем Y на 100% высоты
|
||||
textElement.setAttribute('text-anchor', 'middle')
|
||||
textElement.setAttribute('alignment-baseline', 'baseline') // Привязываем к нижнему краю
|
||||
textElement.setAttribute('dominant-baseline', 'text-after-edge') // Альтернативный способ
|
||||
textElement.setAttribute('fill', color)
|
||||
textElement.setAttribute('font-size', fontSize.toString())
|
||||
textElement.setAttribute('font-family', 'Arial, sans-serif')
|
||||
textElement.textContent = label
|
||||
|
||||
svgElement.appendChild(textElement)
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
return serializer.serializeToString(svgDoc)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { optimize } from 'svgo'
|
||||
|
||||
/**
|
||||
* Оптимизация SVG с использованием SVGO.
|
||||
* @param svgText string изображения SVG.
|
||||
* @returns Строка оптимизированного SVG.
|
||||
*/
|
||||
export function optimizeSvg(svgText: string): string {
|
||||
const optimizedSvg = optimize(svgText, {
|
||||
plugins: [
|
||||
{ name: 'removeDoctype' },
|
||||
{ name: 'removeComments' },
|
||||
{ name: 'removeDimensions' },
|
||||
{ name: 'convertShapeToPath' },
|
||||
{ name: 'convertPathData' },
|
||||
{ name: 'mergePaths' },
|
||||
],
|
||||
})
|
||||
|
||||
if (!optimizedSvg.data) {
|
||||
throw new Error('Ошибка оптимизации SVG')
|
||||
}
|
||||
|
||||
return optimizedSvg.data
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Заменяет атрибут `fill` в SVG-коде.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param fillColor Новый цвет для fill.
|
||||
* @returns Измененный SVG-код.
|
||||
*/
|
||||
export function replaceFillInSvg(svgText: string, fillColor: string): string {
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml')
|
||||
const paths = svgDoc.querySelectorAll('path, rect, circle, polygon, ellipse')
|
||||
|
||||
paths.forEach((path) => {
|
||||
if (path.hasAttribute('fill')) {
|
||||
path.setAttribute('fill', fillColor)
|
||||
}
|
||||
})
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
return serializer.serializeToString(svgDoc)
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
<div v-if="!svgQueryIsLoading && svgQueryData" class="border p-4 bg-white rounded-lg mb-6">
|
||||
<img :src="svgQueryData" alt="QR‑код" />
|
||||
</div>
|
||||
<p v-else-if="svgQueryIsError" class="text-red-500 mt-4">
|
||||
Ошибка генерации QR‑кода: {{ svgQueryError?.message }}
|
||||
</p>
|
||||
|
||||
<!-- Сообщение об ошибке -->
|
||||
<p v-if="qrCodeQueryIsError" class="text-red-500 mt-4">
|
||||
@@ -63,14 +66,17 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import QRCodeStyling from 'qr-code-styling'
|
||||
import { optimize } from 'svgo'
|
||||
import { optimizeSvg } from '@/utils/images/svg/optimizeSvg'
|
||||
import { replaceFillInSvg } from '@/utils/images/svg/replaceFillInSvg'
|
||||
import { getQrCodeDocument } from '@/api/qrCode'
|
||||
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||
import { calculateFontSizeForText } from '@/utils/images/calculateFontSizeForText'
|
||||
import { downloadImage } from '@/utils/images/downloadImage'
|
||||
import { addLabelToSvg } from '@/utils/images/svg/addLabelToSvg'
|
||||
import logo from '@/assets/logo.svg?raw'
|
||||
|
||||
const { qr_code_uri } = defineProps<{ qr_code_uri: QRCodeDocument['uri'] }>()
|
||||
const color = ref('#f43f5e')
|
||||
const color = ref('#000') // Цвет для текста QR-кода
|
||||
const logoColor = ref(color.value) // Цвет логотипа // #FB2500
|
||||
const label = ref('Сообщите о проблеме или задайте вопрос')
|
||||
const width = ref(256 * 4)
|
||||
const height = ref(width.value)
|
||||
@@ -85,123 +91,75 @@ const {
|
||||
queryFn: () => getQrCodeDocument(qr_code_uri),
|
||||
})
|
||||
|
||||
// Инициализация QRCodeStyling
|
||||
const qrCode = new QRCodeStyling({
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
data: '',
|
||||
image: '/images/logo.svg',
|
||||
imageOptions: {
|
||||
margin: 20,
|
||||
// hideBackgroundDots: false,
|
||||
imageSize: 0.45,
|
||||
},
|
||||
type: 'svg',
|
||||
backgroundOptions: {
|
||||
color: 'transparent',
|
||||
},
|
||||
cornersSquareOptions: {
|
||||
type: 'extra-rounded',
|
||||
},
|
||||
cornersDotOptions: {
|
||||
type: 'dot',
|
||||
},
|
||||
dotsOptions: {
|
||||
color: color.value,
|
||||
type: 'classy-rounded',
|
||||
},
|
||||
})
|
||||
|
||||
// Получение имени файла
|
||||
const sanitizedFileName = computed(() => {
|
||||
const sanitized = qrCodeDoc.value?.name
|
||||
?.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') // Убираем запрещённые символы
|
||||
.replace(/\s+/g, '_') // Заменяем пробелы на подчёркивания
|
||||
.replace(/\s+/g, ' ') // Заменяем пробелы на подчёркивания
|
||||
.trim()
|
||||
return sanitized ? `${sanitized}_QR-Code_HereConnect` : 'QR-Code_HereConnect'
|
||||
return sanitized ? `${sanitized} QR-Code - НаСвязи` : 'QR-Code - НаСвязи'
|
||||
})
|
||||
|
||||
// Хук для генерации и оптимизации SVG
|
||||
// Генерация и оптимизация SVG
|
||||
const {
|
||||
isLoading: svgQueryIsLoading,
|
||||
isError: svgQueryIsError,
|
||||
data: svgQueryData,
|
||||
error: svgQueryError,
|
||||
} = useQuery({
|
||||
queryKey: ['svg', computed(() => qr_code_uri)],
|
||||
enabled: computed(() => !!qrCodeDoc.value?.url),
|
||||
retry: false,
|
||||
queryFn: async () => {
|
||||
qrCode.update({ data: qrCodeDoc.value!.url })
|
||||
// Заменяем цвет `fill` в логотипе
|
||||
const updatedLogo = replaceFillInSvg(logo, logoColor.value)
|
||||
|
||||
// Добавляем текст в логотип (если нужно)
|
||||
const logoWithText = addLabelToSvg(updatedLogo, 'НаСвязи', logoColor.value, 0.01)
|
||||
|
||||
const image = URL.createObjectURL(new Blob([logoWithText], { type: 'image/svg+xml' }))
|
||||
|
||||
// Инициализация QRCodeStyling
|
||||
const qrCode = new QRCodeStyling({
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
data: qrCodeDoc.value!.url,
|
||||
image,
|
||||
imageOptions: {
|
||||
margin: 20,
|
||||
imageSize: 0.45,
|
||||
},
|
||||
type: 'svg',
|
||||
backgroundOptions: {
|
||||
color: 'transparent',
|
||||
},
|
||||
cornersSquareOptions: {
|
||||
type: 'extra-rounded',
|
||||
},
|
||||
cornersDotOptions: {
|
||||
type: 'dot',
|
||||
},
|
||||
dotsOptions: {
|
||||
color: color.value,
|
||||
type: 'classy-rounded',
|
||||
},
|
||||
})
|
||||
|
||||
const blob = await qrCode.getRawData('svg')
|
||||
if (blob && blob instanceof Blob) {
|
||||
const text = await blob.text()
|
||||
|
||||
const optimizedSvg = optimize(text, {
|
||||
plugins: [
|
||||
{ name: 'removeDoctype' },
|
||||
{ name: 'removeComments' },
|
||||
{ name: 'removeDimensions' },
|
||||
{ name: 'convertShapeToPath' },
|
||||
{ name: 'convertPathData' },
|
||||
{ name: 'mergePaths' },
|
||||
],
|
||||
})
|
||||
|
||||
if (optimizedSvg.data) {
|
||||
if (!label.value) {
|
||||
// Если label пуст, возвращаем SVG без изменений
|
||||
return URL.createObjectURL(new Blob([optimizedSvg.data], { type: 'image/svg+xml' }))
|
||||
}
|
||||
|
||||
// Добавляем надпись и увеличиваем viewBox
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(optimizedSvg.data, 'image/svg+xml')
|
||||
const svgElement = svgDoc.querySelector('svg')
|
||||
|
||||
if (svgElement) {
|
||||
// Увеличиваем viewBox
|
||||
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number)
|
||||
if (viewBox) {
|
||||
// Расчет реального размера шрифта
|
||||
const fontSize = calculateFontSizeForText(label.value, viewBox[2])
|
||||
|
||||
// Рассчитываем вертикальное расстояние между QR-кодом и текстом
|
||||
const textMargin = Math.min(fontSize, viewBox[3] * 0.1) // Максимум 10% от высоты QR-кода
|
||||
|
||||
viewBox[3] += fontSize + textMargin // Увеличиваем высоту для текста
|
||||
svgElement.setAttribute('viewBox', viewBox.join(' '))
|
||||
|
||||
// Добавляем текст
|
||||
const textElement = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'text')
|
||||
textElement.setAttribute('x', '50%')
|
||||
textElement.setAttribute('y', viewBox[3] - fontSize / 2) // Уменьшаем нижний отступ
|
||||
textElement.setAttribute('text-anchor', 'middle')
|
||||
textElement.setAttribute('fill', color.value)
|
||||
textElement.setAttribute('font-size', fontSize.toString())
|
||||
textElement.setAttribute('font-family', 'Arial, sans-serif')
|
||||
textElement.textContent = label.value
|
||||
|
||||
svgElement.appendChild(textElement)
|
||||
}
|
||||
}
|
||||
|
||||
// Сериализуем обратно в строку
|
||||
const serializer = new XMLSerializer()
|
||||
const updatedSvg = serializer.serializeToString(svgDoc)
|
||||
|
||||
return URL.createObjectURL(new Blob([updatedSvg], { type: 'image/svg+xml' }))
|
||||
} else {
|
||||
let resultSvg = optimizeSvg(text)
|
||||
if (!resultSvg) {
|
||||
throw new Error('Ошибка оптимизации SVG')
|
||||
}
|
||||
if (label.value) {
|
||||
resultSvg = addLabelToSvg(resultSvg, label.value, color.value)
|
||||
}
|
||||
return URL.createObjectURL(new Blob([resultSvg], { type: 'image/svg+xml' }))
|
||||
} else {
|
||||
throw new Error('Ошибка генерации QR‑кода')
|
||||
}
|
||||
},
|
||||
onSettled(data) {
|
||||
if (!data && svgQueryError) {
|
||||
console.error('Ошибка загрузки SVG:', svgQueryError.message)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Скачивание PNG
|
||||
|
||||
Reference in New Issue
Block a user