feat: 优化页面信息展示

This commit is contained in:
meijiali
2026-07-03 18:20:20 +08:00
parent 9244f360be
commit 2dc8ca1b5b
9 changed files with 163 additions and 18 deletions
+5
View File
@@ -34,6 +34,11 @@ class TaskResponse(BaseModel):
current_stage: str | None = None
current_stage_label: str | None = None
last_progress_at: datetime | None = None
display_number: int = 0
display_id: str = ""
created_at_label: str = ""
scale_label: str = ""
error_summary: str | None = None
running_seconds: int = 0
seconds_since_last_progress: int | None = None
running_duration_label: str = "刚刚"
+36
View File
@@ -34,6 +34,10 @@ STAGE_LABELS = {
"success": "已完成",
"failed": "失败",
}
PLATFORM_LABELS = {
"xiaohongshu": "小红书",
"douyin": "抖音",
}
def has_running_task(session: Session) -> bool:
@@ -150,6 +154,15 @@ def get_task(session: Session, task_id: str) -> Task | None:
def hydrate_task_progress(session: Session, task: Task) -> Task:
display_number = calculate_task_display_number(session, task)
task.display_number = display_number
task.display_id = f"#{display_number}"
task.created_at_label = format_datetime_minute(task.created_at)
task.scale_label = (
f"{PLATFORM_LABELS.get(task.platform, task.platform)} · "
f"{task.hotspot_limit}热点 × {task.item_limit_per_hotspot}内容 × {task.comment_limit_per_item}评论"
)
task.error_summary = build_task_error_summary(task)
task.hotspots_count = session.scalar(select(func.count(Hotspot.id)).where(Hotspot.task_id == task.id)) or 0
task.comments_count = session.scalar(select(func.count(Comment.id)).where(Comment.task_id == task.id)) or 0
task.reports_count = session.scalar(select(func.count(Report.id)).where(Report.task_id == task.id)) or 0
@@ -182,6 +195,29 @@ def hydrate_task_progress(session: Session, task: Task) -> Task:
return task
def calculate_task_display_number(session: Session, task: Task) -> int:
task_ids = list(session.scalars(select(Task.id).order_by(Task.created_at, Task.id)))
try:
return task_ids.index(task.id) + 1
except ValueError:
return len(task_ids) + 1
def format_datetime_minute(value: datetime | None) -> str:
value = ensure_utc_datetime(value)
return value.strftime("%Y-%m-%d %H:%M") if value else ""
def build_task_error_summary(task: Task) -> str | None:
if task.error_message:
return task.error_message
if task.error_stage == "system" and task.error_type == "unexpected_restart":
return RESTART_ERROR_MESSAGE
if task.error_stage or task.error_type:
return "任务失败,请查看后端日志"
return None
def format_duration_zh(total_seconds: int | None) -> str:
if total_seconds is None:
return "暂无记录"
-3
View File
@@ -28,8 +28,5 @@
{% endblock %}
{% block content %}{% endblock %}
</main>
<footer class="border-top py-3">
<div class="container text-muted small">内部演示工具 | 仅供学习参考</div>
</footer>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@
{% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item"><a href="/tasks/{{ task.id }}">任务 #{{ task.id }}</a></li>
<li class="breadcrumb-item"><a href="/tasks/{{ task.id }}">任务 {{ task.display_id }}</a></li>
<li class="breadcrumb-item active">汇总报告</li>
</ol></nav>
{% endblock %}
+1 -1
View File
@@ -3,7 +3,7 @@
{% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item"><a href="/tasks/{{ task.id }}">任务 #{{ task.id }}</a></li>
<li class="breadcrumb-item"><a href="/tasks/{{ task.id }}">任务 {{ task.display_id }}</a></li>
<li class="breadcrumb-item"><a href="/hotspots/{{ hotspot.id }}/report">热点 #{{ hotspot.rank }}{{ hotspot.title }}</a></li>
<li class="breadcrumb-item active">{{ item.title or item.source_item_id }}</li>
</ol></nav>
+5 -8
View File
@@ -4,14 +4,14 @@
{% set ai_percent = rate_percent(task.analysis_success_rate) %}
<tr>
<td>
<code>{{ task.id }}</code>
<strong>{{ task.display_id or ("#" ~ loop.index) }}</strong>
{% if task.is_demo %}
<br><span class="badge text-bg-info">Demo 数据</span>
{% endif %}
</td>
<td>{{ task.platform | platform_label }}</td>
<td>{{ task.created_at }}</td>
<td><small class="text-muted">热点 {{ task.hotspot_limit }} / 内容 {{ task.item_limit_per_hotspot }} / 评论 {{ task.comment_limit_per_item }}</small></td>
<td>{{ task.created_at_label }}</td>
<td><small class="text-muted">{{ task.scale_label }}</small></td>
<td class="task-progress-cell">
<div class="d-flex justify-content-between align-items-center gap-2">
<span class="small">已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span>
@@ -32,11 +32,8 @@
</td>
<td>
<span class="badge {{ status_class }}">{{ status_label }}</span>
{% if task.error_stage or task.error_type %}
<br><small class="text-muted">{{ task.error_stage }} / {{ task.error_type }}</small>
{% endif %}
{% if task.error_message %}
<br><small class="text-danger">{{ task.error_message }}</small>
{% if task.error_summary %}
<br><small class="text-danger">{{ task.error_summary }}</small>
{% endif %}
</td>
<td><a class="btn btn-sm btn-outline-primary" href="/tasks/{{ task.id }}">查看</a></td>
+5 -4
View File
@@ -1,14 +1,14 @@
{% extends "base.html" %}
{% block title %}任务 #{{ task.id }} - 热榜评论分析工具{% endblock %}
{% block title %}任务 {{ task.display_id }} - 热榜评论分析工具{% endblock %}
{% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item active">任务 #{{ task.id }}</li>
<li class="breadcrumb-item active">任务 {{ task.display_id }}</li>
</ol></nav>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 mb-0">任务 #{{ task.id }}</h1>
<h1 class="h3 mb-0">任务 {{ task.display_id }}</h1>
<button class="btn btn-outline-secondary btn-sm" onclick="window.location.reload()">手动刷新</button>
</div>
<section class="card mb-4" {% if task.status == "running" %}data-task-id="{{ task.id }}" data-auto-poll="true"{% endif %}><div class="card-body">
@@ -20,6 +20,7 @@
<p class="mb-0">平台:{{ task.platform | platform_label }} <span class="badge {{ cls }}">{{ label }}</span>{% if task.is_demo %}<span class="badge text-bg-info ms-1">Demo 数据</span>{% endif %}</p>
<p class="mb-0 text-muted">阶段:{{ task.current_stage_label or "等待启动" }}</p>
</div>
<div class="small text-muted mb-3">创建时间 {{ task.created_at_label }} · 完整 ID {{ task.id }}</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center gap-2">
<span>已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span>
@@ -72,7 +73,7 @@
<span class="badge text-bg-warning ms-1">AI 样本不足</span>
{% endif %}
</div>
{% if task.error_message %}<div class="alert alert-danger">{{ task.error_stage }} / {{ task.error_type }}{{ task.error_message }}</div>{% endif %}
{% if task.error_summary %}<div class="alert alert-danger">{{ task.error_summary }}</div>{% endif %}
</div></section>
{% if task.status == "running" and not hotspots %}
<div class="text-center text-muted py-5"><div class="spinner-border text-warning mb-3"></div><p>正在抓取热点数据,请稍候...</p></div>
+23
View File
@@ -1325,6 +1325,29 @@ feat: 优化 MVP 页面产品化体验
- UI 风格方向:更偏数据仪表盘、内部工具,还是偏演示型产品页面。
- 是否需要提供简单品牌名 / Logo / 说明文案。
完成记录:
- 完成日期:2026-07-03
- 相关改动:
- 任务列表和任务详情默认展示顺序号 `#1/#2/#3`,不再把完整 UUID 铺在列表和标题中。
- 任务详情页保留“完整 ID”排查信息,方便复制给开发定位。
- 创建时间压缩为 `YYYY-MM-DD HH:mm`,配置规模压缩为 `小红书 · 5热点 × 5内容 × 50评论`
- 失败状态只展示中文错误说明,不再直接展示 `system / unexpected_restart``recover / interrupted` 等内部枚举。
- 移除页脚 `内部演示工具 | 仅供学习参考`
- 验证命令:
- `docker cp app/. hot-comments-tool-app-1:/app/app/`
- `docker cp tests/. hot-comments-tool-app-1:/app/tests/`
- `docker exec hot-comments-tool-app-1 sh -lc 'python -m pytest /app/tests/integration/test_routes.py::test_index_page_uses_short_task_numbers_compact_fields_and_friendly_error_copy /app/tests/integration/test_routes.py::test_task_detail_uses_short_number_title_and_keeps_full_uuid_for_diagnostics -q'`
- `docker exec hot-comments-tool-app-1 sh -lc 'python -m pytest /app/tests/integration/test_routes.py -q'`
- 验证结果:
- WO-16 聚焦测试:`2 passed, 1 warning`
- 路由集成测试:`23 passed, 1 warning`
- 验收结论:
- 任务列表、任务详情、报告页和内容详情页的任务引用已改为短编号。
- 用户界面不再展示内部英文错误枚举和演示页脚文案。
- 遗留问题:
- 本轮不做大规模视觉重设计,品牌名 / Logo 仍需用户确认后再纳入正式工单。
### WO-17 默认规模数据量解释与展示优化
优先级:P1
+87 -1
View File
@@ -1,5 +1,7 @@
from datetime import UTC, datetime, timedelta
from bs4 import BeautifulSoup
from app.models import Comment, ContentItem, Hotspot, Report, Task
from tests.helpers import make_test_client
from sqlalchemy.exc import OperationalError
@@ -59,6 +61,58 @@ def test_index_page_lists_existing_tasks():
assert "DOMContentLoaded" in response.text
def test_index_page_uses_short_task_numbers_compact_fields_and_friendly_error_copy():
first_id = "11111111-1111-4111-8111-111111111111"
second_id = "22222222-2222-4222-8222-222222222222"
with make_test_client() as (client, engine):
from sqlalchemy.orm import Session
with Session(engine) as session:
session.add(
Task(
id=first_id,
platform="xiaohongshu",
status="failed",
created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC),
hotspot_limit=5,
item_limit_per_hotspot=5,
comment_limit_per_item=50,
error_stage="system",
error_type="unexpected_restart",
error_message="系统重启,任务被中断",
)
)
session.add(
Task(
id=second_id,
platform="douyin",
status="success",
created_at=datetime(2026, 7, 3, 10, 35, tzinfo=UTC),
hotspot_limit=1,
item_limit_per_hotspot=1,
comment_limit_per_item=10,
)
)
session.commit()
response = client.get("/")
assert response.status_code == 200
visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ")
assert "#1" in visible_text
assert "#2" in visible_text
assert first_id not in visible_text
assert second_id not in visible_text
assert "2026-07-03 10:30" in visible_text
assert "2026-07-03 10:35" in visible_text
assert "小红书 · 5热点 × 5内容 × 50评论" in visible_text
assert "抖音 · 1热点 × 1内容 × 10评论" in visible_text
assert "系统重启,任务被中断" in visible_text
assert "system / unexpected_restart" not in visible_text
assert "unexpected_restart" not in visible_text
assert "内部演示工具 | 仅供学习参考" not in visible_text
def test_index_page_does_not_auto_poll_without_running_tasks():
with make_test_client() as (client, engine):
from sqlalchemy.orm import Session
@@ -117,6 +171,37 @@ def test_running_task_detail_page_auto_polls_current_task():
assert "AI 成功率 100%" in response.text
def test_task_detail_uses_short_number_title_and_keeps_full_uuid_for_diagnostics():
task_id = "33333333-3333-4333-8333-333333333333"
with make_test_client() as (client, engine):
from sqlalchemy.orm import Session
with Session(engine) as session:
session.add(
Task(
id=task_id,
platform="xiaohongshu",
status="running",
created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC),
hotspot_limit=5,
item_limit_per_hotspot=5,
comment_limit_per_item=50,
)
)
session.commit()
response = client.get(f"/tasks/{task_id}")
assert response.status_code == 200
visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ")
assert "<title>任务 #1 - 热榜评论分析工具</title>" in response.text
assert "任务 #1" in visible_text
assert "完整 ID" in visible_text
assert task_id in visible_text
assert "任务 #33333333-3333-4333-8333-333333333333" not in visible_text
assert "创建时间 2026-07-03 10:30" in visible_text
def test_running_task_detail_page_keeps_polling_after_hotspots_exist():
with make_test_client() as (client, engine):
from sqlalchemy.orm import Session
@@ -241,7 +326,8 @@ def test_failed_task_detail_page_shows_failure_reason_without_loading_copy():
assert response.status_code == 200
assert "失败" in response.text
assert "recover / interrupted系统重启,任务被中断" in response.text
assert "系统重启,任务被中断" in response.text
assert "recover / interrupted" not in response.text
assert "正在抓取热点数据" not in response.text
assert "pollTaskDetailStatus" not in response.text