184 lines
5.2 KiB
React
184 lines
5.2 KiB
React
import { useState, useEffect } from 'react'
|
||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
||
import { Spin } from 'antd'
|
||
import Markdown from 'react-markdown'
|
||
import { generateDiagnosisReport } from '../../services/analyticsService'
|
||
import './DiagnosisReport.css'
|
||
|
||
/**
|
||
* AI 诊断报告详情页
|
||
* - 从 URL query 读取 year、start、end
|
||
* - 调用 generateDiagnosisReport(命中缓存则秒出)
|
||
* - 用 react-markdown 渲染完整报告
|
||
*/
|
||
|
||
const TIER_COLOR = {
|
||
danger: '#7aa874',
|
||
on_target: '#5b8db8',
|
||
excellent: '#c0584f',
|
||
}
|
||
|
||
function DiagnosisReport() {
|
||
const [searchParams] = useSearchParams()
|
||
const navigate = useNavigate()
|
||
|
||
const year = Number(searchParams.get('year'))
|
||
const start = Number(searchParams.get('start'))
|
||
const end = Number(searchParams.get('end'))
|
||
|
||
const [report, setReport] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [regenerating, setRegenerating] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
|
||
const fetchReport = (force = false) => {
|
||
if (!year || !start || !end) {
|
||
setError('缺少必要参数(year / start / end)')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
if (force) {
|
||
setRegenerating(true)
|
||
} else {
|
||
setLoading(true)
|
||
}
|
||
setError(null)
|
||
|
||
generateDiagnosisReport({ year, ep_start: start, ep_end: end, force })
|
||
.then((data) => {
|
||
if (data.error) {
|
||
setError(data.error)
|
||
} else {
|
||
setReport(data)
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
setError(err?.response?.data?.detail || '报告生成失败')
|
||
})
|
||
.finally(() => {
|
||
setLoading(false)
|
||
setRegenerating(false)
|
||
})
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchReport(false)
|
||
}, [year, start, end])
|
||
|
||
// 摸高完成率
|
||
const stretchPct =
|
||
report?.avg_share && report?.highest
|
||
? null // 没有 stretch_target 直接信息,从 tier 推断
|
||
: null
|
||
|
||
// 从 report 中计算摸高完成率(如果有 avg_share 和 episode_count)
|
||
// 实际上后端没返回 stretch_target,所以这里用 pass_count / episode_count 做达标率展示
|
||
const passRate =
|
||
report?.episode_count > 0
|
||
? `${report.pass_count}/${report.episode_count}`
|
||
: '—'
|
||
|
||
const tierColor = report?.tier ? TIER_COLOR[report.tier] : '#999'
|
||
|
||
// 从 report_markdown 中提取第一句作为范围描述的补充
|
||
const startDate = report?.report_markdown
|
||
? '' // 已经在 header 中展示了期号范围
|
||
: ''
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="diagnosis-report-page">
|
||
<div className="diagnosis-report-loading">
|
||
<Spin size="large" />
|
||
<p>正在生成诊断报告,预计 15-30 秒…</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="diagnosis-report-page">
|
||
<span
|
||
className="diagnosis-report-back"
|
||
onClick={() => navigate('/analytics')}
|
||
>
|
||
← 返回收视分析
|
||
</span>
|
||
<div className="diagnosis-report-error">
|
||
<p>{error}</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="diagnosis-report-page">
|
||
{/* 返回链接 */}
|
||
<span
|
||
className="diagnosis-report-back"
|
||
onClick={() => navigate('/analytics')}
|
||
>
|
||
← 返回收视分析
|
||
</span>
|
||
|
||
{/* 头部 */}
|
||
<div className="diagnosis-report-header">
|
||
<h1>收视诊断报告</h1>
|
||
<p className="diagnosis-report-range">
|
||
第{start}期 — 第{end}期 | {year}年 | 共{report?.episode_count}期
|
||
</p>
|
||
{report?.generated_at && (
|
||
<p className="diagnosis-report-date">
|
||
生成时间:{new Date(report.generated_at).toLocaleString('zh-CN')}
|
||
{report?.model && ` | 模型:${report.model}`}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 指标条 */}
|
||
<div className="diagnosis-report-kpi-bar">
|
||
<div className="diagnosis-report-kpi-item">
|
||
<div className="kpi-value" style={{ color: tierColor }}>
|
||
{report?.avg_share?.toFixed(4) || '—'}
|
||
</div>
|
||
<div className="kpi-label">平均份额</div>
|
||
</div>
|
||
<div className="diagnosis-report-kpi-item">
|
||
<div className="kpi-value">{passRate}</div>
|
||
<div className="kpi-label">达标期数</div>
|
||
</div>
|
||
<div className="diagnosis-report-kpi-item">
|
||
<div className="kpi-value" style={{ color: '#c0584f' }}>
|
||
{report?.highest
|
||
? `${report.highest.share.toFixed(3)}(第${report.highest.ep}期)`
|
||
: '—'}
|
||
</div>
|
||
<div className="kpi-label">最高份额</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 报告正文 */}
|
||
<div className="diagnosis-report-body">
|
||
<Markdown>{report?.report_markdown || ''}</Markdown>
|
||
</div>
|
||
|
||
{/* 重新生成 */}
|
||
<div className="diagnosis-report-actions">
|
||
<button onClick={() => fetchReport(true)} disabled={regenerating}>
|
||
{regenerating ? '正在重新生成…' : '重新生成'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* 免责声明 */}
|
||
{report?.disclaimer && (
|
||
<p className="diagnosis-report-disclaimer">
|
||
⚠️ {report.disclaimer}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default DiagnosisReport |