отдельной страницей вывел готовый 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">
|
||||
<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 || 'Неизвестная ошибка'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
+7
-12
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<template>
|
||||
<SelectActions />
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
@@ -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' // Временно захардкожено, заменить на динамическое
|
||||
|
||||
// Функция активации следующих шагов
|
||||
|
||||
@@ -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