feat: 优化页面信息展示
This commit is contained in:
@@ -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 = "刚刚"
|
||||
|
||||
@@ -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 "暂无记录"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user