Добавлены новые настройки сервера и улучшена логика отображения QR-кода. Обновлены стили для адаптивности и добавлены новые состояния для кнопок

This commit is contained in:
Ilia Miheev
2026-06-20 15:20:59 +03:00
parent 47fe6c966c
commit 709eaacadb
4 changed files with 223 additions and 74 deletions
+15 -7
View File
@@ -9,7 +9,8 @@ const {
textInput, textInput,
error, error,
errorColor, errorColor,
hasQr, qrReady,
showModal,
qrCanvasRef, qrCanvasRef,
qrFrameRef, qrFrameRef,
generateQR, generateQR,
@@ -26,12 +27,12 @@ const statusClass = computed(() =>
) )
function onKeydown(event) { function onKeydown(event) {
if (event.key === 'Escape' && hasQr.value) { if (event.key === 'Escape' && showModal.value) {
closeQrModal() closeQrModal()
} }
} }
watch(hasQr, (open) => { watch(showModal, (open) => {
document.body.style.overflow = open ? 'hidden' : '' document.body.style.overflow = open ? 'hidden' : ''
}) })
@@ -84,12 +85,19 @@ onUnmounted(() => {
<div class="btns btns1"> <div class="btns btns1">
<button class="btn" title="Создать QR-код" @click="generateQR">Создать</button> <button class="btn" title="Создать QR-код" @click="generateQR">Создать</button>
<button class="btn" title="Скачать QR-код" @click="downloadQR">Скачать</button> <button
class="btn"
title="Скачать QR-код"
:disabled="!qrReady"
@click="downloadQR"
>
Скачать
</button>
<button class="btn3" title="Очистить поля" @click="clearQR">Очистить</button> <button class="btn3" title="Очистить поля" @click="clearQR">Очистить</button>
</div> </div>
<div <div
v-if="!hasQr" v-if="!showModal"
class="status-slot" class="status-slot"
:class="{ 'status-slot--visible': error }" :class="{ 'status-slot--visible': error }"
> >
@@ -98,7 +106,6 @@ onUnmounted(() => {
</Transition> </Transition>
</div> </div>
</div> </div>
</main>
<a <a
href="https://github.com/IliaMiheev/iQR" href="https://github.com/IliaMiheev/iQR"
@@ -110,11 +117,12 @@ onUnmounted(() => {
<span class="site-banner__brand">iQR</span> <span class="site-banner__brand">iQR</span>
<span class="site-banner__slogan"> qr-код без излишеств</span> <span class="site-banner__slogan"> qr-код без излишеств</span>
</a> </a>
</main>
<Teleport to="body"> <Teleport to="body">
<Transition name="qr-modal"> <Transition name="qr-modal">
<div <div
v-if="hasQr" v-if="showModal"
class="qr-modal" class="qr-modal"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
+175 -45
View File
@@ -72,10 +72,13 @@ body {
.app-shell { .app-shell {
max-width: 560px; max-width: 560px;
margin: 0 auto; margin: 0 auto;
width: 100%;
box-sizing: border-box;
} }
.app-card { .app-card {
width: 100%; width: 100%;
box-sizing: border-box;
padding: 28px 24px 32px; padding: 28px 24px 32px;
border-radius: 20px; border-radius: 20px;
background: var(--surface); background: var(--surface);
@@ -273,41 +276,6 @@ textarea::selection {
font-weight: 600; font-weight: 600;
} }
@media (max-width: 500px) {
.app-card {
padding: 20px 16px 24px;
border-radius: 16px;
}
.inputs {
flex-direction: column;
align-items: center;
}
textarea {
max-width: 100%;
width: 100%;
box-sizing: border-box;
}
.themeSwitch label {
margin-top: 0;
}
.btns {
max-width: 100%;
}
.btns1 {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
}
.btn3 {
grid-column: 1 / -1;
}
}
button { button {
padding: 11px 16px; padding: 11px 16px;
margin: 0; margin: 0;
@@ -343,6 +311,19 @@ button {
background-color: rgb(200, 55, 0); background-color: rgb(200, 55, 0);
} }
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
.btn:disabled:hover {
background-color: var(--accent);
transform: none;
box-shadow: none;
}
.btn3, .btn3,
.btns2 button { .btns2 button {
background: var(--surface-solid); background: var(--surface-solid);
@@ -674,12 +655,15 @@ canvas {
transform: translateX(-50%); transform: translateX(-50%);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; justify-content: center;
flex-wrap: wrap;
gap: 4px 6px;
padding: 10px 22px; padding: 10px 22px;
border-radius: 999px; border-radius: 999px;
text-decoration: none; text-decoration: none;
font-size: 15px; font-size: 15px;
line-height: 1.2; line-height: 1.35;
text-align: center;
white-space: nowrap; white-space: nowrap;
z-index: 10; z-index: 10;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
@@ -691,6 +675,11 @@ canvas {
box-shadow 0.35s ease; box-shadow 0.35s ease;
} }
.site-banner__brand,
.site-banner__slogan {
display: inline;
}
html.dark .site-banner { html.dark .site-banner {
background-color: rgba(30, 30, 30, 0.72); background-color: rgba(30, 30, 30, 0.72);
color: #f5f5f5; color: #f5f5f5;
@@ -723,14 +712,106 @@ html.light .site-banner:hover {
font-weight: 400; font-weight: 400;
} }
@media (max-width: 500px) { /* Портрет <425px и альбомная ориентация на телефонах */
@media (max-width: 424px),
(orientation: landscape) and (max-height: 520px) {
body {
padding: 16px;
display: flex;
flex-direction: column;
min-height: 100dvh;
overflow-x: hidden;
}
#app {
flex: 1;
display: flex;
flex-direction: column;
}
.app-shell {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
min-height: calc(100dvh - 32px);
}
.app-card {
flex-shrink: 0;
}
.site-banner {
position: static;
left: auto;
bottom: auto;
transform: none;
margin-top: auto;
padding: 10px 18px;
align-self: center;
max-width: 100%;
white-space: normal;
font-size: 14px;
flex-shrink: 0;
}
}
@media (max-width: 424px) {
.app-card {
padding: 20px 16px 24px;
border-radius: 16px;
}
.inputs {
flex-direction: column;
align-items: stretch;
width: 100%;
}
textarea {
max-width: 100%;
width: 100%;
box-sizing: border-box;
}
.themeSwitch {
align-self: center;
}
.themeSwitch label {
margin-top: 0;
}
.btns {
max-width: 100%;
}
.btns1 {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
}
.btn3 {
grid-column: 1 / -1;
}
h1 {
font-size: 1.35rem;
}
.qr-modal { .qr-modal {
padding: 16px; padding: 16px;
padding-bottom: 80px;
align-items: flex-start;
} }
.qr-modal__dialog { .qr-modal__dialog {
padding: 16px; padding: 16px;
border-radius: 20px; border-radius: 20px;
margin: 0 auto;
max-width: calc(100vw - 32px);
max-height: calc(100dvh - 120px);
} }
.qr-modal__close { .qr-modal__close {
@@ -738,14 +819,63 @@ html.light .site-banner:hover {
right: -10px; right: -10px;
} }
.qr-modal__toolbar { .qr-modal__viewport {
right: 12px; max-height: calc(100dvh - 200px);
} }
.site-banner { .qr-modal__toolbar {
max-width: calc(100% - 32px); right: auto;
white-space: normal; left: 50%;
text-align: center; top: auto;
font-size: 14px; bottom: max(16px, env(safe-area-inset-bottom, 16px));
transform: translateX(-50%);
flex-direction: row;
padding: 8px 12px;
gap: 8px;
border-radius: 999px;
}
.qr-modal-enter-from .qr-modal__toolbar,
.qr-modal-leave-to .qr-modal__toolbar {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
}
@media (max-width: 424px),
(orientation: landscape) and (max-height: 520px) {
.qr-modal {
padding: 12px;
padding-bottom: 72px;
align-items: flex-start;
}
.qr-modal__dialog {
padding: 14px;
margin: 0 auto;
max-width: calc(100vw - 24px);
max-height: calc(100dvh - 96px);
}
.qr-modal__viewport {
max-height: calc(100dvh - 160px);
}
.qr-modal__toolbar {
right: auto;
left: 50%;
top: auto;
bottom: max(12px, env(safe-area-inset-bottom, 12px));
transform: translateX(-50%);
flex-direction: row;
padding: 8px 12px;
gap: 8px;
border-radius: 999px;
}
.qr-modal-enter-from .qr-modal__toolbar,
.qr-modal-leave-to .qr-modal__toolbar {
opacity: 0;
transform: translateX(-50%) translateY(20px);
} }
} }
+20 -12
View File
@@ -11,7 +11,9 @@ export function useQrCode() {
const error = ref('') const error = ref('')
const errorColor = ref('red') const errorColor = ref('red')
const scale = ref(1) const scale = ref(1)
const hasQr = ref(false) const qrReady = ref(false)
const showModal = ref(false)
const qrDataUrl = ref('')
const qrCanvasRef = ref(null) const qrCanvasRef = ref(null)
const qrFrameRef = ref(null) const qrFrameRef = ref(null)
@@ -39,6 +41,7 @@ export function useQrCode() {
const width = Math.round(BASE_QR_SIZE * scale.value) const width = Math.round(BASE_QR_SIZE * scale.value)
await QRCode.toCanvas(canvas, userText, { width }) await QRCode.toCanvas(canvas, userText, { width })
qrDataUrl.value = canvas.toDataURL('image/png')
} }
async function clampScaleIfNeeded() { async function clampScaleIfNeeded() {
@@ -58,36 +61,38 @@ export function useQrCode() {
} }
localStorage.setItem('userText', userText) localStorage.setItem('userText', userText)
hasQr.value = true showModal.value = true
await nextTick() await nextTick()
try { try {
await renderQrCanvas() await renderQrCanvas()
qrReady.value = true
logMessage('') logMessage('')
await clampScaleIfNeeded() await clampScaleIfNeeded()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
logMessage('Ошибка при генерации QR-кода.') logMessage('Ошибка при генерации QR-кода.')
hasQr.value = false showModal.value = false
qrReady.value = false
qrDataUrl.value = ''
} }
} }
function downloadQR() { function downloadQR() {
const userText = textInput.value.slice(0, 20) const userText = textInput.value.slice(0, 20)
const canvas = qrCanvasRef.value
if (!canvas) {
logMessage('QR-код не сгенерирован!')
return
}
if (!userText) { if (!userText) {
logMessage('Отсутствует текст в поле для ввода') logMessage('Отсутствует текст в поле для ввода')
return return
} }
if (!qrReady.value || !qrDataUrl.value) {
logMessage('QR-код не сгенерирован!')
return
}
const link = document.createElement('a') const link = document.createElement('a')
link.href = canvas.toDataURL('image/png') link.href = qrDataUrl.value
link.download = `qr-code-${userText}.png` link.download = `qr-code-${userText}.png`
link.click() link.click()
logMessage('QR-код скачан!', 'green') logMessage('QR-код скачан!', 'green')
@@ -95,7 +100,9 @@ export function useQrCode() {
function clearQR() { function clearQR() {
textInput.value = '' textInput.value = ''
hasQr.value = false showModal.value = false
qrReady.value = false
qrDataUrl.value = ''
error.value = '' error.value = ''
scale.value = 1 scale.value = 1
localStorage.removeItem('userText') localStorage.removeItem('userText')
@@ -103,7 +110,7 @@ export function useQrCode() {
} }
function closeQrModal() { function closeQrModal() {
hasQr.value = false showModal.value = false
} }
async function zoomIn() { async function zoomIn() {
@@ -162,7 +169,8 @@ export function useQrCode() {
textInput, textInput,
error, error,
errorColor, errorColor,
hasQr, qrReady,
showModal,
qrCanvasRef, qrCanvasRef,
qrFrameRef, qrFrameRef,
generateQR, generateQR,
+3
View File
@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
base: '/', base: '/',
server: {
host: true,
},
}) })