diff --git a/static/css/login.css b/static/css/login.css
index a177290..30cb7b3 100644
--- a/static/css/login.css
+++ b/static/css/login.css
@@ -941,6 +941,69 @@ input:focus {
list-style: none;
}
+.workflow-batch-carousel {
+ gap: 10px;
+}
+
+.workflow-batch-carousel .workflow-card {
+ display: none;
+}
+
+.workflow-batch-carousel .workflow-card.active {
+ display: grid;
+}
+
+.workflow-batch-controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 30px;
+}
+
+.workflow-batch-btn {
+ display: inline-grid;
+ place-items: center;
+ width: 28px;
+ height: 28px;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ background: #ffffff;
+ color: var(--text);
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+}
+
+.workflow-batch-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.workflow-batch-dots {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.workflow-batch-dot {
+ width: 7px;
+ height: 7px;
+ padding: 0;
+ border: 0;
+ border-radius: 999px;
+ background: #cbd5e1;
+ cursor: pointer;
+}
+
+.workflow-batch-dot.active {
+ width: 18px;
+ background: var(--accent);
+}
+
.node-status {
display: flex;
align-items: center;
diff --git a/static/js/app.js b/static/js/app.js
index ca9ae78..cf4c6dd 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -554,9 +554,86 @@
escapeHtml(batch.batch_no || "文件汇总") +
'running
+
{% for batch in summary_batches %}
-
+
diff --git a/tests/test_file_summary_frontend.py b/tests/test_file_summary_frontend.py
index 20de8bf..e60bc35 100644
--- a/tests/test_file_summary_frontend.py
+++ b/tests/test_file_summary_frontend.py
@@ -1,7 +1,7 @@
import pytest
from django.urls import reverse
-from review_agent.models import Conversation, Message
+from review_agent.models import Conversation, FileSummaryBatch, Message, WorkflowNodeRun
pytestmark = pytest.mark.django_db
@@ -32,6 +32,53 @@ def test_workspace_renders_summary_panel(client, django_user_model):
assert "自动汇总文件目录与页数" in content
+def test_workspace_renders_workflow_history_as_batch_carousel(client, django_user_model):
+ user = django_user_model.objects.create_user(username="owner", password="pass")
+ conversation = Conversation.objects.create(user=user, title="会话")
+ older = FileSummaryBatch.objects.create(
+ conversation=conversation,
+ user=user,
+ batch_no="FS-OLDER",
+ status=FileSummaryBatch.Status.SUCCESS,
+ )
+ latest = FileSummaryBatch.objects.create(
+ conversation=conversation,
+ user=user,
+ batch_no="FS-LATEST",
+ status=FileSummaryBatch.Status.FAILED,
+ error_message="解压失败",
+ )
+ WorkflowNodeRun.objects.create(
+ batch=older,
+ node_code="upload",
+ node_name="附件固化",
+ status=WorkflowNodeRun.Status.SUCCESS,
+ progress=100,
+ message="附件固化完成",
+ )
+ WorkflowNodeRun.objects.create(
+ batch=latest,
+ node_code="extract",
+ node_name="压缩包解压",
+ status=WorkflowNodeRun.Status.FAILED,
+ progress=10,
+ message="压缩包损坏",
+ )
+ client.force_login(user)
+
+ response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
+
+ assert response.status_code == 200
+ content = response.content.decode("utf-8")
+ assert "workflow-batch-carousel" in content
+ assert 'class="workflow-card active"' in content
+ assert 'data-workflow-index="0"' in content
+ assert 'data-workflow-action="prev"' in content
+ assert 'data-workflow-action="next"' in content
+ assert content.index("FS-LATEST") < content.index("FS-OLDER")
+ assert "压缩包损坏" in content
+
+
def test_frontend_prevents_long_message_overflow():
css = open("static/css/login.css", encoding="utf-8").read()
@@ -88,3 +135,15 @@ def test_frontend_renders_workflow_error_messages():
assert "workflow-error" in script
assert "node.message" in script
assert ".workflow-error" in css
+
+
+def test_frontend_renders_workflow_batches_as_carousel():
+ script = open("static/js/app.js", encoding="utf-8").read()
+ css = open("static/css/login.css", encoding="utf-8").read()
+
+ assert "selectWorkflowBatchIndex" in script
+ assert "refreshWorkflowBatchCarousel" in script
+ assert "data-workflow-action" in script
+ assert "workflow-batch-carousel" in script
+ assert ".workflow-batch-controls" in css
+ assert ".workflow-card.active" in css