Сделал кастомизацию через env

This commit is contained in:
Ilia Miheev
2026-07-26 14:08:41 +03:00
parent 572ddc35fc
commit d22178e516
15 changed files with 331 additions and 30 deletions
+24
View File
@@ -0,0 +1,24 @@
# Copy to .env and change values as needed.
# If .env is missing or a key is empty, built-in irandom defaults are used.
# Branding
# Display name in sidebar / mobile header
VUE_APP_NAME=irandom
# Browser tab title (can differ from NAME)
VUE_APP_TITLE=irandom
# Link on logo + name
VUE_APP_BRAND_URL=https://git.imiheev.online/admin/irandom
# Logo path or absolute URL (file should be in public/ if relative)
VUE_APP_LOGO_URL=/logo.png
# Theme colors (CSS hex)
VUE_APP_COLOR_PRIMARY=#c9a66b
VUE_APP_COLOR_ACCENT=#e6c566
VUE_APP_COLOR_BG=#1a1815
VUE_APP_COLOR_SURFACE=#2e2820
VUE_APP_COLOR_SURFACE_RAISED=#3a3228
VUE_APP_COLOR_TEXT=#ffffff
VUE_APP_COLOR_TEXT_MUTED=#c8c0b4
VUE_APP_COLOR_DANGER=#ee5c5c
VUE_APP_COLOR_LINK=#4a6a8a
VUE_APP_COLOR_BORDER=#4a4034
+1
View File
@@ -4,6 +4,7 @@
# local env files
.env
.env.local
.env.*.local
+3
View File
@@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"generate-config": "node scripts/generate-config.js",
"preserve": "node scripts/generate-config.js",
"prebuild": "node scripts/generate-config.js",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
+19
View File
@@ -0,0 +1,19 @@
/* Generated by scripts/generate-config.js — do not edit by hand */
window.__APP_CONFIG__ = {
"name": "irandom",
"title": "irandom",
"brandUrl": "https://git.imiheev.online/admin/irandom",
"logoUrl": "/logo.png",
"colors": {
"primary": "#c9a66b",
"accent": "#e6c566",
"bg": "#1a1815",
"surface": "#2e2820",
"surfaceRaised": "#3a3228",
"text": "#ffffff",
"textMuted": "#c8c0b4",
"danger": "#ee5c5c",
"link": "#4a6a8a",
"border": "#4a4034"
}
};
+2 -1
View File
@@ -5,7 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>logo.png">
<title>irandom — простой генератор случайностей</title>
<title>irandom</title>
<script src="<%= BASE_URL %>config.js"></script>
</head>
<body>
<noscript>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

