@@ -10,6 +10,8 @@ VUE_APP_TITLE=irandom
|
|||||||
VUE_APP_BRAND_URL=https://git.imiheev.online/admin/irandom
|
VUE_APP_BRAND_URL=https://git.imiheev.online/admin/irandom
|
||||||
# Logo path or absolute URL (file should be in public/ if relative)
|
# Logo path or absolute URL (file should be in public/ if relative)
|
||||||
VUE_APP_LOGO_URL=/logo.png
|
VUE_APP_LOGO_URL=/logo.png
|
||||||
|
# Default UI language: ru | en (overridden by localStorage after user chooses)
|
||||||
|
VUE_APP_DEFAULT_LOCALE=ru
|
||||||
|
|
||||||
# Theme colors (CSS hex)
|
# Theme colors (CSS hex)
|
||||||
VUE_APP_COLOR_PRIMARY=#c9a66b
|
VUE_APP_COLOR_PRIMARY=#c9a66b
|
||||||
|
|||||||
Generated
+83
@@ -11,6 +11,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"element-plus": "^2.14.3",
|
"element-plus": "^2.14.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^11.4.8",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -653,6 +654,67 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@intlify/core-base": {
|
||||||
|
"version": "11.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.8.tgz",
|
||||||
|
"integrity": "sha512-A+Q7SKm5oEcy1E/cghqd7n/St4XjTqLhiiyDuieNcMrJcrHlkY5n0jp7Q9dD3txvVHzvsmBVV5M9wD5/s1zfzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/devtools-types": "11.4.8",
|
||||||
|
"@intlify/message-compiler": "11.4.8",
|
||||||
|
"@intlify/shared": "11.4.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/devtools-types": {
|
||||||
|
"version": "11.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.8.tgz",
|
||||||
|
"integrity": "sha512-MGpID+rlfzGUbNcnC20bm5NMSBHPrvx0atLTfv9dftn3kjXw1hGKDcIcwrO99tSrZEc2i+hczRL7ks8qXsHPkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/core-base": "11.4.8",
|
||||||
|
"@intlify/shared": "11.4.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/message-compiler": {
|
||||||
|
"version": "11.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.8.tgz",
|
||||||
|
"integrity": "sha512-vbzk17dYwduYiv52EK61+FDCyhfVg1uPUtPmiD/d45W99uJIcXywrweOBcHv7n9/iEqmXiMGT52bgJbZDQqK3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/shared": "11.4.8",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/shared": {
|
||||||
|
"version": "11.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.8.tgz",
|
||||||
|
"integrity": "sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@@ -3201,6 +3263,27 @@
|
|||||||
"eslint": ">=6.0.0"
|
"eslint": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-i18n": {
|
||||||
|
"version": "11.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.8.tgz",
|
||||||
|
"integrity": "sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/core-base": "11.4.8",
|
||||||
|
"@intlify/devtools-types": "11.4.8",
|
||||||
|
"@intlify/shared": "11.4.8",
|
||||||
|
"@vue/devtools-api": "^6.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.6.4",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"element-plus": "^2.14.3",
|
"element-plus": "^2.14.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^11.4.8",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ window.__APP_CONFIG__ = {
|
|||||||
"title": "irandom",
|
"title": "irandom",
|
||||||
"brandUrl": "https://git.imiheev.online/admin/irandom",
|
"brandUrl": "https://git.imiheev.online/admin/irandom",
|
||||||
"logoUrl": "/logo.png",
|
"logoUrl": "/logo.png",
|
||||||
|
"defaultLocale": "ru",
|
||||||
"colors": {
|
"colors": {
|
||||||
"primary": "#c9a66b",
|
"primary": "#c9a66b",
|
||||||
"accent": "#e6c566",
|
"accent": "#e6c566",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULTS = {
|
|||||||
VUE_APP_TITLE: 'irandom',
|
VUE_APP_TITLE: 'irandom',
|
||||||
VUE_APP_BRAND_URL: 'https://git.imiheev.online/admin/irandom',
|
VUE_APP_BRAND_URL: 'https://git.imiheev.online/admin/irandom',
|
||||||
VUE_APP_LOGO_URL: '/logo.png',
|
VUE_APP_LOGO_URL: '/logo.png',
|
||||||
|
VUE_APP_DEFAULT_LOCALE: 'ru',
|
||||||
VUE_APP_COLOR_PRIMARY: '#c9a66b',
|
VUE_APP_COLOR_PRIMARY: '#c9a66b',
|
||||||
VUE_APP_COLOR_ACCENT: '#e6c566',
|
VUE_APP_COLOR_ACCENT: '#e6c566',
|
||||||
VUE_APP_COLOR_BG: '#1a1815',
|
VUE_APP_COLOR_BG: '#1a1815',
|
||||||
@@ -63,6 +64,7 @@ function buildConfig(env) {
|
|||||||
title: pick('VUE_APP_TITLE'),
|
title: pick('VUE_APP_TITLE'),
|
||||||
brandUrl: pick('VUE_APP_BRAND_URL'),
|
brandUrl: pick('VUE_APP_BRAND_URL'),
|
||||||
logoUrl: pick('VUE_APP_LOGO_URL'),
|
logoUrl: pick('VUE_APP_LOGO_URL'),
|
||||||
|
defaultLocale: pick('VUE_APP_DEFAULT_LOCALE'),
|
||||||
colors: {
|
colors: {
|
||||||
primary: pick('VUE_APP_COLOR_PRIMARY'),
|
primary: pick('VUE_APP_COLOR_PRIMARY'),
|
||||||
accent: pick('VUE_APP_COLOR_ACCENT'),
|
accent: pick('VUE_APP_COLOR_ACCENT'),
|
||||||
|
|||||||
+36
-3
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<el-config-provider :locale="elementLocale">
|
||||||
<el-container class="app-shell">
|
<el-container class="app-shell">
|
||||||
<el-aside width="220px" class="aside desktop-aside">
|
<el-aside :width="asideWidth" class="aside desktop-aside">
|
||||||
<ModeSidebar show-brand />
|
<ModeSidebar show-brand @collapse-change="onCollapseChange" />
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container class="main-column">
|
<el-container class="main-column">
|
||||||
<el-header height="auto" class="header mobile-header">
|
<el-header height="auto" class="header mobile-header">
|
||||||
@@ -11,13 +12,17 @@
|
|||||||
<RouterView />
|
<RouterView />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
<nav class="mobile-toolbar" aria-label="Режимы генерации">
|
<nav class="mobile-toolbar" :aria-label="$t('nav.modes')">
|
||||||
<ModeSidebar />
|
<ModeSidebar />
|
||||||
</nav>
|
</nav>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ru from 'element-plus/es/locale/lang/ru'
|
||||||
|
import en from 'element-plus/es/locale/lang/en'
|
||||||
import AppHeader from './components/AppHeader.vue'
|
import AppHeader from './components/AppHeader.vue'
|
||||||
import ModeSidebar from './components/ModeSidebar.vue'
|
import ModeSidebar from './components/ModeSidebar.vue'
|
||||||
|
|
||||||
@@ -26,6 +31,28 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AppHeader,
|
AppHeader,
|
||||||
ModeSidebar
|
ModeSidebar
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { locale } = useI18n()
|
||||||
|
return { locale }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sidebarCollapsed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
elementLocale() {
|
||||||
|
return this.locale === 'en' ? en : ru
|
||||||
|
},
|
||||||
|
asideWidth() {
|
||||||
|
return this.sidebarCollapsed ? '64px' : '220px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onCollapseChange(collapsed) {
|
||||||
|
this.sidebarCollapsed = collapsed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -40,6 +67,7 @@ export default {
|
|||||||
background: var(--ir-surface);
|
background: var(--ir-surface);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
|
||||||
.mode-sidebar {
|
.mode-sidebar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -68,6 +96,11 @@ export default {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ir-lang-popper {
|
||||||
|
background: var(--ir-surface) !important;
|
||||||
|
border: 1px solid var(--ir-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 424px) {
|
@media (max-width: 424px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
|
|||||||
+210
-70
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="mode-sidebar" aria-label="Режимы генерации">
|
<nav class="mode-sidebar" :class="{ collapsed: isCollapsed && !isMobile }" :aria-label="t('nav.modes')">
|
||||||
<a
|
<a
|
||||||
v-if="showBrand"
|
v-if="showBrand"
|
||||||
class="brand"
|
class="brand"
|
||||||
@@ -8,33 +8,84 @@
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<img class="logo" :src="config.logoUrl" :alt="config.name" />
|
<img class="logo" :src="config.logoUrl" :alt="config.name" />
|
||||||
<span class="brand-name">{{ config.name }}</span>
|
<span v-show="!isCollapsed || isMobile" class="brand-name">{{ config.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
<RouterLink
|
|
||||||
|
<button
|
||||||
|
v-if="showBrand && !isMobile"
|
||||||
|
type="button"
|
||||||
|
class="collapse-btn"
|
||||||
|
:title="isCollapsed ? t('nav.expand') : t('nav.collapse')"
|
||||||
|
:aria-label="isCollapsed ? t('nav.expand') : t('nav.collapse')"
|
||||||
|
@click="toggleCollapse"
|
||||||
|
>
|
||||||
|
<el-icon :size="18">
|
||||||
|
<Fold v-if="!isCollapsed" />
|
||||||
|
<Expand v-else />
|
||||||
|
</el-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
class="side-menu"
|
||||||
|
:key="activeIndex + '-' + locale"
|
||||||
|
:collapse="isCollapsed && !isMobile"
|
||||||
|
:collapse-transition="false"
|
||||||
|
:default-active="activeIndex"
|
||||||
|
background-color="transparent"
|
||||||
|
text-color="var(--ir-text-muted)"
|
||||||
|
active-text-color="var(--ir-primary)"
|
||||||
|
@select="onMenuSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
v-for="mode in modes"
|
v-for="mode in modes"
|
||||||
:key="mode.id"
|
:key="mode.id"
|
||||||
:to="mode.to"
|
:index="mode.to"
|
||||||
class="mode-item"
|
|
||||||
active-class="active"
|
|
||||||
>
|
>
|
||||||
<el-icon class="mode-icon" :size="22">
|
<el-icon>
|
||||||
<component :is="mode.icon" />
|
<component :is="mode.icon" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span class="mode-label">{{ mode.label }}</span>
|
<template #title>{{ t(mode.labelKey) }}</template>
|
||||||
</RouterLink>
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-sub-menu index="language" popper-class="ir-lang-popper">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><Flag /></el-icon>
|
||||||
|
<span>{{ t('nav.language') }}</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item
|
||||||
|
index="lang-ru"
|
||||||
|
:class="{ 'is-locale-active': locale === 'ru' }"
|
||||||
|
>
|
||||||
|
{{ t('lang.ru') }}
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item
|
||||||
|
index="lang-en"
|
||||||
|
:class="{ 'is-locale-active': locale === 'en' }"
|
||||||
|
>
|
||||||
|
{{ t('lang.en') }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Histogram, List, Coin } from '@element-plus/icons-vue'
|
import { Histogram, List, Coin, Fold, Expand, Flag } from '@element-plus/icons-vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { getAppConfig } from '@/config/appConfig'
|
import { getAppConfig } from '@/config/appConfig'
|
||||||
|
import i18n, { setAppLocale } from '@/i18n'
|
||||||
|
|
||||||
|
const COLLAPSE_KEY = 'irandom:sidebar-collapsed'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModeSidebar',
|
name: 'ModeSidebar',
|
||||||
components: {
|
components: {
|
||||||
Histogram,
|
Histogram,
|
||||||
List,
|
List,
|
||||||
Coin
|
Coin,
|
||||||
|
Fold,
|
||||||
|
Expand,
|
||||||
|
Flag
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
showBrand: {
|
showBrand: {
|
||||||
@@ -42,15 +93,76 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['collapse-change'],
|
||||||
|
setup() {
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
return { t, locale }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
config: getAppConfig(),
|
config: getAppConfig(),
|
||||||
|
isCollapsed: false,
|
||||||
|
isMobile: false,
|
||||||
modes: [
|
modes: [
|
||||||
{ id: 'range', to: '/range', label: 'Диапазон', icon: 'Histogram' },
|
{ id: 'range', to: '/range', labelKey: 'nav.range', icon: 'Histogram' },
|
||||||
{ id: 'list', to: '/list', label: 'Список', icon: 'List' },
|
{ id: 'list', to: '/list', labelKey: 'nav.list', icon: 'List' },
|
||||||
{ id: 'coin', to: '/coin', label: 'Орёл / решка', icon: 'Coin' }
|
{ id: 'coin', to: '/coin', labelKey: 'nav.coin', icon: 'Coin' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
activeIndex() {
|
||||||
|
return this.$route.path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.isCollapsed = this.readCollapsed()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateMobile()
|
||||||
|
window.addEventListener('resize', this.updateMobile)
|
||||||
|
this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('resize', this.updateMobile)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
readCollapsed() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(COLLAPSE_KEY) === '1'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
persistCollapsed() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(COLLAPSE_KEY, this.isCollapsed ? '1' : '0')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleCollapse() {
|
||||||
|
this.isCollapsed = !this.isCollapsed
|
||||||
|
this.persistCollapsed()
|
||||||
|
this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
|
||||||
|
},
|
||||||
|
updateMobile() {
|
||||||
|
this.isMobile = window.matchMedia('(max-width: 424px)').matches
|
||||||
|
this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
|
||||||
|
},
|
||||||
|
onMenuSelect(index) {
|
||||||
|
if (index === 'lang-ru') {
|
||||||
|
setAppLocale(i18n, 'ru')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (index === 'lang-en') {
|
||||||
|
setAppLocale(i18n, 'en')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (index.startsWith('/') && this.$route.path !== index) {
|
||||||
|
this.$router.push(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -59,9 +171,9 @@ export default {
|
|||||||
.mode-sidebar {
|
.mode-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.35rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem 0.75rem;
|
padding: 1rem 0.5rem;
|
||||||
background: var(--ir-surface);
|
background: var(--ir-surface);
|
||||||
border-right: 1px solid var(--ir-border);
|
border-right: 1px solid var(--ir-border);
|
||||||
}
|
}
|
||||||
@@ -70,10 +182,11 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
padding: 0.35rem 0.5rem 1rem;
|
padding: 0.35rem 0.5rem 0.85rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.15rem;
|
||||||
border-bottom: 1px solid var(--ir-border);
|
border-bottom: 1px solid var(--ir-border);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -90,93 +203,120 @@ export default {
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
color: var(--ir-primary);
|
color: var(--ir-primary);
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand:hover .brand-name {
|
.brand:hover .brand-name {
|
||||||
color: color-mix(in srgb, var(--ir-primary) 70%, white);
|
color: color-mix(in srgb, var(--ir-primary) 70%, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-item {
|
.collapse-btn {
|
||||||
display: flex;
|
align-self: flex-end;
|
||||||
align-items: center;
|
|
||||||
gap: 0.65rem;
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--ir-text-muted);
|
color: var(--ir-text-muted);
|
||||||
padding: 0.85rem 0.75rem;
|
padding: 0.45rem;
|
||||||
text-align: left;
|
|
||||||
text-decoration: none;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--ir-surface-raised);
|
background: var(--ir-surface-raised);
|
||||||
color: var(--ir-text);
|
color: var(--ir-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: rgba(var(--ir-primary-rgb), 0.18);
|
|
||||||
border-color: var(--ir-primary);
|
|
||||||
color: var(--ir-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-icon {
|
.side-menu {
|
||||||
flex-shrink: 0;
|
border-right: none !important;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent !important;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu:not(.el-menu--collapse) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0.15rem 0;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background: rgba(var(--ir-primary-rgb), 0.18) !important;
|
||||||
|
border: 1px solid var(--ir-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item:hover),
|
||||||
|
:deep(.el-sub-menu__title:hover) {
|
||||||
|
background: var(--ir-surface-raised) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-locale-active) {
|
||||||
|
color: var(--ir-primary) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed {
|
||||||
|
.brand {
|
||||||
|
justify-content: center;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 424px) {
|
@media (max-width: 424px) {
|
||||||
.mode-sidebar {
|
.mode-sidebar {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-around;
|
|
||||||
gap: 0;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0.35rem 0.25rem calc(0.35rem + env(safe-area-inset-bottom, 0px));
|
padding: 0.2rem 0.15rem calc(0.2rem + env(safe-area-inset-bottom, 0px));
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-top: 1px solid var(--ir-border);
|
border-top: 1px solid var(--ir-border);
|
||||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.35);
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-item {
|
.side-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.2rem;
|
height: auto;
|
||||||
padding: 0.55rem 0.25rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
border-color: transparent;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--ir-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: transparent;
|
|
||||||
border-color: transparent;
|
|
||||||
color: var(--ir-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-icon {
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-label {
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
max-width: 100%;
|
padding: 0.45rem 0.2rem !important;
|
||||||
overflow: hidden;
|
border: none !important;
|
||||||
text-overflow: ellipsis;
|
font-size: 0.65rem;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu .el-sub-menu__icon-arrow) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="result-display">
|
<div class="result-display">
|
||||||
<div class="result-label">Результат</div>
|
<div class="result-label">{{ t('result.label') }}</div>
|
||||||
<div v-if="hasResult" class="result-value">
|
<div v-if="hasResult" class="result-value">
|
||||||
<template v-if="Array.isArray(result)">
|
<template v-if="Array.isArray(result)">
|
||||||
<span v-for="(item, index) in result" :key="index" class="result-chip">
|
<span v-for="(item, index) in result" :key="index" class="result-chip">
|
||||||
@@ -11,11 +11,13 @@
|
|||||||
{{ result }}
|
{{ result }}
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="result-empty">Нажмите «Сгенерировать»</div>
|
<div v-else class="result-empty">{{ t('result.empty') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ResultDisplay',
|
name: 'ResultDisplay',
|
||||||
props: {
|
props: {
|
||||||
@@ -24,6 +26,10 @@ export default {
|
|||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasResult() {
|
hasResult() {
|
||||||
if (this.result === null || this.result === undefined || this.result === '') {
|
if (this.result === null || this.result === undefined || this.result === '') {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||||
<el-form-item label="Количество бросков">
|
<el-form-item :label="t('coin.flips')">
|
||||||
<el-input-number v-model="count" :min="1" :max="100" :controls="true" />
|
<el-input-number v-model="count" :min="1" :max="100" :controls="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<p class="hint">Каждый бросок даёт «орёл» или «решку».</p>
|
<p class="hint">{{ t('coin.hint') }}</p>
|
||||||
<el-button type="primary" native-type="submit" size="large">
|
<el-button type="primary" native-type="submit" size="large">
|
||||||
Сгенерировать
|
{{ t('coin.generate') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { flipCoin } from '@/utils/random'
|
import { flipCoin } from '@/utils/random'
|
||||||
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
||||||
|
|
||||||
@@ -23,6 +24,10 @@ const DEFAULTS = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'CoinGenerator',
|
name: 'CoinGenerator',
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return loadFormState(STORAGE_KEY, DEFAULTS)
|
return loadFormState(STORAGE_KEY, DEFAULTS)
|
||||||
},
|
},
|
||||||
@@ -37,10 +42,10 @@ export default {
|
|||||||
},
|
},
|
||||||
onGenerate() {
|
onGenerate() {
|
||||||
if (this.count < 1) {
|
if (this.count < 1) {
|
||||||
ElMessage.error('Количество должно быть не меньше 1')
|
ElMessage.error(this.t('coin.errCount'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const flips = flipCoin(this.count)
|
const flips = flipCoin(this.count).map((side) => this.t(`coin.${side}`))
|
||||||
this.$emit('result', this.count === 1 ? flips[0] : flips)
|
this.$emit('result', this.count === 1 ? flips[0] : flips)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||||
<el-form-item label="Список значений (по одному на строку)">
|
<el-form-item :label="t('list.values')">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="rawList"
|
v-model="rawList"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="8"
|
||||||
placeholder="яблоко банан вишня"
|
:placeholder="t('list.placeholder')"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Количество">
|
<el-form-item :label="t('list.count')">
|
||||||
<el-input-number v-model="count" :min="1" :controls="true" />
|
<el-input-number v-model="count" :min="1" :controls="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="unique">Только уникальные значения</el-checkbox>
|
<el-checkbox v-model="unique">{{ t('list.unique') }}</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" native-type="submit" size="large">
|
<el-button type="primary" native-type="submit" size="large">
|
||||||
Сгенерировать
|
{{ t('list.generate') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { generateFromList } from '@/utils/random'
|
import { generateFromList } from '@/utils/random'
|
||||||
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ const DEFAULTS = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'ListGenerator',
|
name: 'ListGenerator',
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return loadFormState(STORAGE_KEY, DEFAULTS)
|
return loadFormState(STORAGE_KEY, DEFAULTS)
|
||||||
},
|
},
|
||||||
@@ -61,17 +66,23 @@ export default {
|
|||||||
},
|
},
|
||||||
onGenerate() {
|
onGenerate() {
|
||||||
if (!this.items.length) {
|
if (!this.items.length) {
|
||||||
ElMessage.error('Список пуст — добавьте хотя бы одно значение')
|
ElMessage.error(this.t('list.errEmpty'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.count < 1) {
|
if (this.count < 1) {
|
||||||
ElMessage.error('Количество должно быть не меньше 1')
|
ElMessage.error(this.t('list.errCount'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.$emit('result', generateFromList(this.items, this.count, this.unique))
|
this.$emit('result', generateFromList(this.items, this.count, this.unique))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error(err.message)
|
const key =
|
||||||
|
err.code === 'LIST_UNIQUE_TOO_LARGE'
|
||||||
|
? 'list.errUniqueList'
|
||||||
|
: err.code === 'LIST_EMPTY'
|
||||||
|
? 'list.errEmpty'
|
||||||
|
: 'list.errCount'
|
||||||
|
ElMessage.error(this.t(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||||
<el-form-item label="Минимум">
|
<el-form-item :label="t('range.min')">
|
||||||
<el-input-number v-model="min" :controls="true" />
|
<el-input-number v-model="min" :controls="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Максимум">
|
<el-form-item :label="t('range.max')">
|
||||||
<el-input-number v-model="max" :controls="true" />
|
<el-input-number v-model="max" :controls="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Количество">
|
<el-form-item :label="t('range.count')">
|
||||||
<el-input-number v-model="count" :min="1" :controls="true" />
|
<el-input-number v-model="count" :min="1" :controls="true" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="unique">Только уникальные значения</el-checkbox>
|
<el-checkbox v-model="unique">{{ t('range.unique') }}</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" native-type="submit" size="large">
|
<el-button type="primary" native-type="submit" size="large">
|
||||||
Сгенерировать
|
{{ t('range.generate') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { generateRange } from '@/utils/random'
|
import { generateRange } from '@/utils/random'
|
||||||
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
import { loadFormState, saveFormState } from '@/utils/persistForm'
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ const DEFAULTS = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'RangeGenerator',
|
name: 'RangeGenerator',
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return loadFormState(STORAGE_KEY, DEFAULTS)
|
return loadFormState(STORAGE_KEY, DEFAULTS)
|
||||||
},
|
},
|
||||||
@@ -54,17 +59,18 @@ export default {
|
|||||||
},
|
},
|
||||||
onGenerate() {
|
onGenerate() {
|
||||||
if (this.min > this.max) {
|
if (this.min > this.max) {
|
||||||
ElMessage.error('Минимум не может быть больше максимума')
|
ElMessage.error(this.t('range.errMinMax'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.count < 1) {
|
if (this.count < 1) {
|
||||||
ElMessage.error('Количество должно быть не меньше 1')
|
ElMessage.error(this.t('range.errCount'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.$emit('result', generateRange(this.min, this.max, this.count, this.unique))
|
this.$emit('result', generateRange(this.min, this.max, this.count, this.unique))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error(err.message)
|
const key = err.code === 'RANGE_UNIQUE_TOO_LARGE' ? 'range.errUniqueRange' : 'range.errCount'
|
||||||
|
ElMessage.error(this.t(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const DEFAULTS = {
|
|||||||
title: 'irandom',
|
title: 'irandom',
|
||||||
brandUrl: 'https://git.imiheev.online/admin/irandom',
|
brandUrl: 'https://git.imiheev.online/admin/irandom',
|
||||||
logoUrl: '/logo.png',
|
logoUrl: '/logo.png',
|
||||||
|
defaultLocale: 'ru',
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#c9a66b',
|
primary: '#c9a66b',
|
||||||
accent: '#e6c566',
|
accent: '#e6c566',
|
||||||
@@ -26,6 +27,7 @@ export function getAppConfig() {
|
|||||||
title: runtime.title || DEFAULTS.title,
|
title: runtime.title || DEFAULTS.title,
|
||||||
brandUrl: runtime.brandUrl || DEFAULTS.brandUrl,
|
brandUrl: runtime.brandUrl || DEFAULTS.brandUrl,
|
||||||
logoUrl: runtime.logoUrl || DEFAULTS.logoUrl,
|
logoUrl: runtime.logoUrl || DEFAULTS.logoUrl,
|
||||||
|
defaultLocale: runtime.defaultLocale || DEFAULTS.defaultLocale,
|
||||||
colors
|
colors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import { getAppConfig } from '@/config/appConfig'
|
||||||
|
import ru from './locales/ru'
|
||||||
|
import en from './locales/en'
|
||||||
|
|
||||||
|
export const LOCALE_STORAGE_KEY = 'irandom:locale'
|
||||||
|
export const SUPPORTED_LOCALES = ['ru', 'en']
|
||||||
|
|
||||||
|
function normalizeLocale(value) {
|
||||||
|
const code = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.slice(0, 2)
|
||||||
|
return SUPPORTED_LOCALES.includes(code) ? code : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveInitialLocale() {
|
||||||
|
const stored = normalizeLocale(
|
||||||
|
typeof localStorage !== 'undefined' ? localStorage.getItem(LOCALE_STORAGE_KEY) : null
|
||||||
|
)
|
||||||
|
if (stored) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromEnv = normalizeLocale(getAppConfig().defaultLocale)
|
||||||
|
return fromEnv || 'ru'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistLocale(locale) {
|
||||||
|
const code = normalizeLocale(locale)
|
||||||
|
if (!code) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCALE_STORAGE_KEY, code)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAppLocale(i18n, locale) {
|
||||||
|
const code = normalizeLocale(locale)
|
||||||
|
if (!code) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i18n.global.locale.value = code
|
||||||
|
persistLocale(code)
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.lang = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: resolveInitialLocale(),
|
||||||
|
fallbackLocale: 'ru',
|
||||||
|
messages: { ru, en }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.lang = i18n.global.locale.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export default i18n
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
export default {
|
||||||
|
nav: {
|
||||||
|
range: 'Range',
|
||||||
|
list: 'List',
|
||||||
|
coin: 'Heads / Tails',
|
||||||
|
language: 'Language',
|
||||||
|
collapse: 'Collapse',
|
||||||
|
expand: 'Expand',
|
||||||
|
modes: 'Generation modes'
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
ru: 'Русский',
|
||||||
|
en: 'English'
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
title: 'Numbers from a range',
|
||||||
|
description: 'Generate several random integers within a chosen range.',
|
||||||
|
min: 'Minimum',
|
||||||
|
max: 'Maximum',
|
||||||
|
count: 'Count',
|
||||||
|
unique: 'Unique values only',
|
||||||
|
generate: 'Generate',
|
||||||
|
errMinMax: 'Minimum cannot be greater than maximum',
|
||||||
|
errCount: 'Count must be at least 1',
|
||||||
|
errUniqueRange: 'Unique count cannot exceed the size of the range'
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
title: 'Values from a list',
|
||||||
|
description: 'Enter a list and pick random items from it.',
|
||||||
|
values: 'List of values (one per line)',
|
||||||
|
placeholder: 'apple\nbanana\ncherry',
|
||||||
|
count: 'Count',
|
||||||
|
unique: 'Unique values only',
|
||||||
|
generate: 'Generate',
|
||||||
|
errEmpty: 'The list is empty — add at least one value',
|
||||||
|
errCount: 'Count must be at least 1',
|
||||||
|
errUniqueList: 'Unique count cannot exceed the list length'
|
||||||
|
},
|
||||||
|
coin: {
|
||||||
|
title: 'Heads / Tails',
|
||||||
|
description: 'Flip a coin once or several times.',
|
||||||
|
flips: 'Number of flips',
|
||||||
|
hint: 'Each flip is either heads or tails.',
|
||||||
|
generate: 'Generate',
|
||||||
|
errCount: 'Count must be at least 1',
|
||||||
|
heads: 'heads',
|
||||||
|
tails: 'tails'
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
label: 'Result',
|
||||||
|
empty: 'Press “Generate”'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
export default {
|
||||||
|
nav: {
|
||||||
|
range: 'Диапазон',
|
||||||
|
list: 'Список',
|
||||||
|
coin: 'Орёл / решка',
|
||||||
|
language: 'Язык',
|
||||||
|
collapse: 'Свернуть',
|
||||||
|
expand: 'Развернуть',
|
||||||
|
modes: 'Режимы генерации'
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
ru: 'Русский',
|
||||||
|
en: 'English'
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
title: 'Числа из диапазона',
|
||||||
|
description: 'Сгенерируйте несколько случайных чисел в заданном диапазоне.',
|
||||||
|
min: 'Минимум',
|
||||||
|
max: 'Максимум',
|
||||||
|
count: 'Количество',
|
||||||
|
unique: 'Только уникальные значения',
|
||||||
|
generate: 'Сгенерировать',
|
||||||
|
errMinMax: 'Минимум не может быть больше максимума',
|
||||||
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
|
errUniqueRange: 'Количество уникальных значений не может превышать размер диапазона'
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
title: 'Значения из списка',
|
||||||
|
description: 'Введите список и выберите случайные элементы из него.',
|
||||||
|
values: 'Список значений (по одному на строку)',
|
||||||
|
placeholder: 'яблоко\nбанан\nвишня',
|
||||||
|
count: 'Количество',
|
||||||
|
unique: 'Только уникальные значения',
|
||||||
|
generate: 'Сгенерировать',
|
||||||
|
errEmpty: 'Список пуст — добавьте хотя бы одно значение',
|
||||||
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
|
errUniqueList: 'Количество уникальных значений не может превышать длину списка'
|
||||||
|
},
|
||||||
|
coin: {
|
||||||
|
title: 'Орёл / решка',
|
||||||
|
description: 'Подбросьте монетку один или несколько раз.',
|
||||||
|
flips: 'Количество бросков',
|
||||||
|
hint: 'Каждый бросок даёт «орёл» или «решку».',
|
||||||
|
generate: 'Сгенерировать',
|
||||||
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
|
heads: 'орёл',
|
||||||
|
tails: 'решка'
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
label: 'Результат',
|
||||||
|
empty: 'Нажмите «Сгенерировать»'
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -4,6 +4,7 @@ import 'element-plus/dist/index.css'
|
|||||||
import './styles/theme.css'
|
import './styles/theme.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import i18n from './i18n'
|
||||||
import { getAppConfig } from './config/appConfig'
|
import { getAppConfig } from './config/appConfig'
|
||||||
import { applyTheme } from './config/applyTheme'
|
import { applyTheme } from './config/applyTheme'
|
||||||
|
|
||||||
@@ -11,4 +12,4 @@ const appConfig = getAppConfig()
|
|||||||
applyTheme(appConfig)
|
applyTheme(appConfig)
|
||||||
document.title = appConfig.title
|
document.title = appConfig.title
|
||||||
|
|
||||||
createApp(App).use(router).use(ElementPlus).mount('#app')
|
createApp(App).use(router).use(i18n).use(ElementPlus).mount('#app')
|
||||||
|
|||||||
+3
-15
@@ -6,29 +6,17 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/range',
|
path: '/range',
|
||||||
name: 'range',
|
name: 'range',
|
||||||
component: GeneratorView,
|
component: GeneratorView
|
||||||
meta: {
|
|
||||||
title: 'Числа из диапазона',
|
|
||||||
description: 'Сгенерируйте несколько случайных чисел в заданном диапазоне.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/list',
|
path: '/list',
|
||||||
name: 'list',
|
name: 'list',
|
||||||
component: GeneratorView,
|
component: GeneratorView
|
||||||
meta: {
|
|
||||||
title: 'Значения из списка',
|
|
||||||
description: 'Введите список и выберите случайные элементы из него.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/coin',
|
path: '/coin',
|
||||||
name: 'coin',
|
name: 'coin',
|
||||||
component: GeneratorView,
|
component: GeneratorView
|
||||||
meta: {
|
|
||||||
title: 'Орёл / решка',
|
|
||||||
description: 'Подбросьте монетку один или несколько раз.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ path: '/:pathMatch(.*)*', redirect: '/range' }
|
{ path: '/:pathMatch(.*)*', redirect: '/range' }
|
||||||
]
|
]
|
||||||
|
|||||||
+11
-5
@@ -14,6 +14,12 @@ export function generateNumber(min, max) {
|
|||||||
return randomInt(min, max)
|
return randomInt(min, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function codedError(code) {
|
||||||
|
const err = new Error(code)
|
||||||
|
err.code = code
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* N random integers in [min, max].
|
* N random integers in [min, max].
|
||||||
* When unique is true, values are distinct (count cannot exceed range size).
|
* When unique is true, values are distinct (count cannot exceed range size).
|
||||||
@@ -25,7 +31,7 @@ export function generateRange(min, max, count, unique = false) {
|
|||||||
|
|
||||||
if (unique) {
|
if (unique) {
|
||||||
if (count > span) {
|
if (count > span) {
|
||||||
throw new Error('Количество уникальных значений не может превышать размер диапазона')
|
throw codedError('RANGE_UNIQUE_TOO_LARGE')
|
||||||
}
|
}
|
||||||
const pool = Array.from({ length: span }, (_, i) => lo + i)
|
const pool = Array.from({ length: span }, (_, i) => lo + i)
|
||||||
for (let i = pool.length - 1; i > 0; i -= 1) {
|
for (let i = pool.length - 1; i > 0; i -= 1) {
|
||||||
@@ -44,12 +50,12 @@ export function generateRange(min, max, count, unique = false) {
|
|||||||
*/
|
*/
|
||||||
export function generateFromList(items, count, unique = false) {
|
export function generateFromList(items, count, unique = false) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
throw new Error('Список пуст')
|
throw codedError('LIST_EMPTY')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unique) {
|
if (unique) {
|
||||||
if (count > items.length) {
|
if (count > items.length) {
|
||||||
throw new Error('Количество уникальных значений не может превышать длину списка')
|
throw codedError('LIST_UNIQUE_TOO_LARGE')
|
||||||
}
|
}
|
||||||
const pool = [...items]
|
const pool = [...items]
|
||||||
for (let i = pool.length - 1; i > 0; i -= 1) {
|
for (let i = pool.length - 1; i > 0; i -= 1) {
|
||||||
@@ -63,8 +69,8 @@ export function generateFromList(items, count, unique = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coin flips: returns array of 'орёл' | 'решка'.
|
* Coin flips: returns array of 'heads' | 'tails'.
|
||||||
*/
|
*/
|
||||||
export function flipCoin(count = 1) {
|
export function flipCoin(count = 1) {
|
||||||
return Array.from({ length: count }, () => (Math.random() < 0.5 ? 'орёл' : 'решка'))
|
return Array.from({ length: count }, () => (Math.random() < 0.5 ? 'heads' : 'tails'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import ResultDisplay from '@/components/ResultDisplay.vue'
|
import ResultDisplay from '@/components/ResultDisplay.vue'
|
||||||
import RangeGenerator from '@/components/generators/RangeGenerator.vue'
|
import RangeGenerator from '@/components/generators/RangeGenerator.vue'
|
||||||
import ListGenerator from '@/components/generators/ListGenerator.vue'
|
import ListGenerator from '@/components/generators/ListGenerator.vue'
|
||||||
@@ -20,6 +21,12 @@ const GENERATORS = {
|
|||||||
coin: CoinGenerator
|
coin: CoinGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const META_KEYS = {
|
||||||
|
range: { title: 'range.title', description: 'range.description' },
|
||||||
|
list: { title: 'list.title', description: 'list.description' },
|
||||||
|
coin: { title: 'coin.title', description: 'coin.description' }
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GeneratorView',
|
name: 'GeneratorView',
|
||||||
components: {
|
components: {
|
||||||
@@ -28,6 +35,10 @@ export default {
|
|||||||
ListGenerator,
|
ListGenerator,
|
||||||
CoinGenerator
|
CoinGenerator
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
result: null
|
result: null
|
||||||
@@ -35,10 +46,12 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
return this.$route.meta.title ?? ''
|
const keys = META_KEYS[this.$route.name]
|
||||||
|
return keys ? this.t(keys.title) : ''
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.$route.meta.description ?? ''
|
const keys = META_KEYS[this.$route.name]
|
||||||
|
return keys ? this.t(keys.description) : ''
|
||||||
},
|
},
|
||||||
generatorComponent() {
|
generatorComponent() {
|
||||||
return GENERATORS[this.$route.name] ?? RangeGenerator
|
return GENERATORS[this.$route.name] ?? RangeGenerator
|
||||||
|
|||||||
Reference in New Issue
Block a user