一、前端项目准备
1.vue-cli 搭建项目
npm install @vue/cli -g (一台电脑只执行一次即可)
vue create 项目名
选 (下键选择 空格确认)
:Manually select features 手动选择某些特性
:Router Vuex CSS
:2.0 x
Use history mode for router?是否使用路由: no
CSS预处理语言 : Less
ESLint 配置模式 - 标准模式:ESLint + Standard config
何时出现ESLint提示- 保存时:Lint on save
配置问件处理:In dedicated config files 单独存放
Save this as a preset for future projects? (y/N) :no
cd 项目名
npm run serve
2.删除无关代码
①App.vue文件中:
<template>
<div id="app"> //待编写 </div>
</template>
<style lang="less">
</style>
②components文件夹、 views文件夹 清空
③router/index.js文件中 剩余内容:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [ ]
const router = new VueRouter({
routes
})
export default router
3.静态资源的引入
图片img、第三方包lib、地图map、主题theme 放在public文件夹下
4.项目的基本配置
根目录下创建vue.config.js文件:
module.exports = {
devServer: {
port: 8999, //前端 端口号 访问网址(http://localhost:8999/#/具体页面路由)
open: true //自动打开浏览器
}
};
5.全局echarts对象的挂载
public/index.html文件中:
<script src="echarts.min.js文件的路径地址"></script>
src/main.js文件中:
// 将全局的echarts对象挂载到Vue的原型对象上
// 别的组件中 使用this.$echarts
Vue.prototype.$echarts = window.echarts;
6.axios的封装与挂载
下载axios模块 :npm install axios
src/main.js文件中:
import axios from "axios"; //引入axios // 请求基准路径的配置 接口前缀 axios.defaults.baseURL = "http://127.0.0.1:8888/api/"; // 将axios挂载到Vue的原型对象上 在别的组件中 使用this.$http发起ajax请求 Vue.prototype.$http = axios;
二、单独图表组件的开发
1.横向柱状图 Seller.vue
①组件结构和布局结构的设计
router/index.js文件中:(定义路由)
import SellerPage from '@/views/SellerPage'
const routes = [{ path: '/sellerpage', component: SellerPage }] //路由规则
App.vue文件中:
<template>
<div id="app">
<router-view></router-view> //路由的占位符
</div>
</template>
<style lang="less">
</style>
父- views/SellerPage.vue文件中:(创建SellerPage.vue文件)
<!--针对于 /sellerpage 这条路径而显示出来的 在这个组件中, 通过子组件注册的方式, 要显示出Seller.vue这个组件 -->
<template>
<div class="com-page">
<Seller></Seller> ③ //显示子组件
</div>
</template>
<script>
import Seller from '@/components/Seller' ① //引入子组件Seller
export default {
data () {
return {}
},
methods: {},
components: {
Seller: Seller ② //注册子组件
}
}
</script>
<style lang="less" scoped>
</style>
子- components/Seller.vue文件中:(创建Seller.vue文件,功能主要写在这个文件里面)
<!-- 商家销量统计的横向柱状图 -->
<template>
<div class="com-container">
<div class="com-chart" > 你好 </div>
</div>
</template>
<script></script>
<style lang="less" scoped></style>
assets/css/global.less文件中: 全局样式
html, body, #app {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
.com-page {
width: 100%;
height: 100%;
overflow: hidden;
}
.com-container {
width: 100%;
height: 100%;
overflow: hidden;
}
.com-chart {
width: 100%;
height: 900px; //注意
overflow: hidden;
}
canvas {
border-radius: 20px; // 全局样式 圆角
}
.com-container {
position: relative;
}
main.js文件中:
// 引入全局的样式文件
import "./assets/css/global.less";
public/index.html文件中: 声明主题
<!-- 引入主题的js文件 -->
<script src="static/theme/chalk.js"></script>
<script src="static/theme/vintage.js"></script>
components/Seller.vue文件中:总的代码
<template>
<div class="com-container">
<div class="com-chart" ref="seller_ref"></div> //ref更好的获取dom
</div>
</template>
<script>
export default {
data () {
return {
chartInstance: null,//图表的数据
allData: null, // 服务器返回的数据
currentPage: 1, // 当前显示的页数
totalPage: 0, // 一共有多少页
timerId: null // 定时器的标识
}
},
mounted () {
this.initChart() //初始化图表
this.getData() //获取后端的数据
window.addEventListener('resize', this.screenAdapter)
// 在页面加载完成的时候, 主动进行屏幕的适配
this.screenAdapter()
},
destroyed () {
// 在组件销毁的时候, 需要将监听器取消掉
clearInterval(this.timerId)
// 在组件销毁的时候, 需要将监听器取消掉
window.removeEventListener('resize', this.screenAdapter)
},
methods: {
initChart () {
this.chartInstance = this.$echarts.init(this.$refs.seller_ref, 'chalk') //chalk使用主题
// start
// 对图表初始化配置的控制
const initOption = {
title: { //设置标题
text: '▎商家销售统计',
left: 20, //标题位置
top: 20 //标题位置
},
grid: { //网格 设置坐标轴位置
top: '20%',
left: '3%',
right: '6%',
bottom: '3%',
containLabel: true // 距离是包含坐标轴上的文字
},
xAxis: {
type: 'value' //数值轴
},
yAxis: {
type: 'category' //类目轴
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
z: 0,
lineStyle: {
color: '#2D3443'
}
}
},
series: [ //数值轴对应的数据
{
type: 'bar', //代表是柱状图
label: { //柱状图上的文字
show: true,
position: 'right',
textStyle: {
color: 'white'
}
},
itemStyle: { //样式
// 指明颜色渐变的方向
// 指明不同百分比之下颜色的值
color: new this.$echarts.graphic.LinearGradient(0, 0, 1, 0, [
// 百分之0状态之下的颜色值
{
offset: 0,
color: '#5052EE'
},
// 百分之100状态之下的颜色值
{
offset: 1,
color: '#AB6EE5'
}
])
}
}
]
}
this.chartInstance.setOption(initOption)
// end
// 对图表对象进行鼠标事件的监听
this.chartInstance.on('mouseover', () => {
clearInterval(this.timerId)
})
this.chartInstance.on('mouseout', () => {
this.startInterval()
})
},
async getData () { //获取后端的数据
// http://127.0.0.1:8888/api/seller
const { data: ret } = await this.$http.get('seller')
console.log(ret)
this.allData = ret
// 从小到大的排序
this.allData.sort((a, b) => {
return a.value - b.value // 从小到大的排序
})
// 每5个元素显示一页
this.totalPage =
this.allData.length % 5 === 0
? this.allData.length / 5
: this.allData.length / 5 + 1
this.updateChart()
this.startInterval()
},
updateChart () {
const start = (this.currentPage - 1) * 5
const end = this.currentPage * 5
const showData = this.allData.slice(start, end)
const sellerNames = showData.map(item => {
return item.name
})
const sellerValues = showData.map(item => {
return item.value
})
// 获取数据之后的配置option
const dataOption = {
yAxis: {
data: sellerNames
},
series: [
{
data: sellerValues
}
]
}
this.chartInstance.setOption(dataOption)
},
// 开启定时器
startInterval () {
if (this.timerId) {
clearInterval(this.timerId)
}
this.timerId = setInterval(() => {
this.currentPage++
if (this.currentPage > this.totalPage) {
this.currentPage = 1
}
this.updateChart()
}, 3000)
},
// 当浏览器的大小发生变化的时候, 会调用的方法, 来完成屏幕的适配
screenAdapter () {
// console.log(this.$refs.seller_ref.offsetWidth) 屏幕大小
const titleFontSize = (this.$refs.seller_ref.offsetWidth / 100) * 3.6
// 和分辨率大小相关的配置项
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
tooltip: {
axisPointer: {
lineStyle: {
width: titleFontSize
}
}
},
series: [
{
barWidth: titleFontSize,
itemStyle: {
barBorderRadius: [0, titleFontSize / 2, titleFontSize / 2, 0]
}
}
]
}
this.chartInstance.setOption(adapterOption)
// 手动的调用图表对象的resize 才能产生效果
this.chartInstance.resize()
}
}
}
</script>
<style>
</style>
2.折线图:Trend.vue
main.js文件中:
import './assets/font/iconfont.css' //引入字体icon的样式
router/index.js文件中:
import MapPage from '@/views/MapPage'
const routes = [
{ path: '/sellerpage', component: SellerPage },
{ path: '/trendpage', component: TrendPage },
]
views/TrendPage.vue文件中:
<template>
<div class="com-page">
<Trend></Trend>
</div>
</template>
<script>
import Trend from '@/components/Trend'
export default {
data () {
return {}
},
methods: {},
components: {
Trend: Trend
}
}
</script>
<style lang="less" scoped></style>
componens/Trend.vue文件中:总的代码
<template>
<div class="com-container">
<div class="title"
:style="comStyle">
<span>{{ '▎ ' + showTitle }}</span>
<span class="iconfont title-icon" //使用字体icon的样式
:style="comStyle"
@click="showChoice = !showChoice"></span>
<div class="select-con"
v-show="showChoice"
:style="marginStyle">
<div class="select-item"
v-for="item in selectTypes"
:key="item.key"
@click="handleSelect(item.key)">
{{ item.text }}
</div>
</div>
</div>
<div class="com-chart"
ref="trend_ref"></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
// import { getThemeValue } from '@/utils/theme_utils'
export default {
data () {
return {
chartInstane: null,
allData: null, // 从服务器中获取的所有数据
showChoice: false, // 是否显示可选项
choiceType: 'map', // 显示的数据类型
titleFontSize: 0 // 指明标题的字体大小
}
},
// created () {
// // 在组件创建完成之后 进行回调函数的注册
// this.$socket.registerCallBack('trendData', this.getData)
// },
mounted () {
this.initChart()
this.getData()
// 发送数据给服务器, 告诉服务器, 我现在需要数据
// this.$socket.send({
// action: 'getData',
// socketType: 'trendData',
// chartName: 'trend',
// value: ''
// })
window.addEventListener('resize', this.screenAdapter)
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
// 在组件销毁的时候, 进行回调函数的取消
// this.$socket.unRegisterCallBack('trendData')
},
computed: {
selectTypes () {
if (!this.allData) {
return []
} else {
return this.allData.type.filter(item => {
return item.key !== this.choiceType
})
}
},
showTitle () {
if (!this.allData) {
return ''
} else {
return this.allData[this.choiceType].title
}
},
// 设置给标题的样式
comStyle () {
return {
fontSize: this.titleFontSize + 'px'
// color: getThemeValue(this.theme).titleColor
}
},
marginStyle () {
return {
marginLeft: this.titleFontSize + 'px'
}
},
...mapState(['theme'])
},
methods: {
initChart () {
this.chartInstane = this.$echarts.init(this.$refs.trend_ref, 'chalk')
const initOption = {
grid: {
left: '3%',
top: '35%',
right: '4%',
bottom: '1%',
containLabel: true
},
tooltip: {
trigger: 'axis'
},
legend: {
left: 20,
top: '15%',
icon: 'circle'
},
xAxis: {
type: 'category',
boundaryGap: false
},
yAxis: {
type: 'value'
}
}
this.chartInstane.setOption(initOption)
},
// ret 就是服务端发送给客户端的图表的数据
async getData () {
// await this.$http.get()
// 对allData进行赋值
const { data: ret } = await this.$http.get('trend')
this.allData = ret
console.log(this.allData)
this.updateChart()
},
updateChart () {
// 半透明的颜色值
const colorArr1 = [
'rgba(11, 168, 44, 0.5)',
'rgba(44, 110, 255, 0.5)',
'rgba(22, 242, 217, 0.5)',
'rgba(254, 33, 30, 0.5)',
'rgba(250, 105, 0, 0.5)'
]
// 全透明的颜色值
const colorArr2 = [
'rgba(11, 168, 44, 0)',
'rgba(44, 110, 255, 0)',
'rgba(22, 242, 217, 0)',
'rgba(254, 33, 30, 0)',
'rgba(250, 105, 0, 0)'
]
// 处理数据
// 类目轴的数据
const timeArr = this.allData.common.month
// y轴的数据 series下的数据
const valueArr = this.allData[this.choiceType].data
const seriesArr = valueArr.map((item, index) => {
return {
name: item.name,
type: 'line',
data: item.data,
stack: this.choiceType,
areaStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colorArr1[index]
}, // %0的颜色值
{
offset: 1,
color: colorArr2[index]
} // 100%的颜色值
])
}
}
})
// 图例的数据
const legendArr = valueArr.map(item => {
return item.name
})
const dataOption = {
xAxis: {
data: timeArr
},
legend: {
data: legendArr
},
series: seriesArr
}
this.chartInstane.setOption(dataOption)
},
screenAdapter () {
this.titleFontSize = this.$refs.trend_ref.offsetWidth / 100 * 3.6
const adapterOption = {
legend: {
itemWidth: this.titleFontSize,
itemHeight: this.titleFontSize,
itemGap: this.titleFontSize,
textStyle: {
fontSize: this.titleFontSize / 2
}
}
}
this.chartInstane.setOption(adapterOption)
this.chartInstane.resize()
},
handleSelect (currentType) {
this.choiceType = currentType
this.updateChart()
this.showChoice = false
}
}
// watch: {
// theme () {
// console.log('主题切换了')
// this.chartInstane.dispose() // 销毁当前的图表
// this.initChart() // 重新以最新的主题名称初始化图表对象
// this.screenAdapter() // 完成屏幕的适配
// this.updateChart() // 更新图表的展示
// }
// }
}
</script>
<style lang="less" scoped>
.title {
position: absolute;
left: 20px;
top: 20px;
z-index: 10;
color: white;
.title-icon {
margin-left: 10px;
cursor: pointer;
}
.select-con {
background-color: #222733;
}
}
</style>
3.地图+散点图
router/index.js文件中:
import MapPage from '@/views/MapPage'
const routes = [
{ path: '/sellerpage', component: SellerPage },
{ path: '/trendpage', component: TrendPage },
{ path: '/mappage', component: MapPage }
]
views/MapPage.vue文件中:
<!-- 针对于 /mappage 这条路径而显示出来的
在这个组件中, 通过子组件注册的方式, 要显示出Map.vue这个组件 -->
<template>
<div class="com-page">
<Map></Map>
</div>
</template>
<script>
import Map from '@/components/Map'
export default {
data () {
return {}
},
methods: {},
components: {
Map: Map
}
}
</script>
<style lang="less" scoped>
</style>
componens/Map.vue文件中:总的代码
<!-- 商家分布图表 地图+散点图-->
<template>
<div class='com-container' @dblclick="revertMap">
<div class='com-chart' ref='map_ref'></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import axios from 'axios'
import { getProvinceMapInfo } from '@/utils/map_utils'
export default {
data () {
return {
chartInstance: null,
allData: null,
mapData: {} // 缓存 所获取的省份的地图矢量数据
}
},
// created () {
// // 在组件创建完成之后 进行回调函数的注册
// this.$socket.registerCallBack('mapData', this.getData)
// },
mounted () {
this.initChart()
this.getData()
// this.$socket.send({
// action: 'getData',
// socketType: 'mapData',
// chartName: 'map',
// value: ''
// })
window.addEventListener('resize', this.screenAdapter)
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
// this.$socket.unRegisterCallBack('mapData')
},
methods: {
async initChart () {
this.chartInstance = this.$echarts.init(this.$refs.map_ref, 'chalk')
// this.chartInstance = this.$echarts.init(this.$refs.map_ref, this.theme)
// 获取中国地图的矢量数据
// http://localhost:8999/static/map/china.json
// 由于我们现在获取的地图矢量数据并不是位于KOA2的后台, 所以咱们不能使用this.$http
const ret = await axios.get('http://localhost:8999/static/map/china.json')
this.$echarts.registerMap('china', ret.data)
const initOption = {
title: {
text: '▎ 商家分布',
left: 20,
top: 20
},
geo: {
type: 'map',
map: 'china',
top: '5%',
bottom: '5%',
itemStyle: {
areaColor: '#2E72BF',
borderColor: '#333'
}
},
legend: {
left: '5%',
bottom: '5%',
orient: 'vertical'
}
}
this.chartInstance.setOption(initOption)
// 点击地图 下钻
this.chartInstance.on('click', async arg => {
console.log(arg)
// arg.name 得到所点击的省份, 这个省份他是中文
const provinceInfo = getProvinceMapInfo(arg.name)
console.log(provinceInfo)
// 需要获取这个省份的地图矢量数据
// 判断当前所点击的这个省份的地图矢量数据在mapData中是否存在
if (!this.mapData[provinceInfo.key]) {
const ret = await axios.get('http://localhost:8999' + provinceInfo.path)
this.mapData[provinceInfo.key] = ret.data
this.$echarts.registerMap(provinceInfo.key, ret.data)
}
const changeOption = {
geo: {
map: provinceInfo.key
}
}
this.chartInstance.setOption(changeOption)
})
},
async getData () {
// 获取服务器的数据, 对this.allData进行赋值之后, 调用updateChart方法更新图表
const { data: ret } = await this.$http.get('map')
this.allData = ret
// console.log(this.allData)
this.updateChart()
},
updateChart () {
// 处理图表需要的数据
// 图例的数据
const legendArr = this.allData.map(item => {
// console.log('item.name', item.name)
return item.name
})
const seriesArr = this.allData.map(item => {
// return的这个对象就代表的是一个类别下的所有散点数据
// 如果想在地图中显示散点的数据, 我们需要给散点的图表增加一个配置, coordinateSystem:geo
return {
type: 'effectScatter',
rippleEffect: {
scale: 5,
brushType: 'stroke'
},
name: item.name,
data: item.children,
coordinateSystem: 'geo'
}
})
const dataOption = {
legend: {
data: legendArr
},
series: seriesArr
}
this.chartInstance.setOption(dataOption)
},
screenAdapter () {
// console.log('this.$refs.map_ref.offsetWidth', this.$refs.map_ref.offsetWidth)
const titleFontSize = this.$refs.map_ref.offsetWidth / 100 * 3.6
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
legend: {
itemWidth: titleFontSize / 2,
itemHeight: titleFontSize / 2,
itemGap: titleFontSize / 2,
textStyle: {
fontSize: titleFontSize / 2
}
}
}
this.chartInstance.setOption(adapterOption)
this.chartInstance.resize()
},
// 回到中国地图
revertMap () {
const revertOption = {
geo: {
map: 'china'
}
}
this.chartInstance.setOption(revertOption)
}
},
computed: {
...mapState(['theme'])
},
watch: {
theme () {
console.log('主题切换了')
this.chartInstance.dispose() // 销毁当前的图表
this.initChart() // 重新以最新的主题名称初始化图表对象
this.screenAdapter() // 完成屏幕的适配
this.updateChart() // 更新图表的展示
}
}
}
</script>
<style lang='less' scoped>
</style>
4.柱状图
router/index.js文件中:
import Vue from 'vue'
import VueRouter from 'vue-router'
import TrendPage from '@/views/TrendPage'
import SellerPage from '@/views/SellerPage'
import MapPage from '@/views/MapPage'
import RankPage from '@/views/RankPage'
Vue.use(VueRouter)
const routes = [
{ path: '/sellerpage', component: SellerPage },
{ path: '/trendpage', component: TrendPage },
{ path: '/mappage', component: MapPage },
{ path: '/rankpage', component: RankPage }
]
const router = new VueRouter({
routes
})
export default router
views/RankPage.vue文件中:
<!--
针对于 /rankpage 这条路径而显示出来的
在这个组件中, 通过子组件注册的方式, 要显示出Rank.vue这个组件
-->
<template>
<div class="com-page">
<Rank></Rank>
</div>
</template>
<script>
import Rank from '@/components/Rank'
export default {
data () {
return {}
},
methods: {},
components: {
Rank: Rank
}
}
</script>
<style lang="less" scoped>
</style>
componens/Rank.vue文件中:总的代码
<!-- 地区销售排行 柱状图-->
<template>
<div class='com-container'>
<div class='com-chart'
ref='rank_ref'></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data () {
return {
chartInstance: null,
allData: null,
startValue: 0, // 区域缩放的起点值
endValue: 9, // 区域缩放的终点值
timerId: null // 定时器的标识
}
},
// created () {
// // 在组件创建完成之后 进行回调函数的注册
// this.$socket.registerCallBack('rankData', this.getData)
// },
mounted () {
this.initChart()
this.getData()
// this.$socket.send({
// action: 'getData',
// socketType: 'rankData',
// chartName: 'rank',
// value: ''
// })
window.addEventListener('resize', this.screenAdapter)
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
clearInterval(this.timerId) // 销毁定时器
// this.$socket.unRegisterCallBack('rankData')
},
methods: {
initChart () {
// this.chartInstance = this.$echarts.init(this.$refs.rank_ref, this.theme)
this.chartInstance = this.$echarts.init(this.$refs.rank_ref, 'chalk')
const initOption = {
title: { // 标题
text: '▎ 地区销售排行',
left: 20,
top: 20
},
grid: { // 坐标轴位置
top: '40%',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true
},
tooltip: { // 工具提示
show: true
},
xAxis: {
type: 'category'
},
yAxis: {
type: 'value'
},
series: [
{
type: 'bar'
}
]
}
this.chartInstance.setOption(initOption)
// 鼠标移入 动画效果停止
this.chartInstance.on('mouseover', () => {
clearInterval(this.timerId)
})
// 鼠标移出 动画效果开始
this.chartInstance.on('mouseout', () => {
this.startInterval()
})
},
async getData () {
// 获取服务器的数据, 对this.allData进行赋值之后, 调用updateChart方法更新图表
const { data: ret } = await this.$http.get('rank')
this.allData = ret
// 对allData里面的每一个元素进行排序, 从大到小进行
this.allData.sort((a, b) => {
return b.value - a.value
})
console.log(this.allData)
this.updateChart()
this.startInterval() // 启动定时器 -开启动画向左移动
},
updateChart () {
const colorArr = [
['#0BA82C', '#4FF778'],
['#2E72BF', '#23E5E5'],
['#5052EE', '#AB6EE5']
]
// 处理图表需要的数据
// 所有省份所形成的数组 x轴数据
const provinceArr = this.allData.map(item => {
return item.name
})
// 所有省份对应的销售金额 y轴数据
const valueArr = this.allData.map(item => {
return item.value
})
const dataOption = {
xAxis: {
data: provinceArr
},
dataZoom: { // 区域缩放 - 使图每隔一段时间向左平移一个
show: false, // 不显示区域缩放组件
startValue: this.startValue, // 区域缩放的起点值
endValue: this.endValue // 区域缩放的终点值
},
series: [
{
data: valueArr,
itemStyle: { // 不同的数据对应不同的颜色 -柱状图的颜色
color: arg => {
let targetColorArr = null
if (arg.value > 300) {
targetColorArr = colorArr[0]
} else if (arg.value > 200) {
targetColorArr = colorArr[1]
} else {
targetColorArr = colorArr[2]
}
return new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [ // 颜色渐变
{
offset: 0,
color: targetColorArr[0]
},
{
offset: 1,
color: targetColorArr[1]
}
])
}
}
}
]
}
this.chartInstance.setOption(dataOption)
},
screenAdapter () { // 标题的文字大小、柱的宽度、柱的圆角
const titleFontSize = this.$refs.rank_ref.offsetWidth / 100 * 3.6
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
series: [
{
barWidth: titleFontSize,
itemStyle: {
barBorderRadius: [titleFontSize / 2, titleFontSize / 2, 0, 0]
}
}
]
}
this.chartInstance.setOption(adapterOption)
this.chartInstance.resize()
},
startInterval () { // 设置定时器 -控制动画的向左移动
if (this.timerId) {
clearInterval(this.timerId)
}
this.timerId = setInterval(() => {
this.startValue++
this.endValue++
if (this.endValue > this.allData.length - 1) {
this.startValue = 0
this.endValue = 9
}
this.updateChart()
}, 2000)
}
},
computed: {
...mapState(['theme'])
},
watch: {
theme () {
console.log('主题切换了')
this.chartInstance.dispose() // 销毁当前的图表
this.initChart() // 重新以最新的主题名称初始化图表对象
this.screenAdapter() // 完成屏幕的适配
this.updateChart() // 更新图表的展示
}
}
}
</script>
<style lang='less' scoped>
</style>
5.饼图
router/index.js文件中:
import Vue from 'vue'
import VueRouter from 'vue-router'
import TrendPage from '@/views/TrendPage'
import SellerPage from '@/views/SellerPage'
import MapPage from '@/views/MapPage'
import RankPage from '@/views/RankPage'
import HotPage from '@/views/HotPage'
Vue.use(VueRouter)
const routes = [
{ path: '/sellerpage', component: SellerPage },
{ path: '/trendpage', component: TrendPage },
{ path: '/mappage', component: MapPage },
{ path: '/rankpage', component: RankPage },
{ path: '/hotpage', component: HotPage }
]
const router = new VueRouter({
routes
})
export default router
views/HotPage.vue文件中:
<!--
针对于 /hotpage 这条路径而显示出来的
在这个组件中, 通过子组件注册的方式, 要显示出Hot.vue这个组件
-->
<template>
<div class="com-page">
<Hot></Hot>
</div>
</template>
<script>
import Hot from '@/components/Hot'
export default {
data () {
return {}
},
methods: {},
components: {
Hot: Hot
}
}
</script>
<style lang="less" scoped>
</style>
componens/Hot.vue文件中:总的代码
<!-- 热销商品图表 -->
<template>
<div class='com-container'>
<div class='com-chart'
ref='hot_ref'></div>
<span class="iconfont arr-left"
@click="toLeft"
:style="comStyle"></span>
<span class="iconfont arr-right"
@click="toRight"
:style="comStyle"></span>
<span class="cat-name"
:style="comStyle">{{ catName }}</span>
</div>
</template>
<script>
import { mapState } from 'vuex'
// import { getThemeValue } from '@/utils/theme_utils'
export default {
data () {
return {
chartInstance: null,
allData: null,
currentIndex: 0, // 当前所展示出的一级分类数据
titleFontSize: 0
}
},
// created () {
// // 在组件创建完成之后 进行回调函数的注册
// this.$socket.registerCallBack('hotData', this.getData)
// },
computed: {
catName () {
if (!this.allData) { // 一级标题的显示
return ''
} else {
return this.allData[this.currentIndex].name
}
},
comStyle () { //
return {
fontSize: this.titleFontSize + 'px'
// color: getThemeValue(this.theme).titleColor
}
},
...mapState(['theme'])
},
mounted () {
this.initChart()
this.getData()
// this.$socket.send({
// action: 'getData',
// socketType: 'hotData',
// chartName: 'hot',
// value: ''
// })
window.addEventListener('resize', this.screenAdapter)
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
// this.$socket.unRegisterCallBack('hotData')
},
methods: {
initChart () {
// this.chartInstance = this.$echarts.init(this.$refs.hot_ref, this.theme)
this.chartInstance = this.$echarts.init(this.$refs.hot_ref, 'chalk')
const initOption = {
title: {
text: '▎ 热销商品的占比',
left: 20,
top: 20
},
legend: { // 图例图标的位置
top: '15%',
icon: 'circle'
},
tooltip: { // 工具提示 - 提示三级占比
show: true,
formatter: arg => {
// console.log(arg)
const thirdCategory = arg.data.children
// 计算出所有三级分类的数值总和
let total = 0
thirdCategory.forEach(item => {
total += item.value
})
let retStr = ''
thirdCategory.forEach(item => {
retStr += `
${item.name}:${parseInt(item.value / total * 100) + '%'}
<br/>
`
})
return retStr
}
},
series: [
{
type: 'pie',
label: { // 饼图的图例
show: false
},
emphasis: { // 高亮显示 - 饼图的图例
label: {
show: true
},
labelLine: { // 饼图的图例线条
show: false
}
}
}
]
}
this.chartInstance.setOption(initOption)
},
async getData () {
// 获取服务器的数据, 对this.allData进行赋值之后, 调用updateChart方法更新图表
const { data: ret } = await this.$http.get('hotproduct')
this.allData = ret
console.log(this.allData)
this.updateChart()
},
updateChart () {
// 处理图表需要的数据
const legendData = this.allData[this.currentIndex].children.map(item => {
return item.name
})
const seriesData = this.allData[this.currentIndex].children.map(item => {
return {
name: item.name,
value: item.value,
children: item.children // 新增加children的原因是为了在tooltip中的formatter的回调函数中,来拿到这个二级分类下的三级分类数据
}
})
const dataOption = {
legend: {
data: legendData
},
series: [
{
data: seriesData
}
]
}
this.chartInstance.setOption(dataOption)
},
screenAdapter () {
this.titleFontSize = this.$refs.hot_ref.offsetWidth / 100 * 3.6
const adapterOption = {
title: { // 标题的文字大小
textStyle: {
fontSize: this.titleFontSize
}
},
legend: { // 图例的大小
itemWidth: this.titleFontSize,
itemHeight: this.titleFontSize,
itemGap: this.titleFontSize / 2,
textStyle: {
fontSize: this.titleFontSize / 2
}
},
series: [
{
radius: this.titleFontSize * 4.5, // 饼图的大小
center: ['50%', '60%']
}
]
}
this.chartInstance.setOption(adapterOption)
this.chartInstance.resize()
},
toLeft () {
this.currentIndex--
if (this.currentIndex < 0) {
this.currentIndex = this.allData.length - 1
}
this.updateChart()
},
toRight () {
this.currentIndex++
if (this.currentIndex > this.allData.length - 1) {
this.currentIndex = 0
}
this.updateChart()
}
},
watch: {
theme () {
console.log('主题切换了')
this.chartInstance.dispose() // 销毁当前的图表
this.initChart() // 重新以最新的主题名称初始化图表对象
this.screenAdapter() // 完成屏幕的适配
this.updateChart() // 更新图表的展示
}
}
}
</script>
<style lang='less' scoped>
.arr-left {
position: absolute;
left: 10%;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: white;
}
.arr-right {
position: absolute;
right: 10%;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: white;
}
.cat-name {
position: absolute;
left: 80%;
bottom: 20px;
color: white;
}
</style>
6.循环饼图
router/index.js文件中:
import Vue from 'vue'
import VueRouter from 'vue-router'
import TrendPage from '@/views/TrendPage'
import SellerPage from '@/views/SellerPage'
import MapPage from '@/views/MapPage'
import RankPage from '@/views/RankPage'
import HotPage from '@/views/HotPage'
import StockPage from '@/views/StockPage'
Vue.use(VueRouter)
const routes = [
{ path: '/sellerpage', component: SellerPage },
{ path: '/trendpage', component: TrendPage },
{ path: '/mappage', component: MapPage },
{ path: '/rankpage', component: RankPage },
{ path: '/hotpage', component: HotPage },
{ path: '/stockpage', component: StockPage }
]
const router = new VueRouter({
routes
})
export default router
views/StockPage.vue文件中:
<!--
针对于 /stockpage 这条路径而显示出来的
在这个组件中, 通过子组件注册的方式, 要显示出Stock.vue这个组件
-->
<template>
<div class="com-page">
<Stock></Stock>
</div>
</template>
<script>
import Stock from '@/components/Stock'
export default {
data () {
return {}
},
methods: {},
components: {
Stock: Stock
}
}
</script>
<style lang="less" scoped>
</style>
componens/Stock.vue文件中:总的代码
<!-- 库存销量分析 -->
<template>
<div class='com-container'>
<div class='com-chart'
ref='stock_ref'></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data () {
return {
chartInstance: null,
allData: null,
currentIndex: 0, // 当前显示的数据
timerId: null // 定时器的标识
}
},
created () {
// 在组件创建完成之后 进行回调函数的注册
this.$socket.registerCallBack('stockData', this.getData)
},
mounted () {
this.initChart()
// this.getData()
this.$socket.send({
action: 'getData',
socketType: 'stockData',
chartName: 'stock',
value: ''
})
window.addEventListener('resize', this.screenAdapter)
this.screenAdapter()
},
destroyed () {
window.removeEventListener('resize', this.screenAdapter)
clearInterval(this.timerId)
this.$socket.unRegisterCallBack('stockData')
},
methods: {
initChart () {
this.chartInstance = this.$echarts.init(this.$refs.stock_ref, this.theme)
const initOption = {
title: {
text: '▎库存和销量分析',
left: 20,
top: 20
}
}
this.chartInstance.setOption(initOption)
this.chartInstance.on('mouseover', () => {
clearInterval(this.timerId)
})
this.chartInstance.on('mouseout', () => {
this.startInterval()
})
},
getData (ret) {
// 获取服务器的数据, 对this.allData进行赋值之后, 调用updateChart方法更新图表
// const { data: ret } = await this.$http.get('stock')
this.allData = ret
console.log(this.allData)
this.updateChart()
this.startInterval()
},
updateChart () {
const centerArr = [
['18%', '40%'],
['50%', '40%'],
['82%', '40%'],
['34%', '75%'],
['66%', '75%']
]
const colorArr = [
['#4FF778', '#0BA82C'],
['#E5DD45', '#E8B11C'],
['#E8821C', '#E55445'],
['#5052EE', '#AB6EE5'],
['#23E5E5', '#2E72BF']
]
// 处理图表需要的数据
const start = this.currentIndex * 5
const end = (this.currentIndex + 1) * 5
const showData = this.allData.slice(start, end)
const seriesArr = showData.map((item, index) => {
return {
type: 'pie',
center: centerArr[index],
hoverAnimation: false, // 关闭鼠标移入到饼图时的动画效果
labelLine: {
show: false // 隐藏指示线
},
label: {
position: 'center',
color: colorArr[index][0]
},
data: [
{
name: item.name + '\n\n' + item.sales,
value: item.sales,
itemStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: colorArr[index][0]
},
{
offset: 1,
color: colorArr[index][1]
}
])
}
},
{
value: item.stock,
itemStyle: {
color: '#333843'
}
}
]
}
})
const dataOption = {
series: seriesArr
}
this.chartInstance.setOption(dataOption)
},
screenAdapter () {
const titleFontSize = this.$refs.stock_ref.offsetWidth / 100 * 3.6
const innerRadius = titleFontSize * 2.8
const outterRadius = innerRadius * 1.125
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
series: [
{
type: 'pie',
radius: [outterRadius, innerRadius],
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',
radius: [outterRadius, innerRadius],
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',
radius: [outterRadius, innerRadius],
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',
radius: [outterRadius, innerRadius],
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',
radius: [outterRadius, innerRadius],
label: {
fontSize: titleFontSize / 2
}
}
]
}
this.chartInstance.setOption(adapterOption)
this.chartInstance.resize()
},
startInterval () {
if (this.timerId) {
clearInterval(this.timerId)
}
this.timerId = setInterval(() => {
this.currentIndex++
if (this.currentIndex > 1) {
this.currentIndex = 0
}
this.updateChart() // 在更改完currentIndex之后 , 需要更新界面
}, 5000)
}
},
computed: {
...mapState(['theme'])
},
watch: {
theme () {
console.log('主题切换了')
this.chartInstance.dispose() // 销毁当前的图表
this.initChart() // 重新以最新的主题名称初始化图表对象
this.screenAdapter() // 完成屏幕的适配
this.updateChart() // 更新图表的展示
}
}
}
</script>
<style lang='less' scoped>
</style>
三、WebSocket的引入
1.后端 app.js文件中:
const webSocketService = require('./service/web_socket_service')
// 开启服务端的监听, 监听客户端的连接
// 当某一个客户端连接成功之后, 就会对这个客户端进行message事件的监听
webSocketService.listen()
// 读取文件的工具方法
const fs = require("fs");
module.exports.getFileJsonData = filePath => {
// return "你好";
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf-8", (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
};
src/service/web_socket_service.js文件中:
const path = require('path')
const fileUtils = require('../utils/file_utils')
//WebSocket的引入
//下载插件 npm i ws -S
const WebSocket = require('ws')
// 创建WebSocket服务端的对象, 绑定的端口号是9998
const wss = new WebSocket.Server({
port: 9998
})
// 服务端开启了监听
module.exports.listen = () => {
// 对客户端的连接事件进行connection事件的监听
// client:代表的是客户端的连接socket对象
wss.on('connection', client => {
console.log('有客户端连接成功了...')
// 对客户端的连接对象进行message事件的监听
// msg: 由客户端发给服务端的数据
client.on('message', async msg => {
console.log('客户端发送数据给服务端了: ' + msg)
let payload = JSON.parse(msg)
const action = payload.action
if (action === 'getData') {
let filePath = '../data/' + payload.chartName + '.json'
// payload.chartName // trend seller map rank hot stock
filePath = path.join(__dirname, filePath)
const ret = await fileUtils.getFileJsonData(filePath)
// 需要在服务端获取到数据的基础之上, 增加一个data的字段
// data所对应的值,就是某个json文件的内容
payload.data = ret
client.send(JSON.stringify(payload))
} else {
// 原封不动的将所接收到的数据转发给每一个处于连接状态的客户端
// wss.clients // 所有客户端的连接
wss.clients.forEach(client => {
client.send(msg)
})
}
// 由服务端往客户端发送数据
// client.send('hello socket from backend')
})
})
}
2.前端
export default class SocketService {
/**
* 单例
*/
static instance = null
static get Instance () {
if (!this.instance) {
this.instance = new SocketService()
}
return this.instance
}
// 和服务端连接的socket对象
ws = null
// 存储回调函数
callBackMapping = {}
// 标识是否连接成功
connected = false
// 记录重试的次数
sendRetryCount = 0
// 重新连接尝试的次数
connectRetryCount = 0
// 定义连接服务器的方法
connect () {
// 连接服务器
if (!window.WebSocket) {
return console.log('您的浏览器不支持WebSocket')
}
this.ws = new WebSocket('ws://localhost:9998')
// 连接成功的事件
this.ws.onopen = () => {
console.log('连接服务端成功了')
this.connected = true
// 重置重新连接的次数
this.connectRetryCount = 0
}
// 1.连接服务端失败
// 2.当连接成功之后, 服务器关闭的情况
this.ws.onclose = () => {
console.log('连接服务端失败')
this.connected = false
this.connectRetryCount++
setTimeout(() => {
this.connect()
}, 500 * this.connectRetryCount)
}
// 得到服务端发送过来的数据
this.ws.onmessage = msg => {
console.log('从服务端获取到了数据')
// 真正服务端发送过来的原始数据时在msg中的data字段
// console.log(msg.data)
const recvData = JSON.parse(msg.data)
const socketType = recvData.socketType
// 判断回调函数是否存在
if (this.callBackMapping[socketType]) {
const action = recvData.action
if (action === 'getData') {
const realData = JSON.parse(recvData.data)
this.callBackMapping[socketType].call(this, realData)
} else if (action === 'fullScreen') {
this.callBackMapping[socketType].call(this, recvData)
} else if (action === 'themeChange') {
this.callBackMapping[socketType].call(this, recvData)
}
}
}
}
// 回调函数的注册
registerCallBack (socketType, callBack) {
this.callBackMapping[socketType] = callBack
}
// 取消某一个回调函数
unRegisterCallBack (socketType) {
this.callBackMapping[socketType] = null
}
// 发送数据的方法
send (data) {
// 判断此时此刻有没有连接成功
if (this.connected) {
this.sendRetryCount = 0
this.ws.send(JSON.stringify(data))
} else {
this.sendRetryCount++
setTimeout(() => {
this.send(data)
}, this.sendRetryCount * 500)
}
}
}
src/main.js文件中:
import SocketService from '@/utils/socket_service'
// 对服务端进行websocket的连接
SocketService.Instance.connect()
// 其他的组件 this.$socket
Vue.prototype.$socket = SocketService.Instance
componens/Seller.vue文件中: 其他组件一样
created () {
// 在组件创建完成之后 进行回调函数的注册
this.$socket.registerCallBack('sellerData', this.getData)
},
mounted () {
// this.getData()
this.$socket.send({
action: 'getData',
socketType: 'sellerData',
chartName: 'seller',
value: ''
})
},
destroyed () {
this.$socket.unRegisterCallBack('sellerData')
},
methods:{
// 获取服务器的数据
getData (ret) {
// const { data: ret } = await this.$http.get('seller')
this.allData = ret
},
}
四、细节处置
1.组件合并
src/router/index.js文件中:
import Vue from 'vue'
import VueRouter from 'vue-router'
import ScreenPage from '@/views/ScreenPage'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/screen'
},
{
path: '/screen',
component: ScreenPage
}
]
const router = new VueRouter({
routes
})
export default router
src/views/ScreenPage.vue文件中:组件 -导入注册引用
<template>
<div class="screen-container"
:style="containerStyle">
<header class="screen-header">
<div>
<img :src="headerSrc" alt="">
</div>
<span class="title">电商平台实时监控系统</span>
<div class="title-right">
<img :src="themeSrc" class="qiehuan" @click="handleChangeTheme">
<span class="datetime">{{timeComput}}</span>
</div>
</header>
<div class="screen-body">
<section class="screen-left">
<div id="left-top" :class="[fullScreenStatus.trend ? 'fullscreen' : '']">
<!-- 销量趋势图表 -->
<Trend ref="trend"></Trend>
<div class="resize">
<span @click="changeSize('trend')"
:class="['iconfont', fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
<div id="left-bottom"
:class="[fullScreenStatus.seller ? 'fullscreen' : '']">
<!-- 商家销售金额图表 -->
<Seller ref="seller"></Seller>
<div class="resize">
<!-- icon-compress-alt -->
<span @click="changeSize('seller')"
:class="['iconfont', fullScreenStatus.seller ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
</section>
<section class="screen-middle">
<div id="middle-top"
:class="[fullScreenStatus.map ? 'fullscreen' : '']">
<!-- 商家分布图表 -->
<Map ref="map"></Map>
<div class="resize">
<!-- icon-compress-alt -->
<span @click="changeSize('map')"
:class="['iconfont', fullScreenStatus.map ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
<div id="middle-bottom"
:class="[fullScreenStatus.rank ? 'fullscreen' : '']">
<!-- 地区销量排行图表 -->
<Rank ref="rank"></Rank>
<div class="resize">
<!-- icon-compress-alt -->
<span @click="changeSize('rank')"
:class="['iconfont', fullScreenStatus.rank ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
</section>
<section class="screen-right">
<div id="right-top"
:class="[fullScreenStatus.hot ? 'fullscreen' : '']">
<!-- 热销商品占比图表 -->
<Hot ref="hot"></Hot>
<div class="resize">
<!-- icon-compress-alt -->
<span @click="changeSize('hot')"
:class="['iconfont', fullScreenStatus.hot ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
<div id="right-bottom"
:class="[fullScreenStatus.stock ? 'fullscreen' : '']">
<!-- 库存销量分析图表 -->
<Stock ref="stock"></Stock>
<div class="resize">
<!-- icon-compress-alt -->
<span @click="changeSize('stock')"
:class="['iconfont', fullScreenStatus.stock ? 'icon-compress-alt' : 'icon-expand-alt']"></span>
</div>
</div>
</section>
</div>
</div>
</template>
<script>
import Hot from '@/components/Hot.vue'
import Map from '@/components/Map.vue'
import Rank from '@/components/Rank.vue'
import Seller from '@/components/Seller.vue'
import Stock from '@/components/Stock.vue'
import Trend from '@/components/Trend.vue'
import { mapState } from 'vuex'
import { getThemeValue } from '@/utils/theme_utils'
export default {
created () {
// 注册接收到数据的回调函数
this.$socket.registerCallBack('fullScreen', this.recvData)
this.$socket.registerCallBack('themeChange', this.recvThemeChange)
},
mounted () {
this.startInterval()
},
destroyed () {
this.$socket.unRegisterCallBack('fullScreen')
this.$socket.unRegisterCallBack('themeChange')
},
data () {
return {
// 定义每一个图表的全屏状态
fullScreenStatus: {
trend: false,
seller: false,
map: false,
rank: false,
hot: false,
stock: false
},
timeComput: '',
timer: ''// 定义一个定时器的变量
}
},
methods: {
// 开启定时器
startInterval () {
if (this.timer) { // 开启定时器之前先取消定时器,节流防抖
clearInterval(this.timer)
}
this.timer = setInterval(() => {
this.CurentTime()
}, 1000)
},
CurentTime () {
const now = new Date()
const year = now.getFullYear() // 年
const month = now.getMonth() + 1 // 月
const day = now.getDate() // 日
const hh = now.getHours() // 时
const mm = now.getMinutes() // 分
const ss = now.getSeconds() // 秒
let clock = year + '-'
if (month < 10) { clock += '0' }
clock += month + '-'
if (day < 10) { clock += '0' }
clock += day + ' '
if (hh < 10) { clock += '0' }
clock += hh + ':'
if (mm < 10) clock += '0'
clock += mm + ':'
if (ss < 10) clock += '0'
clock += ss
this.timeComput = clock
},
changeSize (chartName) {
// 1.改变fullScreenStatus的数据
this.fullScreenStatus[chartName] = !this.fullScreenStatus[chartName]
// 2.需要调用每一个图表组件的screenAdapter的方法
this.$refs[chartName].screenAdapter()
this.$nextTick(() => {
this.$refs[chartName].screenAdapter()
})
// // 全屏联动 -- 将数据发送给服务端
// const targetValue = !this.fullScreenStatus[chartName]
// this.$socket.send({
// action: 'fullScreen',
// socketType: 'fullScreen',
// chartName: chartName,
// value: targetValue
// })
},
// 接收到全屏数据之后的处理
recvData (data) {
// 取出是哪一个图表需要进行切换
const chartName = data.chartName
// 取出, 切换成什么状态
const targetValue = data.value
this.fullScreenStatus[chartName] = targetValue
this.$nextTick(() => {
this.$refs[chartName].screenAdapter()
})
},
handleChangeTheme () {
// 修改VueX中数据
this.$store.commit('changeTheme')
// this.$socket.send({
// action: 'themeChange',
// socketType: 'themeChange',
// chartName: '',
// value: ''
// })
},
recvThemeChange () {
this.$store.commit('changeTheme')
}
},
components: {
Hot,
Map,
Rank,
Seller,
Stock,
Trend
},
computed: {
logoSrc () {
return '/static/img/' + getThemeValue(this.theme).logoSrc
},
headerSrc () {
return '/static/img/' + getThemeValue(this.theme).headerBorderSrc
},
themeSrc () {
return '/static/img/' + getThemeValue(this.theme).themeSrc
},
containerStyle () {
return {
backgroundColor: getThemeValue(this.theme).backgroundColor,
color: getThemeValue(this.theme).titleColor
}
},
...mapState(['theme'])
}
}
</script>
<style lang="less" scoped>
// 全屏样式的定义
.fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
margin: 0 !important;
z-index: 100;
}
.screen-container {
width: 100%;
height: 100%;
padding: 0 20px;
background-color: #161522;
color: #fff;
box-sizing: border-box;
}
.screen-header {
width: 100%;
height: 64px;
font-size: 20px;
position: relative;
> div {
img {
width: 100%;
}
}
.title {
position: absolute;
left: 50%;
top: 50%;
font-size: 20px;
transform: translate(-50%, -50%);
}
.title-right {
display: flex;
align-items: center;
position: absolute;
right: 0px;
top: 50%;
transform: translateY(-80%);
}
.qiehuan {
width: 28px;
height: 21px;
cursor: pointer;
}
.datetime {
font-size: 15px;
margin-left: 10px;
}
.logo {
position: absolute;
left: 0px;
top: 50%;
transform: translateY(-80%);
img {
height: 35px;
width: 128px;
}
}
}
.screen-body {
width: 100%;
height: 100%;
display: flex;
margin-top: 10px;
.screen-left {
height: 100%;
width: 27.6%;
#left-top {
height: 53%;
position: relative;
}
#left-bottom {
height: 31%;
margin-top: 25px;
position: relative;
}
}
.screen-middle {
height: 100%;
width: 41.5%;
margin-left: 1.6%;
margin-right: 1.6%;
#middle-top {
width: 100%;
height: 56%;
position: relative;
}
#middle-bottom {
margin-top: 25px;
width: 100%;
height: 28%;
position: relative;
}
}
.screen-right {
height: 100%;
width: 27.6%;
#right-top {
height: 46%;
position: relative;
}
#right-bottom {
height: 38%;
margin-top: 25px;
position: relative;
}
}
}
.resize {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
}
</style>
2.主题切换
①图表主题
src/store/index.js文件中:数据存储
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
theme: 'chalk'
},
mutations: {
changeTheme (state) {
if (state.theme === 'chalk') {
state.theme = 'vintage'
} else {
state.theme = 'chalk'
}
}
},
actions: {
},
modules: {
}
})
public/static/index.html文件中:
<!-- 引入主题的js文件 -->
<script src="static/theme/chalk.js"></script>
<script src="static/theme/vintage.js"></script>
src/views/ScreenPage.vue文件中:
methods:{
handleChangeTheme () {
// 修改VueX中数据
this.$store.commit('changeTheme')
}
}
components/Seller.vue文件中: 其他组件同样
import { mapState } from 'vuex'
computed: {
...mapState(['theme'])
},
watch: {
theme () {
console.log('主题切换了')
this.chartInstance.dispose() // 销毁当前的图表
this.initChart() // 重新以最新的主题名称初始化图表对象
this.screenAdapter() // 完成屏幕的适配
this.updateChart() // 更新图表的展示
}
},
methods:{
initChart () {
this.chartInstance = this.$echarts.init(this.$refs.hot_ref, this.theme)
}
}
②特殊html的主题:
src/utils/theme_utils.js文件中:
const theme = {
chalk: {
backgroundColor: '#161522', // 背景颜色
titleColor: '#ffffff', // 标题的文字颜色
logoSrc: 'logo_dark.png', // 左上角logo的图标路径
themeSrc: 'qiehuan_dark.png', // 切换主题按钮的图片路径
headerBorderSrc: 'header_border_dark.png'// 页面顶部的边框图片
},
vintage: {
backgroundColor: '#eeeeee', // 背景颜色
titleColor: '#000000',// 标题的文字颜色
logoSrc: 'logo_light2.png', // 左上角logo的图标路径
themeSrc: 'qiehuan_light.png',// 切换主题按钮的图片路径
headerBorderSrc: 'header_border_light.png' // 页面顶部的边框图片
}
}
export function getThemeValue (themeName) {
return theme[themeName]
}
components/Hot.vue文件中: 其他组件同样
import { getThemeValue } from '@/utils/theme_utils'
computed: {
comStyle () {
return {
color: getThemeValue(this.theme).titleColor
}
},
},
src/views/ScreenPage.vue文件中:
<div :style="containerStyle">
<img :src="headerSrc" />
<img :src="themeSrc" />
import { mapState } from 'vuex'
import { getThemeValue } from '@/utils/theme_utils'
computed: {
headerSrc () {
return '/static/img/' + getThemeValue(this.theme).headerBorderSrc
},
themeSrc () {
return '/static/img/' + getThemeValue(this.theme).themeSrc
},
containerStyle () {
return {
backgroundColor: getThemeValue(this.theme).backgroundColor,
color: getThemeValue(this.theme).titleColor
}
},
...mapState(['theme'])
}