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">
|
<div v-if="!svgQueryIsLoading && svgQueryData" class="border p-4 bg-white rounded-lg mb-6">
|
||||||
<img :src="svgQueryData" alt="QR‑код" />
|
<img :src="svgQueryData" alt="QR‑код" />
|
||||||
</div>
|
</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">
|
<p v-if="qrCodeQueryIsError" class="text-red-500 mt-4">
|
||||||
@@ -63,14 +66,17 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import QRCodeStyling from 'qr-code-styling'
|
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 { getQrCodeDocument } from '@/api/qrCode'
|
||||||
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||||
import { calculateFontSizeForText } from '@/utils/images/calculateFontSizeForText'
|
|
||||||
import { downloadImage } from '@/utils/images/downloadImage'
|
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 { 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 label = ref('Сообщите о проблеме или задайте вопрос')
|
||||||
const width = ref(256 * 4)
|
const width = ref(256 * 4)
|
||||||
const height = ref(width.value)
|
const height = ref(width.value)
|
||||||
@@ -85,123 +91,75 @@ const {
|
|||||||
queryFn: () => getQrCodeDocument(qr_code_uri),
|
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 sanitizedFileName = computed(() => {
|
||||||
const sanitized = qrCodeDoc.value?.name
|
const sanitized = qrCodeDoc.value?.name
|
||||||
?.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') // Убираем запрещённые символы
|
?.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') // Убираем запрещённые символы
|
||||||
.replace(/\s+/g, '_') // Заменяем пробелы на подчёркивания
|
.replace(/\s+/g, ' ') // Заменяем пробелы на подчёркивания
|
||||||
.trim()
|
.trim()
|
||||||
return sanitized ? `${sanitized}_QR-Code_HereConnect` : 'QR-Code_HereConnect'
|
return sanitized ? `${sanitized} QR-Code - НаСвязи` : 'QR-Code - НаСвязи'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Хук для генерации и оптимизации SVG
|
// Генерация и оптимизация SVG
|
||||||
const {
|
const {
|
||||||
isLoading: svgQueryIsLoading,
|
isLoading: svgQueryIsLoading,
|
||||||
|
isError: svgQueryIsError,
|
||||||
data: svgQueryData,
|
data: svgQueryData,
|
||||||
error: svgQueryError,
|
error: svgQueryError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['svg', computed(() => qr_code_uri)],
|
queryKey: ['svg', computed(() => qr_code_uri)],
|
||||||
enabled: computed(() => !!qrCodeDoc.value?.url),
|
enabled: computed(() => !!qrCodeDoc.value?.url),
|
||||||
|
retry: false,
|
||||||
queryFn: async () => {
|
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')
|
const blob = await qrCode.getRawData('svg')
|
||||||
if (blob && blob instanceof Blob) {
|
if (blob && blob instanceof Blob) {
|
||||||
const text = await blob.text()
|
const text = await blob.text()
|
||||||
|
let resultSvg = optimizeSvg(text)
|
||||||
const optimizedSvg = optimize(text, {
|
if (!resultSvg) {
|
||||||
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 {
|
|
||||||
throw new Error('Ошибка оптимизации SVG')
|
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 {
|
} else {
|
||||||
throw new Error('Ошибка генерации QR‑кода')
|
throw new Error('Ошибка генерации QR‑кода')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled(data) {
|
|
||||||
if (!data && svgQueryError) {
|
|
||||||
console.error('Ошибка загрузки SVG:', svgQueryError.message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Скачивание PNG
|
// Скачивание PNG
|
||||||
|
|||||||
Reference in New Issue
Block a user