疫情统计页面 H5 vue3+TypeScript+Echarts
Posted Little___Turtle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了疫情统计页面 H5 vue3+TypeScript+Echarts相关的知识,希望对你有一定的参考价值。
目录🧰🧰
🏹🏹UpRefreshAndToTop 上拉刷新 一键回到顶部 组件
功能介绍🎨🎨
下拉刷新🪄 一键回到顶部🚀 echarts中国地图运用🗺️ 数据列表展示🧰 代理🧆
部分效果展示🌌🌌
全部功能展示效果🗺️🗺️
因为功能有点多 所以专门录制了一期视频介绍功能
具体的全部素材 源码也放在评论区 大家可以去看看😁
代码演示🌃🌃
🥰🥰demo目录结构
🍭🍭 DownRefresh.vue 下拉刷新 组件
<!-- 下拉刷新新数据 -->
<template>
<div class="box">
<!-- 内部属性 @scroll 监听滚动条事件 -->
<div
@scroll="scrollEvent"
class="scroll-box"
>
<!-- 插槽 -->
<slot></slot>
<div class="end-text" v-if="!isScroll"> endText </div>
</div>
</div>
</template>
<script setup lang="ts">
import reactive, toRefs from "@vue/reactivity";
const props = defineProps(
//下拉高度
distance: Number ,
//判定是否下拉
isScroll: Boolean,
endText:
type: String,
default: "没有更多了",
,
);
//子传父参数
const $emits = defineEmits(["getList"]);
const data = reactive(
//离屏幕高度
scrollTop: 0,
);
let
scrollTop,
= toRefs(data);
//下拉刷新 判定
const scrollEvent = (e: any) =>
scrollTop = e.srcElement.scrollTop;
if (!props.isScroll) return;
if (
//判定下拉高度
scrollTop + e.srcElement.offsetHeight >
e.srcElement.scrollHeight - props.distance!
)
$emits("getList");
;
</script>
<style lang="scss" scoped>
.box
overflow: hidden;
position: relative;
width: 100%;
height: 90vh;
.scroll-box
height: 90vh;
width: 100%;
overflow: auto;
transition: all 0s ease 0s;
position: absolute;
right: 0;
.end-text
text-align: center;
height: 50px;
line-height: 50px;
color: #999;
</style>
🪜🪜EpidemicList.vue 数据列表 组件
<!-- 疫情list -->
<template>
<div class="list-box">
<div class="info-title info">
<p>地区</p>
<p>现有确诊</p>
<p>确诊</p>
<p>死亡</p>
<p>治愈</p>
</div>
<div class="list" v-for="item in epideList" :key="item.id">
<div class="p-box">
<div @click="getChowChildren(item.id)" class="info">
<p> item.name </p>
<!-- 确诊病例 计数可能出现负数情况 -->
<p>
item.total.confirm - item.total.dead - item.total.heal >= 0
? item.total.confirm - item.total.dead - item.total.heal
: 0
</p>
<p>
<span> item.total.confirm </span>
<span>较昨日+ item.today.confirm ? item.today.confirm : 0 </span>
</p>
<p> item.total.dead </p>
<p> item.total.heal </p>
</div>
<div v-if="showChildren">
<div>
<div v-show="data.showChildrenId == item.id" class="children-box">
<EpidemicList :epideList="item.children" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- name 组件可以自己调用自己(递归调用) 只能通过name选项来做此事-->
<script name="EpidemicList" setup lang="ts">
// 声明props
import reactive from "vue";
import IEpidData from "../type";
//无需引入
const props = defineProps(
//疫情数据
epideList:Array<IEpidData>,
//是否展示子数组 省=>市 区数据
showChildren: Boolean,
);
//点击展示
const data = reactive(
showChildrenId: "",
);
//判定 子列表点击是否展示 各个省下面具体的地区
const getChowChildren = (id: string) =>
//原理:通过id来确定要展示的list 点击第二次则置空 不展示效果
data.showChildrenId == id ? (data.showChildrenId = ""): (data.showChildrenId = id);
;
//确诊案例 存在
</script>
<style lang="scss" scoped>
.list-box
border: 1px solid #d1d1d1;
margin: 1rem 0;
.p-box
.children-box
margin-left: 1rem;
.list-box
border: none;
.info-title
height: 30px;
line-height: 30px;
p
&:nth-child(1)
font-weight: 600;
color: #999;
.info-title
font-weight: 600;
color: #000;
font-size: 16px;
height: 50px;
line-height: 50px;
background: #d1d1d1;
.info
display: flex;
justify-content: space-between;
align-items: center;
> p
width: 15%;
text-align: center;
// white-space: nowrap;
&:nth-child(1)
font-weight: 600;
color: #000;
&:nth-child(2)
width: 23%;
color: red;
&:nth-child(3)
width: 23%;
white-space: nowrap;
span
display: block;
&:last-child
color: #999;
.list
&:nth-of-type(odd)
background: #f6f6f6;
.info
height: 80px;
line-height: 80px;
p
line-height: 20px;
</style>
🏹🏹UpRefreshAndToTop 上拉刷新 一键回到顶部 组件
<!-- 顶部刷新 回退到顶部 -->
<template>
<div class="box">
<!-- 内部属性 事件: 触摸开始 @touchstart 触摸移动 @touchmove 触摸结束 @touchend 滚动条滑动 @scroll-->
<div
@touchend="touchend"
@touchmove="touchmove"
@touchstart="touchstart"
@scroll="scrollEvent"
:style=" top: `$translateYpx` "
class="scroll-box"
>
<div class="loadingBox" v-if="touchstartTitleShow">释放可刷新...</div>
<div class="loadingBox" v-if="touchEndTitleShow">加载中...</div>
<!-- top 回退顶部的定位点 -->
<div id="top"></div>
<!-- 插槽 -->
<slot></slot>
<div v-show="data.isShowTop" class="back-box" @click="toTop">
<img src="../assets/toTop.png">
</div>
</div>
</div>
</template>
<script setup lang="ts">
import reactive, toRefs from "@vue/reactivity";
const $emits = defineEmits(["refreshFun"]);
const data = reactive(
startText: "释放即可刷新",
scrollTop: 0,
startY: 0,
translateY: 0,
touchEndTitleShow: false, //控制手指离开屏幕的title显示
touchstartTitleShow: false, //控制手指按下屏幕的title显示
isShowTop:false
);
let
touchstartTitleShow,
touchEndTitleShow,
translateY,
= toRefs(data);
//手指触碰到屏幕
const touchstart = (e: any) =>
let y = e.targetTouches[0].pageY;
data.startY = y;
;
const scrollEvent = (e: any) =>
data.scrollTop = e.srcElement.scrollTop;
//判定是否展示回退顶部按钮
data.scrollTop>400? (data.isShowTop=true) : (data.isShowTop=false);
const toTop=()=>
//定位到div->top
let anchor = document.getElementById("top")!;
anchor.scrollIntoView();
//手指开始滑动
const touchmove = (e: any) =>
let y = e.targetTouches[0].pageY;
if (y > data.startY && data.scrollTop == 0)
data.touchstartTitleShow = true;
//如果当前移动距离大于初始点击坐标,则视为是下拉。并且要处于顶部才刷新,不能影响正常的列表滑动。
data.translateY = (y - data.startY) / 2;
else
data.touchstartTitleShow = false;
;
//手指松开
const touchend = (e: any) =>
let y = e.changedTouches[0].pageY;
if (y > data.startY && data.scrollTop == 0)
data.translateY = 0;
data.touchstartTitleShow = false;
data.touchEndTitleShow = true;
$emits("refreshFun", () =>
data.touchEndTitleShow = false;
);
data.startY = 0;
;
</script>
<style lang="scss" scoped>
.box
overflow: hidden;
position: relative;
width: 100%;
height: 100vh;
.loadingBox
padding: 20px;
text-align: center;
.scroll-box
height: 100vh;
width: 100%;
overflow: auto;
transition: all 0s ease 0s;
position: absolute;
right: 0;
.back-box
height: 4rem;
width: 4rem;
// 如果对小火箭不满意 可以换成阴影盒子 样式也有
// background-color: #fff;
// 圆角弧度 添加圆角边框
// border-radius: 50%;
//盒子阴影
// box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.4);
// position 属性规定元素的定位类型 fixed元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。
position: fixed;
bottom: 3rem;
right: 1rem;
text-align: center;
line-height: 4rem;
</style>
🗿🗿type index.ts 数据声明
interface IData
//全球疫情数据树
areaTree: IEpidData[];
chinaDayList: [];
//全球疫情列表展示
showList:IEpidData[];
//全球疫情数组 用来完成分页功能
globalEpidemic: Array<IEpidData[]>
// 疫情 中国总统计
chinaTotal: IChinaTotal;
//中国疫情
china: IEpidData[] | undefined;
//江西疫情
jxData: IEpidData | undefined;
// 进行判定 number 全国 1 江西
type: number;
// 判定展示那一个地图 全国现状 全国累计
mapType: number;
lineType: number;
//最新更新时间
lastUpdateTime: string;
//下拉刷新判定
isScroll:Boolean;
//疫情数据单位统计
interface IEpidData
today:
confirm: number;
suspect: number;
heal: number;
dead: number;
severe: number;
storeConfirm: number;
;
total:
confirm: number;
suspect: number;
heal: number;
dead: number;
severe: number;
input: number;
;
extData: ;
name: string;
id: string;
lastUpdateTime: string;
children: IEpidData[] | undefined;
//疫情 中国总统计
interface IChinaTotal
total:
confirm: number;
suspect: number;
heal: number;
dead: number;
severe: number;
input: number;
;
today:
input: number;
storeConfirm: number;
confirm: number;
dead: number;
heal: number;
;
extData:
noSymptom: number;
incrNoSymptom: number;
;
//中国地图数值定义
interface IMap
name: string,
value:number
export type IData, IChinaTotal, IEpidData ,IMap;
📕📕pageTS index.ts 数据处理
import IChinaTotal, IData, IEpidData, IMap from "../type";
import axios from "axios";
import * as echarts from "echarts";
import chinaJson from "../assets/china.json";
//疫情实时数据
class InfoData implements IData
areaTree: IEpidData[] = [];
chinaDayList: [] = [];
globalEpidemic:Array<IEpidData[]>=[];
showList: IEpidData[]=[];
chinaTotal: IChinaTotal =
total:
confirm: 0,
suspect: 0,
heal: 0,
dead: 0,
severe: 0,
input: 0,
,
today:
input: 0,
storeConfirm: 0,
confirm: 0,
dead: 0,
heal: 0,
,
extData:
noSymptom: 0,
incrNoSymptom: 0,
,
;
china: IEpidData[] | undefined = [];
jxData: IEpidData | undefined =
today:
confirm: 0,
suspect: 0,
heal: 0,
dead: 0,
severe: 0,
storeConfirm: 0,
,
total:
confirm: 0,
suspect: 0,
heal: 0,
dead: 0,
severe: 0,
input: 0,
,
extData: ,
name: "",
id: "",
lastUpdateTime: "",
children: ([] = []),
;
// 进行判定 0 全国 1 江西
type = 0;
mapType = 1;
lineType = 0;
lastUpdateTime = "";
isScroll=true;
//数据分页处理 数组[][] 20一页
const getPageList = (list: IEpidData[]) =>
const arr: Array<IEpidData[]> = [];
for (let index = 0; index < list.length; index += 20)
arr.push(list.slice(index, index + 20))
return arr
const initDataFun = async (data: InfoData) =>
//疫情地图数据 初始化
//绑定要渲染的地方
let nowMapDom: htmlElement | null = document.getElementById("nowMap");
let totalmapDom: HTMLElement | null = document.getElementById("totalMap");
//初始化echarts实例
let nowMap=echarts.getInstanceByDom(nowMapDom as HTMLElement); //有的话就获取已有echarts实例的DOM节点。
let totalMap=echarts.getInstanceByDom(totalmapDom as HTMLElement);
if(nowMap ==null || totalMap == null) // 如果不存在,就进行初始化。
nowMap = echarts.init(nowMapDom as HTMLElement);
totalMap = echarts.init(totalmapDom as HTMLElement);
//显示加载圈圈
nowMap.showLoading();
totalMap.showLoading();
//定义两个地图 类型
let nowMapData: IMap[] = [];
let totalMapData: IMap[] = [];
//导入自定义地图数据 registerMap 注册的地图名称。
echarts.registerMap("china", chinaJson as any);
//定义 图表 类型
type EChartsOption = echarts.EChartsOption;
//定义地图配置
let series =
type: "map",
map: "china",
colorBy: "series", //按照系列分配调色盘中的颜色,同一系列中的所有数据都是用相同的颜色
zoom: 1.3, //当前视角的缩放比例
top: 80, //组件离容器上侧的距离
label:
show: true,
color: "#333",
fontSize: 10,
,
;
//点击地图效果
let optionMap: EChartsOption =
title:
//标题内容
// text: '中国疫情图',
subtext: "单击省份可查看病例数",
,
tooltip:
//提示框组件
trigger: "item", //触发类型 数据项图形触发,主要在散点图,饼图等无类目轴的图表中使用。
formatter: "现有确诊病例<br/>b: c ", //提示框浮层内容格式器,支持字符串模板和回调函数两种形式
// 模板变量有 a , b , c , d , e ,分别表示系列名,数据名,数据值等。
//在 trigger 为 'axis' 的时候,会有多个系列的数据,此时可以通过 a0 , a1 , a2 这种后面加索引的方式表示系列的索引。 不同图表类型下的 a , b , c , d 含义不一样。 其中变量 a , b , c , d 在不同图表类型下代表数据含义为:
// 地图: a (系列名称), b (区域名称), c (合并数值), d (无)
,
visualMap:
show: false,
,
;
//获取疫情全部数据接口
//await是等待的意思,await关键字只能放在async函数里
//await配合async一起使用,相当于把异步函数变成了同步,await会等待后面表达式的返回结果之后才执行下一步。
let res=await axios("/prod-api/ug/api/wuhan/app/data/list-total");
//疫情实时数据处理
//解构 [[1-30],[31-60],....]
data.globalEpidemic = getPageList(res.data.data.areaTree);
data.showList = data.globalEpidemic[0];
//普通数据赋值
data.areaTree = res.data.data.areaTree;
data.chinaDayList = res.data.data.chinaDayList;
data.chinaTotal = res.data.data.chinaTotal;
data.lastUpdateTime=res.data.data.lastUpdateTime;
//获取中国数据
data.china = data.areaTree.find((v) => v.id === "0")?.children;
//获取江西疫情数据
data.jxData = data.china?.find((v) => v.id === "360000");
//疫情地图数据处理
data.china?.map((v:IEpidData ) =>
//对于俩地图赋值
nowMapData.push(
name: v.name,
value: v.total.confirm - v.total.dead - v.total.heal || 0,
);
totalMapData.push(
name: v.name,
value: v.total.confirm || 0,
);
);
//隐藏加载 圈圈
nowMap.hideLoading();
totalMap.hideLoading();
//数据入地图配置 绘制图表
nowMap.setOption(
...optionMap,
series:
...series,
data: nowMapData,
,
);
totalMap.setOption(
...optionMap,
series:
...series,
data: totalMapData,
,
);
;
export InfoData, initDataFun ;
🌃🌃App.vue 疫情页面
<template>
<UpRefreshAndToTop @refreshFun="refreshFun">
<div class="box">
<!-- 疫情实时数据 -->
<div class="top-box">
<img class="bg-img" src="./assets/bt.jpg" />
<div class="title-text">
<h1>科学防护 共渡难关</h1>
<h2>肺炎疫情实时动态播报</h2>
<span>更新时间: lastUpdateTime </span>
</div>
<div v-if="chinaTotal" class="cover-cards">
<div class="cover-tab">
<div @click="changeType(0)" :class=" active: data.type === 0 ">
全国疫情数据(含港澳台)
</div>
<div @click="changeType(1)" :class=" active: data.type === 1 ">
江西疫情数据
</div>
</div>
<!-- 全国疫情 -->
<div class="cover-info" v-show="data.type === 0">
<div>
<h4 class="title">境外输入</h4>
<p class="number"> chinaTotal.total.input </p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.today.input </span>
</p>
</div>
<div>
<h4 class="title">无症状感染者</h4>
<p class="number"> chinaTotal.extData.noSymptom </p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.extData.incrNoSymptom </span>
</p>
</div>
<div>
<h4 class="title">现有确诊</h4>
<p class="number">
chinaTotal.total.confirm -
chinaTotal.total.dead -
chinaTotal.total.heal
</p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.today.storeConfirm </span>
</p>
</div>
<div>
<h4 class="title">累计确诊</h4>
<p class="number"> chinaTotal.total.confirm </p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.today.confirm </span>
</p>
</div>
<div>
<h4 class="title">累计死亡</h4>
<p class="number"> chinaTotal.total.dead </p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.today.dead </span>
</p>
</div>
<div>
<h4 class="title">累计治愈</h4>
<p class="number"> chinaTotal.total.heal </p>
<p class="tip">
<span>较昨日</span>
<span>+ chinaTotal.today.heal </span>
</p>
</div>
</div>
<!-- 江西疫情 -->
<div v-if="jxData" v-show="data.type === 1" class="cover-info">
<div>
<h4 class="title">累计确诊</h4>
<p class="number"> jxData.total.confirm </p>
<p class="tip">
较昨日
<span>+ jxData.today.confirm </span>
</p>
</div>
<div>
<h4 class="title">累计死亡</h4>
<p class="number"> jxData.total.dead </p>
<p class="tip">
较昨日
<span>+ jxData.today.dead </span>
</p>
</div>
<div>
<h4 class="title">累计治愈</h4>
<p class="number"> jxData.total.heal </p>
<p class="tip">
较昨日
<span>+ jxData.today.heal </span>
</p>
</div>
</div>
</div>
</div>
<!-- 疫情地图 -->
<div class="data-map content">
<h3>中国疫情</h3>
<div class="map-box">
<div
:class="data.mapType == 1 ? 'to-left' : 'to-right'"
id="nowMap"
></div>
<div
:class="data.mapType == 1 ? 'to-left' : 'to-right'"
id="totalMap"
></div>
</div>
<div class="map-btn">
<div @click="mapTypeChange(1)" :class=" active: data.mapType == 1 ">
现有确诊
</div>
<div @click="mapTypeChange(2)" :class=" active: data.mapType == 2 ">
累计确诊
</div>
</div>
</div>
<!-- 中国疫情列表 -->
<div class="data-list content">
<h3>中国病例</h3>
<span class="hint">温馨提示:点击可展示具体城市</span>
<EpidemicList :epideList="china" :showChildren="true"></EpidemicList>
</div>
<!-- 世界疫情 无点击子模块 -->
<div v-if="data.showList.length" class="data-list content">
<h3>世界病例</h3>
<DownRefresh
:distance="100"
:isScroll="data.isScroll"
@getList="getList"
>
<EpidemicList :showChildren="false" :epideList="data.showList" />
</DownRefresh>
</div>
</div>
</UpRefreshAndToTop>
</template>
<script setup lang="ts">
import onMounted, reactive, toRefs from "vue";
import InfoData, initDataFun from "./pageTs/index";
import EpidemicList from "./components/EpidemicList.vue";
import UpRefreshAndToTop from "./components/UpRefreshAndToTop.vue";
import DownRefresh from "./components/DownRefresh.vue";
const data = reactive(new InfoData());
onMounted(() =>
initDataFun(data);
);
//解构数据
const chinaTotal, jxData, china, lastUpdateTime = toRefs(data);
//切换 疫情实时数据 全国 江西
const changeType = (toType: number) =>
data.type = toType;
;
//地图 切换
const mapTypeChange = (type: number) =>
console.log(type);
data.mapType = type;
;
//下拉效果 全球疫情列表
let page: number = 0;
const getList = () =>
if (page === data.globalEpidemic.length - 1)
data.isScroll = false;
return;
console.log("加载下一页");
// 子组件触发,加载下一页数据
page++;
data.showList.push(...data.globalEpidemic[page]);
;
//重新加载数据
const refreshFun = (fun: Function) =>
initDataFun(data).then(() =>
//疫情实时数据切换成全国数据
data.type = 0;
//重置全球疫情下拉功能
page = 0;
data.isScroll = true;
//控制手指按下屏幕的title显示 去除
fun();
);
;
</script>
<style lang="scss" scoped>
// 滑动动画
@keyframes toRight
from
left: 0;
to
left: calc(0px - 100vw + 1rem);
@keyframes toLeft
from
left: calc(0px - 100vw + 1rem);
to
left: 0;
.bg-img
width: 100%;
//疫情实时数据样式
.top-box
position: relative;
.title-text
position: absolute;
z-index: 2;
color: #fff;
top: 20px;
left: 1rem;
span
color: #000;
.cover-cards
position: absolute;
top: 12rem;
background: #fff;
border-radius: 20px;
width: calc(100% - 2rem);
left: 1rem;
overflow: hidden;
box-shadow: 0 2px 20px rgb(0 0 0 / 10%);
> div
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&.cover-tab
> div
width: 50%;
background: #efefef;
text-align: center;
height: 40px;
line-height: 40px;
&.active
background: #fff;
&.cover-info
> div
width: 33%;
text-align: center;
margin: 10px 0;
&:nth-child(2)
.number,
span
color: #ffa352;
&:nth-child(3)
.number,
span
color: #791618;
&:nth-child(4)
.number,
span
color: #e44a3d;
&:nth-child(5)
.number,
span
color: #333;
&:nth-child(6)
.number,
span
color: #34aa70;
.title
font-size: 12px;
.number
font-size: 1.5rem;
font-weight: 600;
margin: 5px 0;
color: #a31d13;
span
color: #a31d13;
.tip
font-size: 12px;
//疫情地图 数组样式
.content
padding: 0 1rem;
.data-map,
.data-list
margin-top: 300px;
overflow: hidden;
h3
border-left: 8px solid #e10000;
padding-left: 1rem;
.hint
font-size: 0.5rem;
color: coral;
.map-box
display: flex;
width: 200%;
#nowMap,
#totalMap
height: 350px;
width: 50%;
animation-fill-mode: forwards;
left: 0;
#nowMap
margin-right: 1rem;
#totalMap
margin-left: 1rem;
.to-left
animation: toLeft 1s;
.to-right
animation: toRight 1s;
.map-btn,
.line-btn
display: flex;
justify-content: space-between;
align-items: center;
> div
width: 45%;
height: 40px;
line-height: 40px;
border: 1px solid #d2d2d2;
box-shadow: 0 5px 7px 1px rgb(0 0 0 / 5%);
border-radius: 5px;
text-align: center;
&.active
border-color: #ce4733;
background-color: #fef7f7;
color: #ce2c1e;
.line-btn
> div
width: 30%;
height: 55px;
line-height: 25px;
padding-top: 5px;
.data-list
margin-top: 20px;
</style>
📺📺vite.config.ts 代理开通
温馨提示:数据来源 网易云🤭
import defineConfig from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig(
plugins: [vue()],
server:
proxy:
//网易代理
'/prod-api':
target: "https://c.m.163.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\\/prod-api/,'')
)
完结
以上是关于疫情统计页面 H5 vue3+TypeScript+Echarts的主要内容,如果未能解决你的问题,请参考以下文章
EasyRTC如何基于Vue3+TypeScript技术实现在线会议室功能的分析
uniapp系列-超详细教你在uni-app+vue3里通过web-view组件传递信息打开H5页面写入localstorage并解决兼容性
vue3 移动端h5适配ipad ios android rem 适配(很简单,秒懂)