码桶
发现社区成员的开源项目
ScreenSaver.vue7.6 KB
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { usePanelStore } from '@/stores/panel'
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()
const panel = usePanelStore()
const now = ref(new Date())
const battery = ref('电量未知')
const offset = ref({ x: 0, y: 0 })
const containerRef = ref<HTMLElement | null>(null)
let timer = 0
let moveTimer = 0
let wakeLock: any = null
const timeText = computed(() => {
const pad = (value: number) => String(value).padStart(2, '0')
return `${pad(now.value.getHours())}:${pad(now.value.getMinutes())}`
})
const dateText = computed(() => {
const pad = (value: number) => String(value).padStart(2, '0')
const weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.value.getDay()]
return `${now.value.getFullYear()}-${pad(now.value.getMonth() + 1)}-${pad(now.value.getDate())} ${weekday}`
})
function moveScreen() {
const width = window.innerWidth || document.documentElement.clientWidth
const height = window.innerHeight || document.documentElement.clientHeight
const rect = containerRef.value?.getBoundingClientRect()
const safeX = Math.max(24, Math.floor((width - (rect?.width || 620)) / 2) - 28)
const safeY = Math.max(24, Math.floor((height - (rect?.height || 320)) / 2) - 28)
offset.value = {
x: Math.round((Math.random() * 2 - 1) * safeX),
y: Math.round((Math.random() * 2 - 1) * safeY),
}
}
async function readBattery() {
const nav = navigator as any
if (!nav.getBattery) return
try {
const info = await nav.getBattery()
battery.value = `电量 ${Math.round(info.level * 100)}%`
} catch {
battery.value = '电量未知'
}
}
async function enter() {
document.body.style.overflow = 'hidden'
timer = window.setInterval(() => {
now.value = new Date()
void readBattery()
}, 1000)
moveTimer = window.setInterval(moveScreen, 2 * 60 * 1000)
now.value = new Date()
offset.value = { x: 0, y: 0 }
void readBattery()
try {
if ('wakeLock' in navigator) {
wakeLock = await (navigator as any).wakeLock.request('screen')
}
} catch {
wakeLock = null
}
try {
if (!document.fullscreenElement) await document.documentElement.requestFullscreen()
} catch {
// 浏览器可能拒绝全屏,覆盖层仍然可用。
}
}
async function leave() {
document.body.style.overflow = 'auto'
if (timer) window.clearInterval(timer)
if (moveTimer) window.clearInterval(moveTimer)
timer = 0
moveTimer = 0
try {
await wakeLock?.release?.()
} catch {
// ignore
}
wakeLock = null
try {
if (document.fullscreenElement) await document.exitFullscreen()
} catch {
// ignore
}
}
watch(() => props.show, show => {
if (show) void enter()
else void leave()
})
onMounted(() => {
if (props.show) void enter()
})
onUnmounted(() => {
void leave()
})
</script>
<template>
<Teleport to="body">
<Transition name="screen-fade">
<div v-if="show" class="screen-saver" @click="emit('close')">
<div
ref="containerRef"
class="screen-container"
:style="{
transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`,
}"
>
<div class="screen-left">
<div class="screen-brand">FlowLoss</div>
<div class="screen-time">{{ timeText }}</div>
<div class="screen-date">{{ dateText }}</div>
<div class="screen-device">
<span>{{ battery }}</span>
<span>运行 {{ panel.runTimeShow || '00s' }}</span>
</div>
</div>
<div class="screen-right">
<div class="screen-stat">
<span>总流量</span>
<strong>{{ panel.show.allUsed || '-' }}</strong>
</div>
<div class="screen-stat">
<span>{{ panel.isRunning ? '实时速度' : '平均速度' }}</span>
<strong>{{ panel.show.speed || '-' }}</strong>
</div>
<div class="screen-stat">
<span>{{ panel.isRunning ? '实时带宽' : '平均带宽' }}</span>
<strong>{{ panel.show.speedBit || '-' }}</strong>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.screen-saver {
position: fixed;
inset: 0;
z-index: 120;
overflow: hidden;
background: #000;
color: #f5f5f5;
}
.screen-container {
position: absolute;
top: 50%;
left: 50%;
display: grid;
grid-template-columns: minmax(280px, 1fr) 200px;
align-items: center;
gap: clamp(28px, 4vw, 48px);
width: min(580px, calc(100vw - 48px));
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
transition: transform 1.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.screen-left {
min-width: 0;
}
.screen-brand {
display: inline-flex;
align-items: center;
height: 30px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: rgba(138, 152, 179, 0.96);
font-family: "Audiowide", Bahnschrift, "Arial Narrow", "Segoe UI", Arial, sans-serif;
font-size: 26px;
font-weight: 400;
letter-spacing: 0;
line-height: 1;
text-transform: uppercase;
}
.screen-time {
font-family: var(--font-mono);
margin-top: 22px;
font-size: clamp(44px, 7vw, 80px);
font-weight: 750;
letter-spacing: 0;
line-height: 1;
}
.screen-date {
margin-top: 12px;
color: rgba(255, 255, 255, 0.68);
font-size: 15px;
}
.screen-device {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
color: rgba(255, 255, 255, 0.54);
font-size: 12px;
}
.screen-device span {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0;
border-radius: 0;
background: transparent;
}
.screen-right {
display: grid;
gap: 24px;
}
.screen-stat {
display: grid;
gap: 5px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.screen-stat span {
color: rgba(255, 255, 255, 0.52);
font-size: 16px;
font-weight: 600;
}
.screen-stat strong {
color: rgba(255, 255, 255, 0.92);
font-family: var(--font-mono);
font-size: 26px;
font-weight: 750;
line-height: 1.1;
overflow-wrap: anywhere;
}
.screen-fade-enter-active,
.screen-fade-leave-active {
transition: opacity 0.2s ease;
}
.screen-fade-enter-from,
.screen-fade-leave-to {
opacity: 0;
}
@media (max-width: 720px) {
.screen-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
width: auto;
min-height: auto;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
}
.screen-left {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.screen-brand {
font-size: 26px;
}
.screen-time {
margin-top: 18px;
font-size: clamp(48px, 16vw, 72px);
}
.screen-date {
margin-top: 8px;
font-size: 14px;
}
.screen-device {
justify-content: center;
margin-top: 10px;
font-size: 11px;
}
.screen-device span {
padding: 0;
border-radius: 0;
background: transparent;
}
.screen-right {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
margin-top: 32px;
}
.screen-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.screen-stat span {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.72);
}
.screen-stat strong {
font-size: 16px;
}
}
</style>