Files
evening_detective_frontend/src/components/ScenariosPage.vue
T
2026-10-03 02:38:11 +07:00

331 lines
8.0 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 { Archive } from '@vicons/carbon'
import { Icon } from '@vicons/utils'
import { useMessage } from 'naive-ui'
import {
NButton,
NCard,
NFlex,
NInput,
NModal,
NTag,
NText,
NUpload,
NUploadDragger,
type UploadFileInfo,
} from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import { getBinaryClient } from '@/api/binary_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient()
const archiveClient = getBinaryClient()
const scenarios = ref<Scenario[]>([])
const router = useRouter()
const message = useMessage()
const showAddScenarioModal = ref(false)
const newScenarioName = ref('')
const showImportModal = ref(false)
const importFileList = ref<UploadFileInfo[]>([])
const isImporting = ref(false)
async function getScenarios() {
try {
scenarios.value = []
const res = await client.GetMyScenarios({})
scenarios.value = res.scenarios!
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось загрузить сценарии')
}
}
async function addScenario(name: string) {
try {
const res = await client.AddScenario({ name: name })
await toScenarioEditor(res.ID!)
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось создать сценарий')
}
}
async function toScenarioEditor(id: number) {
router.push('/scenarios/' + id + '/editor')
}
function resetImport() {
importFileList.value = []
showImportModal.value = false
}
async function importScenario() {
const file = importFileList.value[0]?.file
if (!file) {
return
}
isImporting.value = true
try {
const res = await archiveClient.uploadScenarioArchive(file)
message.success('Сценарий импортирован из архива')
resetImport()
await toScenarioEditor(res.ID!)
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось импортировать архив')
} finally {
isImporting.value = false
}
}
getScenarios()
</script>
<template>
<div class="center-block-custom">
<div class="width1200 display-table catalog-grid">
<div class="scenario-block" @click="showAddScenarioModal = true">
<div class="scenario-image-block scenario-image-plus">+</div>
<div class="scenario-content-block">
<p class="scenario-title">Создать новый сценарий</p>
</div>
</div>
<div class="scenario-block" @click="showImportModal = true">
<div class="scenario-image-block scenario-image-plus import-icon">
<Icon>
<Archive />
</Icon>
</div>
<div class="scenario-content-block">
<p class="scenario-title">Импортировать из архива</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>
<div class="scenario-content-block">
<p class="scenario-title">{{ scenario.name }}</p>
<p class="scenario-label">
<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>
</div>
</div>
</div>
</div>
<!-- Окно создания сценария -->
<n-modal v-model:show="showAddScenarioModal">
<n-card
style="width: 600px"
title="Создание сценария"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="showAddScenarioModal = false"> x </n-button>
</template>
<n-input
v-model:value="newScenarioName"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<template #footer>
<n-flex justify="end">
<n-button @click="addScenario(newScenarioName)" :disabled="newScenarioName.length == 0">
Создать
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<!-- Окно импорта сценария из архива -->
<n-modal v-model:show="showImportModal">
<n-card
style="width: 600px"
title="Импорт сценария из архива"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="resetImport()"> x </n-button>
</template>
<p class="import-hint">
Выберите ZIP-архив сценария (файл scenario.json + папка images). Архив можно получить через
«Скачать архив» в настройках сценария.
</p>
<n-upload
:max="1"
v-model:file-list="importFileList"
accept=".zip,application/zip,application/x-zip-compressed,application/octet-stream"
>
<n-upload-dragger>
<n-text style="font-size: 16px"> Щелкните или перетащите ZIP-архив в эту область </n-text>
</n-upload-dragger>
</n-upload>
<template #footer>
<n-flex justify="end">
<n-button @click="resetImport()">Отмена</n-button>
<n-button
type="primary"
:loading="isImporting"
:disabled="importFileList.length == 0"
@click="importScenario()"
>
Импортировать
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<HeaderMenu active="scenarios" />
</template>
<style scoped>
.center-block-custom {
height: calc(100dvh - 70px);
overflow: hidden auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 24px;
}
.catalog-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
max-width: 100%;
}
.scenario-block {
background-color: #222;
border: 2px solid #222;
border-radius: 10px;
cursor: pointer;
display: flex;
flex-direction: column;
min-height: 425px;
min-width: 0;
}
.scenario-content-block {
display: flex;
flex: 1;
flex-direction: column;
padding: 12px;
}
.scenario-block:hover {
border: 2px solid #63e2b7;
color: #63e2b7;
}
.scenario-image-block {
aspect-ratio: 1;
background-color: #111;
border-radius: 10px;
}
.scenario-image-plus {
align-items: center;
background-color: #111;
display: flex;
font-size: 120px;
justify-content: center;
}
.import-icon svg {
height: 100px;
width: 100px;
}
.import-icon .xicon {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
}
.import-hint {
color: #aaa;
margin-bottom: 16px;
}
.scenario-image {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.scenario-title {
overflow-wrap: anywhere;
padding: 5px 0;
}
.scenario-label {
margin-top: auto;
padding-top: 12px;
}
.status-block {
height: auto;
line-height: 1.5;
max-width: 100%;
min-height: var(--n-height);
overflow-wrap: anywhere;
white-space: normal;
}
@media (width >=1024px) {
.catalog-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fill, 250px);
justify-content: center;
}
.scenario-block {
border-radius: 12px;
width: 250px;
}
.scenario-content-block {
padding: 12px;
}
}
</style>