Files
evening_detective_frontend/src/components/ScenariosEditorPage.vue
T
2026-08-05 23:10:48 +07:00

446 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Settings } from '@vicons/carbon'
import { Icon } from '@vicons/utils'
import { NButton, type UploadFileInfo, useMessage } from 'naive-ui'
import { NAlert, NCard, NFlex, NInput, NModal, NSpace, NTag,NText, NUpload, NUploadDragger } from 'naive-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
import PlaceBlock from '@/components/PlaceBlock.vue'
import router from '@/router'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const client = getAuthClient()
const route = useRoute()
const scenarioId = route.params.id
const showSettingsModal = ref(false)
const statusScenarioName = ref('')
const deletedScenarioName = ref('')
const message = useMessage()
const fileList = ref<UploadFileInfo[]>([])
const scenario = ref<Scenario>({
id: undefined,
name: undefined,
story: undefined,
author: undefined,
status: undefined,
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
})
async function getScenario(id: number) {
const res = await client.GetFullScenario({ 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 publicScenario(id: number) {
const res = await client.PublicScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
statusScenarioName.value = ''
showSettingsModal.value = false
}
async function addGame(name: string, id: number) {
const now = new Date().toISOString()
const resAddGame = await client.AddGame({
name: ('Тестовая игра: ' + name).trim(),
description: 'Тестовая игра',
startAt: now,
scenarioId: id
})
if (resAddGame.error != '') {
message.error(resAddGame.error!)
return
}
const resAddTeam = await client.AddTeam({
name: 'Тестовая команда',
gameId: resAddGame.id!,
})
if (resAddTeam.error != '') {
message.error(resAddTeam.error!)
return
}
router.push("/team-story/" + resAddTeam.id! + '?password=' + resAddTeam.password!)
}
async function draftScenario(id: number) {
const res = await client.DraftScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
statusScenarioName.value = ''
showSettingsModal.value = false
}
async function uploadFile(file: File | null, filename: string): Promise<string> {
if (!file) {
return ''
}
// Читаем как ArrayBuffer
const arrayBuffer = await file.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
const resUploadFile = await client.UploadFile({
filename: filename,
data: uint8ToBase64(bytes),
})
if (resUploadFile.error != '') {
message.error(resUploadFile.error!)
return ''
}
return resUploadFile.filename || ''
}
async function updateScenario() {
let resUploadFilename = ''
if (fileList.value && fileList.value.length > 0) {
const file = fileList.value[0].file || null
if (file == null) {
return
}
resUploadFilename = await uploadFile(file, 'scenarios_' + scenario.value.id + '_' + file.name)
if (resUploadFilename === '') {
return
}
}
const res = await client.UpdateScenario({
id: scenario.value.id,
name: scenario.value.name,
description: scenario.value.description,
image: resUploadFilename,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
showSettingsModal.value = false
}
function uint8ToBase64(uint8Array: Uint8Array) {
// Преобразуем Uint8Array в двоичную строку
const binaryString = String.fromCharCode(...uint8Array)
// Кодируем в Base64
return btoa(binaryString)
}
const code = ref('')
const name = ref('')
const text = ref('')
const mode = ref('mini-add')
async function addPlace() {
const res = await client.AddScenarioPlace({
id: scenario.value.id,
place: {
code: code.value,
name: name.value,
text: text.value,
image: undefined,
hidden: undefined,
applications: undefined,
doors: undefined,
keys: undefined,
},
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
code.value = ''
name.value = ''
text.value = ''
mode.value = 'add'
}
async function cancelAddPlace() {
code.value = ''
name.value = ''
text.value = ''
mode.value = 'mini-add'
}
async function addNotFoundDoors(scenario: Scenario, place: Place) {
for (const door of place.doors || []) {
const place = scenario.story?.places?.find((place) => {
return place.code == door.code
})
if (place == undefined) {
const res = await client.AddScenarioPlace({
id: scenario.id,
place: {
code: door.code,
name: door.name,
text: undefined,
image: undefined,
hidden: true,
applications: undefined,
doors: undefined,
keys: undefined
},
})
if (res.error != '') {
message.error(res.error!)
return
}
}
}
}
async function cancelUpdatePlace(place: Place) {
await getScenario(Number(scenarioId))
place.mode = 'show-editor'
}
async function updatePlace(place: Place) {
if (place.fileList && place.fileList.length > 0) {
const file = place.fileList[0].file || null
if (file == null) {
return
}
const resUploadFilename = await uploadFile(file, 'scenarios_' + scenario.value.id + '_place_' + place.code + '_' + file.name)
if (resUploadFilename === '') {
return
}
place.image = resUploadFilename
}
await addNotFoundDoors(scenario.value, place)
const res = await client.UpdateScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
place: place,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
place.mode = 'show-editor'
}
async function deletePlace(place: Place) {
const res = await client.DeleteScenarioPlace({
id: scenario.value.id,
code: place.oldCode,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getScenario(Number(scenarioId))
}
function getRandomComplimentForNastya() {
const compliments = [
{ text: 'Настя, ты просто ослепительна сегодня!', nameForm: 'Настя' },
{ text: 'У Насти невероятный вкус!', nameForm: 'Насти' },
{ text: 'Настенька, твоя улыбка делает мир ярче.', nameForm: 'Настенька' },
{ text: 'С Настей всегда интересно общаться.', nameForm: 'Настей' },
{ text: 'Настя, ты — источник вдохновения для всех вокруг.', nameForm: 'Настя' },
{ text: 'Мне нравится, как Настя смотрит на мир.', nameForm: 'Настя' },
{ text: 'Настюха, ты супер!', nameForm: 'Настюха' },
{ text: 'Талант Насти поражает!', nameForm: 'Насти' },
{ text: 'Настя, твоя энергия заряжает батарейки!', nameForm: 'Настя' },
{ text: 'С Настей хочется проводить каждую минуту.', nameForm: 'Настей' },
{ text: 'Настенька, ты — самое красивое, что случилось в этом мире.', nameForm: 'Настенька' },
{ text: 'У Насти золотое сердце.', nameForm: 'Насти' }
];
const randomIndex = Math.floor(Math.random() * compliments.length);
return compliments[randomIndex].text;
}
</script>
<template>
<div class="center-block-custom">
<div class="width700">
<div class="places-container">
<n-alert v-if="scenario.status == 'public'" title="Сценарий опубликован" type="warning"
style="margin-bottom: 20px;">
Не рекомендуется редактировать сценарий когда он опубликован.
</n-alert>
<n-alert v-if="authStore.user?.id == 2" :title="getRandomComplimentForNastya()" type="info"
style="margin-bottom: 20px;"></n-alert>
<div class="settings-block">
Точек: {{ scenario.story?.places?.length }}
<n-button quaternary @click="showSettingsModal = true" class="settings-button">
<Icon class="settings-icon">
<Settings />
</Icon>
Настройки
</n-button>
</div>
<PlaceBlock v-model:mode="mode" v-model:newPlaceCode="code" v-model:newPlaceName="name"
v-model:newPlaceText="text">
<n-flex justify="end">
<n-button @click="cancelAddPlace()">Отмена</n-button>
<n-button @click="addPlace()" :disabled="!code">Добавить точку</n-button>
</n-flex>
</PlaceBlock>
<PlaceBlock :place="place" v-model:mode="place.mode" v-for="place in scenario.story?.places"
v-bind:key="place.oldCode">
<n-flex justify="end">
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button>
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button>
<n-button @click="updatePlace(place)" :disabled="place.code == ''">Сохранить</n-button>
</n-flex>
</PlaceBlock>
</div>
</div>
</div>
<!-- Окно настроек -->
<n-modal v-model:show="showSettingsModal">
<n-card style="width: 600px" title="Настройки сценария" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showSettingsModal = false"> x </n-button>
</template>
<n-space vertical>
<p>Название</p>
<n-input v-model:value="scenario.name" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<p class="settings-header">Описание</p>
<n-input v-model:value="scenario.description" type="textarea" placeholder="Убийство пастуха..." />
<p class="settings-header">Картинка</p>
<n-input v-model:value="scenario.image" type="text" placeholder="default_scenario.png" disabled />
<n-upload directory-dnd :max="1" v-model:file-list="fileList">
<n-upload-dragger>
<n-text style="font-size: 16px">
Щелкните или перетащите файл в эту область для загрузки
</n-text>
</n-upload-dragger>
</n-upload>
<n-button @click="updateScenario()" ghost> Сохранить изменения </n-button>
<hr class="settings-hr" />
<p>Создать тестовую игру</p>
<n-button @click="addGame(scenario.name || '', scenario.id!)" ghost>
Создать игру
</n-button>
<hr class="settings-hr" />
<p>Публикация</p>
<p>
<n-tag v-if="scenario.status == 'public'" :bordered="false" type="success" class="status-block">
Опубликовано
</n-tag>
<n-tag v-if="scenario.status == 'draft'" :bordered="false" type="warning" class="status-block">
В разработке
</n-tag>
</p>
<p>
Введите название сценария <n-text class="name-text" strong>{{ scenario.name }}</n-text>
</p>
<n-input v-model:value="statusScenarioName" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<n-button v-if="scenario.status != 'public'" @click="publicScenario(scenario.id!)" ghost
:disabled="statusScenarioName != scenario.name">
Опубликовать сценарий
</n-button>
<n-button v-if="scenario.status != 'draft'" @click="draftScenario(scenario.id!)" ghost
:disabled="statusScenarioName != scenario.name">
Снять сценарий с публикации
</n-button>
<hr class="settings-hr" />
<p>
Введите название сценария
<n-text class="name-text" strong>{{ scenario.name }}</n-text> чтобы удалить его
</p>
<n-input v-model:value="deletedScenarioName" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<n-button @click="deleteScenario(scenario.id!)" type="error" ghost
:disabled="deletedScenarioName != scenario.name">
Удалить сценарий
</n-button>
</n-space>
<template #footer> </template>
</n-card>
</n-modal>
<HeaderMenu active="scenarios" />
</template>
<style scoped>
.settings-block {
height: 40px;
/* background-color: brown; */
}
.settings-button {
float: right;
}
.settings-icon {
width: 20px;
}
.name-text {
padding: 3px;
border-radius: 3px;
background-color: rgb(255 255 255 / 10%);
}
.settings-header {
margin-top: 20px;
}
.settings-hr {
margin: 20px 0;
}
.places-container {
display: flex;
flex-flow: column nowrap;
}
</style>