From 834b4f1d48263bacb8332ad99f12524d66091293 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 8 Jun 2026 11:03:34 +0800 Subject: [PATCH] feat(documents): refresh detail pages and attachment cards --- backend/app/schemas/document_version.py | 3 + backend/app/services/document_service.py | 11 +- .../components/attachments/AttachmentList.vue | 93 +- frontend/src/types/documents.ts | 6 + .../views/documents/DocumentDetail.test.ts | 47 + .../src/views/documents/DocumentDetail.vue | 1287 ++++++++++------- .../src/views/documents/DocumentList.test.ts | 20 + frontend/src/views/documents/DocumentList.vue | 126 +- frontend/src/views/drug/ShipmentDetail.vue | 536 +++++-- frontend/src/views/fees/ContractFeeDetail.vue | 663 ++++++--- .../materials/MaterialEquipmentDetail.vue | 509 ++++++- 11 files changed, 2372 insertions(+), 929 deletions(-) diff --git a/backend/app/schemas/document_version.py b/backend/app/schemas/document_version.py index 6e54c528..7528e996 100644 --- a/backend/app/schemas/document_version.py +++ b/backend/app/schemas/document_version.py @@ -5,6 +5,8 @@ from typing import Optional from pydantic import BaseModel, ConfigDict +from app.schemas.user import UserDisplay + class DocumentVersionStatus(str, enum.Enum): DRAFT = "DRAFT" @@ -39,6 +41,7 @@ class DocumentVersionRead(BaseModel): mime_type: Optional[str] = None change_summary: Optional[str] = None created_by: Optional[uuid.UUID] = None + created_by_user: Optional[UserDisplay] = None submitted_at: Optional[datetime] = None approved_at: Optional[datetime] = None withdrawn_at: Optional[datetime] = None diff --git a/backend/app/services/document_service.py b/backend/app/services/document_service.py index b6cdf5e1..4d8c71ca 100644 --- a/backend/app/services/document_service.py +++ b/backend/app/services/document_service.py @@ -77,6 +77,13 @@ def _version_snapshot(version: DocumentVersion) -> dict: } +def _version_read(version: DocumentVersion, users_by_id: dict[uuid.UUID, object] | None = None) -> DocumentVersionRead: + creator = users_by_id.get(version.created_by) if users_by_id and version.created_by else None + return DocumentVersionRead.model_validate(version).model_copy( + update={"created_by_user": UserDisplay.model_validate(creator) if creator else None} + ) + + async def _ensure_study_access(db: AsyncSession, trial_id: uuid.UUID, current_user, action: str): study = await study_crud.get(db, trial_id) if not study: @@ -242,6 +249,8 @@ async def get_document_detail( owner_obj = await user_crud.get_by_id(db, doc.owner_id) if owner_obj: owner = UserDisplay.model_validate(owner_obj) + creator_ids = {v.created_by for v in versions if v.created_by} + version_creators = await user_crud.get_users_by_ids(db, creator_ids) return DocumentDetail( id=doc.id, trial_id=doc.trial_id, @@ -257,7 +266,7 @@ async def get_document_detail( created_at=doc.created_at, updated_at=doc.updated_at, owner=owner, - version_timeline=[DocumentVersionRead.model_validate(v) for v in versions], + version_timeline=[_version_read(v, version_creators) for v in versions], distribution_stats=distribution_stats, ) diff --git a/frontend/src/components/attachments/AttachmentList.vue b/frontend/src/components/attachments/AttachmentList.vue index aeeedacf..46f6ca42 100644 --- a/frontend/src/components/attachments/AttachmentList.vue +++ b/frontend/src/components/attachments/AttachmentList.vue @@ -55,19 +55,28 @@ -
-
-
{{ group.label }}
+
+
-
- - 点击上传文件 +
+
{{ group.label }}
+
+ + {{ group.uploadText || "点击上传文件" }} +
@@ -112,7 +121,7 @@ diff --git a/frontend/src/views/documents/DocumentList.test.ts b/frontend/src/views/documents/DocumentList.test.ts index a78fc646..333221c5 100644 --- a/frontend/src/views/documents/DocumentList.test.ts +++ b/frontend/src/views/documents/DocumentList.test.ts @@ -28,4 +28,24 @@ describe("DocumentList permissions", () => { expect(source).not.toContain(" { + const source = readSource(); + + expect(source).toContain("useDrawerDirtyGuard"); + expect(source).toContain('const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);'); + expect(source).toContain(':close-on-click-modal="true"'); + expect(source).toContain(':before-close="editorDirtyGuard.beforeClose"'); + expect(source).toContain("editorDirtyGuard.syncBaseline()"); + }); + + it("uses the shared datetime formatter and splits updated-at into two lines", () => { + const source = readSource(); + + expect(source).toContain("displayDateTime"); + expect(source).toContain("splitDateTime(row.updated_at).date"); + expect(source).toContain("splitDateTime(row.updated_at).time"); + expect(source).toContain("const displayValue = displayDateTime(value)"); + expect(source).not.toContain('replace("T", " ").replace("Z", "")'); + }); }); diff --git a/frontend/src/views/documents/DocumentList.vue b/frontend/src/views/documents/DocumentList.vue index 063851a2..abe5b69b 100644 --- a/frontend/src/views/documents/DocumentList.vue +++ b/frontend/src/views/documents/DocumentList.vue @@ -70,17 +70,22 @@ - +