Files
tps-dashboard/frontend/src/pages/Analytics/DiagnosisReport.jsx
T

184 lines
5.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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