Files
evening_detective_screen/src/components/ScreenPlanPanel.vue
T
2026-10-19 01:47:23 +07:00

313 lines
11 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 { NAlert, NButton, NInput, NInputNumber, NSelect, NSpace } from 'naive-ui'
import { computed } from 'vue'
import { playbackControl } from '../utils/playbackControl'
import type { ChannelState, Intent, ScreenMode, ScreenStep, TimerState } from '../types/screen'
import { modeLabels } from '../utils/presentation'
import ScreenStepPreview from './ScreenStepPreview.vue'
const props = defineProps<{
gameName: string
cover: string | null
background: string | null
steps: ScreenStep[]
currentIndex: number | null
draft: ScreenStep[]
isEditing: boolean
isBusy: boolean
canPlayMain: boolean
mainDesired: ChannelState['desired']
mainStatus: ChannelState['status']
timerStatus: TimerState['status']
canEdit: boolean
notice: string
}>()
const emit = defineEmits<{
add: []
remove: [index: number]
move: [index: number, offset: number]
clear: []
updateStep: [index: number, patch: Partial<ScreenStep>]
command: [intent: Intent]
}>()
const mainControl = computed(() =>
playbackControl({ desired: props.mainDesired, status: props.mainStatus }),
)
const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
value: Number(mode) as ScreenMode,
label,
}))
function timerLabel(index: number) {
if (props.currentIndex !== index) return 'Старт'
if (props.timerStatus === 'running') return 'Пауза'
return props.timerStatus === 'paused' ? 'Продолжить' : 'Старт'
}
</script>
<template>
<n-space vertical data-testid="screen-plan">
<n-alert v-if="notice" type="warning" role="status" class="plan-notice">{{ notice }}</n-alert>
<p v-if="!canEdit">Для редактирования войдите на сервер игры.</p>
<template v-if="isEditing">
<div v-for="(step, index) in draft" :key="index" class="plan-step">
<div class="plan-edit-header">
<div class="plan-step-preview">
<ScreenStepPreview
:step="step"
:game-name="gameName"
:cover="cover"
:background="background"
/>
</div>
<div class="plan-edit-controls">
<n-select
:value="step.mode"
:options="modeOptions"
:disabled="isBusy"
:input-props="{ 'aria-label': `Режим шага ${index + 1}` }"
@update:value="emit('updateStep', index, { mode: $event })"
/>
<n-button
:disabled="isBusy || index === 0"
:aria-label="`Поднять шаг ${index + 1}`"
@click="emit('move', index, -1)"
>↑</n-button
>
<n-button
:disabled="isBusy || index === draft.length - 1"
:aria-label="`Опустить шаг ${index + 1}`"
@click="emit('move', index, 1)"
>↓</n-button
>
<n-button :disabled="isBusy" type="error" ghost @click="emit('remove', index)"
>Удалить</n-button
>
</div>
</div>
<n-input
v-if="step.mode === 8"
:value="step.text"
type="textarea"
:disabled="isBusy"
placeholder="Текст экрана"
:input-props="{ 'aria-label': `Текст шага ${index + 1}` }"
@update:value="emit('updateStep', index, { text: $event })"
/>
<template v-if="step.mode === 7">
<label :for="`plan-label-${index}`">Надпись таймера</label>
<n-input
:value="step.timerLabel"
:disabled="isBusy"
:input-props="{ id: `plan-label-${index}` }"
@update:value="emit('updateStep', index, { timerLabel: $event })"
/>
<label :for="`plan-seconds-${index}`">Длительность в секундах</label>
<n-input-number
:value="step.timerDurationSeconds"
:min="1"
:max="86400"
:disabled="isBusy"
:input-props="{ id: `plan-seconds-${index}` }"
@update:value="emit('updateStep', index, { timerDurationSeconds: $event ?? 0 })"
/>
</template>
</div>
<n-space>
<n-button :disabled="isBusy" @click="emit('add')">Добавить шаг</n-button>
<n-button :disabled="isBusy || !draft.length" @click="emit('clear')">Удалить план</n-button>
</n-space>
</template>
<template v-else>
<p v-if="!steps.length">План экранов пуст.</p>
<div
v-for="(step, index) in steps"
:key="index"
class="plan-step"
:class="{ 'plan-step-current': currentIndex === index }"
:data-step-index="index"
:aria-current="currentIndex === index ? 'step' : undefined"
>
<div class="plan-step-header">
<div class="plan-step-preview">
<ScreenStepPreview
:step="step"
:game-name="gameName"
:cover="cover"
:background="background"
/>
</div>
<div class="plan-step-content">
<div class="plan-step-title">
<span
>{{ modeLabels[step.mode]
}}<template v-if="step.mode === 8">: {{ step.text }}</template
><template v-if="step.mode === 7"
>: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)</template
></span
>
<n-button
data-testid="show-step"
size="small"
:disabled="isBusy || currentIndex === index"
@click="emit('command', { action: 'step', index })"
>{{ currentIndex === index ? 'Показан' : 'Показать' }}</n-button
>
</div>
<div class="plan-step-actions">
<n-space v-if="[2, 4, 7].includes(step.mode)" align="center" :wrap="false">
<template v-if="step.mode === 7">
<n-button
square
:aria-label="timerLabel(index)"
:title="timerLabel(index)"
:disabled="isBusy || currentIndex !== index"
@click="
emit('command', {
action:
timerStatus === 'running'
? 'timer-pause'
: timerStatus === 'paused'
? 'timer-resume'
: 'timer-start',
})
"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path v-if="currentIndex === index && timerStatus === 'running'" d="M6 4h4v16H6zM14 4h4v16h-4z" />
<path v-else d="M7 4v16l13-8z" />
</svg>
</n-button>
<n-button square aria-label="Сбросить" title="Сбросить" :disabled="isBusy || currentIndex !== index" @click="emit('command', { action: 'timer-reset' })"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 4v6h6M3 10a9 9 0 1 1 0 6" />
</svg>
</n-button
>
</template>
<template v-if="step.mode === 2 || step.mode === 4">
<n-button
square
:aria-label="currentIndex === index ? mainControl.label : 'Воспроизвести'"
:title="currentIndex === index ? mainControl.label : 'Воспроизвести'"
:disabled="isBusy || currentIndex !== index || !canPlayMain"
@click="emit('command', { action: mainControl.action, channel: 'main' })"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path v-if="currentIndex === index && mainControl.action === 'pause'" d="M6 4h4v16H6zM14 4h4v16h-4z" />
<path v-else d="M7 4v16l13-8z" />
</svg>
</n-button
>
<n-button
square
aria-label="Стоп"
title="Стоп"
:disabled="isBusy || currentIndex !== index || !canPlayMain"
@click="emit('command', { action: 'stop', channel: 'main' })"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M5 5h14v14H5z" />
</svg>
</n-button
>
<n-button
square
aria-label="Сначала"
title="Сначала"
:disabled="isBusy || currentIndex !== index || !canPlayMain"
@click="emit('command', { action: 'replay', channel: 'main' })"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2 6M20 4v7h-7" />
</svg>
</n-button
>
<slot name="main-volume" :index="index" :active="currentIndex === index" />
</template>
</n-space>
</div>
<slot
v-if="step.mode === 2 || step.mode === 4"
name="main-controls"
:index="index"
:active="currentIndex === index"
/>
</div>
</div>
</div>
</template>
</n-space>
</template>
<style scoped>
.plan-notice {
overflow-wrap: anywhere;
}
.plan-step {
border: 1px solid var(--brand-background);
display: flex;
flex-direction: column;
gap: 12px;
overflow-wrap: anywhere;
padding: 12px;
}
.plan-step-header {
align-items: stretch;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.plan-step-content {
display: flex;
flex: 1 1 380px;
flex-direction: column;
gap: 8px;
justify-content: space-between;
min-width: 0;
}
.plan-step-actions {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: flex-start;
}
.plan-step-title {
align-items: start;
display: grid;
gap: 12px;
grid-template-columns: minmax(0, 1fr) auto;
}
.plan-step-preview {
flex: 0 0 160px;
}
.plan-edit-header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.plan-edit-controls {
align-items: center;
display: grid;
flex: 1 1 320px;
gap: 8px;
grid-template-columns: minmax(0, 1fr) auto auto auto;
min-width: 0;
}
.plan-step-current {
border-color: var(--brand-text);
}
</style>