отдельной страницей вывел готовый qr-код /manage/:qr_code_uri/view
Main daploy / deploy (push) Successful in 38s
Main daploy / deploy (push) Successful in 38s
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
import PouchDB from 'pouchdb-browser'
|
||||||
|
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||||
|
|
||||||
|
export const qrCodeDbRemote = new PouchDB<QRCodeDocument>('https://hereconnect.condev.ru/api/qr', {
|
||||||
|
skip_setup: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const qrCodeDbLocal = new PouchDB<QRCodeDocument>('qr')
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||||
|
import { qrCodeDbLocal, qrCodeDbRemote } from '@/api/dbs'
|
||||||
|
|
||||||
|
const EPOHE = 1731661630981
|
||||||
|
|
||||||
|
export const createQrCodeDocument = async (
|
||||||
|
qrCodeDocPartial: Pick<
|
||||||
|
QRCodeDocument,
|
||||||
|
'name' | 'deliveryMethod' | 'placement' | 'actions' | 'user_uuid' | 'predefinedMessage'
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const uri = Math.abs(Date.now() - EPOHE).toString(36)
|
||||||
|
|
||||||
|
const qrCodeDoc: QRCodeDocument = {
|
||||||
|
_id: `qr_code:${uri}`,
|
||||||
|
type: 'qr_code',
|
||||||
|
uri,
|
||||||
|
url: `https://hereconnect.condev.ru/read/${uri}`,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
...qrCodeDocPartial,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rev } = await qrCodeDbLocal.put(qrCodeDoc)
|
||||||
|
qrCodeDoc._rev = rev
|
||||||
|
|
||||||
|
const replicateResult = await qrCodeDbRemote.replicate.from(qrCodeDbLocal, {
|
||||||
|
doc_ids: [qrCodeDoc._id],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Проверяем результат репликации
|
||||||
|
if (!replicateResult.ok) {
|
||||||
|
throw new Error('Ошибка репликации: операция завершилась с ошибкой.')
|
||||||
|
}
|
||||||
|
if (replicateResult.doc_write_failures > 0) {
|
||||||
|
throw new Error(`Ошибка записи документов: ${replicateResult.doc_write_failures}`)
|
||||||
|
}
|
||||||
|
if (replicateResult.errors.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Ошибки репликации: ${replicateResult.errors.map((e: { message: string }) => e.message).join(', ')}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qrCodeDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getQrCodeDocument = (uri: string) =>
|
||||||
|
qrCodeDbLocal.get<QRCodeDocument>(`qr_code:${uri}`)
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Ваш QR-код готов</h2>
|
|
||||||
<p class="text-gray-600 mb-4">Сохраните его и ознакомьтесь с инструкциями по размещению.</p>
|
|
||||||
|
|
||||||
<!-- Рендер QR-кода -->
|
|
||||||
<div class="border p-4 bg-white rounded-lg mb-6" v-if="svgUrl">
|
|
||||||
<img :src="svgUrl" alt="QR-код" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Кнопки для скачивания -->
|
|
||||||
<div class="flex flex-col md:flex-row gap-4 justify-center">
|
|
||||||
<!-- Основная кнопка для скачивания SVG -->
|
|
||||||
<Button
|
|
||||||
v-if="svgUrl"
|
|
||||||
label="Скачать SVG"
|
|
||||||
icon="pi pi-download"
|
|
||||||
class="p-button-sm"
|
|
||||||
:href="svgUrl"
|
|
||||||
:download="`${sanitizedFileName}.svg`"
|
|
||||||
as="a"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Кнопка для скачивания PNG с outlined стилем -->
|
|
||||||
<Button
|
|
||||||
label="Скачать PNG"
|
|
||||||
icon="pi pi-download"
|
|
||||||
class="p-button-sm p-button-outlined"
|
|
||||||
@click="downloadPng"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="error" class="text-red-500 mt-4">Ошибка: {{ error }}</p>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
||||||
import QRCodeStyling from 'qr-code-styling'
|
|
||||||
import { optimize } from 'svgo'
|
|
||||||
|
|
||||||
const { uri, name } = defineProps<{ uri: string; name: string }>()
|
|
||||||
|
|
||||||
const qrCodeUrl = `https://hereconnect.condev.ru/read/${uri}`
|
|
||||||
const sanitizedFileName = computed(() => {
|
|
||||||
const sanitized = name.replace(/[^a-zA-Z0-9-_]/g, '').trim()
|
|
||||||
return sanitized ? `${sanitized}_QR-Code_HereConnect` : 'QR-Code_HereConnect'
|
|
||||||
})
|
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const svgUrl = ref<string | null>(null)
|
|
||||||
|
|
||||||
// Инициализация QRCodeStyling
|
|
||||||
const qrCode = new QRCodeStyling({
|
|
||||||
width: 256 * 4,
|
|
||||||
height: 256 * 4,
|
|
||||||
data: qrCodeUrl,
|
|
||||||
type: 'svg',
|
|
||||||
backgroundOptions: {
|
|
||||||
color: '#ffffff',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Генерация и оптимизация SVG
|
|
||||||
async function generateOptimizedSvg() {
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
svgUrl.value = URL.createObjectURL(new Blob([optimizedSvg.data], { type: 'image/svg+xml' }))
|
|
||||||
} else {
|
|
||||||
error.value = 'Ошибка оптимизации SVG'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error.value = 'Ошибка генерации QR-кода'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
error.value = (err as Error).message || 'Неизвестная ошибка'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Скачивание PNG
|
|
||||||
async function downloadPng() {
|
|
||||||
try {
|
|
||||||
await qrCode.download({
|
|
||||||
name: sanitizedFileName.value,
|
|
||||||
extension: 'png',
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
error.value = (err as Error).message || 'Ошибка при скачивании QR-кода'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Монтирование
|
|
||||||
onMounted(() => {
|
|
||||||
generateOptimizedSvg()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Очистка ресурсов при размонтировании
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (svgUrl.value) {
|
|
||||||
URL.revokeObjectURL(svgUrl.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
<form @submit.prevent="saveQRCode">
|
<form @submit.prevent="saveQRCode">
|
||||||
<label for="name" class="block text-gray-700 font-semibold mb-2">Название</label>
|
<label for="name" class="block text-gray-700 font-semibold mb-2">Название</label>
|
||||||
<InputText id="name" v-model="nameModel" class="w-full" placeholder="Введите название" />
|
<InputText id="name" v-model="name" class="w-full" placeholder="Введите название" />
|
||||||
<p v-if="!nameModel" class="text-red-500 text-sm mt-1">Название обязательно.</p>
|
<p v-if="!name" class="text-red-500 text-sm mt-1">Название обязательно.</p>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Создать QR-код"
|
label="Создать QR-код"
|
||||||
class="p-button-md mt-4 mb-4"
|
class="p-button-md mt-4 mb-4"
|
||||||
:disabled="!nameModel"
|
:disabled="!name"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<p v-if="error" class="text-red-500 mt-4">Ошибка создания QR-кода: {{ error }}</p>
|
<p v-if="error" class="text-red-500 mt-4">Ошибка создания QR-кода: {{ error }}</p>
|
||||||
@@ -21,8 +21,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { qrCodeDb } from '@/dbs'
|
import { useRouter } from 'vue-router'
|
||||||
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||||
|
import { createQrCodeDocument } from '@/api/qrCode'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const { placement, actions, deliveryMethod, user_uuid, predefinedMessage } = defineProps<{
|
const { placement, actions, deliveryMethod, user_uuid, predefinedMessage } = defineProps<{
|
||||||
placement: QRCodeDocument['placement']
|
placement: QRCodeDocument['placement']
|
||||||
@@ -32,37 +35,23 @@ const { placement, actions, deliveryMethod, user_uuid, predefinedMessage } = def
|
|||||||
predefinedMessage: QRCodeDocument['predefinedMessage'] | undefined
|
predefinedMessage: QRCodeDocument['predefinedMessage'] | undefined
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const uriModel = defineModel<QRCodeDocument['uri']>('uri', { required: true })
|
const name = ref<QRCodeDocument['name']>('')
|
||||||
const nameModel = defineModel<QRCodeDocument['name']>('name', { required: true })
|
|
||||||
const emit = defineEmits(['nextStep'])
|
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
async function saveQRCode() {
|
async function saveQRCode() {
|
||||||
try {
|
try {
|
||||||
// Генерация уникального uri
|
const qrCodeDocument = await createQrCodeDocument({
|
||||||
const EPOHE = 1731661630981
|
name: name.value,
|
||||||
const uri = Math.abs(Date.now() - EPOHE).toString(36)
|
|
||||||
|
|
||||||
const qrCodeDoc: QRCodeDocument = {
|
|
||||||
_id: `qr_code:${uri}`,
|
|
||||||
type: 'qr_code',
|
|
||||||
uri,
|
|
||||||
url: `https://hereconnect.condev.ru/read/${uri}`,
|
|
||||||
name: nameModel.value,
|
|
||||||
deliveryMethod,
|
deliveryMethod,
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
placement,
|
placement,
|
||||||
actions,
|
actions,
|
||||||
user_uuid,
|
user_uuid,
|
||||||
predefinedMessage,
|
predefinedMessage,
|
||||||
}
|
})
|
||||||
|
|
||||||
await qrCodeDb.put(qrCodeDoc)
|
|
||||||
|
|
||||||
uriModel.value = uri
|
|
||||||
error.value = null
|
error.value = null
|
||||||
emit('nextStep')
|
// Перенаправление на страницу /manage/:qr/ready
|
||||||
|
await router.push({ name: 'ManageQRCodeReady', params: { qr_code_uri: qrCodeDocument.uri } })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = (err as Error).message || 'Неизвестная ошибка'
|
error.value = (err as Error).message || 'Неизвестная ошибка'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import PouchDB from 'pouchdb-browser'
|
|
||||||
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
|
||||||
|
|
||||||
export const qrCodeDb = new PouchDB<QRCodeDocument>('https://hereconnect.condev.ru/api/qr', {
|
|
||||||
skip_setup: true,
|
|
||||||
})
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="create-layout-container max-w-lg mx-auto py-6 px-4 md:px-6">
|
|
||||||
<!-- Список шагов -->
|
|
||||||
<!-- <nav class="steps mb-4">-->
|
|
||||||
<!-- <ul class="steps-list">-->
|
|
||||||
<!-- <li :class="{ active: $route.name === 'create' }">-->
|
|
||||||
<!-- <router-link to="/create">Шаг 1: Место размещения</router-link>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- <li :class="{ active: $route.name === 'create-actions' }">-->
|
|
||||||
<!-- <router-link to="/create/actions">Шаг 2: Доступные действия</router-link>-->
|
|
||||||
<!-- </li>-->
|
|
||||||
<!-- </ul>-->
|
|
||||||
<!-- </nav>-->
|
|
||||||
|
|
||||||
<!-- Динамическое отображение компонента в зависимости от маршрута -->
|
|
||||||
<section class="content">
|
|
||||||
<RouterView />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.create-layout-container {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
color: #555;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list li {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list li.active {
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+8
-13
@@ -10,20 +10,15 @@ const routes = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
path: '/create',
|
path: '/create',
|
||||||
component: () => import('@/layouts/CreateLayout.vue'),
|
component: () => import('@/views/CreateView.vue'),
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'create',
|
|
||||||
component: () => import('@/views/CreateView.vue'), // Первый шаг создания QR-кода
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':placement/actions',
|
|
||||||
name: 'create-actions',
|
|
||||||
component: () => import('@/views/CreateActionsView.vue'), // Второй шаг создания QR-кода
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/manage/:qr_code_uri/ready',
|
||||||
|
name: 'ManageQRCodeReady',
|
||||||
|
component: () => import('@/views/ManageQRCodeReadyView.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// path: '/create/actions',
|
// path: '/create/actions',
|
||||||
// name: 'create-actions',
|
// name: 'create-actions',
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface BaseDocument {
|
|||||||
/** Уникальный идентификатор документа */
|
/** Уникальный идентификатор документа */
|
||||||
_id: string
|
_id: string
|
||||||
|
|
||||||
|
_rev?: string
|
||||||
|
|
||||||
/** Тип документа */
|
/** Тип документа */
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<template>
|
|
||||||
<SelectActions />
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
+50
-56
@@ -1,63 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="create-page max-w-lg mx-auto py-8">
|
<div class="max-w-lg mx-auto py-6 px-4 md:px-6">
|
||||||
<!-- Секция выбора размещения -->
|
<section class="content">
|
||||||
<CreateSection isActive>
|
<div class="max-w-lg mx-auto py-8">
|
||||||
<CreatePlacementSection
|
<!-- Секция выбора размещения -->
|
||||||
v-model="placement"
|
<CreateSection isActive>
|
||||||
@nextStep="placement && activateSections('actions')"
|
<CreatePlacementSection
|
||||||
/>
|
v-model="placement"
|
||||||
</CreateSection>
|
@nextStep="placement && activateSections('actions')"
|
||||||
|
/>
|
||||||
|
</CreateSection>
|
||||||
|
|
||||||
<!-- Секция выбора доступных действий -->
|
<!-- Секция выбора доступных действий -->
|
||||||
<CreateSection :isActive="activeSteps.includes('actions')">
|
<CreateSection :isActive="activeSteps.includes('actions')">
|
||||||
<CreateActionsSection
|
<CreateActionsSection
|
||||||
v-model="selectedActions"
|
v-model="selectedActions"
|
||||||
@nextStep="
|
@nextStep="
|
||||||
selectedActions.length &&
|
selectedActions.length &&
|
||||||
activateSections(selectedActions.includes('sendMessage') ? 'delivery' : 'summary')
|
activateSections(selectedActions.includes('sendMessage') ? 'delivery' : 'summary')
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</CreateSection>
|
</CreateSection>
|
||||||
|
|
||||||
<!-- Секция выбора способа доставки -->
|
<!-- Секция выбора способа доставки -->
|
||||||
<CreateSection
|
<CreateSection
|
||||||
v-if="selectedActions.includes('sendMessage')"
|
v-if="selectedActions.includes('sendMessage')"
|
||||||
:isActive="activeSteps.includes('delivery')"
|
:isActive="activeSteps.includes('delivery')"
|
||||||
>
|
>
|
||||||
<CreateDeliverySection
|
<CreateDeliverySection
|
||||||
v-model="deliveryMethod"
|
v-model="deliveryMethod"
|
||||||
@nextStep="deliveryMethod && activateSections('messages', 'summary')"
|
@nextStep="deliveryMethod && activateSections('messages', 'summary')"
|
||||||
/>
|
/>
|
||||||
</CreateSection>
|
</CreateSection>
|
||||||
|
|
||||||
<!-- Секция сообщений для отправки -->
|
<!-- Секция сообщений для отправки -->
|
||||||
<CreateSection
|
<CreateSection
|
||||||
v-if="selectedActions.includes('sendMessage') && activeSteps.includes('messages')"
|
v-if="selectedActions.includes('sendMessage') && activeSteps.includes('messages')"
|
||||||
:isActive="true"
|
:isActive="true"
|
||||||
>
|
>
|
||||||
<CreateMessagesSection :placement v-model="messages" />
|
<CreateMessagesSection :placement v-model="messages" />
|
||||||
</CreateSection>
|
</CreateSection>
|
||||||
|
|
||||||
<!-- Секция итогового резюме -->
|
<!-- Секция итогового резюме -->
|
||||||
<CreateSection :isActive="activeSteps.includes('summary')">
|
<CreateSection :isActive="activeSteps.includes('summary')">
|
||||||
<CreateSummarySection
|
<CreateSummarySection
|
||||||
v-model:uri="uri"
|
:placement="placement"
|
||||||
v-model:name="name"
|
:actions="selectedActions"
|
||||||
:placement="placement"
|
:deliveryMethod="deliveryMethod"
|
||||||
:actions="selectedActions"
|
:user_uuid="user_uuid"
|
||||||
:deliveryMethod="deliveryMethod"
|
:predefinedMessage="selectedActions.includes('sendMessage') ? messages : undefined"
|
||||||
:user_uuid="user_uuid"
|
/>
|
||||||
:predefinedMessage="selectedActions.includes('sendMessage') ? messages : undefined"
|
</CreateSection>
|
||||||
@nextStep="activateSections('qrReady')"
|
|
||||||
/>
|
|
||||||
</CreateSection>
|
|
||||||
|
|
||||||
<!-- Секция готового QR-кода -->
|
<div class="h-[30dvh] md:h-[10dvh] overflow-hidden"></div>
|
||||||
<CreateSection :isActive="activeSteps.includes('qrReady')">
|
</div>
|
||||||
<CreateReadySection :uri="uri" :name="name" />
|
</section>
|
||||||
</CreateSection>
|
|
||||||
|
|
||||||
<div class="h-[30dvh] md:h-[10dvh] overflow-hidden"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -71,8 +67,6 @@ const selectedActions = ref<string[]>([])
|
|||||||
const placement = ref('')
|
const placement = ref('')
|
||||||
const deliveryMethod = ref('')
|
const deliveryMethod = ref('')
|
||||||
const messages = ref<string[]>([])
|
const messages = ref<string[]>([])
|
||||||
const name = ref('')
|
|
||||||
const uri = ref('')
|
|
||||||
const user_uuid = 'example-user-uuid' // Временно захардкожено, заменить на динамическое
|
const user_uuid = 'example-user-uuid' // Временно захардкожено, заменить на динамическое
|
||||||
|
|
||||||
// Функция активации следующих шагов
|
// Функция активации следующих шагов
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div class="max-w-lg mx-auto py-6 px-4 md:px-6">
|
||||||
|
<section class="content text-center">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Ваш QR-код готов</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Сохраните его и ознакомьтесь с инструкциями по размещению.</p>
|
||||||
|
|
||||||
|
<!-- Рендер QR-кода -->
|
||||||
|
<div v-if="!loading && svgUrl" class="border p-4 bg-white rounded-lg mb-6">
|
||||||
|
<img :src="svgUrl" alt="QR-код" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сообщение об ошибке -->
|
||||||
|
<p v-if="error" class="text-red-500 mt-4">{{ error }}</p>
|
||||||
|
<p v-if="loading" class="text-gray-500 mt-4">Загрузка...</p>
|
||||||
|
|
||||||
|
<!-- Кнопки для скачивания -->
|
||||||
|
<div v-if="!loading && !error" class="flex flex-row gap-4 justify-center">
|
||||||
|
<Button
|
||||||
|
v-if="svgUrl"
|
||||||
|
label="Скачать SVG"
|
||||||
|
icon="pi pi-download"
|
||||||
|
class="p-button-sm"
|
||||||
|
:href="svgUrl"
|
||||||
|
:download="`${sanitizedFileName}.svg`"
|
||||||
|
as="a"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Скачать PNG"
|
||||||
|
icon="pi pi-download"
|
||||||
|
class="p-button-sm p-button-outlined"
|
||||||
|
@click="downloadPng"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import QRCodeStyling from 'qr-code-styling'
|
||||||
|
import { getQrCodeDocument } from '@/api/qrCode'
|
||||||
|
import { optimize } from 'svgo'
|
||||||
|
|
||||||
|
// Получаем параметры маршрута
|
||||||
|
const route = useRoute()
|
||||||
|
const { qr_code_uri } = route.params as { qr_code_uri: string }
|
||||||
|
|
||||||
|
// Локальные состояния
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const qrCodeData = ref<null | { name: string; url: string }>(null)
|
||||||
|
const svgUrl = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Получение имени файла
|
||||||
|
const sanitizedFileName = computed(() => {
|
||||||
|
const sanitized = qrCodeData.value?.name.replace(/[^a-zA-Z0-9-_]/g, '').trim()
|
||||||
|
return sanitized ? `${sanitized}_QR-Code_HereConnect` : 'QR-Code_HereConnect'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Инициализация QRCodeStyling
|
||||||
|
const qrCode = new QRCodeStyling({
|
||||||
|
width: 256 * 4,
|
||||||
|
height: 256 * 4,
|
||||||
|
data: '',
|
||||||
|
type: 'svg',
|
||||||
|
backgroundOptions: {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Генерация и оптимизация SVG
|
||||||
|
async function generateOptimizedSvg(url: string) {
|
||||||
|
try {
|
||||||
|
qrCode.update({ data: url })
|
||||||
|
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) {
|
||||||
|
svgUrl.value = URL.createObjectURL(new Blob([optimizedSvg.data], { type: 'image/svg+xml' }))
|
||||||
|
} else {
|
||||||
|
throw new Error('Ошибка оптимизации SVG')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Ошибка генерации QR-кода')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = (err as Error).message || 'Неизвестная ошибка'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скачивание PNG
|
||||||
|
async function downloadPng() {
|
||||||
|
try {
|
||||||
|
await qrCode.download({
|
||||||
|
name: sanitizedFileName.value,
|
||||||
|
extension: 'png',
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
error.value = (err as Error).message || 'Ошибка при скачивании QR-кода'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Монтирование
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// Загружаем данные QR-кода
|
||||||
|
const doc = await getQrCodeDocument(qr_code_uri)
|
||||||
|
qrCodeData.value = { name: doc.name, url: doc.url }
|
||||||
|
await generateOptimizedSvg(doc.url)
|
||||||
|
} catch (err) {
|
||||||
|
error.value = (err as Error).message || 'Ошибка загрузки данных QR-кода'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Очистка ресурсов при размонтировании
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (svgUrl.value) {
|
||||||
|
URL.revokeObjectURL(svgUrl.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user