отдельной страницей вывел готовый qr-код /manage/:qr_code_uri/view
Main daploy / deploy (push) Successful in 38s

This commit is contained in:
2024-11-18 23:28:33 +02:00
parent 6b87a2ca87
commit fe4317982f
11 changed files with 264 additions and 272 deletions
+8
View File
@@ -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')
+47
View File
@@ -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>
+13 -24
View File
@@ -5,14 +5,14 @@
<form @submit.prevent="saveQRCode">
<label for="name" class="block text-gray-700 font-semibold mb-2">Название</label>
<InputText id="name" v-model="nameModel" class="w-full" placeholder="Введите название" />
<p v-if="!nameModel" class="text-red-500 text-sm mt-1">Название обязательно.</p>
<InputText id="name" v-model="name" class="w-full" placeholder="Введите название" />
<p v-if="!name" class="text-red-500 text-sm mt-1">Название обязательно.</p>
<Button
type="submit"
label="Создать QR-код"
class="p-button-md mt-4 mb-4"
:disabled="!nameModel"
:disabled="!name"
/>
</form>
<p v-if="error" class="text-red-500 mt-4">Ошибка создания QR-кода: {{ error }}</p>
@@ -21,8 +21,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import { qrCodeDb } from '@/dbs'
import { useRouter } from 'vue-router'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { createQrCodeDocument } from '@/api/qrCode'
const router = useRouter()
const { placement, actions, deliveryMethod, user_uuid, predefinedMessage } = defineProps<{
placement: QRCodeDocument['placement']
@@ -32,37 +35,23 @@ const { placement, actions, deliveryMethod, user_uuid, predefinedMessage } = def
predefinedMessage: QRCodeDocument['predefinedMessage'] | undefined
}>()
const uriModel = defineModel<QRCodeDocument['uri']>('uri', { required: true })
const nameModel = defineModel<QRCodeDocument['name']>('name', { required: true })
const emit = defineEmits(['nextStep'])
const name = ref<QRCodeDocument['name']>('')
const error = ref<string | null>(null)
async function saveQRCode() {
try {
// Генерация уникального uri
const EPOHE = 1731661630981
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,
const qrCodeDocument = await createQrCodeDocument({
name: name.value,
deliveryMethod,
created_at: new Date().toISOString(),
placement,
actions,
user_uuid,
predefinedMessage,
}
})
await qrCodeDb.put(qrCodeDoc)
uriModel.value = uri
error.value = null
emit('nextStep')
// Перенаправление на страницу /manage/:qr/ready
await router.push({ name: 'ManageQRCodeReady', params: { qr_code_uri: qrCodeDocument.uri } })
} catch (err) {
error.value = (err as Error).message || 'Неизвестная ошибка'
}
-6
View File
@@ -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,
})
-51
View File
@@ -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>
+7 -12
View File
@@ -10,20 +10,15 @@ const routes = [
{
path: '/create',
component: () => import('@/layouts/CreateLayout.vue'),
children: [
component: () => import('@/views/CreateView.vue'),
},
{
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',
// name: 'create-actions',
@@ -6,6 +6,8 @@ export interface BaseDocument {
/** Уникальный идентификатор документа */
_id: string
_rev?: string
/** Тип документа */
type: string
-4
View File
@@ -1,4 +0,0 @@
<template>
<SelectActions />
</template>
<script setup lang="ts"></script>
+5 -11
View File
@@ -1,5 +1,7 @@
<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">
<div class="max-w-lg mx-auto py-8">
<!-- Секция выбора размещения -->
<CreateSection isActive>
<CreatePlacementSection
@@ -41,24 +43,18 @@
<!-- Секция итогового резюме -->
<CreateSection :isActive="activeSteps.includes('summary')">
<CreateSummarySection
v-model:uri="uri"
v-model:name="name"
:placement="placement"
:actions="selectedActions"
:deliveryMethod="deliveryMethod"
:user_uuid="user_uuid"
:predefinedMessage="selectedActions.includes('sendMessage') ? messages : undefined"
@nextStep="activateSections('qrReady')"
/>
</CreateSection>
<!-- Секция готового QR-кода -->
<CreateSection :isActive="activeSteps.includes('qrReady')">
<CreateReadySection :uri="uri" :name="name" />
</CreateSection>
<div class="h-[30dvh] md:h-[10dvh] overflow-hidden"></div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
@@ -71,8 +67,6 @@ const selectedActions = ref<string[]>([])
const placement = ref('')
const deliveryMethod = ref('')
const messages = ref<string[]>([])
const name = ref('')
const uri = ref('')
const user_uuid = 'example-user-uuid' // Временно захардкожено, заменить на динамическое
// Функция активации следующих шагов
+136
View File
@@ -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>