feat: Add dashboard statistics endpoint and initial frontend application for managing companies and reports.
This commit is contained in:
parent
200178ebcc
commit
79d6cedaf1
Binary file not shown.
@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
|
|||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import init_db
|
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.services.scheduler_service import scheduler_service
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
|
|
||||||
@ -55,6 +55,7 @@ app.include_router(companies.router)
|
|||||||
app.include_router(reports.router)
|
app.include_router(reports.router)
|
||||||
app.include_router(analysis.router)
|
app.include_router(analysis.router)
|
||||||
app.include_router(scheduler.router)
|
app.include_router(scheduler.router)
|
||||||
|
app.include_router(stats.router)
|
||||||
|
|
||||||
# 挂载静态文件 (前端)
|
# 挂载静态文件 (前端)
|
||||||
# 确保frontend目录存在
|
# 确保frontend目录存在
|
||||||
|
|||||||
BIN
backend/app/routers/__pycache__/stats.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/stats.cpython-312.pyc
Normal file
Binary file not shown.
42
backend/app/routers/stats.py
Normal file
42
backend/app/routers/stats.py
Normal 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
|
||||||
|
}
|
||||||
@ -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: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小时
|
||||||
|
|||||||
@ -252,44 +252,40 @@ const app = {
|
|||||||
// 获取仪表盘数据
|
// 获取仪表盘数据
|
||||||
async fetchDashboard() {
|
async fetchDashboard() {
|
||||||
try {
|
try {
|
||||||
// 获取统计数据
|
// 获取统计数据和日志
|
||||||
const [companies, reports, logs] = await Promise.all([
|
const [stats, logs] = await Promise.all([
|
||||||
fetch(`${API_BASE}/companies`).then(r => r.json()),
|
fetch(`${API_BASE}/stats/dashboard`).then(r => r.json()),
|
||||||
fetch(`${API_BASE}/reports?page_size=5`).then(r => r.json()),
|
|
||||||
fetch(`${API_BASE}/scheduler/logs`).then(r => r.json())
|
fetch(`${API_BASE}/scheduler/logs`).then(r => r.json())
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 更新计数
|
// 更新计数
|
||||||
document.getElementById('stat-companies').textContent = companies.length;
|
document.getElementById('stat-companies').textContent = stats.company_count;
|
||||||
document.getElementById('stat-reports').textContent = reports.length; // 实际应该请求总数API
|
document.getElementById('stat-reports').textContent = stats.report_count;
|
||||||
|
document.getElementById('stat-analyzed').textContent = stats.analyzed_count;
|
||||||
// 简单估算已分析数(实际应从API获取)
|
|
||||||
const analyzedCount = reports.filter(r => r.is_analyzed).length;
|
|
||||||
document.getElementById('stat-analyzed').textContent = analyzedCount;
|
|
||||||
|
|
||||||
// 渲染最新洞察
|
// 渲染最新洞察
|
||||||
const insightsContainer = document.getElementById('recent-insights-list');
|
const insightsContainer = document.getElementById('recent-insights-list');
|
||||||
insightsContainer.innerHTML = '';
|
insightsContainer.innerHTML = '';
|
||||||
|
|
||||||
reports.slice(0, 5).forEach(report => {
|
if (stats.latest_insight) {
|
||||||
if (report.is_analyzed) {
|
const report = stats.latest_insight;
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'insight-item card';
|
el.className = 'insight-item card';
|
||||||
el.style.marginBottom = '10px';
|
el.style.marginBottom = '10px';
|
||||||
el.style.padding = '12px';
|
el.style.padding = '16px';
|
||||||
el.innerHTML = `
|
el.style.borderLeft = '4px solid #3b82f6';
|
||||||
<div style="font-weight:600;font-size:0.9rem">${report.company_name} - ${report.title}</div>
|
el.innerHTML = `
|
||||||
<div style="font-size:0.8rem;color:#64748b;margin-top:4px">
|
<div style="font-weight:600;font-size:1rem;margin-bottom:8px">${report.title}</div>
|
||||||
AI分析已完成 • ${new Date(report.created_at).toLocaleDateString()}
|
<div style="font-size:0.85rem;color:#64748b;margin-bottom:12px">
|
||||||
</div>
|
<i class="fas fa-check-circle" style="color:#10b981"></i> AI分析已完成 • ${report.date}
|
||||||
<button class="btn-xs" style="margin-top:8px" onclick="app.showReportDetail(${report.id})">查看详情</button>
|
</div>
|
||||||
`;
|
<button class="btn-xs" onclick="app.showReportDetail(${report.id}); return false;">
|
||||||
insightsContainer.appendChild(el);
|
<i class="fas fa-eye"></i> 查看详情
|
||||||
}
|
</button>
|
||||||
});
|
`;
|
||||||
|
insightsContainer.appendChild(el);
|
||||||
if (insightsContainer.children.length === 0) {
|
} else {
|
||||||
insightsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px">暂无分析结果</div>';
|
insightsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;border:1px dashed #eee;border-radius:8px">暂无分析结果</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染日志
|
// 渲染日志
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user