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
View File
@@ -6,11 +6,9 @@ import { RouterView } from 'vue-router'
<template> <template>
<n-config-provider :theme="darkTheme"> <n-config-provider :theme="darkTheme">
<n-message-provider> <n-message-provider>
<RouterView /> <RouterView />
</n-message-provider> </n-message-provider>
</n-config-provider> </n-config-provider>
</template> </template>
+23 -27
View File
@@ -1,39 +1,35 @@
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from '@/stores/auth'
import { createEveningDetectiveServerClient, type EveningDetectiveServer } from "./generated/crabs/evening_detective_server"; import {
import { createHttpHandler } from "./generated/crabs/evening_detective_server/client"; createEveningDetectiveServerClient,
type EveningDetectiveServer,
} from './generated/crabs/evening_detective_server'
import { createHttpHandler } from './generated/crabs/evening_detective_server/client'
export function getAuthClient() { export function getAuthClient() {
const authStore = useAuthStore(); const authStore = useAuthStore()
return buildAuthClient(authStore.refreshTokenAction, authStore.getToken) return buildAuthClient(authStore.refreshTokenAction, authStore.getToken)
} }
function buildAuthClient( function buildAuthClient(
refreshTokens: () => Promise<boolean>, refreshTokens: () => Promise<boolean>,
getToken: () => string | null, getToken: () => string | null,
): EveningDetectiveServer { ): EveningDetectiveServer {
return createEveningDetectiveServerClient( return createEveningDetectiveServerClient(
createAuthHttpHandler( createAuthHttpHandler(refreshTokens, createHttpHandler(getToken)),
refreshTokens, )
createHttpHandler(
getToken,
)
)
)
} }
function createAuthHttpHandler( function createAuthHttpHandler(
refreshTokens: () => Promise<boolean>, refreshTokens: () => Promise<boolean>,
handler: (request: { path: string, method: string, body: string | null }) => Promise<unknown> handler: (request: { path: string; method: string; body: string | null }) => Promise<unknown>,
): (request: { path: string, method: string, body: string | null }) => Promise<unknown> { ): (request: { path: string; method: string; body: string | null }) => Promise<unknown> {
return async ( return async (request: { path: string; method: string; body: string | null }) => {
request: { path: string, method: string, body: string | null }, try {
) => { return await handler(request)
try { } catch {
return await handler(request) await refreshTokens()
} catch { return await handler(request)
await refreshTokens()
return await handler(request)
}
} }
}
} }
@@ -1,58 +1,50 @@
import { createEveningDetectiveServerClient, type EveningDetectiveServer } from "."; import { createEveningDetectiveServerClient, type EveningDetectiveServer } from '.'
export function buildClient( export function buildClient(getToken?: () => string | null): EveningDetectiveServer {
getToken?: () => string | null, return createEveningDetectiveServerClient(createHttpHandler(getToken))
): EveningDetectiveServer {
return createEveningDetectiveServerClient(
createHttpHandler(
getToken,
)
)
} }
export function createHttpHandler( export function createHttpHandler(
getToken?: () => string | null, getToken?: () => string | null,
): (request: { path: string, method: string, body: string | null }) => Promise<unknown> { ): (request: { path: string; method: string; body: string | null }) => Promise<unknown> {
return async ( return async (request: { path: string; method: string; body: string | null }) => {
request: { path: string, method: string, body: string | null }, const baseURL = getURL()
) => {
const baseURL = getURL()
// Формируем полный URL // Формируем полный URL
const url = `${baseURL}/${request.path}`; const url = `${baseURL}/${request.path}`
// Подготавливаем заголовки // Подготавливаем заголовки
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; }
// Добавляем токен, если он есть // Добавляем токен, если он есть
const token = getToken?.(); const token = getToken?.()
if (token) { if (token) {
headers['Authorization'] = `Bearer ${token}`; headers['Authorization'] = `Bearer ${token}`
} }
// Делаем запрос // Делаем запрос
const response = await fetch(url, { const response = await fetch(url, {
method: request.method, method: request.method,
headers, headers,
body: request.body, // body уже JSON-строка из генератора body: request.body, // body уже JSON-строка из генератора
}); })
if (response.status == 401) { if (response.status == 401) {
throw new Error("unauthorized"); throw new Error('unauthorized')
} }
// Парсим ответ // Парсим ответ
const data = await response.json(); const data = await response.json()
// Возвращаем в формате, который ожидает генератор // Возвращаем в формате, который ожидает генератор
return data; return data
}; }
} }
export function getURL(): string { export function getURL(): string {
const url = import.meta.env.VITE_API_URL || "http://localhost:8090" const url = import.meta.env.VITE_API_URL || 'http://localhost:8090'
// console.log(url) // console.log(url)
return url return url
} }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@ body {
} }
@font-face { @font-face {
font-family: "font_old_typer"; font-family: 'font_old_typer';
src: url('@/assets/fonts/a_OldTyper.ttf'); src: url('@/assets/fonts/a_OldTyper.ttf');
} }
@@ -57,4 +57,4 @@ body {
margin-top: 70px; margin-top: 70px;
padding: 0; padding: 0;
} }
} }
+2 -4
View File
@@ -3,11 +3,9 @@ import HeaderMenu from '@/components/HeaderMenu.vue'
</script> </script>
<template> <template>
<HeaderMenu :add-logout="true" active="games" /> <HeaderMenu :add-logout="true" active="games" />
<div class="center-block-custom content-block"> <div class="center-block-custom content-block">Игры</div>
Игры
</div>
</template> </template>
<style scoped></style> <style scoped></style>
+108 -98
View File
@@ -1,155 +1,165 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps({ const props = defineProps({
addLogin: { addLogin: {
type: Boolean, type: Boolean,
required: false required: false,
}, },
addLogout: { addLogout: {
type: Boolean, type: Boolean,
required: false required: false,
}, },
isFoundKey: { isFoundKey: {
type: Boolean, type: Boolean,
required: false required: false,
}, },
active: { active: {
type: String, type: String,
required: false required: false,
} },
}) })
import { DoorOpen } from '@vicons/fa' import { DoorOpen } from '@vicons/fa'
import { Key24Regular } from '@vicons/fluent' import { Key24Regular } from '@vicons/fluent'
import { DoorFrontFilled } from '@vicons/material' import { DoorFrontFilled } from '@vicons/material'
import { Icon } from '@vicons/utils' import { Icon } from '@vicons/utils'
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui'
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
import { ref } from 'vue'; import { ref } from 'vue'
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'; import { getAuthClient } from '@/api/auth_client'
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore(); const authStore = useAuthStore()
authStore.initFromStorage() authStore.initFromStorage()
const router = useRouter() const router = useRouter()
const client = getAuthClient(); const client = getAuthClient()
const message = useMessage() const message = useMessage()
const permissions = ref<string[]>([]); const permissions = ref<string[]>([])
async function getPermissions() { async function getPermissions() {
permissions.value = [] permissions.value = []
const res = await client.GetPermissions({}) const res = await client.GetPermissions({})
permissions.value = res.permissions! permissions.value = res.permissions!
} }
getPermissions() getPermissions()
function hasPermission(permission: string): boolean { function hasPermission(permission: string): boolean {
return permissions.value.includes(permission) return permissions.value.includes(permission)
} }
const goToLogin = () => { const goToLogin = () => {
if (props.isFoundKey) { if (props.isFoundKey) {
router.push('/login') router.push('/login')
return return
} }
message.create( message.create('Дверь закрыта, куда же подевался ключ?', {
"Дверь закрыта, куда же подевался ключ?", icon: () => h(NIcon, null, { default: () => h(Key24Regular) }),
{ })
icon: () => h(NIcon, null, { default: () => h(Key24Regular) })
}
)
} }
const goToLogout = () => { const goToLogout = () => {
authStore.logout() authStore.logout()
router.push('/login') router.push('/login')
} }
</script> </script>
<template> <template>
<div class="menu-block"> <div class="menu-block">
<div class="left-group"> <div class="left-group">
<div class="menu-item-block" :class="{ active: props.active == 'office' }" v-if="authStore.hasRole('user')" <div
@click="router.push('/office')"> class="menu-item-block"
Кабинет :class="{ active: props.active == 'office' }"
</div> v-if="authStore.hasRole('user')"
<div class="menu-item-block" :class="{ active: props.active == 'games' }" v-if="hasPermission('games_page')" @click="router.push('/office')"
@click="router.push('/games')"> >
Игры Кабинет
</div> </div>
<div class="menu-item-block" :class="{ active: props.active == 'scenarios' }" <div
v-if="hasPermission('scenarios_page')" @click="router.push('/scenarios')"> class="menu-item-block"
Мои сценарии :class="{ active: props.active == 'games' }"
</div> v-if="hasPermission('games_page')"
<div class="menu-item-block" :class="{ active: props.active == 'users' }" v-if="hasPermission('users_page')" @click="router.push('/games')"
@click="router.push('/users')"> >
Пользователи Игры
</div> </div>
</div> <div
class="menu-item-block"
<div class="right-group"> :class="{ active: props.active == 'scenarios' }"
<div v-if="props.addLogin" class="menu-item-block right" @click="goToLogin"> v-if="hasPermission('scenarios_page')"
<Icon v-if="isFoundKey" class="door-icon"> @click="router.push('/scenarios')"
<DoorOpen /> >
</Icon> Мои сценарии
<Icon v-else class="door-icon"> </div>
<DoorFrontFilled /> <div
</Icon> class="menu-item-block"
Войти :class="{ active: props.active == 'users' }"
</div> v-if="hasPermission('users_page')"
<div v-if="props.addLogout" class="menu-item-block right" @click="goToLogout"> @click="router.push('/users')"
Выйти >
</div> Пользователи
</div> </div>
</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> </template>
<style scoped> <style scoped>
.menu-block { .menu-block {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 70px; height: 70px;
/* background-color: brown; */ /* background-color: brown; */
padding: 20px; padding: 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.menu-item-block { .menu-item-block {
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: 10px;
cursor: pointer; cursor: pointer;
padding: 5px; padding: 5px;
font-size: 1.1em; font-size: 1.1em;
} }
.menu-item-block:hover { .menu-item-block:hover {
color: #63e2b7; color: #63e2b7;
opacity: 0.8; opacity: 0.8;
} }
.right { .right {
margin-left: auto; margin-left: auto;
} }
.door-icon { .door-icon {
float: left; float: left;
width: 25px; width: 25px;
padding-top: 3px; padding-top: 3px;
padding-right: 5px; padding-right: 5px;
} }
.active { .active {
color: #63e2b7; color: #63e2b7;
} }
</style> </style>
+48 -30
View File
@@ -1,11 +1,21 @@
<script setup lang="ts"> <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 { 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() const router = useRouter()
@@ -16,13 +26,12 @@ const email = ref('')
const password = ref('') const password = ref('')
const approval = ref(false) const approval = ref(false)
const options = computed(() => { const options = computed(() => {
return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => { return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => {
const prefix = email.value.split('@')[0] const prefix = email.value.split('@')[0]
return { return {
label: prefix + suffix, label: prefix + suffix,
value: prefix + suffix value: prefix + suffix,
} }
}) })
}) })
@@ -47,7 +56,7 @@ async function signup() {
message.error(authStore.error) message.error(authStore.error)
return return
} }
message.info("Пароль отправлен на почту") message.info('Пароль отправлен на почту')
} }
</script> </script>
@@ -55,24 +64,25 @@ async function signup() {
<div class="center-block-custom content-block center-middle-block-custom height80"> <div class="center-block-custom content-block center-middle-block-custom height80">
<n-card style="margin-bottom: 16px" class="sign-card"> <n-card style="margin-bottom: 16px" class="sign-card">
<n-tabs type="line" animated> <n-tabs type="line" animated>
<n-tab-pane name="signin" tab="Вход"> <n-tab-pane name="signin" tab="Вход">
<n-space vertical> <n-space vertical>
<div class="form-label">Почта</div> <div class="form-label">Почта</div>
<n-auto-complete v-model:value="email" :input-props="{ <n-auto-complete
autocomplete: 'disabled', v-model:value="email"
}" :options="options" placeholder="detective@mail.ru" clearable /> :input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="detective@mail.ru"
clearable
/>
<div class="form-label">Пароль</div> <div class="form-label">Пароль</div>
<n-input v-model:value="password" type="password" placeholder="********" /> <n-input v-model:value="password" type="password" placeholder="********" />
<div class="form-button-wrapper"> <div class="form-button-wrapper">
<div class="form-label"> <div class="form-label">
<n-button @click="signin" :disabled="email.length == 0 || password.length == 0"> <n-button @click="signin" :disabled="email.length == 0 || password.length == 0">
<span v-if="!authStore.isLoading"> <span v-if="!authStore.isLoading"> Вход </span>
Вход <span v-else> Подождите... </span>
</span>
<span v-else>
Подождите...
</span>
</n-button> </n-button>
</div> </div>
</div> </div>
@@ -84,32 +94,40 @@ async function signup() {
<div class="form-label">Позывной</div> <div class="form-label">Позывной</div>
<n-input v-model:value="username" type="text" placeholder="Шерлок" /> <n-input v-model:value="username" type="text" placeholder="Шерлок" />
<div class="form-label">Почта</div> <div class="form-label">Почта</div>
<n-auto-complete v-model:value="email" :input-props="{ <n-auto-complete
autocomplete: 'disabled', v-model:value="email"
}" :options="options" placeholder="detective@mail.ru" clearable /> :input-props="{
autocomplete: 'disabled',
}"
:options="options"
placeholder="detective@mail.ru"
clearable
/>
<div class="form-label"> <div class="form-label">
<n-checkbox v-model:checked="approval"> <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> </n-checkbox>
</div> </div>
<div class="form-button-wrapper"> <div class="form-button-wrapper">
<div class="form-label"> <div class="form-label">
<n-button @click="signup" :disabled="username.length == 0 || password.length == 0 || !approval"> <n-button
<span v-if="!authStore.isLoading"> @click="signup"
Регистрация :disabled="username.length == 0 || password.length == 0 || !approval"
</span> >
<span v-else> <span v-if="!authStore.isLoading"> Регистрация </span>
Подождите... <span v-else> Подождите... </span>
</span>
</n-button> </n-button>
</div> </div>
</div> </div>
</n-space> </n-space>
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
</n-card> </n-card>
</div> </div>
+16 -18
View File
@@ -1,37 +1,35 @@
<script setup lang="ts"> <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 HeaderMenu from '@/components/HeaderMenu.vue'
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore(); const authStore = useAuthStore()
const client = getAuthClient(); const client = getAuthClient()
const permissions = ref<string[]>([]) const permissions = ref<string[]>([])
async function getPermissions() { async function getPermissions() {
permissions.value = [] permissions.value = []
const res = await client.GetPermissions({}) const res = await client.GetPermissions({})
permissions.value = res.permissions! permissions.value = res.permissions!
} }
getPermissions() getPermissions()
</script> </script>
<template> <template>
<HeaderMenu :add-logout="true" active="office" /> <HeaderMenu :add-logout="true" active="office" />
<div class="center-block-custom content-block"> <div class="center-block-custom content-block">
<p> <p>Доброго времени суток, {{ authStore.username }}.</p>
Доброго времени суток, {{ authStore.username }}. <p>
</p> {{ authStore.userRoles }}
<p> </p>
{{ authStore.userRoles }}
</p>
{{ permissions }} {{ permissions }}
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>
+118 -105
View File
@@ -1,163 +1,176 @@
<script setup lang="ts"> <script setup lang="ts">
import { NDynamicInput, NInput, NInputGroup, NSpace, NSwitch } from 'naive-ui' 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({ const props = defineProps({
place: { place: {
type: Object as PropType<Place>, type: Object as PropType<Place>,
required: true required: true,
}, },
mode: { mode: {
type: String, type: String,
required: true required: true,
} },
}) })
const emit = defineEmits<{ 'update:mode': [value: string] }>() const emit = defineEmits<{ 'update:mode': [value: string] }>()
const updateMode = (mode: string) => { const updateMode = (mode: string) => {
console.log(mode) console.log(mode)
emit('update:mode', mode) emit('update:mode', mode)
} }
function onCreateApplication(): Application { function onCreateApplication(): Application {
return { return {
name: undefined, name: undefined,
image: undefined image: undefined,
} }
} }
function onCreateDoor(): Door { function onCreateDoor(): Door {
return { return {
code: undefined, code: undefined,
name: undefined, name: undefined,
keys: undefined keys: undefined,
} }
} }
function onCreateKey(): Key { function onCreateKey(): Key {
return { return {
name: undefined name: undefined,
} }
} }
</script> </script>
<template> <template>
<div v-if="mode == 'mini-add'" class="place-block-plus show-editor-block" @click="updateMode('edit')"> <div
<p class="place-image-plus">+</p> v-if="mode == 'mini-add'"
</div> 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')"> <div
<p>[{{ props.place.code }}] - {{ props.place.name }}</p> v-if="mode == 'show-editor'"
<div class="message-content">{{ props.place.text }}</div> class="place-block show-editor-block"
<p v-for="application in props.place.applications" v-bind:key="application.name">{{ application.name }}</p> @click="updateMode('edit')"
</div> >
<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"> <div v-if="mode == 'edit'" class="place-block">
<n-space vertical> <n-space vertical>
<n-input-group> <n-input-group>
<n-input :style="{ width: '20%' }" v-model:value="props.place.code" type="text" placeholder='Ы-1' /> <n-input
<n-input :style="{ width: '80%' }" v-model:value="props.place.name" type="text" placeholder='Дом №Ы' /> :style="{ width: '20%' }"
</n-input-group> v-model:value="props.place.code"
<n-input v-model:value="props.place.text" type="textarea" :autosize="{ minRows: 3 }" type="text"
placeholder='В начале было слово...' /> placeholder="Ы-1"
<!-- <n-upload multiple directory-dnd :max="1" v-model:file-list="newPlaceFileList"> />
<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-upload-dragger>
<n-text style="font-size: 16px"> <n-text style="font-size: 16px">
Щелкните или перетащите файл в эту область для загрузки Щелкните или перетащите файл в эту область для загрузки
</n-text> </n-text>
</n-upload-dragger> </n-upload-dragger>
</n-upload> --> </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"> <n-dynamic-input v-model:value="props.place.applications" :on-create="onCreateApplication">
<template #create-button-default> <template #create-button-default> Добавить улику </template>
Добавить улику <template #default="{ value }">
</template> <div style="display: flex; align-items: center; width: 100%">
<template #default="{ value }"> <n-input v-model:value="value.name" type="text" />
<div style="display: flex; align-items: center; width: 100%"> </div>
<n-input v-model:value="value.name" type="text" /> </template>
</div> </n-dynamic-input>
</template>
<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> </n-dynamic-input>
</div>
</template>
</n-dynamic-input>
<n-dynamic-input v-model:value="props.place.doors" :on-create="onCreateDoor"> <n-dynamic-input v-model:value="props.place.keys" :on-create="onCreateKey">
<template #create-button-default> <template #create-button-default> Добавить ключ </template>
Добавить действие <template #default="{ value: key }">
</template> <div style="display: flex; align-items: center; width: 100%">
<template #default="{ value: door }"> <n-input v-model:value="key.name" type="text" placeholder="Название" />
<div style="display: flex; align-items: center; width: 100%"> </div>
<n-input v-model:value="door.code" type="text" placeholder="Код" /> </template>
<n-input v-model:value="door.name" type="text" placeholder="Название" /> </n-dynamic-input>
<n-dynamic-input v-model:value="door.keys" :on-create="onCreateKey"> <slot></slot>
<template #create-button-default> </n-space>
Добавить ключ </div>
</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>
</template> </template>
<style scoped> <style scoped>
.place-block { .place-block {
margin: 20px; margin: 20px;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #444; border: 1px solid #444;
} }
.place-block:hover { .place-block:hover {
color: #63e2b7; color: #63e2b7;
} }
.place-block-plus { .place-block-plus {
margin: 20px; margin: 20px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #444; border: 1px solid #444;
} }
.place-block-plus:hover { .place-block-plus:hover {
color: #63e2b7; color: #63e2b7;
} }
.place-image-plus { .place-image-plus {
font-size: 30px; font-size: 30px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.show-editor-block { .show-editor-block {
cursor: pointer; cursor: pointer;
} }
.message-content { .message-content {
white-space: pre-wrap; white-space: pre-wrap;
} }
</style> </style>
+236 -218
View File
@@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { Settings } from '@vicons/carbon' import { Settings } from '@vicons/carbon'
import { Icon } from '@vicons/utils' import { Icon } from '@vicons/utils'
import { NButton, type UploadFileInfo,useMessage } from 'naive-ui'; import { NButton, type UploadFileInfo, useMessage } from 'naive-ui'
import { NCard,NFlex, NInput, NModal, NSpace, NText, NUpload, NUploadDragger } from 'naive-ui' import { NCard, NFlex, NInput, NModal, NSpace, NText, NUpload, NUploadDragger } from 'naive-ui'
import { ref } from 'vue'; import { ref } from 'vue'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'; import { getAuthClient } from '@/api/auth_client'
import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server'; import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
import PlaceBlock from '@/components/PlaceBlock.vue'; import PlaceBlock from '@/components/PlaceBlock.vue'
import router from '@/router'; import router from '@/router'
const client = getAuthClient(); const client = getAuthClient()
const route = useRoute() const route = useRoute()
const scenarioId = route.params.id const scenarioId = route.params.id
@@ -24,16 +24,100 @@ const message = useMessage()
const fileList = ref<UploadFileInfo[]>([]) const fileList = ref<UploadFileInfo[]>([])
const scenario = ref<Scenario>({ const scenario = ref<Scenario>({
id: undefined, id: undefined,
name: undefined, name: undefined,
story: undefined, story: undefined,
author: undefined, author: undefined,
updatedAt: undefined, updatedAt: undefined,
createdAt: undefined, createdAt: undefined,
publishedAt: undefined publishedAt: undefined,
}) })
const newPlace = ref<Place>({ 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, code: undefined,
name: undefined, name: undefined,
text: undefined, text: undefined,
@@ -42,262 +126,196 @@ const newPlace = ref<Place>({
applications: undefined, applications: undefined,
doors: undefined, doors: undefined,
keys: 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) { async function collapseAddPlace(place: Place) {
place.mode = 'mini-add' place.mode = 'mini-add'
} }
async function cancelUpdatePlace(place: Place) { 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) { async function updatePlace(place: Place) {
const res = await client.UpdateScenarioPlace({ const res = await client.UpdateScenarioPlace({
id: scenario.value.id, id: scenario.value.id,
code: place.oldCode, code: place.oldCode,
place: place place: place,
}) })
if (res.error != '') { if (res.error != '') {
message.error(res.error!) message.error(res.error!)
return return
} }
await getScenario(Number(scenarioId)) await getScenario(Number(scenarioId))
place.mode = 'show-editor' place.mode = 'show-editor'
} }
async function deletePlace(place: Place) { async function deletePlace(place: Place) {
const res = await client.DeleteScenarioPlace({ const res = await client.DeleteScenarioPlace({
id: scenario.value.id, id: scenario.value.id,
code: place.oldCode, code: place.oldCode,
}) })
if (res.error != '') { if (res.error != '') {
message.error(res.error!) message.error(res.error!)
return return
} }
await getScenario(Number(scenarioId)) await getScenario(Number(scenarioId))
} }
</script> </script>
<template> <template>
<HeaderMenu :add-logout="true" active="scenarios" /> <HeaderMenu :add-logout="true" active="scenarios" />
<div class="center-block-custom-big content-block"> <div class="center-block-custom-big content-block">
<div class="places-container"> <div class="places-container">
<div class="settings-block"> <div class="settings-block">
<n-button quaternary @click="showSettingsScenarioModal = true" class="settings-button"> <n-button quaternary @click="showSettingsScenarioModal = true" class="settings-button">
<Icon class="settings-icon"> <Icon class="settings-icon">
<Settings /> <Settings />
</Icon> </Icon>
Настройки Настройки
</n-button> </n-button>
</div> </div>
<PlaceBlock :place="newPlace" v-model:mode="newPlace.mode"> <PlaceBlock :place="newPlace" v-model:mode="newPlace.mode">
<n-flex justify="end"> <n-flex justify="end">
<n-button @click="collapseAddPlace(newPlace)">Отмена</n-button> <n-button @click="collapseAddPlace(newPlace)">Отмена</n-button>
<n-button @click="addPlace()" :disabled="!newPlace.code">Добавить точку</n-button> <n-button @click="addPlace()" :disabled="!newPlace.code">Добавить точку</n-button>
</n-flex> </n-flex>
</PlaceBlock> </PlaceBlock>
<PlaceBlock :place="place" v-model:mode="place.mode" v-for="place in scenario.story?.places" v-bind:key="place.code"> <PlaceBlock
<n-flex justify="end"> :place="place"
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button> v-model:mode="place.mode"
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button> v-for="place in scenario.story?.places"
<n-button @click="updatePlace(place)">Сохранить</n-button> v-bind:key="place.code"
</n-flex> >
</PlaceBlock> <n-flex justify="end">
</div> <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>
</div>
<!-- Окно настроек сценария --> <!-- Окно настроек сценария -->
<n-modal v-model:show="showSettingsScenarioModal"> <n-modal v-model:show="showSettingsScenarioModal">
<n-card style="width: 600px" title="Настройки сценария" :bordered="false" size="huge" role="dialog" <n-card
aria-modal="true"> style="width: 600px"
<template #header-extra> title="Настройки сценария"
<n-button @click="showSettingsScenarioModal = false"> :bordered="false"
x size="huge"
</n-button> role="dialog"
</template> aria-modal="true"
<n-space vertical> >
<p> <template #header-extra>
Название <n-button @click="showSettingsScenarioModal = false"> x </n-button>
</p> </template>
<n-input v-model:value="scenario.name" type="text" placeholder='Дело №0 "Следствие ведут овечки"' /> <n-space vertical>
<p>Название</p>
<n-input
v-model:value="scenario.name"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<p class="settings-header"> <p class="settings-header">Описание</p>
Описание <n-input
</p> v-model:value="scenario.description"
<n-input v-model:value="scenario.description" type="textarea" placeholder='Убийство пастуха...' /> type="textarea"
placeholder="Убийство пастуха..."
/>
<p class="settings-header"> <p class="settings-header">Картинка</p>
Картинка <n-input
</p> v-model:value="scenario.image"
<n-input v-model:value="scenario.image" type="text" placeholder='default_scenario.png' disabled /> type="text"
<n-upload multiple directory-dnd :max="1" v-model:file-list="fileList"> placeholder="default_scenario.png"
<n-upload-dragger> disabled
<n-text style="font-size: 16px"> />
Щелкните или перетащите файл в эту область для загрузки <n-upload multiple directory-dnd :max="1" v-model:file-list="fileList">
</n-text> <n-upload-dragger>
</n-upload-dragger> <n-text style="font-size: 16px">
</n-upload> Щелкните или перетащите файл в эту область для загрузки
</n-text>
</n-upload-dragger>
</n-upload>
<n-button @click="updateScenario()" ghost> <n-button @click="updateScenario()" ghost> Сохранить изменения </n-button>
Сохранить изменения
</n-button>
<hr class="settings-hr"> <hr class="settings-hr" />
<p> <p>
Введите название сценария <n-text class="name-text" strong>{{ scenario.name }}</n-text> чтобы Введите название сценария
удалить <n-text class="name-text" strong>{{ scenario.name }}</n-text> чтобы удалить его
его </p>
</p> <n-input
<n-input v-model:value="deletedScenarioName" type="text" v-model:value="deletedScenarioName"
placeholder='Дело №0 "Следствие ведут овечки"' /> type="text"
<n-button @click="deleteScenario(scenario.id!)" type="error" ghost placeholder='Дело №0 "Следствие ведут овечки"'
:disabled="deletedScenarioName != scenario.name"> />
Удалить сценарий <n-button
</n-button> @click="deleteScenario(scenario.id!)"
</n-space> type="error"
<template #footer> ghost
</template> :disabled="deletedScenarioName != scenario.name"
</n-card> >
</n-modal> Удалить сценарий
</n-button>
</n-space>
<template #footer> </template>
</n-card>
</n-modal>
</template> </template>
<style scoped> <style scoped>
.settings-block { .settings-block {
height: 40px; height: 40px;
/* background-color: brown; */ /* background-color: brown; */
} }
.settings-button { .settings-button {
float: right; float: right;
} }
.settings-icon { .settings-icon {
width: 20px; width: 20px;
} }
.name-text { .name-text {
padding: 3px; padding: 3px;
border-radius: 3px; border-radius: 3px;
background-color: rgb(255 255 255 / 10%); background-color: rgb(255 255 255 / 10%);
} }
.settings-header { .settings-header {
margin-top: 20px; margin-top: 20px;
} }
.settings-hr { .settings-hr {
margin: 20px 0; margin: 20px 0;
} }
.places-container { .places-container {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
overflow-y: auto; overflow-y: auto;
padding: 20px 0; padding: 20px 0;
height: calc(100vh - 150px); height: calc(100vh - 150px);
/* border: 1px solid red; */ /* border: 1px solid red; */
} }
@media (width >= 1024px) { @media (width >= 1024px) {
.places-container { .places-container {
padding: 20px 250px; padding: 20px 250px;
height: calc(100vh - 70px); height: calc(100vh - 70px);
} }
} }
</style> </style>
+96 -87
View File
@@ -1,15 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui'
import { NButton, NCard, NInput,NModal } from 'naive-ui' import { NButton, NCard, NInput, NModal } from 'naive-ui'
import { ref } from 'vue'; import { ref } from 'vue'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'; import { getAuthClient } from '@/api/auth_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'; import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient()
const client = getAuthClient();
const scenarios = ref<Scenario[]>([]) const scenarios = ref<Scenario[]>([])
@@ -21,132 +20,142 @@ const showAddScenarioModal = ref(false)
const newScenarioName = ref('') const newScenarioName = ref('')
async function getScenarios() { async function getScenarios() {
scenarios.value = [] scenarios.value = []
const res = await client.GetMyScenarios({}) const res = await client.GetMyScenarios({})
scenarios.value = res.scenarios! scenarios.value = res.scenarios!
} }
async function addScenario(name: string) { async function addScenario(name: string) {
const res = await client.AddScenario({ name: name }) const res = await client.AddScenario({ name: name })
if (res.error != '') { if (res.error != '') {
message.error(res.error!) message.error(res.error!)
return return
} }
await toScenarioEditor(res.id!) await toScenarioEditor(res.id!)
} }
async function toScenarioEditor(id: number) { async function toScenarioEditor(id: number) {
router.push('/scenarios/' + id) router.push('/scenarios/' + id)
} }
getScenarios() getScenarios()
</script> </script>
<template> <template>
<HeaderMenu :add-logout="true" active="scenarios" /> <HeaderMenu :add-logout="true" active="scenarios" />
<div class="center-block-custom-big content-block"> <div class="center-block-custom-big content-block">
<div class="scenarios-container">
<div class="scenarios-container"> <div class="scenario-block" @click="showAddScenarioModal = true">
<div class="scenario-image-block scenario-image-plus">+</div>
<div class="scenario-block" @click="showAddScenarioModal = true"> <p class="scenario-title">Создать новый сценарий</p>
<div class="scenario-image-block scenario-image-plus">+</div> </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="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>
</div>
<!-- Окно создания сценария --> <!-- Окно создания сценария -->
<n-modal v-model:show="showAddScenarioModal"> <n-modal v-model:show="showAddScenarioModal">
<n-card style="width: 600px" title="Создание сценария" :bordered="false" size="huge" role="dialog" <n-card
aria-modal="true"> style="width: 600px"
<template #header-extra> title="Создание сценария"
<n-button @click="showAddScenarioModal = false"> :bordered="false"
x size="huge"
</n-button> role="dialog"
</template> aria-modal="true"
<n-input v-model:value="newScenarioName" type="text" placeholder='Дело №0 "Следствие ведут овечки"' /> >
<template #footer> <template #header-extra>
<n-button @click="addScenario(newScenarioName)" :disabled="newScenarioName.length == 0"> <n-button @click="showAddScenarioModal = false"> x </n-button>
Создать </template>
</n-button> <n-input
</template> v-model:value="newScenarioName"
</n-card> type="text"
</n-modal> placeholder='Дело №0 "Следствие ведут овечки"'
/>
<template #footer>
<n-button @click="addScenario(newScenarioName)" :disabled="newScenarioName.length == 0">
Создать
</n-button>
</template>
</n-card>
</n-modal>
</template> </template>
<style scoped> <style scoped>
.scenarios-container { .scenarios-container {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: center; justify-content: center;
overflow-y: auto; overflow-y: auto;
padding: 40px 0; padding: 40px 0;
height: calc(100vh - 150px); height: calc(100vh - 150px);
/* border: 1px solid red; */ /* border: 1px solid red; */
} }
.scenario-block { .scenario-block {
margin: 10px; margin: 10px;
width: 150px; width: 150px;
cursor: pointer; cursor: pointer;
/* background-color: #553333; */ /* background-color: #553333; */
} }
.scenario-block:hover { .scenario-block:hover {
color: #63e2b7; color: #63e2b7;
} }
.scenario-image-block { .scenario-image-block {
height: 150px; height: 150px;
border-radius: 10px; border-radius: 10px;
} }
.scenario-image-block:hover { .scenario-image-block:hover {
color: #63e2b7; color: #63e2b7;
border: 2px solid #63e2b7; border: 2px solid #63e2b7;
} }
.scenario-image-plus { .scenario-image-plus {
font-size: 120px; font-size: 120px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #333; background-color: #333;
} }
.scenario-image { .scenario-image {
background-size: cover; background-size: cover;
background-position: center; background-position: center;
} }
.scenario-title { .scenario-title {
padding: 5px 0; padding: 5px 0;
} }
@media (width >= 1024px) { @media (width >= 1024px) {
.scenarios-container { .scenarios-container {
height: calc(100vh - 70px); height: calc(100vh - 70px);
} }
.scenario-block { .scenario-block {
width: 250px; width: 250px;
} }
.scenario-image-block { .scenario-image-block {
height: 250px; height: 250px;
border-radius: 10px; border-radius: 10px;
} }
} }
</style> </style>
+2 -4
View File
@@ -3,11 +3,9 @@ import HeaderMenu from '@/components/HeaderMenu.vue'
</script> </script>
<template> <template>
<HeaderMenu :add-logout="true" active="users" /> <HeaderMenu :add-logout="true" active="users" />
<div class="center-block-custom content-block"> <div class="center-block-custom content-block">Пользователи</div>
Пользователи
</div>
</template> </template>
<style scoped></style> <style scoped></style>
+1 -1
View File
@@ -49,7 +49,7 @@ const router = createRouter({
component: ScenariosView, component: ScenariosView,
}, },
{ {
path: '/scenarios/:id', // Динамический параметр id path: '/scenarios/:id', // Динамический параметр id
name: 'ScenariosEditorView', name: 'ScenariosEditorView',
component: ScenariosEditorView, component: ScenariosEditorView,
}, },
+197 -197
View File
@@ -1,226 +1,226 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
import { computed,ref } from 'vue'; import { computed, ref } from 'vue'
import { type User } from '@/api/generated/crabs/evening_detective_server'; import { type User } from '@/api/generated/crabs/evening_detective_server'
import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'; import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// ===== STATE ===== // ===== STATE =====
const accessToken = ref<string | null>(null); const accessToken = ref<string | null>(null)
const refreshToken = ref<string | null>(null); const refreshToken = ref<string | null>(null)
const user = ref<User | null>(null); const user = ref<User | null>(null)
const isLoading = ref(false); const isLoading = ref(false)
const error = ref<string | null>(null); const error = ref<string | null>(null)
// ===== GETTERS ===== // ===== GETTERS =====
const isAuthenticated = computed(() => !!accessToken.value); const isAuthenticated = computed(() => !!accessToken.value)
const username = computed(() => user.value?.username || ''); const username = computed(() => user.value?.username || '')
const userRoles = computed(() => user.value?.roles || []); const userRoles = computed(() => user.value?.roles || [])
// ===== ACTIONS ===== // ===== ACTIONS =====
// Инициализация из localStorage при загрузке // Инициализация из localStorage при загрузке
function initFromStorage() { function initFromStorage() {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken')
const refresh = localStorage.getItem('refreshToken'); const refresh = localStorage.getItem('refreshToken')
if (token) { if (token) {
accessToken.value = token; accessToken.value = token
refreshToken.value = refresh; refreshToken.value = refresh
// Получаем данные пользователя // Получаем данные пользователя
fetchUser(); fetchUser()
}
}
function getToken(): string | null {
return accessToken.value
}
// Получение данных пользователя
async function fetchUser() {
if (!accessToken.value) return
try {
const client = buildClient(getToken)
const response = await client.GetMe({})
if (response.error) {
error.value = response.error
return
}
if (response.user) {
user.value = response.user
}
} catch (err) {
console.error('Failed to fetch user:', err)
// Если 401 — токен протух
if (err instanceof Error && err.message.includes('401')) {
await logout()
}
}
}
// Логин
async function login(email: string, password: string) {
isLoading.value = true
error.value = null
try {
const client = buildClient(getToken)
const response = await client.Login({ email, password })
if (response.error) {
error.value = response.error
return false
}
// Сохраняем токены
if (response.accessToken) {
accessToken.value = response.accessToken
refreshToken.value = response.refreshToken || null
// Сохраняем в localStorage
localStorage.setItem('accessToken', response.accessToken)
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken)
} }
// Получаем данные пользователя
await fetchUser()
return true
}
return false
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
return false
} finally {
isLoading.value = false
} }
}
function getToken(): string | null { // Обновление токена
return accessToken.value async function refreshTokenAction() {
if (!refreshToken.value) return false
try {
const client = buildClient(getToken)
const response = await client.Refresh({
refreshToken: refreshToken.value,
})
if (response.error || !response.accessToken) {
return false
}
// Обновляем токены
accessToken.value = response.accessToken
refreshToken.value = response.refreshToken || null
localStorage.setItem('accessToken', response.accessToken)
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken)
}
return true
} catch {
return false
} }
}
// Получение данных пользователя // Регистрация
async function fetchUser() { async function signup(username: string, email: string) {
if (!accessToken.value) return; isLoading.value = true
error.value = null
try { try {
const client = buildClient(getToken); const client = buildClient(getToken)
const response = await client.GetMe({}); const response = await client.Signup({ username, email })
if (response.error) { if (response.error) {
error.value = response.error; error.value = response.error
return; return false
} }
if (response.user) { return true
user.value = response.user; } catch (err) {
} error.value = err instanceof Error ? err.message : 'Signup failed'
} catch (err) { return false
console.error('Failed to fetch user:', err); } finally {
// Если 401 — токен протух isLoading.value = false
if (err instanceof Error && err.message.includes('401')) {
await logout();
}
}
} }
}
// Логин // Восстановление пароля
async function login(email: string, password: string) { async function refreshPassword(email: string) {
isLoading.value = true; isLoading.value = true
error.value = null; error.value = null
try { try {
const client = buildClient(getToken); const client = buildClient(getToken)
const response = await client.Login({ email, password }); const response = await client.RefreshPassword({ email })
if (response.error) { if (response.error) {
error.value = response.error; error.value = response.error
return false; return false
} }
// Сохраняем токены return true
if (response.accessToken) { } catch (err) {
accessToken.value = response.accessToken; error.value = err instanceof Error ? err.message : 'Password reset failed'
refreshToken.value = response.refreshToken || null; return false
} finally {
// Сохраняем в localStorage isLoading.value = false
localStorage.setItem('accessToken', response.accessToken);
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken);
}
// Получаем данные пользователя
await fetchUser();
return true;
}
return false;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed';
return false;
} finally {
isLoading.value = false;
}
} }
}
// Обновление токена // Выход
async function refreshTokenAction() { async function logout() {
if (!refreshToken.value) return false; accessToken.value = null
refreshToken.value = null
user.value = null
error.value = null
try { localStorage.removeItem('accessToken')
const client = buildClient(getToken); localStorage.removeItem('refreshToken')
const response = await client.Refresh({ }
refreshToken: refreshToken.value
});
if (response.error || !response.accessToken) { // Проверка ролей
return false; function hasRole(role: string): boolean {
} return userRoles.value.includes(role)
}
// Обновляем токены function hasAnyRole(roles: string[]): boolean {
accessToken.value = response.accessToken; return roles.some((role) => userRoles.value.includes(role))
refreshToken.value = response.refreshToken || null; }
localStorage.setItem('accessToken', response.accessToken); // ===== RETURN =====
if (response.refreshToken) { return {
localStorage.setItem('refreshToken', response.refreshToken); // State
} accessToken,
refreshToken,
user,
isLoading,
error,
return true; // Getters
} catch { isAuthenticated,
return false; username,
} userRoles,
}
// Регистрация // Actions
async function signup(username: string, email: string) { initFromStorage,
isLoading.value = true; getToken,
error.value = null; login,
signup,
try { refreshPassword,
const client = buildClient(getToken); logout,
const response = await client.Signup({ username, email }); fetchUser,
refreshTokenAction,
if (response.error) { hasRole,
error.value = response.error; hasAnyRole,
return false; }
} })
return true
} catch (err) {
error.value = err instanceof Error ? err.message : 'Signup failed';
return false;
} finally {
isLoading.value = false;
}
}
// Восстановление пароля
async function refreshPassword(email: string) {
isLoading.value = true;
error.value = null;
try {
const client = buildClient(getToken);
const response = await client.RefreshPassword({ email });
if (response.error) {
error.value = response.error;
return false;
}
return true;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Password reset failed';
return false;
} finally {
isLoading.value = false;
}
}
// Выход
async function logout() {
accessToken.value = null;
refreshToken.value = null;
user.value = null;
error.value = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
// Проверка ролей
function hasRole(role: string): boolean {
return userRoles.value.includes(role);
}
function hasAnyRole(roles: string[]): boolean {
return roles.some(role => userRoles.value.includes(role));
}
// ===== RETURN =====
return {
// State
accessToken,
refreshToken,
user,
isLoading,
error,
// Getters
isAuthenticated,
username,
userRoles,
// Actions
initFromStorage,
getToken,
login,
signup,
refreshPassword,
logout,
fetchUser,
refreshTokenAction,
hasRole,
hasAnyRole,
};
});
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import GamesPage from '@/components/GamesPage.vue'; import GamesPage from '@/components/GamesPage.vue'
</script> </script>
<template> <template>
+5 -9
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Key24Regular } from '@vicons/fluent' import { Key24Regular } from '@vicons/fluent'
import { Icon } from '@vicons/utils' import { Icon } from '@vicons/utils'
import { computed, ref } from 'vue'; import { computed, ref } from 'vue'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
@@ -9,7 +9,7 @@ const isFoundKey = ref(false)
const keyColorClass = computed(() => { const keyColorClass = computed(() => {
return { return {
'gold-color': isFoundKey.value 'gold-color': isFoundKey.value,
} }
}) })
</script> </script>
@@ -18,12 +18,8 @@ const keyColorClass = computed(() => {
<HeaderMenu :add-login="true" :is-found-key="isFoundKey" /> <HeaderMenu :add-login="true" :is-found-key="isFoundKey" />
<div class="center-block-custom content-block center-middle-block-custom height80"> <div class="center-block-custom content-block center-middle-block-custom height80">
<div> <div>
<h1 class="font-text"> <h1 class="font-text">Вечерний детектив</h1>
Вечерний детектив <h3 class="font-text">Дело 0</h3>
</h1>
<h3 class="font-text">
Дело 0
</h3>
<p class="content-text"> <p class="content-text">
Поиграем? Поиграем?
<Icon class="key-icon" :class="keyColorClass" @click="isFoundKey = !isFoundKey"> <Icon class="key-icon" :class="keyColorClass" @click="isFoundKey = !isFoundKey">
@@ -36,7 +32,7 @@ const keyColorClass = computed(() => {
<style lang="css"> <style lang="css">
.font-text { .font-text {
font-family: "font_old_typer"; font-family: 'font_old_typer';
} }
.content-text { .content-text {
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import LoginPage from '@/components/LoginPage.vue'; import LoginPage from '@/components/LoginPage.vue'
</script> </script>
<template> <template>
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import OfficePage from '@/components/OfficePage.vue'; import OfficePage from '@/components/OfficePage.vue'
</script> </script>
<template> <template>
+1 -3
View File
@@ -1,7 +1,5 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>Соглашением о персональных данных</template>
Соглашением о персональных данных
</template>
<style lang="css"></style> <style lang="css"></style>
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import ScenariosEditorPage from '@/components/ScenariosEditorPage.vue'; import ScenariosEditorPage from '@/components/ScenariosEditorPage.vue'
</script> </script>
<template> <template>
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import ScenariosPage from '@/components/ScenariosPage.vue'; import ScenariosPage from '@/components/ScenariosPage.vue'
</script> </script>
<template> <template>
+1 -3
View File
@@ -1,7 +1,5 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>Пользовательское соглашение</template>
Пользовательское соглашение
</template>
<style lang="css"></style> <style lang="css"></style>
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import UsersPage from '@/components/UsersPage.vue'; import UsersPage from '@/components/UsersPage.vue'
</script> </script>
<template> <template>