简介:这是基于Vue和JavaScript开发的AJ-report大屏驾驶舱设计源码。AJ-report是一套专注于报表设计与大屏展示的开源框架,面向需要建设数据可视化大屏的前端与全栈开发者,可用于业务报表、运营监控、指挥中心等场景,适合有一定Vue和JavaScript基础、希望快速搭建驾驶舱页面的技术人员参考学习。压缩包共928个文件,整体容量约63.9MB,主体包括181个JavaScript文件、179个Java文件、171个Vue文件、209个PNG图片,以及JSON、Markdown、XML、CSS、SCSS等辅助资源,覆盖组件页面、报表逻辑、后端接口、配置样例与说明文档,目录结构清晰,前端页面与后端服务分离,便于按模块阅读理解和二次开发。文件中还包含Dockerfile、SQL脚本和shell脚本,可辅助本地环境搭建、数据库初始化与快速部署;同时附带JAR包、字体及图标样式资源,适合直接构建报表大屏原型并进一步扩展。目前已有581人浏览或学习,可作为理解AJ-report框架、参考大屏布局设计、学习图表组件封装与数据绑定的基础源码。
1. 大屏驾驶舱的切入点:为什么是AJ-report + Vue + JavaScript
第一次接到大屏驾驶舱的需求时,如果只盯着“好看”两个字,很容易把项目做成一张静态PPT。真正的大屏驾驶舱要解决两个问题:数据从哪来,以及视图怎么在固定分辨率下稳定呈现。AJ-report擅长把SQL查询变成可复用的报表数据集,Vue和JavaScript则负责把数据变成可交互的屏幕组件。“基于Vue和JavaScript的AJ-report大屏驾驶舱设计源码”,说的就是把这两层打通的一套前端骨架,而不是某个现成的模板下载包。
适合正在给AJ-report做二次开发、需要从零搭一个可投屏看板的工程师。下文会从数据模型讲起,再到Vue组件结构和请求封装,最后落到缩放与部署的细节。
2. 先理清AJ-report大屏驾驶舱的设计模型与数据流
2.1 从JSONScheme组件树到Vue动态组件
AJ-report在保存大屏看板时,会把画布上的组件位置、数据绑定和刷新时间写成一个JSON结构。前端拿到这个结构,才能知道哪里放KPI、哪里放图表。一个典型的组件节点如下:
{ "screenId": "dash-01", "refreshSec": 30, "components": [ { "id": "kpi1", "type": "KPI", "x": 1, "y": 2, "w": 6, "h": 4, "datasetId": 1001, "enabled": true }, { "id": "line1", "type": "LineChart", "x": 8, "y": 2, "w": 18, "h": 10, "datasetId": 1002, "enabled": true } ] }每个组件都带有绝对坐标和宽高。x、y是左上角相对于画布的百分比坐标,w、h是组件宽度和高度。前端把这份JSON传给一个ScreenRenderer,通过v-for循环组件,再根据type找到对应的Vue组件。这里的关键是不要给每个图表单独写路由,而是写一套“组件注册表”:
import KpiCard from './components/KpiCard.vue' import TrendChart from './components/TrendChart.vue' const componentMap = { KPI: KpiCard, LineChart: TrendChart, BarChart: TrendChart } export function resolveComponent(type) { return componentMap[type] || null }这样,新增一种图表只需要注册新的Vue组件,不影响驾驶舱主框架。对于已经是报表形式的数据,AJ-report往往直接给出一个数组,前端根据组件类型决定如何渲染。下表是常见组件类型与前端处理方式的对应关系。
| componentType | 前端输出 | 数据要求 | 说明 |
|---|---|---|---|
| KPI、Number | 指标卡数字 | 单值或第一行第一列数值 | 保留小数位与单位 |
| LineChart、BarChart | ECharts图表 | 维度列加1到多列指标 | 需要聚合为series |
| ScrollTable | 轮播表格 | 多列结构化数据 | 注意首列对齐和换行 |
| MapChart | 地图 | 区域名称加指标值 | 考虑行政名称归一化 |
2.2 把AJ-report二维结果压成图表Option
AJ-report的大屏接口一般返回字段名和二维行数据,结构像是:
{ "fields": ["date", "zone", "sales"], "rows": [ ["2025-01-01", "华东", 2300], ["2025-01-01", "华南", 1800] ] }其中fields描述列顺序,rows是数据行。ECharts不直接接受这种结构,前端要先把“宽表”展开成“维度数组 + 指标系列”。我一般会在utils/reportData.js里放一个统一转换函数:
/** 将AJ-report行列结果转换为分组对象,维度取第一列,其余视为指标 */ export function transformReport(fields, rows) { const dimIndex = 0 const metrics = fields.slice(1) const result = { dimensions: [], series: [] } metrics.forEach(name => { result.series.push({ name, data: [] }) }) rows.forEach(row => { result.dimensions.push(String(row[dimIndex])) metrics.forEach((metric, idx) => { const value = Number(row[idx + 1]) result.series[idx].data.push(isFinite(value) ? value : 0) }) }) return result }dimIndex用来指定哪一列是X轴维度,调用方也可以把该参数提出来;metrics会排除维度列之外的所有列,适用于多指标折线图。处理空字符串时,Number('')会得到0,但这可能掩盖真实空值,因此用isFinite判断后再决定填入0还是null。如果业务上需要空值断开折线,应当保留null而不是补零。
2.3 用刷新周期裁剪请求量
大屏驾驶舱上线后最常见的问题是:十几个组件各自定时请求,AJ-report后端被瞬时请求打满。解决思路是根据组件配置里的refreshSec分组,而不是每个组件开一个setInterval。例如:
const refreshPool = new Map() function registerRefresh(component) { const key = component.refreshSec || 30 if (!refreshPool.has(key)) { refreshPool.set(key, []) } refreshPool.get(key).push(component) } function triggerGroup(seconds) { const group = refreshPool.get(seconds) || [] group.forEach(component => { loadComponentData(component) }) } setInterval(() => triggerGroup(30), 30000) setInterval(() => triggerGroup(60), 60000)这段代码把刷新时间相同的组件放在同一组里,每组对应一个定时器。实际项目中还要加一个随机延迟,让同一组的请求错峰发出,避免整组同时打爆后端。数据返回后按component.id更新到响应式状态,驾驶舱只关心“组件要不要重绘”,不关心这一步是由哪个定时器触发的。
3. 用Vue和JavaScript搭出大屏驾驶舱的骨架
3.1 初始化项目并处理vue安装依赖和环境配置
大屏端不推荐在现有业务项目里堆页面,尽量独立初始化。采用Vite创建Vue3加JavaScript工程,命令如下:
npm create vite@latest aj-screen -- --template vue cd aj-screen npm install echarts axios vue-router@4 npm install --save-dev sass npm run dev--template vue生成的是JavaScript版工程,不是TypeScript版;如果团队后续需要类型约束,再手动引入lang="ts"。这一步经常遇到的问题就是node版本太低,导致npm install报ERESOLVE依赖树冲突,建议把Node升到18及以上。HBuilderX用户可以用“打开目录”方式导入项目,但它主要适合查看html/css/javascript文件,编译运行仍在终端里执行。
接着修改vite.config.js,把接口前缀指向本地代理:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ base: './', plugins: [vue()], server: { port: 5173, proxy: { '/aj-report': { target: 'http://localhost:9095', changeOrigin: true } } } })base: './'是为了避免之后打包部署到Nginx子目录时,JavaScript和CSS资源路径变成绝对路径而白屏。代理配置只影响本地开发,上线时由Nginx或网关转发/aj-report。
3.2 用设计器坐标生成栅格布局
AJ-report的设计器输出的是坐标和宽高,前端可以直接利用CSS的绝对定位来还原画布。大屏以1920乘1080为基准,在根容器上固定这个尺寸,再通过外层transform: scale做整体缩放。单个组件的位置这样计算:
<template> <div class="screen-stage" :style="{ width: '1920px', height: '1080px' }"> <div v-for="comp in screen.components" :key="comp.id" class="cell-position" :style="cellStyle(comp)" > <component :is="resolveComponent(comp.type)" :component="comp" /> </div> </div> </template> <script setup> function cellStyle(comp) { return { left: comp.x + '%', top: comp.y + '%', width: comp.w + '%', height: comp.h + '%' } } </script> <style scoped> .cell-position { position: absolute; transform: translate(-50%, -50%); } </style>这里x、y的含义是组件中心点坐标,所以加了一个translate(-50%, -50%)让组件居中。另一种常见做法是x、y为左上角坐标,此时不需要这个translate。使用哪一种取决于AJ-report看板导出的语义,拿到JSONScheme后先打印一个组件节点确认,再决定是否保留该行。
3.3 CSS变量与主题控制
大屏视觉风格容易失控,问题往往出在颜色散落在每个组件里。统一用CSS变量管理背景、主色、辅助色、文字颜色,后续换肤不用改动业务组件:
:root { --screen-bg: #0b1a2e; --accent-blue: #00c6ff; --accent-orange: #ff9f43; --text-color: #f0f7ff; --font-scale: 1; } .screen-stage { background: radial-gradient(circle at 50% 0%, #10243d, var(--screen-bg)); color: var(--text-color); } .kpi-title { font-size: calc(16px * var(--font-scale)); }--font-scale可以先写成1,等缩放计算出来后由JavaScript动态修改,这样设计稿内的字号能随屏幕大小按比例变化。相比在组件里硬编码#FFFFFF、#FF0000,变量方式能减少后期调整成本。下面的表总结了初始化阶段需要关注的几个命令点。
| 操作 | 命令 | 容易踩的坑 |
|---|---|---|
| 创建Vue工程 | npm create vite | 目录名不能带中文 |
| 安装依赖 | npm install | Node版本过低报ERESOLVE |
| 本地起服务 | npm run dev | 端口被占用或代理未生效 |
| 打包 | npm run build | 资源路径未设置base |
4. 在组件内请求AJ-report数据并驱动图表
4.1 封装报告请求模块,处理鉴权与统一前缀
大屏工程里不要每个组件各自调fetch,更不要在每个组件里写AJ-report地址。先统一建一个src/api/report.js模块:
import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_REPORT_API || '/aj-report', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('report_token') if (token) { config.headers['X-Report-Token'] = token } return config }) export function fetchReport(datasetId) { return request.get(`/data/${datasetId}`).then(res => res.data) }baseURL从环境变量读取,没有配置时默认走Vite代理。X-Report-Token是自定义请求头,实际鉴权字段名需要和AJ-report后端约定,有的环境需要Authorization: Bearer <token>。不要把这个token写进源码,登录成功后写入localStorage或内存状态,避免大屏源码泄露后接口被随意调用。timeout建议设在8000到15000毫秒,大屏投放在弱网下很常见,设太短会导致刷新失败频繁。
4.2 用通用Chart组件驱动ECharts
大屏驾驶舱里会有多个图表组件,但它们的生命周期高度相似:挂载时初始化、属性变化时更新、卸载时销毁。我一般会抽一个BaseChart.vue:
<script setup> import * as echarts from 'echarts' import { ref, onMounted, onBeforeUnmount, watch } from 'vue' const props = defineProps({ option: { type: Object, required: true } }) const el = ref(null) let chart = null onMounted(() => { chart = echarts.init(el.value) chart.setOption(props.option) }) watch(() => props.option, opt => { chart.setOption(opt, true) }, { deep: true }) onBeforeUnmount(() => { if (chart) { chart.dispose() } }) </script> <template> <div ref="el" class="base-chart"></div> </template> <style scoped> .base-chart { width: 100%; height: 100%; } </style>setOption(opt, true)的第二个参数表示清空之前的series再重绘,适合大屏切换数据集或刷新后结构变化明显的场景。如果希望动画平滑过渡,改成chart.setOption(opt)即可。这里要留意:watch的deep: true会对整个option做深比较,数据量大时有一定开销,驾驶舱组件数量少于20个时没有问题。
4.3 通过vue路由参数加载屏幕并过滤启用组件
大屏通常通过URL区分不同屏幕,例如https://host/screen?screenId=dash-01。用Vue Router的useRoute拿到参数,再结合JavaScript的filter函数只加载启用状态的组件。
import { useRoute } from 'vue-router' import { ref } from 'vue' const route = useRoute() const screenId = route.query.screenId || 'default' const screenModules = ref([]) async function loadScreen() { const screen = await getScreenJSON(screenId) const components = screen.components.filter(item => item.enabled) const requests = components.map(item => fetchReport(item.datasetId)) const datasets = await Promise.all(requests) screenModules.value = components.map((component, index) => ({ ...component, dataset: datasets[index] })) }这里的filter函数是JavaScript数组常用的筛选方法,比for...of加if的写法更直观。路由参数不能直接信任,如果驾驶舱可能被外部URL访问,screenId需要做白名单或签名校验,否则别人可以遍历ID看到其他看板。组件里没有用props传递路由参数,而是使用时再读取,原因是同一屏幕的多个组件都需要知道当前screenId,放在一个可注入的store中更合理。
5. 大屏驾驶舱上线前的一个必做细节:用基准分辨率做动态缩放与打包验证
5.1 基于参考分辨率计算缩放比例
大屏不会被用户拖着窗口乱玩,但投屏的显示器可能不是1920乘1080。常见做法是固定设计稿基准,然后按比例整体缩放,这样页面不会出现横向滚动条。封装一个useScreenScale.js组合式函数:
import { ref, onMounted, onBeforeUnmount } from 'vue' const designWidth = 1920 const designHeight = 1080 const scale = ref(1) function updateScale() { const current = Math.min( window.innerWidth / designWidth, window.innerHeight / designHeight ) scale.value = Number(current.toFixed(4)) } let timer = null function debouncedUpdate() { clearTimeout(timer) timer = setTimeout(updateScale, 150) } export function useScreenScale() { onMounted(() => { updateScale() window.addEventListener('resize', debouncedUpdate) }) onBeforeUnmount(() => { window.removeEventListener('resize', debouncedUpdate) }) return { scale } }Math.min保证高度或宽度哪一边先不够,就按哪一边缩。debouncedUpdate的150毫秒延迟是为了防止窗口连续变化时反复计算布局。缩放后,根容器固定为设计稿尺寸,并把transform: scale(scale)挂在容器上,同时设置transform-origin: top left;如果忘记这个属性,缩放中心会在页面中间,左上角会露出黑边。
5.2 打包部署后的布局自检
打包后在本地静态服务器预览,不要直接双击index.html。常见问题是资源404导致Vue没挂载,画面空白或布局错乱。打开浏览器控制台的Network面板,看JavaScript文件路径是否带着assets/xxx.js。如果路径不对,优先确认vite.config.js里的base: './';如果用router的history模式,还需要让Nginx将所有非文件请求回退到index.html。验证缩放是否正常的办法很简单:把浏览器切换到1366乘768的分辨率,检查页面没有横向滚动条,再看控制台里scale值是否约等于0.71;投屏到目标显示器后,再按实际分辨率检查一次四角是否完整显示。这个检查应该放进发布流程,否则换了一台显示器就要重调布局。
本文还有配套的精品资源,点击获取