Сделал фронтенд
This commit is contained in:
+20
-8
@@ -1,11 +1,23 @@
|
|||||||
# ---> Vue
|
.DS_Store
|
||||||
# gitignore template for Vue.js projects
|
/node_modules
|
||||||
#
|
/dist
|
||||||
# Recommended template: Node.gitignore
|
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
|
||||||
docs/_book
|
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
# local env files
|
||||||
test/
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
# irandom
|
# irandom
|
||||||
|
|
||||||
Проект для работы со случайностью
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+12960
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "irandom",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"element-plus": "^2.14.3",
|
||||||
|
"vue": "^3.2.13",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.16",
|
||||||
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||||
|
"@vue/cli-service": "~5.0.0",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"sass": "^1.32.7",
|
||||||
|
"sass-loader": "^12.0.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@babel/eslint-parser"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie 11"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>logo.png">
|
||||||
|
<title>irandom — простой генератор случайностей</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
+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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const { defineConfig } = require('@vue/cli-service')
|
||||||
|
module.exports = defineConfig({
|
||||||
|
transpileDependencies: true,
|
||||||
|
pages: {
|
||||||
|
index: {
|
||||||
|
entry: 'src/main.js',
|
||||||
|
title: 'irandom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user