feat: 添加 L4 AI 诊断报告(后端 DeepSeek 端点 + 前端摘要块与详情页)
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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
|
||||
Reference in New Issue
Block a user