<template>
<div class="content">
<HeaderFilter v-model:dateRange="dateRange" v-model:selectedMetric="selectedMetric"
v-model:selectedOneSite="selectedOneSite" v-model:selectedAdType="selectedAdType"
:metricOptions="metricOptions" :siteOptions="siteOptions" :loading="loading" :show-date="true"
:show-metric="true" :show-one-site="true" :show-ad-type="true" @filter-change="handleFilterChange" />
<div class="chart-wrapper">
<div ref="chartDom" class="chart-container"></div>
<div v-show="loading" class="loading-overlay">
<div class="loader"></div>
</div>
</div>
</div>
</template>
<script setup>
import { getAdLineChartApI } from '@/api/adunit.js'
import * as echarts from 'echarts'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import HeaderFilter from '@/components/HeaderFilter/index.vue'
import { allMetricName } from '@/utils/common.js'
const metricOptions = ref(allMetricName)
const props = defineProps({
siteOptions: {
type: Array,
default: () => []
}
})
// 状态管理
const chartDom = ref(null)
let myChart = null
const loading = ref(false)
const dateRange = ref((() => {
const formatDate = (date) => {
return date.toISOString().slice(0, 10)
}
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000)
return [formatDate(start), formatDate(end)]
})())
const selectedMetric = ref('imps')
const selectedOneSite = ref('')
const selectedAdType = ref('') // 或者 ref([]) 如果是多选
// 添加一个标志位,避免重复调用
const hasInitialized = ref(false)
// 提取设置默认站点的逻辑
const setDefaultSite = (siteOptions) => {
if (siteOptions && siteOptions.length > 0 &&
(!selectedOneSite.value || selectedOneSite.value === '')) {
selectedOneSite.value = siteOptions[0].value
console.log('Set default selectedOneSite:', selectedOneSite.value)
return true
}
return false
}
const initializeChart = () => {
// 如果已经初始化过,直接返回
if (hasInitialized.value) return
hasInitialized.value = true
// 尝试从本地存储加载设置
const hasSavedSettings = loadChartSettingsFromStorage()
// 如果有保存的设置,直接获取数据
if (hasSavedSettings) {
nextTick(() => {
fetchAndUpdateChart()
})
return
}
// 检查是否有 siteOptions 数据并设置默认值
if (props.siteOptions && props.siteOptions.length > 0) {
const hasSetDefault = setDefaultSite(props.siteOptions)
if (hasSetDefault) {
nextTick(() => {
fetchAndUpdateChart()
})
}
}
}
watch(
() => props.siteOptions,
(newSiteOptions) => {
console.log('siteOptions changed:', newSiteOptions)
// 只在未初始化时设置默认站点并获取数据
if (!hasInitialized.value && newSiteOptions && newSiteOptions.length > 0) {
const hasSetDefault = setDefaultSite(newSiteOptions)
if (hasSetDefault) {
hasInitialized.value = true
nextTick(() => {
fetchAndUpdateChart()
})
}
}
}
)
// 初始化图表(只执行一次)
const initChart = () => {
if (!chartDom.value) return
myChart = echarts.init(chartDom.value)
}
// 处理窗口大小变化
const handleResize = () => {
if (myChart) {
myChart.resize()
}
}
// 转换API数据为图表格式 - 增强健壮性
const transformData = (rawData) => {
try {
// 1. 处理空数据情况
if (!rawData || !Array.isArray(rawData)) {
return { dates: [], series: [] }
}
// 2. 提取所有日期并排序
const datesSet = new Set()
rawData.forEach(item => {
if (item?.data_date) {
datesSet.add(item.data_date)
}
})
const dates = Array.from(datesSet).sort((a, b) => new Date(a) - new Date(b))
// 3. 按 ad_unit_name 分组数据
const groupedData = {}
rawData.forEach(item => {
if (!item?.ad_unit_name) return
const adUnitName = item.ad_unit_name
if (!groupedData[adUnitName]) {
groupedData[adUnitName] = []
}
groupedData[adUnitName].push(item)
})
// 4. 为每个广告单元创建数据系列
const series = []
Object.keys(groupedData).forEach(adUnitName => {
// 如果选择了特定的广告类型,则只显示选中的类型
if (selectedAdType.value && selectedAdType.value.length > 0) {
if (!selectedAdType.value.includes(adUnitName)) {
return; // 跳过未选中的广告类型
}
}
const data = dates.map(date => {
const item = groupedData[adUnitName].find(i => i.data_date === date)
return item ? (item[selectedMetric.value] || 0) : 0
})
if (data.length > 0) {
series.push({
name: adUnitName, // 使用 ad_unit_name 作为系列名称
type: 'line',
data: data,
showSymbol: data.length <= 10,
emphasis: { focus: 'series' },
smooth: true,
id: `series-${adUnitName.replace(/\s+/g, '-')}`
})
}
})
return { dates, series }
} catch (error) {
console.error('数据转换失败:', error)
return { dates: [], series: [] }
}
}
const allAdTypes = ref([])
// 获取数据并更新图表
const fetchAndUpdateChart = async () => {
if (!dateRange.value || dateRange.value.length !== 2) return
// 确保图表已初始化
if (!myChart) {
initChart()
}
loading.value = true
try {
const [startAt, endAt] = dateRange.value
// 构造请求参数
const params = {
start_at: startAt,
end_at: endAt
}
// 如果选择了网站,则添加到参数中
if (selectedOneSite.value && selectedOneSite.value !== '') {
console.log('Sending site_url:', selectedOneSite.value)
params.site_urls = [selectedOneSite.value]
}
// 如果选择了广告类型,则添加到参数中
if (selectedAdType.value) {
params.ad_unit_names = [selectedAdType.value] // 根据API要求调整参数名
}
const response = await getAdLineChartApI(params)
// 添加API响应检查
if (!response || !response.data) {
throw new Error('API响应无效')
}
// 提取所有广告类型
const adTypes = [...new Set(response.data.map(item => item.ad_unit_name).filter(Boolean))]
allAdTypes.value = adTypes
// 如果还没有选择广告类型,则默认选择所有
if (!selectedAdType.value || selectedAdType.value.length === 0) {
selectedAdType.value = adTypes
}
const { dates, series } = transformData(response.data)
console.log(dates, 12313123);
updateChart(dates, series)
} catch (error) {
console.error("获取折线图数据失败:", error)
// 出错时显示空图表
updateChart([], [])
} finally {
loading.value = false
}
}
// 更新图表配置 - 完全重写
const updateChart = (dates, series) => {
console.log(dates, 11);
let safeSeries = series
if (!Array.isArray(safeSeries)) {
safeSeries = []
}
// 如果没有有效数据,创建空数据系列
if (safeSeries.length === 0) {
safeSeries = [{
name: 'The selected link has no data for the moment.',
type: 'line',
data: [],
id: 'empty-series'
}]
}
myChart.clear()
const allLegendNames = safeSeries.map(s => s.name);
// 根据图例数量决定行数
let legendConfig;
if (allLegendNames.length <= 6) {
// 少量图例,使用单行
legendConfig = [{
data: allLegendNames,
type: 'plain',
bottom: 40,
left: 'center',
width: '95%',
itemGap: 15,
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12
},
formatter: function (name) {
return name;
},
selectedMode: true
}];
// 设置底部边距
var gridBottom = '15%';
} else if (allLegendNames.length <= 12) {
// 中等数量图例,使用两行
const halfIndex = Math.ceil(allLegendNames.length / 2);
const firstRowLegends = allLegendNames.slice(0, halfIndex);
const secondRowLegends = allLegendNames.slice(halfIndex);
legendConfig = [
{
data: firstRowLegends,
type: 'plain',
bottom: 100,
left: 'center',
width: '95%',
itemGap: 15,
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12
},
formatter: function (name) {
return name;
},
selectedMode: true
},
{
data: secondRowLegends,
type: 'plain',
bottom: 60,
left: 'center',
width: '95%',
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12
},
formatter: function (name) {
return name;
},
selectedMode: true
}
];
// 设置底部边距
var gridBottom = '25%';
} else {
// 大量图例,使用滚动图例
legendConfig = [{
data: allLegendNames,
type: 'scroll',
bottom: 100,
left: 'center',
width: '95%',
itemGap: 10,
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12
},
pageButtonItemGap: 5,
pageButtonGap: 5,
pageButtonPosition: 'end',
pageFormatter: '{current}/{total}',
pageIcons: {
horizontal: [
'path://M10 12L16 6L10 0L8 2L12 6L8 10L10 12Z',
'path://M6 12L0 6L6 0L8 2L4 6L8 10L6 12Z'
]
},
pageIconSize: 15,
pageTextStyle: {
color: '#333',
fontSize: 12
},
selectedMode: true
}];
// 设置底部边距
var gridBottom = '15%';
}
// 为每个系列添加高亮效果配置
const seriesWithHighlight = safeSeries.map(s => {
return {
...s,
emphasis: {
focus: 'series', // 高亮当前系列
lineStyle: {
width: 4 // 高亮时线条加粗
}
}
}
});
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
show: false
}
},
// 自定义 tooltip 内容,实现高亮效果
formatter: function (params) {
// 获取当前悬停的系列索引
const hoverSeriesIndex = params[0].seriesIndex;
let html = `<div style="font-weight: bold; margin-bottom: 10px; font-size: 14px;">${params[0].axisValue}</div>`;
// 遍历所有参数,构建 tooltip 内容
params.forEach(param => {
const isHovered = param.seriesIndex === hoverSeriesIndex;
const opacity = isHovered ? '1' : '0.3'; // 悬停项不透明,其他项半透明
const fontWeight = isHovered ? 'bold' : 'normal';
const backgroundColor = isHovered ? 'rgba(0, 0, 0, 0.05)' : 'transparent';
const value = param.value;
const metricLabel = getMetricLabel(selectedMetric.value);
html += `
<div style="opacity: ${opacity}; font-weight: ${fontWeight}; background-color: ${backgroundColor};
padding: 5px 10px; border-radius: 4px; margin: 2px 0; transition: all 0.2s ease;">
<span style="display: flex; align-items: center; justify-content: space-between;">
<span style="display: flex; align-items: center;">
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%;
background: ${param.color}; margin-right: 8px;"></span>
<span>${param.seriesName}</span>
</span>
<span style="font-weight: ${fontWeight}; margin-left: 15px;">${value}</span>
</span>
</div>
`;
});
return html;
}
},
legend: legendConfig,
grid: {
left: '3%',
right: '4%',
bottom: gridBottom,
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates || [],
axisLabel: {
rotate: (dates && dates.length > 8) ? 45 : 0,
interval: 0
}
},
yAxis: {
type: 'value',
name: ''
},
series: seriesWithHighlight
}
myChart.setOption(option);
// 添加鼠标离开图表区域时取消高亮的效果
myChart.getZr().on('mouseout', () => {
myChart.dispatchAction({
type: 'downplay'
});
});
}
const getMetricLabel = (value) => {
return metricOptions.value.find(opt => opt.value === value)?.label || value
}
const saveChartSettingsToStorage = () => {
try {
const settings = {
selectedMetric: selectedMetric.value,
selectedOneSite: selectedOneSite.value,
selectedAdType: selectedAdType.value,
dateRange: dateRange.value
};
localStorage.setItem('adunitChartSettings', JSON.stringify(settings));
} catch (error) {
console.error('保存图表设置失败:', error);
}
};
// 从本地存储加载设置
const loadChartSettingsFromStorage = () => {
try {
const savedSettings = localStorage.getItem('adunitChartSettings');
if (savedSettings) {
const settings = JSON.parse(savedSettings);
// 恢复各项设置
if (settings.selectedMetric) {
selectedMetric.value = settings.selectedMetric;
}
if (settings.selectedOneSite) {
selectedOneSite.value = settings.selectedOneSite;
}
if (settings.selectedAdType) {
selectedAdType.value = settings.selectedAdType;
}
if (settings.dateRange) {
dateRange.value = settings.dateRange;
}
return true;
}
} catch (error) {
console.error('加载图表设置失败:', error);
}
return false;
};
const debounce = (func, delay) => {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
}
}
const debouncedHandleFilterChange = debounce(() => {
fetchAndUpdateChart()
}, 500)
const handleFilterChange = () => {
saveChartSettingsToStorage();
debouncedHandleFilterChange()
}
// 生命周期钩子
onMounted(() => {
// 初始化图表
initChart()
console.log('lineChart mounted, siteOptions:', props.siteOptions)
// 初始化图表数据
initializeChart();
})
onBeforeUnmount(() => {
if (myChart) {
window.removeEventListener('resize', handleResize)
myChart.dispose()
myChart = null
}
})
</script>
<style scoped>
.content {
padding: 20px;
background-color: #fff;
position: relative;
border-radius: 15px;
margin-top: 20px;
}
.chart-container {
width: 100%;
height: 550px;
min-height: 800px;
/* height: calc(100vh - 200px); */
background-color: #fff;
border-radius: 15px;
}
/* 如果需要调整加载动画的位置 */
.chart-wrapper .loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
border-radius: 15px;
}
:global(.echarts-tooltip) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
border-radius: 6px !important;
border: none !important;
padding: 12px !important;
}
:global(.echarts-tooltip .highlighted-item) {
background-color: rgba(0, 0, 0, 0.05) !important;
border-radius: 4px !important;
}
</style>
有点bug他默认高亮的第一条的数据,我选择第二条折线的时候,还是高亮的第一条
最新发布