Сделал копирование, почистил репо, перешёл на версию 1.0.0
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# irandom
|
# irandom
|
||||||
|
|
||||||
|
Сервис генерации случайных значений: числа из диапазона, выбор из списка, орёл/решка.
|
||||||
|
Vue 3 + Vite + Element Plus. Тема и брендинг настраиваются через env / Docker.
|
||||||
|
|
||||||
|
**Версия:** `1.0.0`
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
@@ -9,18 +14,10 @@ npm install
|
|||||||
```
|
```
|
||||||
npm run serve
|
npm run serve
|
||||||
```
|
```
|
||||||
or
|
|
||||||
```
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production build
|
### Production build
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
```
|
|
||||||
|
|
||||||
### Preview production build
|
|
||||||
```
|
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -29,43 +26,17 @@ npm run preview
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customization
|
## Customization
|
||||||
Copy `.env.example` to `.env` and set branding / theme variables.
|
Copy `.env.example` to `.env` and set `IRANDOM_*` variables
|
||||||
`npm run serve` / `npm run build` regenerates `public/config.js` automatically.
|
(name, title, logo URL, colors, default locale).
|
||||||
|
`npm run serve` / `npm run build` regenerates `public/config.js`.
|
||||||
### Docker
|
|
||||||
Build and run (config via environment, same keys as `.env.example`):
|
|
||||||
|
|
||||||
|
## Docker
|
||||||
```
|
```
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
→ http://localhost:8080
|
||||||
|
|
||||||
Open http://localhost:8080
|
## Offline (PWA)
|
||||||
|
After one online visit to a production build (`build` + `preview` or Docker),
|
||||||
Change branding without rebuilding the image — only restart with new env:
|
the app shell works offline. Runtime branding updates title, theme-color, icons and manifest.
|
||||||
|
|
||||||
```
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Or:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker build -t irandom .
|
|
||||||
docker run --rm -p 8080:80 ^
|
|
||||||
-e IRANDOM_NAME=LuckyPenguin ^
|
|
||||||
-e IRANDOM_COLOR_PRIMARY=#3DDC97 ^
|
|
||||||
irandom
|
|
||||||
```
|
|
||||||
|
|
||||||
### Offline (PWA)
|
|
||||||
Production build registers a service worker and caches the app shell.
|
|
||||||
After opening the site once online:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
you can reload without network — generators keep working.
|
|
||||||
`config.js` uses NetworkFirst (fresh when online, cached when offline).
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
irandom:
|
irandom:
|
||||||
build: .
|
build: .
|
||||||
image: irandom:latest
|
image: irandom:1.0.0
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "irandom",
|
"name": "irandom",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate-config": "node scripts/generate-config.js",
|
"generate-config": "node scripts/generate-config.js",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="generator-actions">
|
||||||
|
<el-button type="primary" native-type="submit" size="large">
|
||||||
|
<el-icon class="btn-icon"><RefreshRight /></el-icon>
|
||||||
|
{{ t('common.generate') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="large" :disabled="!hasResult" @click.prevent="$emit('copy')">
|
||||||
|
<el-icon class="btn-icon"><DocumentCopy /></el-icon>
|
||||||
|
{{ t('common.copy') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { RefreshRight, DocumentCopy } from '@element-plus/icons-vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GeneratorActions',
|
||||||
|
components: {
|
||||||
|
RefreshRight,
|
||||||
|
DocumentCopy
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
hasResult: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['copy'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return { t }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.generator-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 0.35rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,9 +4,7 @@
|
|||||||
<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">{{ t('coin.hint') }}</p>
|
<p class="hint">{{ t('coin.hint') }}</p>
|
||||||
<el-button type="primary" native-type="submit" size="large">
|
<GeneratorActions :has-result="hasResult" @copy="onCopy" />
|
||||||
{{ t('coin.generate') }}
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -15,6 +13,8 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { useI18n } from 'vue-i18n'
|
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'
|
||||||
|
import { copyText, formatResultForCopy } from '@/utils/copyResult'
|
||||||
|
import GeneratorActions from '@/components/GeneratorActions.vue'
|
||||||
|
|
||||||
const STORAGE_KEY = 'coin'
|
const STORAGE_KEY = 'coin'
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
@@ -23,6 +23,13 @@ const DEFAULTS = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CoinGenerator',
|
name: 'CoinGenerator',
|
||||||
|
components: { GeneratorActions },
|
||||||
|
props: {
|
||||||
|
result: {
|
||||||
|
type: [Number, String, Array],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
setup() {
|
setup() {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -31,6 +38,14 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return loadFormState(STORAGE_KEY, DEFAULTS)
|
return loadFormState(STORAGE_KEY, DEFAULTS)
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
hasResult() {
|
||||||
|
if (this.result === null || this.result === undefined || this.result === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Array.isArray(this.result) ? this.result.length > 0 : true
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
count: 'persist'
|
count: 'persist'
|
||||||
},
|
},
|
||||||
@@ -47,6 +62,14 @@ export default {
|
|||||||
}
|
}
|
||||||
const flips = flipCoin(this.count).map((side) => this.t(`coin.${side}`))
|
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)
|
||||||
|
},
|
||||||
|
async onCopy() {
|
||||||
|
try {
|
||||||
|
await copyText(formatResultForCopy(this.result))
|
||||||
|
ElMessage.success(this.t('common.copied'))
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(this.t('common.copyFailed'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +77,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.generator-form {
|
.generator-form {
|
||||||
max-width: 420px;
|
max-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
|
|||||||
@@ -14,9 +14,7 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="unique">{{ t('list.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">
|
<GeneratorActions :has-result="hasResult" @copy="onCopy" />
|
||||||
{{ t('list.generate') }}
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,6 +23,8 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { useI18n } from 'vue-i18n'
|
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'
|
||||||
|
import { copyText, formatResultForCopy } from '@/utils/copyResult'
|
||||||
|
import GeneratorActions from '@/components/GeneratorActions.vue'
|
||||||
|
|
||||||
const STORAGE_KEY = 'list'
|
const STORAGE_KEY = 'list'
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
@@ -35,6 +35,13 @@ const DEFAULTS = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListGenerator',
|
name: 'ListGenerator',
|
||||||
|
components: { GeneratorActions },
|
||||||
|
props: {
|
||||||
|
result: {
|
||||||
|
type: [Number, String, Array],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
setup() {
|
setup() {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -49,6 +56,12 @@ export default {
|
|||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
hasResult() {
|
||||||
|
if (this.result === null || this.result === undefined || this.result === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Array.isArray(this.result) ? this.result.length > 0 : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -84,6 +97,14 @@ export default {
|
|||||||
: 'list.errCount'
|
: 'list.errCount'
|
||||||
ElMessage.error(this.t(key))
|
ElMessage.error(this.t(key))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async onCopy() {
|
||||||
|
try {
|
||||||
|
await copyText(formatResultForCopy(this.result))
|
||||||
|
ElMessage.success(this.t('common.copied'))
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(this.t('common.copyFailed'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,7 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="unique">{{ t('range.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">
|
<GeneratorActions :has-result="hasResult" @copy="onCopy" />
|
||||||
{{ t('range.generate') }}
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,6 +21,8 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { useI18n } from 'vue-i18n'
|
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'
|
||||||
|
import { copyText, formatResultForCopy } from '@/utils/copyResult'
|
||||||
|
import GeneratorActions from '@/components/GeneratorActions.vue'
|
||||||
|
|
||||||
const STORAGE_KEY = 'range'
|
const STORAGE_KEY = 'range'
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
@@ -34,6 +34,13 @@ const DEFAULTS = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RangeGenerator',
|
name: 'RangeGenerator',
|
||||||
|
components: { GeneratorActions },
|
||||||
|
props: {
|
||||||
|
result: {
|
||||||
|
type: [Number, String, Array],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
emits: ['result'],
|
emits: ['result'],
|
||||||
setup() {
|
setup() {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -42,6 +49,14 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return loadFormState(STORAGE_KEY, DEFAULTS)
|
return loadFormState(STORAGE_KEY, DEFAULTS)
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
hasResult() {
|
||||||
|
if (this.result === null || this.result === undefined || this.result === '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Array.isArray(this.result) ? this.result.length > 0 : true
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
min: 'persist',
|
min: 'persist',
|
||||||
max: 'persist',
|
max: 'persist',
|
||||||
@@ -72,6 +87,14 @@ export default {
|
|||||||
const key = err.code === 'RANGE_UNIQUE_TOO_LARGE' ? 'range.errUniqueRange' : 'range.errCount'
|
const key = err.code === 'RANGE_UNIQUE_TOO_LARGE' ? 'range.errUniqueRange' : 'range.errCount'
|
||||||
ElMessage.error(this.t(key))
|
ElMessage.error(this.t(key))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async onCopy() {
|
||||||
|
try {
|
||||||
|
await copyText(formatResultForCopy(this.result))
|
||||||
|
ElMessage.success(this.t('common.copied'))
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(this.t('common.copyFailed'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +102,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.generator-form {
|
.generator-form {
|
||||||
max-width: 420px;
|
max-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-input-number) {
|
:deep(.el-input-number) {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { getAppConfig } from './appConfig'
|
||||||
|
|
||||||
|
function setMeta(name, content) {
|
||||||
|
if (!content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let el = document.querySelector(`meta[name="${name}"]`)
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('meta')
|
||||||
|
el.setAttribute('name', name)
|
||||||
|
document.head.appendChild(el)
|
||||||
|
}
|
||||||
|
el.setAttribute('content', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLink(rel, href) {
|
||||||
|
if (!href) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let el = document.querySelector(`link[rel="${rel}"]`)
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('link')
|
||||||
|
el.setAttribute('rel', rel)
|
||||||
|
document.head.appendChild(el)
|
||||||
|
}
|
||||||
|
el.setAttribute('href', href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDynamicManifest(config) {
|
||||||
|
const manifest = {
|
||||||
|
name: config.name,
|
||||||
|
short_name: config.name,
|
||||||
|
description: config.title || config.name,
|
||||||
|
theme_color: config.colors.bg,
|
||||||
|
background_color: config.colors.bg,
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'any',
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
lang: config.defaultLocale || 'ru',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: config.logoUrl,
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: config.logoUrl,
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(manifest)], {
|
||||||
|
type: 'application/manifest+json'
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
let link = document.querySelector('link[rel="manifest"]')
|
||||||
|
if (!link) {
|
||||||
|
link = document.createElement('link')
|
||||||
|
link.rel = 'manifest'
|
||||||
|
document.head.appendChild(link)
|
||||||
|
} else if (link.dataset.irandomBlob) {
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}
|
||||||
|
link.href = url
|
||||||
|
link.dataset.irandomBlob = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync document title, theme-color, icons and web manifest with runtime config.
|
||||||
|
*/
|
||||||
|
export function applyBrandingMeta(config = getAppConfig()) {
|
||||||
|
document.title = config.title || config.name
|
||||||
|
setMeta('theme-color', config.colors.bg)
|
||||||
|
setMeta('description', config.title || config.name)
|
||||||
|
setLink('icon', config.logoUrl)
|
||||||
|
setLink('apple-touch-icon', config.logoUrl)
|
||||||
|
applyDynamicManifest(config)
|
||||||
|
}
|
||||||
@@ -12,6 +12,12 @@ export default {
|
|||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
en: 'English'
|
en: 'English'
|
||||||
},
|
},
|
||||||
|
common: {
|
||||||
|
generate: 'Generate',
|
||||||
|
copy: 'Copy',
|
||||||
|
copied: 'Copied',
|
||||||
|
copyFailed: 'Failed to copy'
|
||||||
|
},
|
||||||
range: {
|
range: {
|
||||||
title: 'Numbers from a range',
|
title: 'Numbers from a range',
|
||||||
description: 'Generate several random integers within a chosen range.',
|
description: 'Generate several random integers within a chosen range.',
|
||||||
@@ -19,7 +25,6 @@ export default {
|
|||||||
max: 'Maximum',
|
max: 'Maximum',
|
||||||
count: 'Count',
|
count: 'Count',
|
||||||
unique: 'Unique values only',
|
unique: 'Unique values only',
|
||||||
generate: 'Generate',
|
|
||||||
errMinMax: 'Minimum cannot be greater than maximum',
|
errMinMax: 'Minimum cannot be greater than maximum',
|
||||||
errCount: 'Count must be at least 1',
|
errCount: 'Count must be at least 1',
|
||||||
errUniqueRange: 'Unique count cannot exceed the size of the range'
|
errUniqueRange: 'Unique count cannot exceed the size of the range'
|
||||||
@@ -31,7 +36,6 @@ export default {
|
|||||||
placeholder: 'apple\nbanana\ncherry',
|
placeholder: 'apple\nbanana\ncherry',
|
||||||
count: 'Count',
|
count: 'Count',
|
||||||
unique: 'Unique values only',
|
unique: 'Unique values only',
|
||||||
generate: 'Generate',
|
|
||||||
errEmpty: 'The list is empty — add at least one value',
|
errEmpty: 'The list is empty — add at least one value',
|
||||||
errCount: 'Count must be at least 1',
|
errCount: 'Count must be at least 1',
|
||||||
errUniqueList: 'Unique count cannot exceed the list length'
|
errUniqueList: 'Unique count cannot exceed the list length'
|
||||||
@@ -41,7 +45,6 @@ export default {
|
|||||||
description: 'Flip a coin once or several times.',
|
description: 'Flip a coin once or several times.',
|
||||||
flips: 'Number of flips',
|
flips: 'Number of flips',
|
||||||
hint: 'Each flip is either heads or tails.',
|
hint: 'Each flip is either heads or tails.',
|
||||||
generate: 'Generate',
|
|
||||||
errCount: 'Count must be at least 1',
|
errCount: 'Count must be at least 1',
|
||||||
heads: 'heads',
|
heads: 'heads',
|
||||||
tails: 'tails'
|
tails: 'tails'
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export default {
|
|||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
en: 'English'
|
en: 'English'
|
||||||
},
|
},
|
||||||
|
common: {
|
||||||
|
generate: 'Сгенерировать',
|
||||||
|
copy: 'Копировать',
|
||||||
|
copied: 'Скопировано',
|
||||||
|
copyFailed: 'Не удалось скопировать'
|
||||||
|
},
|
||||||
range: {
|
range: {
|
||||||
title: 'Числа из диапазона',
|
title: 'Числа из диапазона',
|
||||||
description: 'Сгенерируйте несколько случайных чисел в заданном диапазоне.',
|
description: 'Сгенерируйте несколько случайных чисел в заданном диапазоне.',
|
||||||
@@ -19,7 +25,6 @@ export default {
|
|||||||
max: 'Максимум',
|
max: 'Максимум',
|
||||||
count: 'Количество',
|
count: 'Количество',
|
||||||
unique: 'Только уникальные значения',
|
unique: 'Только уникальные значения',
|
||||||
generate: 'Сгенерировать',
|
|
||||||
errMinMax: 'Минимум не может быть больше максимума',
|
errMinMax: 'Минимум не может быть больше максимума',
|
||||||
errCount: 'Количество должно быть не меньше 1',
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
errUniqueRange: 'Количество уникальных значений не может превышать размер диапазона'
|
errUniqueRange: 'Количество уникальных значений не может превышать размер диапазона'
|
||||||
@@ -31,7 +36,6 @@ export default {
|
|||||||
placeholder: 'яблоко\nбанан\nвишня',
|
placeholder: 'яблоко\nбанан\nвишня',
|
||||||
count: 'Количество',
|
count: 'Количество',
|
||||||
unique: 'Только уникальные значения',
|
unique: 'Только уникальные значения',
|
||||||
generate: 'Сгенерировать',
|
|
||||||
errEmpty: 'Список пуст — добавьте хотя бы одно значение',
|
errEmpty: 'Список пуст — добавьте хотя бы одно значение',
|
||||||
errCount: 'Количество должно быть не меньше 1',
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
errUniqueList: 'Количество уникальных значений не может превышать длину списка'
|
errUniqueList: 'Количество уникальных значений не может превышать длину списка'
|
||||||
@@ -41,7 +45,6 @@ export default {
|
|||||||
description: 'Подбросьте монетку один или несколько раз.',
|
description: 'Подбросьте монетку один или несколько раз.',
|
||||||
flips: 'Количество бросков',
|
flips: 'Количество бросков',
|
||||||
hint: 'Каждый бросок даёт «орёл» или «решку».',
|
hint: 'Каждый бросок даёт «орёл» или «решку».',
|
||||||
generate: 'Сгенерировать',
|
|
||||||
errCount: 'Количество должно быть не меньше 1',
|
errCount: 'Количество должно быть не меньше 1',
|
||||||
heads: 'орёл',
|
heads: 'орёл',
|
||||||
tails: 'решка'
|
tails: 'решка'
|
||||||
|
|||||||
+2
-1
@@ -7,12 +7,13 @@ import router from './router'
|
|||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
import { getAppConfig } from './config/appConfig'
|
import { getAppConfig } from './config/appConfig'
|
||||||
import { applyTheme } from './config/applyTheme'
|
import { applyTheme } from './config/applyTheme'
|
||||||
|
import { applyBrandingMeta } from './config/applyBrandingMeta'
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
|
||||||
registerSW({ immediate: true })
|
registerSW({ immediate: true })
|
||||||
|
|
||||||
const appConfig = getAppConfig()
|
const appConfig = getAppConfig()
|
||||||
applyTheme(appConfig)
|
applyTheme(appConfig)
|
||||||
document.title = appConfig.title
|
applyBrandingMeta(appConfig)
|
||||||
|
|
||||||
createApp(App).use(router).use(i18n).use(ElementPlus).mount('#app')
|
createApp(App).use(router).use(i18n).use(ElementPlus).mount('#app')
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export function formatResultForCopy(result) {
|
||||||
|
if (result === null || result === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result.join(', ')
|
||||||
|
}
|
||||||
|
return String(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyText(text) {
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('EMPTY')
|
||||||
|
}
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const area = document.createElement('textarea')
|
||||||
|
area.value = text
|
||||||
|
area.setAttribute('readonly', '')
|
||||||
|
area.style.position = 'fixed'
|
||||||
|
area.style.opacity = '0'
|
||||||
|
document.body.appendChild(area)
|
||||||
|
area.select()
|
||||||
|
const ok = document.execCommand('copy')
|
||||||
|
document.body.removeChild(area)
|
||||||
|
if (!ok) {
|
||||||
|
throw new Error('COPY_FAILED')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
<h1 class="page-title">{{ title }}</h1>
|
<h1 class="page-title">{{ title }}</h1>
|
||||||
<p class="page-desc">{{ description }}</p>
|
<p class="page-desc">{{ description }}</p>
|
||||||
|
|
||||||
<component :is="generatorComponent" @result="onResult" />
|
<component
|
||||||
|
:is="generatorComponent"
|
||||||
|
:result="result"
|
||||||
|
@result="onResult"
|
||||||
|
/>
|
||||||
<ResultDisplay :result="result" />
|
<ResultDisplay :result="result" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// Runtime branding (name / theme / logo) is applied in applyBrandingMeta.js
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,webmanifest}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,webmanifest}'],
|
||||||
navigateFallback: '/index.html',
|
navigateFallback: '/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user