新增主页布局和侧边栏菜单,创建放映器、插件和二维码扫描页面

This commit is contained in:
hello8693 2025-03-21 20:49:05 +08:00
parent 3889f73fcf
commit 77a69e206b
5 changed files with 409 additions and 1 deletions

View File

@ -1,12 +1,71 @@
<template> <template>
<div class="mainpage"> <div class="mainpage">
<t-layout class="mainpage-layout">
<t-aside width="60px">
<t-menu theme="dark" v-model="currentMenu" :collapsed="true" @change="handleMenuChange">
<t-menu-item value="home">
<template #icon>
<t-icon name="home" />
</template>
主页
</t-menu-item>
<t-menu-item value="playerhome">
<template #icon>
<t-icon name="play-circle" />
</template>
放映器
</t-menu-item>
<t-menu-item value="pluginpage">
<template #icon>
<t-icon name="extension" />
</template>
插件
</t-menu-item>
</t-menu>
</t-aside>
<t-content class="mainpage-content">
<router-view />
</t-content>
</t-layout>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { FileIcon } from 'tdesign-icons-vue-next' import { FileIcon } from 'tdesign-icons-vue-next'
const router = useRouter()
const route = useRoute()
const routeMap = { //
'home': '/mainpage',
'pluginpage': '/pluginpage',
'playerhome': '/playerhome',
}
const currentMenu = ref(Object.keys(routeMap).find(key => routeMap[key] === route.path) || 'home')
const handleMenuChange = (value) => {
const route = routeMap[value]
if (route) {
router.push(route)
}
}
watch(route, (newRoute) => {
currentMenu.value = Object.keys(routeMap).find(key => routeMap[key] === newRoute.path) || 'home'
})
</script> </script>
<style scoped> <style scoped>
.mainpage-layout {
height: 100%;
}
.mainpage-content {
height: 100%;
padding: 10px;
overflow: auto;
}
</style> </style>

View File

@ -0,0 +1,51 @@
<script setup lang="jsx">
import { ref, onMounted, onUnmounted } from 'vue';
import { v4 as uuidv4 } from 'uuid';
const { ipcRenderer } = window.electron;
const taskQueue = ref([]);
const intervalId = ref(null);
const configData = ref(null);
const addTask = (executeTime, taskFunction) => {
const task = {
id: uuidv4(),
executeTime,
taskFunction,
};
taskQueue.value.push(task);
taskQueue.value.sort((a, b) => a.executeTime - b.executeTime);
};
const checkTasks = () => {
const now = new Date().getTime();
while (taskQueue.value.length > 0 && taskQueue.value[0].executeTime <= now) {
const task = taskQueue.value.shift();
task.taskFunction();
}
};
onMounted(() => {
intervalId.value = setInterval(checkTasks, 200);
console.log('Sending load-config event');
ipcRenderer.on('load-config', (event, data) => {
configData.value = JSON.parse(data);
console.log('Config data loaded:', configData.value);
});
});
onUnmounted(() => {
if (intervalId.value) {
clearInterval(intervalId.value);
}
});
</script>
<template>
<div v-if="configData">
<!-- 在这里显示配置文件内容 -->
<pre>{{ configData }}</pre>
</div>
</template>

View File

