feat(attachments): 新增附件管理页面
This commit is contained in:
@@ -2,10 +2,11 @@ from django.contrib import admin
|
||||
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView
|
||||
from django.urls import include, path
|
||||
|
||||
from review_agent.views import stream_chat, workspace
|
||||
from review_agent.views import attachment_manager, stream_chat, workspace
|
||||
|
||||
urlpatterns = [
|
||||
path("", workspace, name="home"),
|
||||
path("attachments/", attachment_manager, name="attachment_manager"),
|
||||
path("", include("review_agent.urls")),
|
||||
path("chat/stream/", stream_chat, name="chat_stream"),
|
||||
path(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse, StreamingHttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views.decorators.http import require_http_methods
|
||||
@@ -10,7 +11,7 @@ from .services import (
|
||||
send_message,
|
||||
stream_message,
|
||||
)
|
||||
from .models import FileAttachment, FileSummaryBatch
|
||||
from .models import Conversation, FileAttachment, FileSummaryBatch
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -56,6 +57,38 @@ def workspace(request: HttpRequest) -> HttpResponse:
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def attachment_manager(request: HttpRequest) -> HttpResponse:
|
||||
conversations = (
|
||||
Conversation.objects.filter(user=request.user)
|
||||
.annotate(
|
||||
attachment_count=Count(
|
||||
"file_attachments",
|
||||
filter=~Q(file_attachments__upload_status=FileAttachment.UploadStatus.DELETED),
|
||||
)
|
||||
)
|
||||
.order_by("-updated_at", "-id")
|
||||
)
|
||||
selected = get_conversation_for_user(request.user, request.GET.get("conversation"))
|
||||
attachments = (
|
||||
FileAttachment.objects.filter(conversation=selected)
|
||||
.order_by("original_name", "-version_no")
|
||||
if selected
|
||||
else []
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"attachment_manager.html",
|
||||
{
|
||||
"page_title": "附件管理",
|
||||
"conversations": conversations,
|
||||
"selected_conversation": selected,
|
||||
"attachments": attachments,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def stream_chat(request: HttpRequest) -> HttpResponse:
|
||||
|
||||
@@ -367,6 +367,8 @@ input:focus {
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
border: 0;
|
||||
@@ -376,6 +378,7 @@ input:focus {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
@@ -889,6 +892,23 @@ input:focus {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment-manager-link {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.attachment-manager-link:hover {
|
||||
border-color: var(--accent);
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
@@ -1177,6 +1197,215 @@ input:focus {
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-manager-page {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding: 16px 24px 20px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.attachment-manager-hero,
|
||||
.attachment-manager-panel,
|
||||
.attachment-manager-content {
|
||||
width: min(1440px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.attachment-manager-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.attachment-manager-hero h1 {
|
||||
margin: 2px 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.attachment-manager-hero p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.attachment-manager-toolbar {
|
||||
min-height: 66px;
|
||||
}
|
||||
|
||||
.attachment-manager-selectbar {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(420px, 680px) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: min(900px, 60vw);
|
||||
}
|
||||
|
||||
.attachment-manager-selectbar label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.return-chat-link {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.attachment-manager-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.attachment-manager-panel label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.attachment-manager-panel select,
|
||||
.attachment-search {
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.attachment-manager-panel select,
|
||||
.attachment-manager-select-control {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.attachment-manager-select-control:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(58, 114, 216, 0.14);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.attachment-manager-content {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment-manager-split {
|
||||
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.attachment-search {
|
||||
width: 220px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.manager-upload-dropzone {
|
||||
min-height: 132px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.upload-manager-panel .summary-subheading span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.attachment-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.attachment-table th,
|
||||
.attachment-table td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.attachment-table th {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
max-width: 360px;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-actions a,
|
||||
.attachment-actions button {
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.attachment-actions a:hover,
|
||||
.attachment-actions button:hover {
|
||||
border-color: var(--accent);
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.table-empty,
|
||||
.attachment-manager-empty {
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.attachment-manager-empty {
|
||||
min-height: 150px;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.attachment-manager-empty h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.attachment-manager-empty p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tabbar {
|
||||
overflow-x: auto;
|
||||
@@ -1244,6 +1473,30 @@ input:focus {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.attachment-manager-page {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.attachment-manager-hero {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.attachment-manager-selectbar {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attachment-manager-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.attachment-search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-caret {
|
||||
|
||||
147
static/js/attachment_manager.js
Normal file
147
static/js/attachment_manager.js
Normal file
@@ -0,0 +1,147 @@
|
||||
(function () {
|
||||
var page = document.querySelector(".attachment-manager-page");
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
var conversationSelect = document.getElementById("attachmentConversationSelect");
|
||||
var uploadDropzone = document.getElementById("managerUploadDropzone");
|
||||
var attachmentInput = document.getElementById("managerAttachmentInput");
|
||||
var uploadStatus = document.getElementById("managerUploadStatus");
|
||||
var searchInput = document.getElementById("attachmentSearch");
|
||||
var table = document.getElementById("attachmentManagerTable");
|
||||
|
||||
function csrfToken() {
|
||||
var cookie = document.cookie.split("; ").find(function (item) {
|
||||
return item.indexOf("csrftoken=") === 0;
|
||||
});
|
||||
return cookie ? decodeURIComponent(cookie.split("=")[1]) : "";
|
||||
}
|
||||
|
||||
function selectedConversationUrl(id) {
|
||||
return id ? "/attachments/?conversation=" + encodeURIComponent(id) : "/attachments/";
|
||||
}
|
||||
|
||||
async function patchAttachment(row, payload) {
|
||||
var response = await fetch(row.getAttribute("data-update-url"), {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": csrfToken(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("附件更新失败。");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function deleteAttachment(row) {
|
||||
var response = await fetch(row.getAttribute("data-update-url"), {
|
||||
method: "DELETE",
|
||||
headers: { "X-CSRFToken": csrfToken() },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("附件删除失败。");
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFiles(files) {
|
||||
if (!uploadDropzone || !files || !files.length) {
|
||||
return;
|
||||
}
|
||||
var formData = new FormData();
|
||||
Array.prototype.forEach.call(files, function (file) {
|
||||
formData.append("files", file);
|
||||
});
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "上传中...";
|
||||
}
|
||||
try {
|
||||
var response = await fetch(uploadDropzone.getAttribute("data-upload-url"), {
|
||||
method: "POST",
|
||||
headers: { "X-CSRFToken": csrfToken() },
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败。");
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
if (uploadStatus) {
|
||||
uploadStatus.textContent = "上传失败,请重试。";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationSelect) {
|
||||
conversationSelect.addEventListener("change", function () {
|
||||
window.location.href = selectedConversationUrl(conversationSelect.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadDropzone && attachmentInput) {
|
||||
uploadDropzone.addEventListener("click", function () {
|
||||
attachmentInput.click();
|
||||
});
|
||||
uploadDropzone.addEventListener("dragover", function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.add("dragging");
|
||||
});
|
||||
uploadDropzone.addEventListener("dragleave", function () {
|
||||
uploadDropzone.classList.remove("dragging");
|
||||
});
|
||||
uploadDropzone.addEventListener("drop", function (event) {
|
||||
event.preventDefault();
|
||||
uploadDropzone.classList.remove("dragging");
|
||||
uploadFiles(event.dataTransfer.files);
|
||||
});
|
||||
attachmentInput.addEventListener("change", function () {
|
||||
uploadFiles(attachmentInput.files);
|
||||
attachmentInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput && table) {
|
||||
searchInput.addEventListener("input", function () {
|
||||
var keyword = searchInput.value.trim().toLowerCase();
|
||||
table.querySelectorAll("tbody tr[data-attachment-id]").forEach(function (row) {
|
||||
var name = (row.querySelector(".attachment-name") || row).textContent.toLowerCase();
|
||||
row.hidden = keyword && name.indexOf(keyword) === -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.addEventListener("click", async function (event) {
|
||||
var actionButton = event.target.closest("[data-attachment-action]");
|
||||
if (!actionButton) {
|
||||
return;
|
||||
}
|
||||
var row = actionButton.closest("tr[data-attachment-id]");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
var action = actionButton.getAttribute("data-attachment-action");
|
||||
try {
|
||||
if (action === "edit") {
|
||||
var nameCell = row.querySelector(".attachment-name");
|
||||
var nextName = window.prompt("请输入新的附件展示名", nameCell ? nameCell.textContent.trim() : "");
|
||||
if (nextName) {
|
||||
await patchAttachment(row, { original_name: nextName });
|
||||
window.location.reload();
|
||||
}
|
||||
} else if (action === "toggle") {
|
||||
await patchAttachment(row, { is_active: actionButton.textContent.trim() === "启用" });
|
||||
window.location.reload();
|
||||
} else if (action === "delete" && window.confirm("确认删除该附件?")) {
|
||||
await deleteAttachment(row);
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(error.message || "附件操作失败。");
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
139
templates/attachment_manager.html
Normal file
139
templates/attachment_manager.html
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}附件管理 - DEMO-AGENT V2{% endblock %}
|
||||
{% block body_class %}app-body{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="app-shell">
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">首页</a>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">知识库管理</button>
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">审核智能体</a>
|
||||
<a class="tab active" href="{% url 'attachment_manager' %}" role="tab" aria-selected="true">附件管理</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<div class="user-menu">
|
||||
<button class="user-menu-trigger" type="button">
|
||||
<span class="avatar large">{{ request.user.username|slice:":1"|upper }}</span>
|
||||
<div class="user-copy">
|
||||
<strong>{{ request.user.username }}</strong>
|
||||
<span>当前登录用户</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="attachment-manager-page"
|
||||
data-selected-conversation="{% if selected_conversation %}{{ selected_conversation.pk }}{% endif %}"
|
||||
>
|
||||
<header class="attachment-manager-hero attachment-manager-toolbar">
|
||||
<div>
|
||||
<p class="eyebrow">附件管理</p>
|
||||
<h1>附件管理</h1>
|
||||
<p>管理各对话下上传的审核资料、版本、状态和下载。</p>
|
||||
</div>
|
||||
<div class="attachment-manager-selectbar">
|
||||
<label for="attachmentConversationSelect">对话</label>
|
||||
<select class="attachment-manager-select-control" id="attachmentConversationSelect">
|
||||
<option value="">请选择对话</option>
|
||||
{% for conversation in conversations %}
|
||||
<option
|
||||
value="{{ conversation.pk }}"
|
||||
{% if selected_conversation and selected_conversation.pk == conversation.pk %}selected{% endif %}
|
||||
>
|
||||
{{ conversation.title|default:"新对话" }} · {{ conversation.updated_at|date:"m月d日 H:i" }} · {{ conversation.attachment_count }} 个附件
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if selected_conversation %}
|
||||
<a class="return-chat-link" href="{% url 'home' %}?conversation={{ selected_conversation.pk }}">返回对话</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if selected_conversation %}
|
||||
<div class="attachment-manager-content attachment-manager-split">
|
||||
<section class="attachment-manager-panel upload-manager-panel">
|
||||
<div class="summary-subheading">
|
||||
<h3>上传附件</h3>
|
||||
<span>{{ selected_conversation.title|default:"新对话" }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="upload-dropzone manager-upload-dropzone"
|
||||
id="managerUploadDropzone"
|
||||
data-upload-url="{% url 'file_summary_attachment_upload' selected_conversation.pk %}"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
<input id="managerAttachmentInput" type="file" multiple hidden>
|
||||
<strong>拖拽文件到这里</strong>
|
||||
<span>支持 doc、docx、xls、xlsx、ppt、pptx、pdf、zip、7z、rar</span>
|
||||
</div>
|
||||
<p class="upload-status" id="managerUploadStatus">上传后会归属到当前选择的对话。</p>
|
||||
</section>
|
||||
|
||||
<section class="attachment-manager-panel">
|
||||
<div class="summary-subheading">
|
||||
<h3>附件列表</h3>
|
||||
<input class="attachment-search" id="attachmentSearch" type="search" placeholder="搜索文件名">
|
||||
</div>
|
||||
<div class="attachment-table-wrap">
|
||||
<table class="attachment-table" id="attachmentManagerTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>状态</th>
|
||||
<th>文件名</th>
|
||||
<th>版本</th>
|
||||
<th>大小</th>
|
||||
<th>上传时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for attachment in attachments %}
|
||||
<tr
|
||||
data-attachment-id="{{ attachment.pk }}"
|
||||
data-update-url="{% url 'file_summary_attachment_detail' selected_conversation.pk attachment.pk %}"
|
||||
data-download-url="{% url 'file_summary_attachment_download' selected_conversation.pk attachment.pk %}"
|
||||
>
|
||||
<td>{% if attachment.is_active %}启用{% else %}禁用{% endif %}</td>
|
||||
<td class="attachment-name">{{ attachment.original_name }}</td>
|
||||
<td>v{{ attachment.version_no }}</td>
|
||||
<td>{{ attachment.file_size }} bytes</td>
|
||||
<td>{{ attachment.created_at|date:"Y-m-d H:i" }}</td>
|
||||
<td class="attachment-actions">
|
||||
<a href="{% url 'file_summary_attachment_download' selected_conversation.pk attachment.pk %}">下载</a>
|
||||
<button type="button" data-attachment-action="edit">编辑</button>
|
||||
<button type="button" data-attachment-action="toggle">{% if attachment.is_active %}禁用{% else %}启用{% endif %}</button>
|
||||
<button type="button" data-attachment-action="delete">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="table-empty">当前对话暂无附件</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="attachment-manager-panel attachment-manager-empty attachment-manager-content">
|
||||
<h2>请选择一个对话查看附件</h2>
|
||||
<p>通过上方下拉框选择对话后,可上传、下载、编辑、启用禁用或删除附件。</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'js/attachment_manager.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -9,10 +9,10 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="tabbar" role="tablist" aria-label="页面切换">
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">首页</button>
|
||||
<a class="tab" href="/" role="tab" aria-selected="false">首页</a>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">知识库管理</button>
|
||||
<button class="tab active" type="button" role="tab" aria-selected="true">审核智能体</button>
|
||||
<button class="tab" type="button" role="tab" aria-selected="false">视频实时监测</button>
|
||||
<a class="tab active" href="/" role="tab" aria-selected="true">审核智能体</a>
|
||||
<a class="tab" href="{% url 'attachment_manager' %}" role="tab" aria-selected="false">附件管理</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,6 +195,11 @@
|
||||
<section class="summary-section attachment-section">
|
||||
<div class="summary-subheading">
|
||||
<h3>附件</h3>
|
||||
<a
|
||||
class="attachment-manager-link"
|
||||
href="{% url 'attachment_manager' %}{% if current_conversation %}?conversation={{ current_conversation.pk }}{% endif %}"
|
||||
aria-label="打开附件管理页面"
|
||||
>↗</a>
|
||||
</div>
|
||||
<div class="attachment-list" id="attachmentList">
|
||||
{% for attachment in attachments %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from review_agent.models import Conversation, FileSummaryBatch, Message, WorkflowNodeRun
|
||||
from review_agent.models import Conversation, FileAttachment, FileSummaryBatch, Message, WorkflowNodeRun
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -32,6 +32,82 @@ def test_workspace_renders_summary_panel(client, django_user_model):
|
||||
assert "自动汇总文件目录与页数" in content
|
||||
|
||||
|
||||
def test_workspace_links_to_attachment_manager(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
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 "附件管理" in content
|
||||
assert "视频实时监测" not in content
|
||||
assert f'href="{reverse("attachment_manager")}?conversation={conversation.pk}"' in content
|
||||
assert 'class="attachment-manager-link"' in content
|
||||
|
||||
|
||||
def test_attachment_manager_requires_conversation_selection(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
Conversation.objects.create(user=user, title="待选择会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(reverse("attachment_manager"))
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "附件管理" in content
|
||||
assert "请选择一个对话查看附件" in content
|
||||
assert "待选择会话" in content
|
||||
assert 'id="attachmentConversationSelect"' in content
|
||||
|
||||
|
||||
def test_attachment_manager_selects_conversation_and_lists_attachments(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="资料会话")
|
||||
FileAttachment.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
original_name="a.docx",
|
||||
storage_path="x/a.docx",
|
||||
file_size=128,
|
||||
is_active=True,
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('attachment_manager')}?conversation={conversation.pk}")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "资料会话" in content
|
||||
assert "a.docx" in content
|
||||
assert "下载" in content
|
||||
assert "编辑" in content
|
||||
assert "删除" in content
|
||||
assert "attachment-manager-split" in content
|
||||
assert reverse("home") + f"?conversation={conversation.pk}" in content
|
||||
|
||||
|
||||
def test_attachment_manager_uses_compact_admin_layout(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
Conversation.objects.create(user=user, title="紧凑会话")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(reverse("attachment_manager"))
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
css = open("static/css/login.css", encoding="utf-8").read()
|
||||
assert "attachment-manager-toolbar" in content
|
||||
assert "attachment-manager-content" in content
|
||||
assert "attachment-manager-select-control" in content
|
||||
assert ".attachment-manager-page" in css
|
||||
assert "align-content: start" in css
|
||||
assert ".attachment-manager-toolbar" in css
|
||||
assert ".attachment-manager-select-control" in css
|
||||
assert ".attachment-manager-split" in css
|
||||
|
||||
|
||||
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="会话")
|
||||
|
||||
Reference in New Issue
Block a user