From 3c6ec67371d0111b98bf8f824d68e7d4bdc1d7cc Mon Sep 17 00:00:00 2001 From: bruce Date: Sat, 6 Jun 2026 22:20:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(file-summary):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=89=B9=E6=AC=A1=E8=BD=AE=E6=92=AD?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/login.css | 63 ++++++++++++++++ static/js/app.js | 107 ++++++++++++++++++++++++++++ templates/home.html | 26 ++++++- tests/test_file_summary_frontend.py | 61 +++++++++++++++- 4 files changed, 254 insertions(+), 3 deletions(-) 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
    '; workflowCardList.prepend(card); + refreshWorkflowBatchCarousel(0); return card; } + function workflowCards() { + if (!workflowCardList) { + return []; + } + return Array.prototype.slice.call(workflowCardList.querySelectorAll(".workflow-card")); + } + + function ensureWorkflowBatchControls() { + if (!workflowCardList || workflowCardList.querySelector(".workflow-batch-controls")) { + return; + } + var controls = document.createElement("div"); + controls.className = "workflow-batch-controls"; + controls.innerHTML = + '' + + '
    ' + + ''; + workflowCardList.appendChild(controls); + } + + function selectWorkflowBatchIndex(index) { + var cards = workflowCards(); + if (!workflowCardList || !cards.length) { + return; + } + var safeIndex = Math.max(0, Math.min(index, cards.length - 1)); + workflowCardList.setAttribute("data-active-index", safeIndex); + cards.forEach(function (card, cardIndex) { + var isActive = cardIndex === safeIndex; + card.classList.toggle("active", isActive); + card.setAttribute("data-workflow-index", cardIndex); + card.setAttribute("aria-hidden", isActive ? "false" : "true"); + }); + var dots = workflowCardList.querySelector(".workflow-batch-dots"); + if (!dots) { + return; + } + dots.querySelectorAll("[data-workflow-index-dot]").forEach(function (dot) { + var dotIndex = parseInt(dot.getAttribute("data-workflow-index-dot"), 10); + var isActive = dotIndex === safeIndex; + dot.classList.toggle("active", isActive); + dot.setAttribute("aria-current", isActive ? "true" : "false"); + }); + } + + function refreshWorkflowBatchCarousel(preferredIndex) { + var cards = workflowCards(); + if (!workflowCardList || !cards.length) { + return; + } + workflowCardList.classList.add("workflow-batch-carousel"); + ensureWorkflowBatchControls(); + var dots = workflowCardList.querySelector(".workflow-batch-dots"); + if (dots) { + dots.innerHTML = ""; + cards.forEach(function (card, index) { + card.setAttribute("data-workflow-index", index); + var title = card.querySelector("strong"); + var dot = document.createElement("button"); + dot.type = "button"; + dot.className = "workflow-batch-dot"; + dot.setAttribute("data-workflow-index-dot", index); + dot.setAttribute("aria-label", "查看" + (title ? title.textContent.trim() : "工作流") + "状态"); + dots.appendChild(dot); + }); + } + var activeIndex = + typeof preferredIndex === "number" + ? preferredIndex + : parseInt(workflowCardList.getAttribute("data-active-index") || "0", 10); + if (Number.isNaN(activeIndex)) { + activeIndex = 0; + } + selectWorkflowBatchIndex(activeIndex); + } + async function refreshWorkflowCard(batchId) { if (!summaryPanel || !batchId) { return ""; @@ -612,9 +689,37 @@ "%"; list.appendChild(item); }); + refreshWorkflowBatchCarousel(); return payload.batch.status || ""; } + function bindWorkflowBatchCarouselControls() { + if (!workflowCardList) { + return; + } + workflowCardList.addEventListener("click", function (event) { + var cards = workflowCards(); + if (!cards.length) { + return; + } + var actionButton = event.target.closest("[data-workflow-action]"); + var dotButton = event.target.closest("[data-workflow-index-dot]"); + var currentIndex = parseInt(workflowCardList.getAttribute("data-active-index") || "0", 10); + if (Number.isNaN(currentIndex)) { + currentIndex = 0; + } + if (actionButton) { + var nextIndex = + actionButton.getAttribute("data-workflow-action") === "next" + ? (currentIndex + 1) % cards.length + : (currentIndex - 1 + cards.length) % cards.length; + selectWorkflowBatchIndex(nextIndex); + } else if (dotButton) { + selectWorkflowBatchIndex(parseInt(dotButton.getAttribute("data-workflow-index-dot"), 10)); + } + }); + } + function isWorkflowTerminalStatus(status) { return status === "success" || status === "failed"; } @@ -817,6 +922,8 @@ syncLatestMessageIdFromDom(); bindNodeAnchorClicks(); renderExistingAssistantMessages(); + refreshWorkflowBatchCarousel(0); + bindWorkflowBatchCarouselControls(); refreshRunningWorkflowCards(); if (chatScroll) { diff --git a/templates/home.html b/templates/home.html index be9f2e5..90cca20 100644 --- a/templates/home.html +++ b/templates/home.html @@ -215,9 +215,14 @@

    工作流

    -
    + 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