@ -0,0 +1,181 @@
<template>
<t-row class="mainpage-grid">
<t-col :span="5">
<div class="main-button-wrapper">
<div class="main-button-grid">
<div class="main-button-container">
<t-button class="main-button" theme="success" @click="handleEditor">
<EditIcon size="50px" />
</t-button>
<p class="button-description">编辑器</p>
</div>
<div class="main-button-container">
<t-button class="main-button" theme="warning" @click="router.push('/playerhome')">
<PlayCircleIcon size="50px" />
</t-button>
<p class="button-description">放映器</p>
</div>
<div class="main-button-container">
<t-button class="main-button">
<LinkIcon size="50px" />
</t-button>
<p class="button-description"> URL 放映</p>
</div>
<div class="main-button-container">
<t-button class="main-button">
<ServerIcon size="50px" />
</t-button>
<p class="button-description"> ECS 放映</p>
</div>
</div>
</div>
</t-col>
<t-col :span="1" style="height: 100%">
<t-divider layout="vertical" style="height: 95%" />
</t-col>
<t-col :span="6" style="height: 99%">
<h1>扫描二维码</h1>
<!-- 右侧二维码扫描 -->
<p>Coming Sn.</p>
<div class="qrcode-container"></div>
<t-drawer v-model:visible="visible" header="选择摄像头" :close-btn="true">
<t-select v-model="selectedConstraints" placeholder="请选择摄像头" clearable>
<t-option
v-for="option in constraintOptions"
:key="option.label"
:value="option.constraints"
:label="option.label"
></t-option>
</t-select>
<qrcode-stream
@decode="onDecodeQR"
@init="onInitQR"
@camera-on="onCameraReady"
:constraints="selectedConstraints"
></qrcode-stream>
<template #footer>
<template></template>
</template>
</t-drawer>
<t-button shape="circle" theme="primary" @click="handleClickDrawerBtn" id="openDrawerBtn">
<template #icon><SettingIcon /></template>
</t-button>
</t-col>
</t-row>
</template>
<script setup lang="jsx">
import { ref, watch } from 'vue'
import { EditIcon, PlayCircleIcon, LinkIcon, ServerIcon, SettingIcon } from 'tdesign-icons-vue-next'
import { QrcodeStream } from 'vue-qrcode-reader'
import { MessagePlugin } from 'tdesign-vue-next'
import { useRouter } from 'vue-router'
const router = useRouter()
const onDecodeQR = (result) => {
console.log('二维码内容:', result)
//
}
const onInitQR = (promise) => {
promise.catch((error) => {
if (error.name === 'NotAllowedError') {
alert('没有摄像头权限')
} else if (error.name === 'NotFoundError') {
alert('没有找到摄像头')
} else {
alert('摄像头初始化失败')
}
})
}
const selectedConstraints = ref({})
const constraintOptions = ref([])
let isFirstLoad = true
const onCameraReady = async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
const videoDevices = devices.filter(({ kind }) => kind === 'videoinput')
constraintOptions.value = videoDevices.map(({ deviceId, label }) => ({
label: label || `摄像头 (ID: ${deviceId})`,
constraints: { deviceId }
}))
if (isFirstLoad && constraintOptions.value.length > 0) {
selectedConstraints.value = constraintOptions.value[0].constraints
isFirstLoad = false
}
}
watch(constraintOptions, (newOptions) => {
if (isFirstLoad && newOptions.length > 0 && !selectedConstraints.value.deviceId) {
selectedConstraints.value = newOptions[0].constraints
isFirstLoad = false
}
})
const visible = ref(false)
const handleClickDrawerBtn = () => {
visible.value = true
}
const handleEditor = () => {
window.electron.ipcRenderer.send('open-editor-window')
}
</script>
<style scoped>
.mainpage-grid,
t-col {
height: 100%;
}
.main-button-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.main-button-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
column-gap: 25px; /* 左右按钮间距 */
row-gap: 10px; /* 上下按钮间距 */
}
.main-button-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.main-button {
width: 80px;
height: 80px;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
}
.button-description {
margin-top: 10px;
font-size: 16px;
text-align: center;
}
#openDrawerBtn {
position: absolute;
bottom: 20px;
right: 8px;
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="plugin-view">
<h2>放映器</h2>
<p>选择一个 ExamAware 2 档案文件以开始放映</p>
<t-row :gutter="25">
<t-col :span="4">
<t-card class="card-button" @click="selectFile">
<div class="card-content">
<t-icon name="file" size="60px" class="card-button-icon"></t-icon>
<p>本地文件</p>
</div>
</t-card>
</t-col>
<t-col :span="8">
<t-card class="card-button" @click="openUrl">
<div class="card-content">
<t-icon name="code" size="60px" class="card-button-icon"></t-icon>
<p>更多打开方式正在开发中</p>
</div>
</t-card>
</t-col>
<!-- <t-col :span="4">
<t-card class="card-button" @click="openUrl">
<div class="card-content">
<t-icon name="link" size="60px" class="card-button-icon"></t-icon>
<p>URL</p>
</div>
</t-card>
</t-col>
<t-col :span="4">
<t-card class="card-button" @click="selectFile">
<div class="card-content">
<t-icon name="server" size="60px" class="card-button-icon"></t-icon>
<p>连接服务器</p>
</div>
</t-card>
</t-col> -->
</t-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NotifyPlugin } from 'tdesign-vue-next'
const ipcRenderer = window.electron.ipcRenderer
const filePath = ref('')
const selectFile = async () => {
const result = await ipcRenderer.invoke('select-file')
if (result) {
filePath.value = result
openPlayerWindow()
}
}
const openUrl = () => {
console.log('打开 URL')
}
const openPlayerWindow = () => {
if (filePath.value) {
ipcRenderer.send('open-player-window', filePath.value)
}
console.log('打开放映器', filePath.value)
}
</script>
<style scoped>
.plugin-view {
padding: 20px;
height: 100%;
}
h2,
p {
user-select: none;
}
.card-button {
cursor: pointer;
text-align: center;
padding: 20px;
margin-bottom: 10px;
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
}
.card-content t-icon {
margin-bottom: 10px;
}
.card-content p {
margin: 0;
}
.card-button-icon {
padding-bottom: 15px;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div class="plugin-view">
<h2>插件</h2>
<t-empty type="maintenance" description="Coming S∞n."></t-empty>
</div>
</template>
<style scoped>
.plugin-view {
padding: 20px;
}
</style>