Сделал фронтенд
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-container class="app-shell">
|
||||
<el-aside width="220px" class="aside desktop-aside">
|
||||
<ModeSidebar show-brand />
|
||||
</el-aside>
|
||||
<el-container class="main-column">
|
||||
<el-header height="auto" class="header mobile-header">
|
||||
<AppHeader />
|
||||
</el-header>
|
||||
<el-main class="main">
|
||||
<RouterView />
|
||||
</el-main>
|
||||
</el-container>
|
||||
<nav class="mobile-toolbar" aria-label="Режимы генерации">
|
||||
<ModeSidebar />
|
||||
</nav>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import ModeSidebar from './components/ModeSidebar.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AppHeader,
|
||||
ModeSidebar
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-shell {
|
||||
height: 100%;
|
||||
background: var(--ir-bg);
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: var(--ir-surface);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
.mode-sidebar {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0;
|
||||
background: var(--ir-surface);
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 1.5rem 2rem 2.5rem;
|
||||
background: var(--ir-bg);
|
||||
}
|
||||
|
||||
.mobile-toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 424px) {
|
||||
.app-shell {
|
||||
flex-direction: column !important;
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
.desktop-aside {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: block;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 1rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.mobile-toolbar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: var(--ir-surface);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<a
|
||||
class="brand"
|
||||
href="https://git.imiheev.online/admin/irandom"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Gitea"
|
||||
>
|
||||
<img class="logo" src="/logo.png" alt="irandom" />
|
||||
<span class="brand-name">irandom</span>
|
||||
</a>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppHeader'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--ir-surface);
|
||||
border-bottom: 1px solid var(--ir-border);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ir-primary);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.brand:hover .brand-name {
|
||||
color: var(--ir-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<nav class="mode-sidebar" aria-label="Режимы генерации">
|
||||
<a
|
||||
v-if="showBrand"
|
||||
class="brand"
|
||||
href="https://git.imiheev.online/admin/irandom"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Gitea"
|
||||
>
|
||||
<img class="logo" src="/logo.png" alt="irandom" />
|
||||
<span class="brand-name">irandom</span>
|
||||
</a>
|
||||
<RouterLink
|
||||
v-for="mode in modes"
|
||||
:key="mode.id"
|
||||
:to="mode.to"
|
||||
class="mode-item"
|
||||
active-class="active"
|
||||
>
|
||||
<el-icon class="mode-icon" :size="22">
|
||||
<component :is="mode.icon" />
|
||||
</el-icon>
|
||||
<span class="mode-label">{{ mode.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Histogram, List, Coin } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'ModeSidebar',
|
||||
components: {
|
||||
Histogram,
|
||||
List,
|
||||
Coin
|
||||
},
|
||||
props: {
|
||||
showBrand: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modes: [
|
||||
{ id: 'range', to: '/range', label: 'Диапазон', icon: 'Histogram' },
|
||||
{ id: 'list', to: '/list', label: 'Список', icon: 'List' },
|
||||
{ id: 'coin', to: '/coin', label: 'Орёл / решка', icon: 'Coin' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mode-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
background: var(--ir-surface);
|
||||
border-right: 1px solid var(--ir-border);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.35rem 0.5rem 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--ir-border);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ir-primary);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.brand:hover .brand-name {
|
||||
color: var(--ir-accent);
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--ir-text-muted);
|
||||
padding: 0.85rem 0.75rem;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
font: inherit;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--ir-surface-raised);
|
||||
color: var(--ir-text);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(201, 166, 107, 0.18);
|
||||
border-color: var(--ir-primary);
|
||||
color: var(--ir-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 424px) {
|
||||
.mode-sidebar {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 0;
|
||||
height: auto;
|
||||
padding: 0.35rem 0.25rem calc(0.35rem + env(safe-area-inset-bottom, 0px));
|
||||
border-right: none;
|
||||
border-top: 1px solid var(--ir-border);
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
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-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="result-display">
|
||||
<div class="result-label">Результат</div>
|
||||
<div v-if="hasResult" class="result-value">
|
||||
<template v-if="Array.isArray(result)">
|
||||
<span v-for="(item, index) in result" :key="index" class="result-chip">
|
||||
{{ item }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ result }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="result-empty">Нажмите «Сгенерировать»</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResultDisplay',
|
||||
props: {
|
||||
result: {
|
||||
type: [Number, String, Array],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasResult() {
|
||||
if (this.result === null || this.result === undefined || this.result === '') {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(this.result)) {
|
||||
return this.result.length > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-display {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: var(--ir-surface);
|
||||
border: 1px solid var(--ir-border);
|
||||
border-radius: 12px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ir-text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--ir-accent);
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.result-chip {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.65rem;
|
||||
background: rgba(230, 197, 102, 0.12);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.result-empty {
|
||||
color: var(--ir-text-muted);
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||
<el-form-item label="Количество бросков">
|
||||
<el-input-number v-model="count" :min="1" :max="100" :controls="true" />
|
||||
</el-form-item>
|
||||
<p class="hint">Каждый бросок даёт «орёл» или «решку».</p>
|
||||
<el-button type="primary" native-type="submit" size="large">
|
||||
Сгенерировать
|
||||
</el-button>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { flipCoin } from '@/utils/random'
|
||||
|
||||
export default {
|
||||
name: 'CoinGenerator',
|
||||
emits: ['result'],
|
||||
data() {
|
||||
return {
|
||||
count: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onGenerate() {
|
||||
if (this.count < 1) {
|
||||
ElMessage.error('Количество должно быть не меньше 1')
|
||||
return
|
||||
}
|
||||
const flips = flipCoin(this.count)
|
||||
this.$emit('result', this.count === 1 ? flips[0] : flips)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.generator-form {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0 0 1.25rem;
|
||||
color: var(--ir-text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||
<el-form-item label="Список значений (по одному на строку)">
|
||||
<el-input
|
||||
v-model="rawList"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="яблоко банан вишня"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Количество">
|
||||
<el-input-number v-model="count" :min="1" :controls="true" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="unique">Только уникальные значения</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-button type="primary" native-type="submit" size="large">
|
||||
Сгенерировать
|
||||
</el-button>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { generateFromList } from '@/utils/random'
|
||||
|
||||
export default {
|
||||
name: 'ListGenerator',
|
||||
emits: ['result'],
|
||||
data() {
|
||||
return {
|
||||
rawList: 'яблоко\nбанан\nвишня\nгруша\nдыня',
|
||||
count: 1,
|
||||
unique: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items() {
|
||||
return this.rawList
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onGenerate() {
|
||||
if (!this.items.length) {
|
||||
ElMessage.error('Список пуст — добавьте хотя бы одно значение')
|
||||
return
|
||||
}
|
||||
if (this.count < 1) {
|
||||
ElMessage.error('Количество должно быть не меньше 1')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.$emit('result', generateFromList(this.items, this.count, this.unique))
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.generator-form {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-form label-position="top" class="generator-form" @submit.prevent="onGenerate">
|
||||
<el-form-item label="Минимум">
|
||||
<el-input-number v-model="min" :controls="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Максимум">
|
||||
<el-input-number v-model="max" :controls="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Количество">
|
||||
<el-input-number v-model="count" :min="1" :controls="true" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="unique">Только уникальные значения</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-button type="primary" native-type="submit" size="large">
|
||||
Сгенерировать
|
||||
</el-button>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { generateRange } from '@/utils/random'
|
||||
|
||||
export default {
|
||||
name: 'RangeGenerator',
|
||||
emits: ['result'],
|
||||
data() {
|
||||
return {
|
||||
min: 1,
|
||||
max: 100,
|
||||
count: 5,
|
||||
unique: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onGenerate() {
|
||||
if (this.min > this.max) {
|
||||
ElMessage.error('Минимум не может быть больше максимума')
|
||||
return
|
||||
}
|
||||
if (this.count < 1) {
|
||||
ElMessage.error('Количество должно быть не меньше 1')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.$emit('result', generateRange(this.min, this.max, this.count, this.unique))
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.generator-form {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './styles/theme.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).use(ElementPlus).mount('#app')
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import GeneratorView from '@/views/GeneratorView.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/range' },
|
||||
{
|
||||
path: '/range',
|
||||
name: 'range',
|
||||
component: GeneratorView,
|
||||
meta: {
|
||||
title: 'Числа из диапазона',
|
||||
description: 'Сгенерируйте несколько случайных чисел в заданном диапазоне.'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: GeneratorView,
|
||||
meta: {
|
||||
title: 'Значения из списка',
|
||||
description: 'Введите список и выберите случайные элементы из него.'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/coin',
|
||||
name: 'coin',
|
||||
component: GeneratorView,
|
||||
meta: {
|
||||
title: 'Орёл / решка',
|
||||
description: 'Подбросьте монетку один или несколько раз.'
|
||||
}
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/range' }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,54 @@
|
||||
:root {
|
||||
--ir-primary: #c9a66b;
|
||||
--ir-accent: #e6c566;
|
||||
--ir-bg: #1a1815;
|
||||
--ir-surface: #2e2820;
|
||||
--ir-surface-raised: #3a3228;
|
||||
--ir-text: #ffffff;
|
||||
--ir-text-muted: #c8c0b4;
|
||||
--ir-danger: #ee5c5c;
|
||||
--ir-link: #4a6a8a;
|
||||
--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-danger: var(--ir-danger);
|
||||
--el-bg-color: var(--ir-surface);
|
||||
--el-bg-color-overlay: var(--ir-surface-raised);
|
||||
--el-text-color-primary: var(--ir-text);
|
||||
--el-text-color-regular: var(--ir-text-muted);
|
||||
--el-text-color-secondary: var(--ir-text-muted);
|
||||
--el-border-color: var(--ir-border);
|
||||
--el-border-color-light: var(--ir-border);
|
||||
--el-fill-color-blank: var(--ir-surface);
|
||||
--el-fill-color-light: var(--ir-surface-raised);
|
||||
--el-mask-color: rgba(26, 24, 21, 0.8);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: var(--ir-bg);
|
||||
color: var(--ir-text);
|
||||
font-family: 'Segoe UI', 'Candara', 'Trebuchet MS', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Inclusive random integer in [min, max].
|
||||
*/
|
||||
export function randomInt(min, max) {
|
||||
const lo = Math.ceil(min)
|
||||
const hi = Math.floor(max)
|
||||
return Math.floor(Math.random() * (hi - lo + 1)) + lo
|
||||
}
|
||||
|
||||
/**
|
||||
* One random integer in [min, max].
|
||||
*/
|
||||
export function generateNumber(min, max) {
|
||||
return randomInt(min, max)
|
||||
}
|
||||
|
||||
/**
|
||||
* N random integers in [min, max].
|
||||
* When unique is true, values are distinct (count cannot exceed range size).
|
||||
*/
|
||||
export function generateRange(min, max, count, unique = false) {
|
||||
const lo = Math.ceil(min)
|
||||
const hi = Math.floor(max)
|
||||
const span = hi - lo + 1
|
||||
|
||||
if (unique) {
|
||||
if (count > span) {
|
||||
throw new Error('Количество уникальных значений не может превышать размер диапазона')
|
||||
}
|
||||
const pool = Array.from({ length: span }, (_, i) => lo + i)
|
||||
for (let i = pool.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[pool[i], pool[j]] = [pool[j], pool[i]]
|
||||
}
|
||||
return pool.slice(0, count)
|
||||
}
|
||||
|
||||
return Array.from({ length: count }, () => randomInt(lo, hi))
|
||||
}
|
||||
|
||||
/**
|
||||
* N random picks from items (with replacement).
|
||||
* When unique is true, picks are distinct (count cannot exceed list length).
|
||||
*/
|
||||
export function generateFromList(items, count, unique = false) {
|
||||
if (!items.length) {
|
||||
throw new Error('Список пуст')
|
||||
}
|
||||
|
||||
if (unique) {
|
||||
if (count > items.length) {
|
||||
throw new Error('Количество уникальных значений не может превышать длину списка')
|
||||
}
|
||||
const pool = [...items]
|
||||
for (let i = pool.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[pool[i], pool[j]] = [pool[j], pool[i]]
|
||||
}
|
||||
return pool.slice(0, count)
|
||||
}
|
||||
|
||||
return Array.from({ length: count }, () => items[randomInt(0, items.length - 1)])
|
||||
}
|
||||
|
||||
/**
|
||||
* Coin flips: returns array of 'орёл' | 'решка'.
|
||||
*/
|
||||
export function flipCoin(count = 1) {
|
||||
return Array.from({ length: count }, () => (Math.random() < 0.5 ? 'орёл' : 'решка'))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="generator-view">
|
||||
<h1 class="page-title">{{ title }}</h1>
|
||||
<p class="page-desc">{{ description }}</p>
|
||||
|
||||
<component :is="generatorComponent" @result="onResult" />
|
||||
<ResultDisplay :result="result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ResultDisplay from '@/components/ResultDisplay.vue'
|
||||
import RangeGenerator from '@/components/generators/RangeGenerator.vue'
|
||||
import ListGenerator from '@/components/generators/ListGenerator.vue'
|
||||
import CoinGenerator from '@/components/generators/CoinGenerator.vue'
|
||||
|
||||
const GENERATORS = {
|
||||
range: RangeGenerator,
|
||||
list: ListGenerator,
|
||||
coin: CoinGenerator
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'GeneratorView',
|
||||
components: {
|
||||
ResultDisplay,
|
||||
RangeGenerator,
|
||||
ListGenerator,
|
||||
CoinGenerator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
result: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.$route.meta.title ?? ''
|
||||
},
|
||||
description() {
|
||||
return this.$route.meta.description ?? ''
|
||||
},
|
||||
generatorComponent() {
|
||||
return GENERATORS[this.$route.name] ?? RangeGenerator
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.name'() {
|
||||
this.result = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onResult(value) {
|
||||
this.result = value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--ir-text);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 0 0 1.5rem;
|
||||
color: var(--ir-text-muted);
|
||||
max-width: 40rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user