疫情统计页面 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: `${translateY}px` }"
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/,'')
}
}
}
})
完结