generated from VLADIMIR/template_frontend
up
This commit is contained in:
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-2430
File diff suppressed because one or more lines are too long
Vendored
+2430
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Вечерний детектив</title>
|
<title>Вечерний детектив</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CNblC4ZU.js"></script>
|
<script type="module" crossorigin src="/assets/index-CRAnZlT1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BlFsLzpe.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CtSjox4D.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export default defineConfigWithVueTs(
|
|||||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||||
},
|
},
|
||||||
|
|
||||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**', 'src/api/generated/**']),
|
||||||
|
|
||||||
pluginVue.configs['flat/essential'],
|
pluginVue.configs['flat/essential'],
|
||||||
vueTsConfigs.recommended,
|
vueTsConfigs.recommended,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function getTextForCounter(count: number): string[] {
|
|||||||
<Footsteps />
|
<Footsteps />
|
||||||
</Icon>
|
</Icon>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="n in getTextForCounter(allPlacesCount)" class="places-text places-text-number">{{ n }}</div>
|
<div v-for="(n, index) in getTextForCounter(allPlacesCount)" :key="index" class="places-text places-text-number">{{ n }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="counter-block">
|
<div class="counter-block">
|
||||||
<div class="places-text">
|
<div class="places-text">
|
||||||
@@ -78,7 +78,7 @@ function getTextForCounter(count: number): string[] {
|
|||||||
<ContactMailRound />
|
<ContactMailRound />
|
||||||
</Icon>
|
</Icon>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="n in getTextForCounter(newPlacesCount)" class="places-text places-text-number">{{ n }}</div>
|
<div v-for="(n, index) in getTextForCounter(newPlacesCount)" :key="index" class="places-text places-text-number">{{ n }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ useSimplePolling(() => {
|
|||||||
<p class="place-image-plus">+</p>
|
<p class="place-image-plus">+</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="team in game.teams" class="team-block">
|
<div v-for="team in game.teams" :key="team.id" class="team-block">
|
||||||
<div class="team-content-block">
|
<div class="team-content-block">
|
||||||
|
|
||||||
<n-flex justify="space-between">
|
<n-flex justify="space-between">
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NInput, NSpace, NText, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
|
import { computed, type PropType } from 'vue'
|
||||||
|
|
||||||
|
import { getURL } from '@/api/generated/crabs/evening_detective_server/client'
|
||||||
|
import type { EditableIntroduction } from '@/types/editor'
|
||||||
|
|
||||||
|
import MessagePaper from './MessagePaper.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
introduction: {
|
||||||
|
type: Object as PropType<EditableIntroduction>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:mode': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const updateMode = (mode: string) => {
|
||||||
|
emit('update:mode', mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasText = computed(() => Boolean(props.introduction?.text))
|
||||||
|
const hasAudio = computed(() => Boolean(props.introduction?.audio))
|
||||||
|
|
||||||
|
// Бэкенд отдаёт аудио полным URL (домен + имя файла); на случай голого
|
||||||
|
// имени файла строим URL до /api/files самостоятельно.
|
||||||
|
const audioUrl = computed(() => {
|
||||||
|
const value = props.introduction?.audio
|
||||||
|
if (!value) return undefined
|
||||||
|
if (/^https?:\/\//i.test(value)) return value
|
||||||
|
return `${getURL()}/api/files/${encodeURIComponent(value)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeAudio = () => {
|
||||||
|
if (props.introduction) {
|
||||||
|
props.introduction!.audio = ''
|
||||||
|
props.introduction!.fileList = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Просмотр вступления командой (первый лист дела) -->
|
||||||
|
<div v-if="mode == 'show'">
|
||||||
|
<MessagePaper>
|
||||||
|
<div class="intro-title font">Вступление</div>
|
||||||
|
<hr class="intro-hr" />
|
||||||
|
<div class="intro-text font">{{ introduction.text }}</div>
|
||||||
|
<div v-if="hasAudio" class="intro-audio-block">
|
||||||
|
<audio :src="audioUrl" controls preload="none" class="intro-audio"></audio>
|
||||||
|
</div>
|
||||||
|
</MessagePaper>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Просмотр вступления в редакторе: клик — в режим редактирования -->
|
||||||
|
<div v-if="mode == 'show-editor'" class="place-block-hover show-editor-block" @click="updateMode('edit')">
|
||||||
|
<div class="message-header">
|
||||||
|
Вступление
|
||||||
|
</div>
|
||||||
|
<hr class="hr" />
|
||||||
|
<div class="message-content">
|
||||||
|
<div v-if="hasAudio" class="intro-audio-block">
|
||||||
|
<audio :src="audioUrl" controls preload="none" class="intro-audio"></audio>
|
||||||
|
</div>
|
||||||
|
<span v-if="hasText">{{ introduction.text }}</span>
|
||||||
|
<span v-else class="intro-empty">Введение не задано — нажмите, чтобы добавить</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Редактирование вступления автором -->
|
||||||
|
<div v-if="mode == 'edit'" class="place-block">
|
||||||
|
<n-space vertical>
|
||||||
|
<p class="settings-header">Текст вступления</p>
|
||||||
|
<n-input v-model:value="props.introduction!.text" type="textarea"
|
||||||
|
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||||
|
placeholder="Дорогие сыщики! В этом деле вам предстоит..." />
|
||||||
|
|
||||||
|
<p class="settings-header">Аудио вступления</p>
|
||||||
|
<div class="intro-edit-audio">
|
||||||
|
<n-space vertical>
|
||||||
|
<audio v-if="hasAudio" :src="audioUrl" controls preload="none" class="intro-audio"></audio>
|
||||||
|
<n-text v-else depth="3">Аудио не задано</n-text>
|
||||||
|
<n-button @click="removeAudio()" :disabled="!hasAudio">Убрать аудио</n-button>
|
||||||
|
</n-space>
|
||||||
|
<n-upload directory-dnd :max="1" v-model:file-list="props.introduction!.fileList">
|
||||||
|
<n-upload-dragger>
|
||||||
|
<n-text style="font-size: 16px">
|
||||||
|
Щелкните или перетащите аудиофайл в эту область для загрузки
|
||||||
|
</n-text>
|
||||||
|
</n-upload-dragger>
|
||||||
|
</n-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slot-block">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Бумажный лист вступления (просмотр командой и в каталоге) */
|
||||||
|
.font {
|
||||||
|
font-family: 'font_old_typer';
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: var(--second-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-hr {
|
||||||
|
margin: 10px 0;
|
||||||
|
border: dashed 1px;
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Панели в стиле блоков точек (см. PlaceBlock.vue) */
|
||||||
|
.place-block-hover,
|
||||||
|
.place-block {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #222;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-block-hover:hover {
|
||||||
|
color: #63e2b7;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #63e2b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-editor-block {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr {
|
||||||
|
margin: 10px 0;
|
||||||
|
border: dashed 1px;
|
||||||
|
border-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
font-size: 20px;
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-empty {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-audio-block {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-audio {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 460px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-edit-audio {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-block {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,13 +2,18 @@
|
|||||||
import { Key24Regular } from '@vicons/fluent'
|
import { Key24Regular } from '@vicons/fluent'
|
||||||
import { LockOutlined } from '@vicons/material'
|
import { LockOutlined } from '@vicons/material'
|
||||||
import { Icon } from '@vicons/utils'
|
import { Icon } from '@vicons/utils'
|
||||||
import { NButton, NDynamicInput, NInput, NInputGroup, NSpace, NSwitch, NText, NUpload, NUploadDragger } from 'naive-ui'
|
import { NButton, NDynamicInput, NInput, NInputGroup, NSpace, NSwitch, NText, NUpload, NUploadDragger, type UploadFileInfo } 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'
|
||||||
|
|
||||||
import MessagePaper from './MessagePaper.vue';
|
import MessagePaper from './MessagePaper.vue';
|
||||||
|
|
||||||
|
// Точка сценария + служебное поле fileList (выбранные файлы для загрузки картинки)
|
||||||
|
type PlaceWithFileList = Place & {
|
||||||
|
fileList?: UploadFileInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
newPlaceCode: {
|
newPlaceCode: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -20,7 +25,7 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
place: {
|
place: {
|
||||||
type: Object as PropType<Place>,
|
type: Object as PropType<PlaceWithFileList>,
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -123,7 +128,7 @@ function onCreateKey(): Key {
|
|||||||
<span v-if="props.place?.hidden">
|
<span v-if="props.place?.hidden">
|
||||||
Скрытая точка
|
Скрытая точка
|
||||||
</span>
|
</span>
|
||||||
<span v-for="key in props.place?.keys" class="key-block">
|
<span v-for="key in props.place?.keys" :key="key.name" class="key-block">
|
||||||
<Icon class="lock-icon">
|
<Icon class="lock-icon">
|
||||||
<Key24Regular />
|
<Key24Regular />
|
||||||
</Icon> {{ key.name }}
|
</Icon> {{ key.name }}
|
||||||
@@ -184,7 +189,7 @@ function onCreateKey(): Key {
|
|||||||
<n-space>
|
<n-space>
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<img :src="props.place!.image" class="message-edit-image">
|
<img :src="props.place!.image" class="message-edit-image">
|
||||||
<n-button @click="props.place!.image = '', props.place!.fileList = null">Убрать картинку</n-button>
|
<n-button @click="props.place!.image = '', props.place!.fileList = undefined">Убрать картинку</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<n-input v-model:value="props.place!.image" type="text" placeholder="Не задано" disabled />
|
<n-input v-model:value="props.place!.image" type="text" placeholder="Не задано" disabled />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useRoute } 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'
|
||||||
|
import IntroBlock from '@/components/IntroBlock.vue'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const client = getAuthClient()
|
const client = getAuthClient()
|
||||||
@@ -50,6 +51,9 @@ getScenario(Number(scenarioId))
|
|||||||
<n-tag :bordered="false" type="success" class="status-block">Автор: {{ scenario.author?.username
|
<n-tag :bordered="false" type="success" class="status-block">Автор: {{ scenario.author?.username
|
||||||
}}</n-tag>
|
}}</n-tag>
|
||||||
</div>
|
</div>
|
||||||
|
<IntroBlock
|
||||||
|
v-if="scenario.story?.introduction && (scenario.story.introduction.text || scenario.story.introduction.audio)"
|
||||||
|
:introduction="scenario.story.introduction" mode="show" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ 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 { NAlert, NCard, NFlex, NInput, NModal, NSpace, NTag, NText, NUpload, NUploadDragger } from 'naive-ui'
|
import { NAlert, NCard, NFlex, NInput, NModal, NSpace, NTag, NText, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { getArchiveClient } from '@/api/archive_client'
|
import { getArchiveClient } from '@/api/archive_client'
|
||||||
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 { Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||||
|
import IntroBlock from '@/components/IntroBlock.vue'
|
||||||
import PlaceBlock from '@/components/PlaceBlock.vue'
|
import PlaceBlock from '@/components/PlaceBlock.vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import type { EditableIntroduction, EditablePlace } from '@/types/editor'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const client = getAuthClient()
|
const client = getAuthClient()
|
||||||
@@ -40,6 +42,14 @@ const scenario = ref<Scenario>({
|
|||||||
publishedAt: undefined,
|
publishedAt: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const intro = ref<EditableIntroduction>({ text: '', audio: '' })
|
||||||
|
const introMode = ref('show-editor')
|
||||||
|
|
||||||
|
// Точки сценария со служебными полями редактора (oldCode, mode, fileList)
|
||||||
|
const editorPlaces = computed<EditablePlace[]>(() => {
|
||||||
|
return (scenario.value.story?.places || []) as EditablePlace[]
|
||||||
|
})
|
||||||
|
|
||||||
async function getScenario(id: number) {
|
async function getScenario(id: number) {
|
||||||
const res = await client.GetFullScenario({ id: id })
|
const res = await client.GetFullScenario({ id: id })
|
||||||
if (res.error != '') {
|
if (res.error != '') {
|
||||||
@@ -48,9 +58,12 @@ async function getScenario(id: number) {
|
|||||||
}
|
}
|
||||||
scenario.value = res.scenario!
|
scenario.value = res.scenario!
|
||||||
scenario.value.story?.places?.forEach((item) => {
|
scenario.value.story?.places?.forEach((item) => {
|
||||||
item.oldCode = item.code
|
const editable = item as EditablePlace
|
||||||
item.mode = 'show-editor'
|
editable.oldCode = editable.code
|
||||||
|
editable.mode = 'show-editor'
|
||||||
})
|
})
|
||||||
|
intro.value = scenario.value.story?.introduction || { text: '', audio: '' }
|
||||||
|
introMode.value = 'show-editor'
|
||||||
}
|
}
|
||||||
getScenario(Number(scenarioId))
|
getScenario(Number(scenarioId))
|
||||||
|
|
||||||
@@ -232,7 +245,7 @@ async function cancelAddPlace() {
|
|||||||
mode.value = 'mini-add'
|
mode.value = 'mini-add'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addNotFoundDoors(scenario: Scenario, place: Place) {
|
async function addNotFoundDoors(scenario: Scenario, place: EditablePlace) {
|
||||||
for (const door of place.doors || []) {
|
for (const door of place.doors || []) {
|
||||||
const place = scenario.story?.places?.find((place) => {
|
const place = scenario.story?.places?.find((place) => {
|
||||||
return place.code == door.code
|
return place.code == door.code
|
||||||
@@ -259,13 +272,13 @@ async function addNotFoundDoors(scenario: Scenario, place: Place) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelUpdatePlace(place: Place) {
|
async function cancelUpdatePlace(place: EditablePlace) {
|
||||||
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: EditablePlace) {
|
||||||
if (place.fileList && place.fileList.length > 0) {
|
if (place.fileList && place.fileList.length > 0) {
|
||||||
const file = place.fileList[0].file || null
|
const file = place.fileList[0].file || null
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
@@ -293,7 +306,7 @@ async function updatePlace(place: Place) {
|
|||||||
place.mode = 'show-editor'
|
place.mode = 'show-editor'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletePlace(place: Place) {
|
async function deletePlace(place: EditablePlace) {
|
||||||
const res = await client.DeleteScenarioPlace({
|
const res = await client.DeleteScenarioPlace({
|
||||||
id: scenario.value.id,
|
id: scenario.value.id,
|
||||||
code: place.oldCode,
|
code: place.oldCode,
|
||||||
@@ -305,6 +318,49 @@ async function deletePlace(place: Place) {
|
|||||||
await getScenario(Number(scenarioId))
|
await getScenario(Number(scenarioId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Бэкенд хранит аудио вступления как имя файла, а отдаёт полным URL
|
||||||
|
// (домен + имя). При сохранении отправляем только имя файла.
|
||||||
|
function toRawFilename(value: string | undefined): string {
|
||||||
|
if (!value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return value.split('/').pop() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateIntro() {
|
||||||
|
let audio = ''
|
||||||
|
// NUpload кладёт выбранный файл в intro.fileList (поле объекта вступления)
|
||||||
|
if (intro.value.fileList && intro.value.fileList.length > 0) {
|
||||||
|
const file = intro.value.fileList[0].file || null
|
||||||
|
if (file == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audio = await uploadFile(file, 'scenarios_' + scenario.value.id + '_intro_' + file.name)
|
||||||
|
if (audio === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
audio = toRawFilename(intro.value.audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await client.UpdateScenarioIntro({
|
||||||
|
id: scenario.value.id,
|
||||||
|
introduction: {
|
||||||
|
text: intro.value.text,
|
||||||
|
audio: audio,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (res.error != '') {
|
||||||
|
message.error(res.error!)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await getScenario(Number(scenarioId))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelUpdateIntro() {
|
||||||
|
await getScenario(Number(scenarioId))
|
||||||
|
}
|
||||||
|
|
||||||
function getRandomComplimentForNastya() {
|
function getRandomComplimentForNastya() {
|
||||||
const compliments = [
|
const compliments = [
|
||||||
{ text: 'Настя, ты просто ослепительна сегодня!', nameForm: 'Настя' },
|
{ text: 'Настя, ты просто ослепительна сегодня!', nameForm: 'Настя' },
|
||||||
@@ -348,6 +404,13 @@ function getRandomComplimentForNastya() {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<IntroBlock :introduction="intro" v-model:mode="introMode">
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="cancelUpdateIntro()">Отмена</n-button>
|
||||||
|
<n-button @click="updateIntro()">Сохранить вступление</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</IntroBlock>
|
||||||
|
|
||||||
<PlaceBlock v-model:mode="mode" v-model:newPlaceCode="code" v-model:newPlaceName="name"
|
<PlaceBlock v-model:mode="mode" v-model:newPlaceCode="code" v-model:newPlaceName="name"
|
||||||
v-model:newPlaceText="text">
|
v-model:newPlaceText="text">
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
@@ -356,8 +419,8 @@ function getRandomComplimentForNastya() {
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
</PlaceBlock>
|
</PlaceBlock>
|
||||||
|
|
||||||
<PlaceBlock :place="place" v-model:mode="place.mode" v-for="place in scenario.story?.places"
|
<PlaceBlock :place="place" v-model:mode="place.mode" v-for="place in editorPlaces"
|
||||||
v-bind:key="place.oldCode">
|
v-bind:key="place.oldCode ?? place.code ?? ''">
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button>
|
<n-button @click="deletePlace(place)" type="error" ghost>Удалить</n-button>
|
||||||
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button>
|
<n-button @click="cancelUpdatePlace(place)">Отмена</n-button>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
import { NButton, NFlex, NQrCode } from 'naive-ui'
|
import { NButton, NFlex, NQrCode } from 'naive-ui'
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { nextTick, ref } from 'vue';
|
import { nextTick, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { getAuthClient } from '@/api/auth_client';
|
import { getAuthClient } from '@/api/auth_client';
|
||||||
import type { Game, Place } from '@/api/generated/crabs/evening_detective_server';
|
import type { Game, Introduction, Place } from '@/api/generated/crabs/evening_detective_server';
|
||||||
import GameInputForm from '@/components/GameInputForm.vue'
|
import GameInputForm from '@/components/GameInputForm.vue'
|
||||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||||
|
import IntroBlock from '@/components/IntroBlock.vue'
|
||||||
import PlaceBlock from '@/components/PlaceBlock.vue'
|
import PlaceBlock from '@/components/PlaceBlock.vue'
|
||||||
import { useSimplePolling } from '@/composables/useSimplePolling';
|
import { useSimplePolling } from '@/composables/useSimplePolling';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
@@ -17,7 +18,6 @@ import MessagePaper from './MessagePaper.vue';
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const client = getAuthClient()
|
const client = getAuthClient()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const teamId = route.params.id
|
const teamId = route.params.id
|
||||||
const password = route.query.password
|
const password = route.query.password
|
||||||
@@ -27,6 +27,7 @@ const newPlacesCount = ref(0)
|
|||||||
const scrollContainer = ref<HTMLDivElement | null>();
|
const scrollContainer = ref<HTMLDivElement | null>();
|
||||||
|
|
||||||
const story = ref<AdvancedStory>({
|
const story = ref<AdvancedStory>({
|
||||||
|
introduction: undefined,
|
||||||
places: []
|
places: []
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ async function getStory() {
|
|||||||
const oldCount = story.value.places.length
|
const oldCount = story.value.places.length
|
||||||
|
|
||||||
story.value = {
|
story.value = {
|
||||||
|
introduction: res.story!.introduction,
|
||||||
places: res.story!.places?.map((item, i) => {
|
places: res.story!.places?.map((item, i) => {
|
||||||
console.log(oldStory?.places[i]?.mode)
|
console.log(oldStory?.places[i]?.mode)
|
||||||
return {
|
return {
|
||||||
@@ -116,6 +118,7 @@ async function addAction(newCode: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AdvancedStory = {
|
type AdvancedStory = {
|
||||||
|
introduction?: Introduction;
|
||||||
places: AdvancedPlace[];
|
places: AdvancedPlace[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,19 +151,23 @@ useSimplePolling(() => {
|
|||||||
<GameInputForm :addAction="addAction" :allPlacesCount="story.places.length" :newPlacesCount="newPlacesCount"
|
<GameInputForm :addAction="addAction" :allPlacesCount="story.places.length" :newPlacesCount="newPlacesCount"
|
||||||
:gameStatus="game?.status || ''"></GameInputForm>
|
:gameStatus="game?.status || ''"></GameInputForm>
|
||||||
|
|
||||||
|
<IntroBlock
|
||||||
|
v-if="game?.status && game.status != 'draft' && story.introduction && (story.introduction.text || story.introduction.audio)"
|
||||||
|
:introduction="story.introduction" mode="show">
|
||||||
|
</IntroBlock>
|
||||||
|
|
||||||
<PlaceBlock :place="advancedPlace.place" v-model:mode="advancedPlace.mode"
|
<PlaceBlock :place="advancedPlace.place" v-model:mode="advancedPlace.mode"
|
||||||
v-for="advancedPlace in story.places" v-bind:key="advancedPlace.place.code" :addAction="addAction">
|
v-for="advancedPlace in story.places" v-bind:key="advancedPlace.place.code" :addAction="addAction">
|
||||||
</PlaceBlock>
|
</PlaceBlock>
|
||||||
|
|
||||||
<MessagePaper class="qr-main-block" v-if="story.places.length == 0">
|
<MessagePaper class="qr-main-block" v-if="story.places.length == 0 && game?.status == 'draft'">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<h2 class="qr-main-block-text">Это код этого дела</h2>
|
<h2 class="qr-main-block-text">Это код этого дела</h2>
|
||||||
<p class="qr-main-block-text">Поделись с командой</p>
|
<p class="qr-main-block-text">Поделись с командой</p>
|
||||||
<div class="qr-block">
|
<div class="qr-block">
|
||||||
<n-qr-code :value="url" :size="300" :padding="0" />
|
<n-qr-code :value="url" :size="300" :padding="0" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="game?.status == 'draft'" class="qr-main-block-text">Игра скоро начнется</p>
|
<p class="qr-main-block-text">Игра скоро начнется</p>
|
||||||
<p v-if="game?.status == 'run'" class="qr-main-block-text">Игра началась!!!</p>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</MessagePaper>
|
</MessagePaper>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { UploadFileInfo } from 'naive-ui'
|
||||||
|
|
||||||
|
import type { Introduction, Place } from '@/api/generated/crabs/evening_detective_server'
|
||||||
|
|
||||||
|
// Служебные поля интерфейса, которых нет в сгенерированной схеме API
|
||||||
|
// (типы из генератора — type-алиасы, их нельзя расширить через declare module).
|
||||||
|
// Редактор сценария хранит эти поля прямо на объектах API-моделей.
|
||||||
|
|
||||||
|
/** Точка сценария со служебными полями редактора */
|
||||||
|
export type EditablePlace = Place & {
|
||||||
|
/** Код точки до редактирования (нужен для UpdateScenarioPlace) */
|
||||||
|
oldCode?: string
|
||||||
|
/** Режим отображения блока точки в редакторе (заполняется в getScenario) */
|
||||||
|
mode: string
|
||||||
|
/** Выбранные файлы для загрузки картинки точки */
|
||||||
|
fileList?: UploadFileInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Вступление сценария со служебными полями редактора */
|
||||||
|
export type EditableIntroduction = Introduction & {
|
||||||
|
/** Выбранные файлы для загрузки аудио вступления */
|
||||||
|
fileList?: UploadFileInfo[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user