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: str | None = None
current_stage_label: str | None = None current_stage_label: str | None = None
last_progress_at: datetime | 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 running_seconds: int = 0
seconds_since_last_progress: int | None = None seconds_since_last_progress: int | None = None
running_duration_label: str = "刚刚" running_duration_label: str = "刚刚"
+36
View File
@@ -34,6 +34,10 @@ STAGE_LABELS = {
"success": "已完成", "success": "已完成",
"failed": "失败", "failed": "失败",
} }
PLATFORM_LABELS = {
"xiaohongshu": "小红书",
"douyin": "抖音",
}
def has_running_task(session: Session) -> bool: 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: 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.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.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 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 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: def format_duration_zh(total_seconds: int | None) -> str:
if total_seconds is None: if total_seconds is None:
return "暂无记录" return "暂无记录"
-3
View File
@@ -28,8 +28,5 @@
{% endblock %} {% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="border-top py-3">
<div class="container text-muted small">内部演示工具 | 仅供学习参考</div>
</footer>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@
{% block breadcrumbs %} {% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb"> <nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li> <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> <li class="breadcrumb-item active">汇总报告</li>
</ol></nav> </ol></nav>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -3,7 +3,7 @@
{% block breadcrumbs %} {% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb"> <nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li> <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"><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> <li class="breadcrumb-item active">{{ item.title or item.source_item_id }}</li>
</ol></nav> </ol></nav>
+5 -8
View File
@@ -4,14 +4,14 @@
{% set ai_percent = rate_percent(task.analysis_success_rate) %} {% set ai_percent = rate_percent(task.analysis_success_rate) %}
<tr> <tr>
<td> <td>
<code>{{ task.id }}</code> <strong>{{ task.display_id or ("#" ~ loop.index) }}</strong>
{% if task.is_demo %} {% if task.is_demo %}
<br><span class="badge text-bg-info">Demo 数据</span> <br><span class="badge text-bg-info">Demo 数据</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ task.platform | platform_label }}</td> <td>{{ task.platform | platform_label }}</td>
<td>{{ task.created_at }}</td> <td>{{ task.created_at_label }}</td>
<td><small class="text-muted">热点 {{ task.hotspot_limit }} / 内容 {{ task.item_limit_per_hotspot }} / 评论 {{ task.comment_limit_per_item }}</small></td> <td><small class="text-muted">{{ task.scale_label }}</small></td>
<td class="task-progress-cell"> <td class="task-progress-cell">
<div class="d-flex justify-content-between align-items-center gap-2"> <div class="d-flex justify-content-between align-items-center gap-2">
<span class="small">已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span> <span class="small">已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span>
@@ -32,11 +32,8 @@
</td> </td>
<td> <td>
<span class="badge {{ status_class }}">{{ status_label }}</span> <span class="badge {{ status_class }}">{{ status_label }}</span>
{% if task.error_stage or task.error_type %} {% if task.error_summary %}
<br><small class="text-muted">{{ task.error_stage }} / {{ task.error_type }}</small> <br><small class="text-danger">{{ task.error_summary }}</small>
{% endif %}
{% if task.error_message %}
<br><small class="text-danger">{{ task.error_message }}</small>
{% endif %} {% endif %}
</td> </td>
<td><a class="btn btn-sm btn-outline-primary" href="/tasks/{{ task.id }}">查看</a></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" %} {% extends "base.html" %}
{% block title %}任务 #{{ task.id }} - 热榜评论分析工具{% endblock %} {% block title %}任务 {{ task.display_id }} - 热榜评论分析工具{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<nav aria-label="breadcrumb"><ol class="breadcrumb"> <nav aria-label="breadcrumb"><ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">首页</a></li> <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> </ol></nav>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-3"> <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> <button class="btn btn-outline-secondary btn-sm" onclick="window.location.reload()">手动刷新</button>
</div> </div>
<section class="card mb-4" {% if task.status == "running" %}data-task-id="{{ task.id }}" data-auto-poll="true"{% endif %}><div class="card-body"> <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">平台:{{ 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> <p class="mb-0 text-muted">阶段:{{ task.current_stage_label or "等待启动" }}</p>
</div> </div>
<div class="small text-muted mb-3">创建时间 {{ task.created_at_label }} · 完整 ID {{ task.id }}</div>
<div class="mb-3"> <div class="mb-3">
<div class="d-flex justify-content-between align-items-center gap-2"> <div class="d-flex justify-content-between align-items-center gap-2">
<span>已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span> <span>已处理 {{ task.processed_items_count }} / 共 {{ task.total_items_count }} 条内容</span>
@@ -72,7 +73,7 @@
<span class="badge text-bg-warning ms-1">AI 样本不足</span> <span class="badge text-bg-warning ms-1">AI 样本不足</span>
{% endif %} {% endif %}
</div> </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> </div></section>
{% if task.status == "running" and not hotspots %} {% 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> <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 风格方向:更偏数据仪表盘、内部工具,还是偏演示型产品页面。 - UI 风格方向:更偏数据仪表盘、内部工具,还是偏演示型产品页面。
- 是否需要提供简单品牌名 / Logo / 说明文案。 - 是否需要提供简单品牌名 / 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 默认规模数据量解释与展示优化 ### WO-17 默认规模数据量解释与展示优化
优先级:P1 优先级:P1
+87 -1
View File
@@ -1,5 +1,7 @@
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from bs4 import BeautifulSoup
from app.models import Comment, ContentItem, Hotspot, Report, Task from app.models import Comment, ContentItem, Hotspot, Report, Task
from tests.helpers import make_test_client from tests.helpers import make_test_client
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
@@ -59,6 +61,58 @@ def test_index_page_lists_existing_tasks():
assert "DOMContentLoaded" in response.text 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(): def test_index_page_does_not_auto_poll_without_running_tasks():
with make_test_client() as (client, engine): with make_test_client() as (client, engine):
from sqlalchemy.orm import Session 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 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(): def test_running_task_detail_page_keeps_polling_after_hotspots_exist():
with make_test_client() as (client, engine): with make_test_client() as (client, engine):
from sqlalchemy.orm import Session 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 response.status_code == 200
assert "失败" in response.text 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 "正在抓取热点数据" not in response.text
assert "pollTaskDetailStatus" not in response.text assert "pollTaskDetailStatus" not in response.text