feat(report): ship daily report v1 experience

This commit is contained in:
phamnazage-jpg
2026-05-13 20:13:02 +08:00
parent 6a2cd3f159
commit 85f37a4d95
13 changed files with 3541 additions and 565 deletions

View File

@@ -200,6 +200,150 @@
background: #fff;
}
.report-section {
margin-bottom: 24px;
padding: 20px;
border: 1px solid #bfdbfe;
border-radius: 12px;
background: linear-gradient(180deg, #eff6ff 0%, #ffffff 100%);
}
.report-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.report-header h3 {
margin: 0;
color: #111827;
}
.report-header p {
margin: 6px 0 0;
color: #6b7280;
font-size: 14px;
}
.report-status {
display: inline-flex;
align-items: center;
padding: 8px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
text-transform: lowercase;
}
.report-status-generated {
background: #dbeafe;
color: #1d4ed8;
}
.report-status-other {
background: #e5e7eb;
color: #374151;
}
.report-card {
padding: 18px;
border: 1px solid #dbeafe;
border-radius: 18px;
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.10), transparent 35%),
rgba(255, 255, 255, 0.94);
box-shadow: 0 12px 30px rgba(37, 99, 235, 0.08);
}
.report-hero {
padding: 18px;
border-radius: 16px;
background: linear-gradient(135deg, #123c63 0%, #24507a 100%);
color: #ffffff;
margin-bottom: 16px;
}
.report-eyebrow {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.78;
margin-bottom: 8px;
}
.report-summary {
font-size: 22px;
line-height: 1.35;
font-weight: 800;
}
.report-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 14px;
color: #4b5563;
font-size: 14px;
}
.report-highlights {
display: grid;
gap: 10px;
margin-bottom: 14px;
}
.report-highlight {
display: grid;
gap: 4px;
padding: 12px 14px;
border-radius: 12px;
background: rgba(239, 246, 255, 0.92);
border: 1px solid #dbeafe;
}
.report-highlight strong {
color: #1e3a8a;
font-size: 13px;
}
.report-highlight span {
color: #475569;
font-size: 14px;
}
.report-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.report-link {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 14px;
border: 1px solid #93c5fd;
border-radius: 8px;
color: #1d4ed8;
text-decoration: none;
font-weight: 600;
background: #ffffff;
}
.report-link-primary {
background: #2563eb;
border-color: #2563eb;
color: #ffffff;
}
.report-note {
margin-top: 12px;
color: #6b7280;
font-size: 13px;
}
.subscription-section {
padding: 20px;
border: 1px solid #fde68a;
@@ -302,6 +446,22 @@
flex-direction: column;
}
.report-header {
flex-direction: column;
}
.report-summary {
font-size: 24px;
}
.report-actions {
flex-direction: column;
}
.report-link {
width: 100%;
}
.subscription-summary {
white-space: normal;
}

View File

@@ -12,6 +12,46 @@ import {
type SubscriptionPlan,
} from '../lib/models'
type LatestReport = {
reportDate: string
status: string
modelCount: number
summaryMD: string
markdownUrl: string
htmlUrl: string
updatedAt: string
}
function formatLocalReportDate(date: Date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildFallbackLatestReport(): LatestReport {
const reportDate = formatLocalReportDate(new Date())
return {
reportDate,
status: 'generated',
modelCount: 0,
summaryMD: '最新日报入口可用,后端元数据暂未返回摘要。',
markdownUrl: `/reports/daily/daily_report_${reportDate}.md`,
htmlUrl: `/reports/daily/html/daily_report_${reportDate}.html`,
updatedAt: '',
}
}
function summarizeLatestReport(report: LatestReport) {
if (report.summaryMD.trim()) {
return report.summaryMD.trim()
}
if (report.modelCount > 0) {
return `最新日报已生成,覆盖 ${report.modelCount} 个模型,可直接查看完整 HTML 页面。`
}
return '最新日报入口已准备好,可直接打开 HTML 或 Markdown 查看。'
}
function Dashboard() {
const chartRef = useRef<HTMLDivElement>(null)
const [modelCount, setModelCount] = useState(0)
@@ -20,6 +60,8 @@ function Dashboard() {
const [subscriptionPlans, setSubscriptionPlans] = useState<SubscriptionPlan[]>([])
const [planCount, setPlanCount] = useState(0)
const [planMinPrice, setPlanMinPrice] = useState(0)
const [latestReport, setLatestReport] = useState<LatestReport | null>(null)
const [reportFallback, setReportFallback] = useState(false)
useEffect(() => {
let chart: echarts.ECharts | null = null
@@ -108,8 +150,40 @@ function Dashboard() {
}
}
const loadLatestReport = async () => {
try {
const response = await fetch('/api/v1/reports/latest')
if (!response.ok) {
throw new Error(`latest report request failed: ${response.status}`)
}
const payload = await response.json()
const report = payload?.data
if (!report?.reportDate || !report?.htmlUrl || !report?.markdownUrl) {
throw new Error('latest report payload invalid')
}
if (!disposed) {
setLatestReport({
reportDate: report.reportDate,
status: report.status || 'generated',
modelCount: Number(report.modelCount || 0),
summaryMD: report.summaryMD || '',
markdownUrl: report.markdownUrl,
htmlUrl: report.htmlUrl,
updatedAt: report.updatedAt || '',
})
setReportFallback(false)
}
} catch {
if (!disposed) {
setLatestReport(buildFallbackLatestReport())
setReportFallback(true)
}
}
}
void loadModels()
void loadSubscriptionPlans()
void loadLatestReport()
return () => {
disposed = true
@@ -141,6 +215,55 @@ function Dashboard() {
<div className="chart-container">
<div ref={chartRef} style={{ width: '100%', height: '400px' }} />
</div>
<section className="report-section">
<div className="report-header">
<div>
<h3>📰 </h3>
<p>线</p>
</div>
{latestReport && (
<span className={`report-status ${latestReport.status === 'generated' ? 'report-status-generated' : 'report-status-other'}`}>
{latestReport.status}
</span>
)}
</div>
{latestReport ? (
<div className="report-card">
<div className="report-hero">
<div className="report-eyebrow"></div>
<div className="report-summary">{summarizeLatestReport(latestReport)}</div>
</div>
<div className="report-meta">
<span> {latestReport.reportDate}</span>
{latestReport.modelCount > 0 && <span>{latestReport.modelCount} </span>}
{latestReport.updatedAt && <span> {latestReport.updatedAt}</span>}
</div>
<div className="report-highlights">
<div className="report-highlight">
<strong></strong>
<span> HTML Markdown </span>
</div>
<div className="report-highlight">
<strong></strong>
<span></span>
</div>
</div>
<div className="report-actions">
<a className="report-link report-link-primary" href={latestReport.htmlUrl} target="_blank" rel="noreferrer">
HTML
</a>
<a className="report-link" href={latestReport.markdownUrl} target="_blank" rel="noreferrer">
Markdown
</a>
</div>
{reportFallback && (
<div className="report-note">使退</div>
)}
</div>
) : (
<div className="subscription-empty"></div>
)}
</section>
<section className="subscription-section">
<div className="subscription-header">
<div>