|
|
@@ -178,6 +178,66 @@ const loadTeamStats = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 销毁图表实例
|
|
|
+const destroyChart = () => {
|
|
|
+ if (chartInstance.value) {
|
|
|
+ try {
|
|
|
+ chartInstance.value.destroy()
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('销毁图表实例失败:', e)
|
|
|
+ }
|
|
|
+ chartInstance.value = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建图表配置
|
|
|
+const createChartConfig = (labels, datasets) => {
|
|
|
+ return {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels,
|
|
|
+ datasets
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ boxWidth: 10
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ callbacks: {
|
|
|
+ label: function(context) {
|
|
|
+ let label = context.dataset.label || '';
|
|
|
+ if (label) {
|
|
|
+ label += ': ';
|
|
|
+ }
|
|
|
+ if (context.parsed.y !== null) {
|
|
|
+ label += context.parsed.y.toFixed(2);
|
|
|
+ }
|
|
|
+ return label;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ beginAtZero: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 300
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 加载收入统计数据
|
|
|
const loadIncomeStats = async () => {
|
|
|
try {
|
|
|
@@ -200,24 +260,16 @@ const loadIncomeStats = async () => {
|
|
|
}
|
|
|
|
|
|
// 获取数据前先销毁旧图表
|
|
|
- if (chartInstance.value) {
|
|
|
- try {
|
|
|
- chartInstance.value.destroy()
|
|
|
- chartInstance.value = null
|
|
|
- } catch (e) {
|
|
|
- console.warn('销毁旧图表实例失败:', e)
|
|
|
- }
|
|
|
- }
|
|
|
+ destroyChart()
|
|
|
|
|
|
const data = await getIncomeStatistics(startDate, endDate, userId)
|
|
|
|
|
|
- // 确保数据结构完整
|
|
|
+ // 确保数据结构完整并转换为数字类型
|
|
|
incomeStats.value = {
|
|
|
dates: data?.dates || [],
|
|
|
- total: data?.total || [],
|
|
|
- totalTip: data?.totalTip || [],
|
|
|
- totalCommission: data?.totalCommission || [],
|
|
|
- agents: data?.agents || []
|
|
|
+ total: data?.total?.map(v => Number(v) || 0) || [],
|
|
|
+ totalTip: data?.totalTip?.map(v => Number(v) || 0) || [],
|
|
|
+ totalCommission: data?.totalCommission?.map(v => Number(v) || 0) || []
|
|
|
}
|
|
|
|
|
|
// 渲染图表
|
|
|
@@ -229,141 +281,93 @@ const loadIncomeStats = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 重置画布
|
|
|
-const resetCanvas = () => {
|
|
|
+// 创建新画布
|
|
|
+const createCanvas = () => {
|
|
|
const chartContainer = document.querySelector('.chart-wrapper')
|
|
|
- if (!chartContainer) return null
|
|
|
-
|
|
|
- // 移除旧画布
|
|
|
- const oldCanvas = document.getElementById('incomeChart')
|
|
|
- if (oldCanvas) {
|
|
|
- oldCanvas.remove()
|
|
|
+ if (!chartContainer) {
|
|
|
+ console.error('找不到图表容器')
|
|
|
+ return null
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ // 清空容器
|
|
|
+ chartContainer.innerHTML = ''
|
|
|
+
|
|
|
// 创建新画布
|
|
|
- const newCanvas = document.createElement('canvas')
|
|
|
- newCanvas.id = 'incomeChart'
|
|
|
- newCanvas.height = 300
|
|
|
- chartContainer.appendChild(newCanvas)
|
|
|
-
|
|
|
- return newCanvas
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.id = 'incomeChart'
|
|
|
+ canvas.height = 300
|
|
|
+ chartContainer.appendChild(canvas)
|
|
|
+
|
|
|
+ return canvas
|
|
|
}
|
|
|
|
|
|
// 渲染图表
|
|
|
const renderChart = () => {
|
|
|
- if (!incomeStats.value) {
|
|
|
- console.warn('没有收入统计数据,跳过图表渲染')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 重置画布
|
|
|
- const canvas = resetCanvas()
|
|
|
- if (!canvas) {
|
|
|
- console.error('无法创建图表画布')
|
|
|
+ // 检查数据有效性
|
|
|
+ if (!incomeStats.value || !incomeStats.value.dates || !incomeStats.value.dates.length) {
|
|
|
+ console.warn('没有有效的收入统计数据')
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
- // 获取上下文
|
|
|
+
|
|
|
+ // 创建新画布
|
|
|
+ const canvas = createCanvas()
|
|
|
+ if (!canvas) return
|
|
|
+
|
|
|
+ // 获取绘图上下文
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
if (!ctx) {
|
|
|
- console.error('无法获取图表上下文')
|
|
|
+ console.error('无法获取绘图上下文')
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
- // 准备数据并验证
|
|
|
- const labels = incomeStats.value.dates || []
|
|
|
|
|
|
- // 验证数据完整性
|
|
|
- if (!labels.length) {
|
|
|
- console.warn('图表日期数据不完整,跳过渲染')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 准备数据集
|
|
|
- const datasets = []
|
|
|
+ // 准备数据
|
|
|
+ const { dates, total, totalTip, totalCommission } = incomeStats.value
|
|
|
|
|
|
- // 添加总收入数据集
|
|
|
- const totalData = incomeStats.value.total || []
|
|
|
- if (totalData.length > 0) {
|
|
|
- datasets.push({
|
|
|
+ // 创建数据集
|
|
|
+ const datasets = [
|
|
|
+ {
|
|
|
label: '总收入',
|
|
|
- data: [...totalData],
|
|
|
+ data: total,
|
|
|
borderColor: '#2563eb',
|
|
|
- backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
|
|
+ backgroundColor: 'rgba(37, 99, 235, 0.05)',
|
|
|
fill: true,
|
|
|
- tension: 0.4
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // 添加打赏收入数据集
|
|
|
- const tipData = incomeStats.value.totalTip || []
|
|
|
- datasets.push({
|
|
|
- label: '打赏收入',
|
|
|
- data: [...tipData],
|
|
|
- borderColor: '#10b981',
|
|
|
- backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
|
- fill: true,
|
|
|
- tension: 0.4
|
|
|
- })
|
|
|
-
|
|
|
- // 添加返佣收入数据集
|
|
|
- const commissionData = incomeStats.value.totalCommission || []
|
|
|
- datasets.push({
|
|
|
- label: '返佣收入',
|
|
|
- data: [...commissionData],
|
|
|
- borderColor: '#f59e0b',
|
|
|
- backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
|
- fill: true,
|
|
|
- tension: 0.4
|
|
|
- })
|
|
|
-
|
|
|
- // 如果没有有效数据集,跳过渲染
|
|
|
- if (datasets.length === 0) {
|
|
|
- console.warn('没有有效的数据集,跳过图表渲染')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 使用requestAnimationFrame确保DOM已更新
|
|
|
- requestAnimationFrame(() => {
|
|
|
+ tension: 0.3
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '打赏收入',
|
|
|
+ data: totalTip,
|
|
|
+ borderColor: '#10b981',
|
|
|
+ backgroundColor: 'rgba(16, 185, 129, 0.05)',
|
|
|
+ fill: true,
|
|
|
+ tension: 0.3
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '返佣收入',
|
|
|
+ data: totalCommission,
|
|
|
+ borderColor: '#f59e0b',
|
|
|
+ backgroundColor: 'rgba(245, 158, 11, 0.05)',
|
|
|
+ fill: true,
|
|
|
+ tension: 0.3
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 使用setTimeout确保DOM已更新
|
|
|
+ setTimeout(() => {
|
|
|
try {
|
|
|
- // 创建图表配置
|
|
|
- const config = {
|
|
|
- type: 'line',
|
|
|
- data: {
|
|
|
- labels: [...labels],
|
|
|
- datasets: datasets
|
|
|
- },
|
|
|
- options: {
|
|
|
- responsive: true,
|
|
|
- maintainAspectRatio: false,
|
|
|
- plugins: {
|
|
|
- legend: {
|
|
|
- position: 'top'
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- mode: 'index',
|
|
|
- intersect: false
|
|
|
- }
|
|
|
- },
|
|
|
- scales: {
|
|
|
- y: {
|
|
|
- beginAtZero: true
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 创建图表实例
|
|
|
+ // 创建图表
|
|
|
+ const config = createChartConfig(dates, datasets)
|
|
|
chartInstance.value = new Chart(ctx, config)
|
|
|
} catch (error) {
|
|
|
console.error('图表渲染失败:', error)
|
|
|
chartInstance.value = null
|
|
|
}
|
|
|
- })
|
|
|
+ }, 0)
|
|
|
}
|
|
|
|
|
|
// 监听团队选择变化
|
|
|
watch(selectedChartTeamId, () => {
|
|
|
+ // 先销毁当前图表,再加载新数据
|
|
|
+ destroyChart()
|
|
|
loadIncomeStats()
|
|
|
})
|
|
|
|
|
|
@@ -373,15 +377,16 @@ onMounted(async () => {
|
|
|
await teamStore.loadTeams()
|
|
|
}
|
|
|
await loadTeamStats()
|
|
|
- await loadIncomeStats()
|
|
|
+
|
|
|
+ // 延迟加载图表数据,确保DOM已渲染
|
|
|
+ setTimeout(() => {
|
|
|
+ loadIncomeStats()
|
|
|
+ }, 100)
|
|
|
})
|
|
|
|
|
|
// 组件卸载时清理图表实例
|
|
|
onUnmounted(() => {
|
|
|
- if (chartInstance.value) {
|
|
|
- chartInstance.value.destroy()
|
|
|
- chartInstance.value = null
|
|
|
- }
|
|
|
+ destroyChart()
|
|
|
})
|
|
|
</script>
|
|
|
|