feat: Add dashboard statistics endpoint and initial frontend application for managing companies and reports.

This commit is contained in:
24024 2026-01-23 00:15:38 +08:00
parent 200178ebcc
commit 79d6cedaf1
6 changed files with 75 additions and 30 deletions

View File

@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
from app.config import settings
from app.database import init_db
from app.routers import companies, reports, analysis, scheduler
from app.routers import companies, reports, analysis, scheduler, stats
from app.services.scheduler_service import scheduler_service
from app.utils.logger import logger
@ -55,6 +55,7 @@ app.include_router(companies.router)
app.include_router(reports.router)
app.include_router(analysis.router)
app.include_router(scheduler.router)
app.include_router(stats.router)
# 挂载静态文件 (前端)
# 确保frontend目录存在

Binary file not shown.

View File

@ -0,0 +1,42 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import Company, Report, AnalysisResult
router = APIRouter(prefix="/api/stats", tags=["统计数据"])
@router.get("/dashboard")
async def get_dashboard_stats(db: AsyncSession = Depends(get_db)):
"""获取仪表盘统计数据"""
# 统计公司数
company_count = await db.scalar(select(func.count(Company.id)))
# 统计报告数
report_count = await db.scalar(select(func.count(Report.id)))
# 统计已分析数
analyzed_count = await db.scalar(select(func.count(Report.id)).where(Report.is_analyzed == True))
# 获取最新的一条分析完成的报告
stmt = select(Report).where(Report.is_analyzed == True).order_by(desc(Report.updated_at)).limit(1).options(selectinload(Report.company))
result = await db.execute(stmt)
latest_report = result.scalar_one_or_none()
latest_insight = None
if latest_report:
latest_insight = {
"id": latest_report.id,
"title": f"{latest_report.company.short_name or latest_report.company.company_name} - {latest_report.title}",
"date": latest_report.updated_at.strftime("%Y/%m/%d")
}
return {
"company_count": company_count or 0,
"report_count": report_count or 0,
"analyzed_count": analyzed_count or 0,
"latest_insight": latest_insight
}

View File

@ -1 +1,7 @@
2026-01-23 00:00:32 | WARNING | app.services.ai_analyzer:call_ai:77 - AI调用异常: Server disconnected without sending a response.2秒后重试...
2026-01-23 00:11:08 | INFO | app.main:lifespan:34 - 应用正在关闭...
2026-01-23 00:11:08 | INFO | app.services.scheduler_service:stop:49 - 定时任务调度器已停止
2026-01-23 00:11:25 | INFO | app.main:lifespan:21 - 正在启动 眼镜行业情报分析系统...
2026-01-23 00:11:39 | INFO | app.main:lifespan:21 - 正在启动 眼镜行业情报分析系统...
2026-01-23 00:11:39 | INFO | app.main:lifespan:25 - 数据库已初始化
2026-01-23 00:11:39 | INFO | app.services.scheduler_service:start:42 - 定时任务调度器已启动,间隔: 24小时

View File

@ -252,44 +252,40 @@ const app = {
// 获取仪表盘数据
async fetchDashboard() {
try {
// 获取统计数据
const [companies, reports, logs] = await Promise.all([
fetch(`${API_BASE}/companies`).then(r => r.json()),
fetch(`${API_BASE}/reports?page_size=5`).then(r => r.json()),
// 获取统计数据和日志
const [stats, logs] = await Promise.all([
fetch(`${API_BASE}/stats/dashboard`).then(r => r.json()),
fetch(`${API_BASE}/scheduler/logs`).then(r => r.json())
]);
// 更新计数
document.getElementById('stat-companies').textContent = companies.length;
document.getElementById('stat-reports').textContent = reports.length; // 实际应该请求总数API
// 简单估算已分析数实际应从API获取
const analyzedCount = reports.filter(r => r.is_analyzed).length;
document.getElementById('stat-analyzed').textContent = analyzedCount;
document.getElementById('stat-companies').textContent = stats.company_count;
document.getElementById('stat-reports').textContent = stats.report_count;
document.getElementById('stat-analyzed').textContent = stats.analyzed_count;
// 渲染最新洞察
const insightsContainer = document.getElementById('recent-insights-list');
insightsContainer.innerHTML = '';
reports.slice(0, 5).forEach(report => {
if (report.is_analyzed) {
if (stats.latest_insight) {
const report = stats.latest_insight;
const el = document.createElement('div');
el.className = 'insight-item card';
el.style.marginBottom = '10px';
el.style.padding = '12px';
el.style.padding = '16px';
el.style.borderLeft = '4px solid #3b82f6';
el.innerHTML = `
<div style="font-weight:600;font-size:0.9rem">${report.company_name} - ${report.title}</div>
<div style="font-size:0.8rem;color:#64748b;margin-top:4px">
AI分析已完成 ${new Date(report.created_at).toLocaleDateString()}
<div style="font-weight:600;font-size:1rem;margin-bottom:8px">${report.title}</div>
<div style="font-size:0.85rem;color:#64748b;margin-bottom:12px">
<i class="fas fa-check-circle" style="color:#10b981"></i> AI ${report.date}
</div>
<button class="btn-xs" style="margin-top:8px" onclick="app.showReportDetail(${report.id})">查看详情</button>
<button class="btn-xs" onclick="app.showReportDetail(${report.id}); return false;">
<i class="fas fa-eye"></i>
</button>
`;
insightsContainer.appendChild(el);
}
});
if (insightsContainer.children.length === 0) {
insightsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px">暂无分析结果</div>';
} else {
insightsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;border:1px dashed #eee;border-radius:8px">暂无分析结果</div>';
}
// 渲染日志