建项
项目需求写法——可视化报表
可视化报表项目效果
开源表格样式库阿帕奇
绘制echarte图标的流程
- 在视图中放置一个容器,这个容器需要有一个固定的宽高
- 获取容器,调用init方法,初始化echarts实例
let container = document.querySelector('.app')
let myChart = echarte.init(container) // 初始化图表实例
- 给实例调用setOption方法传入配置对象,通过这个配置对象确认绘制的图表内容(在官网寻找示例,根据示例
<body>
<div class="app" style="width: 600px;height: 400px;">
</div>
<script src="
https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js
"></script>
<script>
console.log(echarts, 123)
// 绘制echarts图标的流程
let container = document.querySelector('.app')
let myChart = echarts.init(container) // 初始化图表实例
myChart.setOption({
// 传入配置对象,通过这个配置对象确认绘制的图表内容
xAxis: {
// 默认情况下,x粥通常为category类型,y轴一般为value
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
// series决定绘制一个什么类型的图表
series: [ // 一个框只绘制一个图像时可以不用写成数组
{
data: [820, 932, 901, 934, 1290, 1330, 1320],/* 数据 */
type: 'line', /* 数据显示类型 */
areaStyle:{
color:"purple",
opacity:1,/* 颜色透明度 */
},
smooth: true, /* 光滑 */
itemStyle:{
opacity:0 // 点透明度
},
}
]
})
</script>
</body>
修改图表样式
在网站的文档中找到配置项手册,
myChart.setOption({
// 传入配置对象,通过这个配置对象确认绘制的图表内容
xAxis: {
// 默认情况下,x粥通常为category类型,y轴一般为value
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value',
splitLine:{
show: false // 去掉柱状图背景的分隔线
}
},
// series决定绘制一个什么类型的图表
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],/* 数据 */
type: 'bar', /* 数据显示类型 */
}
],
tooltip:{}, // 鼠标移上去显示标签
})
从 npm 获取 echarts
npm install echarts
Vue2项目
开始前准备
给vue的prototype原型对象挂载的属性,可以在入口中给每个组件实例都注入一些成员。
在入口注册全局组件v-chart
import Vue from 'vue'
import App from './App.vue'
import * as echarts from 'echarts'
import vcharts from 'vue-echarts'
Vue.component('v-chart', vcharts) // 注册全局组件v-chart
Vue.config.productionTip = false
// 给vue的prototype原型对象挂载的属性,可以在入口中给每个组件实例都注入一些成员
Vue.prototype.$echarts = echarts
new Vue({
render: h => h(App)
}).$mount('#app')
封装组件,初始化图表可以通过封装好的组件来完成。使用封装好的组件可以不再进行原本的初始化内容
改为了
input为v-model绑定的data
在视图中需要写入:option="option"
<div class="wrapper">
<div class="container" >
<v-chart :option="option"></v-chart>
</div>
<el-button>这是按钮</el-button>
<el-input v-model="input" placeholder="请输入内容"></el-input>
</div>
封装好的初始化图表组件网站
// 全局注册组件
npm install vue-echarts
// ui组件vue2和vue3
npm i element-ui
npm i element-plus
// reset-css 初始化全局样式组件
npm i reset-css
拆分目录,把不同的功能分散在不同的组件中
- 新建plugins文件夹
- 在文件夹下新建element-ui与vue-echarts两个js文件
- 分别将element引入与echarts引入拆分进这两个文件夹
- 把这两个文件夹引入main文件,避免main文件过于杂乱
- 其中element-ui文件可以实现组件的按需引入,以免最终项目文件过于庞大
- echarts用于注册全局组件Vue.component('v-chart', vcharts)
// main
import Vue from 'vue'
import App from './App.vue'
import * as echarts from 'echarts'
import './plugins/element-ui' // 引入组件库
import './plugins/vue-echarts'
import 'reset-css' // 引入清除默认样式css
Vue.config.productionTip = false
// 给vue的prototype原型对象挂载的属性,可以在入口中给每个组件实例都注入一些成员
Vue.prototype.$echarts = echarts
new Vue({
render: h => h(App)
}).$mount('#app')
// element-ui 实现组件的按需引入
import Vue from 'vue'
import 'element-ui/lib/theme-chalk/index.css'
import { Button, Input } from 'element-ui' // 按需引入el的组件
Vue.use(Button)
Vue.use(Input) // 组件引入后需要在下面注册一下
// vue-echarts 实现组件的全局注册
import Vue from 'vue'
import vcharts from 'vue-echarts'
Vue.component('v-chart', vcharts) // 注册全局组件v-chart
手动去除大驼峰规则
在.eslintrc.js中的rules里书写一个'vue/multi-word-component-names': 0,将命名必须要大驼峰的规则去掉
Topcomp
公共部分
- 把四个组件引入到app,由于是vue2,还需要注册一下,并在视图中写组件出口
<template>
<div class="app">
<TopComp/>
<SecondComp/>
<ThirdComp/>
<MapComp/>
</div>
</template>
<script>
import TopComp from './components/TopComp'
import SecondComp from './components/SecondComp'
import ThirdComp from './components/ThirdComp'
import MapComp from './components/MapComp'
export default {
components: {
TopComp,
SecondComp,
ThirdComp,
MapComp
}
}
- 新建main.css,写入公共样式背景颜色,并引入到入口
/* 公共样式 */
html,body {
background-color: #eee;
}
- 在top中新建上栏,写四个卡片输出到top(四个卡片结构类似,通过props传递不同的数据到不同的卡片。只需要封装一个卡片,接收两个props,两个slot。top的index是第一栏的总体入口,在CommonCard中向props中传入每个卡片需要的数据
-
- 组件库中寻找layout布局,按需引入col和row组件到element-ui
- 注册card组件,新建vue封装一个带两个插槽的卡片,最后使用插槽进行结构分发
- 在chart和footer中写插槽
- 写样式
<template>
<div class="commom-card">
<div class="title">{{ title }}</div>
<div class="value">{{ value }}</div>
<div class="chart">
<slot></slot>
</div>
<div class="line"></div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
export default {
props: ['title', 'value']
}
</script>
span.increase{
display: inline-block;
width: 0;
height: 0;
border-width: 4px;
border-color: transparent transparent green transparent;
border-style: solid;
margin-left: 10px;
transform: translateY(-50%);
}
TotalSale
新建TotalSale.vue,使用CommonCard组件写卡片中的内容
<template>
<div class="total-sale">
<CommonCard title="累计销售额" :value="reportData.salesToday">
<div>
<span>日同比</span>
<span class="css-1">{{reportData.salesGrowLastDay}}%</span>
<span class="increase"></span>
</div>
<div>
<span>月同比</span>
<span class="css-1">{{reportData.saleSGrowLastMonth}}%</span>
<span class="increase"></span>
</div>
<template #footer>
<span>昨日销售额</span>
<span class="css-1">¥{{reportData.salesLastDay }}</span>
</template>
</CommonCard>
</div>
</template>
TotalOrder
绘制第二个卡片的图TotalOrder.vue
- grid属性,最外层叫容器,内层叫网格,与容器有一些默认边距。在一些宽高很小的容器中绘图时,可以将grid网格和容器靠近,否则图片很容易无法出现——将四周数据都设为0
- 通过x,y和series设置图片的样式,在通过接口地址的数据规定图像的数据
- 使用areaStyle属性显示折线覆盖的区域,再使用itemStyle去掉折线点,在做一个平滑
- 使x轴的boundaryGap为false,去掉坐标轴两边的留白
mixins: [CommonCardMixin],
data () {
return {
option: null
}
},
mounted () {},
watch: {
reportData (newValue) {
this.renderChart(newValue.orderTrend)
}
},
methods: {
renderChart (data) {
this.option = {
xAxis: {
type: 'category',
show: false,
boundaryGap: false
},
yAxis: {
type: 'value',
show: false
},
// 在一些宽高很小的容器上绘图的时候 可以将网格和容器靠近
grid: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
series: {
type: 'line',
data,
areaStyle: {
color: 'purple'
},
lineStyle: {
width: 0
},
itemStyle: {
opacity: 0
},
smooth: true
}}}}
- 在顶部视图中显示图像和部分文字数据
<template>
<div class="total-order">
<CommonCard title="累积订单额" value="13145">
<v-chart :option="option" />
<template #footer>
<span>昨日销售额</span>
<span class="css-1">¥ 12768</span>
</template>
</CommonCard>
</div>
</template>
TodayUser
绘制第三个卡片的图,创建TodayUser.vue
- 显示footer的文字,文字-今日用户交易数
- 规定图像为柱状图
- grid限定加上,规定series,type为bar,规定name,传入data
<template>
<div class="today-user">
<CommonCard title="今日用户交易数" :value="reportData.userToday">
<v-chart :option="option"/>
<template #footer>
<span>退货率</span>
<span class="css-1">{{reportData.returnRate}}%</span>
</template>
</CommonCard>
</div>
</template>
<script>
import CommonCardMixin from '../../mixins/CommonCardMixin.js'
export default {
mixins: [CommonCardMixin],
data () {
return {
option: null
}
},
methods: {
renderChart (data) {
this.option = {
xAxis: {
type: 'category',
show: false,
data: [
'00:00',
'03:00',
'05:00',
'07:00',
'09:00',
'11:00',
'13:00',
'15:00',
'17:00',
'19:00',
'21:00',
'23:00'
]
},
yAxis: {
type: 'value',
show: false
},
tooltip: {},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0
},
series: {
type: 'bar',
name: '实时交易量',
data,
barWidth: '60%'
}
}
}
},
mounted () {},
watch: {
reportData (newValue) {
this.renderChart(newValue.orderUserTrend)
}
}
}
ToalUser
- 绘制第四个卡片的图ToalUser.vue
-
- 显示footer的文字,三角,上半的文字
- 调换x轴和y轴,使柱状图横向排列——x轴为value,y轴为category
- grid限定加上,规定series,type为bar,规定name,传入data
- 调整barWidth为10,itemStyle的color为green
- 绘制第四个卡片的三角形type=custom(配置项文档中找)可以自定义一些形状不同的图表
-
- 在后面再写一个图标项目(此时series为数组类型,该图表为数组中第二个项目)
- 由于横向柱状图规定了data为130,则在custom中,data也是130,与柱状图等长。data写在renderItem后面。
- renderItem可以自定义渲染逻辑,传入两个参数(文档中规定的)params,api
- 绘制三角形(利用svg路径)确定两值:一个是三角形的位置,二是如何绘制三角形本身
- api。value为130这个值,其中传入的参数[0],0,是他的位置
- return一个对象,这里为要画的图类型,由于需要画两个三角形,所以return一个type:group,并为group写两个children
- 为每一个children写类型path和路径,shape中的d为具体的路径,
- 第一个三角形的路径为 'M511.744 319.999l-383.744 383.744h767.488l-383.744-383.744z' ,第二个三角形的路径为 'M889.696 320.8H158.848l365.504 365.536 365.344-365.536z'
- x为x轴上的偏移量,y为y轴上的偏移量偏移量(第一个为35,第二个为5),其中宽高为10,layout为cover
- 给style的fill改为green
series: [
{
type: 'bar',
name: '上月平台用户数',
data: [data1],
barWidth: 10,
itemStyle: {
color: 'green'
},
stack: '1'
}, {
type: 'bar',
name: '本月平台用户数',
data: [data2],
itemStyle: {
color: '#ddd'
},
barWidth: 10,
stack: '1'
}, {
type: 'custom',
renderItem: (params, api) => {
// 绘制三角形(利用svg路径) 确定2个是: 确定三角形位置 如何绘制2个三角形本身
const endPoint = api.coord([api.value(0), 0])
return {
type: 'group',
children: [
{
type: 'path',
shape: {
d: 'M511.744 319.999l-383.744 383.744h767.488l-383.744-383.744z',
x: endPoint[0] - 5,
y: 35,
width: 10,
height: 10,
layout: 'cover'
},
style: {
fill: 'green'
}
},
{
type: 'path',
shape: {
d: 'M889.696 320.8H158.848l365.504 365.536 365.344-365.536z',
x: endPoint[0] - 5,
y: 5,
width: 10,
height: 10,
layout: 'cover'
},
style: {
fill: 'green'
}
}
]
}
},
data: [data1]
}
接口部分
写接口http://project.x-zd.net:3001/apis/reportdata
- 新建api文件夹,用来封装请求方法
- 新建axios.js,对axios进行封装,使用axios的create方法,提炼出URL,并增加响应时间
- 利用axios拦截器处理返回数据,只需要data数据
// 封装发送请求的axios
import axios from 'axios'
const request = axios.create({
baseURL: 'http://project.x-zd.net:3001/apis',
timeout: 3000
})
// 利用axios拦截器处理一下返回的数据(只想要返回的data字段)
request.interceptors.response.use((res) => {
return res.data
}, (err) => { return Promise.reject(err) })
export default request
- index.js,中封装请求的具体方法,getRportDat
// 封装请求数据的具体方法
import request from './axios'
export const getReportData = () => request.get('/reportdata')
- 传入index中进行调用,存入数据,给四个卡片进行传入
import { getReportData } from '../../api'
export default {
data () {
return {
reportData: {}
}
},
components: { TotalSale, TotalOrder, TodayUser, TotalUser },
async mounted () {
const res = await getReportData()
this.reportData = res
}
}
<template>
<div class="top-comp">
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover">
<TotalSale :reportData="reportData"/> <!-- 示例,分别传给四个组件 -->
- 在top的四个组件中接收接口传入的数据,将写死的数据改为接口中的数据,使用接受到的有值的值进行页面内容渲染
<template>
<div class="total-user">
<CommonCard title='累计用户数' :value="reportData.totalUser">
<v-chart :option="option"/>
<template #footer>
<div class="wrapper">
<div>
<span>日同比</span>
<span class="css-1">{{reportData.userGrowLastDay}}%</span>
<span class="increase"></span>
</div>
<div>
<span>月同比</span>
<span class="css-1">{{reportData.userGrowLastMonth}}%</span>
<span class="increase"></span>
- 在有表格的组件中,给renderChart方法传入一个data参数,将表格中的数据改为传入的data
- 加载的时候在mounted中传入参数(是空对象,不传了),并且监听reportData,得到最新值,传入监听的reportData中renderChart方法
export default {
mixins: [CommonCardMixin],
data () {
return {
option: null
}
},
methods: {
renderChart (data1, data2) {
this.option = {
xAxis: {
type: 'value',
show: false
},
yAxis: {
type: 'category',
show: false
},
grid: {...},
series: [
{
type: 'bar',
name: '上月平台用户数',
data: [data1],
barWidth: 10,
itemStyle: {
color: 'green'
},
stack: '1'
}, {
type: 'bar',
name: '本月平台用户数',
data: [data2],
itemStyle: {
color: '#ddd'
},
barWidth: 10,
stack: '1'
}, {
type: 'custom',
......
},
data: [data1]
}
]
}
}
},
mounted () {
// this.renderChart()
},
watch: {
reportData (newValue) {
this.renderChart(newValue.userLastMonth, newValue.userToday)
}
}
}
mixin公共逻辑书写
四个头部卡片中拥有一些相同逻辑,并且都接受了rportData,可以将相同的逻辑使用mixin(混入,用来提取公共逻辑)抽离出来。就不用再在不同的组件中重复书写相同的逻辑。
- 新建文件夹mixin文件夹,在其中写一个commoncardMixin.js
- 把公共配置放在这里(引入和传参
import CommonCard from '../components/TopComp/CommonCard.vue'
export default {
props: ['reportData'],
components: {
CommonCard
}
}
- 在各个组件中的data上面写,mixins: [CommonCardMixin],即可在每个组件中不写mixin中的内容(数组形式,可以写多个mixin)
import CommonCardMixin from '../../mixins/CommonCardMixin.js'
export default {
mixins: [CommonCardMixin],
data () {
return {
option: null
}
},
SecondComp
效果演示
第二栏接口数据
头部导航栏
- 现在index中书写第二栏的显示页面
-
- 在显示页面写头部插槽-card组件(不要用element自带的插槽,会多包一层div,使用template来写不用多包div)
<template>
<div class="second-comp">
<el-card class="box-card">
<template #header>
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
mode="horizontal"
@select="handleSelect"
>
<el-menu-item index="1">销售额</el-menu-item>
<el-menu-item index="2">访问量</el-menu-item>
</el-menu>
- 使用NavMenu导航菜单组件,为导航栏写两个标签页切换。
-
- elsumenu为二级菜单,如果需要可以加上
- 修改标签页的内容
- disabled为禁用,此处不需要禁用
- 把activeIndex设定为1,为默认index为1的视图显示
export default {
data () {
return {
activeIndex: '1',
-
- 为menu和menuItem注册按需注册element
import Vue from 'vue'
import 'element-ui/lib/theme-chalk/index.css'
import { Button, Input, Row, Col, Card, Menu, MenuItem, RadioButton, RadioGroup, DatePicker } from 'element-ui'
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(RadioButton)
Vue.use(RadioGroup)
Vue.use(DatePicker)
-
- handleSelect绑定切换标签页事件,并修改标签页样式(上边距,去掉header的下边线)。element样式有时需要使用样式穿透
export default {
data () {...},
methods: {
handleSelect (index) {
this.activeIndex = index// 让选中的菜单激活
...},
- 使用radio单选框样式和DatePicker日期选择器,为头部导航栏写时间切换栏。
-
- 使用radio-group进行右侧导航栏书写,给右边导航栏套一个div,方便定位书写(写样式和定位
- 在element-ui中注册RadioButton和RadioGroup
- DatePicker日期选择器书写右侧组件的右侧部分
...
<template #header>
...
<div class="right">
<el-radio-group v-model="time">
<el-radio-button label="今日"></el-radio-button>
<el-radio-button label="本周"></el-radio-button>
<el-radio-button label="本月"></el-radio-button>
<el-radio-button label="今年"></el-radio-button>
</el-radio-group>
<el-date-picker
v-model="pickerTime"
type="daterange"
align="right"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:picker-options="pickerOptions"
>
</el-date-picker>
</div>
</template>
-
- 在data中实现一下v-model绑定的pickerTime, PickerOptions则在element中已经写好可以直接使用
export default {
data () {
return {
activeIndex: '1',
time: '今日',
pickerTime: '',
pickerOptions: {
shortcuts: [
{
text: '最近一周',
onClick (picker) {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
picker.$emit('pick', [start, end])
}
},
{
text: '最近一个月',
onClick (picker) {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
picker.$emit('pick', [start, end])
}
},
{
text: '最近三个月',
onClick (picker) {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
picker.$emit('pick', [start, end])
}
}
]
},
柱状图默认插槽
- index中写一个template,写默认插槽#default,下套一个div.content,再下套左右两个div,left和right
<template #default>
<div class="content">
<div class="left-chart">
<v-chart :option="option" />
</div>
<div class="right-list">
<div class="list-title">排行榜</div>
<div class="list-item" v-for="item in rankData" :key="item.no" >
<span :class="{'top-3':item.no<=3}">{{ item.no }}</span>
<span>{{ item.title }}</span>
<span>{{ item.sales }}</span>
</div>
</div>
</div>
</template>
- 在左边div中嵌套一个v-chart,并为其定义一个动态的:option="option",并在data中定义为一个空对象
export default {
data () {
return {...
option: {},}}
- 给left和right写样式,左边:flex:0 0 70%,不拉伸不缩小,占据70%宽度。右边flex:1,占据剩余全部
.content {
display: flex;
.left-chart {
flex: 0 0 70%;
height: 434px;
}
.right-list {...}
}
- 在methods中写一个renderChart,其中option为图表方法
-
- 写title样式和文字
- 柱状图,不隐藏x轴(show=false不写),x轴为类目轴,需要data值,为1-12月的月份;y轴是value
- 由于该图表较大,暂时不写grid,最后视情况调整grid边框(left为40,
- 调整柱状图的样式(粗细,颜色,y轴的splitLine-背景轴线样式。
export default {
data () {
return {...}
},
methods: {
handleSelect (index) {...},
renderChart (data1, data2) {
this.option = {
title: {
text: '年度销售额',
textStyle: {
fontWeight: 600,
fontSize: 14
}
},
xAxis: {
type: 'category',
data: data1,
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dotted'
}
}
},
grid: {
left: 40
},
series: {
type: 'bar',
data: data2,
barWidth: '40%'
},
color: 'skyblue'
}
}
},
async mounted () {
const res = await getSaleData()
this.saleData = res
this.rankData = res.saleRank
this.renderChart(this.saleData.saleFulleYearAxis,
this.saleData.saleFulleYear)
}
}
- 在mounted中调用this.renderChart()方法↑
右侧排行榜
- 在index中上部div——list-title
- 下部div——list-item,静态三个span,分别为排行,名称,数值
<template>
<div class="second-comp">
<el-card class="box-card">
<template #header>....</template>
<template #default>
<div class="content">
<div class="left-chart"><v-chart :option="option" /></div>
<div class="right-list">
<div class="list-title">排行榜</div>
<div class="list-item"
v-for="item in rankData"
:key="item.no" >
<span :class="{'top-3':item.no<=3}">{{ item.no }}</span>
<span>{{ item.title }}</span>
<span>{{ item.sales }}</span>
</div>
</div>
</div>
</template>
- 写样式
- span写弹性盒子flex布局,其中第二个span设置flex:1,占满剩余空间。第一个span有圆形框
.content {
display: flex;
.left-chart {...}
.right-list {
flex: 1;
.list-title {
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
}
.list-item {
margin: 20px 0px;
display: flex;
gap: 20px;
span {
font-size: 14px;
color: #464545;
}
span:nth-child(2) {
flex: 1;
}
span:nth-child(1) {
width: 20px;
height: 20px;
border-radius: 10px;
text-align: center;
line-height: 20px;
}
.top-3{
background-color:#09b3f7 ;
color:#fff
}
}
}
}
封装接口数据
- 在api的index中封装接口数据
// 封装请求数据的具体方法
import request from './axios'
export const getReportData = () => request.get('/reportdata')
export const getSaleData = () => request.get('/saledata')
- 并在second中的mounted中调用一下(记得从api中引入
- 在data中注册saleData。
import { getSaleData } from '@/api'
export default {
data () {
return {
activeIndex: '1',
time: '今日',
pickerTime: '',
pickerOptions: {...},
option: {},
saleData: null,
rankData: []
}
},
- 在list-item中遍历rankData(右边排行的data),并将写死的数据改为动态数据
- 在第一个span中绑定一个动态类名,在item.no<=3的时候,动态添加圆形框的背景颜色,字体颜色为白。
<div class="right-list">
<div class="list-title">排行榜</div>
<div class="list-item" v-for="item in rankData" :key="item.no" >
<span :class="{'top-3':item.no<=3}">{{ item.no }}</span>
<span>{{ item.title }}</span>
<span>{{ item.sales }}</span>
</div>
</div>
- 将图表数据替换为动态的data
点击标签切换图表视图
- handleSelect事件中,把index值赋值给activeIndex,让点选的菜单激活
- 如果index为1,则激活销售额视图,获取销售额视图的数据saleFulleYearAxis,并同步渲染到右边排行榜rankData
- else,获取访问量数据,放在方法中,激活访问量视图visitFullYeadAxis(切换仅仅切换Data,即可渲染不同的数据
export default {
data () {
return {
...
saleData: null,
rankData: []
}
},
methods: {
handleSelect (index) {
this.activeIndex = index// 让选中的菜单激活
if (index === '1') {
this.rankData = this.saleData.saleRank
this.renderChart(this.saleData.saleFulleYearAxis,
this.saleData.saleFulleYear)
} else { // 此处yead为接口名写错,是可以使用的
this.rankData = this.saleData.visitRank
this.renderChart(this.saleData.visitFullYeadAxis,
this.saleData.visitFullYear)
}
},
ThirdComp
可以不拆成两个组件,统一写在项目结构中(也可以拆开写成两个组件,主要是看有没有别的地方也用了类似格式
整体布局
- 给根div类名third-comp,分两个div,left和right
- 先写third-comp基本样式css。css的>表示子代选择器,给左右两个宽度flex:1
.third-comp {
margin-top: 20px;
display: flex;
gap: 20px;
& > div {
flex: 1;
}
- 先给左边写el-card,给他添加hover。
- 在下面写一个header插槽,下面关键词搜索
- 再下一个main部分,写两个charts,一左一右,再一个table
<template>
<div class="third-comp">
<div class="left">
<el-card shadow="hover">
<template #header>
<div>关键词搜索</div>
</template>
<div class="main">
<div class="charts">
<div class="left-chart">...</div>
<div class="right-chart">...</div>
</div>
<div class="table">...</div>
</div>
</el-card>
</div>
<div class="right"></div>
</div>
</template>
左侧图表部分
- 先写左边的整体框架
-
- charts下面v-chart :option=option1,在data中空对象一个option1,右边同左,改为option2
<div class="main">
<div class="charts">
<div class="left-chart">
<div class="title">搜索用户量</div>
<div class="number">{{ totalUser }}</div>
<v-chart :option="option1" />
</div>
<div class="right-chart">
<div class="title">搜索量</div>
<div class="number">{{ totalSearch }}</div>
<v-chart :option="option2" />
</div>
</div>
-
- table组件和分页器(Pagination)组件在element中注册一下
import Vue from 'vue'
import 'element-ui/lib/theme-chalk/index.css'
import { ... Table, TableColumn, Pagination } from 'element-ui'
...
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Pagination)
-
- el-table,绑定一个:data=tableData,并在data中注册tableData为空数组
<div class="main">
<div class="charts">
<div class="left-chart">... </div>
<div class="right-chart">... </div>
</div>
<div class="table">
<el-table :data="tableData">
<el-table-column></el-table-column>*4
</el-table>
-
- 在分页器组件中选择有背景颜色的分页器组件来使用,total为总共的条目数,page-size为每页显示的个数,有多少页得到的是total/page-size的结果
<div class="main">
<div class="charts">
<div class="left-chart">... </div>
<div class="right-chart">... </div>
</div>
<div class="table">
<el-table :data="tableData">...</el-table>
<el-pagination
background
layout="prev, pager, next"
:total="20"
:page-size="pageSize"
@current-change="currentChange"
>
</el-pagination>
</div>
</div>
- 获取左侧数据,并升序排序,制成表格
-
- 在api中引入网址,并在第三栏中从api中注册
import request from './axios' ...
export const getKeyWordData = () => request.get('/keyworddata')
-
- async mounted中const一个res,await注册的数据()
- 此时不能直接将数据传给tableData,需要注册一个totalData为空对象,并把res赋值给totalData,并做截取,初始状态下的totalData为totalData的前六个数据slice(0, 6)(6改为pageSize
export default {
data () {...}
},
methods: {
currentChange (page) {...}
},
async mounted () {
const res = await getKeyWordData()
this.totalData = res
// 初始状态下,tableData显示前六条数据
this.tableData = this.totalData.slice(0, this.pageSize)
}
}
-
- 为视图中的el-table-column绑定不同的prop和label,prop来自于数据相对应的字段
- 调整第一个column的宽度,并让所有column居中显示文本
<el-table :data="tableData">
<el-table-column prop="rank"
label="排名" width="60"></el-table-column>
<el-table-column prop="keyWord"
label="关键词" align="center"></el-table-column>
<el-table-column prop="totalSearch"
label='总搜索量' align="center"></el-table-column>
<el-table-column prop="totalUser"
label="搜索用户数" align="center"></el-table-column>
</el-table>
-
- 写table的el-pagination的样式
- 在currentChange传入的page可以得到当前选中的页码,在点选后,需要重新修改渲染的tableData。需要提炼分页规律,在第一页时(0+1, 6)2(6+1, 12)3(12+1, 18)4(18+1, 24)
- 单独在el-pagination封装一个:pageSize,data中pageSize为6(pageSize在下面写成this调用模式,即可在data中修改pageSize来改变每页显示的条目数
<div class="table">
<el-table :data="tableData">... </el-table>
<el-pagination
background
layout="prev, pager, next"
:total="20"
:page-size="pageSize"
@current-change="currentChange"
>
</el-pagination>
</div>
-
- 在currentChange中书写点选切换页码时列表切换的逻辑。该方法由组件库源码调用,不用自己调用
export default {
data () {
return {
option1: {},
option2: {},
tableData: [],
totalData: {},
pageSize: 6
}},
methods: {
currentChange (page) {
// page为当前选中的页码
this.tableData = this.totalData.slice(this.pageSize * (page - 1),
this.pageSize * page)}},
async mounted () {...}}
- 使用左侧数据,画有面积的折线图
-
- 在methods中写一个renderChart1(data),在其中写图
- option1,不要x轴,留白不要,小容器图表grid0,
- series中覆盖的区域写颜色,itemStyle透明度0,平滑smooth为true
export default {
data () {
return {
option1: {},
option2: {},
tableData: [],
totalData: {},
pageSize: 6
}
},
methods: {
currentChange (page) {
// page为当前选中的页码
this.tableData = this.totalData.slice(this.pageSize * (page - 1), this.pageSize * page)
},
renderChart1 (data) {
this.option1 = {
xAxis: {
type: 'category',
show: false,
boundaryGap: false
},
yAxis: {
type: 'value',
show: false
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0
},
series: {
type: 'line',
data,
areaStyle: {
color: 'skyblue'
},
itemStyle: {
opacity: 0
},
smooth: true
}
}
},
renderChart2 (data) {
this.option2 = {
xAxis: {
type: 'category',
show: false,
boundaryGap: false
},
yAxis: {
type: 'value',
show: false
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0
},
series: {
type: 'line',
data,
areaStyle: {
color: 'skyblue'
},
itemStyle: {
opacity: 0
},
smooth: true
}
}
}
},
-
- 在mounted中执行以下renderChart1方法,传参为this.totalData,给数据调map方法,item中只需要totalUser字段,只要10个升序排列的数据(翻转)
async mounted () {
...
this.renderChart1(this.totalData.map(item => item.totalUser).slice(0, 10).reverse())
this.renderChart2(this.totalData.map(item => item.totalSearch).slice(0, 10).reverse())
}
-
- 在计算属性中写totalUser和totalSearch,使用reduce属性来计算,初始值为0,return(初始值需要和定义的类型相同,定义的是数组,初始值也要写成数组)
computed: {
totalSearch () {
return this.totalData.reduce((pre, cur) => {
return pre + cur.totalSearch
}, 0)
},
totalUser () {
return this.totalData.reduce((pre, cur) => {
return pre + cur.totalUser
}, 0)
}
},
右侧图表部分
整体框架
- right为一个带头部的饼状图
- 给template添加header插槽,在right中给右边卡片以及顶部插槽写定位
<template>
<div class="third-comp">
<div class="left">...</div>
<div class="right">
<el-card>
<template #header>
-
- right的高度为100%,高度和左边持平
- el-card为组件内部样式,需要给它的header和body写样式穿透::v-deep
.right {
.el-card {
height: 100%;
::v-deep .el-card__body {
height: 558px;
.pie-chart {
height: 100%;
}
}
::v-deep .el-card__header {
position: relative;
.el-radio-group {
position: absolute;
right: 2%;
top: 10%;
}
}
}
}
- el-radio-group下面有两个el-radio-button
- 在template下面写一个<div class="pie-chart">嵌套带:option的v-chard
<div class="right">
<el-card>
<template #header>
<div class="css-2">分类销售排行</div>
<el-radio-group v-model="radio" @input="handleRadio">
<el-radio-button label="品类"></el-radio-button>
<el-radio-button label="商品"></el-radio-button>
</el-radio-group>
</template>
<div class="pie-chart">
<v-chart :option="pieChartOption" />
</div>
</el-card>
</div>
- 在methods中写renderPirChart方法,传一个data
-
- 给一个副标题title(写成数组形式可以写多个),并给副标题一个left和top的偏移量
- 再写一个副标题,累计订单量,其中有一个subtext为计算属性,给偏移量
renderPieChart (data) {
// 需要给data添加上一个name字段 从而可以让legend读到
data = data.map((item) => {
item.name = item.title + '|' + item.value
return item
})
const totalSale = data.reduce((pre, cur) => {
return pre + cur.value
}, 0)
this.pieChartOption = {
title: [
{
text: '品类分布',
textStyle: {
fontSize: 14,
color: '#666'
},
left: 20,
top: 20
},
{
text: '累计订单量',
subtext: totalSale,
x: '40%',
y: '45%',
textAlign: 'center',
textStyle: {
fontSize: 14,
color: '#999'
},
subtextStyle: {
fontSize: 28,
color: '#333'
}
}
],
-
- 给series写一个符合当前视图的name,最后会显示在视图上。type为pie,定位为center,给center(环形的中心点位置)设置偏移量
- 给环状图设置label设置show,位置为outside,
renderPieChart (data) {
// 需要给data添加上一个name字段 从而可以让legend读到
data = data.map((item) => {
item.name = item.title + '|' + item.value
return item
})
const totalSale = data.reduce((pre, cur) => {
return pre + cur.value
}, 0)
this.pieChartOption = {
title: [...],
series: {
name: '品类分布',
type: 'pie',
data,
radius: ['45%', '60%'],
center: ['40%', '50%'],
itemStyle: {
borderWidth: 8,
borderColor: '#fff'
},
label: {
show: true,
position: 'outside',
formatter: (params) => {
return params.data.title
}
}
},
-
- title无法显示,由于数据变成了对象的进一步嵌套,为了得到每一个数据中的title,在文档中的lable中找formatter的params,通过回调函数得到params.data.title
- 在lable下面写tooltip提示框组件,触发类型中,把tirgger设置为item,再给它每一项根据数据的名字,写字符串拼接和圆点换行符的格式调整
renderPieChart (data) {
// 需要给data添加上一个name字段 从而可以让legend读到
data = data.map((item) => {
item.name = item.title + '|' + item.value
return item
})
const totalSale = data.reduce((pre, cur) => {
return pre + cur.value
}, 0)
this.pieChartOption = {
title: [...],
series: {...},
tooltip: {
trigger: 'item',
formatter: (params) => {
return params.seriesName + '<br/>' + params.marker + params.data.title + '<br/>' + params.marker + '销售额' + params.data.value
}
},
-
- 在公共样式中消除边距并box-sizing: border-box;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
- 在api中加载接口数据,引入组件,在async mounted中调用,传入需要数据的部分
async mounted () {
const _res = await getCategoryData()
this.categoryData = _res
this.renderPieChart(this.categoryData.data1)
}
- 在tooltip下面写legend(含二次处理数据)
-
- 距离左边80%,改文本样式
- legend无法直接读取到data中的name属性(根本没有),所以给data添加一个name字段,从而让legend读取到
- 重新使用map遍历data,给item增加name字段,值为title与value的拼接
- 返回处理好的item
renderPieChart (data) {
// 需要给data添加上一个name字段 从而可以让legend读到
data = data.map((item) => {
item.name = item.title + '|' + item.value
return item
})
const totalSale = data.reduce((pre, cur) => {
return pre + cur.value
}, 0)
this.pieChartOption = {
title: [...],
series: {...},
tooltip: {...},
legend: {
// 会自动读取data数据的 name字段
left: '80%',
top: 'top',
textStyle: {
color: '#888'
}
}
}
},
-
- let一个totalSale,在计算属性中,对item的value进行累加,return一个累加和
- 把subText的值替换为动态累加结果totalSale
computed: {
totalSearch () {
return this.totalData.reduce((pre, cur) => {
return pre + cur.totalSearch
}, 0)
},
totalUser () {
return this.totalData.reduce((pre, cur) => {
return pre + cur.totalUser
}, 0)
}
},
- 做radio-group中品类和商品的切换@input=handleRadio
-
- 在methods中写handleRadio方法,传参label
- if-else,选哪个就渲染相应的数据
export default {
data () {...},
methods: {
handleRadio (label) {
if (label === '品类') {
this.renderPieChart(this.categoryData.data1)
} else {
this.renderPieChart(this.categoryData.data2)
}
}
},
MapComp
创建应用
在百度地图开发者平台中的应用管理中创建应用
拆分为三个组件来写
将三个组件vue引入index,注册,写在视图中,添加样式
在需要绘图的组件上规定宽高,否则无法撑起盒子
地图
- 使用百度地图开发者平台,在public中的index里引入百度地图(百度地图引入指南
<head>
....
<script src="https://api.map.baidu.com/api?v=2.0&ak=秘钥"></script>
</head>
- 新建BmapScatter.vue
- 在data中option:null
- 引入echarts中对百度地图的支持
<script>
import 'echarts/extension/bmap/bmap'
export default {
- 在methods中定义renderChart方法,初始化。
-
- 在方法中bmap的key中填写秘钥ak。
- 设置center,初始化时地图的显示中心(可以设置为咸阳 108.954355, 34.346721
- 设置zoom为5,初始缩放
- roam,默认是否能缩放,布尔值
- mounted中this.renderChart
export default {
data () {
return {
option: null
}
},
methods: {
renderChart () {
this.option = {
bmap: {
key: '秘钥',
center: [108.954355, 34.346721],
zoom: 5,
roam: false, // 是否可以缩放
mapStyle: {...}
}
}
}
},
mounted () {
this.renderChart()
}
-
- 可以通过mapStyle使用JSON对象来设置地图风格(平时不需要加载,太大了
styleJson: [
{
featureType: 'water',
elementType: 'all',
stylers: {
color: '#d1d1d1'
}
},
{
featureType: 'land',
elementType: 'all',
stylers: {
color: '#f3f3f3'
}
},
{
featureType: 'railway',
elementType: 'all',
stylers: {
visibility: 'off'
}
},
{
featureType: 'highway',
elementType: 'all',
stylers: {
color: '#fdfdfd'
}
},
{
featureType: 'highway',
elementType: 'labels',
stylers: {
visibility: 'off'
}
},
{
featureType: 'arterial',
elementType: 'geometry',
stylers: {
color: '#fefefe'
}
},
{
featureType: 'arterial',
elementType: 'geometry.fill',
stylers: {
color: '#fefefe'
}
},
{
featureType: 'poi',
elementType: 'all',
stylers: {
visibility: 'off'
}
},
{
featureType: 'green',
elementType: 'all',
stylers: {
visibility: 'off'
}
},
{
featureType: 'subway',
elementType: 'all',
stylers: {
visibility: 'off'
}
},
{
featureType: 'manmade',
elementType: 'all',
stylers: {
color: '#d1d1d1'
}
},
{
featureType: 'local',
elementType: 'all',
stylers: {
color: '#d1d1d1'
}
},
{
featureType: 'arterial',
elementType: 'labels',
stylers: {
visibility: 'off'
}
},
{
featureType: 'boundary',
elementType: 'all',
stylers: {
color: '#fefefe'
}
},
{
featureType: 'building',
elementType: 'all',
stylers: {
color: '#d1d1d1'
}
},
{
featureType: 'label',
elementType: 'labels.text.fill',
stylers: {
color: '#999999'
}
}
]
- 给地图写title
散点图绘制
在地图上写散点图,title下面写。把series写为数组对象,一组普通散点,一组波纹散点
基础散点图
- 在配置选项手册里找coordinateSystem:bmap,意味着绘图的坐标系改为bmap
- type为scatter,并从后端传递data
export default {
data () {
return {
option: null
}
},
methods: {
renderChart (data) {
this.option = {
bmap: {...},
title: {
text: '新中地网点地图',
left: 'center'
},
series: [
{
name: '新中地外卖',
coordinateSystem: 'bmap',
type: 'scatter',
data
}, {}
]
- 在api中引入mapdata
import request from './axios' ...
export const getMapData = () => request.get('/mapdata')
- mapdata中返回了两组值,为城市和序号(city),城市名称和经纬度(geodata)
- 整合这两组数据,生成一个唯一的data。数组的每个对象中,有name:city,value[经度,纬度,销售额],最终将整合后的data传入renderChart,渲染该数据
- 在bmap引入getMapData
- async mounted中写一个整合数据的方法converData,由于仅在本组件中调用,可以在export前面直接function
import { getMapData } from '@/api'
function converData (city, geodata) {
// city => [{name:'海门',value:10},{}...]
// geodata => {'海门':[80,100],....}
// res => [{name:'海门',value:[80,100,10]},...]
const res = []
city.forEach(item => {
// item.name为城市名称,可以根据城市名称找到经纬度
const geo = geodata[item.name]
if (geo) {
res.push({
name: item.name,
value: geo.concat(item.value) // 拼接数组
})
}
- 在methods的series中添加encode和symbolSize两个属性共同控制散点图的出现与大小(value[2]/10是控制大小用的,每个大小只有0.2
methods: {
renderChart (data) {
this.option = {
bmap: {...},
title: {...},
tooltip: {
trigger: 'item'
},
series: [
{
name: '新中地外卖',
coordinateSystem: 'bmap',
type: 'scatter',
data,
encode: {
value: 2
},
symbolSize (value) {
return value[2] / 10
}
}, {}
]
- 给图添加tooltip,其中trigger为item,是数据中,对应的属性。
涟漪散点图
- 在series的第二个数组元素中写
-
- 起名
- 坐标系为bmap
- type类型effectScatter
- data数据做排序,截取销售额最高的10个点,制作涟漪效果(value[2]为销售额,0,1为经纬度
}, {
name: '新中地外卖',
coordinateSystem: 'bmap',
type: 'effectScatter',
data: data.sort((a, b) => {
return b.value[2] - a.value[2]
}).slice(0, 10),
encode: {
value: 2
},
-
- rippleEffect中的brushType可以更改涟漪效果的样式,将样式更改为波纹的涟漪效果stroke
- 散点图的大小更改为[2]/10
- 更改波纹涟漪rippleEffect的color
symbolSize (value) {
return value[2] / 10
},
rippleEffect: {
brushTypy: 'storke',
color: 'purple'
},
- 提示框tooltip
-
- formatter,拼接字符串,得到提示框内容
- 更改提示框字体颜色,改为绿色
export default {
data () {},
methods: {
renderChart (data) {
this.option = {...
series: [
{...}, {
name: '新中地外卖',
coordinateSystem: 'bmap',
type: 'effectScatter',
data: data.sort((a, b) => {
return b.value[2] - a.value[2]
}).slice(0, 10),
encode: {
value: 2
},
symbolSize (value) {
return value[2] / 10
},
rippleEffect: {
brushTypy: 'storke',
color: 'purple'
},
tooltip: {
formatter: (params) => {
return params.data.name + '销售额'
+ params.data.value[2]
},
textStyle: {
color: 'green'}}}]}}}}
水滴图
LiquidFill.vue
在npm中搜索npm install echarts-liquidfill
echarts-liquidfill - npm
- 安装包,并在liquidfill.vue中引入
<script>
import 'echarts-liquidfill'
export default {...
- 视图书写和data
- 在methods中封装renderChart方法
-
- this.option中series
- type为水球图,水球高度data0.6(60%)
- radius字体缩放?
- color写成数组,可以给多个波浪添加不同的颜色
- 振幅amplitude为4%
- 更改outline的样式
export default {
data () {
return {
option: {}
}
},
methods: {
renderChart (data) {
this.option = {
series: {
type: 'liquidFill',
data: [data],
radius: '80%',
color: ['red'],
amplitude: '4%',
outline: {
borderDistance: 2,
itemStyle: {
borderWidth: 2
}
...
- 在mounted中调用一下
-
- 在import中调用封装好的data{getReportData}
- 写成异步形式
- 调用this.renderChart把传入的数据做简单处理并转为数值型(传入时为字符串型
- 除以100再保留两位,处理为水滴图可以接收的数值(0.02
<script>
import { getReportData } from '@/api'
export default {
...
async mounted () {
const res = await getReportData()
this.renderChart((+res.salesGrowLastDay / 100).toFixed(2))
}
}
词云图
WordCloud.vue
npm install echarts-wordcloud
echarts-wordcloud
在仓库地址中寻找使用说明
- 安装包,并在wordcloud.vue中引入
<script>
import 'echarts-wordcloud'
export default {...
- 视图书写和data
<template>
<v-chart :option="option" />
</template>
-
- 类型为词云图
- shape为cardioid(可以根据说明改变
- 词云图的数据为搜索量最高的几个数据,可以直接引入
<script>
import 'echarts-wordcloud'
import { getKeyWordData } from '@/api'
export default {
data () {
return {
option: {}
}
},
methods: {
renderChart (data) {
this.option = {
series: {
type: 'wordCloud',
shape: 'cardioid',
data,
...
- 异步引用方法
- 传入数据getKeyWordData
-
- 词云图插件规定传入的data必须为数组,每个数组项目必须拥有一个name字段和value字段
- 但传入的数据getKeyWordData中没有这两个字段,需要改传入数据的字段
<script>
import 'echarts-wordcloud'
import { getKeyWordData } from '@/api'
export default {
data () {...},
methods: {...},
async mounted () {
let res = await getKeyWordData()
res = res.slice(0, 6).map(item => {
return {
name: item.keyWord,
value: item.totalSearch
}
})
this.renderChart(res)
}
}
</script>
- 修改词云图的样式
-
- 宽度高度
- 颜色为随机数写法(随机rgb颜色)
- 鼠标移入样式tooltip
- 鼠标移入样式其他隐藏的阴影效果
-
- emphasis属性,直接从文档中拷贝
export default {
data () {...},
methods: {
renderChart (data) {
this.option = {
series: {
type: 'wordCloud',
shape: 'cardioid',
data,
width: '100%',
height: '100%',
textStyle: {
// Color can be a callback function or a color string
color: function () {
// Random color
return 'rgb(' + [
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') + ')'
}
},
emphasis: {
focus: 'self',
textStyle: {
textShadowBlur: 5,
textShadowColor: '#333'
}
}
},
tooltip: {} // 空数组即可,只要出现
}
}
},