From 1338fa7e2b0b7d7aa902b8964bcffee9ff3a6ab9 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 12 Jan 2026 11:14:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E5=B1=80=E4=B8=AD=E6=96=87=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/admin_users.py | 2 +- backend/app/api/v1/aes.py | 26 +- backend/app/api/v1/attachments.py | 30 +- backend/app/api/v1/audit_logs.py | 2 +- backend/app/api/v1/auth.py | 2 +- backend/app/api/v1/drug_shipments.py | 14 +- backend/app/api/v1/faq_categories.py | 28 +- backend/app/api/v1/faqs.py | 68 +- backend/app/api/v1/finance_contracts.py | 14 +- backend/app/api/v1/finance_specials.py | 14 +- backend/app/api/v1/knowledge_notes.py | 14 +- backend/app/api/v1/members.py | 8 +- backend/app/api/v1/sites.py | 4 +- backend/app/api/v1/startup.py | 50 +- backend/app/api/v1/studies.py | 6 +- backend/app/api/v1/subject_histories.py | 16 +- backend/app/api/v1/subjects.py | 16 +- backend/app/api/v1/users.py | 6 +- backend/app/api/v1/visits.py | 18 +- backend/app/core/deps.py | 10 +- backend/app/core/security.py | 2 +- backend/app/crud/ae.py | 4 +- backend/app/crud/faq_item.py | 4 +- backend/app/crud/subject.py | 2 +- backend/app/crud/subject_history.py | 2 +- frontend/public/favicon.ico | Bin 0 -> 74 bytes frontend/src/api/axios.ts | 5 +- frontend/src/audit/auditDict.ts | 89 +- .../src/audit/export/auditExportColumns.ts | 30 +- .../src/audit/export/auditExportFormatter.ts | 6 +- frontend/src/audit/index.ts | 17 +- frontend/src/components/FaqCategoryForm.vue | 21 +- frontend/src/components/FaqCategoryPanel.vue | 20 +- frontend/src/components/FaqItemForm.vue | 23 +- frontend/src/components/FaqList.vue | 45 +- frontend/src/components/Layout.vue | 53 +- frontend/src/components/ModulePlaceholder.vue | 9 +- frontend/src/components/QuickActions.vue | 15 +- frontend/src/components/StateEmpty.vue | 3 +- frontend/src/components/StateError.vue | 5 +- frontend/src/components/StudySelector.vue | 11 +- frontend/src/components/ThreadComposer.vue | 17 +- frontend/src/components/ThreadList.vue | 13 +- .../components/attachments/AttachmentList.vue | 27 +- .../attachments/AttachmentUploader.vue | 9 +- .../src/components/selectors/UserSelect.vue | 49 ++ frontend/src/dictionaries/role.dict.ts | 11 +- frontend/src/dictionaries/status.dict.ts | 37 +- frontend/src/guards/actionGuard.ts | 5 +- frontend/src/locales/index.ts | 19 + frontend/src/locales/zh-CN.ts | 787 ++++++++++++++++++ frontend/src/router/index.ts | 113 +-- frontend/src/utils/display/index.ts | 6 + frontend/src/utils/permission.ts | 23 +- frontend/src/views/Faq.vue | 11 +- frontend/src/views/FaqDetail.vue | 65 +- frontend/src/views/ForgotPassword.vue | 22 + frontend/src/views/Login.vue | 37 +- frontend/src/views/ProfileSettings.vue | 51 +- frontend/src/views/Register.vue | 49 +- frontend/src/views/StudyHome.vue | 32 +- .../src/views/admin/AdminUserApproval.vue | 49 +- frontend/src/views/admin/AuditLogs.vue | 57 +- frontend/src/views/admin/ProjectDetail.vue | 31 +- frontend/src/views/admin/ProjectForm.vue | 49 +- frontend/src/views/admin/ProjectMembers.vue | 113 +-- frontend/src/views/admin/Projects.vue | 35 +- frontend/src/views/admin/SiteCraBinding.vue | 21 +- frontend/src/views/admin/SiteForm.vue | 41 +- frontend/src/views/admin/Sites.vue | 47 +- frontend/src/views/admin/UserForm.vue | 45 +- .../src/views/admin/UserResetPassword.vue | 53 +- frontend/src/views/admin/Users.vue | 69 +- frontend/src/views/drug/ShipmentDetail.vue | 33 +- frontend/src/views/drug/ShipmentForm.vue | 83 +- frontend/src/views/finance/ContractDetail.vue | 23 +- frontend/src/views/finance/ContractForm.vue | 43 +- frontend/src/views/finance/SpecialDetail.vue | 25 +- frontend/src/views/finance/SpecialForm.vue | 55 +- frontend/src/views/ia/AuditPlaceholder.vue | 9 +- frontend/src/views/ia/DrugShipments.vue | 67 +- frontend/src/views/ia/FinanceContracts.vue | 49 +- frontend/src/views/ia/FinanceSpecial.vue | 63 +- frontend/src/views/ia/KnowledgeNotes.vue | 45 +- .../src/views/ia/MonitoringPlaceholder.vue | 9 +- .../src/views/ia/StartupFeasibilityEthics.vue | 51 +- frontend/src/views/ia/StartupMeetingAuth.vue | 53 +- frontend/src/views/ia/SubjectManagement.vue | 51 +- frontend/src/views/knowledge/NoteDetail.vue | 19 +- frontend/src/views/knowledge/NoteForm.vue | 37 +- frontend/src/views/startup/EthicsDetail.vue | 21 +- frontend/src/views/startup/EthicsForm.vue | 41 +- .../src/views/startup/FeasibilityDetail.vue | 19 +- .../src/views/startup/FeasibilityForm.vue | 37 +- frontend/src/views/startup/KickoffDetail.vue | 17 +- frontend/src/views/startup/KickoffForm.vue | 29 +- frontend/src/views/startup/TrainingDetail.vue | 29 +- frontend/src/views/startup/TrainingForm.vue | 49 +- frontend/src/views/subjects/SubjectDetail.vue | 189 +++-- frontend/src/views/subjects/SubjectForm.vue | 55 +- frontend/src/views/workbench/MyWorkbench.vue | 50 +- .../workbench/components/PersonalTodo.vue | 11 +- .../workbench/components/SectionCard.vue | 7 +- 103 files changed, 2443 insertions(+), 1533 deletions(-) create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/src/components/selectors/UserSelect.vue create mode 100644 frontend/src/locales/index.ts create mode 100644 frontend/src/locales/zh-CN.ts create mode 100644 frontend/src/views/ForgotPassword.vue diff --git a/backend/app/api/v1/admin_users.py b/backend/app/api/v1/admin_users.py index 840fccf8..d6b8be7c 100644 --- a/backend/app/api/v1/admin_users.py +++ b/backend/app/api/v1/admin_users.py @@ -24,7 +24,7 @@ async def list_users_for_review( async def _get_review_user(db: AsyncSession, user_id: uuid.UUID) -> User: user = await user_crud.get_by_id(db, user_id) if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") if user.role == UserRole.ADMIN: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="管理员账号不允许审核") return user diff --git a/backend/app/api/v1/aes.py b/backend/app/api/v1/aes.py index e28d1f46..dc31260a 100644 --- a/backend/app/api/v1/aes.py +++ b/backend/app/api/v1/aes.py @@ -21,7 +21,7 @@ ALLOWED_UPDATE_ROLES = {"PM", "PV", "CRA"} async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -48,10 +48,10 @@ async def create_ae( ) -> AERead: await _ensure_study_exists(db, study_id) if ae_in.onset_date and ae_in.onset_date > date.today(): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Onset date cannot be in the future") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role != "ADMIN" and member_role not in ALLOWED_CREATE_ROLES: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") try: ae = await ae_crud.create_ae(db, study_id, ae_in, created_by=current_user.id) except ValueError as exc: @@ -62,7 +62,7 @@ async def create_ae( entity_type="ae", entity_id=ae.id, action="CREATE_AE", - detail=f"AE {ae.id} created", + detail=f"AE {ae.id} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -108,7 +108,7 @@ async def get_ae( await _ensure_study_exists(db, study_id) ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") data = AERead.model_validate(ae) data.is_overdue = _is_overdue(data) return data @@ -128,10 +128,10 @@ async def update_ae( ) -> AERead: await _ensure_study_exists(db, study_id) if ae_in.onset_date and ae_in.onset_date > date.today(): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Onset date cannot be in the future") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天") ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role == "ADMIN": @@ -140,11 +140,11 @@ async def update_ae( pass elif member_role == "CRA": if ae.created_by != current_user.id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only creator can update AE") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅创建人可更新 AE") if ae_in.status == "CLOSED": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="CRA cannot close AE") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="CRA 无法关闭 AE") else: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") old_status = ae.status detail_before = { @@ -195,10 +195,10 @@ async def delete_ae( await _ensure_study_exists(db, study_id) ae = await ae_crud.get_ae(db, ae_id) if not ae or ae.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在") member_role = await _get_member_role(db, study_id, current_user.id) if current_user.role != "ADMIN" and member_role not in {"PM", "PV"}: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await ae_crud.delete_ae(db, ae) await audit_crud.log_action( db, @@ -206,7 +206,7 @@ async def delete_ae( entity_type="ae", entity_id=ae_id, action="DELETE_AE", - detail=f"AE {ae_id} deleted", + detail=f"AE {ae_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/attachments.py b/backend/app/api/v1/attachments.py index 0c90bfc7..730cf5b9 100644 --- a/backend/app/api/v1/attachments.py +++ b/backend/app/api/v1/attachments.py @@ -26,7 +26,7 @@ UPLOAD_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads" async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -71,7 +71,7 @@ async def upload_attachment( entity_type=entity_type, entity_id=entity_id, action="UPLOAD_FILE", - detail=f"File uploaded: {file.filename}", + detail=f"文件已上传:{file.filename}", operator_id=current_user.id, operator_role=current_user.role, ) @@ -130,9 +130,9 @@ async def download_attachment( await _ensure_study_exists(db, study_id) attachment = await attachment_crud.get_attachment(db, attachment_id) if not attachment or attachment.study_id != study_id or attachment.entity_id != entity_id or attachment.entity_type != entity_type: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") if not os.path.exists(attachment.file_path): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File missing on server") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="服务器未找到文件") return FileResponse( path=attachment.file_path, filename=attachment.filename, @@ -148,16 +148,16 @@ async def _authorize_global(request: Request, db: AsyncSession, study_id: uuid.U if not token: token = request.query_params.get("token") if not token: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录") payload = decode_token(token) user = await user_crud.get_by_id(db, uuid.UUID(str(payload.get("sub")))) if not user or not user.is_active: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive or missing user") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账号不存在或已停用") if user.role == "ADMIN": return user, None membership = await member_crud.get_member(db, study_id, user.id) if not membership or not membership.is_active: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not project member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员") return user, membership @@ -172,11 +172,11 @@ async def global_download_attachment( ): attachment = await attachment_crud.get_attachment(db, attachment_id) if not attachment: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") await _ensure_study_exists(db, attachment.study_id) user, _ = await _authorize_global(request, db, attachment.study_id) if not os.path.exists(attachment.file_path): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File missing on server") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="服务器未找到文件") return FileResponse( path=attachment.file_path, filename=attachment.filename, @@ -195,7 +195,7 @@ async def global_delete_attachment( ): attachment = await attachment_crud.get_attachment(db, attachment_id) if not attachment: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") await _ensure_study_exists(db, attachment.study_id) user, membership = await _authorize_global(request, db, attachment.study_id) can_delete = ( @@ -204,7 +204,7 @@ async def global_delete_attachment( or (membership and getattr(membership, "role_in_study", None) == "PM") ) if not can_delete: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to delete attachment") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限删除附件") await attachment_crud.soft_delete_attachment(db, attachment) await audit_crud.log_action( db, @@ -212,7 +212,7 @@ async def global_delete_attachment( entity_type=attachment.entity_type, entity_id=attachment.entity_id, action="DELETE_ATTACHMENT", - detail=f"File deleted: {attachment.filename}", + detail=f"文件已删除:{attachment.filename}", operator_id=user.id, operator_role=user.role, ) @@ -240,7 +240,7 @@ async def delete_attachment( or attachment.entity_type != entity_type or attachment.is_deleted ): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") membership = None if current_user.role != "ADMIN": @@ -252,7 +252,7 @@ async def delete_attachment( or (membership and getattr(membership, "role_in_study", None) == "PM") ) if not can_delete: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to delete attachment") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限删除附件") await attachment_crud.soft_delete_attachment(db, attachment) await audit_crud.log_action( @@ -261,7 +261,7 @@ async def delete_attachment( entity_type=entity_type, entity_id=entity_id, action="DELETE_ATTACHMENT", - detail=f"File deleted: {attachment.filename}", + detail=f"文件已删除:{attachment.filename}", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/audit_logs.py b/backend/app/api/v1/audit_logs.py index eec9b725..abc19c6f 100644 --- a/backend/app/api/v1/audit_logs.py +++ b/backend/app/api/v1/audit_logs.py @@ -50,5 +50,5 @@ async def delete_audit_log( ): log = await audit_crud.get_log(db, log_id) if not log or log.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Audit log not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="审计日志不存在") await audit_crud.delete_log(db, log) diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index 7ae39326..259bc906 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -124,5 +124,5 @@ async def upload_avatar( async def get_avatar(user_id: str, filename: str): file_path = AVATAR_ROOT / user_id / filename if not file_path.exists(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Avatar not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="头像不存在") return FileResponse(file_path) diff --git a/backend/app/api/v1/drug_shipments.py b/backend/app/api/v1/drug_shipments.py index e1e099f4..a0c30497 100644 --- a/backend/app/api/v1/drug_shipments.py +++ b/backend/app/api/v1/drug_shipments.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -39,7 +39,7 @@ async def create_shipment( entity_type="drug_shipment", entity_id=shipment.id, action="CREATE_DRUG_SHIPMENT", - detail=f"Drug shipment {shipment.id} created", + detail=f"药品运输 {shipment.id} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -80,7 +80,7 @@ async def get_shipment( await _ensure_study_exists(db, study_id) shipment = await shipment_crud.get_shipment(db, shipment_id) if not shipment or shipment.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shipment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在") return DrugShipmentRead.model_validate(shipment) @@ -99,7 +99,7 @@ async def update_shipment( await _ensure_study_exists(db, study_id) shipment = await shipment_crud.get_shipment(db, shipment_id) if not shipment or shipment.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shipment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在") shipment = await shipment_crud.update_shipment(db, shipment, shipment_in) await audit_crud.log_action( db, @@ -107,7 +107,7 @@ async def update_shipment( entity_type="drug_shipment", entity_id=shipment_id, action="UPDATE_DRUG_SHIPMENT", - detail=f"Drug shipment {shipment_id} updated", + detail=f"药品运输 {shipment_id} 已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -128,7 +128,7 @@ async def delete_shipment( await _ensure_study_exists(db, study_id) shipment = await shipment_crud.get_shipment(db, shipment_id) if not shipment or shipment.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shipment not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在") await shipment_crud.delete_shipment(db, shipment) await audit_crud.log_action( db, @@ -136,7 +136,7 @@ async def delete_shipment( entity_type="drug_shipment", entity_id=shipment_id, action="DELETE_DRUG_SHIPMENT", - detail=f"Drug shipment {shipment_id} deleted", + detail=f"药品运输 {shipment_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/faq_categories.py b/backend/app/api/v1/faq_categories.py index 38d86fec..773e3974 100644 --- a/backend/app/api/v1/faq_categories.py +++ b/backend/app/api/v1/faq_categories.py @@ -17,7 +17,7 @@ router = APIRouter() def _check_permission_for_scope(study_id: uuid.UUID, current_user, member_role: str | None): if current_user.role != "ADMIN" and member_role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") @router.post( @@ -33,14 +33,14 @@ async def create_category( current_user=Depends(get_current_user), ) -> CategoryRead: if not payload.study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID") existing = await category_crud.get_category_by_name(db, payload.study_id, payload.name) if existing: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在") member_role = None member = await member_crud.get_member(db, payload.study_id, current_user.id) if not member and current_user.role != "ADMIN": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") member_role = member.role_in_study if member else None _check_permission_for_scope(payload.study_id, current_user, member_role) category = await category_crud.create_category(db, payload) @@ -50,7 +50,7 @@ async def create_category( entity_type="faq_category", entity_id=category.id, action="CREATE_FAQ_CATEGORY", - detail=f"FAQ category {category.name} created", + detail=f"FAQ 分类 {category.name} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -70,11 +70,11 @@ async def list_categories( current_user=Depends(get_current_user), ) -> list[CategoryRead]: if not study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID") if study_id: member = await member_crud.get_member(db, study_id, current_user.id) if not member and current_user.role != "ADMIN": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") categories = await category_crud.list_categories(db, study_id, include_global=False, is_active=is_active) return paginate([CategoryRead.model_validate(c) for c in categories], total=len(categories)) @@ -93,22 +93,22 @@ async def update_category( ) -> CategoryRead: category = await category_crud.get_category(db, category_id) if not category: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分类不存在") member_role = None update_data = payload.model_dump(exclude_unset=True) target_study_id = update_data.get("study_id", category.study_id) target_name = update_data.get("name", category.name) if "study_id" in update_data and update_data["study_id"] != category.study_id and current_user.role != "ADMIN": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can change category scope") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可修改分类范围") existing = await category_crud.get_category_by_name(db, target_study_id, target_name) if existing and existing.id != category.id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在") if not target_study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID") member = await member_crud.get_member(db, target_study_id, current_user.id) member_role = member.role_in_study if member else None if not member and current_user.role != "ADMIN": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") _check_permission_for_scope(target_study_id, current_user, member_role) updated = await category_crud.update_category(db, category, payload) await audit_crud.log_action( @@ -117,7 +117,7 @@ async def update_category( entity_type="faq_category", entity_id=category_id, action="UPDATE_FAQ_CATEGORY", - detail=f"FAQ category {category_id} updated", + detail=f"FAQ 分类 {category_id} 已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -137,9 +137,9 @@ async def delete_category( ) -> None: category = await category_crud.get_category(db, category_id) if not category: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分类不存在") if current_user.role != "ADMIN": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can delete FAQ category") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可删除 FAQ 分类") item_count = await item_crud.count_items_by_category(db, category_id) if item_count > 0: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类下存在 FAQ,无法删除") @@ -151,7 +151,7 @@ async def delete_category( entity_type="faq_category", entity_id=category_id, action="DELETE_FAQ_CATEGORY", - detail=f"FAQ category {category_id} deleted", + detail=f"FAQ 分类 {category_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/faqs.py b/backend/app/api/v1/faqs.py index 72ba660e..5d1f1206 100644 --- a/backend/app/api/v1/faqs.py +++ b/backend/app/api/v1/faqs.py @@ -27,12 +27,12 @@ router = APIRouter() def _check_write_permission(study_id: uuid.UUID, current_user, member_role: str | None): if current_user.role != "ADMIN" and member_role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") def _check_create_permission(current_user, is_member: bool): if current_user.role != "ADMIN" and not is_member: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") @router.post( @@ -48,12 +48,12 @@ async def create_faq( current_user=Depends(get_current_user), ) -> FaqRead: if not payload.study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID") cat = await category_crud.get_category(db, payload.category_id) if not cat: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分类不存在") if cat.study_id != payload.study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Category scope mismatch") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类范围不匹配") member_role = None is_member = False member = await member_crud.get_member(db, payload.study_id, current_user.id) @@ -79,7 +79,7 @@ async def create_faq( entity_type="faq_item", entity_id=item.id, action="CREATE_FAQ_ITEM", - detail="FAQ created", + detail="FAQ 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -111,16 +111,16 @@ async def list_faqs( return role if not study_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID") if current_user.role != "ADMIN": role = await _get_member_role(study_id) if not role: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") if is_active is False and current_user.role != "ADMIN": role = await _get_member_role(study_id) if role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") items = await faq_crud.list_items( db, @@ -158,18 +158,18 @@ async def get_faq( ) -> FaqRead: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if not item.study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if current_user.role != "ADMIN": member = await member_crud.get_member(db, item.study_id, current_user.id) if not member: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") if not item.is_active and current_user.role not in {"ADMIN"}: member = await member_crud.get_member(db, item.study_id, current_user.id) member_role = member.role_in_study if member else None if member_role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive FAQ") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="FAQ 已停用") return FaqRead.model_validate(item) @@ -187,7 +187,7 @@ async def update_faq( ) -> FaqRead: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if item.created_by != current_user.id: member_role = None if item.study_id: @@ -228,16 +228,16 @@ async def update_faq_status( ) -> FaqRead: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if payload.status != "RESOLVED": - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only RESOLVED is allowed") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许设置为已解决") if item.created_by != current_user.id and current_user.role != "ADMIN": member_role = None if item.study_id: member = await member_crud.get_member(db, item.study_id, current_user.id) member_role = member.role_in_study if member else None if member_role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") await faq_crud.set_status(db, item.id, "RESOLVED", resolved_by_confirm=True) updated = await faq_crud.get_item(db, item_id) return FaqRead.model_validate(updated) @@ -257,9 +257,9 @@ async def set_best_reply( ) -> FaqRead: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if not item.study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") is_member = False member = await member_crud.get_member(db, item.study_id, current_user.id) is_member = member is not None @@ -267,9 +267,9 @@ async def set_best_reply( if payload.best_reply_id: reply = await reply_crud.get_reply(db, payload.best_reply_id) if not reply or reply.faq_id != item.id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid reply") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复无效") if reply.is_deleted: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Reply deleted") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复已删除") await faq_crud.set_best_reply(db, item.id, reply.id) await faq_crud.set_status(db, item.id, "RESOLVED", resolved_by_confirm=item.resolved_by_confirm) else: @@ -299,11 +299,11 @@ async def list_replies( ) -> list[FaqReplyRead]: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if item.study_id and current_user.role != "ADMIN": member = await member_crud.get_member(db, item.study_id, current_user.id) if not member: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是该项目成员") replies = await reply_crud.list_replies(db, item_id) reply_map = {r.id: r for r in replies} result: list[FaqReplyRead] = [] @@ -339,11 +339,11 @@ async def create_reply( ) -> FaqReplyRead: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if not item.study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if not payload.content.strip(): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Reply content is required") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复内容不能为空") is_member = False member = await member_crud.get_member(db, item.study_id, current_user.id) is_member = member is not None @@ -352,7 +352,7 @@ async def create_reply( if payload.quote_reply_id: quote = await reply_crud.get_reply(db, payload.quote_reply_id) if not quote or quote.faq_id != item.id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid quote reply") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="引用回复无效") reply = await reply_crud.create_reply( db, faq_id=item.id, @@ -369,7 +369,7 @@ async def create_reply( entity_type="faq_reply", entity_id=reply.id, action="CREATE_FAQ_REPLY", - detail="FAQ replied", + detail="FAQ 已回复", operator_id=current_user.id, operator_role=current_user.role, ) @@ -400,7 +400,7 @@ async def delete_faq( ) -> None: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") if item.created_by != current_user.id: member_role = None if item.study_id: @@ -416,7 +416,7 @@ async def delete_faq( entity_type="faq_item", entity_id=item_id, action="DELETE_FAQ_ITEM", - detail="FAQ deleted", + detail="FAQ 已删除", operator_id=current_user.id, operator_role=current_user.role, ) @@ -436,17 +436,17 @@ async def delete_reply( ) -> None: item = await faq_crud.get_item(db, item_id) if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在") reply = await reply_crud.get_reply(db, reply_id) if not reply or reply.faq_id != item.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reply not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="回复不存在") if reply.created_by != current_user.id and current_user.role != "ADMIN": member_role = None if item.study_id: member = await member_crud.get_member(db, item.study_id, current_user.id) member_role = member.role_in_study if member else None if member_role != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") if item.best_reply_id == reply.id: await faq_crud.set_best_reply(db, item.id, None) ref_count = await reply_crud.count_quote_references(db, reply.id) @@ -472,7 +472,7 @@ async def delete_reply( entity_type="faq_reply", entity_id=reply_id, action="DELETE_FAQ_REPLY", - detail="FAQ reply deleted", + detail="FAQ 回复已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/finance_contracts.py b/backend/app/api/v1/finance_contracts.py index b0cb351f..5c08b811 100644 --- a/backend/app/api/v1/finance_contracts.py +++ b/backend/app/api/v1/finance_contracts.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -39,7 +39,7 @@ async def create_contract( entity_type="finance_contract", entity_id=contract.id, action="CREATE_FINANCE_CONTRACT", - detail=f"Finance contract {contract.contract_no} created", + detail=f"合同费用 {contract.contract_no} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -77,7 +77,7 @@ async def get_contract( await _ensure_study_exists(db, study_id) contract = await contract_crud.get_contract(db, contract_id) if not contract or contract.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Contract not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在") return FinanceContractRead.model_validate(contract) @@ -96,7 +96,7 @@ async def update_contract( await _ensure_study_exists(db, study_id) contract = await contract_crud.get_contract(db, contract_id) if not contract or contract.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Contract not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在") contract = await contract_crud.update_contract(db, contract, contract_in) await audit_crud.log_action( db, @@ -104,7 +104,7 @@ async def update_contract( entity_type="finance_contract", entity_id=contract_id, action="UPDATE_FINANCE_CONTRACT", - detail=f"Finance contract {contract_id} updated", + detail=f"合同费用 {contract_id} 已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -125,7 +125,7 @@ async def delete_contract( await _ensure_study_exists(db, study_id) contract = await contract_crud.get_contract(db, contract_id) if not contract or contract.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Contract not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在") await contract_crud.delete_contract(db, contract) await audit_crud.log_action( db, @@ -133,7 +133,7 @@ async def delete_contract( entity_type="finance_contract", entity_id=contract_id, action="DELETE_FINANCE_CONTRACT", - detail=f"Finance contract {contract_id} deleted", + detail=f"合同费用 {contract_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/finance_specials.py b/backend/app/api/v1/finance_specials.py index 381cab4f..7efc752e 100644 --- a/backend/app/api/v1/finance_specials.py +++ b/backend/app/api/v1/finance_specials.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -39,7 +39,7 @@ async def create_special( entity_type="finance_special", entity_id=item.id, action="CREATE_FINANCE_SPECIAL", - detail=f"Finance special {item.id} created", + detail=f"特殊费用 {item.id} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -77,7 +77,7 @@ async def get_special( await _ensure_study_exists(db, study_id) item = await special_crud.get_special(db, special_id) if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Special fee not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") return FinanceSpecialRead.model_validate(item) @@ -96,7 +96,7 @@ async def update_special( await _ensure_study_exists(db, study_id) item = await special_crud.get_special(db, special_id) if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Special fee not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") item = await special_crud.update_special(db, item, special_in) await audit_crud.log_action( db, @@ -104,7 +104,7 @@ async def update_special( entity_type="finance_special", entity_id=special_id, action="UPDATE_FINANCE_SPECIAL", - detail=f"Finance special {special_id} updated", + detail=f"特殊费用 {special_id} 已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -125,7 +125,7 @@ async def delete_special( await _ensure_study_exists(db, study_id) item = await special_crud.get_special(db, special_id) if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Special fee not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") await special_crud.delete_special(db, item) await audit_crud.log_action( db, @@ -133,7 +133,7 @@ async def delete_special( entity_type="finance_special", entity_id=special_id, action="DELETE_FINANCE_SPECIAL", - detail=f"Finance special {special_id} deleted", + detail=f"特殊费用 {special_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/knowledge_notes.py b/backend/app/api/v1/knowledge_notes.py index 215dded8..94b228ac 100644 --- a/backend/app/api/v1/knowledge_notes.py +++ b/backend/app/api/v1/knowledge_notes.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -39,7 +39,7 @@ async def create_note( entity_type="knowledge_note", entity_id=note.id, action="CREATE_KNOWLEDGE_NOTE", - detail=f"Knowledge note {note.title} created", + detail=f"注意事项 {note.title} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -77,7 +77,7 @@ async def get_note( await _ensure_study_exists(db, study_id) note = await note_crud.get_note(db, note_id) if not note or note.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Note not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="注意事项不存在") return KnowledgeNoteRead.model_validate(note) @@ -96,7 +96,7 @@ async def update_note( await _ensure_study_exists(db, study_id) note = await note_crud.get_note(db, note_id) if not note or note.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Note not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="注意事项不存在") note = await note_crud.update_note(db, note, note_in) await audit_crud.log_action( db, @@ -104,7 +104,7 @@ async def update_note( entity_type="knowledge_note", entity_id=note_id, action="UPDATE_KNOWLEDGE_NOTE", - detail=f"Knowledge note {note_id} updated", + detail=f"注意事项 {note_id} 已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -125,7 +125,7 @@ async def delete_note( await _ensure_study_exists(db, study_id) note = await note_crud.get_note(db, note_id) if not note or note.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Note not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="注意事项不存在") await note_crud.delete_note(db, note) await audit_crud.log_action( db, @@ -133,7 +133,7 @@ async def delete_note( entity_type="knowledge_note", entity_id=note_id, action="DELETE_KNOWLEDGE_NOTE", - detail=f"Knowledge note {note_id} deleted", + detail=f"注意事项 {note_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/members.py b/backend/app/api/v1/members.py index 720c4561..19666b98 100644 --- a/backend/app/api/v1/members.py +++ b/backend/app/api/v1/members.py @@ -16,7 +16,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -34,7 +34,7 @@ async def add_member( await _ensure_study_exists(db, study_id) existing = await member_crud.get_member(db, study_id, member_in.user_id) if existing: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Member already exists") + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="成员已存在") member = await member_crud.add_member(db, study_id, member_in) return member @@ -90,7 +90,7 @@ async def update_member( await _ensure_study_exists(db, study_id) member = await member_crud.get_member_by_id(db, member_id) if not member or member.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="成员不存在") updated = await member_crud.update_member(db, member, member_in) return updated @@ -108,6 +108,6 @@ async def remove_member( await _ensure_study_exists(db, study_id) member = await member_crud.get_member_by_id(db, member_id) if not member or member.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="成员不存在") removed = await member_crud.remove_member(db, member) return removed diff --git a/backend/app/api/v1/sites.py b/backend/app/api/v1/sites.py index c6bdb4e8..4afaab3d 100644 --- a/backend/app/api/v1/sites.py +++ b/backend/app/api/v1/sites.py @@ -14,7 +14,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -64,6 +64,6 @@ async def update_site( await _ensure_study_exists(db, study_id) site = await site_crud.get_site(db, site_id) if not site or site.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分中心不存在") updated = await site_crud.update_site(db, site, site_in) return updated diff --git a/backend/app/api/v1/startup.py b/backend/app/api/v1/startup.py index bfe86e29..2814183a 100644 --- a/backend/app/api/v1/startup.py +++ b/backend/app/api/v1/startup.py @@ -28,7 +28,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -52,7 +52,7 @@ async def create_feasibility( entity_type="startup_feasibility", entity_id=record.id, action="CREATE_STARTUP_FEASIBILITY", - detail="Startup feasibility record created", + detail="立项记录已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -88,7 +88,7 @@ async def get_feasibility( await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feasibility record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") return StartupFeasibilityRead.model_validate(record) @@ -107,7 +107,7 @@ async def update_feasibility( await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feasibility record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") record = await startup_crud.update_feasibility(db, record, record_in) await audit_crud.log_action( db, @@ -115,7 +115,7 @@ async def update_feasibility( entity_type="startup_feasibility", entity_id=record_id, action="UPDATE_STARTUP_FEASIBILITY", - detail="Startup feasibility record updated", + detail="立项记录已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -136,7 +136,7 @@ async def delete_feasibility( await _ensure_study_exists(db, study_id) record = await startup_crud.get_feasibility(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feasibility record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在") await startup_crud.delete_feasibility(db, record) await audit_crud.log_action( db, @@ -144,7 +144,7 @@ async def delete_feasibility( entity_type="startup_feasibility", entity_id=record_id, action="DELETE_STARTUP_FEASIBILITY", - detail="Startup feasibility record deleted", + detail="立项记录已删除", operator_id=current_user.id, operator_role=current_user.role, ) @@ -170,7 +170,7 @@ async def create_ethics( entity_type="startup_ethics", entity_id=record.id, action="CREATE_STARTUP_ETHICS", - detail="Startup ethics record created", + detail="伦理记录已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -206,7 +206,7 @@ async def get_ethics( await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ethics record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") return StartupEthicsRead.model_validate(record) @@ -225,7 +225,7 @@ async def update_ethics( await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ethics record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") record = await startup_crud.update_ethics(db, record, record_in) await audit_crud.log_action( db, @@ -233,7 +233,7 @@ async def update_ethics( entity_type="startup_ethics", entity_id=record_id, action="UPDATE_STARTUP_ETHICS", - detail="Startup ethics record updated", + detail="伦理记录已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -254,7 +254,7 @@ async def delete_ethics( await _ensure_study_exists(db, study_id) record = await startup_crud.get_ethics(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ethics record not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在") await startup_crud.delete_ethics(db, record) await audit_crud.log_action( db, @@ -262,7 +262,7 @@ async def delete_ethics( entity_type="startup_ethics", entity_id=record_id, action="DELETE_STARTUP_ETHICS", - detail="Startup ethics record deleted", + detail="伦理记录已删除", operator_id=current_user.id, operator_role=current_user.role, ) @@ -288,7 +288,7 @@ async def create_kickoff( entity_type="startup_kickoff", entity_id=meeting.id, action="CREATE_KICKOFF_MEETING", - detail="Kickoff meeting created", + detail="启动会记录已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -324,7 +324,7 @@ async def get_kickoff( await _ensure_study_exists(db, study_id) meeting = await startup_crud.get_kickoff(db, meeting_id) if not meeting or meeting.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kickoff meeting not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在") return KickoffMeetingRead.model_validate(meeting) @@ -343,7 +343,7 @@ async def update_kickoff( await _ensure_study_exists(db, study_id) meeting = await startup_crud.get_kickoff(db, meeting_id) if not meeting or meeting.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kickoff meeting not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在") meeting = await startup_crud.update_kickoff(db, meeting, meeting_in) await audit_crud.log_action( db, @@ -351,7 +351,7 @@ async def update_kickoff( entity_type="startup_kickoff", entity_id=meeting_id, action="UPDATE_KICKOFF_MEETING", - detail="Kickoff meeting updated", + detail="启动会记录已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -372,7 +372,7 @@ async def delete_kickoff( await _ensure_study_exists(db, study_id) meeting = await startup_crud.get_kickoff(db, meeting_id) if not meeting or meeting.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kickoff meeting not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在") await startup_crud.delete_kickoff(db, meeting) await audit_crud.log_action( db, @@ -380,7 +380,7 @@ async def delete_kickoff( entity_type="startup_kickoff", entity_id=meeting_id, action="DELETE_KICKOFF_MEETING", - detail="Kickoff meeting deleted", + detail="启动会记录已删除", operator_id=current_user.id, operator_role=current_user.role, ) @@ -406,7 +406,7 @@ async def create_training_authorization( entity_type="training_authorization", entity_id=record.id, action="CREATE_TRAINING_AUTH", - detail="Training authorization created", + detail="培训授权人员已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -442,7 +442,7 @@ async def get_training_authorization( await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Training authorization not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") return TrainingAuthorizationRead.model_validate(record) @@ -461,7 +461,7 @@ async def update_training_authorization( await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Training authorization not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") record = await startup_crud.update_training_authorization(db, record, record_in) await audit_crud.log_action( db, @@ -469,7 +469,7 @@ async def update_training_authorization( entity_type="training_authorization", entity_id=record_id, action="UPDATE_TRAINING_AUTH", - detail="Training authorization updated", + detail="培训授权人员已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -490,7 +490,7 @@ async def delete_training_authorization( await _ensure_study_exists(db, study_id) record = await startup_crud.get_training_authorization(db, record_id) if not record or record.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Training authorization not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在") await startup_crud.delete_training_authorization(db, record) await audit_crud.log_action( db, @@ -498,7 +498,7 @@ async def delete_training_authorization( entity_type="training_authorization", entity_id=record_id, action="DELETE_TRAINING_AUTH", - detail="Training authorization deleted", + detail="培训授权人员已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/studies.py b/backend/app/api/v1/studies.py index 2b505a69..00da7a41 100644 --- a/backend/app/api/v1/studies.py +++ b/backend/app/api/v1/studies.py @@ -20,7 +20,7 @@ async def create_study( ) -> StudyRead: existing = await study_crud.get_by_code(db, study_in.code) if existing: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Study code already exists") + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="项目编号已存在") study = await study_crud.create(db, study_in, created_by=current_user.id) return study @@ -48,7 +48,7 @@ async def get_study( ) -> StudyRead: study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -64,6 +64,6 @@ async def update_study( ) -> StudyRead: study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") updated = await study_crud.update(db, study, study_in) return updated diff --git a/backend/app/api/v1/subject_histories.py b/backend/app/api/v1/subject_histories.py index c2c77be5..dc4ced75 100644 --- a/backend/app/api/v1/subject_histories.py +++ b/backend/app/api/v1/subject_histories.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -34,7 +34,7 @@ async def create_history( ) -> SubjectHistoryRead: await _ensure_study_exists(db, study_id) if history_in.subject_id != subject_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Subject mismatch") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="受试者不匹配") history = await history_crud.create_history(db, study_id, history_in, created_by=current_user.id) await audit_crud.log_action( db, @@ -42,7 +42,7 @@ async def create_history( entity_type="subject_history", entity_id=history.id, action="CREATE_SUBJECT_HISTORY", - detail="Subject history created", + detail="病史记录已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -80,7 +80,7 @@ async def get_history( await _ensure_study_exists(db, study_id) history = await history_crud.get_history(db, history_id) if not history or history.study_id != study_id or history.subject_id != subject_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="History not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="病史记录不存在") return SubjectHistoryRead.model_validate(history) @@ -100,7 +100,7 @@ async def update_history( await _ensure_study_exists(db, study_id) history = await history_crud.get_history(db, history_id) if not history or history.study_id != study_id or history.subject_id != subject_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="History not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="病史记录不存在") history = await history_crud.update_history(db, history, history_in) await audit_crud.log_action( db, @@ -108,7 +108,7 @@ async def update_history( entity_type="subject_history", entity_id=history_id, action="UPDATE_SUBJECT_HISTORY", - detail="Subject history updated", + detail="病史记录已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -130,7 +130,7 @@ async def delete_history( await _ensure_study_exists(db, study_id) history = await history_crud.get_history(db, history_id) if not history or history.study_id != study_id or history.subject_id != subject_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="History not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="病史记录不存在") await history_crud.delete_history(db, history) await audit_crud.log_action( db, @@ -138,7 +138,7 @@ async def delete_history( entity_type="subject_history", entity_id=history_id, action="DELETE_SUBJECT_HISTORY", - detail="Subject history deleted", + detail="病史记录已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/subjects.py b/backend/app/api/v1/subjects.py index 4b730fa8..41efa8c5 100644 --- a/backend/app/api/v1/subjects.py +++ b/backend/app/api/v1/subjects.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): study = await study_crud.get(db, study_id) if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") return study @@ -42,7 +42,7 @@ async def create_subject( entity_type="subject", entity_id=subject.id, action="CREATE_SUBJECT", - detail=f"Subject {subject.subject_no} created", + detail=f"受试者 {subject.subject_no} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -81,7 +81,7 @@ async def get_subject( await _ensure_study_exists(db, study_id) subject = await subject_crud.get_subject(db, subject_id) if not subject or subject.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="受试者不存在") return subject @@ -100,7 +100,7 @@ async def update_subject( await _ensure_study_exists(db, study_id) subject = await subject_crud.get_subject(db, subject_id) if not subject or subject.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="受试者不存在") old_status = subject.status updated = await subject_crud.update_subject(db, subject, subject_in) # auto-generate visits when enrolled @@ -108,14 +108,14 @@ async def update_subject( await subject_crud.generate_default_visits(db, updated) detail = None if subject_in.status and subject_in.status != old_status: - detail = f"subject {updated.subject_no} status {old_status} -> {subject_in.status}" + detail = f"受试者 {updated.subject_no} 状态 {old_status} -> {subject_in.status}" await audit_crud.log_action( db, study_id=study_id, entity_type="subject", entity_id=subject_id, action="SUBJECT_STATUS_CHANGE" if detail else "UPDATE_SUBJECT", - detail=detail or "Subject updated", + detail=detail or "受试者已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -136,7 +136,7 @@ async def delete_subject( await _ensure_study_exists(db, study_id) subject = await subject_crud.get_subject(db, subject_id) if not subject or subject.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="受试者不存在") await subject_crud.delete_subject(db, subject) await audit_crud.log_action( db, @@ -144,7 +144,7 @@ async def delete_subject( entity_type="subject", entity_id=subject_id, action="DELETE_SUBJECT", - detail=f"Subject {subject_id} deleted", + detail=f"受试者 {subject_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index 4bff6e46..76a4b61d 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -36,7 +36,7 @@ async def create_user( if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail="Email already exists", + detail="邮箱已存在", ) user = await user_crud.create_user(db, user_in) return user @@ -51,7 +51,7 @@ async def update_user( ) -> UserRead: db_user = await user_crud.get_by_id(db, user_id) if not db_user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") if db_user.role.value == "ADMIN": requested_role = user_in.role requested_status = user_in.status @@ -79,7 +79,7 @@ async def delete_user( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员密码错误") db_user = await user_crud.get_by_id(db, user_id) if not db_user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") if db_user.id == current_user.id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许删除自己") if db_user.role.value == "ADMIN" and db_user.status.value == "ACTIVE": diff --git a/backend/app/api/v1/visits.py b/backend/app/api/v1/visits.py index 7ebd6c13..5a8030df 100644 --- a/backend/app/api/v1/visits.py +++ b/backend/app/api/v1/visits.py @@ -15,7 +15,7 @@ router = APIRouter() async def _ensure_subject(db: AsyncSession, study_id: uuid.UUID, subject_id: uuid.UUID): subject = await subject_crud.get_subject(db, subject_id) if not subject or subject.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="受试者不存在") return subject @@ -49,7 +49,7 @@ async def create_visit( ) -> VisitRead: subject = await _ensure_subject(db, study_id, subject_id) if visit_in.subject_id != subject_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Subject mismatch") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="受试者不匹配") visit = await visit_crud.create_visit( db, study_id=study_id, @@ -65,7 +65,7 @@ async def create_visit( entity_type="visit", entity_id=visit.id, action="CREATE_VISIT", - detail=f"Visit {visit.visit_code} created", + detail=f"访视 {visit.visit_code} 已创建", operator_id=current_user.id, operator_role=current_user.role, ) @@ -88,20 +88,20 @@ async def update_visit( await _ensure_subject(db, study_id, subject_id) visit = await visit_crud.get_visit(db, visit_id) if not visit or visit.subject_id != subject_id or visit.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Visit not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="访视不存在") if visit_in.status == "DONE" and not visit_in.actual_date: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="actual_date required when DONE") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="状态为已完成时必须填写实际日期") updated = await visit_crud.update_visit(db, visit, visit_in) detail = None if visit_in.status: - detail = f"visit {visit.visit_code} {visit.status} -> {visit_in.status}" + detail = f"访视 {visit.visit_code} {visit.status} -> {visit_in.status}" await audit_crud.log_action( db, study_id=study_id, entity_type="visit", entity_id=visit_id, action="VISIT_STATUS_CHANGE" if detail else "UPDATE_VISIT", - detail=detail or "Visit updated", + detail=detail or "访视已更新", operator_id=current_user.id, operator_role=current_user.role, ) @@ -123,7 +123,7 @@ async def delete_visit( await _ensure_subject(db, study_id, subject_id) visit = await visit_crud.get_visit(db, visit_id) if not visit or visit.subject_id != subject_id or visit.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Visit not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="访视不存在") await visit_crud.delete_visit(db, visit) await audit_crud.log_action( db, @@ -131,7 +131,7 @@ async def delete_visit( entity_type="visit", entity_id=visit_id, action="DELETE_VISIT", - detail=f"Visit {visit_id} deleted", + detail=f"访视 {visit_id} 已删除", operator_id=current_user.id, operator_role=current_user.role, ) diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py index 7d92883b..3ff3a673 100644 --- a/backend/app/core/deps.py +++ b/backend/app/core/deps.py @@ -28,7 +28,7 @@ async def get_current_user( except ValidationError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", + detail="无法验证登录凭据", headers={"WWW-Authenticate": "Bearer"}, ) from exc @@ -36,7 +36,7 @@ async def get_current_user( if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Inactive or missing user", + detail="账号不存在或已停用", headers={"WWW-Authenticate": "Bearer"}, ) return user @@ -50,7 +50,7 @@ def require_roles(roles: Iterable[str]) -> Callable: if current_role not in roles_set: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Insufficient permissions", + detail="权限不足", ) return current_user @@ -81,7 +81,7 @@ def require_study_member(): if not membership or not membership.is_active: raise AppException( code="FORBIDDEN", - message="Not a member of this study", + message="不是该项目成员", status_code=status.HTTP_403_FORBIDDEN, ) return current_user @@ -104,7 +104,7 @@ def require_study_roles(roles: Iterable[str]): if not membership or not membership.is_active or membership.role_in_study not in roles_set: raise AppException( code="FORBIDDEN", - message="Insufficient study permissions", + message="项目权限不足", status_code=status.HTTP_403_FORBIDDEN, ) return current_user diff --git a/backend/app/core/security.py b/backend/app/core/security.py index fe9430ca..a6b3fa3c 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -28,7 +28,7 @@ def decode_token(token: str) -> Dict[str, Any]: except JWTError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", + detail="无法验证登录凭据", headers={"WWW-Authenticate": "Bearer"}, ) from exc return payload diff --git a/backend/app/crud/ae.py b/backend/app/crud/ae.py index f2e05571..3fcdb6d3 100644 --- a/backend/app/crud/ae.py +++ b/backend/app/crud/ae.py @@ -23,12 +23,12 @@ async def _validate_site_subject(db: AsyncSession, study_id: uuid.UUID, site_id: result = await db.execute(select(Site).where(Site.id == site_id)) site = result.scalar_one_or_none() if not site or site.study_id != study_id: - raise ValueError("Site not found in study") + raise ValueError("分中心不属于当前项目") if subject_id: result = await db.execute(select(Subject).where(Subject.id == subject_id)) subj = result.scalar_one_or_none() if not subj or subj.study_id != study_id: - raise ValueError("Subject not found in study") + raise ValueError("受试者不属于当前项目") async def create_ae( diff --git a/backend/app/crud/faq_item.py b/backend/app/crud/faq_item.py index c40120ba..44d726d9 100644 --- a/backend/app/crud/faq_item.py +++ b/backend/app/crud/faq_item.py @@ -13,11 +13,11 @@ async def create_item(db: AsyncSession, item_in: FaqCreate, *, created_by: uuid. # ensure category exists and matches study scope category = await db.get(FaqCategory, item_in.category_id) if not category: - raise ValueError("Category not found") + raise ValueError("分类不存在") if category.study_id != item_in.study_id: # allow global category if item is global (None) or project if matches if not (category.study_id is None and item_in.study_id is None): - raise ValueError("Category scope mismatch") + raise ValueError("分类范围不匹配") item = FaqItem( study_id=item_in.study_id, diff --git a/backend/app/crud/subject.py b/backend/app/crud/subject.py index 04a08b7a..79367ebf 100644 --- a/backend/app/crud/subject.py +++ b/backend/app/crud/subject.py @@ -15,7 +15,7 @@ async def _validate_site(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UU result = await db.execute(select(Site).where(Site.id == site_id)) site = result.scalar_one_or_none() if not site or site.study_id != study_id: - raise ValueError("Site not found in study") + raise ValueError("分中心不属于当前项目") async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: SubjectCreate) -> Subject: diff --git a/backend/app/crud/subject_history.py b/backend/app/crud/subject_history.py index 504e43ca..1e6386fa 100644 --- a/backend/app/crud/subject_history.py +++ b/backend/app/crud/subject_history.py @@ -13,7 +13,7 @@ async def _ensure_subject(db: AsyncSession, study_id: uuid.UUID, subject_id: uui result = await db.execute(select(Subject).where(Subject.id == subject_id)) subject = result.scalar_one_or_none() if not subject or subject.study_id != study_id: - raise ValueError("Subject not found in study") + raise ValueError("受试者不属于当前项目") async def create_history( diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fad805aa375144c78f2cdfa5048d45be3a76be42 GIT binary patch literal 74 kcmZQzU<5%%1|X@xV8Xz_AO^%5KnxUOU;@($K$0*304MJNyZ`_I literal 0 HcmV?d00001 diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index f87c959b..37e0b412 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -5,6 +5,7 @@ import { getToken } from "../utils/auth"; import { useAuthStore } from "../store/auth"; import type { ApiError } from "../types/api"; import { useStudyStore } from "../store/study"; +import { TEXT } from "../locales"; let unauthorizedNotified = false; @@ -39,7 +40,7 @@ instance.interceptors.response.use( auth.logout(); if (!unauthorizedNotified) { unauthorizedNotified = true; - ElMessage.error(data?.message || "登录已过期,请重新登录"); + ElMessage.error(data?.message || TEXT.common.messages.sessionExpired); setTimeout(() => { unauthorizedNotified = false; }, 1000); @@ -61,7 +62,7 @@ instance.interceptors.response.use( } else if (data?.message) { ElMessage.error(data.message); } else { - ElMessage.error("请求失败,请稍后重试"); + ElMessage.error(TEXT.common.messages.requestFailed); } return Promise.reject(error); } diff --git a/frontend/src/audit/auditDict.ts b/frontend/src/audit/auditDict.ts index bf57293b..3c0a845e 100644 --- a/frontend/src/audit/auditDict.ts +++ b/frontend/src/audit/auditDict.ts @@ -1,85 +1,4 @@ -export const auditDict: Record< - string, - { label: string; actionText: string; targetLabel: string } -> = { - AE_CLOSED: { - label: "关闭 AE(不良事件)", - actionText: "关闭了 AE", - targetLabel: "不良事件", - }, - SUBJECT_STATUS_CHANGED: { - label: "受试者状态变更", - actionText: "变更了受试者状态", - targetLabel: "受试者", - }, - FINANCE_STATUS_CHANGED: { - label: "费用状态变更", - actionText: "变更了费用状态", - targetLabel: "费用记录", - }, - ADMIN_RESET_PASSWORD: { - label: "重置用户密码", - actionText: "重置了用户密码", - targetLabel: "用户账号", - }, - ISSUE_STATUS_CHANGED: { - label: "风险/问题状态变更", - actionText: "更新了风险/问题状态", - targetLabel: "风险/问题", - }, - UNAUTHORIZED_ACTION_ATTEMPT: { - label: "越权操作尝试", - actionText: "尝试执行无权限操作", - targetLabel: "系统资源", - }, - INVALID_STATE_TRANSITION_ATTEMPT: { - label: "非法状态流转尝试", - actionText: "尝试执行非法状态流转", - targetLabel: "业务对象", - }, - PROJECT_MEMBER_UPDATED: { - label: "项目成员变更", - actionText: "调整了项目成员", - targetLabel: "项目成员", - }, - SITE_CRA_BOUND: { - label: "中心 CRA 绑定", - actionText: "调整了中心与 CRA 的绑定", - targetLabel: "中心", - }, - SITE_STATUS_CHANGED: { - label: "中心状态变更", - actionText: "更新了中心信息", - targetLabel: "中心", - }, - AUDIT_EXPORT_SYSTEM: { - label: "导出系统审计", - actionText: "导出了系统审计日志", - targetLabel: "审计日志", - }, - AUDIT_EXPORT_PROJECT: { - label: "导出项目审计", - actionText: "导出了项目审计日志", - targetLabel: "审计日志", - }, - FINANCE_APPROVED: { - label: "费用审批", - actionText: "审批通过费用", - targetLabel: "费用", - }, - FINANCE_REJECTED: { - label: "费用驳回", - actionText: "驳回了费用", - targetLabel: "费用", - }, - IMP_CONFIRMED: { - label: "药品出入库确认", - actionText: "确认了药品出入库记录", - targetLabel: "药品交易", - }, - QUERY_RESOLVED: { - label: "数据问题解决", - actionText: "标记数据问题已解决", - targetLabel: "数据问题", - }, -}; +import { TEXT } from "../locales"; + +export const auditDict: Record = + TEXT.audit.eventDict; diff --git a/frontend/src/audit/export/auditExportColumns.ts b/frontend/src/audit/export/auditExportColumns.ts index f4225aa0..27d0f4e6 100644 --- a/frontend/src/audit/export/auditExportColumns.ts +++ b/frontend/src/audit/export/auditExportColumns.ts @@ -1,21 +1,23 @@ +import { TEXT } from "../../locales"; + export interface AuditExportColumn { key: string; label: string; } export const auditExportColumns: AuditExportColumn[] = [ - { key: "timestamp", label: "审计时间" }, - { key: "actorName", label: "操作人姓名" }, - { key: "actorId", label: "操作人账号" }, - { key: "actorRoleLabel", label: "操作人角色" }, - { key: "eventLabel", label: "操作类型" }, - { key: "targetTypeLabel", label: "操作对象类型" }, - { key: "targetIdentifier", label: "操作对象标识" }, - { key: "description", label: "操作描述" }, - { key: "beforeStatus", label: "操作前状态" }, - { key: "afterStatus", label: "操作后状态" }, - { key: "resultLabel", label: "操作结果" }, - { key: "reason", label: "拒绝原因" }, - { key: "ip", label: "IP 地址" }, - { key: "remark", label: "备注" }, + { key: "timestamp", label: TEXT.audit.exportColumns.timestamp }, + { key: "actorName", label: TEXT.audit.exportColumns.actorName }, + { key: "actorId", label: TEXT.audit.exportColumns.actorId }, + { key: "actorRoleLabel", label: TEXT.audit.exportColumns.actorRoleLabel }, + { key: "eventLabel", label: TEXT.audit.exportColumns.eventLabel }, + { key: "targetTypeLabel", label: TEXT.audit.exportColumns.targetTypeLabel }, + { key: "targetIdentifier", label: TEXT.audit.exportColumns.targetIdentifier }, + { key: "description", label: TEXT.audit.exportColumns.description }, + { key: "beforeStatus", label: TEXT.audit.exportColumns.beforeStatus }, + { key: "afterStatus", label: TEXT.audit.exportColumns.afterStatus }, + { key: "resultLabel", label: TEXT.audit.exportColumns.resultLabel }, + { key: "reason", label: TEXT.audit.exportColumns.reason }, + { key: "ip", label: TEXT.audit.exportColumns.ip }, + { key: "remark", label: TEXT.audit.exportColumns.remark }, ]; diff --git a/frontend/src/audit/export/auditExportFormatter.ts b/frontend/src/audit/export/auditExportFormatter.ts index bc1fee52..15d07792 100644 --- a/frontend/src/audit/export/auditExportFormatter.ts +++ b/frontend/src/audit/export/auditExportFormatter.ts @@ -1,6 +1,7 @@ import { getDictLabel, statusDict } from "../../dictionaries"; import type { AuditEvent } from ".."; import { auditExportColumns } from "./auditExportColumns"; +import { TEXT } from "../../locales"; export interface AuditExportRow { [key: string]: string; @@ -19,11 +20,12 @@ export const formatAuditRow = (event: AuditEvent): AuditExportRow => { descriptionParts.push(`${event.actorName} ${event.actionText}`); if (event.targetTypeLabel || event.targetName || event.targetId) { descriptionParts.push( - `对象:${event.targetTypeLabel || ""}${event.targetName ? `(${event.targetName})` : `(${event.targetId}`})` + `${TEXT.audit.objectLabel}${event.targetTypeLabel || ""}${ + event.targetName ? `(${event.targetName})` : `(${event.targetId}`})` ); } if (event.diffText?.length) { - descriptionParts.push(`变更:${event.diffText.join(";")}`); + descriptionParts.push(`${TEXT.audit.changeLabel}${event.diffText.join(";")}`); } return { diff --git a/frontend/src/audit/index.ts b/frontend/src/audit/index.ts index 53268ad7..b98f8d8a 100644 --- a/frontend/src/audit/index.ts +++ b/frontend/src/audit/index.ts @@ -2,6 +2,7 @@ import { auditDict } from "./auditDict"; import { roleDict, getDictLabel, statusDict } from "../dictionaries"; import { useAuthStore } from "../store/auth"; import { useStudyStore } from "../store/study"; +import { TEXT } from "../locales"; export interface AuditEvent { eventType: string; @@ -32,9 +33,9 @@ const buildDiffText = (before?: Record, after?: Record const next = after[key]; if (prev === next) return; if (key.toLowerCase().includes("status")) { - lines.push(`状态:${getDictLabel(statusDict, prev)} → ${getDictLabel(statusDict, next)}`); + lines.push(`${TEXT.audit.statusLabel}${getDictLabel(statusDict, prev)} → ${getDictLabel(statusDict, next)}`); } else { - lines.push(`${key}:${prev ?? "-"} → ${next ?? "-"}`); + lines.push(`${key}:${prev ?? TEXT.audit.emptyValue} → ${next ?? TEXT.audit.emptyValue}`); } }); return lines; @@ -42,9 +43,9 @@ const buildDiffText = (before?: Record, after?: Record export const normalizeAuditEvent = (raw: any, userMap: Record): AuditEvent => { const dict = auditDict[raw.action] || { - label: "系统操作", - actionText: "执行了操作", - targetLabel: raw.entity_type || "对象", + label: TEXT.audit.eventFallback, + actionText: TEXT.audit.actionFallback, + targetLabel: raw.entity_type || TEXT.audit.targetFallback, }; let detailObj: any = {}; if (raw.detail) { @@ -74,7 +75,7 @@ export const normalizeAuditEvent = (raw: any, userMap: Record): diffText: Array.isArray(diffText) ? diffText : diffText ? [diffText] : [], reason: detailObj.reason, result: detailObj.result || "SUCCESS", - resultLabel: detailObj.result === "FAIL" ? "失败" : "成功", + resultLabel: detailObj.result === "FAIL" ? TEXT.audit.resultFail : TEXT.audit.resultSuccess, timestamp: raw.created_at, }; }; @@ -105,9 +106,9 @@ export const logAudit = async (eventType: string, payload: Record) const studyId = study.currentStudy?.id || null; const log = { action: eventType, - operator_id: user?.id || user?.username || "current_user", + operator_id: user?.id || user?.username || TEXT.audit.currentUser, operator_role: user?.role || "", - entity_type: payload.targetType || "client", + entity_type: payload.targetType || TEXT.audit.localClient, entity_id: payload.targetId || "", created_at: new Date().toISOString(), detail: JSON.stringify({ diff --git a/frontend/src/components/FaqCategoryForm.vue b/frontend/src/components/FaqCategoryForm.vue index 0e18eb05..b86688d9 100644 --- a/frontend/src/components/FaqCategoryForm.vue +++ b/frontend/src/components/FaqCategoryForm.vue @@ -1,22 +1,22 @@ @@ -26,6 +26,7 @@ import { computed, reactive, ref, watch } from "vue"; import { ElMessage, FormInstance, FormRules } from "element-plus"; import { createFaqCategory, updateFaqCategory } from "../api/faqs"; import { useStudyStore } from "../store/study"; +import { TEXT, requiredMessage } from "../locales"; const props = defineProps<{ modelValue: boolean; category?: any; isAdmin: boolean }>(); const emit = defineEmits(["update:modelValue", "success"]); @@ -43,7 +44,7 @@ const form = reactive({ }); const rules: FormRules = { - name: [{ required: true, message: "请输入名称", trigger: "blur" }], + name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }], }; const isEdit = computed(() => !!props.category); @@ -100,11 +101,11 @@ const onSubmit = async () => { } else { await createFaqCategory(payload); } - ElMessage.success("保存成功"); + ElMessage.success(TEXT.common.messages.saveSuccess); emit("success"); close(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { submitting.value = false; } diff --git a/frontend/src/components/FaqCategoryPanel.vue b/frontend/src/components/FaqCategoryPanel.vue index dd1a586e..21ee391d 100644 --- a/frontend/src/components/FaqCategoryPanel.vue +++ b/frontend/src/components/FaqCategoryPanel.vue @@ -1,23 +1,23 @@ @@ -59,6 +59,7 @@ import { useStudyStore } from "../../store/study"; import { createFinanceContract, getFinanceContract, updateFinanceContract } from "../../api/financeContracts"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -91,7 +92,7 @@ const load = async () => { remark: data.remark || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -109,15 +110,15 @@ const submit = async () => { }; if (isEdit.value && contractId.value) { await updateFinanceContract(studyId.value, contractId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/finance/contracts/${contractId.value}`); } else { const { data } = await createFinanceContract(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/finance/contracts/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/finance/SpecialDetail.vue b/frontend/src/views/finance/SpecialDetail.vue index 398dfbef..662a191a 100644 --- a/frontend/src/views/finance/SpecialDetail.vue +++ b/frontend/src/views/finance/SpecialDetail.vue @@ -2,23 +2,23 @@
- {{ detail.site_name || "—" }} - {{ detail.fee_type || "—" }} - {{ displayDate(detail.occur_date) }} - {{ detail.staff_name || "—" }} - {{ detail.amount || "—" }} - {{ detail.remark || "—" }} + {{ detail.site_name || TEXT.common.fallback }} + {{ displayEnum(TEXT.enums.financeSpecialType, detail.fee_type) }} + {{ displayDate(detail.occur_date) }} + {{ detail.staff_name || TEXT.common.fallback }} + {{ detail.amount || TEXT.common.fallback }} + {{ detail.remark || TEXT.common.fallback }} @@ -37,8 +37,9 @@ import { useRoute, useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import { useStudyStore } from "../../store/study"; import { getFinanceSpecial } from "../../api/financeSpecials"; -import { displayDate } from "../../utils/display"; +import { displayDate, displayEnum } from "../../utils/display"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -63,7 +64,7 @@ const load = async () => { const { data } = await getFinanceSpecial(studyId, specialId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/finance/SpecialForm.vue b/frontend/src/views/finance/SpecialForm.vue index 79ea3def..49125989 100644 --- a/frontend/src/views/finance/SpecialForm.vue +++ b/frontend/src/views/finance/SpecialForm.vue @@ -2,40 +2,40 @@
- - + + - - - - - - + + + + + + - + - - + + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -47,7 +47,7 @@ :entity-id="specialId" /> - +
@@ -60,6 +60,7 @@ import { useStudyStore } from "../../store/study"; import { createFinanceSpecial, getFinanceSpecial, updateFinanceSpecial } from "../../api/financeSpecials"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -72,7 +73,7 @@ const studyId = computed(() => study.currentStudy?.id || ""); const form = reactive({ site_name: "", - fee_type: "差旅", + fee_type: "TRAVEL", amount: 0, occur_date: "", staff_name: "", @@ -85,14 +86,14 @@ const load = async () => { const { data } = await getFinanceSpecial(studyId.value, specialId.value); Object.assign(form, { site_name: data.site_name || "", - fee_type: data.fee_type || "差旅", + fee_type: data.fee_type || "TRAVEL", amount: Number(data.amount || 0), occur_date: data.occur_date || "", staff_name: data.staff_name || "", remark: data.remark || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -110,15 +111,15 @@ const submit = async () => { }; if (isEdit.value && specialId.value) { await updateFinanceSpecial(studyId.value, specialId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/finance/special/${specialId.value}`); } else { const { data } = await createFinanceSpecial(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/finance/special/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/ia/AuditPlaceholder.vue b/frontend/src/views/ia/AuditPlaceholder.vue index 544c845f..6b571896 100644 --- a/frontend/src/views/ia/AuditPlaceholder.vue +++ b/frontend/src/views/ia/AuditPlaceholder.vue @@ -1,12 +1,13 @@ diff --git a/frontend/src/views/ia/DrugShipments.vue b/frontend/src/views/ia/DrugShipments.vue index e5e59071..9b67357c 100644 --- a/frontend/src/views/ia/DrugShipments.vue +++ b/frontend/src/views/ia/DrugShipments.vue @@ -2,54 +2,58 @@
- - + + - - + + - - - - - - - + + + + + + + - 查询 - 重置 + {{ TEXT.common.actions.search }} + {{ TEXT.common.actions.reset }} - - - - + + + + + + - - + + + + - +
@@ -60,8 +64,9 @@ import { useRouter } from "vue-router"; import { ElMessage, ElMessageBox } from "element-plus"; import { useStudyStore } from "../../store/study"; import { listDrugShipments, deleteDrugShipment } from "../../api/drugShipments"; -import { displayDate } from "../../utils/display"; +import { displayDate, displayEnum } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -85,7 +90,7 @@ const load = async () => { }); items.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } @@ -105,14 +110,14 @@ const goEdit = (id: string) => router.push(`/drug/shipments/${id}/edit`); const remove = async (row: any) => { const studyId = study.currentStudy?.id; if (!studyId) return; - const ok = await ElMessageBox.confirm("确认删除该运输记录?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.modules.drugShipments.confirmDelete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteDrugShipment(studyId, row.id); - ElMessage.success("已删除"); + ElMessage.success(TEXT.common.messages.deleteSuccess); load(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; diff --git a/frontend/src/views/ia/FinanceContracts.vue b/frontend/src/views/ia/FinanceContracts.vue index 511c5880..679602f3 100644 --- a/frontend/src/views/ia/FinanceContracts.vue +++ b/frontend/src/views/ia/FinanceContracts.vue @@ -2,55 +2,55 @@
- - + + - - + + - 查询 - 重置 + {{ TEXT.common.actions.search }} + {{ TEXT.common.actions.reset }} - - - + + + - + - + - + - +
@@ -63,6 +63,7 @@ import { useStudyStore } from "../../store/study"; import { listFinanceContracts, deleteFinanceContract } from "../../api/financeContracts"; import { displayDate, displayDateTime } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -84,7 +85,7 @@ const load = async () => { }); items.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } @@ -103,14 +104,14 @@ const goEdit = (id: string) => router.push(`/finance/contracts/${id}/edit`); const remove = async (row: any) => { const studyId = study.currentStudy?.id; if (!studyId) return; - const ok = await ElMessageBox.confirm("确认删除该合同费用?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteFinanceContract(studyId, row.id); - ElMessage.success("已删除"); + ElMessage.success(TEXT.common.messages.deleteSuccess); load(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; diff --git a/frontend/src/views/ia/FinanceSpecial.vue b/frontend/src/views/ia/FinanceSpecial.vue index 45fe8b6a..6bf5115b 100644 --- a/frontend/src/views/ia/FinanceSpecial.vue +++ b/frontend/src/views/ia/FinanceSpecial.vue @@ -2,61 +2,65 @@
- - + + - - - - - - + + + + + + - 查询 - 重置 + {{ TEXT.common.actions.search }} + {{ TEXT.common.actions.reset }} - - - + + + + + - - + + - + - + - +
@@ -67,8 +71,9 @@ import { useRouter } from "vue-router"; import { ElMessage, ElMessageBox } from "element-plus"; import { useStudyStore } from "../../store/study"; import { listFinanceSpecials, deleteFinanceSpecial } from "../../api/financeSpecials"; -import { displayDate, displayDateTime } from "../../utils/display"; +import { displayDate, displayDateTime, displayEnum } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -90,7 +95,7 @@ const load = async () => { }); items.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } @@ -109,14 +114,14 @@ const goEdit = (id: string) => router.push(`/finance/special/${id}/edit`); const remove = async (row: any) => { const studyId = study.currentStudy?.id; if (!studyId) return; - const ok = await ElMessageBox.confirm("确认删除该特殊费用?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteFinanceSpecial(studyId, row.id); - ElMessage.success("已删除"); + ElMessage.success(TEXT.common.messages.deleteSuccess); load(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; diff --git a/frontend/src/views/ia/KnowledgeNotes.vue b/frontend/src/views/ia/KnowledgeNotes.vue index 3286b8bb..0cab18ff 100644 --- a/frontend/src/views/ia/KnowledgeNotes.vue +++ b/frontend/src/views/ia/KnowledgeNotes.vue @@ -2,44 +2,44 @@
- - + + - - + + - 查询 - 重置 + {{ TEXT.common.actions.search }} + {{ TEXT.common.actions.reset }} - - - - + + + + - + - +
@@ -52,6 +52,7 @@ import { useStudyStore } from "../../store/study"; import { listKnowledgeNotes, deleteKnowledgeNote } from "../../api/knowledgeNotes"; import { displayDateTime } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -73,7 +74,7 @@ const load = async () => { }); items.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } @@ -92,14 +93,14 @@ const goEdit = (id: string) => router.push(`/knowledge/notes/${id}/edit`); const remove = async (row: any) => { const studyId = study.currentStudy?.id; if (!studyId) return; - const ok = await ElMessageBox.confirm("确认删除该注意事项?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteKnowledgeNote(studyId, row.id); - ElMessage.success("已删除"); + ElMessage.success(TEXT.common.messages.deleteSuccess); load(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; diff --git a/frontend/src/views/ia/MonitoringPlaceholder.vue b/frontend/src/views/ia/MonitoringPlaceholder.vue index 8038f2f5..e63cd8a3 100644 --- a/frontend/src/views/ia/MonitoringPlaceholder.vue +++ b/frontend/src/views/ia/MonitoringPlaceholder.vue @@ -1,12 +1,13 @@ diff --git a/frontend/src/views/ia/StartupFeasibilityEthics.vue b/frontend/src/views/ia/StartupFeasibilityEthics.vue index 745bed26..bd05fa0a 100644 --- a/frontend/src/views/ia/StartupFeasibilityEthics.vue +++ b/frontend/src/views/ia/StartupFeasibilityEthics.vue @@ -2,63 +2,63 @@
- +
- 新增立项记录 + {{ TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}
- + - + - + - - + + - +
- +
- 新增伦理记录 + {{ TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}
- + - + - + - + - - + + - +
@@ -73,6 +73,7 @@ import { useStudyStore } from "../../store/study"; import { listFeasibility, listEthics } from "../../api/startup"; import { displayDate } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -90,7 +91,7 @@ const loadFeasibility = async () => { const { data } = await listFeasibility(studyId); feasibilityItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingFeasibility.value = false; } @@ -104,7 +105,7 @@ const loadEthics = async () => { const { data } = await listEthics(studyId); ethicsItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingEthics.value = false; } diff --git a/frontend/src/views/ia/StartupMeetingAuth.vue b/frontend/src/views/ia/StartupMeetingAuth.vue index 3d072ae8..18d6ca21 100644 --- a/frontend/src/views/ia/StartupMeetingAuth.vue +++ b/frontend/src/views/ia/StartupMeetingAuth.vue @@ -2,61 +2,61 @@
- +
- 新增启动会 + {{ TEXT.modules.startupMeetingAuth.kickoffNewTitle }}
- + - + - + - +
- +
- 新增人员 + {{ TEXT.modules.startupMeetingAuth.trainingNewTitle }}
- - - - + + + + - + - + - +
@@ -71,6 +71,7 @@ import { useStudyStore } from "../../store/study"; import { listKickoffs, listTrainingAuthorizations, updateTrainingAuthorization } from "../../api/startup"; import { displayDate } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -88,7 +89,7 @@ const loadKickoffs = async () => { const { data } = await listKickoffs(studyId); kickoffItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingKickoff.value = false; } @@ -102,7 +103,7 @@ const loadTraining = async () => { const { data } = await listTrainingAuthorizations(studyId); trainingItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingTraining.value = false; } @@ -116,9 +117,9 @@ const toggleTraining = async (row: any) => { trained: row.trained, authorized: row.authorized, }); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); loadTraining(); } }; diff --git a/frontend/src/views/ia/SubjectManagement.vue b/frontend/src/views/ia/SubjectManagement.vue index 24808e43..8087fc95 100644 --- a/frontend/src/views/ia/SubjectManagement.vue +++ b/frontend/src/views/ia/SubjectManagement.vue @@ -2,50 +2,52 @@
- - + + - - - - - - + + + + + + - 查询 - 重置 + {{ TEXT.common.actions.search }} + {{ TEXT.common.actions.reset }} - - - + + + - - + + + + - + - +
@@ -57,8 +59,9 @@ import { ElMessage } from "element-plus"; import { useStudyStore } from "../../store/study"; import { fetchSubjects } from "../../api/subjects"; import { fetchSites } from "../../api/sites"; -import { displayDate } from "../../utils/display"; +import { displayDate, displayEnum } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const router = useRouter(); const study = useStudyStore(); @@ -96,7 +99,7 @@ const load = async () => { }); items.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/knowledge/NoteDetail.vue b/frontend/src/views/knowledge/NoteDetail.vue index 90ec329e..84eda300 100644 --- a/frontend/src/views/knowledge/NoteDetail.vue +++ b/frontend/src/views/knowledge/NoteDetail.vue @@ -2,21 +2,21 @@
- {{ detail.site_name || "—" }} - {{ detail.level || "—" }} - {{ detail.title || "—" }} - {{ detail.content || "—" }} + {{ detail.site_name || TEXT.common.fallback }} + {{ detail.level || TEXT.common.fallback }} + {{ detail.title || TEXT.common.fallback }} + {{ detail.content || TEXT.common.fallback }} @@ -36,6 +36,7 @@ import { ElMessage } from "element-plus"; import { useStudyStore } from "../../store/study"; import { getKnowledgeNote } from "../../api/knowledgeNotes"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -58,7 +59,7 @@ const load = async () => { const { data } = await getKnowledgeNote(studyId, noteId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/knowledge/NoteForm.vue b/frontend/src/views/knowledge/NoteForm.vue index e97b9ce1..d3e819d7 100644 --- a/frontend/src/views/knowledge/NoteForm.vue +++ b/frontend/src/views/knowledge/NoteForm.vue @@ -2,29 +2,29 @@
- - + + - - + + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -36,7 +36,7 @@ :entity-id="noteId" /> - +
@@ -49,6 +49,7 @@ import { useStudyStore } from "../../store/study"; import { createKnowledgeNote, getKnowledgeNote, updateKnowledgeNote } from "../../api/knowledgeNotes"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -77,7 +78,7 @@ const load = async () => { content: data.content || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -93,15 +94,15 @@ const submit = async () => { }; if (isEdit.value && noteId.value) { await updateKnowledgeNote(studyId.value, noteId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/knowledge/notes/${noteId.value}`); } else { const { data } = await createKnowledgeNote(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/knowledge/notes/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/startup/EthicsDetail.vue b/frontend/src/views/startup/EthicsDetail.vue index 12f159be..d5b3e6aa 100644 --- a/frontend/src/views/startup/EthicsDetail.vue +++ b/frontend/src/views/startup/EthicsDetail.vue @@ -2,22 +2,22 @@
- {{ displayDate(detail.submit_date) }} - {{ displayDate(detail.accept_date) }} - {{ displayDate(detail.meeting_date) }} - {{ displayDate(detail.approved_date) }} - {{ detail.approval_no || "—" }} + {{ displayDate(detail.submit_date) }} + {{ displayDate(detail.accept_date) }} + {{ displayDate(detail.meeting_date) }} + {{ displayDate(detail.approved_date) }} + {{ detail.approval_no || TEXT.common.fallback }} @@ -38,6 +38,7 @@ import { useStudyStore } from "../../store/study"; import { getEthics } from "../../api/startup"; import { displayDate } from "../../utils/display"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -61,7 +62,7 @@ const load = async () => { const { data } = await getEthics(studyId, recordId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/startup/EthicsForm.vue b/frontend/src/views/startup/EthicsForm.vue index d9e4b959..2a4dcffb 100644 --- a/frontend/src/views/startup/EthicsForm.vue +++ b/frontend/src/views/startup/EthicsForm.vue @@ -2,32 +2,32 @@
- - + + - - + + - - + + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -39,7 +39,7 @@ :entity-id="recordId" /> - +
@@ -52,6 +52,7 @@ import { useStudyStore } from "../../store/study"; import { createEthics, getEthics, updateEthics } from "../../api/startup"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -82,7 +83,7 @@ const load = async () => { approval_no: data.approval_no || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -99,15 +100,15 @@ const submit = async () => { }; if (isEdit.value && recordId.value) { await updateEthics(studyId.value, recordId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/startup/ethics/${recordId.value}`); } else { const { data } = await createEthics(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/startup/ethics/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/startup/FeasibilityDetail.vue b/frontend/src/views/startup/FeasibilityDetail.vue index 63da9bfc..5a68eac3 100644 --- a/frontend/src/views/startup/FeasibilityDetail.vue +++ b/frontend/src/views/startup/FeasibilityDetail.vue @@ -2,21 +2,21 @@
- {{ displayDate(detail.submit_date) }} - {{ displayDate(detail.accept_date) }} - {{ displayDate(detail.approved_date) }} - {{ detail.project_no || "—" }} + {{ displayDate(detail.submit_date) }} + {{ displayDate(detail.accept_date) }} + {{ displayDate(detail.approved_date) }} + {{ detail.project_no || TEXT.common.fallback }} @@ -37,6 +37,7 @@ import { useStudyStore } from "../../store/study"; import { getFeasibility } from "../../api/startup"; import { displayDate } from "../../utils/display"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -59,7 +60,7 @@ const load = async () => { const { data } = await getFeasibility(studyId, recordId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/startup/FeasibilityForm.vue b/frontend/src/views/startup/FeasibilityForm.vue index dd593de5..6c870aae 100644 --- a/frontend/src/views/startup/FeasibilityForm.vue +++ b/frontend/src/views/startup/FeasibilityForm.vue @@ -2,29 +2,29 @@
- - + + - - + + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -36,7 +36,7 @@ :entity-id="recordId" /> - +
@@ -49,6 +49,7 @@ import { useStudyStore } from "../../store/study"; import { createFeasibility, getFeasibility, updateFeasibility } from "../../api/startup"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -77,7 +78,7 @@ const load = async () => { project_no: data.project_no || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -93,15 +94,15 @@ const submit = async () => { }; if (isEdit.value && recordId.value) { await updateFeasibility(studyId.value, recordId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/startup/feasibility/${recordId.value}`); } else { const { data } = await createFeasibility(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/startup/feasibility/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/startup/KickoffDetail.vue b/frontend/src/views/startup/KickoffDetail.vue index d6b27ff9..c9e4e5bb 100644 --- a/frontend/src/views/startup/KickoffDetail.vue +++ b/frontend/src/views/startup/KickoffDetail.vue @@ -2,20 +2,20 @@
- {{ displayDate(detail.kickoff_date) }} - - {{ (detail.attendees || []).join("、") || "—" }} + {{ displayDate(detail.kickoff_date) }} + + {{ (detail.attendees || []).join("、") || TEXT.common.fallback }} @@ -37,6 +37,7 @@ import { useStudyStore } from "../../store/study"; import { getKickoff } from "../../api/startup"; import { displayDate } from "../../utils/display"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -57,7 +58,7 @@ const load = async () => { const { data } = await getKickoff(studyId, meetingId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/startup/KickoffForm.vue b/frontend/src/views/startup/KickoffForm.vue index 7b1b52ab..52805722 100644 --- a/frontend/src/views/startup/KickoffForm.vue +++ b/frontend/src/views/startup/KickoffForm.vue @@ -2,28 +2,28 @@
- - + + - + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -35,7 +35,7 @@ :entity-id="meetingId" /> - +
@@ -48,6 +48,7 @@ import { useStudyStore } from "../../store/study"; import { createKickoff, getKickoff, updateKickoff } from "../../api/startup"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -79,7 +80,7 @@ const load = async () => { attendees: Array.isArray(data.attendees) ? data.attendees.join("\n") : "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -93,15 +94,15 @@ const submit = async () => { }; if (isEdit.value && meetingId.value) { await updateKickoff(studyId.value, meetingId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/startup/kickoff/${meetingId.value}`); } else { const { data } = await createKickoff(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/startup/kickoff/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/startup/TrainingDetail.vue b/frontend/src/views/startup/TrainingDetail.vue index 28440191..a422b85c 100644 --- a/frontend/src/views/startup/TrainingDetail.vue +++ b/frontend/src/views/startup/TrainingDetail.vue @@ -2,25 +2,25 @@
- {{ detail.name || "—" }} - {{ detail.role || "—" }} - {{ detail.site_name || "—" }} - {{ detail.trained ? "是" : "否" }} - {{ displayDate(detail.trained_date) }} - {{ detail.authorized ? "是" : "否" }} - {{ displayDate(detail.authorized_date) }} - {{ detail.remark || "—" }} + {{ detail.name || TEXT.common.fallback }} + {{ displayEnum(TEXT.enums.userRole, detail.role) }} + {{ detail.site_name || TEXT.common.fallback }} + {{ detail.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no }} + {{ displayDate(detail.trained_date) }} + {{ detail.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no }} + {{ displayDate(detail.authorized_date) }} + {{ detail.remark || TEXT.common.fallback }} @@ -39,8 +39,9 @@ import { useRoute, useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import { useStudyStore } from "../../store/study"; import { getTrainingAuthorization } from "../../api/startup"; -import { displayDate } from "../../utils/display"; +import { displayDate, displayEnum } from "../../utils/display"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -67,7 +68,7 @@ const load = async () => { const { data } = await getTrainingAuthorization(studyId, recordId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/startup/TrainingForm.vue b/frontend/src/views/startup/TrainingForm.vue index ef209958..37ba6e2c 100644 --- a/frontend/src/views/startup/TrainingForm.vue +++ b/frontend/src/views/startup/TrainingForm.vue @@ -2,41 +2,41 @@
- - + + - - + + - - + + - + - - + + - + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -48,7 +48,7 @@ :entity-id="recordId" /> - +
@@ -61,6 +61,7 @@ import { useStudyStore } from "../../store/study"; import { createTrainingAuthorization, getTrainingAuthorization, updateTrainingAuthorization } from "../../api/startup"; import AttachmentList from "../../components/attachments/AttachmentList.vue"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -97,7 +98,7 @@ const load = async () => { remark: data.remark || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -117,15 +118,15 @@ const submit = async () => { }; if (isEdit.value && recordId.value) { await updateTrainingAuthorization(studyId.value, recordId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/startup/training/${recordId.value}`); } else { const { data } = await createTrainingAuthorization(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/startup/training/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/subjects/SubjectDetail.vue b/frontend/src/views/subjects/SubjectDetail.vue index 70195fd0..7ffefaa7 100644 --- a/frontend/src/views/subjects/SubjectDetail.vue +++ b/frontend/src/views/subjects/SubjectDetail.vue @@ -2,187 +2,195 @@
- {{ detail.subject_no || "—" }} - {{ siteMap[detail.site_id] || "—" }} - {{ detail.status || "—" }} - {{ displayDate(detail.screening_date) }} - {{ displayDate(detail.enrollment_date) }} - {{ displayDate(detail.completion_date) }} - {{ detail.drop_reason || "—" }} + {{ detail.subject_no || TEXT.common.fallback }} + {{ siteMap[detail.site_id] || TEXT.common.fallback }} + {{ displayEnum(TEXT.enums.subjectStatus, detail.status) }} + {{ displayDate(detail.screening_date) }} + {{ displayDate(detail.enrollment_date) }} + {{ displayDate(detail.completion_date) }} + {{ detail.drop_reason || TEXT.common.fallback }} - +
- 新增病史 + {{ TEXT.common.actions.newHistory }}
- + - - + + - +
- +
- 新增访视 + {{ TEXT.common.actions.newVisit }}
- - - + + + + + - + - - + + + + - +
- +
- 新增 AE + {{ TEXT.common.actions.newAe }}
- + - - - - - - + + + + + + + + - +
- + - + - + - + - + - + - + - + - - + + - + - - - + + + - + - + - +
@@ -198,8 +206,9 @@ import { fetchSites } from "../../api/sites"; import { listSubjectHistories, createSubjectHistory, updateSubjectHistory, deleteSubjectHistory } from "../../api/subjectHistories"; import { fetchVisits, createVisit, updateVisit, deleteVisit } from "../../api/visits"; import { fetchAes, createAe, updateAe, deleteAe } from "../../api/aes"; -import { displayDate } from "../../utils/display"; +import { displayDate, displayEnum, displayText } from "../../utils/display"; import StateEmpty from "../../components/StateEmpty.vue"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -280,7 +289,7 @@ const loadSubject = async () => { const { data } = await getSubject(studyId, subjectId); Object.assign(detail, data); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loading.value = false; } @@ -293,7 +302,7 @@ const loadHistories = async () => { const { data } = await listSubjectHistories(studyId, subjectId); historyItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "病史加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingHistory.value = false; } @@ -322,19 +331,19 @@ const saveHistory = async () => { historyDialogVisible.value = false; loadHistories(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } }; const removeHistory = async (row: any) => { if (!studyId || !subjectId) return; - const ok = await ElMessageBox.confirm("确认删除该病史记录?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteSubjectHistory(studyId, subjectId, row.id); loadHistories(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; @@ -345,7 +354,7 @@ const loadVisits = async () => { const { data } = await fetchVisits(studyId, subjectId); visitItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "访视加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingVisits.value = false; } @@ -394,19 +403,19 @@ const saveVisit = async () => { visitDialogVisible.value = false; loadVisits(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } }; const removeVisit = async (row: any) => { if (!studyId || !subjectId) return; - const ok = await ElMessageBox.confirm("确认删除该访视记录?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteVisit(studyId, subjectId, row.id); loadVisits(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; @@ -417,7 +426,7 @@ const loadAes = async () => { const { data } = await fetchAes(studyId, { subject_id: subjectId }); aeItems.value = Array.isArray(data) ? data : data.items || []; } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "AE 加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } finally { loadingAes.value = false; } @@ -459,19 +468,19 @@ const saveAe = async () => { aeDialogVisible.value = false; loadAes(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } }; const removeAe = async (row: any) => { if (!studyId) return; - const ok = await ElMessageBox.confirm("确认删除该 AE 记录?", "提示").catch(() => null); + const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null); if (!ok) return; try { await deleteAe(studyId, row.id); loadAes(); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "删除失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed); } }; diff --git a/frontend/src/views/subjects/SubjectForm.vue b/frontend/src/views/subjects/SubjectForm.vue index 7e687fc5..92e0219e 100644 --- a/frontend/src/views/subjects/SubjectForm.vue +++ b/frontend/src/views/subjects/SubjectForm.vue @@ -2,51 +2,51 @@
- - + + - - + + - + - - - - - - + + + + + + - - + + - - + + - - + + - 保存 - 取消 + {{ TEXT.common.actions.save }} + {{ TEXT.common.actions.cancel }} @@ -60,6 +60,7 @@ import { ElMessage } from "element-plus"; import { useStudyStore } from "../../store/study"; import { createSubject, getSubject, updateSubject } from "../../api/subjects"; import { fetchSites } from "../../api/sites"; +import { TEXT } from "../../locales"; const route = useRoute(); const router = useRouter(); @@ -105,7 +106,7 @@ const load = async () => { drop_reason: data.drop_reason || "", }); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed); } }; @@ -121,7 +122,7 @@ const submit = async () => { drop_reason: form.drop_reason || null, }; await updateSubject(studyId.value, subjectId.value, payload); - ElMessage.success("已保存"); + ElMessage.success(TEXT.common.messages.saveSuccess); router.push(`/subjects/${subjectId.value}`); } else { const payload = { @@ -130,11 +131,11 @@ const submit = async () => { screening_date: form.screening_date || null, }; const { data } = await createSubject(studyId.value, payload); - ElMessage.success("已创建"); + ElMessage.success(TEXT.common.messages.createSuccess); router.push(`/subjects/${data.id}`); } } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "保存失败"); + ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed); } finally { saving.value = false; } diff --git a/frontend/src/views/workbench/MyWorkbench.vue b/frontend/src/views/workbench/MyWorkbench.vue index e44db696..0d83e14c 100644 --- a/frontend/src/views/workbench/MyWorkbench.vue +++ b/frontend/src/views/workbench/MyWorkbench.vue @@ -3,8 +3,10 @@
-

我的工作台

-

欢迎回来,这是基于您的角色 {{ roleLabel }} 聚合的待办事项

+

{{ TEXT.modules.workbench.title }}

+

+ {{ TEXT.modules.workbench.subtitle.replace("{role}", roleLabel) }} +

{{ roleLabel }} @@ -14,7 +16,7 @@
@@ -48,19 +50,19 @@
- 快捷入口 + {{ TEXT.modules.workbench.quickEntryTitle }} - 进入项目 + {{ TEXT.modules.workbench.enterProject }}
@@ -98,6 +100,8 @@ import { useStudyStore } from "../../store/study"; import { fetchAes } from "../../api/aes"; import { listFinanceContracts } from "../../api/financeContracts"; import { Right, Timer, UserFilled, Warning, Money, Collection } from "@element-plus/icons-vue"; +import { displayEnum } from "../../utils/display"; +import { TEXT } from "../../locales"; interface WorkItem { title: string; @@ -121,8 +125,8 @@ const actionList = ref([]); const currentStudyId = computed(() => study.currentStudy?.id || ""); const todayStr = computed(() => new Date().toISOString().slice(0, 10)); -const roleLabel = computed(() => auth.user?.role || "-"); -const emptyText = "暂无需要你处理的事项"; +const roleLabel = computed(() => displayEnum(TEXT.enums.userRole, auth.user?.role)); +const emptyText = TEXT.modules.workbench.quickEmpty; const showPersonalTodo = computed(() => role.value !== "ADMIN"); const todoStorageKey = computed(() => `workbench_todos_${auth.user?.id || "guest"}`); @@ -131,17 +135,17 @@ const quickActions = computed(() => { if (!currentStudyId.value) return []; if (role.value === "CRA") return [ - { label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer }, - { label: "AE 列表", path: "/subjects", icon: Warning }, + { label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer }, + { label: TEXT.modules.workbench.aeList, path: "/subjects", icon: Warning }, ]; if (role.value === "PM") return [ - { label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer }, - { label: "合同费用", path: "/finance/contracts", icon: Money }, - { label: "医学咨询", path: "/knowledge/medical-consult", icon: Collection }, + { label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer }, + { label: TEXT.modules.workbench.contractList, path: "/finance/contracts", icon: Money }, + { label: TEXT.modules.workbench.medicalConsult, path: "/knowledge/medical-consult", icon: Collection }, ]; - if (role.value === "PV") return [{ label: "AE 列表", path: "/subjects", icon: Warning }]; - return [{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer }]; + if (role.value === "PV") return [{ label: TEXT.modules.workbench.aeList, path: "/subjects", icon: Warning }]; + return [{ label: TEXT.modules.workbench.feasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer }]; }); const todayMorePath = computed(() => "/subjects"); @@ -157,7 +161,7 @@ const isOverdue = (d?: string | null) => { }; const toAeItem = (ae: any): WorkItem => ({ - title: ae.term || "不良事件", + title: ae.term || TEXT.modules.workbench.aeTitleFallback, subtitle: study.currentStudy?.name || "", status: ae.status, overdue: isOverdue(ae.expected_resolution_date || ae.updated_at), @@ -165,7 +169,7 @@ const toAeItem = (ae: any): WorkItem => ({ }); const toContractItem = (fi: any): WorkItem => ({ - title: fi.contract_no || "合同费用", + title: fi.contract_no || TEXT.modules.workbench.contractTitleFallback, subtitle: study.currentStudy?.name || "", status: fi.site_name, overdue: false, @@ -186,7 +190,7 @@ const loadData = async () => { buildSections(aes, finances); } catch (e: any) { - ElMessage.error(e?.response?.data?.message || "工作台数据加载失败"); + ElMessage.error(e?.response?.data?.message || TEXT.modules.workbench.loadFailed); } finally { loading.value = false; } diff --git a/frontend/src/views/workbench/components/PersonalTodo.vue b/frontend/src/views/workbench/components/PersonalTodo.vue index a4485293..805747ac 100644 --- a/frontend/src/views/workbench/components/PersonalTodo.vue +++ b/frontend/src/views/workbench/components/PersonalTodo.vue @@ -4,10 +4,10 @@
- 个人备忘 (本地存储) + {{ TEXT.modules.workbench.todoTitle }}
- {{ todos.length }} 条记录 + {{ TEXT.modules.workbench.todoCount.replace("{count}", String(todos.length)) }}
@@ -16,7 +16,7 @@
@@ -25,7 +25,7 @@ - 添加 + {{ TEXT.modules.workbench.todoAdd }}
@@ -51,7 +51,7 @@
- +
@@ -61,6 +61,7 @@ import { onMounted, ref, watch } from "vue"; import { Notebook, EditPen, Plus, Delete } from "@element-plus/icons-vue"; import StateEmpty from "../../../components/StateEmpty.vue"; +import { TEXT } from "../../../locales"; const props = defineProps<{ storageKey: string | undefined }>(); diff --git a/frontend/src/views/workbench/components/SectionCard.vue b/frontend/src/views/workbench/components/SectionCard.vue index 26f16ee1..48dface8 100644 --- a/frontend/src/views/workbench/components/SectionCard.vue +++ b/frontend/src/views/workbench/components/SectionCard.vue @@ -4,7 +4,7 @@
{{ title }} - 查看更多 + {{ TEXT.common.actions.more }}
@@ -29,13 +29,13 @@ :effect="item.overdue ? 'dark' : 'light'" class="status-tag" > - {{ item.status || "待处理" }} + {{ item.status || TEXT.modules.workbench.todoStatusFallback }}
- +
@@ -46,6 +46,7 @@ import { useRouter } from "vue-router"; import { ArrowRight } from "@element-plus/icons-vue"; import StateEmpty from "../../../components/StateEmpty.vue"; import StateLoading from "../../../components/StateLoading.vue"; +import { TEXT } from "../../../locales"; defineProps<{ title: string;