generated from VLADIMIR/template_frontend
format
This commit is contained in:
@@ -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
@@ -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
+1
-1
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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>
|
||||||
|
|||||||
@@ -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,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,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,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,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,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,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,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user