mirror of
https://hub.gitmirror.com/https://github.com/ExamAware/ExamAware2-Desktop
synced 2025-04-29 03:16:38 +00:00
新增主页布局和侧边栏菜单,创建放映器、插件和二维码扫描页面
This commit is contained in:
parent
3889f73fcf
commit
77a69e206b
@ -1,12 +1,71 @@
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.mainpage-layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mainpage-content {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
51
src/renderer/src/views/PlayerView.vue
Normal file
51
src/renderer/src/views/PlayerView.vue
Normal 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>
|
181
src/renderer/src/views/home/MainpageView.vue
Normal file
181
src/renderer/src/views/home/MainpageView.vue
Normal 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 S∞n.</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>
|
104
src/renderer/src/views/home/PlayerHomeView.vue
Normal file
104
src/renderer/src/views/home/PlayerHomeView.vue
Normal 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>
|
13
src/renderer/src/views/home/PluginView.vue
Normal file
13
src/renderer/src/views/home/PluginView.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user