Добавлены новые настройки сервера и улучшена логика отображения 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
+25 -17
View File
@@ -9,7 +9,8 @@ const {
textInput,
error,
errorColor,
hasQr,
qrReady,
showModal,
qrCanvasRef,
qrFrameRef,
generateQR,
@@ -26,12 +27,12 @@ const statusClass = computed(() =>
)
function onKeydown(event) {
if (event.key === 'Escape' && hasQr.value) {
if (event.key === 'Escape' && showModal.value) {
closeQrModal()
}
}
watch(hasQr, (open) => {
watch(showModal, (open) => {
document.body.style.overflow = open ? 'hidden' : ''
})
@@ -84,12 +85,19 @@ onUnmounted(() => {
<div class="btns btns1">
<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>
</div>
<div
v-if="!hasQr"
v-if="!showModal"
class="status-slot"
:class="{ 'status-slot--visible': error }"
>
@@ -98,23 +106,23 @@ onUnmounted(() => {
</Transition>
</div>
</div>
</main>
<a
href="https://github.com/IliaMiheev/iQR"
target="_blank"
rel="noopener noreferrer"
class="site-banner"
title="Репозиторий на GitHub"
>
<span class="site-banner__brand">iQR</span>
<span class="site-banner__slogan"> qr-код без излишеств</span>
</a>
<a
href="https://github.com/IliaMiheev/iQR"
target="_blank"
rel="noopener noreferrer"
class="site-banner"
title="Репозиторий на GitHub"
>
<span class="site-banner__brand">iQR</span>
<span class="site-banner__slogan"> qr-код без излишеств</span>
</a>
</main>
<Teleport to="body">
<Transition name="qr-modal">
<div
v-if="hasQr"
v-if="showModal"
class="qr-modal"
role="dialog"
aria-modal="true"
+175 -45
View File
@@ -72,10 +72,13 @@ body {
.app-shell {
max-width: 560px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.app-card {
width: 100%;
box-sizing: border-box;
padding: 28px 24px 32px;
border-radius: 20px;
background: var(--surface);
@@ -273,41 +276,6 @@ textarea::selection {
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 {
padding: 11px 16px;
margin: 0;
@@ -343,6 +311,19 @@ button {
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,
.btns2 button {
background: var(--surface-solid);
@@ -674,12 +655,15 @@ canvas {
transform: translateX(-50%);
display: inline-flex;
align-items: center;
gap: 6px;
justify-content: center;
flex-wrap: wrap;
gap: 4px 6px;
padding: 10px 22px;
border-radius: 999px;
text-decoration: none;
font-size: 15px;
line-height: 1.2;
line-height: 1.35;
text-align: center;
white-space: nowrap;
z-index: 10;
backdrop-filter: blur(12px);
@@ -691,6 +675,11 @@ canvas {
box-shadow 0.35s ease;
}
.site-banner__brand,
.site-banner__slogan {
display: inline;
}
html.dark .site-banner {
background-color: rgba(30, 30, 30, 0.72);
color: #f5f5f5;
@@ -723,14 +712,106 @@ html.light .site-banner:hover {
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 {
padding: 16px;
padding-bottom: 80px;
align-items: flex-start;
}
.qr-modal__dialog {
padding: 16px;
border-radius: 20px;
margin: 0 auto;
max-width: calc(100vw - 32px);
max-height: calc(100dvh - 120px);
}
.qr-modal__close {
@@ -738,14 +819,63 @@ html.light .site-banner:hover {
right: -10px;
}
.qr-modal__toolbar {
right: 12px;
.qr-modal__viewport {
max-height: calc(100dvh - 200px);
}
.site-banner {
max-width: calc(100% - 32px);
white-space: normal;
text-align: center;
font-size: 14px;
.qr-modal__toolbar {
right: auto;
left: 50%;
top: auto;
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 errorColor = ref('red')
const scale = ref(1)
const hasQr = ref(false)
const qrReady = ref(false)
const showModal = ref(false)
const qrDataUrl = ref('')
const qrCanvasRef = ref(null)
const qrFrameRef = ref(null)
@@ -39,6 +41,7 @@ export function useQrCode() {
const width = Math.round(BASE_QR_SIZE * scale.value)
await QRCode.toCanvas(canvas, userText, { width })
qrDataUrl.value = canvas.toDataURL('image/png')
}
async function clampScaleIfNeeded() {
@@ -58,36 +61,38 @@ export function useQrCode() {
}
localStorage.setItem('userText', userText)
hasQr.value = true
showModal.value = true
await nextTick()
try {
await renderQrCanvas()
qrReady.value = true
logMessage('')
await clampScaleIfNeeded()
} catch (err) {
console.error(err)
logMessage('Ошибка при генерации QR-кода.')
hasQr.value = false
showModal.value = false
qrReady.value = false
qrDataUrl.value = ''
}
}
function downloadQR() {
const userText = textInput.value.slice(0, 20)
const canvas = qrCanvasRef.value
if (!canvas) {
logMessage('QR-код не сгенерирован!')
return
}
if (!userText) {
logMessage('Отсутствует текст в поле для ввода')
return
}
if (!qrReady.value || !qrDataUrl.value) {
logMessage('QR-код не сгенерирован!')
return
}
const link = document.createElement('a')
link.href = canvas.toDataURL('image/png')
link.href = qrDataUrl.value
link.download = `qr-code-${userText}.png`
link.click()
logMessage('QR-код скачан!', 'green')
@@ -95,7 +100,9 @@ export function useQrCode() {
function clearQR() {
textInput.value = ''
hasQr.value = false
showModal.value = false
qrReady.value = false
qrDataUrl.value = ''
error.value = ''
scale.value = 1
localStorage.removeItem('userText')
@@ -103,7 +110,7 @@ export function useQrCode() {
}
function closeQrModal() {
hasQr.value = false
showModal.value = false
}
async function zoomIn() {
@@ -162,7 +169,8 @@ export function useQrCode() {
textInput,
error,
errorColor,
hasQr,
qrReady,
showModal,
qrCanvasRef,
qrFrameRef,
generateQR,
+3
View File
@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
base: '/',
server: {
host: true,
},
})