import { useState, useEffect } from 'react' import { Row, Col, Card, Avatar, Tooltip, Button, Spin } from 'antd' import { EyeOutlined, FireOutlined, CalendarOutlined, BarChartOutlined, PictureOutlined, UploadOutlined, LineChartOutlined, AimOutlined, } from '@ant-design/icons' import useAuthStore from '../../stores/authStore' import { listEpisodes } from '../../services/episodeService' import { listTargets } from '../../services/yearlyTargetService' import { getCurrentCover, getUpcomingSchedules } from '../../services/dashboardService' import ChangeCoverModal from './ChangeCoverModal' import './Dashboard.css' /** * 收视份额颜色判定(极易写反,禁止凭直觉): * audience_share > stretch_target → 🔴 红(优秀) * base_target ≤ audience_share ≤ stretch_target → 🔵 蓝(达标) * audience_share < base_target → 🟢 绿(待提升) * 判色取该期 air_date 所属年份的 yearly_targets 行(不是当前年)。 */ function getShareColor(share, targets, airYear) { const n = Number(share) const target = (targets || []).find(t => Number(t.year) === Number(airYear)) if (!target) return '#999' if (isNaN(n)) return '#999' const stretch = Number(target.stretch_target) const base = Number(target.base_target) if (n > stretch) return '#c0584f' // 红=超过摸高目标(优秀) if (n >= base) return '#5b8db8' // 蓝=未达摸高目标(达标) return '#7aa874' // 绿=未达基础目标(待提升) } function getColorLabel(share, targets, airYear) { const n = Number(share) const target = (targets || []).find(t => Number(t.year) === Number(airYear)) if (!target) return '未知' const stretch = Number(target.stretch_target) const base = Number(target.base_target) if (n > stretch) return '优秀' if (n >= base) return '达标' return '待提升' } function getBarHeight(share) { return Math.round((Number(share) / 1.0) * 160) } function getShortTitle(title) { if (!title) return '?' return title.length > 9 ? title.slice(0, 9) + '...' : title } /** * 计算当年完成率(纯前端,量纲已确认为 0~1 小数直接相除) * - "当年"= 当前自然年(new Date().getFullYear()) * - avg(audience_share) 当年已录期 ÷ base/stretch_target * - 无数据时始终返回 { base: '—', stretch: '—' } * - 完成率可 >100% 正常显示 */ function calcCompletionRate(episodes, targets) { const yearInt = new Date().getFullYear() const yearEpisodes = episodes.filter(e => { const airYear = new Date(e.air_date).getFullYear() return airYear === yearInt && e.audience_share != null }) const target = (targets || []).find(t => Number(t.year) === yearInt) if (!yearEpisodes.length || !target) return { base: '—', stretch: '—' } const avgShare = yearEpisodes.reduce((sum, e) => sum + Number(e.audience_share), 0) / yearEpisodes.length const baseRate = avgShare / Number(target.base_target) const stretchRate = avgShare / Number(target.stretch_target) return { base: (baseRate * 100).toFixed(1) + '%', stretch: (stretchRate * 100).toFixed(1) + '%', } } function Dashboard() { const { user } = useAuthStore() const [episodes, setEpisodes] = useState([]) const [targets, setTargets] = useState([]) const [cover, setCover] = useState({ cover_path: null, episode_number: null, episode_title: null }) const [schedules, setSchedules] = useState([]) const [loading, setLoading] = useState(true) const [coverModalOpen, setCoverModalOpen] = useState(false) const showChangeCoverBtn = user?.role === 'zhipianren' || user?.role === 'zebian' useEffect(() => { Promise.all([ listEpisodes(50), listTargets(), getCurrentCover(), getUpcomingSchedules(6), ]).then(([epData, tgtData, coverData, schedData]) => { const sorted = (epData || []).sort((a, b) => new Date(b.air_date) - new Date(a.air_date)) setEpisodes(sorted) setTargets(tgtData || []) setCover(coverData || { cover_path: null, episode_number: null, episode_title: null }) setSchedules(Array.isArray(schedData) ? schedData : []) setLoading(false) }).catch(() => { setLoading(false) }) }, []) // 显示最近有份额数据的期次(最多12个) const displayEpisodes = episodes .filter(e => e.audience_share != null) .slice(0, 12) const bestEpisode = [...displayEpisodes].sort((a, b) => Number(b.audience_share) - Number(a.audience_share))[0] const handleCoverUploaded = (newCover) => { setCover(newCover) setCoverModalOpen(false) } // KPI 数据(扩为 5 项:原有 3 项 + 年度完成率 2 项) const completion = calcCompletionRate(episodes, targets) const kpiData = [ { icon: , bg: '#e8f5e9', label: '近12期已录', value: displayEpisodes.length || '--', }, { icon: , bg: '#fff3e0', label: '最佳份额', value: bestEpisode ? (Number(bestEpisode.audience_share) * 100).toFixed(2) + '%' : '--', }, { icon: , bg: '#e3f2fd', label: '年度目标', value: targets.length || '--', }, { icon: , bg: '#f3e5f5', label: '基础目标完成率', value: completion.base, }, { icon: , bg: '#fce4ec', label: '摸高目标完成率', value: completion.stretch, }, ] return (
{/* ===== Banner ===== */}
{/* 题图底图 或 默认渐变 */} {cover.cover_path ? ( 题图 ) : (
)} {/* 暗化渐变层 */}
{/* 右上角 Logo */} 栏目logo { e.target.style.display = 'none' }} /> {/* 左侧文字 */}

