Files
easy-slides/dist-cli/theme/components/PresenterAccess.vue
T
yulonger e23fa413be
Engine CI / test (push) Waiting to run
feat: sync interactive image previews
2026-08-30 22:20:30 +08:00

124 lines
4.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 { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const configuredHash = import.meta.env.VITE_EASY_SLIDES_PRESENTER_HASH || ''
const isPresenter = computed(() => /\/presenter(?:\/\d+)?\/?$/.test(window.location.pathname))
const storageKey = `easy-slides:presenter:${import.meta.env.BASE_URL}`
const unlocked = ref(false)
const token = ref('')
const error = ref('')
const noteSize = ref(20)
const contrast = ref(false)
const gate = ref<HTMLDialogElement>()
const tokenInput = ref<HTMLInputElement>()
onMounted(async () => {
unlocked.value = sessionStorage.getItem(storageKey) === configuredHash
|| (!configuredHash && import.meta.env.DEV)
syncBodyState()
await syncPresenterGate()
})
onBeforeUnmount(() => {
if (gate.value?.open)
gate.value.close()
document.body.classList.remove('easy-presenter-active', 'easy-presenter-locked', 'easy-notes-contrast')
})
watch(unlocked, () => void syncPresenterGate())
async function unlock() {
error.value = ''
const candidate = await sha256(token.value)
if (candidate !== configuredHash) {
error.value = 'Token 不正确'
return
}
sessionStorage.setItem(storageKey, configuredHash)
unlocked.value = true
token.value = ''
await nextTick()
syncBodyState()
}
function lock() {
sessionStorage.removeItem(storageKey)
unlocked.value = false
syncBodyState()
}
function resizeNotes(delta: number) {
noteSize.value = Math.min(38, Math.max(14, noteSize.value + delta))
document.documentElement.style.setProperty('--easy-presenter-note-size', `${noteSize.value}px`)
}
function toggleContrast() {
contrast.value = !contrast.value
syncBodyState()
}
function syncBodyState() {
document.body.classList.toggle('easy-presenter-active', isPresenter.value && unlocked.value)
document.body.classList.toggle('easy-notes-contrast', isPresenter.value && unlocked.value && contrast.value)
}
async function syncPresenterGate() {
await nextTick()
const shouldOpen = isPresenter.value && !unlocked.value
document.body.classList.toggle('easy-presenter-locked', shouldOpen)
if (!shouldOpen) {
if (gate.value?.open)
gate.value.close()
return
}
if (gate.value && !gate.value.open)
gate.value.showModal()
tokenInput.value?.focus()
}
function keepGateOpen(event: Event) {
event.preventDefault()
tokenInput.value?.focus()
}
async function sha256(value: string) {
const bytes = new TextEncoder().encode(value.trim())
const digest = await crypto.subtle.digest('SHA-256', bytes)
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')
}
</script>
<template>
<Teleport to="body">
<dialog
v-if="isPresenter && !unlocked"
ref="gate"
class="easy-presenter-gate"
aria-labelledby="easy-presenter-title"
@cancel="keepGateOpen"
@click.stop
@keydown.stop
@keyup.stop
@pointerdown.stop
>
<section class="easy-presenter-dialog">
<h1 id="easy-presenter-title">演示者模式</h1>
<p v-if="configuredHash">输入 token 后查看下一页演讲备注和计时器</p>
<p v-else>该站点没有配置演示者 token</p>
<form v-if="configuredHash" @submit.prevent="unlock">
<input ref="tokenInput" v-model="token" type="password" autocomplete="current-password" autofocus aria-label="演示者 token">
<button type="submit">解锁</button>
</form>
<p v-if="error" class="easy-presenter-error" role="alert">{{ error }}</p>
</section>
</dialog>
</Teleport>
<nav v-if="isPresenter && unlocked" class="easy-presenter-tools" aria-label="提词器设置">
<button type="button" title="减小讲稿字号" @click="resizeNotes(-2)">A</button>
<button type="button" title="增大讲稿字号" @click="resizeNotes(2)">A+</button>
<button type="button" title="切换高对比讲稿" @click="toggleContrast"></button>
<button type="button" title="锁定演示者模式" @click="lock">锁定</button>
</nav>
</template>