简介:基于Vue框架的新冠疫情可视化页面设计源码,是一套可直接运行的前端项目,面向学习Vue的初学者、毕业设计以及数据展示开发人员。页面使用HTML、CSS和JavaScript实现丰富的疫情数据交互看板,通过Vue组件化开发、路由配置和状态数据组织等设计,清晰展示了从原始数据到可视化图表的转换过程;压缩包共20个文件,包含5个JavaScript脚本、2个Vue组件、3个JSON配置以及多张PNG/GIF图像,整体体积仅1.06MB,文件结构紧凑且职责明确。同时,项目内置了浏览器兼容列表、代码转译、Vue构建等工程配置文件,学习者可以直观了解浏览器兼容处理、ES6转译、开发服务器和构建命令等日常开发必需的知识点;从目录结构来看,视图、组件、路由与数据管理等模块划分得十分清晰,适合仿照学习一个标准Vue应用的代码组织方式。另外,通过分析图片和资源目录的管理方式,也能掌握常见静态资源组织规律,例如如何按模块存放图片、灵活引用公共资源等。目前已有91人学习下载,非常适合用于课程实践、技能进阶或作为简历中的完整项目案例。
1. 一个只有20个文件的新冠疫情可视化页面,拆开看才知道什么是Vue的最小可行项目
拿到这份源码时,第一反应是“怎么连数据接口都没有”。但正是这种没有后端、没有复杂业务、只有20个文件的组合,反而把Vue项目的骨架暴露得最清楚:src/views下面是一个登录页,router里是路由表,store里存的是疫情数据,assets里放着图片和样式,剩下的是babel、webpack、浏览器的配置文件。它解决的核心问题是:在数据以静态方式存在前端时,如何用登录页和数个图表页面完成疫情信息的可视化展示。适合刚学完Vue基础、想了解真实项目文件结构的开发者,也适合需要快速交付可视化大屏demo的工程师。拆完之后你会发现,可视化页面的重点不在图表库本身,而在于组件生命周期和数据流在哪里接上。
2. Vue项目启动链路:从package.json到main.js,先搞清楚这些文件谁在做什么
2.1 目录结构里埋着Vue可视化项目的标准答案
先看这份源码的根目录文件,它们不是随手生成的。打开upload.zip后,顶层放着.browserslistrc、babel.config.js、vue.config.js、package.json、package-lock.json,src目录下是App.vue、main.js、store/index.js、router/index.js、views/Login.vue,以及assets里的图片和样式,另外还有一个readme.txt。这些文件的组织方式就是Vue脚手架create出来的标准形态。与纯业务代码相比,真正值得看的是这几个文件的职责边界:
| 文件或目录 | 实际作用 | 在这个疫情可视化项目中的落点 |
|---|---|---|
| package.json | 声明依赖、脚本命令、项目元信息 | 确定Vue、Vue Router、Vuex版本,执行npm run serve启动 |
| package-lock.json | 锁定依赖树版本 | 保证团队安装的依赖一致,解决“在我电脑上能跑”的问题 |
| .browserslistrc | 告诉Babel和postcss要兼容哪些浏览器 | 影响转译和自动前缀的产出粒度 |
| babel.config.js | 配置Babel转译ES6+语法 | 指定@vue/app预设,把.vue文件中的script编译成浏览器可运行代码 |
| vue.config.js | 覆盖webpack默认配置 | 设置publicPath和devServer代理,打包时用的关键配置 |
| src/main.js | 创建Vue实例,挂载App、router、store | 整个可视化应用的启动入口 |
| src/App.vue | 根组件,同时承担可视化主页面 | 疫情图表、统计卡片和登录后的整体布局都放在这里 |
| src/router/index.js | 路由表与导航守卫 | 控制登录页和疫情数据页面之间的跳转 |
| src/store/index.js | 全局状态管理 | 存放疫情数据、统计口径与登录态 |
| src/views/Login.vue | 登录页组件 | 实现登录表单和交互逻辑 |
| src/assets | 静态资源 | 存放logo、背景图、CSS基础样式 |
| public/index.html | HTML模板 | 提供id为app的挂载点,并加载favicon.ico |
| jsconfig.json | 给IDE提供路径提示 | 让vscode识别@/目录别名,写import时减少手动拼路径 |
| readme.txt | 项目说明 | 通常写运行环境和注意事项,拿到源码先看它 |
很多初学者会把注意力全放在App.vue上,实际在这个项目里App.vue不只是容器,它同时承担了可视化主体的展示。疫情数据可视化这类页面,一旦涉及登录跳转和全局图表数据,就必须把store独立出来,否则每个组件都从接口拉一次数据,性能会很差。assets目录下的css负责全站基础样式,images里存的是疫情图标或背景图,logo.png会被导航栏或登录页引用。
2.2 main.js里发生了什么:Vue实例挂载前的最后一步
打开src/main.js,代码量很小,但每行都决定了应用怎么启动:
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')这段代码的核心是new Vue({ router, store }),它不是普通组件,而是整个应用的根实例。router和store被以选项形式注入,之后在任意组件里都能通过this.$router、this.$route、this.$store访问到它们。render函数中h是createElement的缩写,h(App)的作用是让根组件App成为整棵组件树的起点,这比template字符串解析更高效,也避免了运行时模板编译器带来的体积开销。$mount('#app')指定挂在public/index.html中的div#app上。
有一个容易被忽略的参数:Vue.config.productionTip = false。它的作用是在生产环境构建时关闭开发模式的提示,减少控制台噪音。如果在某些Vue项目里把这个配置删掉,开发模式下每次打开页面都会看到一个黄条提示,不影响运行,但会干扰调试。另外,如果项目引用了Element UI、ECharts这类第三方库,main.js里通常还会有Vue.use(ElementUI)和全局样式引入,这份源码没写,说明要么登录页使用的组件是手写的,要么第三方库是通过CDN在index.html中加载的。自己做练习时,建议把第三方库显式安装到package.json,方便版本管理。
2.3 开发环境与构建配置:babel、browserslist和vue.config.js的关系
package.json中通常有一段scripts,常见的是"serve": "vue-cli-service serve"和"build": "vue-cli-service build"。拿到源码后,第一步执行npm install安装依赖,再npm run serve启动开发服务器。如果你在安装时遇到依赖版本冲突,先检查package-lock.json和package.json是否匹配,这在Vue2项目中经常发生。特别是node版本过高时,node-sass编译会报错,可以选择改成sass或sass-loader版本下调。
.browserslistrc这个文件虽然只有几行,却同时影响babel.config.js和autoprefixer的产物。比如写成"last 2 versions"和"ie >= 11",转译出来的代码大小和对Promise、async的降级策略完全不同。在这个疫情可视化项目里,如果目标用户是医院或政务内网,浏览器版本往往偏低,需要把browserslist写严,但代价是打包体积变大。常见的一行配置是:
> 0.5% last 2 versions not dead提示:改完.browserslistrc后要重新build才会生效,开发服务器下也可能需要重启。
vue.config.js里最常见的配置是publicPath和devServer:
module.exports = { publicPath: './', devServer: { port: 8080, host: '0.0.0.0', proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } }publicPath设为'./',是可视化大屏项目打包后直接双击index.html就能打开的关键配置。默认的'/'路径在file协议下会找不到JS和CSS文件,页面白屏。devServer默认只绑定localhost,host改为'0.0.0.0'后,同一个局域网内其他设备就能通过本机IP访问这个大屏,这在疫情数据展示的现场调试里很实用。proxy配置通常用于解决本地开发跨域,如果把疫情数据存放在远程JSON接口,就需要在这里把/api开头的请求代理到真实服务器。注意proxy只在devServer阶段有用。
3. 疫情可视化图表是怎么画出来的:数据、状态和ECharts实例管理
3.1 为什么可视化部分选择ECharts而不是纯CSS或SVG
新冠疫情可视化页面里需要展示确诊、疑似、治愈、死亡等维度的趋势,如果靠手写SVG,折线图的坐标轴、tooltip、缩放都要自己实现,开发周期至少翻倍。ECharts在这个场景下的优势是开箱即用的折线图、柱状图、饼图和地图,并且底层用Canvas渲染,数据量大时比SVG性能更稳。如果在package.json里没有看到echarts依赖,说明原始项目用的是CDN引入或尚未安装,常见的做法是执行npm install echarts --save,然后在需要的组件里按需导入。Vue组件里使用ECharts的标准流程是:在mounted中初始化实例,在setOption里传入数据和配置,在beforeDestroy中销毁实例,避免内存泄漏。
这份源码里,可视化主体并没有单独拆成Dashboard.vue,而是集中在App.vue中。为了讲清楚图表初始化,下面把App.vue里的script部分摘出来说明:
import * as echarts from 'echarts' import { mapGetters } from 'vuex' export default { name: 'CovidDashboard', computed: { ...mapGetters(['lineData']), }, mounted() { this.$nextTick(() => { this.initChart() window.addEventListener('resize', this.handleResize) }) }, beforeDestroy() { window.removeEventListener('resize', this.handleResize) this.chart && this.chart.dispose() }, methods: { initChart() { this.chart = echarts.init(this.$refs.chart) this.chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: this.lineData.dates }, yAxis: { type: 'value' }, series: [{ name: '新增确诊', type: 'line', smooth: true, data: this.lineData.confirmed }] }) }, handleResize() { this.chart && this.chart.resize() } } }这段代码里有三个关键动作。echarts.init的第一个参数是DOM容器,必须保证已经有真实宽度和高度,所以外面套了一层this.$nextTick,否则会出现canvas宽度为0的问题。setOption中的xAxis.data和series.data都来自Vuex的getter,组件不直接持有数据,这样数据更新时只需要修改store,再调用chart.setOption即可更新。resize监听挂在window上,窗口变化时调用chart.resize(),这是大屏适配的基本操作,但在退出组件时一定要removeEventListener并调用dispose,否则在多路由切换场景下会看到白屏和性能下降。
3.2 疫情数据放在store里而不是组件里的理由与实现
可视化页面通常需要多个组件共享同一份数据,比如顶部统计卡片、趋势折线图、省份地图。如果每个组件各自定义data,数据一旦变化就要反复传props,代码会越来越乱。把数据放到src/store/index.js中,用state保存原始数据,用getters做格式转换,用mutations同步修改,这是Vuex在可视化项目中最典型的使用方式。看一下store中疫情数据模块的结构:
const state = { covidData: [ { date: '2023-01-01', confirmed: 1280, cured: 900, dead: 3 }, { date: '2023-01-02', confirmed: 1365, cured: 950, dead: 5 } ], loading: false } const getters = { lineData: (state) => { return { dates: state.covidData.map(item => item.date), confirmed: state.covidData.map(item => item.confirmed), cured: state.covidData.map(item => item.cured) } }, totalConfirmed: (state) => { return state.covidData.reduce((sum, item) => sum + item.confirmed, 0) } } const mutations = { SET_COVID_DATA(state, payload) { state.covidData = payload }, SET_LOADING(state, val) { state.loading = val } } const actions = { fetchCovidData({ commit }) { commit('SET_LOADING', true) // 模拟接口请求 setTimeout(() => { commit('SET_COVID_DATA', []) commit('SET_LOADING', false) }, 500) } } export default { state, getters, mutations, actions }state.covidData是原始数据,getters.lineData把它转换成ECharts需要的x轴和series结构。这里有一个设计细节:不要在组件里直接对state做reduce或filter,而是提炼成getter,这样所有组件拿到的数据格式是一致的。如果后续接口返回的字段名是confirmCount而不是confirmed,只需要改getter一处,所有图表随之更新。mutations必须用commit触发,这是Vuex的硬性规定,直接给state赋值会绕过devtools的记录逻辑。actions里模拟了异步请求,通过setTimeout把空数组写入state,实际项目中这里会换成axios.get('/api/covid'),然后把response.data交给commit。
在组件中使用mapGetters展开,比如computed: { ...mapGetters(['lineData']) },然后在模板中通过this.lineData访问。一旦store中的covidData被更新,getter会重新计算,图表组件在watch或mounted逻辑中重新setOption即可。
3.3 折线图、柱状图和地图的配置要点与常见坑
疫情数据可视化页面最常见的图表是折线图、柱状图和地图。折线图适合表示时间序列上的新增/累计趋势,柱状图适合对比不同省份或不同日期的数值,地图适合展示地理分布。ECharts在配置这些图表时,几个容易出问题的参数值得单独说明:
| 配置项 | 作用 | 疫情可视化中的坑 |
|---|---|---|
| series.smooth | 让折线平滑 | smooth设为true时,首尾数据点形状会改变,小范围数据时慎用 |
| tooltip.trigger | 触发方式axis和item | 地图用item,折线图用axis,否则tooltip显示不完整 |
| dataZoom | 缩放时间轴 | 日期超过30个时一定要加,否则折线挤在一起看不清 |
| color | 色板 | 同一页面多个图表的颜色必须统一,否则用户误判等级 |
| grid | 控制图表区域间距 | 大屏边缘被裁切时,优先调grid.bottom而不是改容器尺寸 |
以dataZoom为例,当折线图横坐标超过30个日期,就应该加入如下配置让用户拖拽查看:
dataZoom: [ { type: 'inside', start: 0, end: 100 }, { type: 'slider', start: 0, end: 100, bottom: 10 } ]inside表示鼠标滚轮缩放,slider提供底部的拖动条。start和end是百分比,表示初始展示范围。设成0到100代表默认展示全部数据,用户手动缩放后会改变。这个交互对疫情数据可视化页面尤为重要,因为用户既想看整体趋势,也想聚焦某一段暴发期。如果不加数据缩放,折线图在数据多的时候会变得非常密,几乎无法读值。
地图的部分,ECharts官方从5.0开始不再默认注册中国地图数据,需要单独引入china.js文件。常见做法是在main.js中全局导入,或者放到public目录下通过script标签加载。省份名称要和数据中的name完全一致,比如“确诊病例”匹配“湖北”而不是“湖北省”,否则地图会显示灰色无数据。如果数据中没有地理坐标,直接用geo的map属性配合series的data即可。
4. 登录页到可视化页面的路由守卫:Vue Router里的鉴权流程和状态回写
4.1 路由表先按页面和权限拆开
项目中的src/router/index.js定义了前端路由。登录页和可视化主页面是两种不同性质的页面,登录页不需要鉴权,可视化页面必须登录后才能进入。直接把这些信息写进路由meta里,后续导航守卫判断起来会非常清晰。实际源码里,可视化主体是写在App.vue中的,路由表相对简化;下面这段路由示例把它按规范化结构拆出来,便于理解页面间的层级关系:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) // 实际源码中Dashboard内容在App.vue里,这里按独立组件拆分演示 import Dashboard from '../App.vue' const routes = [ { path: '/login', name: 'Login', component: () => import('../views/Login.vue'), meta: { public: true, title: '用户登录' } }, { path: '/', name: 'Dashboard', component: Dashboard, meta: { title: '疫情可视化大屏' } } ] const router = new VueRouter({ mode: 'hash', routes }) export default router这里有两个要点。登录页的component用了() => import(...)的动态导入,登录页会被拆成独立chunk,首次加载只下载登录页代码,可视化图表库在主页面需要时才请求,这对提升疫情大屏的首屏速度很有帮助。mode: 'hash'是兼容性最好的选择,对于纯静态托管的可视化页面,hash模式刷新不会404。如果项目部署在Nginx下且配置了try_files,可以换成mode: 'history',但一般情况下可视化大屏优先选hash。
路由的meta可以继续扩展,比如给Dashboard加上roles: ['admin']、menuKey: 'overview',这样导航守卫和侧边栏菜单渲染都能通过meta统一判断,而不需要在组件里写死权限字符串。在可视化项目中,如果后期要加入全国地图、海外疫情等多个页面,按权限拆分会更安全。
4.2 全局前置守卫:没有token就回登录页
Vue Router的beforeEach是拦截路由跳转的钩子函数,它接收三个参数:to是目标路由,from是来源路由,next是放行函数。在这个项目中,登录态用localStorage保存,所以守卫逻辑可以写成:
const whiteList = ['/login'] router.beforeEach((to, from, next) => { document.title = to.meta.title || '疫情可视化' const token = localStorage.getItem('covid_token') if (whiteList.includes(to.path)) { next() } else if (!token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })代码中的whiteList是免登录白名单,目前只有/login。如果后续增加注册页、访客模式,直接往这个数组里加路径即可。判断到目标路由不在白名单且没有covid_token时,next跳转到登录页,并用query.redirect记录用户本来要访问的地址,登录成功后再跳回去,这是可视化后台系统里最常用的回跳方式。document.title写在守卫里,比在每个组件mounted里设置更统一。
这里有一个值得注意的边界:token只存在localStorage是不够的,XSS脚本一旦注入就能读取它。但在这个纯前端demo里,token本身没有实际签名,只做界面级拦截。如果要对接真实接口,应该在axios响应拦截器里处理401,并清理本地token再跳回登录页:
service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('covid_token') router.push('/login') } return Promise.reject(error) } )axios拦截器是补充说明,不在这份源码的文件列表内,但实际项目中几乎必用。响应拦截器统一判断状态码,401就踢回登录页,不等业务代码逐个处理。
4.3 Login.vue中的表单校验与登录状态落库
Login.vue是一个单文件组件,包含template、script、style三部分。登录表单通常有用户名和密码两个字段,这里的关键不是校验逻辑本身,而是登录成功后如何把状态交给Vuex和路由。下面是一个贴合本项目场景的写法:
<template> <div class="login-container"> <el-form :model="loginForm" :rules="rules" ref="loginForm"> <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码"></el-input> </el-form-item> <el-button type="primary" @click="handleLogin">登 录</el-button> </el-form> </div> </template>脚本部分处理登录动作:
export default { data() { return { loginForm: { username: '', password: '' }, rules: { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, min: 6, message: '密码至少6位', trigger: 'blur' }] } } }, methods: { handleLogin() { this.$refs.loginForm.validate((valid) => { if (!valid) return localStorage.setItem('covid_token', 'demo-token-' + Date.now()) this.$store.commit('SET_LOGIN_STATE', true) const redirect = this.$route.query.redirect || '/' this.$router.replace(redirect) }) }, handleLogout() { localStorage.removeItem('covid_token') this.$store.commit('SET_LOGIN_STATE', false) this.$router.push('/login') } } }这里的rules对应el-form-item的prop,required和min是Element UI内置的校验规则,在blur触发。validate回调接收valid参数,表单不合法时直接return,不会做后续登录动作。登录成功后,把token写入localStorage,同时调用store的SET_LOGIN_STATE mutation修改内存中的登录状态。使用$router.replace而不是push,是为了避免登录页被加入历史栈,用户按返回按钮不会回到登录页。redirect优先取路由query中保存的原目标地址,没有就默认跳到可视化大屏根路径。handleLogout是退出登录的常见补充,清除token并跳回登录页。
这里用Element UI作为表单组件库是可视化项目的常见选择,如果你拿到源码时发现组件库里没有el-form这些标签,说明需要先npm install element-ui并在main.js中全局注册。如果项目用了原生HTML的input配合自定义校验,逻辑也是一样的,只是把rules换成手动判断。登录页的样式通常会设置全屏背景、卡片居中,这些在assets/css里都有现成样式,改颜色就能适配不同大屏主题。
5. 打包后布局异常排查:publicPath、大屏适配和ECharts宽高初始化
5.1 先处理打包后的白屏与资源路径
疫情可视化页面开发完,执行npm run build后dist目录里会出现index.html和static资源。双击index.html如果白屏,第一反应去看控制台的资源404错误,这基本是publicPath没配好。vue.config.js中publicPath设为'./',所有JS、CSS、图片请求会变成相对路径,在file协议下才能正常加载。如果你的项目部署在Nginx子路径下,比如example.com/covid/,publicPath要改成'/covid/',注意前后斜杠都不能丢。另外,查看dist/index.html里script标签的src,如果出现/css/app.css前多了一个/,说明配置还是默认值。一个易错点:publicPath修改后需要重新npm run build,dev server不会自动应用构建级配置。
5.2 可视化大屏适配:固定设计稿比例再用transform缩放到真实屏幕
疫情大屏一般面向会议室大屏或宽屏显示器,常见设计稿是1920x1080。如果按百分比设计每个图表,在不同分辨率下会错位。我常用的一套方案是把页面主体固定为1920x1080,然后在最外层容器上用CSS计算缩放比例:
function resizeScale() { const scaleX = window.innerWidth / 1920 const scaleY = window.innerHeight / 1080 const scale = Math.min(scaleX, scaleY) document.querySelector('#dashboard-scale').style.transform = `scale(${scale})` } window.addEventListener('resize', resizeScale) resizeScale()页面容器需要设置transformOrigin: 'left top',否则缩放中心会跑到中间。适用场景是数据大屏不需要滚动,所有内容一屏展示。缺点是非16:9屏幕上会出现两侧留白,但视觉上比拉伸变形要专业得多。这个技巧对疫情可视化项目尤其合适,因为大屏中的图表密集,任何变形都会影响读数。
5.3 ECharts容器宽度为0的排查清单
最后一个高频坑是图表初始化时容器还没渲染完,导致echarts.init拿到一个宽高为0的元素,图表不显示。解决方式已经在3.1中提过,用this.$nextTick(() => this.initChart()),但还有两个隐藏场景。第一,如果容器本身使用了flex或百分比高度,父级高度没有撑开,即使nextTick也拿不到高度,需要先给容器设置一个明确的高度如height: 400px。第二,在tab切换中初始化的图表,如果tab内容display:none,宽度计算也会为0,需要在tab切换完成后再调用chart.resize()。排查时打开浏览器开发者工具,选中图表canvas元素看它的宽度属性,如果为0,问题就出在容器尺寸或初始化时机上。
本文还有配套的精品资源,点击获取