+90
View File
@@ -0,0 +1,90 @@
const fs = require('fs')
const path = require('path')
const ROOT = path.resolve(__dirname, '..')
const ENV_PATH = path.join(ROOT, '.env')
const OUT_PATH = path.join(ROOT, 'public', 'config.js')
const DEFAULTS = {
VUE_APP_NAME: 'irandom',
VUE_APP_TITLE: 'irandom',
VUE_APP_BRAND_URL: 'https://git.imiheev.online/admin/irandom',
VUE_APP_LOGO_URL: '/logo.png',
VUE_APP_COLOR_PRIMARY: '#c9a66b',
VUE_APP_COLOR_ACCENT: '#e6c566',
VUE_APP_COLOR_BG: '#1a1815',
VUE_APP_COLOR_SURFACE: '#2e2820',
VUE_APP_COLOR_SURFACE_RAISED: '#3a3228',
VUE_APP_COLOR_TEXT: '#ffffff',
VUE_APP_COLOR_TEXT_MUTED: '#c8c0b4',
VUE_APP_COLOR_DANGER: '#ee5c5c',
VUE_APP_COLOR_LINK: '#4a6a8a',
VUE_APP_COLOR_BORDER: '#4a4034'
}
function parseEnvFile(filePath) {
if (!fs.existsSync(filePath)) {
return {}
}
const result = {}
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/)
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) {
continue
}
const eq = trimmed.indexOf('=')
if (eq === -1) {
continue
}
const key = trimmed.slice(0, eq).trim()
let value = trimmed.slice(eq + 1).trim()
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1)
}
result[key] = value
}
return result
}
function buildConfig(env) {
const pick = (key) => env[key] || process.env[key] || DEFAULTS[key]
return {
name: pick('VUE_APP_NAME'),
title: pick('VUE_APP_TITLE'),
brandUrl: pick('VUE_APP_BRAND_URL'),
logoUrl: pick('VUE_APP_LOGO_URL'),
colors: {
primary: pick('VUE_APP_COLOR_PRIMARY'),
accent: pick('VUE_APP_COLOR_ACCENT'),
bg: pick('VUE_APP_COLOR_BG'),
surface: pick('VUE_APP_COLOR_SURFACE'),
surfaceRaised: pick('VUE_APP_COLOR_SURFACE_RAISED'),
text: pick('VUE_APP_COLOR_TEXT'),
textMuted: pick('VUE_APP_COLOR_TEXT_MUTED'),
danger: pick('VUE_APP_COLOR_DANGER'),
link: pick('VUE_APP_COLOR_LINK'),
border: pick('VUE_APP_COLOR_BORDER')
}
}
}
const fileEnv = parseEnvFile(ENV_PATH)
const config = buildConfig(fileEnv)
const payload = JSON.stringify(config, null, 2)
const output = `/* Generated by scripts/generate-config.js — do not edit by hand */
window.__APP_CONFIG__ = ${payload};
`
fs.writeFileSync(OUT_PATH, output, 'utf8')
console.log(`Wrote ${path.relative(ROOT, OUT_PATH)}`)
+12 -6
View File
@@ -2,20 +2,26 @@
<header class="app-header">
<a
class="brand"
href="https://git.imiheev.online/admin/irandom"
:href="config.brandUrl"
target="_blank"
rel="noopener noreferrer"
title="Gitea"
>
<img class="logo" src="/logo.png" alt="irandom" />
<span class="brand-name">irandom</span>
<img class="logo" :src="config.logoUrl" :alt="config.name" />
<span class="brand-name">{{ config.name }}</span>
</a>
</header>
</template>
<script>
import { getAppConfig } from '@/config/appConfig'
export default {
name: 'AppHeader'
name: 'AppHeader',
data() {
return {
config: getAppConfig()
}
}
}
</script>
@@ -55,6 +61,6 @@ export default {
}
.brand:hover .brand-name {
color: var(--ir-accent);
color: color-mix(in srgb, var(--ir-primary) 70%, white);
}
</style>
+9 -8
View File
@@ -3,13 +3,12 @@
<a
v-if="showBrand"
class="brand"
href="https://git.imiheev.online/admin/irandom"
:href="config.brandUrl"
target="_blank"
rel="noopener noreferrer"
title="Gitea"
>
<img class="logo" src="/logo.png" alt="irandom" />
<span class="brand-name">irandom</span>
<img class="logo" :src="config.logoUrl" :alt="config.name" />
<span class="brand-name">{{ config.name }}</span>
</a>
<RouterLink
v-for="mode in modes"
@@ -28,6 +27,7 @@
<script>
import { Histogram, List, Coin } from '@element-plus/icons-vue'
import { getAppConfig } from '@/config/appConfig'
export default {
name: 'ModeSidebar',
@@ -44,6 +44,7 @@ export default {
},
data() {
return {
config: getAppConfig(),
modes: [
{ id: 'range', to: '/range', label: 'Диапазон', icon: 'Histogram' },
{ id: 'list', to: '/list', label: 'Список', icon: 'List' },
@@ -92,7 +93,7 @@ export default {
}
.brand:hover .brand-name {
color: var(--ir-accent);
color: color-mix(in srgb, var(--ir-primary) 70%, white);
}
.mode-item {
@@ -118,9 +119,9 @@ export default {
}
&.active {
background: rgba(201, 166, 107, 0.18);
background: rgba(var(--ir-primary-rgb), 0.18);
border-color: var(--ir-primary);
color: var(--ir-accent);
color: var(--ir-primary);
font-weight: 600;
}
}
@@ -162,7 +163,7 @@ export default {
&.active {
background: transparent;
border-color: transparent;
color: var(--ir-accent);
color: var(--ir-primary);
}
}
+2 -2
View File
@@ -62,7 +62,7 @@ export default {
gap: 0.5rem;
font-size: 2rem;
font-weight: 700;
color: var(--ir-accent);
color: var(--ir-primary);
line-height: 1.3;
word-break: break-word;
}
@@ -70,7 +70,7 @@ export default {
.result-chip {
display: inline-block;
padding: 0.15rem 0.65rem;
background: rgba(230, 197, 102, 0.12);
background: rgba(var(--ir-primary-rgb), 0.12);
border-radius: 8px;
}
+31
View File
@@ -0,0 +1,31 @@
const DEFAULTS = {
name: 'irandom',
title: 'irandom',
brandUrl: 'https://git.imiheev.online/admin/irandom',
logoUrl: '/logo.png',
colors: {
primary: '#c9a66b',
accent: '#e6c566',
bg: '#1a1815',
surface: '#2e2820',
surfaceRaised: '#3a3228',
text: '#ffffff',
textMuted: '#c8c0b4',
danger: '#ee5c5c',
link: '#4a6a8a',
border: '#4a4034'
}
}
export function getAppConfig() {
const runtime = (typeof window !== 'undefined' && window.__APP_CONFIG__) || {}
const colors = { ...DEFAULTS.colors, ...(runtime.colors || {}) }
return {
name: runtime.name || DEFAULTS.name,
title: runtime.title || DEFAULTS.title,
brandUrl: runtime.brandUrl || DEFAULTS.brandUrl,
logoUrl: runtime.logoUrl || DEFAULTS.logoUrl,
colors
}
}
+123
View File
@@ -0,0 +1,123 @@
import { getAppConfig } from './appConfig'
const COLOR_TO_CSS_VAR = {
primary: '--ir-primary',
accent: '--ir-accent',
bg: '--ir-bg',
surface: '--ir-surface',
surfaceRaised: '--ir-surface-raised',
text: '--ir-text',
textMuted: '--ir-text-muted',
danger: '--ir-danger',
link: '--ir-link',
border: '--ir-border'
}
function parseHex(hex) {
const raw = String(hex).replace('#', '').trim()
if (raw.length !== 3 && raw.length !== 6) {
return null
}
const full =
raw.length === 3
? raw
.split('')
.map((c) => c + c)
.join('')
: raw
const n = Number.parseInt(full, 16)
if (Number.isNaN(n)) {
return null
}
return {
r: (n >> 16) & 255,
g: (n >> 8) & 255,
b: n & 255
}
}
function toHex({ r, g, b }) {
const h = (v) => v.toString(16).padStart(2, '0')
return `#${h(r)}${h(g)}${h(b)}`
}
/** Mix color toward white (weight 0..1 = amount of white). */
function mixWithWhite(hex, weight) {
const c = parseHex(hex)
if (!c) {
return hex
}
return toHex({
r: Math.round(c.r + (255 - c.r) * weight),
g: Math.round(c.g + (255 - c.g) * weight),
b: Math.round(c.b + (255 - c.b) * weight)
})
}
/** Mix color toward black (weight 0..1 = amount of black). */
function mixWithBlack(hex, weight) {
const c = parseHex(hex)
if (!c) {
return hex
}
return toHex({
r: Math.round(c.r * (1 - weight)),
g: Math.round(c.g * (1 - weight)),
b: Math.round(c.b * (1 - weight))
})
}
function toRgbTriplet(hex) {
const c = parseHex(hex)
return c ? `${c.r}, ${c.g}, ${c.b}` : null
}
function applyElementPrimary(root, primary) {
root.style.setProperty('--el-color-primary', primary)
root.style.setProperty('--el-color-primary-light-3', mixWithWhite(primary, 0.3))
root.style.setProperty('--el-color-primary-light-5', mixWithWhite(primary, 0.5))
root.style.setProperty('--el-color-primary-light-7', mixWithWhite(primary, 0.7))
root.style.setProperty('--el-color-primary-light-8', mixWithWhite(primary, 0.8))
root.style.setProperty('--el-color-primary-light-9', mixWithWhite(primary, 0.9))
root.style.setProperty('--el-color-primary-dark-2', mixWithBlack(primary, 0.2))
const rgb = toRgbTriplet(primary)
if (rgb) {
root.style.setProperty('--el-color-primary-rgb', rgb)
root.style.setProperty('--ir-primary-rgb', rgb)
}
}
function applyElementDanger(root, danger) {
root.style.setProperty('--el-color-danger', danger)
root.style.setProperty('--el-color-danger-light-3', mixWithWhite(danger, 0.3))
root.style.setProperty('--el-color-danger-light-5', mixWithWhite(danger, 0.5))
root.style.setProperty('--el-color-danger-light-7', mixWithWhite(danger, 0.7))
root.style.setProperty('--el-color-danger-light-8', mixWithWhite(danger, 0.8))
root.style.setProperty('--el-color-danger-light-9', mixWithWhite(danger, 0.9))
root.style.setProperty('--el-color-danger-dark-2', mixWithBlack(danger, 0.2))
}
export function applyTheme(config = getAppConfig()) {
const root = document.documentElement
const { colors } = config
Object.entries(COLOR_TO_CSS_VAR).forEach(([key, cssVar]) => {
if (colors[key]) {
root.style.setProperty(cssVar, colors[key])
}
})
if (colors.primary) {
applyElementPrimary(root, colors.primary)
}
if (colors.danger) {
applyElementDanger(root, colors.danger)
}
if (colors.accent) {
const accentRgb = toRgbTriplet(colors.accent)
if (accentRgb) {
root.style.setProperty('--ir-accent-rgb', accentRgb)
}
}
}
+6
View File
@@ -4,5 +4,11 @@ import 'element-plus/dist/index.css'
import './styles/theme.css'
import App from './App.vue'
import router from './router'
import { getAppConfig } from './config/appConfig'
import { applyTheme } from './config/applyTheme'
const appConfig = getAppConfig()
applyTheme(appConfig)
document.title = appConfig.title
createApp(App).use(router).use(ElementPlus).mount('#app')
+8 -6
View File
@@ -11,12 +11,14 @@
--ir-border: #4a4034;
--el-color-primary: var(--ir-primary);
--el-color-primary-light-3: #d4b882;
--el-color-primary-light-5: #dfcaa0;
--el-color-primary-light-7: #e9dbbd;
--el-color-primary-light-8: #eee4cc;
--el-color-primary-light-9: #f4ede0;
--el-color-primary-dark-2: #a18856;
--el-color-primary-light-3: color-mix(in srgb, var(--ir-primary) 70%, white);
--el-color-primary-light-5: color-mix(in srgb, var(--ir-primary) 50%, white);
--el-color-primary-light-7: color-mix(in srgb, var(--ir-primary) 30%, white);
--el-color-primary-light-8: color-mix(in srgb, var(--ir-primary) 20%, white);
--el-color-primary-light-9: color-mix(in srgb, var(--ir-primary) 10%, white);
--el-color-primary-dark-2: color-mix(in srgb, var(--ir-primary) 80%, black);
--ir-primary-rgb: 201, 166, 107;
--ir-accent-rgb: 230, 197, 102;
--el-color-danger: var(--ir-danger);
--el-bg-color: var(--ir-surface);
+1 -7
View File
@@ -1,10 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
pages: {
index: {
entry: 'src/main.js',
title: 'irandom'
}
}
transpileDependencies: true
})