feat: Dashboard 接真 API,题图渐变占位,排播占位,近9期收视柱图,颜色按年度目标判定

This commit is contained in:
simonkoson
2026-05-21 19:47:36 +08:00
parent 4eca31f8eb
commit e0ccfb20e7
2 changed files with 134 additions and 52 deletions
+107 -52
View File
@@ -1,32 +1,30 @@
import { Row, Col, Card, Avatar, Tooltip } from 'antd'
import { useState, useEffect } from 'react'
import { Row, Col, Card, Avatar, Tooltip, Button } from 'antd'
import {
BarChartOutlined,
EyeOutlined,
CalendarOutlined,
FireOutlined,
PictureOutlined,
} from '@ant-design/icons'
import useAuthStore from '../../stores/authStore'
import { listEpisodes } from '../../services/episodeService'
import { listTargets } from '../../services/yearlyTargetService'
import './Dashboard.css'
// 假数据:近 9 期收视(基础目标 0.6,摸高目标 0.9)
const fakeEpisodes = [
{ id: 1, title: '第 1247 期 现代防空反导大对决', editor: 'simonkoson', share: 0.72 },
{ id: 2, title: '第 1246 期 深海暗战', editor: 'simonkoson', share: 0.85 },
{ id: 3, title: '第 1245 期 隐形战场', editor: 'simonkoson', share: 0.94 },
{ id: 4, title: '第 1244 期 无人机革命', editor: 'simonkoson', share: 0.65 },
{ id: 5, title: '第 1243 期 核潜艇传奇', editor: 'simonkoson', share: 0.58 },
{ id: 6, title: '第 1242 期 电子战风暴', editor: 'simonkoson', share: 0.88 },
{ id: 7, title: '第 1241 期 太空博弈', editor: 'simonkoson', share: 0.91 },
{ id: 8, title: '第 1240 期 装甲对决', editor: 'simonkoson', share: 0.67 },
{ id: 9, title: '第 1239 期 隐身战机', editor: 'simonkoson', share: 0.55 },
]
const BASE_TARGET = 0.6
const STRETCH_TARGET = 0.9
function getBarColor(share) {
if (share > STRETCH_TARGET) return '#cf1322' // 红=优秀
if (share >= BASE_TARGET) return '#6b8e6b' // 蓝=达标
return '#d4a017' // 绿=待提升
/**
* 收视份额颜色判定(极易写反,禁止凭直觉):
* audience_share > stretch_target → 🔴 红(优秀)
* base_target ≤ audience_share ≤ stretch_target → 🔵 蓝(达标)
* audience_share < base_target → 🟢 绿(待提升)
* 判色取该期 air_date 所属年份的 yearly_targets 行(不是当前年)。
*/
function getShareColor(share, targets, airYear) {
const target = targets.find(t => t.year === airYear)
if (!target) return '#999'
if (share > target.stretch_target) return '#cf1322' // 红=优秀
if (share >= target.base_target) return '#6b8e6b' // 蓝=达标
return '#d4a017' // 绿=待提升
}
function getBarHeight(share) {
@@ -38,16 +36,63 @@ function getShortTitle(title) {
}
function Dashboard() {
const { user } = useAuthStore()
const [episodes, setEpisodes] = useState([])
const [targets, setTargets] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.all([
listEpisodes(limit = 9),
listTargets(),
]).then(([epData, tgtData]) => {
// 按 air_date 倒序取前 9
const sorted = (epData || []).sort((a, b) => new Date(b.air_date) - new Date(a.air_date)).slice(0, 9)
setEpisodes(sorted)
setTargets(tgtData || [])
setLoading(false)
}).catch(() => {
setLoading(false)
})
}, [])
// 显示最近有份额数据的期次(最多9个,不够用真实数据补位)
const hasShare = episodes.filter(e => e.audience_share != null)
const displayEpisodes = hasShare.length >= 5 ? hasShare.slice(0, 9) : episodes.slice(0, Math.max(9, hasShare.length || 5))
const bestEpisode = [...displayEpisodes].filter(e => e.audience_share != null).sort((a, b) => b.audience_share - a.audience_share)[0]
const showChangeCoverBtn = user?.role === 'zhipianren' || user?.role === 'zebian'
return (
<div className="dashboard">
{/* 顶部 Banner */}
<div className="dashboard-banner">
<div className="banner-text">
<h2>本月收视最佳</h2>
<p> 1245 隐形战场 · 收视份额 0.94</p>
{bestEpisode ? (
<p>
{bestEpisode.episode_number} {bestEpisode.program_name} ·
收视份额 {bestEpisode.audience_share}
</p>
) : (
<p>暂无收视数据</p>
)}
</div>
{/* 题图渐变占位 */}
<div className="banner-image-placeholder">
<span>题图位 · Phase 2 实装</span>
<div className="banner-placeholder-gradient" />
{showChangeCoverBtn && (
<Button
size="small"
icon={<PictureOutlined />}
disabled
className="change-cover-btn"
>
更换题图
</Button>
)}
<span className="placeholder-label">敬请期待</span>
</div>
</div>
@@ -59,8 +104,8 @@ function Dashboard() {
<EyeOutlined style={{ color: '#6b8e6b', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">0.82</span>
<span className="kpi-label">本期平均收视</span>
<span className="kpi-value">{displayEpisodes.filter(e => e.audience_share).length || '--'}</span>
<span className="kpi-label"> 9 期已录</span>
</div>
</Card>
</Col>
@@ -70,8 +115,10 @@ function Dashboard() {
<FireOutlined style={{ color: '#ff9800', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">98.6%</span>
<span className="kpi-label">完成率</span>
<span className="kpi-value">
{bestEpisode ? (bestEpisode.audience_share * 100).toFixed(1) + '%' : '--'}
</span>
<span className="kpi-label">最佳收视份额</span>
</div>
</Card>
</Col>
@@ -81,8 +128,8 @@ function Dashboard() {
<CalendarOutlined style={{ color: '#2196f3', fontSize: 24 }} />
</div>
<div className="kpi-info">
<span className="kpi-value">6</span>
<span className="kpi-label">未来待排期</span>
<span className="kpi-value">{targets.length || '--'}</span>
<span className="kpi-label">年度目标条数</span>
</div>
</Card>
</Col>
@@ -95,35 +142,43 @@ function Dashboard() {
<Card className="content-card" title="热点雷达">
<div className="placeholder-text">
<BarChartOutlined style={{ fontSize: 32, color: '#ccc', marginBottom: 8 }} />
<p>模块 H · Phase 4c 实施</p>
<small>热点雷达将在 Phase 4c 开发作为编导个人首页核心功能</small>
<p>热点雷达 · Phase 4c</p>
<small>编导个人首页核心功能Phase 4c 实施</small>
</div>
</Card>
</Col>
{/* 近 9 期收视柱图 */}
<Col span={8}>
<Card className="content-card" title="近 9 期收视">
<Card className="content-card" title="近 9 期收视" loading={loading}>
<div className="chart-container">
<div className="bars-wrapper">
{fakeEpisodes.map((ep) => (
<div key={ep.id} className="bar-item">
<Tooltip title={`${ep.title} · ${ep.share}`}>
<div
className="bar"
style={{
height: getBarHeight(ep.share),
backgroundColor: getBarColor(ep.share),
}}
/>
</Tooltip>
<div className="bar-label-top">{getShortTitle(ep.title)}</div>
<div className="bar-label-bottom">
<Avatar size={20} style={{ fontSize: 10 }}>{ep.editor[0]}</Avatar>
<span>{ep.editor}</span>
{displayEpisodes.map((ep) => {
const airYear = new Date(ep.air_date).getFullYear()
const color = ep.audience_share != null
? getShareColor(ep.audience_share, targets, airYear)
: '#ccc'
return (
<div key={ep.id} className="bar-item">
<Tooltip title={`${ep.program_name} · ${ep.audience_share ?? '无数据'}`}>
<div
className="bar"
style={{
height: ep.audience_share != null ? getBarHeight(ep.audience_share) : 20,
backgroundColor: color,
}}
/>
</Tooltip>
<div className="bar-label-top">{getShortTitle(ep.program_name)}</div>
<div className="bar-label-bottom">
<Avatar size={20} style={{ fontSize: 10 }}>
{(ep.editor_name_snapshot || '?')[0]}
</Avatar>
<span>{ep.editor_name_snapshot || '未知'}</span>
</div>
</div>
</div>
))}
)
})}
</div>
<div className="chart-legend">
<span className="legend-item"><span className="dot" style={{ background: '#cf1322' }}></span>=优秀</span>
@@ -136,11 +191,11 @@ function Dashboard() {
{/* 排播计划 */}
<Col span={8}>
<Card className="content-card" title="排播计划">
<Card className="content-card" title="未来排播计划">
<div className="placeholder-text">
<CalendarOutlined style={{ fontSize: 32, color: '#ccc', marginBottom: 8 }} />
<p>模块 F · Phase 4b 实施</p>
<small>甘特图排期将在 Phase 4b 开发使用 frappe-gantt 实现拖拽排期</small>
<p>排播计划 · Phase 4b</p>
<small>甘特图排期使用 frappe-gantt 实现Phase 4b 开发</small>
</div>
</Card>
</Col>