191 lines
5.1 KiB
Vue
191 lines
5.1 KiB
Vue
<template>
|
|
<view class="terminal-page">
|
|
<ScreenPlayer
|
|
v-if="terminalState === 'playing' && playlist.items.length"
|
|
:items="playlist.items"
|
|
@cycle-end="refreshPlaylist"
|
|
/>
|
|
|
|
<view v-else class="status-page">
|
|
<view class="brand">华莱士</view>
|
|
|
|
<template v-if="terminalState === 'binding'">
|
|
<view class="status-title">绑定门店后即可播放</view>
|
|
<image class="qr-code" :src="qrImageUrl" mode="aspectFit" />
|
|
<view class="status-tip">请使用华莱士小程序扫码绑定本门店电视</view>
|
|
<view class="device-code">设备编号:{{ deviceSn }}</view>
|
|
<button v-if="isMock" class="mock-action" @tap="completeMockBinding">
|
|
模拟完成绑定
|
|
</button>
|
|
</template>
|
|
|
|
<template v-else-if="terminalState === 'loading'">
|
|
<view class="status-title">广告终端启动中</view>
|
|
<view class="status-tip">正在读取终端状态</view>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<view class="status-title">暂时无法播放广告</view>
|
|
<view class="status-tip">{{ errorMessage || '请检查网络连接后重试' }}</view>
|
|
<button class="retry-action" @tap="initialize">重新连接</button>
|
|
</template>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
|
import ScreenPlayer from '@/components/ScreenPlayer/ScreenPlayer.vue'
|
|
import { TERMINAL_CONFIG } from '@/config/terminal'
|
|
import { bindMockTerminal, fetchTerminalBinding, fetchTerminalPlaylist } from '@/services/terminal'
|
|
import type { Playlist } from '@/types/terminal'
|
|
import { getOrCreateDeviceSn } from '@/utils/device'
|
|
|
|
type TerminalState = 'loading' | 'binding' | 'playing' | 'error'
|
|
|
|
const terminalState = ref<TerminalState>('loading')
|
|
const deviceSn = ref('')
|
|
const errorMessage = ref('')
|
|
const playlist = reactive<Playlist>({ revision: '', items: [] })
|
|
const isMock = TERMINAL_CONFIG.useMock
|
|
|
|
let bindingTimer: ReturnType<typeof setInterval> | undefined
|
|
let mockAutoBindTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
|
const bindingUrl = computed(() => {
|
|
const separator = TERMINAL_CONFIG.bindPageUrl.includes('?') ? '&' : '?'
|
|
return `${TERMINAL_CONFIG.bindPageUrl}${separator}sn=${encodeURIComponent(deviceSn.value)}`
|
|
})
|
|
|
|
/** Mock 阶段由公共二维码服务将真实绑定链接编码为可扫码图片。正式版替换为本地 QR 组件。 */
|
|
const qrImageUrl = computed(() =>
|
|
`https://api.qrserver.com/v1/create-qr-code/?size=420x420&margin=12&data=${encodeURIComponent(bindingUrl.value)}`
|
|
)
|
|
|
|
const clearTimers = () => {
|
|
if (bindingTimer) clearInterval(bindingTimer)
|
|
if (mockAutoBindTimer) clearTimeout(mockAutoBindTimer)
|
|
bindingTimer = undefined
|
|
mockAutoBindTimer = undefined
|
|
}
|
|
|
|
const refreshPlaylist = async () => {
|
|
try {
|
|
const nextPlaylist = await fetchTerminalPlaylist(deviceSn.value)
|
|
playlist.revision = nextPlaylist.revision
|
|
playlist.items = nextPlaylist.items
|
|
} catch (error: any) {
|
|
errorMessage.value = error?.message || '广告节目单加载失败'
|
|
}
|
|
}
|
|
|
|
const checkBinding = async () => {
|
|
try {
|
|
const binding = await fetchTerminalBinding(deviceSn.value)
|
|
if (!binding.bound) {
|
|
terminalState.value = 'binding'
|
|
return
|
|
}
|
|
|
|
clearTimers()
|
|
await refreshPlaylist()
|
|
terminalState.value = playlist.items.length ? 'playing' : 'error'
|
|
if (!playlist.items.length) errorMessage.value = '门店暂未配置广告节目单'
|
|
} catch (error: any) {
|
|
terminalState.value = 'error'
|
|
errorMessage.value = error?.message || '终端状态读取失败'
|
|
}
|
|
}
|
|
|
|
const completeMockBinding = async () => {
|
|
await bindMockTerminal()
|
|
await checkBinding()
|
|
}
|
|
|
|
const initialize = async () => {
|
|
clearTimers()
|
|
terminalState.value = 'loading'
|
|
errorMessage.value = ''
|
|
deviceSn.value = getOrCreateDeviceSn()
|
|
await checkBinding()
|
|
|
|
if (terminalState.value !== 'binding') return
|
|
bindingTimer = setInterval(checkBinding, TERMINAL_CONFIG.bindingPollIntervalMs)
|
|
if (isMock) {
|
|
mockAutoBindTimer = setTimeout(completeMockBinding, TERMINAL_CONFIG.mockAutoBindAfterMs)
|
|
}
|
|
}
|
|
|
|
onMounted(initialize)
|
|
onBeforeUnmount(clearTimers)
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.terminal-page,
|
|
.status-page {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
|
|
.status-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: radial-gradient(circle at 50% 44%, #333333 0%, #171717 46%, #0d0d0d 100%);
|
|
color: #ffffff;
|
|
}
|
|
|
|
.brand {
|
|
margin-bottom: 42rpx;
|
|
font-size: 68rpx;
|
|
font-weight: 700;
|
|
letter-spacing: 14rpx;
|
|
}
|
|
|
|
.status-title {
|
|
font-size: 40rpx;
|
|
font-weight: 600;
|
|
letter-spacing: 2rpx;
|
|
}
|
|
|
|
.status-tip {
|
|
margin-top: 16rpx;
|
|
color: rgba(255, 255, 255, 0.62);
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.qr-code {
|
|
width: 360rpx;
|
|
height: 360rpx;
|
|
margin-top: 34rpx;
|
|
border-radius: 12rpx;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.device-code {
|
|
margin-top: 20rpx;
|
|
color: rgba(255, 255, 255, 0.42);
|
|
font-family: monospace;
|
|
font-size: 20rpx;
|
|
letter-spacing: 1rpx;
|
|
}
|
|
|
|
.mock-action,
|
|
.retry-action {
|
|
margin-top: 32rpx;
|
|
padding: 0 34rpx;
|
|
border: 1rpx solid rgba(255, 255, 255, 0.42);
|
|
border-radius: 6rpx;
|
|
background: transparent;
|
|
color: rgba(255, 255, 255, 0.85);
|
|
font-size: 24rpx;
|
|
line-height: 66rpx;
|
|
}
|
|
|
|
.mock-action::after,
|
|
.retry-action::after {
|
|
border: none;
|
|
}
|
|
</style>
|