当前位置: 首页 > news >正文

自定义监控大屏项目实现方案

创建一个包含完整前后端的监控大屏项目,支持拖拽、鉴权、自动更新等功能。以下是详细实现步骤:

一、项目结构设计

monitor-dashboard/
├── client/                 # 前端Vue3项目
└── server/                 # 后端Node.js服务

二、后端服务搭建(Node.js)

  1. 创建后端项目
    mkdir monitor-dashboard && cd monitor-dashboard
    mkdir server && cd server
    npm init -y
    npm install express jsonwebtoken cors express-rate-limit helmet body-parser
    npm install nodemon -D

  2. 创建基础文件结构
    mkdir controllers routes middleware models utils
    touch server.js

  3. 编写服务器配置(server.js)
    const express = require('express');
    const cors = require('cors');
    const helmet = require('helmet');
    const rateLimit = require('express-rate-limit');
    const bodyParser = require('body-parser');
    const authRoutes = require('./routes/auth.routes');
    const dashboardRoutes = require('./routes/dashboard.routes');const app = express();
    const PORT = process.env.PORT || 3001;// 安全中间件
    app.use(helmet()); // 增强HTTP头安全性
    app.use(cors()); // 处理跨域
    app.use(bodyParser.json());// 限流保护
    const limiter = rateLimit({windowMs: 15 * 60 * 1000, // 15分钟max: 100 // 每IP限制请求数
    });
    app.use('/api/', limiter);// 路由
    app.use('/api/auth', authRoutes);
    app.use('/api/dashboard', dashboardRoutes);// 错误处理中间件
    app.use((err, req, res, next) => {console.error(err.stack);res.status(500).json({ message: '服务器内部错误' });
    });app.listen(PORT, () => {console.log(`服务器运行在 http://localhost:${PORT}`);
    });

  4. 创建认证中间件(middleware/auth.middleware.js)
    const jwt = require('jsonwebtoken');// JWT密钥,实际项目中应使用环境变量
    const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';// 验证令牌中间件
    const authenticateToken = (req, res, next) => {const authHeader = req.headers['authorization'];const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKENif (!token) return res.status(401).json({ message: '未提供令牌' });jwt.verify(token, JWT_SECRET, (err, user) => {if (err) return res.status(403).json({ message: '令牌无效或已过期' });req.user = user;next();});
    };// 生成新令牌
    const generateToken = (user) => {// 设置1小时过期return jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });
    };module.exports = { authenticateToken, generateToken };

  5. 创建认证路由(routes/auth.routes.js)
    const express = require('express');
    const router = express.Router();
    const { generateToken } = require('../middleware/auth.middleware');// 模拟用户数据库
    const users = [{ id: 1, username: 'admin', password: 'admin123' } // 实际项目中使用加密存储
    ];// 登录接口
    router.post('/login', (req, res) => {const { username, password } = req.body;// 查找用户const user = users.find(u => u.username === username && u.password === password);if (!user) {return res.status(401).json({ message: '用户名或密码错误' });}// 生成JWT令牌const token = generateToken(user);res.json({message: '登录成功',token,user: { id: user.id, username: user.username }});
    });// 刷新令牌接口
    router.post('/refresh-token', (req, res) => {// 在实际应用中,这里应该验证refresh tokenconst { username } = req.body;const user = users.find(u => u.username === username);if (!user) {return res.status(401).json({ message: '用户不存在' });}const newToken = generateToken(user);res.json({ token: newToken });
    });module.exports = router;

  6. 创建数据接口(routes/dashboard.routes.js)
    const express = require('express');
    const router = express.Router();
    const { authenticateToken } = require('../middleware/auth.middleware');// 模拟数据生成函数
    const generateMockData = () => {return {systemStatus: {online: Math.random() > 0.1, // 90%概率在线cpuUsage: Math.floor(Math.random() * 80) + 10, // 10-90%memoryUsage: Math.floor(Math.random() * 70) + 20, // 20-90%diskUsage: Math.floor(Math.random() * 60) + 30, // 30-90%timestamp: new Date().toISOString()},trafficData: Array.from({ length: 24 }, (_, i) => ({hour: i,value: Math.floor(Math.random() * 1000) + 100})),deviceStatus: Array.from({ length: 10 }, (_, i) => ({id: `device-${i+1}`,name: `设备 ${i+1}`,status: Math.random() > 0.2 ? '正常' : '异常', // 80%正常率temperature: Math.floor(Math.random() * 30) + 20 // 20-50°C}))};
    };// 获取仪表盘数据(需要认证)
    router.get('/data', authenticateToken, (req, res) => {// 生成模拟数据const data = generateMockData();res.json(data);
    });module.exports = router;

  7. 配置启动脚本(package.json)
    "scripts": {"start": "node server.js","dev": "nodemon server.js"
    }

三、前端项目搭建(Vue3 + ECharts)

  1. 创建 Vue3 项目
    cd ..  # 返回项目根目录
    npm create vue@latest client

      

  2. 按照提示选择:
  • 项目名称:client
  • 类型:Customize with create-vue
  • TypeScript:Yes
  • JSX Support:Yes
  • Vue Router:Yes
  • Pinia:Yes
  • ESLint:Yes
  • Prettier:Yes
  • 其他选项:No

3.进入前端目录并安装依赖

cd client
npm install echarts vuedraggable@next axios jwt-decode

4.创建基础目录结构

mkdir -p src/components/dashboard src/utils src/stores src/views

5.配置 Axios(src/utils/axios.js)

import axios from 'axios';
import { useAuthStore } from '../stores/auth';const api = axios.create({baseURL: 'http://localhost:3001/api',timeout: 5000
});// 请求拦截器 - 添加令牌
api.interceptors.request.use((config) => {const authStore = useAuthStore();if (authStore.token) {config.headers.Authorization = `Bearer ${authStore.token}`;}return config;},(error) => Promise.reject(error)
);// 响应拦截器 - 处理令牌过期
api.interceptors.response.use((response) => response,async (error) => {const originalRequest = error.config;// 如果是403错误且未尝试刷新令牌if (error.response.status === 403 && !originalRequest._retry) {originalRequest._retry = true;try {const authStore = useAuthStore();// 尝试刷新令牌const response = await axios.post('http://localhost:3001/api/auth/refresh-token', {username: authStore.user?.username});// 存储新令牌authStore.setToken(response.data.token);// 用新令牌重试原始请求originalRequest.headers.Authorization = `Bearer ${response.data.token}`;return api(originalRequest);} catch (refreshError) {// 刷新令牌失败,需要重新登录const authStore = useAuthStore();authStore.logout();return Promise.reject(refreshError);}}return Promise.reject(error);}
);export default api;

6.创建认证存储(src/stores/auth.js)

import { defineStore } from 'pinia';
import api from '../utils/axios';
import jwtDecode from 'jwt-decode';export const useAuthStore = defineStore('auth', {state: () => ({token: localStorage.getItem('token') || null,user: JSON.parse(localStorage.getItem('user')) || null,isAuthenticated: !!localStorage.getItem('token')}),actions: {// 登录async login(username, password) {try {const response = await api.post('/auth/login', { username, password });this.token = response.data.token;this.user = response.data.user;this.isAuthenticated = true;// 保存到本地存储localStorage.setItem('token', this.token);localStorage.setItem('user', JSON.stringify(this.user));// 设置定期刷新令牌(每55分钟)this.setupTokenRefresh();return true;} catch (error) {console.error('登录失败:', error);return false;}},// 登出logout() {this.token = null;this.user = null;this.isAuthenticated = false;// 清除本地存储localStorage.removeItem('token');localStorage.removeItem('user');// 清除刷新定时器if (this.refreshTimer) {clearInterval(this.refreshTimer);}},// 设置令牌自动刷新setupTokenRefresh() {// 每55分钟刷新一次令牌this.refreshTimer = setInterval(async () => {try {const response = await api.post('/auth/refresh-token', {username: this.user?.username});this.token = response.data.token;localStorage.setItem('token', this.token);} catch (error) {console.error('令牌刷新失败:', error);this.logout();}}, 55 * 60 * 1000); // 55分钟},// 检查令牌是否过期isTokenExpired() {if (!this.token) return true;try {const decoded = jwtDecode(this.token);return decoded.exp * 1000 < Date.now();} catch (error) {return true;}}}
});

7.创建仪表盘数据存储(src/stores/dashboard.js)

import { defineStore } from 'pinia';
import api from '../utils/axios';export const useDashboardStore = defineStore('dashboard', {state: () => ({data: null,loading: false,error: null,widgets: [{ id: 'system-status', type: 'system', title: '系统状态', x: 10, y: 10, width: 300, height: 200 },{ id: 'traffic-chart', type: 'traffic', title: '流量统计', x: 320, y: 10, width: 600, height: 400 },{ id: 'device-list', type: 'devices', title: '设备状态', x: 10, y: 220, width: 300, height: 300 }],updateInterval: null}),actions: {// 获取仪表盘数据async fetchData() {this.loading = true;this.error = null;try {const response = await api.get('/dashboard/data');this.data = response.data;return true;} catch (err) {this.error = '获取数据失败,请重试';console.error(err);return false;} finally {this.loading = false;}},// 启动数据自动更新(每分钟)startAutoUpdate() {this.fetchData();this.updateInterval = setInterval(() => {this.fetchData();}, 60 * 1000); // 1分钟},// 停止自动更新stopAutoUpdate() {if (this.updateInterval) {clearInterval(this.updateInterval);this.updateInterval = null;}},// 更新组件位置和大小updateWidget(id, newProps) {const index = this.widgets.findIndex(w => w.id === id);if (index !== -1) {this.widgets[index] = { ...this.widgets[index], ...newProps };}},// 添加新组件addWidget(type) {const id = `${type}-${Date.now()}`;this.widgets.push({id,type,title: this.getWidgetTitle(type),x: 10,y: 10,width: 300,height: 200});},// 获取组件标题getWidgetTitle(type) {const titles = {system: '系统状态',traffic: '流量统计',devices: '设备状态'};return titles[type] || '新组件';}}
});

8.创建拖拽组件容器(src/components/dashboard/DashboardContainer.vue)

<template><div class="dashboard-container"><draggable v-model="widgets" :sort="false"class="widgets-wrapper"><template #item="{ element }"><Widget :widget="element" @update:widget="updateWidget"/></template></draggable><div class="add-widgets"><button @click="addWidget('system')">添加系统状态</button><button @click="addWidget('traffic')">添加流量统计</button><button @click="addWidget('devices')">添加设备状态</button></div></div>
</template><script setup>
import { ref, computed } from 'vue';
import { useDashboardStore } from '../../stores/dashboard';
import Widget from './Widget.vue';
import draggable from 'vuedraggable';const dashboardStore = useDashboardStore();
const widgets = computed({get: () => dashboardStore.widgets,set: (value) => { dashboardStore.widgets = value; }
});const updateWidget = (id, newProps) => {dashboardStore.updateWidget(id, newProps);
};const addWidget = (type) => {dashboardStore.addWidget(type);
};
</script><style scoped>
.dashboard-container {position: relative;width: 100%;min-height: 800px;padding: 20px;background-color: #1a1a2e;
}.widgets-wrapper {display: grid;grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));gap: 20px;width: 100%;
}.add-widgets {position: fixed;bottom: 20px;right: 20px;display: flex;gap: 10px;
}.add-widgets button {padding: 8px 16px;background-color: #4CAF50;color: white;border: none;border-radius: 4px;cursor: pointer;
}
</style>

9.创建可拖拽调整大小的组件(src/components/dashboard/Widget.vue)

<template><div class="widget":style="{width: `${widget.width}px`,height: `${widget.height}px`,left: `${widget.x}px`,top: `${widget.y}px`}"v-draggable="dragOptions"><div class="widget-header"><h3>{{ widget.title }}</h3><div class="resize-handle" @mousedown="startResize"></div></div><div class="widget-content"><component :is="getWidgetComponent()" :data="getWidgetData()"/></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useDashboardStore } from '../../stores/dashboard';
import SystemStatus from './widgets/SystemStatus.vue';
import TrafficChart from './widgets/TrafficChart.vue';
import DeviceList from './widgets/DeviceList.vue';
import { draggable } from 'vuedraggable';const props = defineProps({widget: {type: Object,required: true}
});const emit = defineEmits(['update:widget']);
const dashboardStore = useDashboardStore();
const isResizing = ref(false);
const startX = ref(0);
const startY = ref(0);
const startWidth = ref(0);
const startHeight = ref(0);// 拖拽配置
const dragOptions = {onEnd: (event) => {emit('update:widget', props.widget.id, {x: event.clientX,y: event.clientY});}
};// 根据类型获取组件
const getWidgetComponent = () => {const components = {system: SystemStatus,traffic: TrafficChart,devices: DeviceList};return components[props.widget.type] || SystemStatus;
};// 获取组件所需数据
const getWidgetData = () => {if (!dashboardStore.data) return null;const dataMap = {system: dashboardStore.data.systemStatus,traffic: dashboardStore.data.trafficData,devices: dashboardStore.data.deviceStatus};return dataMap[props.widget.type] || null;
};// 开始调整大小
const startResize = (e) => {e.preventDefault();isResizing.value = true;startX.value = e.clientX;startY.value = e.clientY;startWidth.value = props.widget.width;startHeight.value = props.widget.height;document.addEventListener('mousemove', resize);document.addEventListener('mouseup', stopResize);
};// 调整大小
const resize = (e) => {if (!isResizing.value) return;const newWidth = Math.max(200, startWidth.value + (e.clientX - startX.value));const newHeight = Math.max(150, startHeight.value + (e.clientY - startY.value));emit('update:widget', props.widget.id, {width: newWidth,height: newHeight});
};// 停止调整大小
const stopResize = () => {isResizing.value = false;document.removeEventListener('mousemove', resize);document.removeEventListener('mouseup', stopResize);
};onMounted(() => {// 如果数据未加载,触发一次加载if (!dashboardStore.data) {dashboardStore.fetchData();}
});
</script><style scoped>
.widget {position: absolute;background-color: #252a41;border-radius: 8px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);color: white;overflow: hidden;
}.widget-header {padding: 10px 15px;background-color: #16213e;display: flex;justify-content: space-between;align-items: center;cursor: move;
}.widget-header h3 {margin: 0;font-size: 16px;
}.widget-content {padding: 15px;width: 100%;height: calc(100% - 44px);box-sizing: border-box;
}.resize-handle {width: 15px;height: 15px;background-color: #4CAF50;border-radius: 0 0 0 5px;cursor: nwse-resize;
}
</style>

    10.创建系统状态组件(src/components/dashboard/widgets/SystemStatus.vue)

    <template><div class="system-status"><div class="status-indicator"><span :class="data?.online ? 'online' : 'offline'"></span><span>{{ data?.online ? '在线' : '离线' }}</span></div><div class="metrics"><div class="metric"><span>CPU使用率</span><span class="value">{{ data?.cpuUsage }}%</span></div><div class="metric"><span>内存使用率</span><span class="value">{{ data?.memoryUsage }}%</span></div><div class="metric"><span>磁盘使用率</span><span class="value">{{ data?.diskUsage }}%</span></div></div><div class="update-time">最后更新: {{ formatTime(data?.timestamp) }}</div></div>
    </template><script setup>
    import { defineProps } from 'vue';const props = defineProps({data: {type: Object,default: null}
    });const formatTime = (timestamp) => {if (!timestamp) return '无数据';const date = new Date(timestamp);return date.toLocaleString();
    };
    </script><style scoped>
    .system-status {display: flex;flex-direction: column;height: 100%;
    }.status-indicator {display: flex;align-items: center;gap: 8px;margin-bottom: 20px;
    }.status-indicator span:first-child {width: 12px;height: 12px;border-radius: 50%;display: inline-block;
    }.online {background-color: #4CAF50;
    }.offline {background-color: #f44336;
    }.metrics {flex-grow: 1;display: flex;flex-direction: column;justify-content: center;gap: 15px;
    }.metric {display: flex;justify-content: space-between;
    }.metric .value {font-weight: bold;
    }.update-time {margin-top: auto;font-size: 12px;color: #aaa;text-align: right;
    }
    </style>

    11.创建流量图表组件(src/components/dashboard/widgets/TrafficChart.vue)

    <template><div class="traffic-chart" ref="chartContainer"></div>
    </template><script setup>
    import { defineProps, ref, onMounted, watch } from 'vue';
    import * as echarts from 'echarts';const props = defineProps({data: {type: Array,default: () => []}
    });const chartContainer = ref(null);
    let chart = null;// 初始化图表
    const initChart = () => {if (chart) {chart.dispose();}chart = echarts.init(chartContainer.value);// 设置图表配置const option = {grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: {type: 'category',data: props.data.map(item => `${item.hour}:00`),axisLine: {lineStyle: {color: '#666'}},axisLabel: {color: '#ccc'}},yAxis: {type: 'value',axisLine: {lineStyle: {color: '#666'}},axisLabel: {color: '#ccc'}},series: [{data: props.data.map(item => item.value),type: 'line',smooth: true,areaStyle: {color: {type: 'linear',x: 0,y: 0,x2: 0,y2: 1,colorStops: [{ offset: 0, color: 'rgba(76, 175, 80, 0.6)' },{ offset: 1, color: 'rgba(76, 175, 80, 0)' }]}},lineStyle: {color: '#4CAF50'},itemStyle: {color: '#4CAF50'}}]};chart.setOption(option);
    };// 监听数据变化,更新图表
    watch(() => props.data,(newData) => {if (newData.length && chartContainer.value) {initChart();}},{ deep: true }
    );// 窗口大小变化时重绘图表
    const handleResize = () => {if (chart) {chart.resize();}
    };onMounted(() => {if (props.data.length) {initChart();}window.addEventListener('resize', handleResize);
    });// 清理
    onUnmounted(() => {window.removeEventListener('resize', handleResize);if (chart) {chart.dispose();}
    });
    </script><style scoped>
    .traffic-chart {width: 100%;height: 100%;
    }
    </style>

    12.创建设备列表组件(src/components/dashboard/widgets/DeviceList.vue)

    <template><div class="device-list"><div class="list-header"><div class="id-column">ID</div><div class="name-column">名称</div><div class="status-column">状态</div><div class="temp-column">温度</div></div><div class="list-body"><div class="device-item" v-for="device in data" :key="device.id"><div class="id-column">{{ device.id }}</div><div class="name-column">{{ device.name }}</div><div class="status-column"><span :class="device.status === '正常' ? 'status-normal' : 'status-error'">{{ device.status }}</span></div><div class="temp-column">{{ device.temperature }}°C</div></div><div v-if="!data || data.length === 0" class="no-data">暂无设备数据</div></div></div>
    </template><script setup>
    import { defineProps } from 'vue';const props = defineProps({data: {type: Array,default: () => []}
    });
    </script><style scoped>
    .device-list {display: flex;flex-direction: column;height: 100%;overflow: hidden;
    }.list-header {display: flex;padding: 8px 0;font-weight: bold;border-bottom: 1px solid #444;
    }.list-body {flex-grow: 1;overflow-y: auto;padding: 5px 0;
    }.device-item {display: flex;padding: 8px 0;border-bottom: 1px solid #333;
    }.device-item:last-child {border-bottom: none;
    }.id-column {width: 20%;
    }.name-column {width: 30%;
    }.status-column {width: 25%;
    }.temp-column {width: 25%;
    }.status-normal {color: #4CAF50;
    }.status-error {color: #f44336;
    }.no-data {text-align: center;padding: 20px;color: #aaa;
    }
    </style>

    13.创建登录页面(src/views/LoginView.vue)

    <template><div class="login-container"><div class="login-form"><h2>监控大屏登录</h2><div class="form-group"><label for="username">用户名</label><input type="text" id="username" v-model="username" placeholder="请输入用户名"></div><div class="form-group"><label for="password">密码</label><input type="password" id="password" v-model="password" placeholder="请输入密码"></div><button @click="handleLogin" :disabled="loading"><span v-if="loading">登录中...</span><span v-else>登录</span></button><p class="error-message" v-if="error">{{ error }}</p></div></div>
    </template><script setup>
    import { ref } from 'vue';
    import { useRouter } from 'vue-router';
    import { useAuthStore } from '../stores/auth';const username = ref('admin');
    const password = ref('admin123');
    const loading = ref(false);
    const error = ref('');
    const router = useRouter();
    const authStore = useAuthStore();const handleLogin = async () => {if (!username.value || !password.value) {error.value = '请输入用户名和密码';return;}loading.value = true;error.value = '';try {const success = await authStore.login(username.value, password.value);if (success) {router.push('/dashboard');} else {error.value = '登录失败,请检查用户名和密码';}} catch (err) {error.value = '登录过程中发生错误';console.error(err);} finally {loading.value = false;}
    };
    </script><style scoped>
    .login-container {display: flex;justify-content: center;align-items: center;height: 100vh;background-color: #f5f5f5;
    }.login-form {width: 350px;padding: 30px;background-color: white;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }.login-form h2 {text-align: center;margin-bottom: 25px;color: #333;
    }.form-group {margin-bottom: 20px;
    }.form-group label {display: block;margin-bottom: 8px;color: #666;
    }.form-group input {width: 100%;padding: 10px;border: 1px solid #ddd;border-radius: 4px;box-sizing: border-box;
    }.login-form button {width: 100%;padding: 12px;background-color: #4CAF50;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 16px;
    }.login-form button:disabled {background-color: #cccccc;cursor: not-allowed;
    }.error-message {color: #f44336;text-align: center;margin-top: 15px;margin-bottom: 0;
    }
    </style>

    14.创建仪表盘页面(src/views/DashboardView.vue)

    <template><div class="dashboard-view"><header><h1>自定义监控大屏</h1><button @click="handleLogout">退出登录</button></header><main><DashboardContainer /></main></div>
    </template><script setup>
    import { onMounted } from 'vue';
    import { useRouter } from 'vue-router';
    import { useAuthStore } from '../stores/auth';
    import { useDashboardStore } from '../stores/dashboard';
    import DashboardContainer from '../components/dashboard/DashboardContainer.vue';const router = useRouter();
    const authStore = useAuthStore();
    const dashboardStore = useDashboardStore();// 检查登录状态
    onMounted(() => {if (!authStore.isAuthenticated || authStore.isTokenExpired()) {router.push('/login');return;}// 启动数据自动更新dashboardStore.startAutoUpdate();
    });// 退出登录
    const handleLogout = () => {authStore.logout();dashboardStore.stopAutoUpdate();router.push('/login');
    };
    </script><style scoped>
    .dashboard-view {width: 100%;min-height: 100vh;background-color: #1a1a2e;color: white;
    }header {display: flex;justify-content: space-between;align-items: center;padding: 15px 30px;background-color: #16213e;
    }header h1 {margin: 0;font-size: 24px;
    }header button {padding: 8px 16px;background-color: #f44336;color: white;border: none;border-radius: 4px;cursor: pointer;
    }main {padding: 20px;
    }
    </style>

    15.配置路由(src/router/index.js)

    import { createRouter, createWebHistory } from 'vue-router';
    import { useAuthStore } from '../stores/auth';
    import LoginView from '../views/LoginView.vue';
    import DashboardView from '../views/DashboardView.vue';const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/login',name: 'login',component: LoginView},{path: '/dashboard',name: 'dashboard',component: DashboardView,meta: { requiresAuth: true }},{path: '/',redirect: '/dashboard'}]
    });// 路由守卫 - 检查认证状态
    router.beforeEach((to, from, next) => {const authStore = useAuthStore();// 如果路由需要认证if (to.meta.requiresAuth) {// 检查是否已登录且令牌未过期if (authStore.isAuthenticated && !authStore.isTokenExpired()) {next();} else {// 未登录或令牌过期,重定向到登录页next('/login');}} else {// 不需要认证的路由next();}
    });export default router;

    16.启动前端服务

    四、项目运行说明

    五、功能说明

    这个项目提供了一个完整的监控大屏解决方案,你可以根据实际需求扩展更多类型的组件和数据接口。

    1. 先启动后端服务
      cd server
      npm run dev

    2. 再启动前端服务
      cd ../client
      npm run dev

    3. 访问 http://localhost:5173 即可看到登录页面,使用默认账号密码 admin/admin123 登录

    4. 登录后进入监控大屏,可以:

      • 拖拽组件改变位置
      • 拖动右下角调整组件大小
      • 点击底部按钮添加新组件
      • 数据会每分钟自动更新
      • 令牌会每 55 分钟自动刷新
      • 长时间未操作也不会因令牌过期而退出
    5. 拖拽功能:使用 vuedraggable 实现组件拖拽,自定义指令实现大小调整
    6. 鉴权功能:基于 JWT 的认证机制,保护 API 接口
    7. 令牌自动更新:55 分钟自动刷新一次令牌,避免会话过期
    8. 数据自动更新:每分钟自动获取最新数据并更新图表
    9. 响应式设计:图表会根据组件大小自动调整

    这个项目提供了一个完整的监控大屏解决方案,你可以根据实际需求扩展更多类型的组件和数据接口。

    http://www.dtcms.com/a/536640.html

    相关文章:

  1. h5游戏免费下载:HTML5拉杆子过关小游戏
  2. 电商系统经典陷阱
  3. 5.5类的主方法
  4. 带后台的php网站模板营销推广是一种什么的促销方式
  5. 神经网络进化史:从理论到变革
  6. 系统集成项目管理工程师案例分析:整合管理高频考点精要
  7. 快速达建网站怎么给餐饮店做网站
  8. 国产化Excel开发组件Spire.XLS教程:使用Python将CSV转换为XML(处理现实数据问题)
  9. 常用软件下载地址
  10. 开网站做外贸东莞市阳光网
  11. 面向光学引导热红外无人机图像超分辨率的引导解耦网络
  12. Java医院管理系统HIS源码带小程序和安装教程
  13. 自监督 YOLO:利用对比学习实现标签高效的目标检测
  14. 快速排序(Quick Sort)详解与图解
  15. NB-IOT(4) :从媒体接入到数据传输的全链路解析
  16. 如何使用Advanced Installer打包C#程序生成安装程序
  17. 做网站的开题报告怎么写云服务器
  18. 产品公司网站建设方案模板美味的树莓派wordpress
  19. Word VBA中的Collapse方法详解
  20. 介绍一下Spring Cloud LoadBalancer
  21. 写作网站排名南京专业网站优化公司
  22. 今日印度股市最新行情与实时走势分析(截至2025年10月27日)
  23. KingbaseES数据库操作指南(2):SQL语法从入门到精通
  24. 介绍一个不错的新闻源汇总开源Github项目 BestBlogs
  25. 第3章 运行时数据区概述及线程
  26. 深入理解C语言函数栈帧:程序运行的底层密码
  27. 谷歌网站怎么做外链站长工具端口查询
  28. FPGA DDR3实战(十):基于DDR3的高速故障录播系统(二)—— 数据流转换与时钟域设计
  29. 运维蓝图 用多工具组合把 iOS 混淆变成可复用的工程能力(iOS 混淆 IPA 加固 )
  30. Caddyfile:用最简单的方式配置最现代的 Web 服务器