本月收视最佳·题图

{cover.episode_number ? `第 ${cover.episode_number} 期` : bestEpisode ? `第 ${bestEpisode.episode_number} 期` : '暂无收视数据'} {cover.episode_title || (bestEpisode ? ` · ${bestEpisode.program_name}` : '')}

{bestEpisode ? `收视份额 ${Number(bestEpisode.audience_share).toFixed(4)} · 当月最高` : '暂无收视数据'}

建议:可选用近期收视表现好的节目海报

{/* 右下角换图按钮 */} {showChangeCoverBtn && ( )}
{/* ===== KPI 细条 ===== */}
{kpiData.map((item, i) => (
{item.icon}

{item.label}

{item.value}

))}
{/* ===== 近9期收视柱图(全宽) ===== */}

近 12 期收视份额

悬浮看编导/期次/收视率·颜色只对收视份额
{loading ? (
加载中...
) : ( <>
{displayEpisodes.map((ep) => { const airYear = new Date(ep.air_date).getFullYear() const color = ep.audience_share != null ? getShareColor(ep.audience_share, targets, airYear) : '#ccc' const colorLabel = ep.audience_share != null ? getColorLabel(ep.audience_share, targets, airYear) : '无数据' const tooltipContent = ` 第 ${ep.episode_number} 期 · ${ep.program_name}
编导:${ep.editor_name_snapshot || '未知'}
收视份额:${ep.audience_share != null ? Number(ep.audience_share).toFixed(4) : '无数据'}(${colorLabel})
收视率:${ep.audience_rating != null ? Number(ep.audience_rating).toFixed(4) : '无数据'} ` return (
{ep.audience_share != null ? Number(ep.audience_share).toFixed(2) : '--'}
}>
{getShortTitle(ep.program_name)}
) })}
绿=未达基础目标(待提升) 蓝=未达摸高目标(达标) 红=超过摸高目标(优秀)
)}
{/* ===== 下方两栏 ===== */}
{/* 左:雷达占位 */}

军事科技热点雷达

Phase 4c·占位
1. 神舟飞船对接空间站
2. 新型无人机集群技术

真实抓取 Phase 4c 实现

编导个人首页核心功能
{/* 右:排播计划 */}

未来排播计划

{loading ? (
) : schedules.length > 0 ? (
    {schedules.map((item, i) => (
  • 第{item.episode_number}期 {item.program_name} {item.planned_air_date} {item.editor_name_snapshot || ''}
  • ))}
) : (

暂无排播数据

)}
{/* 换图 Modal */} setCoverModalOpen(false)} onUploaded={handleCoverUploaded} episodes={episodes.slice(0, 20)} />
) } export default Dashboard