feat(report): ship daily report v1 experience
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user