|
@@ -0,0 +1,152 @@
|
|
|
|
|
+<!DOCTYPE html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="UTF-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
+ <title>实时数据大屏</title>
|
|
|
|
|
+ <script src="vue.global.min.js"></script>
|
|
|
|
|
+ <link rel="stylesheet" href="index.css">
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+<div id="app">
|
|
|
|
|
+ <div v-if="profiles.length > 0" class="cards-grid">
|
|
|
|
|
+ <div v-for="(profile, index) in profiles" :key="index" class="card">
|
|
|
|
|
+ <div class="avatar-container">
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="profile.webUrl"
|
|
|
|
|
+ class="avatar-image"
|
|
|
|
|
+ @error="handleAvatarError"
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-section">
|
|
|
|
|
+ <div class="card-label">学院</div>
|
|
|
|
|
+ <div class="card-school-value">{{ profile.className}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-section">
|
|
|
|
|
+ <div class="card-label">姓名</div>
|
|
|
|
|
+ <div class="card-name-value">{{ profile.studentName}}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-divider"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-section">
|
|
|
|
|
+ <div class="card-label">成绩</div>
|
|
|
|
|
+ <div class="card-score">{{ profile.achievement ? profile.achievement : '0' }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="card-section">
|
|
|
|
|
+ <div class="card-label">违规</div>
|
|
|
|
|
+ <div class="card-violation">{{ profile.violationTimes ? profile.violationTimes : '0' }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</div>
|
|
|
|
|
+<script>
|
|
|
|
|
+ const { createApp } = Vue;
|
|
|
|
|
+ createApp({
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ profiles: [],
|
|
|
|
|
+ connected: false,
|
|
|
|
|
+ wsUrl: (() => {
|
|
|
|
|
+ // const hostname = "192.168.1.32" // 测试地址
|
|
|
|
|
+ const hostname = window.location.hostname; // 正式地址从浏览器获取
|
|
|
|
|
+ let url = `ws://${hostname}:8090/api/screen/websocket`;
|
|
|
|
|
+ const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
+ if (urlParams.toString()) {
|
|
|
|
|
+ url += `?${urlParams.toString()}`;
|
|
|
|
|
+ console.log("连接地址:", url)
|
|
|
|
|
+ }
|
|
|
|
|
+ return url;
|
|
|
|
|
+ })(),
|
|
|
|
|
+ ws: null,
|
|
|
|
|
+ maxProfiles: 5,
|
|
|
|
|
+ noDataTimeout: null,
|
|
|
|
|
+ noDataDelay: 180000 // 3分钟
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.connect();
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ connect() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.ws) {
|
|
|
|
|
+ this.ws.close();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.ws = new WebSocket(this.wsUrl);
|
|
|
|
|
+
|
|
|
|
|
+ this.ws.onopen = () => {
|
|
|
|
|
+ this.connected = true;
|
|
|
|
|
+ console.log('WebSocket已连接');
|
|
|
|
|
+ this.resetNoDataTimer();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.ws.onmessage = (event) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(event.data);
|
|
|
|
|
+ if (Array.isArray(data)) {
|
|
|
|
|
+ data.forEach(item => {
|
|
|
|
|
+ this.addNewProfile(item);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (data && typeof data === 'object') {
|
|
|
|
|
+ this.addNewProfile(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.resetNoDataTimer();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('数据解析错误:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.ws.onerror = (error) => {
|
|
|
|
|
+ console.error('WebSocket错误:', error);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.ws.onclose = () => {
|
|
|
|
|
+ this.connected = false;
|
|
|
|
|
+ console.log('WebSocket已断开');
|
|
|
|
|
+ setTimeout(() => this.connect(), 1000);
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('连接失败:', e.message);
|
|
|
|
|
+ setTimeout(() => this.connect(), 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ addNewProfile(newProfile) {
|
|
|
|
|
+ this.profiles.unshift(newProfile);
|
|
|
|
|
+ // 如果超过最大数量,移除最早的数据
|
|
|
|
|
+ if (this.profiles.length > this.maxProfiles) {
|
|
|
|
|
+ this.profiles.pop();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ resetNoDataTimer() {
|
|
|
|
|
+ if (this.noDataTimeout) {
|
|
|
|
|
+ clearTimeout(this.noDataTimeout);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.noDataTimeout = setTimeout(() => {
|
|
|
|
|
+ this.profiles = [];
|
|
|
|
|
+ }, this.noDataDelay);
|
|
|
|
|
+ },
|
|
|
|
|
+ // getFullAvatarUrl(avatar) {
|
|
|
|
|
+ // const hostname = window.location.hostname;
|
|
|
|
|
+ // return `http://${hostname}${avatar}`
|
|
|
|
|
+ // },
|
|
|
|
|
+ handleAvatarError(event) {
|
|
|
|
|
+ event.target.src = '/avatar.png'; // 图片加载失败时采用默认头像
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeUnmount() {
|
|
|
|
|
+ if (this.ws) {
|
|
|
|
|
+ this.ws.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.noDataTimeout) {
|
|
|
|
|
+ clearTimeout(this.noDataTimeout);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }).mount('#app');
|
|
|
|
|
+</script>
|
|
|
|
|
+</body>
|
|
|
|
|
+</html>
|