generated from VLADIMIR/template_frontend
1055 lines
30 KiB
Vue
1055 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import { Key24Regular } from '@vicons/fluent'
|
||
import { CloseOutlined, LockOutlined } from '@vicons/material'
|
||
import { Icon } from '@vicons/utils'
|
||
import {
|
||
NButton,
|
||
NCard,
|
||
NDynamicInput,
|
||
NInput,
|
||
NInputGroup,
|
||
NModal,
|
||
NSpace,
|
||
NSwitch,
|
||
NTag,
|
||
NText,
|
||
NUpload,
|
||
NUploadDragger,
|
||
type UploadFileInfo,
|
||
} from 'naive-ui'
|
||
import { computed, type PropType, ref } from 'vue'
|
||
|
||
import type { Application, Door, Key, Place } from '@/api/generated/crabs/evening_detective_server'
|
||
import type { EditableApplication } from '@/types/editor'
|
||
import { applicationFileName, applicationFileType, applicationFileURL } from '@/utils/application'
|
||
|
||
import MessagePaper from './MessagePaper.vue'
|
||
|
||
// Точка сценария + служебное поле fileList (выбранные файлы для загрузки картинки)
|
||
type PlaceWithFileList = Place & {
|
||
fileList?: UploadFileInfo[]
|
||
}
|
||
|
||
const props = defineProps({
|
||
newPlaceCode: {
|
||
type: String,
|
||
},
|
||
newPlaceName: {
|
||
type: String,
|
||
},
|
||
newPlaceText: {
|
||
type: String,
|
||
},
|
||
place: {
|
||
type: Object as PropType<PlaceWithFileList>,
|
||
},
|
||
mode: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
mcp: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
addAction: {
|
||
type: Function as PropType<(param: string) => void>,
|
||
},
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
'update:newPlaceCode': [value: string]
|
||
'update:newPlaceName': [value: string]
|
||
'update:newPlaceText': [value: string]
|
||
'update:mode': [value: string]
|
||
}>()
|
||
const updateCode = (code: string) => {
|
||
emit('update:newPlaceCode', code)
|
||
}
|
||
const updateName = (name: string) => {
|
||
emit('update:newPlaceName', name)
|
||
}
|
||
const updateText = (text: string) => {
|
||
emit('update:newPlaceText', text)
|
||
}
|
||
const updateMode = (mode: string) => {
|
||
emit('update:mode', mode)
|
||
}
|
||
|
||
function onCreateApplication(): EditableApplication {
|
||
return {
|
||
name: undefined,
|
||
image: undefined,
|
||
file_type: undefined,
|
||
}
|
||
}
|
||
|
||
function onCreateDoor(): Door {
|
||
return {
|
||
code: undefined,
|
||
name: undefined,
|
||
keys: undefined,
|
||
}
|
||
}
|
||
|
||
function onCreateKey(): Key {
|
||
return {
|
||
name: undefined,
|
||
}
|
||
}
|
||
|
||
// Приводит улику из шаблона n-dynamic-input к редакторскому типу
|
||
// (в объекте лежит fileList, которого нет в схеме API).
|
||
function editableApplication(value: Application): EditableApplication {
|
||
return value as EditableApplication
|
||
}
|
||
|
||
// Убирает файл улики: очищает ссылку, тип и выбранный для загрузки файл.
|
||
function removeApplicationFile(application: EditableApplication) {
|
||
application.image = undefined
|
||
application.file_type = undefined
|
||
application.fileList = undefined
|
||
}
|
||
|
||
// Просмотр улики в модальном окне (картинка целиком / PDF во встроенном
|
||
// просмотрщике / аудио с плеером). viewImage — просмотр обычной картинки
|
||
// (например, картинки самой точки).
|
||
const viewApplication = ref<Application | null>(null)
|
||
const viewImage = ref<{ title: string; url: string } | null>(null)
|
||
const showViewApplicationModal = ref(false)
|
||
const viewPhotoURL = computed(() =>
|
||
viewImage.value
|
||
? viewImage.value.url
|
||
: applicationFileType(viewApplication.value || undefined) == 'image'
|
||
? applicationFileURL(viewApplication.value?.image)
|
||
: '',
|
||
)
|
||
|
||
function openApplicationView(application: Application) {
|
||
viewApplication.value = application
|
||
viewImage.value = null
|
||
showViewApplicationModal.value = true
|
||
}
|
||
|
||
function openImageView(title: string, url: string) {
|
||
viewImage.value = { title: title, url: url }
|
||
viewApplication.value = null
|
||
showViewApplicationModal.value = true
|
||
}
|
||
|
||
function closeApplicationView() {
|
||
showViewApplicationModal.value = false
|
||
viewApplication.value = null
|
||
viewImage.value = null
|
||
}
|
||
|
||
// Текст точки разбивается на две части: улика встаёт в поток текста, и
|
||
// текст обтекает её целиком. Разрез выбирается по границе (перенос строки
|
||
// или пробел), после которой остаётся ~45% текста — хвост достаточно
|
||
// длинный, чтобы обойти картинку по всей высоте и не оставить пустого
|
||
// места слева от неё.
|
||
function splitTextForApplications(text: string | undefined): { before: string; after: string } {
|
||
const value = text || ''
|
||
if (value.length == 0) {
|
||
return { before: '', after: '' }
|
||
}
|
||
const trailingTarget = Math.min(value.length, Math.max(120, Math.floor(value.length * 0.45)))
|
||
let best = -1
|
||
let bestDistance = Infinity
|
||
for (let i = 0; i < value.length; i++) {
|
||
const ch = value[i]
|
||
if (ch != '\n' && ch != ' ') {
|
||
continue
|
||
}
|
||
const distance = Math.abs(value.length - i - trailingTarget)
|
||
if (distance < bestDistance) {
|
||
bestDistance = distance
|
||
best = i
|
||
}
|
||
}
|
||
if (best < 0) {
|
||
// Текст без переносов и пробелов: блок ставится после всего текста.
|
||
return { before: value, after: '' }
|
||
}
|
||
return { before: value.slice(0, best), after: value.slice(best) }
|
||
}
|
||
|
||
const textBeforeApplications = computed(() => splitTextForApplications(props.place?.text).before)
|
||
const textAfterApplications = computed(() => splitTextForApplications(props.place?.text).after)
|
||
|
||
// Если у точки нет своей картинки, первая картинка-улика занимает её место
|
||
// в самом начале текста; остальные улики встают в поток текста как раньше.
|
||
const firstImageApplication = computed<Application | null>(() => {
|
||
if (props.place?.image) {
|
||
return null
|
||
}
|
||
return (
|
||
(props.place?.applications || []).find(
|
||
(application) => applicationFileType(application) == 'image' && application.image,
|
||
) || null
|
||
)
|
||
})
|
||
|
||
// Улики в потоке текста: только картинки (без первой картинки,
|
||
// занявшей место картинки точки).
|
||
const flowApplications = computed<Application[]>(() => {
|
||
const applications = props.place?.applications || []
|
||
return applications.filter((application) => {
|
||
if (application === firstImageApplication.value) {
|
||
return false
|
||
}
|
||
return applicationFileType(application) == 'image' && application.image
|
||
})
|
||
})
|
||
|
||
// Аудио, PDF и улики без файла — в самом низу после пунктирной линии.
|
||
const bottomApplications = computed<Application[]>(() => {
|
||
const applications = props.place?.applications || []
|
||
return applications.filter((application) => {
|
||
if (application === firstImageApplication.value) {
|
||
return false
|
||
}
|
||
return !(applicationFileType(application) == 'image' && application.image)
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
v-if="mode == 'mini-add'"
|
||
class="place-block-hover show-editor-block"
|
||
@click="updateMode('add')"
|
||
>
|
||
<p class="place-image-plus">+</p>
|
||
</div>
|
||
|
||
<div v-if="mode == 'add'" class="place-block">
|
||
<n-space vertical>
|
||
<n-input-group>
|
||
<n-input
|
||
:style="{ width: '20%' }"
|
||
:value="newPlaceCode"
|
||
@update:value="updateCode"
|
||
type="text"
|
||
placeholder="Ы-1"
|
||
/>
|
||
<n-input
|
||
:style="{ width: '80%' }"
|
||
:value="newPlaceName"
|
||
@update:value="updateName"
|
||
type="text"
|
||
placeholder="Дом №Ы"
|
||
/>
|
||
</n-input-group>
|
||
<n-input
|
||
:value="newPlaceText"
|
||
@update:value="updateText"
|
||
type="textarea"
|
||
:autosize="{ minRows: 3 }"
|
||
placeholder="В начале было слово..."
|
||
/>
|
||
<slot></slot>
|
||
</n-space>
|
||
</div>
|
||
|
||
<div
|
||
v-if="mode == 'show-editor'"
|
||
class="place-block-hover show-editor-block"
|
||
@click="updateMode('edit')"
|
||
>
|
||
<div class="message-header">{{ props.place?.code }}: {{ props.place?.name }}</div>
|
||
<hr class="hr" />
|
||
<div class="message-content">
|
||
<div v-if="props.place?.image">
|
||
<div
|
||
class="message-image-border"
|
||
@click.stop="openImageView(props.place?.name || '', props.place?.image || '')"
|
||
>
|
||
<img :src="props.place?.image" class="message-image" />
|
||
</div>
|
||
</div>
|
||
<span>{{ props.place?.text }}</span>
|
||
</div>
|
||
|
||
<div v-for="door in props.place?.doors" v-bind:key="door.code" class="door-block">
|
||
{{ door.name }}
|
||
<span v-for="key in door.keys" :key="key.name" class="key-block">
|
||
<Icon class="lock-icon">
|
||
<LockOutlined />
|
||
</Icon>
|
||
{{ key.name }}
|
||
</span>
|
||
</div>
|
||
|
||
<hr class="hr" v-if="props.place?.applications?.length" />
|
||
<div
|
||
class="message-footer application-block"
|
||
v-for="application in props.place?.applications"
|
||
v-bind:key="application.name"
|
||
>
|
||
<div class="application-name">Приложение: {{ application.name }}</div>
|
||
<div
|
||
v-if="applicationFileType(application) == 'image' && application.image"
|
||
class="application-image-border"
|
||
@click.stop="openApplicationView(application)"
|
||
>
|
||
<img :src="applicationFileURL(application.image)" class="application-image" />
|
||
</div>
|
||
<audio
|
||
v-else-if="applicationFileType(application) == 'audio' && application.image"
|
||
:src="applicationFileURL(application.image)"
|
||
controls
|
||
preload="none"
|
||
class="application-audio"
|
||
@click.stop
|
||
></audio>
|
||
<a
|
||
v-else-if="applicationFileType(application) == 'pdf' && application.image"
|
||
:href="applicationFileURL(application.image)"
|
||
target="_blank"
|
||
rel="noopener"
|
||
class="application-pdf-link"
|
||
@click.stop
|
||
>
|
||
PDF
|
||
</a>
|
||
</div>
|
||
|
||
<p class="gray-block">
|
||
<span v-if="props.place?.hidden"> Скрытая точка </span>
|
||
<span v-for="key in props.place?.keys" :key="key.name" class="key-block">
|
||
<Icon class="lock-icon">
|
||
<Key24Regular />
|
||
</Icon>
|
||
{{ key.name }}
|
||
</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="mode == 'show'">
|
||
<MessagePaper>
|
||
<div class="message-show-header font">
|
||
<span v-if="mcp" class="mcp-label" title="Ход сделан через MCP">MCP</span>
|
||
{{ place?.code }}: {{ place?.name }}
|
||
<span class="collapse-icon collapse-icon-up" @click="updateMode('show-collapse')"></span>
|
||
</div>
|
||
<hr class="show-hr" />
|
||
<div class="message-show-content font">
|
||
<div v-if="place?.image?.length">
|
||
<div
|
||
class="message-image-border"
|
||
@click="openImageView(place?.name || '', place?.image || '')"
|
||
>
|
||
<img v-bind:src="place?.image" class="message-image" />
|
||
</div>
|
||
</div>
|
||
<!-- Если у точки нет своей картинки, первая картинка-улика занимает
|
||
её место в самом начале текста -->
|
||
<div
|
||
v-else-if="firstImageApplication"
|
||
class="message-image-border"
|
||
@click="openApplicationView(firstImageApplication)"
|
||
>
|
||
<img :src="applicationFileURL(firstImageApplication.image)" class="message-image" />
|
||
</div>
|
||
<span>{{ textBeforeApplications }}</span>
|
||
|
||
<!-- Остальные улики-картинки встроены в поток текста: блок справа, текст обтекает его -->
|
||
<div v-if="flowApplications.length" class="show-applications-block">
|
||
<div
|
||
class="show-application"
|
||
v-for="application in flowApplications"
|
||
:key="application.name"
|
||
>
|
||
<!-- Картинка-улика без заголовка, просто фото внизу справа -->
|
||
<div class="application-image-block" @click="openApplicationView(application)">
|
||
<img :src="applicationFileURL(application.image)" class="application-image" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<span>{{ textAfterApplications }}</span>
|
||
</div>
|
||
<hr class="show-hr" v-if="props.place?.doors?.length" />
|
||
<div
|
||
v-for="door in props.place?.doors"
|
||
:key="door.code"
|
||
class="button-dialog font"
|
||
v-on:click="addAction!(door.code!)"
|
||
>
|
||
<div class="button-dialog-text">{{ door.code }} {{ door.name }}</div>
|
||
</div>
|
||
|
||
<!-- Аудио и PDF-улики (и улики без файла): в самом низу после
|
||
пунктирной линии; PDF открывается по нажатию на название -->
|
||
<hr class="show-hr" v-if="bottomApplications.length" />
|
||
<div
|
||
class="message-show-footer font"
|
||
v-for="application in bottomApplications"
|
||
:key="application.name"
|
||
>
|
||
<a
|
||
v-if="applicationFileType(application) == 'pdf' && application.image"
|
||
:href="applicationFileURL(application.image)"
|
||
target="_blank"
|
||
rel="noopener"
|
||
class="pdf-application-name"
|
||
>
|
||
Приложение: {{ application.name }}
|
||
</a>
|
||
<template v-else>
|
||
<div class="application-label">Приложение: {{ application.name }}</div>
|
||
<audio
|
||
v-if="applicationFileType(application) == 'audio' && application.image"
|
||
:src="applicationFileURL(application.image)"
|
||
controls
|
||
preload="none"
|
||
class="application-audio"
|
||
></audio>
|
||
</template>
|
||
</div>
|
||
</MessagePaper>
|
||
</div>
|
||
|
||
<div v-if="mode == 'show-collapse'">
|
||
<MessagePaper>
|
||
<div class="message-header font">
|
||
<span v-if="mcp" class="mcp-label" title="Ход сделан через MCP">MCP</span>
|
||
{{ place?.code }}: {{ place?.name }}
|
||
<span class="collapse-icon" @click="updateMode('show')"></span>
|
||
</div>
|
||
</MessagePaper>
|
||
</div>
|
||
|
||
<div v-if="mode == 'edit'" class="place-block">
|
||
<n-space vertical>
|
||
<n-input-group>
|
||
<n-input
|
||
:style="{ width: '20%' }"
|
||
v-model:value="props.place!.code"
|
||
type="text"
|
||
placeholder="Ы-1"
|
||
/>
|
||
<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="В начале было слово..."
|
||
/>
|
||
|
||
<p class="settings-header">Картинка</p>
|
||
<div>
|
||
<n-space>
|
||
<n-space vertical>
|
||
<img :src="props.place!.image" class="message-edit-image" />
|
||
<n-button @click="((props.place!.image = ''), (props.place!.fileList = undefined))"
|
||
>Убрать картинку</n-button
|
||
>
|
||
</n-space>
|
||
<n-space vertical>
|
||
<n-input
|
||
v-model:value="props.place!.image"
|
||
type="text"
|
||
placeholder="Не задано"
|
||
disabled
|
||
/>
|
||
<n-upload directory-dnd :max="1" v-model:file-list="props.place!.fileList">
|
||
<n-upload-dragger>
|
||
<n-text style="font-size: 16px">
|
||
Щелкните или перетащите файл в эту область для загрузки
|
||
</n-text>
|
||
</n-upload-dragger>
|
||
</n-upload>
|
||
</n-space>
|
||
</n-space>
|
||
</div>
|
||
|
||
<p class="settings-header">Видимость точки</p>
|
||
<n-switch v-model:value="props.place!.hidden">
|
||
<template #checked> Скрытая точка </template>
|
||
<template #unchecked> Обычная точка </template>
|
||
</n-switch>
|
||
|
||
Улики
|
||
<n-dynamic-input v-model:value="props.place!.applications" :on-create="onCreateApplication">
|
||
<template #create-button-default> Добавить улику </template>
|
||
<template #default="{ value }">
|
||
<div style="display: flex; flex-direction: column; width: 100%">
|
||
<n-input v-model:value="value.name" type="text" placeholder="Название" />
|
||
<div v-if="value.image" class="application-edit-file">
|
||
<n-space align="center" justify="space-between">
|
||
<n-text depth="3">
|
||
{{ applicationFileName(value.image) }}
|
||
<n-tag size="small" :bordered="false">
|
||
{{ applicationFileType(value) || 'файл' }}
|
||
</n-tag>
|
||
</n-text>
|
||
<n-button
|
||
size="small"
|
||
quaternary
|
||
@click="removeApplicationFile(editableApplication(value))"
|
||
>
|
||
Убрать файл
|
||
</n-button>
|
||
</n-space>
|
||
<img
|
||
v-if="applicationFileType(value) == 'image'"
|
||
:src="applicationFileURL(value.image)"
|
||
class="application-edit-image"
|
||
/>
|
||
<audio
|
||
v-else-if="applicationFileType(value) == 'audio'"
|
||
:src="applicationFileURL(value.image)"
|
||
controls
|
||
preload="none"
|
||
class="application-edit-audio"
|
||
></audio>
|
||
<a
|
||
v-else-if="applicationFileType(value) == 'pdf'"
|
||
:href="applicationFileURL(value.image)"
|
||
target="_blank"
|
||
rel="noopener"
|
||
class="application-pdf-link"
|
||
>
|
||
PDF
|
||
</a>
|
||
</div>
|
||
<n-upload
|
||
:max="1"
|
||
v-model:file-list="editableApplication(value).fileList"
|
||
accept="image/*,application/pdf,audio/*"
|
||
>
|
||
<n-button size="small">Загрузить файл улики</n-button>
|
||
</n-upload>
|
||
</div>
|
||
</template>
|
||
</n-dynamic-input>
|
||
|
||
Действия
|
||
<n-dynamic-input v-model:value="props.place!.doors" :on-create="onCreateDoor">
|
||
<template #create-button-default> Добавить действие </template>
|
||
<template #action="{ index, create, remove }">
|
||
<div style="display: flex; flex-direction: column">
|
||
<n-button @click="() => create(index)" class="plus-minus-buttons">+</n-button>
|
||
<n-button @click="() => remove(index)" class="plus-minus-buttons">-</n-button>
|
||
</div>
|
||
</template>
|
||
<template #default="{ value: door }">
|
||
<div style="display: flex; flex-direction: column; width: 100%">
|
||
<div class="door-add-block">
|
||
<n-input-group>
|
||
<n-input
|
||
:style="{ width: '30%' }"
|
||
v-model:value="door.code"
|
||
type="text"
|
||
placeholder="Код"
|
||
/>
|
||
<n-input
|
||
:style="{ width: '70%' }"
|
||
v-model:value="door.name"
|
||
type="text"
|
||
placeholder="Название"
|
||
/>
|
||
</n-input-group>
|
||
|
||
<p class="need-keys-text">Требуемые ключи</p>
|
||
<div class="door-field">
|
||
<n-dynamic-input v-model:value="door.keys" :on-create="onCreateKey">
|
||
<template #create-button-default> Добавить ключ </template>
|
||
<template #default="{ value: key }">
|
||
<div style="align-items: center; display: flex; width: 100%">
|
||
<n-input v-model:value="key.name" type="text" placeholder="Название" />
|
||
</div>
|
||
</template>
|
||
</n-dynamic-input>
|
||
</div>
|
||
</div>
|
||
</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="align-items: center; display: flex; width: 100%">
|
||
<n-input v-model:value="key.name" type="text" placeholder="Название" />
|
||
</div>
|
||
</template>
|
||
</n-dynamic-input>
|
||
|
||
<div class="slot-block">
|
||
<slot></slot>
|
||
</div>
|
||
</n-space>
|
||
</div>
|
||
|
||
<!-- Просмотр улики / картинки точки -->
|
||
<n-modal v-model:show="showViewApplicationModal" :mask-closable="true">
|
||
<n-card
|
||
:class="{ 'photo-paper': viewPhotoURL }"
|
||
:theme-overrides="
|
||
viewPhotoURL
|
||
? {
|
||
titleTextColor: 'black',
|
||
actionColor: 'transparent',
|
||
paddingMedium: '14px',
|
||
borderRadius: '10px',
|
||
}
|
||
: undefined
|
||
"
|
||
:style="{
|
||
maxWidth: 'min(94vw, 1200px)',
|
||
width: viewPhotoURL ? 'max-content' : 'min(94vw, 1200px)',
|
||
}"
|
||
:bordered="false"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
:header-style="viewPhotoURL ? { paddingBottom: '10px' } : undefined"
|
||
:content-style="{ padding: viewPhotoURL ? '0 14px' : '14px' }"
|
||
>
|
||
<template #header>
|
||
<span :class="{ 'photo-paper-title': viewPhotoURL }">
|
||
{{ viewApplication?.name || viewImage?.title }}
|
||
</span>
|
||
</template>
|
||
<template #header-extra>
|
||
<n-button
|
||
quaternary
|
||
circle
|
||
:theme-overrides="viewPhotoURL ? { textColor: 'black', textColorHover: '#63e2b7' } : undefined"
|
||
aria-label="Закрыть"
|
||
@click="closeApplicationView()"
|
||
>
|
||
<Icon size="20"><CloseOutlined /></Icon>
|
||
</n-button>
|
||
</template>
|
||
<div
|
||
v-if="viewApplication && applicationFileType(viewApplication) == 'image'"
|
||
class="view-application-image-block"
|
||
>
|
||
<img
|
||
:src="applicationFileURL(viewApplication.image)"
|
||
:alt="viewApplication.name || 'Фото улики'"
|
||
class="view-application-image"
|
||
/>
|
||
</div>
|
||
<iframe
|
||
v-else-if="viewApplication && applicationFileType(viewApplication) == 'pdf'"
|
||
:src="applicationFileURL(viewApplication.image)"
|
||
class="view-application-pdf"
|
||
></iframe>
|
||
<audio
|
||
v-else-if="viewApplication && applicationFileType(viewApplication) == 'audio'"
|
||
:src="applicationFileURL(viewApplication.image)"
|
||
controls
|
||
autoplay
|
||
class="view-application-audio"
|
||
></audio>
|
||
<div v-else-if="viewImage" class="view-application-image-block">
|
||
<img :src="viewImage.url" :alt="viewImage.title || 'Фото точки'" class="view-application-image" />
|
||
</div>
|
||
<template v-if="viewPhotoURL" #action>
|
||
<n-button
|
||
class="photo-paper-action"
|
||
tag="a"
|
||
:href="viewPhotoURL"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
:bordered="false"
|
||
:theme-overrides="{ textColor: '#bfa07d', textColorHover: '#63e2b7' }"
|
||
>
|
||
Открыть в новой вкладке
|
||
</n-button>
|
||
</template>
|
||
</n-card>
|
||
</n-modal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.hr {
|
||
border: dashed 1px;
|
||
|
||
/* border-color: black; */
|
||
border-color: #eee;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.place-block-hover,
|
||
.place-block {
|
||
background-color: #222;
|
||
border: 1px solid #222;
|
||
border-radius: 10px;
|
||
margin: 10px 0;
|
||
padding: 20px;
|
||
}
|
||
|
||
.place-block-hover:hover {
|
||
border: 1px solid #63e2b7;
|
||
color: #63e2b7;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.place-image-plus {
|
||
align-items: center;
|
||
display: flex;
|
||
font-size: 30px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.show-editor-block {
|
||
cursor: pointer;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.message-content {
|
||
display: flow-root;
|
||
font-weight: 500;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.gray-block {
|
||
color: #777;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.application-block {
|
||
color: burlywood;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.application-name {
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.application-image-border {
|
||
background-image: url('@/assets/images/paper_white.jpg');
|
||
background-size: cover;
|
||
box-shadow: 0 3px 15px rgb(98 98 98);
|
||
cursor: zoom-in;
|
||
float: left;
|
||
margin-right: 15px;
|
||
max-width: 120px;
|
||
padding: 7px;
|
||
transform: rotate(-3deg);
|
||
width: 40%;
|
||
}
|
||
|
||
/* В редакторе улики показываются друг под другом:
|
||
картинка улики не плавает, а идёт под своим названием */
|
||
.message-footer .application-image-border {
|
||
float: none;
|
||
margin: 4px 0 10px;
|
||
max-width: 120px;
|
||
width: 120px;
|
||
}
|
||
|
||
.application-image {
|
||
width: 100%;
|
||
}
|
||
|
||
.application-image-block {
|
||
background-image: url('@/assets/images/paper_white.jpg');
|
||
background-size: cover;
|
||
box-shadow: 0 3px 15px rgb(98 98 98);
|
||
cursor: zoom-in;
|
||
margin: 14px 0 5px;
|
||
max-width: 120px;
|
||
padding: 7px;
|
||
}
|
||
|
||
.application-audio {
|
||
margin: 5px 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.application-pdf-link {
|
||
color: var(--second-color);
|
||
cursor: pointer;
|
||
display: inline-block;
|
||
margin: 5px 0;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.photo-paper {
|
||
background-image: url('@/assets/images/paper_white.jpg');
|
||
background-size: cover;
|
||
border-radius: 10px;
|
||
box-shadow: 0 3px 15px rgb(98 98 98);
|
||
color: black;
|
||
}
|
||
|
||
.photo-paper-title {
|
||
font-family: 'font_old_typer', serif;
|
||
font-size: 18px;
|
||
font-weight: 400;
|
||
line-height: 1.3;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.photo-paper-action {
|
||
background-image: url('@/assets/images/belt.png');
|
||
background-size: cover;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.view-application-image-block {
|
||
text-align: center;
|
||
}
|
||
|
||
.view-application-image {
|
||
border-radius: 5px;
|
||
display: block;
|
||
margin: 0 auto;
|
||
max-height: 72vh;
|
||
max-width: min(calc(94vw - 28px), 1172px);
|
||
}
|
||
|
||
.view-application-pdf {
|
||
border: none;
|
||
height: 72vh;
|
||
width: 100%;
|
||
}
|
||
|
||
.view-application-audio {
|
||
width: 100%;
|
||
}
|
||
|
||
.application-edit-file {
|
||
background-color: #222;
|
||
border-radius: 5px;
|
||
margin: 8px 0 4px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.application-edit-image {
|
||
border-radius: 4px;
|
||
margin-top: 6px;
|
||
max-width: 120px;
|
||
width: 100%;
|
||
}
|
||
|
||
.application-edit-audio {
|
||
margin-top: 6px;
|
||
width: 100%;
|
||
}
|
||
|
||
.application-label {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.door-block {
|
||
background-color: #222;
|
||
border-radius: 5px;
|
||
color: #eee;
|
||
display: inline-block;
|
||
margin: 10px 5px 10px 0;
|
||
padding: 5px 10px;
|
||
}
|
||
|
||
.door-add-block {
|
||
background-color: #222;
|
||
border-radius: 5px;
|
||
color: #eee;
|
||
padding: 10px;
|
||
}
|
||
|
||
.key-block {
|
||
color: gold;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.lock-icon {
|
||
position: relative;
|
||
top: 2px;
|
||
}
|
||
|
||
.door-field {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.need-keys-text {
|
||
color: gold;
|
||
margin: 15px 0 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.slot-block {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.plus-minus-buttons {
|
||
border: 1px solid #444;
|
||
border-radius: 40px;
|
||
color: #eee;
|
||
font-size: 25px;
|
||
margin: 0 0 10px 5px;
|
||
padding: 0 13px;
|
||
text-align: center;
|
||
}
|
||
|
||
.plus-minus-buttons:hover {
|
||
color: #63e2b7;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.message-image-border {
|
||
background-image: url('@/assets/images/paper_white.jpg');
|
||
background-size: cover;
|
||
box-shadow: 0 3px 15px rgb(98 98 98);
|
||
cursor: zoom-in;
|
||
float: left;
|
||
margin-right: 15px;
|
||
max-width: 200px;
|
||
padding: 7px;
|
||
transform: rotate(-3deg);
|
||
width: 40%;
|
||
}
|
||
|
||
.message-image {
|
||
width: 100%;
|
||
}
|
||
|
||
.message-edit-image {
|
||
height: 135px;
|
||
width: 135px;
|
||
}
|
||
|
||
.message-header {
|
||
font-size: 20px;
|
||
padding-right: 50px;
|
||
}
|
||
|
||
.message-footer {
|
||
color: burlywood;
|
||
font-weight: 400;
|
||
padding-right: 50px;
|
||
position: relative;
|
||
}
|
||
|
||
.message-show-header {
|
||
font-size: 20px;
|
||
padding-right: 50px;
|
||
}
|
||
|
||
.mcp-label {
|
||
border: 1px solid var(--second-color);
|
||
border-radius: 3px;
|
||
box-shadow: 1px 1px 0 var(--second-color);
|
||
color: var(--second-color);
|
||
display: inline-block;
|
||
font-family: system-ui, sans-serif;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
line-height: 1;
|
||
margin-right: 4px;
|
||
padding: 4px 6px 3px;
|
||
transform: translateY(-2px) rotate(-2deg);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.collapse-icon {
|
||
background-image: url('@/assets/images/collapse.png');
|
||
background-position: center;
|
||
background-size: cover;
|
||
cursor: pointer;
|
||
height: 28px;
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 12px;
|
||
width: 45px;
|
||
}
|
||
|
||
.collapse-icon-up {
|
||
transform: rotate(-180deg);
|
||
}
|
||
|
||
.show-hr {
|
||
border: dashed 1px;
|
||
border-color: black;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.font {
|
||
font-family: 'font_old_typer', serif;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.message-show-content {
|
||
/* BFC: плавающий блок улик остаётся внутри текста и не наезжает
|
||
на следующие секции (двери/действия) при коротком тексте */
|
||
display: flow-root;
|
||
font-weight: 500;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
/* PDF-улики внизу после пунктирной линии */
|
||
.message-show-footer {
|
||
color: var(--second-color);
|
||
font-weight: 400;
|
||
padding-right: 50px;
|
||
position: relative;
|
||
}
|
||
|
||
.pdf-application-name {
|
||
color: var(--second-color);
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Улики встроены в поток текста: блок прижат к правому краю,
|
||
текст обтекает его с левой стороны */
|
||
.show-applications-block {
|
||
align-items: flex-end;
|
||
display: flex;
|
||
flex-direction: column;
|
||
float: right;
|
||
gap: 12px;
|
||
margin: 4px 0 10px 14px;
|
||
}
|
||
|
||
.show-application {
|
||
align-items: flex-end;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.show-application .application-image-block {
|
||
margin: 4px 0 0;
|
||
}
|
||
|
||
.show-application .application-audio {
|
||
margin: 4px 0 0;
|
||
width: 260px;
|
||
}
|
||
|
||
.show-application .application-pdf-link {
|
||
margin: 4px 0 0;
|
||
}
|
||
|
||
.button-dialog {
|
||
background-image: url('@/assets/images/belt.png');
|
||
background-size: cover;
|
||
border-radius: 5px;
|
||
color: #bfa07d;
|
||
cursor: pointer;
|
||
display: inline-block;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.button-dialog-text {
|
||
padding: 5px 12px;
|
||
}
|
||
</style>
|