feat: add easy-jyy Slidev theme

This commit is contained in:
2026-08-29 18:59:41 +08:00
parent 33152a90dc
commit 5224d45994
23 changed files with 1875 additions and 0 deletions
@@ -0,0 +1,137 @@
<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 openPresenter() {
window.location.href = `${import.meta.env.BASE_URL}presenter/`
}
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>
<button
v-if="!isPresenter && configuredHash"
class="easy-presenter-launcher"
type="button"
title="打开演示者模式"
@click="openPresenter"
>
演示者
</button>
<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>