up
This commit is contained in:
@@ -297,3 +297,20 @@
|
|||||||
33 локальные Markdown-ссылки и git diff --check — PASS.
|
33 локальные Markdown-ссылки и git diff --check — PASS.
|
||||||
- Границы: изменения только evening_detective_screen. Архивы не пересобирались;
|
- Границы: изменения только evening_detective_screen. Архивы не пересобирались;
|
||||||
физический проектор не проверялся. Commit/push не выполнялись.
|
физический проектор не проверялся. Commit/push не выполнялись.
|
||||||
|
|
||||||
|
## SCREEN-015 — Управление вступлением и видео через план
|
||||||
|
|
||||||
|
- Статус: `DONE`.
|
||||||
|
- Дата: 2026-10-18.
|
||||||
|
- Результат: удалён отдельный блок «Вступление и видео» и ручной выбор медиа.
|
||||||
|
Файл определяется режимом шага. В активном шаге «Введение»/«Закрывашка» доступны
|
||||||
|
управление воспроизведением, громкость, перемотка, позиция, длительность,
|
||||||
|
статус, ошибка и прежние программные замеры основного канала.
|
||||||
|
- Воспроизведение и перемотка требуют открытого показа и выбранного файла.
|
||||||
|
Музыка и общий mute сохранены отдельно. Серверный API и типы команд не менялись.
|
||||||
|
- Проверки: type-check, lint, lint:css, сборка, Electron GUI smoke — PASS.
|
||||||
|
GUI проверяет отсутствие блока, запуск/паузу/стоп в обоих медийных шагах,
|
||||||
|
перемотку и громкость на паузе, независимость музыки, план и синхронизацию.
|
||||||
|
Документация обновлена; локальные Markdown-ссылки и git diff --check — PASS.
|
||||||
|
- Границы: изменения только evening_detective_screen. Архивы не пересобирались;
|
||||||
|
физический проектор не проверялся. Commit/push не выполнялись.
|
||||||
|
|||||||
@@ -189,8 +189,9 @@ URL, независимо от исходного имени файла. Лок
|
|||||||
## Локальный проигрыватель SCREEN-004
|
## Локальный проигрыватель SCREEN-004
|
||||||
|
|
||||||
Подключите игру в блоке «Игра и сервер», выберите внешний дисплей
|
Подключите игру в блоке «Игра и сервер», выберите внешний дисплей
|
||||||
и откройте показ. В основном канале доступны вступление и заключительное видео;
|
и откройте показ. В активном шаге «Введение» или «Закрывашка» доступны управление воспроизведением,
|
||||||
их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным
|
громкость, перемотка и статус основного канала. Файл определяется режимом шага;
|
||||||
|
отдельного блока выбора медиа нет. Смена шага останавливает прежнее основное медиа. Музыка воспроизводится отдельным
|
||||||
каналом без автоматического приглушения. Звук выводится только окном показа.
|
каналом без автоматического приглушения. Звук выводится только окном показа.
|
||||||
Начальная громкость каждого канала — 35%; общий mute не меняет сохранённые уровни.
|
Начальная громкость каждого канала — 35%; общий mute не меняет сохранённые уровни.
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел
|
|||||||
|
|
||||||
## Сценарии
|
## Сценарии
|
||||||
|
|
||||||
- Выберите вступление, дождитесь готовности, проверьте пуск с начала, паузу,
|
- Покажите шаг «Введение» в плане, дождитесь готовности; в активном шаге
|
||||||
|
проверьте «Запуск», паузу,
|
||||||
продолжение, повтор с начала и остановку. Пауза не должна отматывать звук назад.
|
продолжение, повтор с начала и остановку. Пауза не должна отматывать звук назад.
|
||||||
- Перемотайте на середину в воспроизведении и на паузе; затем на конец.
|
- Перемотайте на середину в воспроизведении и на паузе; затем на конец.
|
||||||
Проверьте фактическую позицию и сохранение состояния паузы/воспроизведения.
|
Проверьте фактическую позицию и сохранение состояния паузы/воспроизведения.
|
||||||
- Переключитесь на заключительное видео: вступление прекращается. Повторите
|
- Покажите шаг «Закрывашка»: вступление прекращается. В активном шаге повторите
|
||||||
команды и перемотку, проверьте изображение и звук на внешнем дисплее.
|
команды и перемотку, проверьте изображение и звук на внешнем дисплее.
|
||||||
- Запустите музыку вместе с видео/вступлением: оба канала слышны независимо,
|
- Запустите музыку вместе с видео/вступлением: оба канала слышны независимо,
|
||||||
автоматического приглушения нет. Изменение громкости одного канала не должно
|
автоматического приглушения нет. Изменение громкости одного канала не должно
|
||||||
@@ -39,8 +40,8 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел
|
|||||||
поздний play не оживляет предыдущий ресурс.
|
поздний play не оживляет предыдущий ресурс.
|
||||||
- Закройте окно показа и откройте снова. Проверьте оба канала: звук не начинается
|
- Закройте окно показа и откройте снова. Проверьте оба канала: звук не начинается
|
||||||
до явной команды, позиции сброшены. Пульт и технические ошибки не видны на проекторе.
|
до явной команды, позиции сброшены. Пульт и технические ошибки не видны на проекторе.
|
||||||
- Проверьте отдельный тестовый режим SCREEN-001. Он не должен подменять выбранный
|
- Убедитесь, что отдельного блока «Вступление и видео» и выбора медиа нет:
|
||||||
игровой пакет автоматически во время нормального воспроизведения.
|
громкость, перемотка и статус находятся в активном медийном шаге плана.
|
||||||
|
|
||||||
## Измерение задержки
|
## Измерение задержки
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
|
|||||||
| «Следующий трек» | Переключает музыку, сохраняя паузу или воспроизведение. |
|
| «Следующий трек» | Переключает музыку, сохраняя паузу или воспроизведение. |
|
||||||
| «Повторять плейлист» | После последнего трека возвращается к первому. |
|
| «Повторять плейлист» | После последнего трека возвращается к первому. |
|
||||||
|
|
||||||
|
Вступление и видео управляются в активном шаге «Введение» или «Закрывашка»:
|
||||||
|
«Запуск», «Пауза», «Продолжить», «Стоп», «Повтор», громкость и перемотка.
|
||||||
|
Здесь же видны позиция, длительность, статус и ошибка воспроизведения.
|
||||||
|
Файл определяется режимом шага; отдельного выбора медиа нет.
|
||||||
|
Кнопки воспроизведения и перемотка доступны при открытом показе и выбранном файле.
|
||||||
Вступление и видео используют один основной канал и не звучат одновременно.
|
Вступление и видео используют один основной канал и не звучат одновременно.
|
||||||
Музыка независима, автоматически не приглушается; перед речью уменьшайте её
|
Музыка независима, автоматически не приглушается; перед речью уменьшайте её
|
||||||
громкость сами. Смена сцены останавливает основное медиа, музыка продолжает играть.
|
громкость сами. Смена сцены останавливает основное медиа, музыка продолжает играть.
|
||||||
|
|||||||
+49
-4
@@ -257,10 +257,8 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await checkDisplay(output)
|
await checkDisplay(output)
|
||||||
await evaluate(
|
await command({ action: 'play', channel: 'main' })
|
||||||
`Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).click()`,
|
await poll(async () => (await state()).channels.main.status === 'playing', 'fixture play')
|
||||||
)
|
|
||||||
await poll(async () => (await state()).channels.main.status === 'playing', 'UI fixture play')
|
|
||||||
await command({ action: 'pause', channel: 'main' })
|
await command({ action: 'pause', channel: 'main' })
|
||||||
await command({ action: 'resume', channel: 'main' })
|
await command({ action: 'resume', channel: 'main' })
|
||||||
await command({ action: 'stop', channel: 'main' })
|
await command({ action: 'stop', channel: 'main' })
|
||||||
@@ -275,6 +273,7 @@ app.whenReady().then(async () => {
|
|||||||
assert.deepEqual(range, { status: 206, size: 100 })
|
assert.deepEqual(range, { status: 206, size: 100 })
|
||||||
await command({ action: 'pause', channel: 'main' })
|
await command({ action: 'pause', channel: 'main' })
|
||||||
assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false)
|
assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false)
|
||||||
|
assert.equal(await evaluate('document.body.textContent.includes("Вступление и видео")'), false)
|
||||||
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
|
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
|
||||||
await poll(async () => {
|
await poll(async () => {
|
||||||
const s = await state()
|
const s = await state()
|
||||||
@@ -403,6 +402,52 @@ app.whenReady().then(async () => {
|
|||||||
if (index === 0)
|
if (index === 0)
|
||||||
assert.equal(await inspect('document.querySelector("img.cover").naturalWidth'), 320)
|
assert.equal(await inspect('document.querySelector("img.cover").naturalWidth'), 320)
|
||||||
else assert.equal(await inspect('document.querySelector("img.background").naturalWidth'), 320)
|
else assert.equal(await inspect('document.querySelector("img.background").naturalWidth'), 320)
|
||||||
|
if (index === 1 || index === 3) {
|
||||||
|
const clickMedia = (label) =>
|
||||||
|
evaluate(`(() => {
|
||||||
|
const row = document.querySelector('[data-step-index="${index}"]')
|
||||||
|
const button = Array.from(row.querySelectorAll('button')).find(b => b.textContent.trim() === ${JSON.stringify(label)})
|
||||||
|
if (!button || button.disabled) throw new Error('Main control unavailable')
|
||||||
|
button.click()
|
||||||
|
})()`)
|
||||||
|
await clickMedia('Запуск')
|
||||||
|
await poll(
|
||||||
|
async () => (await state()).channels.main.status === 'playing',
|
||||||
|
'plan media plays',
|
||||||
|
)
|
||||||
|
await clickMedia('Пауза')
|
||||||
|
await poll(
|
||||||
|
async () => (await state()).channels.main.status === 'paused',
|
||||||
|
'plan media paused',
|
||||||
|
)
|
||||||
|
await evaluate(`(() => {
|
||||||
|
const input = document.querySelector('#position-main')
|
||||||
|
input.focus(); input.value = '1'
|
||||||
|
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }))
|
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
||||||
|
input.blur()
|
||||||
|
})()`)
|
||||||
|
await clickMedia('Перемотать')
|
||||||
|
await poll(
|
||||||
|
async () => Math.abs((await state()).channels.main.position - 1) < 0.1,
|
||||||
|
'plan media seek',
|
||||||
|
)
|
||||||
|
await evaluate(`(() => {
|
||||||
|
const input = document.querySelector('#volume-main')
|
||||||
|
input.focus(); input.value = '42'
|
||||||
|
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }))
|
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
||||||
|
input.blur()
|
||||||
|
})()`)
|
||||||
|
await poll(async () => (await state()).channels.main.volume === 42, 'plan media volume')
|
||||||
|
assert.equal((await state()).channels.main.desired, 'paused')
|
||||||
|
assert.equal((await state()).channels.music.desired, 'playing')
|
||||||
|
await clickMedia('Стоп')
|
||||||
|
await poll(
|
||||||
|
async () => (await state()).channels.main.status === 'stopped',
|
||||||
|
'plan media stopped',
|
||||||
|
)
|
||||||
|
}
|
||||||
if (index === 2) assert.ok(await inspect('document.body.textContent.includes("Успехов")'))
|
if (index === 2) assert.ok(await inspect('document.body.textContent.includes("Успехов")'))
|
||||||
if (index === 4)
|
if (index === 4)
|
||||||
assert.ok(await inspect('document.body.textContent.includes("Спасибо за игру!")'))
|
assert.ok(await inspect('document.body.textContent.includes("Спасибо за игру!")'))
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async function refreshPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const channels = ['main', 'music'] as const
|
const channel = 'music' as const
|
||||||
const playlist = computed(() =>
|
const playlist = computed(() =>
|
||||||
state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId),
|
state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId),
|
||||||
)
|
)
|
||||||
@@ -296,6 +296,7 @@ onUnmounted(() => {
|
|||||||
:draft="planDraft"
|
:draft="planDraft"
|
||||||
:is-editing="isPlanEditing"
|
:is-editing="isPlanEditing"
|
||||||
:is-busy="isBusy || isPlanSaving"
|
:is-busy="isBusy || isPlanSaving"
|
||||||
|
:can-play-main="state.outputOpen && !!state.channels.main.source"
|
||||||
:can-edit="planStatus.key === state.catalog.key && planStatus.editable"
|
:can-edit="planStatus.key === state.catalog.key && planStatus.editable"
|
||||||
:notice="planStatus.key === state.catalog.key ? planStatus.notice : ''"
|
:notice="planStatus.key === state.catalog.key ? planStatus.notice : ''"
|
||||||
@edit="editPlan"
|
@edit="editPlan"
|
||||||
@@ -311,7 +312,49 @@ onUnmounted(() => {
|
|||||||
@clear="planDraft = []"
|
@clear="planDraft = []"
|
||||||
@update-step="(index, patch) => Object.assign(planDraft[index]!, patch)"
|
@update-step="(index, patch) => Object.assign(planDraft[index]!, patch)"
|
||||||
@command="send"
|
@command="send"
|
||||||
/>
|
>
|
||||||
|
<template #main-controls>
|
||||||
|
<label for="position-main">Позиция, секунды</label>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="positions.main"
|
||||||
|
:input-props="{ id: 'position-main' }"
|
||||||
|
:min="0"
|
||||||
|
:max="state.channels.main.duration ?? undefined"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
:disabled="
|
||||||
|
!state.outputOpen || !state.channels.main.source || isBusy || isPlanSaving
|
||||||
|
"
|
||||||
|
@click="send({ action: 'seek', channel: 'main', position: positions.main ?? 0 })"
|
||||||
|
>Перемотать</n-button
|
||||||
|
>
|
||||||
|
<label for="volume-main">Громкость, %</label>
|
||||||
|
<n-input-number
|
||||||
|
:value="state.channels.main.volume"
|
||||||
|
:input-props="{ id: 'volume-main' }"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:disabled="isBusy || isPlanSaving"
|
||||||
|
@update:value="send({ action: 'volume', channel: 'main', volume: $event ?? 0 })"
|
||||||
|
/>
|
||||||
|
<p role="status">
|
||||||
|
{{ statusLabels[state.channels.main.status] }} ·
|
||||||
|
{{ state.channels.main.position.toFixed(1) }} /
|
||||||
|
{{ state.channels.main.duration?.toFixed(1) ?? '—' }} с
|
||||||
|
</p>
|
||||||
|
<n-alert v-if="state.channels.main.error" type="error">{{
|
||||||
|
state.channels.main.error
|
||||||
|
}}</n-alert>
|
||||||
|
<details>
|
||||||
|
<summary>Программные замеры запуска</summary>
|
||||||
|
<p>{{ metrics('main') }}</p>
|
||||||
|
<p>
|
||||||
|
Приём команды → подтверждение проигрывателя. Физический звук и первый кадр
|
||||||
|
проектора здесь не измеряются.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
</ScreenPlanPanel>
|
||||||
<p role="status">{{ sceneLabels[state.scene.status] }}</p>
|
<p role="status">{{ sceneLabels[state.scene.status] }}</p>
|
||||||
<p v-if="state.timer.durationMs > 0" role="status" class="timer-status">
|
<p v-if="state.timer.durationMs > 0" role="status" class="timer-status">
|
||||||
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
|
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
|
||||||
@@ -361,51 +404,36 @@ onUnmounted(() => {
|
|||||||
aria-label="Выключить весь звук"
|
aria-label="Выключить весь звук"
|
||||||
@update:value="send({ action: 'mute', enabled: $event })"
|
@update:value="send({ action: 'mute', enabled: $event })"
|
||||||
/></label>
|
/></label>
|
||||||
<n-card
|
<n-card title="Музыка">
|
||||||
v-for="channel in channels"
|
|
||||||
:key="channel"
|
|
||||||
:title="channel === 'main' ? 'Вступление и видео' : 'Музыка'"
|
|
||||||
>
|
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<n-select
|
<n-select
|
||||||
v-if="channel === 'main'"
|
:value="state.channels.music.playlistId"
|
||||||
:value="state.channels.main.source?.id ?? null"
|
:options="state.catalog.playlists.map((p) => ({ label: p.name, value: p.id }))"
|
||||||
:options="state.catalog.media.map((s) => ({ label: s.name, value: s.id }))"
|
placeholder="Выберите плейлист"
|
||||||
placeholder="Выберите медиа"
|
|
||||||
:disabled="isBusy"
|
:disabled="isBusy"
|
||||||
:input-props="{ 'aria-label': 'Медиа показа' }"
|
:input-props="{ 'aria-label': 'Плейлист' }"
|
||||||
@update:value="send({ action: 'select', channel: 'main', sourceId: $event })"
|
@update:value="send({ action: 'playlist', channel: 'music', playlistId: $event })"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<n-select
|
||||||
<n-select
|
:value="state.channels.music.source ? state.channels.music.trackIndex : null"
|
||||||
:value="state.channels.music.playlistId"
|
:options="
|
||||||
:options="state.catalog.playlists.map((p) => ({ label: p.name, value: p.id }))"
|
(playlist?.tracks ?? []).map((t, index) => ({
|
||||||
placeholder="Выберите плейлист"
|
label: t.name || `Трек ${index + 1}`,
|
||||||
:disabled="isBusy"
|
value: index,
|
||||||
:input-props="{ 'aria-label': 'Плейлист' }"
|
}))
|
||||||
@update:value="send({ action: 'playlist', channel: 'music', playlistId: $event })"
|
"
|
||||||
/>
|
placeholder="Выберите трек"
|
||||||
<n-select
|
:disabled="isBusy"
|
||||||
:value="state.channels.music.source ? state.channels.music.trackIndex : null"
|
:input-props="{ 'aria-label': 'Трек' }"
|
||||||
:options="
|
@update:value="send({ action: 'track', channel: 'music', trackIndex: $event })"
|
||||||
(playlist?.tracks ?? []).map((t, index) => ({
|
/>
|
||||||
label: t.name || `Трек ${index + 1}`,
|
<label
|
||||||
value: index,
|
>Повторять плейлист
|
||||||
}))
|
<n-switch
|
||||||
"
|
:value="state.channels.music.loop"
|
||||||
placeholder="Выберите трек"
|
aria-label="Повторять плейлист"
|
||||||
:disabled="isBusy"
|
@update:value="send({ action: 'loop', channel: 'music', enabled: $event })"
|
||||||
:input-props="{ 'aria-label': 'Трек' }"
|
/></label>
|
||||||
@update:value="send({ action: 'track', channel: 'music', trackIndex: $event })"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
>Повторять плейлист
|
|
||||||
<n-switch
|
|
||||||
:value="state.channels.music.loop"
|
|
||||||
aria-label="Повторять плейлист"
|
|
||||||
@update:value="send({ action: 'loop', channel: 'music', enabled: $event })"
|
|
||||||
/></label>
|
|
||||||
</template>
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
:disabled="!state.outputOpen || !state.channels[channel].source || isBusy"
|
||||||
@@ -433,7 +461,6 @@ onUnmounted(() => {
|
|||||||
>Повторить с начала</n-button
|
>Повторить с начала</n-button
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="channel === 'music'"
|
|
||||||
:disabled="!state.outputOpen || !state.channels.music.source || isBusy"
|
:disabled="!state.outputOpen || !state.channels.music.source || isBusy"
|
||||||
@click="send({ action: 'next', channel: 'music' })"
|
@click="send({ action: 'next', channel: 'music' })"
|
||||||
>Следующий трек</n-button
|
>Следующий трек</n-button
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defineProps<{
|
|||||||
draft: ScreenStep[]
|
draft: ScreenStep[]
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isBusy: boolean
|
isBusy: boolean
|
||||||
|
canPlayMain: boolean
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
notice: string
|
notice: string
|
||||||
}>()
|
}>()
|
||||||
@@ -150,32 +151,36 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)">
|
<template v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)">
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="isBusy"
|
:disabled="isBusy || !canPlayMain"
|
||||||
@click="emit('command', { action: 'play', channel: 'main' })"
|
@click="emit('command', { action: 'play', channel: 'main' })"
|
||||||
>Запуск</n-button
|
>Запуск</n-button
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="isBusy"
|
:disabled="isBusy || !canPlayMain"
|
||||||
@click="emit('command', { action: 'pause', channel: 'main' })"
|
@click="emit('command', { action: 'pause', channel: 'main' })"
|
||||||
>Пауза</n-button
|
>Пауза</n-button
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="isBusy"
|
:disabled="isBusy || !canPlayMain"
|
||||||
@click="emit('command', { action: 'resume', channel: 'main' })"
|
@click="emit('command', { action: 'resume', channel: 'main' })"
|
||||||
>Продолжить</n-button
|
>Продолжить</n-button
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="isBusy"
|
:disabled="isBusy || !canPlayMain"
|
||||||
@click="emit('command', { action: 'stop', channel: 'main' })"
|
@click="emit('command', { action: 'stop', channel: 'main' })"
|
||||||
>Стоп</n-button
|
>Стоп</n-button
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="isBusy"
|
:disabled="isBusy || !canPlayMain"
|
||||||
@click="emit('command', { action: 'replay', channel: 'main' })"
|
@click="emit('command', { action: 'replay', channel: 'main' })"
|
||||||
>Повтор</n-button
|
>Повтор</n-button
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
<slot
|
||||||
|
v-if="currentIndex === index && (step.mode === 2 || step.mode === 4)"
|
||||||
|
name="main-controls"
|
||||||
|
/>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button
|
<n-button
|
||||||
|
|||||||
Reference in New Issue
Block a user