Добавил докер и касторизацию через env
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Заголовок на главной карточке
|
||||
VITE_APP_TITLE=Генератор QR-кодов
|
||||
|
||||
# Плашка внизу: бренд, слоган и ссылка
|
||||
VITE_BANNER_BRAND=iQR
|
||||
VITE_BANNER_SLOGAN=— qr-код без излишеств
|
||||
VITE_BANNER_URL=https://github.com/IliaMiheev/iQR
|
||||
|
||||
# Акцентный цвет (hex, rgb или css-имя)
|
||||
VITE_ACCENT_COLOR=#ff4500
|
||||
@@ -0,0 +1,2 @@
|
||||
*.sh text eol=lf
|
||||
docker/entrypoint.sh text eol=lf
|
||||
@@ -2,5 +2,6 @@ node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.local
|
||||
.env
|
||||
__pycache__
|
||||
ideas.md
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Вводишь текст и генерируешь. Всё просто.
|
||||
|
||||
Проект переписан на **Vue 3 + Vite**.
|
||||
Проект на **Vue 3 + Vite**. Подходит для деплоя на поддомен (например `qr.example.com`) с кастомизацией брендинга.
|
||||
|
||||
### Запуск локально
|
||||
|
||||
@@ -11,26 +11,66 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Приложение откроется по адресу `http://localhost:5173/iQR/`.
|
||||
Приложение откроется по адресу `http://localhost:5173/`.
|
||||
|
||||
### Сборка для GitHub Pages
|
||||
Файл `.env` **не обязателен** — без него работают значения по умолчанию. Нужен только если хотите кастомизировать интерфейс при локальной разработке (`npm run dev` / `npm run build`).
|
||||
|
||||
### Настройка через `.env` (только локально)
|
||||
|
||||
Скопируйте `.env.example` в `.env` и измените значения:
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|---|---|---|
|
||||
| `VITE_APP_TITLE` | Заголовок | `Генератор QR-кодов` |
|
||||
| `VITE_BANNER_BRAND` | Бренд в плашке | `iQR` |
|
||||
| `VITE_BANNER_SLOGAN` | Слоган в плашке | `— qr-код без излишеств` |
|
||||
| `VITE_BANNER_URL` | Ссылка плашки | репозиторий iQR |
|
||||
| `VITE_ACCENT_COLOR` | Акцентный цвет | `#ff4500` |
|
||||
|
||||
После изменения `.env` перезапустите `npm run dev`.
|
||||
|
||||
### Docker
|
||||
|
||||
Для Docker **`.env` не нужен** — настройки передаются переменными окружения в `docker-compose.yml`.
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Сайт будет на `http://localhost:8080`.
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
APP_TITLE: "QR на моём сайте"
|
||||
BANNER_BRAND: "MySite"
|
||||
BANNER_SLOGAN: "— быстрые qr-коды"
|
||||
BANNER_URL: "https://example.com"
|
||||
ACCENT_COLOR: "#7c3aed"
|
||||
```
|
||||
|
||||
При старте контейнера генерируется `config.json`, поэтому пересборка образа для смены текстов не нужна.
|
||||
|
||||
### Сборка
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Содержимое папки `dist/` нужно задеплоить на GitHub Pages (корень сайта — `dist`, base path уже настроен как `/iQR/`).
|
||||
Результат в папке `dist/`.
|
||||
|
||||
### Структура
|
||||
|
||||
```
|
||||
src/
|
||||
App.vue — главный компонент
|
||||
App.vue
|
||||
config/ — загрузка настроек
|
||||
composables/
|
||||
useTheme.js — тёмная/светлая тема
|
||||
useQrCode.js — генерация, скачивание, масштаб
|
||||
assets/style.css — стили
|
||||
public/img/ — иконки, курсоры, favicon
|
||||
useTheme.js
|
||||
useQrCode.js
|
||||
useAppConfig.js
|
||||
assets/style.css
|
||||
public/img/
|
||||
docker/ — nginx и entrypoint
|
||||
```
|
||||
|
||||
<div style="text-align: center">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
iqr:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
APP_TITLE: "Генератор QR-кодов"
|
||||
BANNER_BRAND: "iQR"
|
||||
BANNER_SLOGAN: "— qr-код без излишеств"
|
||||
BANNER_URL: "https://github.com/IliaMiheev/iQR"
|
||||
ACCENT_COLOR: "#ff4500"
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
APP_TITLE="${APP_TITLE:-Генератор QR-кодов}"
|
||||
BANNER_BRAND="${BANNER_BRAND:-iQR}"
|
||||
BANNER_SLOGAN="${BANNER_SLOGAN:-— qr-код без излишеств}"
|
||||
BANNER_URL="${BANNER_URL:-https://github.com/IliaMiheev/iQR}"
|
||||
ACCENT_COLOR="${ACCENT_COLOR:-#ff4500}"
|
||||
|
||||
cat > /usr/share/nginx/html/config.json <<EOF
|
||||
{
|
||||
"title": "$(printf '%s' "$APP_TITLE" | sed 's/"/\\"/g')",
|
||||
"bannerBrand": "$(printf '%s' "$BANNER_BRAND" | sed 's/"/\\"/g')",
|
||||
"bannerSlogan": "$(printf '%s' "$BANNER_SLOGAN" | sed 's/"/\\"/g')",
|
||||
"bannerUrl": "$(printf '%s' "$BANNER_URL" | sed 's/"/\\"/g')",
|
||||
"accentColor": "$(printf '%s' "$ACCENT_COLOR" | sed 's/"/\\"/g')"
|
||||
}
|
||||
EOF
|
||||
|
||||
exec nginx -g 'daemon off;'
|
||||
@@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /config.json {
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="revisit-after" content="8 days">
|
||||
<meta name="author" content="Ilia Miheev">
|
||||
<meta name="theme-color" content="orangered">
|
||||
<meta name="theme-color" content="#ff4500">
|
||||
|
||||
<meta property="og:title" content="Генератор QR кодов">
|
||||
<meta property="og:description" content="Простой в использовании генератор QR кодов. Сделано программистом с любовью к пользователям">
|
||||
|
||||
+8
-5
@@ -2,7 +2,9 @@
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useTheme } from './composables/useTheme'
|
||||
import { useQrCode } from './composables/useQrCode'
|
||||
import { useAppConfig } from './composables/useAppConfig'
|
||||
|
||||
const config = useAppConfig()
|
||||
const { isLightTheme, toggleTheme } = useTheme()
|
||||
|
||||
const {
|
||||
@@ -51,7 +53,7 @@ onUnmounted(() => {
|
||||
<main class="app-shell">
|
||||
<div class="app-card">
|
||||
<header class="logo">
|
||||
<h1>Генератор QR-кодов</h1>
|
||||
<h1>{{ config.title }}</h1>
|
||||
<a
|
||||
href="https://github.com/IliaMiheev/iQR"
|
||||
target="_blank"
|
||||
@@ -105,17 +107,18 @@ onUnmounted(() => {
|
||||
<p v-if="error" :key="error" class="status" :class="statusClass">{{ error }}</p>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://github.com/IliaMiheev/iQR"
|
||||
:href="config.bannerUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="site-banner"
|
||||
title="Репозиторий на GitHub"
|
||||
:title="config.bannerBrand"
|
||||
>
|
||||
<span class="site-banner__brand">iQR</span>
|
||||
<span class="site-banner__slogan">— qr-код без излишеств</span>
|
||||
<span class="site-banner__brand">{{ config.bannerBrand }}</span>
|
||||
<span class="site-banner__slogan">{{ config.bannerSlogan }}</span>
|
||||
</a>
|
||||
</main>
|
||||
|
||||
|
||||
+14
-14
@@ -1,5 +1,5 @@
|
||||
::selection {
|
||||
background-color: orangered;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ html {
|
||||
|
||||
html.dark {
|
||||
--bg: #1a1a1e;
|
||||
--bg-accent: rgba(255, 69, 0, 0.06);
|
||||
--surface: rgba(38, 38, 42, 0.72);
|
||||
--surface-hover: rgba(52, 52, 58, 0.88);
|
||||
--surface-solid: rgba(30, 30, 34, 0.85);
|
||||
@@ -20,20 +19,23 @@ html.dark {
|
||||
--placeholder: rgba(255, 255, 255, 0.35);
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
--accent: orangered;
|
||||
--accent-glow: rgba(255, 69, 0, 0.35);
|
||||
--accent: #ff4500;
|
||||
--accent-glow: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
--bg-accent: color-mix(in srgb, var(--accent) 6%, transparent);
|
||||
--canvas-bg: #fff;
|
||||
--canvas-border: rgba(255, 255, 255, 0.15);
|
||||
|
||||
background-color: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -10%, var(--bg-accent), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(255, 69, 0, 0.04), transparent);
|
||||
radial-gradient(ellipse 60% 40% at 100% 100%, color-mix(in srgb, var(--accent) 4%, transparent), transparent);
|
||||
}
|
||||
|
||||
html.light {
|
||||
--bg: #f4f4f6;
|
||||
--bg-accent: rgba(255, 69, 0, 0.08);
|
||||
--accent: #ff4500;
|
||||
--accent-glow: color-mix(in srgb, var(--accent) 25%, transparent);
|
||||
--bg-accent: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
--surface: rgba(255, 255, 255, 0.78);
|
||||
--surface-hover: rgba(255, 255, 255, 0.95);
|
||||
--surface-solid: rgba(255, 255, 255, 0.92);
|
||||
@@ -43,15 +45,13 @@ html.light {
|
||||
--placeholder: rgba(0, 0, 0, 0.35);
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
--accent: orangered;
|
||||
--accent-glow: rgba(255, 69, 0, 0.25);
|
||||
--canvas-bg: #fff;
|
||||
--canvas-border: rgba(0, 0, 0, 0.1);
|
||||
|
||||
background-color: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -10%, var(--bg-accent), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 0% 100%, rgba(255, 69, 0, 0.05), transparent);
|
||||
radial-gradient(ellipse 60% 40% at 0% 100%, color-mix(in srgb, var(--accent) 5%, transparent), transparent);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -186,7 +186,7 @@ textarea:focus {
|
||||
}
|
||||
|
||||
textarea::selection {
|
||||
background-color: orangered;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -301,14 +301,14 @@ button {
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #ff5722;
|
||||
background-color: color-mix(in srgb, var(--accent) 88%, white);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
background-color: rgb(200, 55, 0);
|
||||
background-color: color-mix(in srgb, var(--accent) 80%, black);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -349,7 +349,7 @@ button {
|
||||
}
|
||||
|
||||
button::selection {
|
||||
background-color: rgb(172, 46, 0);
|
||||
background-color: color-mix(in srgb, var(--accent) 70%, black);
|
||||
}
|
||||
|
||||
.status-slot {
|
||||
@@ -705,7 +705,7 @@ html.light .site-banner:hover {
|
||||
|
||||
.site-banner__brand {
|
||||
font-weight: 700;
|
||||
color: orangered;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.site-banner__slogan {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { inject } from 'vue'
|
||||
|
||||
export function useAppConfig() {
|
||||
const config = inject('appConfig')
|
||||
|
||||
if (!config) {
|
||||
throw new Error('appConfig не найден. Проверьте инициализацию в main.js.')
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function applyAppConfig(config) {
|
||||
const root = document.documentElement
|
||||
|
||||
root.style.setProperty('--accent', config.accentColor)
|
||||
|
||||
const themeMeta = document.querySelector('meta[name="theme-color"]')
|
||||
if (themeMeta) {
|
||||
themeMeta.setAttribute('content', config.accentColor)
|
||||
}
|
||||
|
||||
document.title = config.title
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const defaultConfig = {
|
||||
title: 'Генератор QR-кодов',
|
||||
bannerBrand: 'iQR',
|
||||
bannerSlogan: '— qr-код без излишеств',
|
||||
bannerUrl: 'https://github.com/IliaMiheev/iQR',
|
||||
accentColor: '#ff4500',
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { defaultConfig } from './defaults'
|
||||
|
||||
function fromBuildEnv() {
|
||||
const env = import.meta.env
|
||||
|
||||
return pickDefined({
|
||||
title: env.VITE_APP_TITLE,
|
||||
bannerBrand: env.VITE_BANNER_BRAND,
|
||||
bannerSlogan: env.VITE_BANNER_SLOGAN,
|
||||
bannerUrl: env.VITE_BANNER_URL,
|
||||
accentColor: env.VITE_ACCENT_COLOR,
|
||||
})
|
||||
}
|
||||
|
||||
function pickDefined(source) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(source).filter(([, value]) => value !== undefined && value !== ''),
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadConfig() {
|
||||
let runtime = {}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.BASE_URL}config.json`, {
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
runtime = await response.json()
|
||||
}
|
||||
} catch {
|
||||
// Локально config.json может отсутствовать — это нормально.
|
||||
}
|
||||
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
...fromBuildEnv(),
|
||||
}
|
||||
|
||||
for (const key of Object.keys(defaultConfig)) {
|
||||
if (key in runtime) {
|
||||
config[key] = runtime[key]
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
+12
-1
@@ -1,5 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './assets/style.css'
|
||||
import { loadConfig } from './config/loadConfig'
|
||||
import { applyAppConfig } from './config/applyTheme'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
async function bootstrap() {
|
||||
const config = await loadConfig()
|
||||
applyAppConfig(config)
|
||||
|
||||
const app = createApp(App)
|
||||
app.provide('appConfig', config)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
Reference in New Issue
Block a user