This commit is contained in:
2026-07-20 22:49:45 +07:00
parent c314e4f329
commit db558c87d1
24 changed files with 1464 additions and 1362 deletions
+2 -4
View File
@@ -3,11 +3,9 @@ import HeaderMenu from '@/components/HeaderMenu.vue'
</script>
<template>
<HeaderMenu :add-logout="true" active="games" />
<HeaderMenu :add-logout="true" active="games" />
<div class="center-block-custom content-block">
Игры
</div>
<div class="center-block-custom content-block">Игры</div>
</template>
<style scoped></style>
+108 -98
View File
@@ -1,155 +1,165 @@
<script setup lang="ts">
const props = defineProps({
addLogin: {
type: Boolean,
required: false
},
addLogout: {
type: Boolean,
required: false
},
isFoundKey: {
type: Boolean,
required: false
},
active: {
type: String,
required: false
}
addLogin: {
type: Boolean,
required: false,
},
addLogout: {
type: Boolean,
required: false,
},
isFoundKey: {
type: Boolean,
required: false,
},
active: {
type: String,
required: false,
},
})
import { DoorOpen } from '@vicons/fa'
import { Key24Regular } from '@vicons/fluent'
import { DoorFrontFilled } from '@vicons/material'
import { Icon } from '@vicons/utils'
import { useMessage } from 'naive-ui';
import { useMessage } from 'naive-ui'
import { NIcon } from 'naive-ui'
import { ref } from 'vue';
import { ref } from 'vue'
import { h } from 'vue'
import { useRouter } from 'vue-router';
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client';
import { useAuthStore } from '@/stores/auth';
import { getAuthClient } from '@/api/auth_client'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore();
const authStore = useAuthStore()
authStore.initFromStorage()
const router = useRouter()
const client = getAuthClient();
const client = getAuthClient()
const message = useMessage()
const permissions = ref<string[]>([]);
const permissions = ref<string[]>([])
async function getPermissions() {
permissions.value = []
const res = await client.GetPermissions({})
permissions.value = res.permissions!
permissions.value = []
const res = await client.GetPermissions({})
permissions.value = res.permissions!
}
getPermissions()
function hasPermission(permission: string): boolean {
return permissions.value.includes(permission)
return permissions.value.includes(permission)
}
const goToLogin = () => {
if (props.isFoundKey) {
router.push('/login')
return
}
message.create(
"Дверь закрыта, куда же подевался ключ?",
{
icon: () => h(NIcon, null, { default: () => h(Key24Regular) })
}
)
if (props.isFoundKey) {
router.push('/login')
return
}
message.create('Дверь закрыта, куда же подевался ключ?', {
icon: () => h(NIcon, null, { default: () => h(Key24Regular) }),
})
}
const goToLogout = () => {
authStore.logout()
router.push('/login')
authStore.logout()
router.push('/login')
}
</script>
<template>
<div class="menu-block">
<div class="left-group">
<div class="menu-item-block" :class="{ active: props.active == 'office' }" v-if="authStore.hasRole('user')"
@click="router.push('/office')">
Кабинет
</div>
<div class="menu-item-block" :class="{ active: props.active == 'games' }" v-if="hasPermission('games_page')"
@click="router.push('/games')">
Игры
</div>
<div class="menu-item-block" :class="{ active: props.active == 'scenarios' }"
v-if="hasPermission('scenarios_page')" @click="router.push('/scenarios')">
Мои сценарии
</div>
<div class="menu-item-block" :class="{ active: props.active == 'users' }" v-if="hasPermission('users_page')"
@click="router.push('/users')">
Пользователи
</div>
</div>
<div class="right-group">
<div v-if="props.addLogin" class="menu-item-block right" @click="goToLogin">
<Icon v-if="isFoundKey" class="door-icon">
<DoorOpen />
</Icon>
<Icon v-else class="door-icon">
<DoorFrontFilled />
</Icon>
Войти
</div>
<div v-if="props.addLogout" class="menu-item-block right" @click="goToLogout">
Выйти
</div>
</div>
<div class="menu-block">
<div class="left-group">
<div
class="menu-item-block"
:class="{ active: props.active == 'office' }"
v-if="authStore.hasRole('user')"
@click="router.push('/office')"
>
Кабинет
</div>
<div
class="menu-item-block"
:class="{ active: props.active == 'games' }"
v-if="hasPermission('games_page')"
@click="router.push('/games')"
>
Игры
</div>
<div
class="menu-item-block"
:class="{ active: props.active == 'scenarios' }"
v-if="hasPermission('scenarios_page')"
@click="router.push('/scenarios')"
>
Мои сценарии
</div>
<div
class="menu-item-block"
:class="{ active: props.active == 'users' }"
v-if="hasPermission('users_page')"
@click="router.push('/users')"
>
Пользователи
</div>
</div>
<div class="right-group">
<div v-if="props.addLogin" class="menu-item-block right" @click="goToLogin">
<Icon v-if="isFoundKey" class="door-icon">
<DoorOpen />
</Icon>
<Icon v-else class="door-icon">
<DoorFrontFilled />
</Icon>
Войти
</div>
<div v-if="props.addLogout" class="menu-item-block right" @click="goToLogout">Выйти</div>
</div>
</div>
</template>
<style scoped>
.menu-block {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 70px;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 70px;
/* background-color: brown; */
/* background-color: brown; */
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-item-block {
display: inline-block;
margin-left: 10px;
cursor: pointer;
padding: 5px;
font-size: 1.1em;
display: inline-block;
margin-left: 10px;
cursor: pointer;
padding: 5px;
font-size: 1.1em;
}
.menu-item-block:hover {
color: #63e2b7;
opacity: 0.8;
color: #63e2b7;
opacity: 0.8;
}
.right {
margin-left: auto;
margin-left: auto;
}
.door-icon {
float: left;
width: 25px;
padding-top: 3px;
padding-right: 5px;
float: left;
width: 25px;
padding-top: 3px;
padding-right: 5px;
}
.active {
color: #63e2b7;
color: #63e2b7;
}
</style>
+48 -30
View File
@@ -1,11 +1,21 @@
<script setup lang="ts">
import { NAutoComplete, NButton, NCard, NCheckbox, NInput, NSpace, NTabPane, NTabs, useMessage } from 'naive-ui'
import {
NAutoComplete,
NButton,
NCard,
NCheckbox,
NInput,
NSpace,
NTabPane,
NTabs,
useMessage,
} from 'naive-ui'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router';
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth';
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore();
const authStore = useAuthStore()
const router = useRouter()
@@ -16,13 +26,12 @@ const email = ref('')
const password = ref('')
const approval = ref(false)
const options = computed(() => {
return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => {
const prefix = email.value.split('@')[0]
return {
label: prefix + suffix,
value: prefix + suffix
value: prefix + suffix,
}
})
})
@@ -47,7 +56,7 @@ async function signup() {
message.error(authStore.error)
return
}
message.info("Пароль отправлен на почту")
message.info('Пароль отправлен на почту')
}
</script>
@@ -55,24 +64,25 @@ async function signup() {
<div class="center-block-custom content-block center-middle-block-custom height80">
<n-card style="margin-bottom: 16px" class="sign-card">
<n-tabs type="line" animated>
<n-tab-pane name="signin" tab="Вход">
<n-space vertical>
<div class="form-label">Почта</div>
<n-auto-complete v-model:value="email" :input-props="{
autocomplete: 'disabled',
}" :options="options" placeholder="detective@mail.ru" clearable />
<n-auto-complete
v-model:value="email"
:input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="detective@mail.ru"
clearable
/>
<div class="form-label">Пароль</div>
<n-input v-model:value="password" type="password" placeholder="********" />
<div class="form-button-wrapper">
<div class="form-label">
<n-button @click="signin" :disabled="email.length == 0 || password.length == 0">
<span v-if="!authStore.isLoading">
Вход
</span>
<span v-else>
Подождите...
</span>
<span v-if="!authStore.isLoading"> Вход </span>
<span v-else> Подождите... </span>
</n-button>
</div>
</div>
@@ -84,32 +94,40 @@ async function signup() {
<div class="form-label">Позывной</div>
<n-input v-model:value="username" type="text" placeholder="Шерлок" />
<div class="form-label">Почта</div>
<n-auto-complete v-model:value="email" :input-props="{
autocomplete: 'disabled',
}" :options="options" placeholder="detective@mail.ru" clearable />
<n-auto-complete
v-model:value="email"
:input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="detective@mail.ru"
clearable
/>
<div class="form-label">
<n-checkbox v-model:checked="approval">
Я согласен с
<a href="/user-agreement" target="_blank" class="docs-link">пользовательским соглашением</a><br>
<a href="/user-agreement" target="_blank" class="docs-link"
>пользовательским соглашением</a
><br />
и
<a href="/privacy-policy" target="_blank" class="docs-link">соглашением о персональных данных</a>
<a href="/privacy-policy" target="_blank" class="docs-link"
>соглашением о персональных данных</a
>
</n-checkbox>
</div>
<div class="form-button-wrapper">
<div class="form-label">
<n-button @click="signup" :disabled="username.length == 0 || password.length == 0 || !approval">
<span v-if="!authStore.isLoading">
Регистрация
</span>
<span v-else>
Подождите...
</span>
<n-button
@click="signup"
:disabled="username.length == 0 || password.length == 0 || !approval"
>
<span v-if="!authStore.isLoading"> Регистрация </span>
<span v-else> Подождите... </span>
</n-button>
</div>
</div>
</n-space>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
+16 -18
View File
@@ -1,37 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref } from 'vue'
import { getAuthClient } from '@/api/auth_client';
import { getAuthClient } from '@/api/auth_client'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useAuthStore } from '@/stores/auth';
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore();
const client = getAuthClient();
const authStore = useAuthStore()
const client = getAuthClient()
const permissions = ref<string[]>([])
async function getPermissions() {
permissions.value = []
const res = await client.GetPermissions({})
permissions.value = res.permissions!
permissions.value = []
const res = await client.GetPermissions({})
permissions.value = res.permissions!
}
getPermissions()
</script>
<template>
<HeaderMenu :add-logout="true" active="office" />
<HeaderMenu :add-logout="true" active="office" />
<div class="center-block-custom content-block">
<p>
Доброго времени суток, {{ authStore.username }}.
</p>
<p>
{{ authStore.userRoles }}
</p>
<div class="center-block-custom content-block">
<p>Доброго времени суток, {{ authStore.username }}.</p>
<p>
{{ authStore.userRoles }}
</p>
{{ permissions }}
</div>
{{ permissions }}
</div>
</template>
<style scoped></style>
+118 -105
View File
@@ -1,163 +1,176 @@
<script setup lang="ts">
import { NDynamicInput, NInput, NInputGroup, NSpace, NSwitch } from 'naive-ui'
import type { PropType } from 'vue';
import type { PropType } from 'vue'
import type { Application, Door, Key, Place } from '@/api/generated/crabs/evening_detective_server';
import type { Application, Door, Key, Place } from '@/api/generated/crabs/evening_detective_server'
const props = defineProps({
place: {
type: Object as PropType<Place>,
required: true
},
mode: {
type: String,
required: true
}
place: {
type: Object as PropType<Place>,
required: true,
},
mode: {
type: String,
required: true,
},
})
const emit = defineEmits<{ 'update:mode': [value: string] }>()
const updateMode = (mode: string) => {
console.log(mode)
emit('update:mode', mode)
console.log(mode)
emit('update:mode', mode)
}
function onCreateApplication(): Application {
return {
name: undefined,
image: undefined
}
return {
name: undefined,
image: undefined,
}
}
function onCreateDoor(): Door {
return {
code: undefined,
name: undefined,
keys: undefined
}
return {
code: undefined,
name: undefined,
keys: undefined,
}
}
function onCreateKey(): Key {
return {
name: undefined
}
return {
name: undefined,
}
}
</script>
<template>
<div v-if="mode == 'mini-add'" class="place-block-plus show-editor-block" @click="updateMode('edit')">
<p class="place-image-plus">+</p>
</div>
<div
v-if="mode == 'mini-add'"
class="place-block-plus show-editor-block"
@click="updateMode('edit')"
>
<p class="place-image-plus">+</p>
</div>
<div v-if="mode == 'show-editor'" class="place-block show-editor-block" @click="updateMode('edit')">
<p>[{{ props.place.code }}] - {{ props.place.name }}</p>
<div class="message-content">{{ props.place.text }}</div>
<p v-for="application in props.place.applications" v-bind:key="application.name">{{ application.name }}</p>
</div>
<div
v-if="mode == 'show-editor'"
class="place-block show-editor-block"
@click="updateMode('edit')"
>
<p>[{{ props.place.code }}] - {{ props.place.name }}</p>
<div class="message-content">{{ props.place.text }}</div>
<p v-for="application in props.place.applications" v-bind:key="application.name">
{{ application.name }}
</p>
</div>
<div v-if="mode == 'edit'" class="place-block">
<n-space vertical>
<n-input-group>
<n-input :style="{ width: '20%' }" v-model:value="props.place.code" type="text" placeholder='Ы-1' />
<n-input :style="{ width: '80%' }" v-model:value="props.place.name" type="text" placeholder='Дом №Ы' />
</n-input-group>
<n-input v-model:value="props.place.text" type="textarea" :autosize="{ minRows: 3 }"
placeholder='В начале было слово...' />
<!-- <n-upload multiple directory-dnd :max="1" v-model:file-list="newPlaceFileList">
<div v-if="mode == 'edit'" class="place-block">
<n-space vertical>
<n-input-group>
<n-input
:style="{ width: '20%' }"
v-model:value="props.place.code"
type="text"
placeholder="Ы-1"
/>
<n-input
:style="{ width: '80%' }"
v-model:value="props.place.name"
type="text"
placeholder="Дом №Ы"
/>
</n-input-group>
<n-input
v-model:value="props.place.text"
type="textarea"
:autosize="{ minRows: 3 }"
placeholder="В начале было слово..."
/>
<!-- <n-upload multiple directory-dnd :max="1" v-model:file-list="newPlaceFileList">
<n-upload-dragger>
<n-text style="font-size: 16px">
Щелкните или перетащите файл в эту область для загрузки
</n-text>
</n-upload-dragger>
</n-upload> -->
<n-switch v-model:value="props.place.hidden" />
<n-switch v-model:value="props.place.hidden" />
<n-dynamic-input v-model:value="props.place.applications" :on-create="onCreateApplication">
<template #create-button-default>
Добавить улику
</template>
<template #default="{ value }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="value.name" type="text" />
</div>
</template>
<n-dynamic-input v-model:value="props.place.applications" :on-create="onCreateApplication">
<template #create-button-default> Добавить улику </template>
<template #default="{ value }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="value.name" type="text" />
</div>
</template>
</n-dynamic-input>
<n-dynamic-input v-model:value="props.place.doors" :on-create="onCreateDoor">
<template #create-button-default> Добавить действие </template>
<template #default="{ value: door }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="door.code" type="text" placeholder="Код" />
<n-input v-model:value="door.name" type="text" placeholder="Название" />
<n-dynamic-input v-model:value="door.keys" :on-create="onCreateKey">
<template #create-button-default> Добавить ключ </template>
<template #default="{ value: key }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="key.name" type="text" placeholder="Название" />
</div>
</template>
</n-dynamic-input>
</div>
</template>
</n-dynamic-input>
<n-dynamic-input v-model:value="props.place.doors" :on-create="onCreateDoor">
<template #create-button-default>
Добавить действие
</template>
<template #default="{ value: door }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="door.code" type="text" placeholder="Код" />
<n-input v-model:value="door.name" type="text" placeholder="Название" />
<n-dynamic-input v-model:value="props.place.keys" :on-create="onCreateKey">
<template #create-button-default> Добавить ключ </template>
<template #default="{ value: key }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="key.name" type="text" placeholder="Название" />
</div>
</template>
</n-dynamic-input>
<n-dynamic-input v-model:value="door.keys" :on-create="onCreateKey">
<template #create-button-default>
Добавить ключ
</template>
<template #default="{ value: key }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="key.name" type="text" placeholder="Название" />
</div>
</template>
</n-dynamic-input>
</div>
</template>
</n-dynamic-input>
<n-dynamic-input v-model:value="props.place.keys" :on-create="onCreateKey">
<template #create-button-default>
Добавить ключ
</template>
<template #default="{ value: key }">
<div style="display: flex; align-items: center; width: 100%">
<n-input v-model:value="key.name" type="text" placeholder="Название" />
</div>
</template>
</n-dynamic-input>
<slot></slot>
</n-space>
</div>
<slot></slot>
</n-space>
</div>
</template>
<style scoped>
.place-block {
margin: 20px;
padding: 20px;
border-radius: 10px;
border: 1px solid #444;
margin: 20px;
padding: 20px;
border-radius: 10px;
border: 1px solid #444;
}
.place-block:hover {
color: #63e2b7;
color: #63e2b7;
}
.place-block-plus {
margin: 20px;
border-radius: 10px;
border: 1px solid #444;
margin: 20px;
border-radius: 10px;
border: 1px solid #444;
}
.place-block-plus:hover {
color: #63e2b7;
color: #63e2b7;
}
.place-image-plus {
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.show-editor-block {
cursor: pointer;
cursor: pointer;
}
.message-content {
white-space: pre-wrap;
white-space: pre-wrap;
}
</style>
+236 -218
View File
@@ -1,18 +1,18 @@
<script setup lang="ts">
import { Settings } from '@vicons/carbon'
import { Icon } from '@vicons/utils'
import { NButton, type UploadFileInfo,useMessage } from 'naive-ui';
import { NCard,NFlex, NInput, NModal, NSpace, NText, NUpload, NUploadDragger } from 'naive-ui'
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { NButton, type UploadFileInfo, useMessage } from 'naive-ui'
import { NCard, NFlex, NInput, NModal, NSpace, NText, NUpload, NUploadDragger } from 'naive-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { getAuthClient } from '@/api/auth_client';
import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server';
import { getAuthClient } from '@/api/auth_client'
import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
import PlaceBlock from '@/components/PlaceBlock.vue';
import router from '@/router';
import PlaceBlock from '@/components/PlaceBlock.vue'
import router from '@/router'
const client = getAuthClient();
const client = getAuthClient()
const route = useRoute()
const scenarioId = route.params.id
@@ -24,16 +24,100 @@ const message = useMessage()
const fileList = ref<UploadFileInfo[]>([])
const scenario = ref<Scenario>({
id: undefined,
name: undefined,
story: undefined,
author: undefined,
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined
id: undefined,
name: undefined,
story: undefined,
author: undefined,
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
})
const newPlace = ref<Place>({
code: undefined,
name: undefined,
text: undefined,
image: undefined,
hidden: undefined,
applications: undefined,
doors: undefined,
keys: undefined,
mode: 'mini-add',
})
async function getScenario(id: number) {
const res = await client.GetScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
scenario.value = res.scenario!
scenario.value.story?.places?.forEach((item) => {
item.oldCode = item.code
item.mode = 'show-editor'
})
}
getScenario(Number(scenarioId))
async function deleteScenario(id: number) {
const res = await client.DeleteScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
router.push('/scenarios')
}
async function updateScenario() {
const file = fileList.value[0].file
if (!file) return
// Читаем как ArrayBuffer
const arrayBuffer = await file.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
const resUploadFile = await client.UploadFile({
filename: 'scenarios_' + scenario.value.id + '_' + file.name,
data: uint8ToBase64(bytes),
})
if (resUploadFile.error != '') {
message.error(resUploadFile.error!)
return
}
const res = await client.UpdateScenario({
id: scenario.value.id,
name: scenario.value.name,
description: scenario.value.description,
image: resUploadFile.filename,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
showSettingsScenarioModal.value = false
}
function uint8ToBase64(uint8Array: Uint8Array) {
// Преобразуем Uint8Array в двоичную строку
const binaryString = String.fromCharCode(...uint8Array)
// Кодируем в Base64
return btoa(binaryString)
}
async function addPlace() {
const res = await client.AddScenarioPlace({
id: scenario.value.id,
place: newPlace.value,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
newPlace.value = {
code: undefined,
name: undefined,
text: undefined,
@@ -42,262 +126,196 @@ const newPlace = ref<Place>({
applications: undefined,
doors: undefined,
keys: undefined,
mode: 'mini-add'
})
async function getScenario(id: number) {
const res = await client.GetScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
scenario.value = res.scenario!
scenario.value.story?.places?.forEach((item) => {
item.oldCode = item.code
item.mode = 'show-editor'
})
}
getScenario(Number(scenarioId))
async function deleteScenario(id: number) {
const res = await client.DeleteScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
router.push('/scenarios')
}
async function updateScenario() {
const file = fileList.value[0].file
if (!file) return
// Читаем как ArrayBuffer
const arrayBuffer = await file.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
const resUploadFile = await client.UploadFile({
filename: 'scenarios_' + scenario.value.id + '_' + file.name,
data: uint8ToBase64(bytes)
})
if (resUploadFile.error != '') {
message.error(resUploadFile.error!)
return
}
const res = await client.UpdateScenario({
id: scenario.value.id,
name: scenario.value.name,
description: scenario.value.description,
image: resUploadFile.filename,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
showSettingsScenarioModal.value = false
}
function uint8ToBase64(uint8Array: Uint8Array) {
// Преобразуем Uint8Array в двоичную строку
const binaryString = String.fromCharCode(...uint8Array);
// Кодируем в Base64
return btoa(binaryString);
}
async function addPlace() {
const res = await client.AddScenarioPlace({
id: scenario.value.id,
place: newPlace.value
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
newPlace.value = {
code: undefined,
name: undefined,
text: undefined,
image: undefined,
hidden: undefined,
applications: undefined,
doors: undefined,
keys: undefined
}
}
}
async function collapseAddPlace(place: Place) {
place.mode = 'mini-add'
place.mode = 'mini-add'
}
async function cancelUpdatePlace(place: Place) {
await getScenario(Number(scenarioId))
await getScenario(Number(scenarioId))
place.mode = 'show-editor'
place.mode = 'show-editor'
}
async function updatePlace(place: Place) {
const res = await client.UpdateScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
place: place
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
const res = await client.UpdateScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
place: place,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
place.mode = 'show-editor'
place.mode = 'show-editor'
}
async function deletePlace(place: Place) {
const res = await client.DeleteScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
const res = await client.DeleteScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
}
</script>
<template>
<HeaderMenu :add-logout="true" active="scenarios" />
<HeaderMenu :add-logout="true" active="scenarios" />
<div class="center-block-custom-big content-block">
<div class="places-container">
<div class="settings-block">
<n-button quaternary @click="showSettingsScenarioModal = true" class="settings-button">
<Icon class="settings-icon">
<Settings />
</Icon>
Настройки
</n-button>
</div>
<div class="center-block-custom-big content-block">
<div class="places-container">
<div class="settings-block">
<n-button quaternary @click="showSettingsScenarioModal = true" class="settings-button">
<Icon class="settings-icon">
<Settings />
</Icon>
Настройки
</n-button>
</div>
<PlaceBlock :place="newPlace" v-model:mode="newPlace.mode">
<n-flex justify="end">
<n-button @click="collapseAddPlace(newPlace)">Отмена</n-button>
<n-button @click="addPlace()" :disabled="!newPlace.code">Добавить точку</n-button>
</n-flex>
</PlaceBlock>
<PlaceBlock :place="newPlace" v-model:mode="newPlace.mode">
<n-flex justify="end">
<n-button @click="collapseAddPlace(newPlace)">Отмена</n-button>
<n-button @click="addPlace()" :disabled="!newPlace.code">Добавить точку</n-button>
</n-flex>
</PlaceBlock>
<PlaceBlock :place="place" v-model:mode="place.mode" v-for="place in scenario.story?.places" v-bind:key="place.code">
<n-flex justify="end">
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button>
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button>
<n-button @click="updatePlace(place)">Сохранить</n-button>
</n-flex>
</PlaceBlock>
</div>
<PlaceBlock
:place="place"
v-model:mode="place.mode"
v-for="place in scenario.story?.places"
v-bind:key="place.code"
>
<n-flex justify="end">
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button>
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button>
<n-button @click="updatePlace(place)">Сохранить</n-button>
</n-flex>
</PlaceBlock>
</div>
</div>
<!-- Окно настроек сценария -->
<n-modal v-model:show="showSettingsScenarioModal">
<n-card style="width: 600px" title="Настройки сценария" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showSettingsScenarioModal = false">
x
</n-button>
</template>
<n-space vertical>
<p>
Название
</p>
<n-input v-model:value="scenario.name" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<!-- Окно настроек сценария -->
<n-modal v-model:show="showSettingsScenarioModal">
<n-card
style="width: 600px"
title="Настройки сценария"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="showSettingsScenarioModal = false"> x </n-button>
</template>
<n-space vertical>
<p>Название</p>
<n-input
v-model:value="scenario.name"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<p class="settings-header">
Описание
</p>
<n-input v-model:value="scenario.description" type="textarea" placeholder='Убийство пастуха...' />
<p class="settings-header">Описание</p>
<n-input
v-model:value="scenario.description"
type="textarea"
placeholder="Убийство пастуха..."
/>
<p class="settings-header">
Картинка
</p>
<n-input v-model:value="scenario.image" type="text" placeholder='default_scenario.png' disabled />
<n-upload multiple directory-dnd :max="1" v-model:file-list="fileList">
<n-upload-dragger>
<n-text style="font-size: 16px">
Щелкните или перетащите файл в эту область для загрузки
</n-text>
</n-upload-dragger>
</n-upload>
<p class="settings-header">Картинка</p>
<n-input
v-model:value="scenario.image"
type="text"
placeholder="default_scenario.png"
disabled
/>
<n-upload multiple directory-dnd :max="1" v-model:file-list="fileList">
<n-upload-dragger>
<n-text style="font-size: 16px">
Щелкните или перетащите файл в эту область для загрузки
</n-text>
</n-upload-dragger>
</n-upload>
<n-button @click="updateScenario()" ghost>
Сохранить изменения
</n-button>
<n-button @click="updateScenario()" ghost> Сохранить изменения </n-button>
<hr class="settings-hr">
<hr class="settings-hr" />
<p>
Введите название сценария <n-text class="name-text" strong>{{ scenario.name }}</n-text> чтобы
удалить
его
</p>
<n-input v-model:value="deletedScenarioName" type="text"
placeholder='Дело №0 "Следствие ведут овечки"' />
<n-button @click="deleteScenario(scenario.id!)" type="error" ghost
:disabled="deletedScenarioName != scenario.name">
Удалить сценарий
</n-button>
</n-space>
<template #footer>
</template>
</n-card>
</n-modal>
<p>
Введите название сценария
<n-text class="name-text" strong>{{ scenario.name }}</n-text> чтобы удалить его
</p>
<n-input
v-model:value="deletedScenarioName"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<n-button
@click="deleteScenario(scenario.id!)"
type="error"
ghost
:disabled="deletedScenarioName != scenario.name"
>
Удалить сценарий
</n-button>
</n-space>
<template #footer> </template>
</n-card>
</n-modal>
</template>
<style scoped>
.settings-block {
height: 40px;
height: 40px;
/* background-color: brown; */
/* background-color: brown; */
}
.settings-button {
float: right;
float: right;
}
.settings-icon {
width: 20px;
width: 20px;
}
.name-text {
padding: 3px;
border-radius: 3px;
background-color: rgb(255 255 255 / 10%);
padding: 3px;
border-radius: 3px;
background-color: rgb(255 255 255 / 10%);
}
.settings-header {
margin-top: 20px;
margin-top: 20px;
}
.settings-hr {
margin: 20px 0;
margin: 20px 0;
}
.places-container {
display: flex;
flex-flow: column nowrap;
overflow-y: auto;
padding: 20px 0;
height: calc(100vh - 150px);
display: flex;
flex-flow: column nowrap;
overflow-y: auto;
padding: 20px 0;
height: calc(100vh - 150px);
/* border: 1px solid red; */
/* border: 1px solid red; */
}
@media (width >= 1024px) {
.places-container {
padding: 20px 250px;
height: calc(100vh - 70px);
}
.places-container {
padding: 20px 250px;
height: calc(100vh - 70px);
}
}
</style>
+96 -87
View File
@@ -1,15 +1,14 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui';
import { NButton, NCard, NInput,NModal } from 'naive-ui'
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui'
import { NButton, NCard, NInput, NModal } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client';
import type { Scenario } from '@/api/generated/crabs/evening_detective_server';
import { getAuthClient } from '@/api/auth_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient();
const client = getAuthClient()
const scenarios = ref<Scenario[]>([])
@@ -21,132 +20,142 @@ const showAddScenarioModal = ref(false)
const newScenarioName = ref('')
async function getScenarios() {
scenarios.value = []
const res = await client.GetMyScenarios({})
scenarios.value = res.scenarios!
scenarios.value = []
const res = await client.GetMyScenarios({})
scenarios.value = res.scenarios!
}
async function addScenario(name: string) {
const res = await client.AddScenario({ name: name })
if (res.error != '') {
message.error(res.error!)
return
}
await toScenarioEditor(res.id!)
const res = await client.AddScenario({ name: name })
if (res.error != '') {
message.error(res.error!)
return
}
await toScenarioEditor(res.id!)
}
async function toScenarioEditor(id: number) {
router.push('/scenarios/' + id)
router.push('/scenarios/' + id)
}
getScenarios()
</script>
<template>
<HeaderMenu :add-logout="true" active="scenarios" />
<HeaderMenu :add-logout="true" active="scenarios" />
<div class="center-block-custom-big content-block">
<div class="scenarios-container">
<div class="scenario-block" @click="showAddScenarioModal = true">
<div class="scenario-image-block scenario-image-plus">+</div>
<p class="scenario-title">Создать новый сценарий</p>
</div>
<div class="scenario-block" v-for="scenario in scenarios" @click="toScenarioEditor(scenario.id!)" v-bind:key="scenario.id">
<div class="scenario-image-block scenario-image"
:style="{ backgroundImage: `url(${scenario.image})` }"></div>
<p class="scenario-title">{{ scenario.name }}</p>
</div>
</div>
<div class="center-block-custom-big content-block">
<div class="scenarios-container">
<div class="scenario-block" @click="showAddScenarioModal = true">
<div class="scenario-image-block scenario-image-plus">+</div>
<p class="scenario-title">Создать новый сценарий</p>
</div>
<div
class="scenario-block"
v-for="scenario in scenarios"
@click="toScenarioEditor(scenario.id!)"
v-bind:key="scenario.id"
>
<div
class="scenario-image-block scenario-image"
:style="{ backgroundImage: `url(${scenario.image})` }"
></div>
<p class="scenario-title">{{ scenario.name }}</p>
</div>
</div>
</div>
<!-- Окно создания сценария -->
<n-modal v-model:show="showAddScenarioModal">
<n-card style="width: 600px" title="Создание сценария" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showAddScenarioModal = false">
x
</n-button>
</template>
<n-input v-model:value="newScenarioName" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<template #footer>
<n-button @click="addScenario(newScenarioName)" :disabled="newScenarioName.length == 0">
Создать
</n-button>
</template>
</n-card>
</n-modal>
<!-- Окно создания сценария -->
<n-modal v-model:show="showAddScenarioModal">
<n-card
style="width: 600px"
title="Создание сценария"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="showAddScenarioModal = false"> x </n-button>
</template>
<n-input
v-model:value="newScenarioName"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<template #footer>
<n-button @click="addScenario(newScenarioName)" :disabled="newScenarioName.length == 0">
Создать
</n-button>
</template>
</n-card>
</n-modal>
</template>
<style scoped>
.scenarios-container {
display: flex;
flex-flow: row wrap;
justify-content: center;
overflow-y: auto;
padding: 40px 0;
height: calc(100vh - 150px);
display: flex;
flex-flow: row wrap;
justify-content: center;
overflow-y: auto;
padding: 40px 0;
height: calc(100vh - 150px);
/* border: 1px solid red; */
/* border: 1px solid red; */
}
.scenario-block {
margin: 10px;
width: 150px;
cursor: pointer;
margin: 10px;
width: 150px;
cursor: pointer;
/* background-color: #553333; */
/* background-color: #553333; */
}
.scenario-block:hover {
color: #63e2b7;
color: #63e2b7;
}
.scenario-image-block {
height: 150px;
border-radius: 10px;
height: 150px;
border-radius: 10px;
}
.scenario-image-block:hover {
color: #63e2b7;
border: 2px solid #63e2b7;
color: #63e2b7;
border: 2px solid #63e2b7;
}
.scenario-image-plus {
font-size: 120px;
display: flex;
justify-content: center;
align-items: center;
background-color: #333;
font-size: 120px;
display: flex;
justify-content: center;
align-items: center;
background-color: #333;
}
.scenario-image {
background-size: cover;
background-position: center;
background-size: cover;
background-position: center;
}
.scenario-title {
padding: 5px 0;
padding: 5px 0;
}
@media (width >= 1024px) {
.scenarios-container {
height: calc(100vh - 70px);
}
.scenarios-container {
height: calc(100vh - 70px);
}
.scenario-block {
width: 250px;
}
.scenario-block {
width: 250px;
}
.scenario-image-block {
height: 250px;
border-radius: 10px;
}
.scenario-image-block {
height: 250px;
border-radius: 10px;
}
}
</style>
+2 -4
View File
@@ -3,11 +3,9 @@ import HeaderMenu from '@/components/HeaderMenu.vue'
</script>
<template>
<HeaderMenu :add-logout="true" active="users" />
<HeaderMenu :add-logout="true" active="users" />
<div class="center-block-custom content-block">
Пользователи
</div>
<div class="center-block-custom content-block">Пользователи</div>
</template>
<style scoped></style>