import { useMemo } from 'react' import { Empty } from 'antd' import ReactECharts from 'echarts-for-react' import * as echarts from 'echarts' /** * 题材对比 — 饼图(期数占比)+ 水平柱图(按平均份额排名) * 右列通高,对齐 l3_report.html 原型 */ const TOPIC_COLORS = { '装备深解': '#5b8db8', '历史纵深': '#e8a838', '前沿科技': '#7aa874', '横切类比': '#9b7eb8', '人物牵引': '#d4816b', '事件战例': '#9b7eb8', } /** * 将 hex 颜色转为 rgba 字符串 */ function hexToRgba(hex, alpha) { const h = hex.replace('#', '') const r = parseInt(h.substring(0, 2), 16) const g = parseInt(h.substring(2, 4), 16) const b = parseInt(h.substring(4, 6), 16) return `rgba(${r},${g},${b},${alpha})` } function TopicCompare({ episodes }) { // 过滤有 program_format 的期次 const validEpisodes = useMemo(() => { if (!episodes || !episodes.length) return [] return episodes.filter((ep) => ep.program_format != null && ep.program_format !== '') }, [episodes]) // 按 program_format 分组统计 const topicStats = useMemo(() => { if (!validEpisodes.length) return null const groupMap = {} validEpisodes.forEach((ep) => { const format = ep.program_format if (!groupMap[format]) { groupMap[format] = [] } groupMap[format].push(ep) }) return Object.entries(groupMap).map(([name, eps]) => { const count = eps.length const withShare = eps.filter((ep) => ep.audience_share != null) const avgShare = withShare.length > 0 ? withShare.reduce((sum, ep) => sum + Number(ep.audience_share), 0) / withShare.length : 0 return { name, count, avgShare: Number(avgShare.toFixed(4)), } }) }, [validEpisodes]) // 饼图配置 const pieOption = useMemo(() => { if (!topicStats || !topicStats.length) return null // 按期数排序 const sorted = [...topicStats].sort((a, b) => b.count - a.count) const data = sorted.map((t) => ({ name: t.name, value: t.count })) const colorArr = sorted.map((t) => TOPIC_COLORS[t.name] || '#999') return { tooltip: { trigger: 'item', formatter: (params) => { return params.name + '
期数:' + params.value + ' 期(' + params.percent + '%)' }, }, series: [ { type: 'pie', radius: ['40%', '70%'], center: ['50%', '55%'], data, color: colorArr, label: { fontSize: 11, formatter: '{b}\n{c}期 ({d}%)', }, emphasis: { label: { fontSize: 14, fontWeight: 'bold' }, }, }, ], } }, [topicStats]) // 水平柱图配置 const barOption = useMemo(() => { if (!topicStats || !topicStats.length) return null // 按平均份额升序排列(最低在下,最高在上) const sorted = [...topicStats].sort((a, b) => a.avgShare - b.avgShare) const yData = sorted.map((t) => t.name) const countMap = {} sorted.forEach((t) => { countMap[t.name] = t.count }) // 每根柱子渐变填充 + 胶囊圆角,对齐原型 LinearGradient 写法 const barData = sorted.map((t) => { const color = TOPIC_COLORS[t.name] || '#999' return { value: t.avgShare, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: hexToRgba(color, 0.15) }, { offset: 1, color }, ]), borderRadius: [0, 10, 10, 0], }, } }) // x 轴范围 const values = sorted.map((t) => t.avgShare) const dataMin = Math.min(...values) const dataMax = Math.max(...values) const xMin = Math.max(0, Math.floor((dataMin - 0.1) * 10) / 10) const xMax = Math.ceil((dataMax + 0.1) * 10) / 10 return { tooltip: { trigger: 'axis', formatter: (params) => { const name = params[0].name const count = countMap[name] || 0 return name + '
平均份额:' + Number(params[0].value).toFixed(3) + '
期数:' + count }, }, grid: { left: 80, right: 30, top: 10, bottom: 30 }, xAxis: { type: 'value', min: xMin, max: xMax, }, yAxis: { type: 'category', data: yData, axisLabel: { fontSize: 12 }, }, series: [ { type: 'bar', data: barData, barWidth: '50%', label: { show: true, position: 'right', fontSize: 11, formatter: (p) => p.value.toFixed(3), }, }, ], } }, [topicStats]) if (!validEpisodes.length) { return } return (
) } export default TopicCompare