提交 a05f81a5 authored 作者: lidongxu's avatar lidongxu

fix(bi/competitor/cmm): 修复已知bug

图表筛选问题和图例筛选问题以及左右 2 个 y 轴显示内容,并封装根据原始数据决定如何显示数字字符串带单位描述的函数使用
上级 5768f05c
......@@ -12,10 +12,9 @@
@click="handleClick">
<svg-icon icon-class="tool"></svg-icon>
</div>
<!-- 工具抽屉 -->
<el-drawer title="工具箱"
:visible.sync="drawer"
v-model="drawer"
size="20%"
@click.native="drawer = false">
<slot></slot>
......@@ -112,6 +111,7 @@ export default {
},
methods: {
handleClick() {
console.log(this.downPoint.x,this.upPoint.x,this.downPoint.y,this.upPoint.y)
if (this.downPoint.x === this.upPoint.x && this.downPoint.y === this.upPoint.y) {
this.drawer = !this.drawer
}
......
export const useDatePickerOptions = () => {
const pickerOptions = ref([// 日期选项配置
{
text: '最近一周',
value() {
const end = new Date().setDate((new Date().getDate() - 1));
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end]
}
}, {
text: '最近一个月',
value() {
const end = new Date().setDate((new Date().getDate() - 1));
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end]
}
}
])
return {
pickerOptions
}
}
\ No newline at end of file
......@@ -403,3 +403,19 @@ export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
}
// 清除对象里属性的非响应式的值
export function resetObjValue(obj, props) {
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
// 判断属性值是否为数组或对象
if (Array.isArray(obj[prop])) {
obj[prop] = [];
} else if (typeof obj[prop] === 'object' && obj[prop] !== null) {
obj[prop] = {};
} else {
obj[prop] = undefined; // 基础类型重置为 undefined
}
}
}
}
\ No newline at end of file
......@@ -20,14 +20,66 @@ export function divSafe(arg1, arg2) {
}
/**
* 小数数字,转格式千分位用逗号,例如:12345.678 转换后 12,346
* @param {*} num
* @returns
* 将数值转换为带单位的字符串,并添加千分位逗号
* @param {number} value - 要转换的数值
* @param {string} extraDescription - 额外的描述字符串(没有直接返回数据)
* @param {bool} bool - 是否开启转换
* @param {bool} round - 是否取整
* @returns {string} 格式化后的字符串
*/
export function toThousands(num) {
if (num === null || num === undefined) return '0'
// 暂存小数位
return (Math.round(num) || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
export function formatNumberWithUnit(value, extraDescription, bool, round) {
if (typeof value !== 'number') {
throw new Error('输入值必须是数字');
}
// 不转换
if (!bool) {
if(!round){
if (!extraDescription) {
return value.toFixed(2)
} else {
return value.toFixed(2) + extraDescription
}
}else{
if (!extraDescription) {
return Math.round(value) + ''
} else {
return Math.round(value) + extraDescription
}
}
}
// 转换单位
let unit;
let formattedValue;
if (value >= 100000000) {
unit = '亿';
formattedValue = (value / 100000000);
} else if (value >= 10000) {
unit = '万';
formattedValue = (value / 10000);
} else {
unit = '';
formattedValue = value;
}
// 判断是否取整
if (round) {
formattedValue = Math.round(formattedValue) + '';
} else {
formattedValue = formattedValue.toFixed(2);
}
// 如果没有额外描述,直接返回数值
if (!extraDescription) {
return formattedValue;
}
// 添加千分位逗号
formattedValue = formattedValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${formattedValue}${unit}${extraDescription}`;
}
/**
......@@ -44,43 +96,6 @@ export function roundUpToNextHighestPowerOfTen(num) {
return (Number(first) + 1) + result
}
/**
* 把数字转换成带元/万/亿单位结尾字符串
* @param {*} number
* @param {*} isThousand 是否万分位数值(外面已经把数据除以万以后的数据)
* @returns
*/
export function convertToUnit(number, isThousand) {
if (number === 0 || number === undefined || number === null) {
return "0";
}
let unit;
let value;
if (isThousand) {
number = number * 10000
}
if (number >= 100000000) {
unit = " 亿元";
value = number / 100000000;
} else if (number >= 10000) {
unit = " 万元";
value = number / 10000;
} else {
unit = " 元";
value = number;
}
// 保留两位小数
let formattedValue = value.toFixed(2);
// 去除末尾可能多余的0
formattedValue = parseFloat(formattedValue).toString();
return `${formattedValue}${unit}`;
}
// 保留 2 位小数
export function toFixed2(num) {
if (num === null || num === undefined) return '0'
......
<template>
<div :class="className"
<div ref="echartsRef"
:class="className"
:style="{ height: height, width: width }" />
</template>
<script>
<script setup>
import * as echarts from 'echarts'
import resize from '@/views/dashboard/mixins/resize'
import { convertToUnit, toFixed2 } from '@/utils'
export default {
mixins: [resize],
props: {
import { formatNumberWithUnit } from '@/utils'
const props = defineProps({
className: {
type: String,
default: 'chart'
......@@ -21,7 +18,7 @@ export default {
},
height: {
type: String,
default: '500px'
default: '600px'
},
autoResize: {
type: Boolean,
......@@ -31,65 +28,74 @@ export default {
type: Object,
required: true
}
})
const echartsRef = ref(null)
const chart = shallowRef(null)
const myThousand = ref(false) // 是否显示万单位
const setOptions = () => {
// 计算 y 轴显示
const yAxis = [
{
type: 'value',
name: '销售额',
nameTextStyle: {
padding: [0, 60, 0, 0]
},
data() {
return {
chart: null,
myThousand: false
axisLabel: {
formatter(value) {
return formatNumberWithUnit(value, '元', myThousand.value, true)
}
},
watch: {
chartData: {
deep: true,
handler(val) {
this.myThousand = val.series?.some(o => {
return o.data.some(num => num >= 10000)
})
// this.chart.dispose()
// this.initChart()
this.chart.clear(); // 清除图表(重新设置)
this.setOptions(val)
axisPointer: {
label: {
formatter: (params) => {
return formatNumberWithUnit(params.value, '元', myThousand.value, true)
}
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
{
type: 'value',
name: '观看人次',
nameTextStyle: {
padding: [0, 0, 0, 60]
},
beforeDestroy() {
if (!this.chart) {
return
axisLabel: {
formatter(value) {
return formatNumberWithUnit(value, '人', myThousand.value, true)
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
setOptions({ expectedData, actualData } = {}) {
// 动态计算 max 值并向上取整
// const maxSaleValue = Math.ceil(
// Math.max(
// ...this.chartData?.data?.saleData.flat()
// )
// );
// const maxGkrcValue = Math.ceil(
// Math.max(
// ...this.chartData?.data?.gkrcData.flat()
// )
// );
// const a = roundUpToNextHighestPowerOfTen(maxSaleValue);
// const b = roundUpToNextHighestPowerOfTen(maxGkrcValue);
this.chart.setOption({
axisPointer: {
label: {
formatter: (params) => {
return formatNumberWithUnit(params.value, '人', myThousand.value, true)
}
}
}
}
]
yAxis.filter(yObj => {
return props.chartData.legend.some(o => {
const target = o.data.find(o => {
return o.name.split('-')[1] === yObj.name
})
if (!target?.show) yObj.name = ''
else yObj.name = target.name.split('-')[1]
})
})
chart.value.setOption({
xAxis: {
data: this.chartData.xAxis,
// boundaryGap: false,
type: 'category',
data: props.chartData.xAxis,
axisPointer: {
type: 'shadow'
}
},
grid: {
top: '6%',
top: '9%',
left: '3%',
right: '3%',
bottom: '3%',
......@@ -100,15 +106,15 @@ export default {
feature: {
saveAsImage: {}, // 保存为图片
magicType: {
type: ['stack', 'tiled'] // 切换图表类型
type: ['tiled'] // 切换图表类型
},
myThousandTool: {
show: true,
title: '切换万单位',
icon: 'path://M50,50 L100,50 L100,100 L150,100 L150,150 L100,150 L100,200 L50,200 L50,150 L0,150 L0,100 L50,100 Z',
onclick: () => {
this.myThousand = !this.myThousand
this.setOptions()
myThousand.value = !myThousand.value
setOptions()
}
}
}
......@@ -116,65 +122,67 @@ export default {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
type: 'cross',
crossStyle: {
color: '#999'
}
},
padding: [5, 10],
formatter: (params) => {
formatter: function (params) {
let tooltip = '';
params.forEach((item) => {
// 获取系列颜色
var color = item.color;
// 拼接提示内容
if (item.seriesName.split('-')[1] === '销售额') {
tooltip += item.marker + item.seriesName + ': ' + (this.myThousand ? convertToUnit(item.value, true) : toFixed2(item.value) + '元') + '<br>';
tooltip += '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:' + color + ';"></span>' + item.seriesName + ': ' + formatNumberWithUnit(item.value, '元', myThousand.value) + '<br>';
} else if (item.seriesName.split('-')[1] === '观看人次') {
tooltip += item.marker + item.seriesName + ': ' + toFixed2(item.value) + (this.myThousand ? '万' : '') + '人<br>';
tooltip += '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:' + color + ';"></span>' + item.seriesName + ': ' + formatNumberWithUnit(item.value, '人', myThousand.value) + '<br>';
}
});
return tooltip;
}
},
yAxis: [
{
type: 'value',
name: '销售额',
// nameTextStyle: {
// padding: [0, 80, 0, 0]
// },
axisLabel: {
formatter: '{value} ' + (this.myThousand ? '万' : '') + '元'
},
// min: 0, // 设置最小值
// max: a,
// offset: 20
},
{
type: 'value',
name: '观看人次',
// nameTextStyle: {
// padding: [0, 0, 0, 80]
// },
axisLabel: {
formatter: '{value} ' + (this.myThousand ? '万' : '') + '人'
},
// min: 0, // 设置最小值
// max: b,
// offset: 20
},
],
// legend: {
// data: this.chartData.legend
// },
// legend: this.chartData.legend,
series: [
...this.chartData.series?.map(o => {
return {
...o,
data: o.data.map(num => {
return parseFloat((this.myThousand ? num / 10000 : num).toFixed(2))
})
}
// padding: [5, 10],
// formatter: (params) => {
// let tooltip = '';
// params.forEach((item) => {
// if (item.seriesName.split('-')[1] === '销售额') {
// tooltip += item.marker + item.seriesName + ': ' + (this.myThousand ? convertToUnit(item.value, true) : toFixed2(item.value) + '元') + '<br>';
// } else if (item.seriesName.split('-')[1] === '观看人次') {
// tooltip += item.marker + item.seriesName + ': ' + toFixed2(item.value) + (this.myThousand ? '' : '') + '<br>';
// }
// });
// console.log(tooltip)
// return tooltip;
// }
},
yAxis,
series: props.chartData.series
}, true)
}
watchEffect(() => {
if (!chart.value) return
myThousand.value = props.chartData.series?.some(o => {
return o.data.some(num => num >= 10000)
})
]
setOptions()
})
const initChart = () => {
chart.value = echarts.init(echartsRef.value)
setOptions()
}
onMounted(() => {
nextTick(() => {
initChart()
})
})
onBeforeUnmount(() => {
if (!chart.value) {
return
}
}
}
chart.value.dispose()
chart.value = null
})
</script>
<template>
<div class="chart_wrap">
<div class="tabs-container">
<el-form :model="queryParams"
inline
size="default">
......@@ -16,6 +16,19 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数据类型">
<el-select v-model="queryParams.typeList"
@change="queryChangeFn"
multiple
clearable
collapse-tags
collapse-tags-tooltip>
<el-option v-for="str in typeList"
:label="str"
:value="str">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="日期选择">
<el-date-picker v-model="queryParams.date"
type="daterange"
......@@ -24,17 +37,10 @@
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:picker-options="pickerOptions"
:shortcuts="pickerOptions"
@change="queryChangeFn('date')">
</el-date-picker>
</el-form-item>
<el-form-item label="数据类型">
<el-checkbox-group v-model="queryParams.typeList"
@change="queryChangeFn">
<el-checkbox label="销售额"></el-checkbox>
<el-checkbox label="观看人次"></el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<div class="chart_wrap">
<group-legend :legendData="chartData.legend"
......@@ -52,48 +58,39 @@
import { ref, reactive } from 'vue'
import LineCharts from './LineAndBar.vue'
import { getCmmListAPI } from '@/api'
import { generatorDayList, parseTime, getBrandColor } from '@/utils'
import { generatorDayList, parseTime, getBrandColor, resetObjValue } from '@/utils'
import { useDatePickerOptions } from '@/hooks/date'
// 最近 30 日日期数组
const dateList = [new Date().setDate((new Date().getDate() - 30)), new Date().setDate((new Date().getDate() - 1))]
// 数据类型
const dataTypeList = ['销售额', '观看人次']
const queryParams = reactive({ // 查询表单
brandList: [],
date: [new Date().setDate((new Date().getDate() - 30)), new Date().setDate((new Date().getDate() - 1))],
typeList: ['销售额', '观看人次']
})
const pickerOptions = reactive({// 日期选项配置
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date().setDate((new Date().getDate() - 1));
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date().setDate((new Date().getDate() - 1));
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}]
date: dateList,
typeList: dataTypeList
})
const { pickerOptions } = useDatePickerOptions()
const brandList = ref([])// 直播间列表
const typeList = ref(dataTypeList)
const allChartData = reactive({ // 图表所有数据
series: [],
xAxis: [],
legend: []
legend: [],
series: []
})
const chartData = reactive({ // 图表内要用的数据
series: [],
xAxis: [],
legend: []
legend: [],
series: []
})
// 获取数据
const getList = async () => {
allChartData.series = []
allChartData.legend = []
// 重置数据源
resetObjValue(allChartData)
// 获取 x 轴时间
allChartData.xAxis = generatorDayList(queryParams.date[0], queryParams.date[1])
const { data } = await getCmmListAPI({
startDate: parseTime(queryParams.date[0], '{y}-{m}-{d}'),
endDate: parseTime(queryParams.date[1], '{y}-{m}-{d}')
......@@ -101,29 +98,38 @@ const getList = async () => {
data.map((list, index) => {
// list:每个直播间
let color = getBrandColor(list[0]?.name)
// 每个系列对象
const saleObj = {
name: list[0]?.name + '-销售额',
type: 'bar',
data: [],
itemStyle: {
color
}
},
color,
yAxisIndex: 0
}
const gkrcObj = {
name: list[0]?.name + '-观看人次',
type: 'line',
data: [],
showSymbol: false,
symbol: 'none',
lineStyle: {
width: 2, // 设置折线的宽度
type: 'solid', // 设置折线的类型
color
},
color,
yAxisIndex: 1,
}
// 按照日期添加数据
// 按照日期归并数据
const listMap = new Map()
list.forEach(o => {
listMap.set(o.date.split('-').slice(1).join('-'), o)
})
allChartData.xAxis.forEach(date => {
const findObj = list.find(o => o.date.includes(date))
const findObj = listMap.get(date)
if (findObj) {
saleObj.data.push(findObj.saleSumTotal)
gkrcObj.data.push(findObj.gkrcNumTotal)
......@@ -132,14 +138,14 @@ const getList = async () => {
gkrcObj.data.push(0)
}
})
// legendData 名字
// 添加图例数据
allChartData.legend.push({
data: [{
name: saleObj.name,
type: 'bar',
color: color,
effective: true,
show: true
show: true,
}, {
name: gkrcObj.name,
type: 'line',
......@@ -149,8 +155,7 @@ const getList = async () => {
}],
orient: 'verticalAlign'
})
// allChartData.legend.push(saleObj.name)
// allChartData.legend.push(gkrcObj.name)
// 添加系列数据
allChartData.series.push(saleObj)
allChartData.series.push(gkrcObj)
})
......@@ -170,25 +175,32 @@ const filterData = () => {
return queryParams.brandList.find(nameStr => obj.data.find(o => o.name.split('-')[0] === nameStr))
})
}
// 查看数据类型必须走
// 数据类型筛选
if (queryParams.typeList.length > 0) {
series = series.filter(obj => {
return queryParams.typeList.find(str => obj.name.split('-')[1] === str)
})
legend = legend.filter(obj => {
// TODO:会不会有丢失响应式的
const list = [...obj.data]
list.forEach((o, index) => {
// 图例组件数据源不变,只是显示/隐藏
legend.filter(obj => {
obj.data.forEach((o, index) => {
const isHave = queryParams.typeList.includes(o.name.split('-')[1])
// 设置图例组件显示/隐藏
if (!isHave) o.show = false
else o.show = true
})
obj.data = list
return true
})
// 根据图例状态筛选数据是否显示
} else {
// 数据类型都没选
legend.forEach(obj => {
obj.data.forEach(o => {
o.show = true
})
})
}
// 据图例状态,筛选数据显示
series = series.filter(sObj => {
let nowSeries = true
chartData.legend.forEach(obj => {
legend.forEach(obj => {
const now = obj.data.find(o => {
return o.name === sObj.name
})
......@@ -205,47 +217,50 @@ const filterData = () => {
chartData.xAxis = allChartData.xAxis
}
// 默认打开页面请求一次所有数据,并保存在数据源
(async function init() {
// 生成 x 轴时间
allChartData.xAxis = generatorDayList(queryParams.date[0], queryParams.date[1])
const init = async function () {
// 请求数据
const data = await getList()
// 初始化直播间列表
brandList.value = data.map(list => list[0].name)
// 筛选图表数据
filterData()
})();
}
init()
// 筛选条件改变了
const queryChangeFn = async (arg) => {
if (arg === 'date' && !queryParams.date) return
if (arg === 'date' && queryParams.date) {
// 时间变化,并且有值,需要重新请求
allChartData.xAxis = generatorDayList(queryParams.date[0], queryParams.date[1])
await getList()
}
filterData()
}
// 图例点击改变了
const legendChangeFn = (o) => {
const legendChangeFn = () => {
filterData()
}
// 重置
const reset = async () => {
// 重置查询参数
queryParams.brandList = []
queryParams.date = [new Date().setDate((new Date().getDate() - 30)), new Date().setDate((new Date().getDate() - 1))]
queryParams.typeList = ['销售额', '观看人次']
allChartData.xAxis = generatorDayList(queryParams.date[0], queryParams.date[1])
// 重新获取
await getList()
// 筛选图表数据
filterData()
queryParams.date = dateList
queryParams.typeList = dataTypeList
init()
}
</script>
<style scoped
lang="scss"></style>
\ No newline at end of file
lang="scss">
.tabs-container {
height: 100%;
display: flex;
flex-direction: column;
.chart_wrap {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
}
}
</style>
\ No newline at end of file
......@@ -20,9 +20,9 @@ import sycmStore from './sycm_store'
import sycmPrd from './sycm_prd'
import { ref } from 'vue'
const list = ref([
{ name: '蝉妈妈', component: cmm },
{ name: '生意参谋-竞店', component: sycmStore },
{ name: '生意参谋-竞品', component: sycmPrd }
{ name: '蝉妈妈', component: shallowRef(cmm) },
{ name: '生意参谋-竞店', component: shallowRef(sycmStore) },
{ name: '生意参谋-竞品', component: shallowRef(sycmPrd) }
])
const activeName = ref(list.value[0].name)
</script>
......@@ -30,9 +30,23 @@ const activeName = ref(list.value[0].name)
<style scoped
lang="scss">
.app-container {
height: calc(100vh - 84px);
.tabs {
background: var(--el-bg-color-overlay);
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
::v-deep(.el-tabs__content) {
flex: 1;
.el-tab-pane {
height: 100%;
}
}
}
}
</style>
\ No newline at end of file
......@@ -22,7 +22,7 @@ export default {
},
height: {
type: String,
default: '500px'
default: '600px'
},
autoResize: {
type: Boolean,
......
......@@ -41,7 +41,7 @@
</el-form-item> -->
</el-form>
<div class="chart_wrap">
<!-- <gradient-area :chartData="chartData"></gradient-area> -->
<gradient-area :chartData="chartData"></gradient-area>
</div>
</div>
</template>
......@@ -204,10 +204,10 @@ const filterData = () => {
// 直播间列表
brandList.value = data.map(list => list[0].platformStore)
// 初始化筛选条件(默认请求第一个店铺的第一类型数据)
// queryParams.brandList = [data[0][0].platformStore]
// queryParams.typeList = [typeList.value[0]]
queryParams.brandList = [data[0][0].platformStore]
queryParams.typeList = [typeList.value[0]]
// 筛选图表数据
// filterData()
filterData()
})
})();
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论