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

feat(sycm_prd): 竞品分析_复制竞店代码

同上
上级 5366e586
......@@ -2,9 +2,9 @@
VITE_APP_TITLE = 王小卤-链路中心
# 基地址
# VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_API = '/dev-api'
# VITE_APP_BASE_API = '/qllan'
VITE_APP_BASE_API = '/bclan'
# VITE_APP_BASE_API = '/bclan'
# VITE_APP_BASE_API = '/home'
# 开发环境配置
......
......@@ -15,6 +15,13 @@ export function getStoreListAPI() {
})
}
// 竞品列表
export function getComPrdListAPI(){
return request({
url: '/bi/oppo/sycm/prdInfo'
})
}
// 生意参谋-各大竞店
export function getSycmStoreListAPI(params) {
return request({
......
......@@ -24,7 +24,7 @@ const list = ref([
{ name: '生意参谋-竞店', component: shallowRef(sycmStore) },
{ name: '生意参谋-竞品', component: shallowRef(sycmPrd) }
])
const activeName = ref(list.value[1].name)
const activeName = ref(list.value[2].name)
</script>
<style scoped
......
<template>
<div ref="echartsRef"
:class="className"
:style="{ height: height, width: width }" />
</template>
<script setup>
import * as echarts from 'echarts'
import { formatNumberWithUnit } from '@/utils'
import { useWindowResize } from '@/hooks'
const props = defineProps({
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '600px'
},
autoResize: {
type: Boolean,
default: true
},
chartData: {
type: Object,
required: true
}
})
const echartsRef = ref(null)
const chart = shallowRef(null)
let myThousand = false // 是否显示万单位
const emits = defineEmits(['changeType'])
const setOptions = () => {
chart.value.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: function (params) {
let tooltip = '';
params.forEach((item) => {
// 获取系列颜色
var color = item.color;
// 拼接提示内容
if (item.seriesName.split('-')[1].includes('速')) {
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, '%', false, false) + '<br>';
} else {
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, '人', false, true) + '<br>';
}
});
return tooltip;
}
},
// legend: {
// data: props.chartData.legend
// },
toolbox: {
feature: {
saveAsImage: {},
magicType: {
type: ['stack', 'tiled'] // 切换图表类型
},
myTool1: {
show: true,
title: '切换表格',
icon: 'path://M5,5 L5,15 L15,15 L15,5 Z M20,5 L20,15 L30,15 L30,5 Z M5,20 L5,30 L15,30 L15,20 Z M20,20 L20,30 L30,30 L30,20 Z',
onclick: () => {
emits('changeType', 'table')
}
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
// type: 'category',
// boundaryGap: false,
data: props.chartData.xAxis
}
],
yAxis: [
{
type: 'value'
}
],
series: props.chartData.series
}, true)
}
watchEffect(() => {
if (!chart.value) return
myThousand = props.chartData.series?.some(o => {
return o.data.some(num => num >= 10000)
})
setOptions()
})
const initChart = () => {
chart.value = echarts.init(echartsRef.value)
}
const resize = () => {
chart.value.resize()
}
useWindowResize(resize)
onMounted(() => {
nextTick(() => {
initChart()
})
})
onBeforeUnmount(() => {
if (!chart.value) {
return
}
chart.value.dispose()
chart.value = null
})
</script>
\ No newline at end of file
<template>
<div class="toolbar">
<right-toolbar :search="false"
@queryTable="queryTable">
<el-tooltip class="item"
effect="dark"
content="日期合并/分散"
placement="top">
<el-button circle
icon="Calendar"
@click="dateMergeFn()" />
</el-tooltip>
<el-tooltip class="item"
effect="dark"
content="切换图表"
placement="top">
<el-button circle
icon="Histogram"
@click="changeType()" />
</el-tooltip>
</right-toolbar>
</div>
<el-table :data="data"
style="width: 100%">
<el-table-column prop="platformStore"
label="店铺" />
<el-table-column prop="date"
label="日期"
v-if="!dateMerge" />
<el-table-column v-for="str in column"
:prop="columnKey[str]"
:label="str"
:formatter="formatterFn" />
</el-table>
</template>
<script lang="ts"
setup>
const columnKey = {
'支付买家数': 'zfmj',
'交易增速': 'jyzs',
'独立访客范围': 'uv',
'流量增速': 'llzs'
}
defineProps({
column: {
type: Object,
default: () => {
return {}
},
},
data: {
type: Array,
default: () => {
return []
},
},
dateMerge: { // 是否把日期数据合并
type: Boolean
}
})
const formatterFn = (row, column, cellValue) => {
if (column.property === 'jyzs' || column.property === 'llzs') {
return cellValue + '%'
} else {
return cellValue
}
}
const emits = defineEmits(['changeType', 'queryTable', 'dateMergeFn'])
const queryTable = () => {
emits('queryTable')
}
const changeType = () => {
emits('changeType', 'charts')
}
const dateMergeFn = () => {
emits('dateMergeFn')
}
</script>
<style scoped
style="sass">
.toolbar {
float: right;
}
</style>
\ No newline at end of file
<template>
<div>
<p>生意参谋品类</p>
<el-form :model="queryParams"
inline>
<el-form-item label="商品">
<el-select v-model="queryParams.prdList"
class="prd-select"
@change="queryChangeFn"
filterable
multiple
clearable
collapse-tags
collapse-tags-tooltip>
<el-option v-for="obj in prdList"
:label="obj.prdName"
:value="obj.prdId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数据类型">
<el-select v-model="queryParams.typeList"
@change="queryChangeFn"
multiple
clearable
collapse-tags>
<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"
unlink-panels
:clearable="false"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="pickerOptions"
@change="queryChangeFn('date')">
</el-date-picker>
</el-form-item>
</el-form>
<div class="chart_wrap"
v-loading="loading">
<template v-if="queryParams.prdList.length > 0 && queryParams.typeList.length > 0 && !loading">
<template v-if="showType === 'charts'">
<group-legend :legendData="chartData.legend"
@click-item="legendChangeFn"></group-legend>
<gradient-area :chartData="chartData"
@changeType="changeType"></gradient-area>
</template>
<template v-else-if="showType === 'table'">
<table-list :column="queryParams.typeList"
:data="tableList"
@changeType="changeType"
@queryTable="getList"
@dateMergeFn="dateMergeFn"></table-list>
</template>
</template>
<no-data v-else-if="!loading"></no-data>
</div>
<levitated-sphere>
<el-button type="danger"
@click="reset">重置本页</el-button>
</levitated-sphere>
</div>
</template>
<script setup>
</script>
import GradientArea from './GradientArea.vue';
import TableList from './TableList.vue';
import { getComPrdListAPI } from '@/api'
import { generatorDayList, parseTime, getBrandColor, resetObjValue } from '@/utils'
import { useDatePickerOptions } from '@/hooks'
// 静态数据
const dateList = [new Date().setDate((new Date().getDate() - 30)), new Date().setDate((new Date().getDate() - 1))] // 最近 30 日日期数组
const dataTypeList = ['支付买家数', '交易增速', '独立访客范围', '流量增速'] // 数据类型
// 本页数据
const loading = ref(true)
const prdList = ref([]) // 商品列表
const typeList = ref(dataTypeList) // 数据类型
const { pickerOptions } = useDatePickerOptions()
const showType = ref('charts') // 展示类型('charts' / 'table')
const dateMerge = ref(false) // 表格中日期是否合并
const queryParams = reactive({ // 查询表单
prdList: [],
typeList: [typeList.value[0]],
date: dateList
})
const allChartData = reactive({ // 图表所有数据
xAxis: [],
legend: [],
series: []
})
const chartData = reactive({ // 图表内要用的数据
xAxis: [],
legend: [],
series: [],
})
const allTableList = ref([])
const tableList = ref([])
// 获取竞品列表
const getPrdList = async () => {
const { data } = await getComPrdListAPI()
prdList.value = data
console.log(data)
// 初始化筛选条件(默认请求第一个店铺的第一类型数据)
queryParams.prdList = [data[0]?.prdId]
}
// 初始化图表数据
const initChartsData = (data) => {
resetObjValue(allChartData)
allChartData.xAxis = generatorDayList(queryParams.date[0], queryParams.date[1])
// 图表数据
data.forEach(list => {
// list: 每个店铺数据集合
// 公共的配置
let commonObj = {
type: 'line',
// stack: 'Total',
smooth: true,
// stackStrategy: 'all',
lineStyle: {
width: 0
},
showSymbol: false,
emphasis: {
focus: 'series'
}
}
let jyzsObj = {
...commonObj,
itemStyle: {
color: getBrandColor(list[0]?.platformStore, 0)
},
areaStyle: {
opacity: 0.8,
color: getBrandColor(list[0]?.platformStore, 0)
},
name: list[0]?.platformStore + '-交易增速',
data: []
}
let llzsObj = {
...commonObj,
itemStyle: {
color: getBrandColor(list[0]?.platformStore, 1)
},
areaStyle: {
opacity: 0.8,
color: getBrandColor(list[0]?.platformStore, 1)
},
name: list[0]?.platformStore + '-流量增速',
data: [],
}
let uvObj = {
...commonObj,
itemStyle: {
color: getBrandColor(list[0]?.platformStore, 2)
},
areaStyle: {
opacity: 0.8,
color: getBrandColor(list[0]?.platformStore, 2)
},
name: list[0]?.platformStore + '-独立访客范围',
data: []
}
let zfmjObj = {
...commonObj,
itemStyle: {
color: getBrandColor(list[0]?.platformStore, 3)
},
areaStyle: {
opacity: 0.8,
color: getBrandColor(list[0]?.platformStore, 3)
},
name: list[0]?.platformStore + '-支付买家数',
data: []
}
// list: 循环每一天的数据
// 按照日期添加数据
allChartData.xAxis.forEach(date => {
const findObj = list.find(o => o.date.includes(date))
if (findObj) {
jyzsObj.data.push((findObj.jyzsPeak + findObj.jyzsUnder) / 2)
llzsObj.data.push((findObj.llzsPeak + findObj.llzsUnder) / 2)
uvObj.data.push((findObj.uvPeak + findObj.uvUnder) / 2)
zfmjObj.data.push((findObj.zfmjPeak + findObj.zfmjUnder) / 2)
} else {
jyzsObj.data.push(0)
llzsObj.data.push(0)
uvObj.data.push(0)
zfmjObj.data.push(0)
}
})
allChartData.legend.push({
data: [{
name: jyzsObj.name,
type: 'circle',
color: getBrandColor(list[0]?.platformStore, 0),
effective: true,
show: true,
}, {
name: llzsObj.name,
type: 'circle',
color: getBrandColor(list[0]?.platformStore, 1),
effective: true,
show: true
}, {
name: uvObj.name,
type: 'circle',
color: getBrandColor(list[0]?.platformStore, 2),
effective: true,
show: true
}, {
name: zfmjObj.name,
type: 'circle',
color: getBrandColor(list[0]?.platformStore, 3),
effective: true,
show: true
}],
orient: 'verticalAlign'
})
allChartData.series.push(jyzsObj, llzsObj, uvObj, zfmjObj)
})
}
// 初始化表格数据
const initTableData = (data) => {
// 表格数据
allTableList.value = data
}
// 筛选图表数据
const filterChartsData = () => {
let legend = allChartData.legend
let series = allChartData.series
// 选择了数据类型,筛选
if (queryParams.typeList.length > 0) {
series = series.filter(item => {
return queryParams.typeList.find(o => o === item.name.split('-')[1])
})
// // 图例组件数据源不变,只是显示/隐藏
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
})
})
} else {
// 数据类型都没选
legend.forEach(obj => {
obj.data.forEach(o => {
o.show = true
})
})
}
// 据图例状态,筛选数据显示
series = series.filter(sObj => {
let nowSeries = true
legend.forEach(obj => {
const now = obj.data.find(o => {
return o.name === sObj.name
})
// now 图例对象无效,则当前 sObj 对象无效
if (now?.effective === false) {
nowSeries = false
}
})
return nowSeries
})
chartData.legend = legend
chartData.series = series
chartData.xAxis = allChartData.xAxis
}
<style scoped lang="scss">
// 筛选表格数据
const filterTableData = () => {
// 没有日期合并
if (dateMerge.value) {
tableList.value = allTableList.value.map(o => {
const platformStore = o[0].platformStore
const zfmj = (o.reduce((sum, obj) => {
return sum += ((obj.zfmjPeak + obj.zfmjUnder) / 2)
}, 0) / o.length).toFixed(2)
const jyzs = (o.reduce((sum, obj) => {
return sum += ((obj.jyzsPeak + obj.jyzsUnder) / 2)
}, 0) / o.length).toFixed(2)
const uv = (o.reduce((sum, obj) => {
return sum += ((obj.uvPeak + obj.uvUnder) / 2)
}, 0) / o.length).toFixed(2)
const llzs = (o.reduce((sum, obj) => {
return sum += ((obj.llzsPeak + obj.llzsUnder) / 2)
}, 0) / o.length).toFixed(2)
return {
platformStore, zfmj, jyzs, uv, llzs
}
})
} else {
tableList.value = allTableList.value.flat().map(o => {
return {
...o,
zfmj: (o.zfmjPeak + o.zfmjUnder) / 2,
jyzs: (o.jyzsPeak + o.jyzsUnder) / 2,
uv: (o.uvPeak + o.uvUnder) / 2,
llzs: (o.llzsPeak + o.llzsUnder) / 2,
}
})
}
}
// 获取数据
const getList = async () => {
loading.value = true
const { data } = await getSycmStoreListAPI({
startDate: parseTime(queryParams.date[0], '{y}-{m}-{d}'),
endDate: parseTime(queryParams.date[1], '{y}-{m}-{d}'),
platformStore: queryParams.brandList.join(','),
dataType: queryParams.typeList.join(',')
})
if (showType.value === 'charts') {
initChartsData(data)
filterChartsData()
}
else if (showType.value === 'table') {
initTableData(data)
filterTableData()
}
loading.value = false
return data
}
// 表格数据合并
const dateMergeFn = () => {
dateMerge.value = !dateMerge.value
filterTableData()
}
// 默认打开页面请求一次所有数据,并保存在数据源
async function init() {
await getPrdList()
// await getList()
};
onMounted(() => {
init()
})
// 图例点击直接过滤数据
const legendChangeFn = () => {
filterChartsData()
}
// 图表/表格类型切换
const changeType = (val) => {
showType.value = val
getList()
}
// 查询条件改变
const queryChangeFn = async (arg) => {
getList()
}
// 重置
const reset = async () => {
// 重置查询参数
loading.value = true
queryParams.brandList = []
queryParams.date = dateList
queryParams.typeList = [dataTypeList[0]]
init()
}
</script>
<style scoped
lang="scss">
.prd-select {
width: 550px;
}
</style>
\ No newline at end of file
......@@ -76,7 +76,7 @@ const setOptions = () => {
title: '切换表格',
icon: 'path://M5,5 L5,15 L15,15 L15,5 Z M20,5 L20,15 L30,15 L30,5 Z M5,20 L5,30 L15,30 L15,20 Z M20,20 L20,30 L30,30 L30,20 Z',
onclick: () => {
emits('changeType', 'table')
emits('changeType', 'table')
}
}
}
......@@ -119,6 +119,7 @@ const resize = () => {
chart.value.resize()
}
useWindowResize(resize)
onMounted(() => {
nextTick(() => {
initChart()
......
<template>
<div class="toolbar">
<right-toolbar :search="false" @queryTable="queryTable">
<el-tooltip class="item" effect="dark" content="切换图表" placement="top">
<el-button circle icon="Histogram" @click="changeType()" />
<right-toolbar :search="false"
@queryTable="queryTable">
<el-tooltip class="item"
effect="dark"
content="日期合并/分散"
placement="top">
<el-button circle
icon="Calendar"
@click="dateMergeFn()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="日期合并/分散" placement="top">
<el-button circle icon="Calendar" @click="dateMergeFn()" />
<el-tooltip class="item"
effect="dark"
content="切换图表"
placement="top">
<el-button circle
icon="Histogram"
@click="changeType()" />
</el-tooltip>
</right-toolbar>
</div>
<el-table :data="data"
......@@ -14,7 +26,8 @@
<el-table-column prop="platformStore"
label="店铺" />
<el-table-column prop="date"
label="日期" v-if="!dateMerge"/>
label="日期"
v-if="!dateMerge" />
<el-table-column v-for="str in column"
:prop="columnKey[str]"
:label="str"
......@@ -24,7 +37,6 @@
<script lang="ts"
setup>
const columnKey = {
'支付买家数': 'zfmj',
'交易增速': 'jyzs',
......
......@@ -54,8 +54,7 @@
:data="tableList"
@changeType="changeType"
@queryTable="getList"
@dateMergeFn="dateMergeFn"
:dateMerge="dateMerge"></table-list>
@dateMergeFn="dateMergeFn"></table-list>
</template>
</template>
<no-data v-else-if="!loading"></no-data>
......@@ -63,12 +62,6 @@
<levitated-sphere>
<el-button type="danger"
@click="reset">重置本页</el-button>
<el-button type="primary"
@click="changeType(showType === 'charts' ? 'table' : 'charts')">切换{{ showType === 'charts' ? '表格' :
'图表' }}</el-button>
<el-button v-if="showType === 'table'"
type="warning"
@click="dateMergeFn">日期{{ dateMerge ? '分散' : '合并' }}</el-button>
</levitated-sphere>
</div>
</template>
......@@ -112,9 +105,7 @@ const chartData = reactive({ // 图表内要用的数据
const allTableList = ref([])
const tableList = ref([])
const changeType = (val) => {
showType.value = val
}
// 获取竞店列表
const getStoreList = async () => {
......@@ -292,7 +283,6 @@ const filterChartsData = () => {
chartData.legend = legend
chartData.series = series
console.log(allChartData.xAxis)
chartData.xAxis = allChartData.xAxis
}
......@@ -369,18 +359,21 @@ async function init() {
await getList()
};
watch(showType, () => {
getList()
})
onMounted(() => {
init()
})
// 图例点击直接过滤数据
const legendChangeFn = () => {
filterChartsData()
}
// 图表/表格类型切换
const changeType = (val) => {
showType.value = val
getList()
}
// 查询条件改变
const queryChangeFn = async (arg) => {
getList()
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论