全局中文化
This commit is contained in:
@@ -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
|
||||
|
||||
+13
-13
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+34
-34
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 74 B |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string, { label: string; actionText: string; targetLabel: string }> =
|
||||
TEXT.audit.eventDict;
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, any>, after?: Record<string, any>
|
||||
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<string, any>, after?: Record<string, any>
|
||||
|
||||
export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>): 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<string, string>):
|
||||
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<string, any>)
|
||||
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({
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<el-dialog :model-value="modelValue" :title="isEdit ? '编辑分类' : '新增分类'" width="480px" @close="close">
|
||||
<el-dialog :model-value="modelValue" :title="isEdit ? TEXT.modules.knowledgeMedicalConsult.editCategory : TEXT.modules.knowledgeMedicalConsult.newCategory" width="480px" @close="close">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-form-item :label="TEXT.common.fields.description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-form-item :label="TEXT.common.fields.sortOrder">
|
||||
<el-input-number v-model="form.sort_order" :min="0" :step="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-form-item :label="TEXT.common.fields.enabled">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="close">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<el-card class="panel">
|
||||
<div class="header">
|
||||
<span>分类</span>
|
||||
<span>{{ TEXT.common.labels.category }}</span>
|
||||
<div v-if="canMaintain">
|
||||
<PermissionAction action="faq.edit">
|
||||
<el-button type="primary" size="small" @click="openForm()">新建</el-button>
|
||||
<el-button type="primary" size="small" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newCategory }}</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeCategory" @select="onSelect" class="menu">
|
||||
<el-menu-item index="">全部</el-menu-item>
|
||||
<el-menu-item index="">{{ TEXT.common.labels.all }}</el-menu-item>
|
||||
<el-menu-item v-for="c in sortedCategories" :key="c.id" :index="c.id">
|
||||
<span class="name" :title="c.name">{{ c.name }}</span>
|
||||
<div v-if="canEdit(c) || canDelete" class="actions">
|
||||
<PermissionAction v-if="canEdit(c)" action="faq.edit">
|
||||
<el-button type="text" size="small" @click.stop="openForm(c)">编辑</el-button>
|
||||
<el-button type="text" size="small" @click.stop="openForm(c)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction v-if="canDelete" action="faq.edit">
|
||||
<el-button type="text" size="small" class="danger" @click.stop="onDelete(c)">删除</el-button>
|
||||
<el-button type="text" size="small" class="danger" @click.stop="onDelete(c)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
@@ -36,6 +36,7 @@ import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
categories: FaqCategory[];
|
||||
@@ -76,14 +77,17 @@ const openForm = (cat?: FaqCategory) => {
|
||||
};
|
||||
|
||||
const onDelete = async (cat: FaqCategory) => {
|
||||
const ok = await ElMessageBox.confirm(`确认删除分类「${cat.name}」?`, "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(
|
||||
TEXT.modules.knowledgeMedicalConsult.confirmDeleteCategory.replace("{name}", cat.name),
|
||||
TEXT.common.labels.tips
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFaqCategory(cat.id);
|
||||
ElMessage.success("删除成功");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
emit("refresh");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<el-dialog :model-value="modelValue" :title="isEdit ? '编辑咨询' : '新建咨询'" width="640px" @close="close">
|
||||
<el-dialog :model-value="modelValue" :title="isEdit ? TEXT.modules.knowledgeMedicalConsult.editItem : TEXT.modules.knowledgeMedicalConsult.newItem" width="640px" @close="close">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="分类" prop="category_id">
|
||||
<el-select v-model="form.category_id" placeholder="请选择分类">
|
||||
<el-form-item :label="TEXT.common.labels.category" prop="category_id">
|
||||
<el-select v-model="form.category_id" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题" prop="question">
|
||||
<el-form-item :label="TEXT.common.fields.question" prop="question">
|
||||
<el-input v-model="form.question" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-form-item :label="TEXT.common.fields.enabled">
|
||||
<el-switch v-model="form.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||
<el-button @click="close">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.submit }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -25,6 +25,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus";
|
||||
import { createFaqItem, updateFaqItem } from "../api/faqs";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; item?: any; categories: any[] }>();
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
@@ -41,8 +42,8 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
category_id: [{ required: true, message: "请选择分类", trigger: "change" }],
|
||||
question: [{ required: true, message: "请输入问题", trigger: "blur" }],
|
||||
category_id: [{ required: true, message: requiredMessage(TEXT.common.labels.category), trigger: "change" }],
|
||||
question: [{ required: true, message: requiredMessage(TEXT.common.fields.question), trigger: "blur" }],
|
||||
};
|
||||
|
||||
const isEdit = computed(() => !!props.item);
|
||||
@@ -87,11 +88,11 @@ const onSubmit = async () => {
|
||||
} else {
|
||||
await createFaqItem(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;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<template>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%" @row-click="onRowClick">
|
||||
<el-table-column prop="question" label="问题" min-width="200">
|
||||
<el-table-column prop="question" :label="TEXT.common.fields.question" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" :underline="false" class="question-link">
|
||||
{{ scope.row.question }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="category_id" label="分类" width="140">
|
||||
<el-table-column prop="category_id" :label="TEXT.common.labels.category" width="140">
|
||||
<template #default="scope">
|
||||
{{ categoryMap[scope.row.category_id] || scope.row.category_id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_active" label="启用" width="80">
|
||||
<el-table-column prop="is_active" :label="TEXT.common.fields.enabled" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_active ? 'success' : 'info'">{{ scope.row.is_active ? "是" : "否" }}</el-tag>
|
||||
<el-tag :type="scope.row.is_active ? 'success' : 'info'">
|
||||
{{ scope.row.is_active ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTagType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="180">
|
||||
<template #default="scope">
|
||||
<el-button v-if="canDelete(scope.row)" type="text" size="small" class="danger" @click="remove(scope.row)">
|
||||
删除
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
<PermissionAction v-if="canEdit" action="faq.edit">
|
||||
<el-button
|
||||
@@ -37,7 +39,7 @@
|
||||
@click="toggle(scope.row)"
|
||||
:style="{ color: scope.row.is_active ? '#f56c6c' : '#67c23a' }"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</template>
|
||||
@@ -52,8 +54,9 @@ import { useRouter } from "vue-router";
|
||||
import { deleteFaqItem, updateFaqItem } from "../api/faqs";
|
||||
import type { FaqItem, FaqCategory } from "../api/faqs";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
import { displayDateTime } from "../utils/display";
|
||||
import { displayDateTime, displayEnum } from "../utils/display";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
items: FaqItem[];
|
||||
@@ -83,9 +86,7 @@ const onRowClick = (row: FaqItem, column: any, event: MouseEvent) => {
|
||||
};
|
||||
|
||||
const statusLabel = (status?: string) => {
|
||||
if (status === "RESOLVED") return "已解决";
|
||||
if (status === "PROCESSING") return "处理中";
|
||||
return "待回答";
|
||||
return displayEnum(TEXT.enums.faqStatus, status || "");
|
||||
};
|
||||
|
||||
const statusTagType = (status?: string) => {
|
||||
@@ -99,26 +100,32 @@ const canDelete = (row: FaqItem) => props.canEdit || row.created_by === auth.use
|
||||
|
||||
const toggle = async (row: FaqItem) => {
|
||||
const target = !row.is_active;
|
||||
const ok = await ElMessageBox.confirm(`确认${target ? "启用" : "停用"}此咨询?`, "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(
|
||||
TEXT.modules.knowledgeMedicalConsult.confirmToggleItem.replace("{action}", target ? TEXT.common.actions.enable : TEXT.common.actions.disable),
|
||||
TEXT.common.labels.tips
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateFaqItem(row.id, { is_active: target });
|
||||
ElMessage.success("操作成功");
|
||||
ElMessage.success(TEXT.common.messages.updateSuccess);
|
||||
emit("refresh");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (row: FaqItem) => {
|
||||
const ok = await ElMessageBox.confirm(`确认删除咨询「${row.question}」?`, "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(
|
||||
TEXT.modules.knowledgeMedicalConsult.confirmDeleteItem.replace("{name}", row.question),
|
||||
TEXT.common.labels.tips
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFaqItem(row.id);
|
||||
ElMessage.success("问题已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
emit("refresh");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-aside :width="isCollapsed ? '72px' : '240px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
|
||||
<div class="aside-logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span class="logo-text">CTMS</span>
|
||||
<span class="logo-text">{{ TEXT.common.appName }}</span>
|
||||
</div>
|
||||
<el-menu
|
||||
router
|
||||
@@ -14,73 +14,73 @@
|
||||
>
|
||||
<el-menu-item v-if="!isAdmin" index="/workbench">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>我的工作台</span>
|
||||
<span>{{ TEXT.menu.workbench }}</span>
|
||||
</el-menu-item>
|
||||
|
||||
<template v-if="isAdmin">
|
||||
<div class="menu-divider">管理后台</div>
|
||||
<div class="menu-divider">{{ TEXT.menu.admin }}</div>
|
||||
<el-menu-item index="/admin/users">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>账号管理</span>
|
||||
<span>{{ TEXT.menu.accountManagement }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/projects">
|
||||
<el-icon><Suitcase /></el-icon>
|
||||
<span>项目管理</span>
|
||||
<span>{{ TEXT.menu.projectManagement }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="isAdmin" index="/admin/audit-logs">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>审计日志</span>
|
||||
<span>{{ TEXT.menu.auditLogs }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<template v-if="study.currentStudy">
|
||||
<div class="menu-divider">当前项目</div>
|
||||
<div class="menu-divider">{{ TEXT.menu.currentProject }}</div>
|
||||
<el-menu-item index="/project/overview">
|
||||
<el-icon><House /></el-icon>
|
||||
<span>项目总览</span>
|
||||
<span>{{ TEXT.menu.projectOverview }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="finance">
|
||||
<template #title>
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>费用管理</span>
|
||||
<span>{{ TEXT.menu.finance }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/finance/contracts">合同费用</el-menu-item>
|
||||
<el-menu-item index="/finance/special">特殊费用</el-menu-item>
|
||||
<el-menu-item index="/finance/contracts">{{ TEXT.menu.financeContracts }}</el-menu-item>
|
||||
<el-menu-item index="/finance/special">{{ TEXT.menu.financeSpecials }}</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="drug">
|
||||
<template #title>
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>药品管理</span>
|
||||
<span>{{ TEXT.menu.drug }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/drug/shipments">运输/流向</el-menu-item>
|
||||
<el-menu-item index="/drug/shipments">{{ TEXT.menu.drugShipments }}</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/startup/feasibility-ethics">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
<span>立项与伦理</span>
|
||||
<span>{{ TEXT.menu.startupFeasibilityEthics }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/startup/meeting-auth">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
<span>启动与授权</span>
|
||||
<span>{{ TEXT.menu.startupMeetingAuth }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/subjects">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>受试者管理</span>
|
||||
<span>{{ TEXT.menu.subjects }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/monitoring">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>监查</span>
|
||||
<span>{{ TEXT.menu.monitoring }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/audit">
|
||||
<el-icon><Flag /></el-icon>
|
||||
<span>稽查</span>
|
||||
<span>{{ TEXT.menu.audit }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="knowledge">
|
||||
<template #title>
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>知识库</span>
|
||||
<span>{{ TEXT.menu.knowledge }}</span>
|
||||
</template>
|
||||
<el-menu-item index="/knowledge/medical-consult">医学咨询</el-menu-item>
|
||||
<el-menu-item index="/knowledge/notes">注意事项</el-menu-item>
|
||||
<el-menu-item index="/knowledge/medical-consult">{{ TEXT.menu.knowledgeMedicalConsult }}</el-menu-item>
|
||||
<el-menu-item index="/knowledge/notes">{{ TEXT.menu.knowledgeNotes }}</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
@@ -89,7 +89,7 @@
|
||||
<el-container class="main-container">
|
||||
<el-header class="layout-header">
|
||||
<div class="header-left">
|
||||
<el-button class="collapse-btn" @click="toggleCollapse" aria-label="收缩侧边栏">
|
||||
<el-button class="collapse-btn" @click="toggleCollapse" :aria-label="TEXT.common.labels.collapseSidebar">
|
||||
<el-icon class="collapse-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" />
|
||||
@@ -108,20 +108,20 @@
|
||||
shape="square"
|
||||
class="user-avatar-comp"
|
||||
>
|
||||
{{ (auth.user?.username || auth.user?.email || "U").charAt(0).toUpperCase() }}
|
||||
{{ (auth.user?.username || auth.user?.email || TEXT.common.labels.userInitialFallback).charAt(0).toUpperCase() }}
|
||||
</el-avatar>
|
||||
<span class="username">{{ auth.user?.username || auth.user?.email || "用户" }}</span>
|
||||
<span class="username">{{ auth.user?.username || auth.user?.email || TEXT.common.labels.userFallback }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="user-dropdown-menu">
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>个人中心</span>
|
||||
<span>{{ TEXT.menu.profile }}</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
<span>退出登录</span>
|
||||
<span>{{ TEXT.menu.logout }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -148,6 +148,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import StudySelector from "./StudySelector.vue";
|
||||
import { TEXT } from "../locales";
|
||||
import {
|
||||
Monitor, User, Suitcase, House, Calendar, Flag, ChatDotRound,
|
||||
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="module-list">
|
||||
<div class="module-list-header">
|
||||
<span class="list-title">{{ listTitle }}</span>
|
||||
<span class="list-note">列表区域预留</span>
|
||||
<span class="list-note">{{ TEXT.common.empty.listPlaceholder }}</span>
|
||||
</div>
|
||||
<StateEmpty :title="emptyTitle" :description="emptyDescription" />
|
||||
</div>
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import StateEmpty from "./StateEmpty.vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -32,9 +33,9 @@ withDefaults(
|
||||
}>(),
|
||||
{
|
||||
subtitle: "",
|
||||
listTitle: "列表",
|
||||
emptyTitle: "功能建设中",
|
||||
emptyDescription: "该模块基础骨架已建立,后续将补充完整交互与数据能力。",
|
||||
listTitle: TEXT.common.empty.listPlaceholder,
|
||||
emptyTitle: TEXT.common.empty.building,
|
||||
emptyDescription: TEXT.common.empty.buildingDesc,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="quick-actions-section">
|
||||
<div class="section-header">
|
||||
<el-icon class="header-icon"><Pointer /></el-icon>
|
||||
<span class="header-title">通往业务模块的快捷入口</span>
|
||||
<span class="header-title">{{ TEXT.common.labels.quickActions }}</span>
|
||||
</div>
|
||||
<div class="actions-grid">
|
||||
<div
|
||||
@@ -27,16 +27,17 @@ import {
|
||||
Pointer, ArrowRight, Timer, UserFilled,
|
||||
Management, Money, Collection
|
||||
} from "@element-plus/icons-vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const actions = [
|
||||
{ label: "立项与伦理", path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: "启动与授权", path: "/startup/meeting-auth", icon: Timer },
|
||||
{ label: "受试者管理", path: "/subjects", icon: UserFilled },
|
||||
{ label: "药品流向", path: "/drug/shipments", icon: Management },
|
||||
{ label: "费用管理", path: "/finance/contracts", icon: Money },
|
||||
{ label: "知识库", path: "/knowledge/medical-consult", icon: Collection },
|
||||
{ label: TEXT.menu.startupFeasibilityEthics, path: "/startup/feasibility-ethics", icon: Timer },
|
||||
{ label: TEXT.menu.startupMeetingAuth, path: "/startup/meeting-auth", icon: Timer },
|
||||
{ label: TEXT.menu.subjects, path: "/subjects", icon: UserFilled },
|
||||
{ label: TEXT.menu.drugShipments, path: "/drug/shipments", icon: Management },
|
||||
{ label: TEXT.menu.finance, path: "/finance/contracts", icon: Money },
|
||||
{ label: TEXT.menu.knowledge, path: "/knowledge/medical-consult", icon: Collection },
|
||||
];
|
||||
|
||||
const go = (path: string) => router.push(path);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Document } from "@element-plus/icons-vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -18,7 +19,7 @@ withDefaults(
|
||||
description?: string;
|
||||
}>(),
|
||||
{
|
||||
title: "暂无数据",
|
||||
title: TEXT.common.empty.noData,
|
||||
description: "",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -18,8 +19,8 @@ withDefaults(
|
||||
description?: string;
|
||||
}>(),
|
||||
{
|
||||
title: "加载失败",
|
||||
description: "请稍后重试",
|
||||
title: TEXT.common.messages.loadFailed,
|
||||
description: TEXT.common.messages.requestFailed,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<el-dropdown trigger="click" class="study-selector-dropdown" @command="onCommand">
|
||||
<div class="selector-trigger">
|
||||
<el-icon class="trigger-icon"><OfficeBuilding /></el-icon>
|
||||
<span class="current-study-name">{{ study.currentStudy?.name || "选择项目" }}</span>
|
||||
<span class="current-study-name">{{ study.currentStudy?.name || TEXT.common.empty.selectProject }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="study-dropdown">
|
||||
<el-dropdown-item v-if="loading" disabled>
|
||||
<el-icon><Loading /></el-icon>加载项目中...
|
||||
<el-icon><Loading /></el-icon>{{ TEXT.common.loading }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else-if="!studies.length" disabled>暂无可用项目</el-dropdown-item>
|
||||
<el-dropdown-item v-else-if="!studies.length" disabled>{{ TEXT.common.empty.noData }}</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-for="item in studies"
|
||||
:key="item.id"
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="clear" class="danger-item">
|
||||
<el-icon><Close /></el-icon>退出当前项目
|
||||
<el-icon><Close /></el-icon>{{ TEXT.common.actions.exitProject }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -38,6 +38,7 @@ import { useStudyStore } from "../store/study";
|
||||
import { fetchStudies } from "../api/studies";
|
||||
import type { Study } from "../types/api";
|
||||
import { OfficeBuilding, ArrowDown, Close, Loading } from "@element-plus/icons-vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
@@ -50,7 +51,7 @@ const loadStudies = async () => {
|
||||
const { data } = await fetchStudies();
|
||||
studies.value = 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;
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<div class="thread-composer">
|
||||
<div v-if="quoteItem" class="quote-box">
|
||||
<div class="quote-meta">
|
||||
引用 {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
|
||||
{{ TEXT.common.actions.quote }} {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
|
||||
· {{ displayDateTime(quoteItem.created_at) }}
|
||||
<el-button type="text" size="small" @click="$emit('clear-quote')">取消引用</el-button>
|
||||
<el-button type="text" size="small" @click="$emit('clear-quote')">{{ TEXT.modules.knowledgeMedicalConsult.clearQuote }}</el-button>
|
||||
</div>
|
||||
<div class="quote-content">{{ quoteContent(quoteItem) }}</div>
|
||||
</div>
|
||||
<el-input v-model="contentProxy" type="textarea" rows="3" placeholder="输入内容" />
|
||||
<el-input v-model="contentProxy" type="textarea" rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
<div v-if="allowAttachments" class="upload-row">
|
||||
<div class="upload-label">附件</div>
|
||||
<div class="upload-label">{{ TEXT.common.labels.attachments }}</div>
|
||||
<el-upload v-model:file-list="fileListProxy" :auto-upload="false" multiple list-type="picture" class="thread-upload">
|
||||
<el-button size="small" class="upload-button">上传附件</el-button>
|
||||
<el-button size="small" class="upload-button">{{ TEXT.common.actions.upload }}</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">提交</el-button>
|
||||
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">{{ TEXT.common.actions.submit }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,6 +26,7 @@
|
||||
import { computed } from "vue";
|
||||
import type { UploadUserFile } from "element-plus";
|
||||
import { displayDateTime, displayUser } from "../utils/display";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
@@ -56,7 +57,7 @@ const fileListProxy = computed({
|
||||
set: (val: UploadUserFile[]) => emit("update:fileList", val),
|
||||
});
|
||||
|
||||
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
|
||||
const quoteContent = (item: any) => (item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<div class="thread-meta">
|
||||
<strong>{{ displayUser(item.created_by, { users: userMap, members: memberMap }) }}</strong>
|
||||
<div class="thread-actions">
|
||||
<el-button v-if="canQuote" type="text" size="small" @click="$emit('quote', item)">引用</el-button>
|
||||
<el-button v-if="canQuote" type="text" size="small" @click="$emit('quote', item)">{{ TEXT.common.actions.quote }}</el-button>
|
||||
<el-button v-if="canDelete?.(item)" type="text" size="small" class="danger" @click="$emit('delete', item)">
|
||||
删除
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
<slot name="actions" :item="item" />
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
{{ file.filename }}
|
||||
</div>
|
||||
<el-link v-else :underline="false" class="file-link" @click="download(file.id)">
|
||||
<span class="file-badge">文件</span>
|
||||
<span class="file-badge">{{ TEXT.common.labels.file }}</span>
|
||||
<span class="file-name">{{ file.filename }}</span>
|
||||
</el-link>
|
||||
</div>
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { displayDateTime, displayUser } from "../utils/display";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
items: any[];
|
||||
@@ -62,8 +63,10 @@ defineEmits<{
|
||||
(e: "delete", item: any): void;
|
||||
}>();
|
||||
|
||||
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
|
||||
const contentText = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
|
||||
const quoteContent = (item: any) =>
|
||||
item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback;
|
||||
const contentText = (item: any) =>
|
||||
item?.is_deleted ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback;
|
||||
|
||||
const attachmentUrl = (id: string) => {
|
||||
const token = localStorage.getItem("ctms_token");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<span>附件</span>
|
||||
<span>{{ TEXT.common.labels.attachments }}</span>
|
||||
<AttachmentUploader
|
||||
:study-id="studyId"
|
||||
:entity-type="entityType"
|
||||
@@ -10,23 +10,23 @@
|
||||
/>
|
||||
</div>
|
||||
<el-table :data="attachments" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="filename" label="文件名" min-width="200" />
|
||||
<el-table-column label="大小" width="120">
|
||||
<el-table-column prop="filename" :label="TEXT.common.labels.filename" min-width="200" />
|
||||
<el-table-column :label="TEXT.common.labels.size" width="120">
|
||||
<template #default="scope">{{ formatFileSize(scope.row.file_size ?? scope.row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" width="180">
|
||||
<el-table-column :label="TEXT.common.labels.uploader" width="180">
|
||||
<template #default="scope">
|
||||
{{ uploaderLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploaded_at" label="上传时间" width="180">
|
||||
<el-table-column prop="uploaded_at" :label="TEXT.common.labels.uploadedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ displayDateTime(scope.row.uploaded_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="download(scope.row)">下载</el-button>
|
||||
<el-button link type="primary" size="small" @click="download(scope.row)">{{ TEXT.common.actions.download }}</el-button>
|
||||
<el-button
|
||||
v-if="canDelete(scope.row)"
|
||||
link
|
||||
@@ -34,7 +34,7 @@
|
||||
size="small"
|
||||
@click="remove(scope.row)"
|
||||
>
|
||||
删除
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -52,6 +52,7 @@ import { useAuthStore } from "../../store/auth";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
studyId: string;
|
||||
@@ -72,7 +73,7 @@ const load = async () => {
|
||||
const { data } = await fetchAttachments(props.studyId, props.entityType, props.entityId);
|
||||
attachments.value = data.items || data || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "附件加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ const uploaderLabel = (row: any) => {
|
||||
return acc;
|
||||
}, {});
|
||||
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
|
||||
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || "—";
|
||||
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || TEXT.common.fallback;
|
||||
}
|
||||
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
|
||||
};
|
||||
@@ -116,14 +117,14 @@ const canDelete = (row: any) => {
|
||||
};
|
||||
|
||||
const remove = async (row: any) => {
|
||||
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 deleteAttachment(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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:disabled="loading"
|
||||
:auto-upload="true"
|
||||
>
|
||||
<el-button type="primary" :loading="loading">上传附件</el-button>
|
||||
<el-button type="primary" :loading="loading">{{ TEXT.common.actions.upload }}</el-button>
|
||||
</el-upload>
|
||||
<el-progress v-if="progress > 0 && progress < 100" :percentage="progress" :stroke-width="6" />
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { uploadAttachment } from "../../api/attachments";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
studyId: string;
|
||||
@@ -34,7 +35,7 @@ const doUpload = async (options: any) => {
|
||||
const file: File = options.file;
|
||||
if (!file) return;
|
||||
if (file.size > maxSize * 1024 * 1024) {
|
||||
ElMessage.error(`文件大小不能超过 ${maxSize}MB`);
|
||||
ElMessage.error(`${TEXT.common.messages.fileTooLarge} ${maxSize}MB`);
|
||||
return;
|
||||
}
|
||||
progress.value = 0;
|
||||
@@ -47,10 +48,10 @@ const doUpload = async (options: any) => {
|
||||
}
|
||||
},
|
||||
});
|
||||
ElMessage.success("上传成功");
|
||||
ElMessage.success(TEXT.common.messages.uploadSuccess);
|
||||
emit("uploaded");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "上传失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.uploadFailed);
|
||||
} finally {
|
||||
progress.value = 0;
|
||||
loading.value = false;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:placeholder="placeholder || TEXT.common.placeholders.select"
|
||||
:filterable="filterable"
|
||||
:clearable="clearable"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<el-option v-for="opt in options" :key="String(opt.value)" :label="opt.label" :value="opt.value" />
|
||||
<slot />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
type OptionItem = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string | number | Array<string | number> | null;
|
||||
options?: OptionItem[];
|
||||
multiple?: boolean;
|
||||
placeholder?: string;
|
||||
filterable?: boolean;
|
||||
clearable?: boolean;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: string | number | Array<string | number> | null): void;
|
||||
}>();
|
||||
|
||||
const onUpdate = (value: string | number | Array<string | number> | null) => {
|
||||
emit("update:modelValue", value);
|
||||
};
|
||||
|
||||
const options = computed(() => props.options ?? []);
|
||||
const multiple = computed(() => props.multiple ?? false);
|
||||
const filterable = computed(() => props.filterable ?? true);
|
||||
const clearable = computed(() => props.clearable ?? true);
|
||||
const loading = computed(() => props.loading ?? false);
|
||||
</script>
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Dict } from "./types";
|
||||
import { createDict } from "./utils";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const items = [
|
||||
{ value: "ADMIN", label: "管理员", order: 1 },
|
||||
{ value: "PM", label: "项目负责人", order: 2 },
|
||||
{ value: "CRA", label: "CRA", order: 3 },
|
||||
{ value: "PV", label: "PV", order: 4 },
|
||||
{ value: "IMP", label: "药品管理员", order: 5 },
|
||||
{ value: "ADMIN", label: TEXT.enums.userRole.ADMIN, order: 1 },
|
||||
{ value: "PM", label: TEXT.enums.userRole.PM, order: 2 },
|
||||
{ value: "CRA", label: TEXT.enums.userRole.CRA, order: 3 },
|
||||
{ value: "PV", label: TEXT.enums.userRole.PV, order: 4 },
|
||||
{ value: "IMP", label: TEXT.enums.userRole.IMP, order: 5 },
|
||||
];
|
||||
|
||||
export const roleDict: Dict = createDict(items);
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import type { Dict } from "./types";
|
||||
import { createDict } from "./utils";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const items = [
|
||||
{ value: "TODO", label: "待处理", color: "info", order: 1 },
|
||||
{ value: "DOING", label: "进行中", color: "warning", order: 2 },
|
||||
{ value: "DONE", label: "已完成", color: "success", order: 3 },
|
||||
{ value: "BLOCKED", label: "阻塞", color: "danger", order: 4 },
|
||||
{ value: "DRAFT", label: "草稿", color: "info", order: 10 },
|
||||
{ value: "SUBMITTED", label: "已提交", color: "warning", order: 11 },
|
||||
{ value: "APPROVED", label: "已审批", color: "success", order: 12 },
|
||||
{ value: "REJECTED", label: "已驳回", color: "danger", order: 13 },
|
||||
{ value: "PAID", label: "已支付", color: "success", order: 14 },
|
||||
{ value: "CLOSED", label: "已关闭", color: "success", order: 20 },
|
||||
{ value: "OPEN", label: "开放", color: "info", order: 19 },
|
||||
{ value: "NEW", label: "新建", color: "info", order: 17 },
|
||||
{ value: "FOLLOW_UP", label: "随访中", color: "warning", order: 18 },
|
||||
{ value: "OVERDUE", label: "已逾期", color: "danger", order: 30 },
|
||||
{ value: "SCREENING", label: "筛选中", color: "info", order: 40 },
|
||||
{ value: "ENROLLED", label: "已入组", color: "success", order: 41 },
|
||||
{ value: "COMPLETED", label: "已完成", color: "success", order: 42 },
|
||||
{ value: "DROPPED", label: "已脱落", color: "danger", order: 43 },
|
||||
{ value: "TODO", label: TEXT.enums.generalStatus.TODO, color: "info", order: 1 },
|
||||
{ value: "DOING", label: TEXT.enums.generalStatus.DOING, color: "warning", order: 2 },
|
||||
{ value: "DONE", label: TEXT.enums.generalStatus.DONE, color: "success", order: 3 },
|
||||
{ value: "BLOCKED", label: TEXT.enums.generalStatus.BLOCKED, color: "danger", order: 4 },
|
||||
{ value: "DRAFT", label: TEXT.enums.generalStatus.DRAFT, color: "info", order: 10 },
|
||||
{ value: "SUBMITTED", label: TEXT.enums.generalStatus.SUBMITTED, color: "warning", order: 11 },
|
||||
{ value: "APPROVED", label: TEXT.enums.generalStatus.APPROVED, color: "success", order: 12 },
|
||||
{ value: "REJECTED", label: TEXT.enums.generalStatus.REJECTED, color: "danger", order: 13 },
|
||||
{ value: "PAID", label: TEXT.enums.generalStatus.PAID, color: "success", order: 14 },
|
||||
{ value: "CLOSED", label: TEXT.enums.generalStatus.CLOSED, color: "success", order: 20 },
|
||||
{ value: "OPEN", label: TEXT.enums.generalStatus.OPEN, color: "info", order: 19 },
|
||||
{ value: "NEW", label: TEXT.enums.generalStatus.NEW, color: "info", order: 17 },
|
||||
{ value: "FOLLOW_UP", label: TEXT.enums.generalStatus.FOLLOW_UP, color: "warning", order: 18 },
|
||||
{ value: "OVERDUE", label: TEXT.enums.generalStatus.OVERDUE, color: "danger", order: 30 },
|
||||
{ value: "SCREENING", label: TEXT.enums.generalStatus.SCREENING, color: "info", order: 40 },
|
||||
{ value: "ENROLLED", label: TEXT.enums.generalStatus.ENROLLED, color: "success", order: 41 },
|
||||
{ value: "COMPLETED", label: TEXT.enums.generalStatus.COMPLETED, color: "success", order: 42 },
|
||||
{ value: "DROPPED", label: TEXT.enums.generalStatus.DROPPED, color: "danger", order: 43 },
|
||||
];
|
||||
|
||||
export const statusDict: Dict = createDict(items);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { canDoAction, type StateMachine } from "../state-machine";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
export interface ActionContext {
|
||||
actorRole?: string | null;
|
||||
@@ -27,7 +28,7 @@ export const evaluateAction = (context: ActionContext): ActionDecision => {
|
||||
return {
|
||||
allowed: false,
|
||||
severity: "violation",
|
||||
reason: "无权限执行该操作",
|
||||
reason: TEXT.common.messages.noPermission,
|
||||
auditType: "UNAUTHORIZED_ACTION_ATTEMPT",
|
||||
};
|
||||
}
|
||||
@@ -36,7 +37,7 @@ export const evaluateAction = (context: ActionContext): ActionDecision => {
|
||||
return {
|
||||
allowed: false,
|
||||
severity: "warning",
|
||||
reason: "当前状态不允许该操作",
|
||||
reason: TEXT.common.messages.invalidStateAction,
|
||||
auditType: "INVALID_STATE_TRANSITION_ATTEMPT",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TEXT } from "./zh-CN";
|
||||
|
||||
export { TEXT };
|
||||
|
||||
export const formatEnum = (
|
||||
value: string | number | null | undefined,
|
||||
map: Record<string, string>,
|
||||
fallback: string = TEXT.common.fallback
|
||||
) => {
|
||||
if (value === null || value === undefined || value === "") return fallback;
|
||||
return map[String(value)] || fallback;
|
||||
};
|
||||
|
||||
export const formatBool = (value: boolean | null | undefined) => {
|
||||
if (value === null || value === undefined) return TEXT.common.fallback;
|
||||
return value ? TEXT.common.boolean.yes : TEXT.common.boolean.no;
|
||||
};
|
||||
|
||||
export const requiredMessage = (label: string) => `${label}${TEXT.common.validation.requiredSuffix}`;
|
||||
@@ -0,0 +1,787 @@
|
||||
export const TEXT = {
|
||||
common: {
|
||||
appName: "CTMS",
|
||||
fallback: "—",
|
||||
loading: "加载中",
|
||||
actions: {
|
||||
add: "新增",
|
||||
create: "创建",
|
||||
edit: "编辑",
|
||||
save: "保存",
|
||||
cancel: "取消",
|
||||
delete: "删除",
|
||||
remove: "移除",
|
||||
enable: "启用",
|
||||
disable: "停用",
|
||||
upload: "上传",
|
||||
download: "下载",
|
||||
back: "返回",
|
||||
search: "搜索",
|
||||
reset: "重置",
|
||||
confirm: "确认",
|
||||
close: "关闭",
|
||||
submit: "提交",
|
||||
view: "查看",
|
||||
refresh: "刷新",
|
||||
quote: "引用",
|
||||
more: "查看更多",
|
||||
exitProject: "退出当前项目",
|
||||
login: "登录",
|
||||
register: "注册",
|
||||
newHistory: "新增病史",
|
||||
newVisit: "新增访视",
|
||||
newAe: "新增 AE",
|
||||
},
|
||||
messages: {
|
||||
loadSuccess: "加载成功",
|
||||
loadFailed: "加载失败",
|
||||
saveSuccess: "已保存",
|
||||
saveFailed: "保存失败",
|
||||
createSuccess: "已创建",
|
||||
createFailed: "创建失败",
|
||||
updateSuccess: "已更新",
|
||||
updateFailed: "更新失败",
|
||||
deleteSuccess: "已删除",
|
||||
deleteFailed: "删除失败",
|
||||
uploadSuccess: "上传成功",
|
||||
uploadFailed: "上传失败",
|
||||
actionFailed: "操作失败",
|
||||
noPermission: "无权限执行该操作",
|
||||
networkError: "网络错误,请稍后重试",
|
||||
fileTooLarge: "文件大小不能超过",
|
||||
sessionExpired: "登录已过期,请重新登录",
|
||||
requestFailed: "请求失败,请稍后重试",
|
||||
invalidStateAction: "当前状态不允许该操作",
|
||||
},
|
||||
confirm: {
|
||||
delete: "确认删除该记录?",
|
||||
},
|
||||
placeholders: {
|
||||
input: "请输入",
|
||||
select: "请选择",
|
||||
keyword: "输入关键词",
|
||||
},
|
||||
labels: {
|
||||
attachments: "附件",
|
||||
size: "大小",
|
||||
uploader: "上传人",
|
||||
uploadedAt: "上传时间",
|
||||
actions: "操作",
|
||||
filename: "文件名",
|
||||
role: "角色",
|
||||
quickActions: "通往业务模块的快捷入口",
|
||||
updatedAt: "更新时间",
|
||||
activeOnly: "仅启用",
|
||||
category: "分类",
|
||||
all: "全部",
|
||||
tips: "提示",
|
||||
file: "文件",
|
||||
collapseSidebar: "收缩侧边栏",
|
||||
userInitialFallback: "用",
|
||||
userFallback: "用户",
|
||||
},
|
||||
units: {
|
||||
case: "例",
|
||||
},
|
||||
fields: {
|
||||
subjectNo: "受试者编号",
|
||||
site: "分中心",
|
||||
status: "状态",
|
||||
keyword: "关键词",
|
||||
title: "标题",
|
||||
level: "重要等级",
|
||||
name: "姓名",
|
||||
email: "邮箱",
|
||||
department: "部门",
|
||||
avatar: "头像",
|
||||
projectName: "项目名称",
|
||||
projectCode: "项目编号",
|
||||
sponsor: "申办方",
|
||||
protocolNo: "方案号",
|
||||
phase: "分期",
|
||||
siteName: "中心名称",
|
||||
city: "城市",
|
||||
phone: "电话",
|
||||
contact: "负责人",
|
||||
description: "描述",
|
||||
sortOrder: "排序",
|
||||
enabled: "启用",
|
||||
category: "分类",
|
||||
question: "问题",
|
||||
screeningDate: "筛选日期",
|
||||
enrollmentDate: "入组日期",
|
||||
completionDate: "完成日期",
|
||||
dropReason: "退出原因",
|
||||
recordDate: "日期",
|
||||
content: "内容",
|
||||
visitCode: "访视编号",
|
||||
visitName: "访视名称",
|
||||
plannedDate: "计划日期",
|
||||
actualDate: "实际日期",
|
||||
windowStart: "窗口开始",
|
||||
windowEnd: "窗口结束",
|
||||
notes: "备注",
|
||||
event: "事件",
|
||||
onsetDate: "发生日期",
|
||||
seriousness: "严重程度",
|
||||
severity: "严重等级",
|
||||
causality: "关联性",
|
||||
outcome: "结局",
|
||||
submitDate: "递交日期",
|
||||
acceptDate: "受理日期",
|
||||
approvedDate: "批准日期",
|
||||
meetingDate: "会议日期",
|
||||
approvalNo: "批件号",
|
||||
projectNo: "项目编号",
|
||||
kickoffDate: "启动会日期",
|
||||
attendees: "参训人员",
|
||||
person: "人员",
|
||||
trained: "已培训",
|
||||
authorized: "已授权",
|
||||
trainedDate: "培训日期",
|
||||
authorizedDate: "授权日期",
|
||||
direction: "类型",
|
||||
trackingNo: "运单号",
|
||||
shipDate: "日期",
|
||||
drugDesc: "物品描述",
|
||||
carrier: "快递公司",
|
||||
fromParty: "发出方",
|
||||
toParty: "接收方",
|
||||
occurDate: "发生日期",
|
||||
amount: "金额",
|
||||
type: "类型",
|
||||
staff: "人员",
|
||||
contractNo: "合同编号",
|
||||
signedDate: "签署日期",
|
||||
currency: "币种",
|
||||
currencyDefault: "CNY",
|
||||
remark: "备注",
|
||||
},
|
||||
empty: {
|
||||
noData: "暂无数据",
|
||||
building: "功能建设中",
|
||||
buildingDesc: "该模块基础骨架已建立,后续将补充完整交互与数据能力。",
|
||||
listPlaceholder: "列表区域预留",
|
||||
selectProject: "请先选择一个项目",
|
||||
selectSite: "请先选择一个分中心",
|
||||
},
|
||||
validation: {
|
||||
requiredSuffix: "不能为空",
|
||||
invalidEmail: "邮箱格式不正确",
|
||||
},
|
||||
boolean: {
|
||||
yes: "是",
|
||||
no: "否",
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
statusLabel: "状态:",
|
||||
objectLabel: "对象:",
|
||||
changeLabel: "变更:",
|
||||
emptyValue: "-",
|
||||
resultSuccess: "成功",
|
||||
resultFail: "失败",
|
||||
eventFallback: "系统操作",
|
||||
actionFallback: "执行了操作",
|
||||
targetFallback: "对象",
|
||||
currentUser: "当前用户",
|
||||
localClient: "客户端",
|
||||
exportColumns: {
|
||||
timestamp: "审计时间",
|
||||
actorName: "操作人姓名",
|
||||
actorId: "操作人账号",
|
||||
actorRoleLabel: "操作人角色",
|
||||
eventLabel: "操作类型",
|
||||
targetTypeLabel: "操作对象类型",
|
||||
targetIdentifier: "操作对象标识",
|
||||
description: "操作描述",
|
||||
beforeStatus: "操作前状态",
|
||||
afterStatus: "操作后状态",
|
||||
resultLabel: "操作结果",
|
||||
reason: "拒绝原因",
|
||||
ip: "IP 地址",
|
||||
remark: "备注",
|
||||
},
|
||||
eventDict: {
|
||||
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: "数据问题" },
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
workbench: "我的工作台",
|
||||
admin: "管理后台",
|
||||
accountManagement: "账号管理",
|
||||
projectManagement: "项目管理",
|
||||
auditLogs: "审计日志",
|
||||
currentProject: "当前项目",
|
||||
projectOverview: "项目总览",
|
||||
finance: "费用管理",
|
||||
financeContracts: "合同费用",
|
||||
financeSpecials: "特殊费用",
|
||||
drug: "药品管理",
|
||||
drugShipments: "运输/流向",
|
||||
startupFeasibilityEthics: "立项与伦理",
|
||||
startupMeetingAuth: "启动与授权",
|
||||
subjects: "受试者管理",
|
||||
monitoring: "监查",
|
||||
audit: "稽查",
|
||||
knowledge: "知识库",
|
||||
knowledgeMedicalConsult: "医学咨询",
|
||||
knowledgeNotes: "注意事项",
|
||||
profile: "个人中心",
|
||||
logout: "退出登录",
|
||||
},
|
||||
modules: {
|
||||
auth: {
|
||||
brandSubtitle: "临床试验管理系统",
|
||||
loginTitle: "欢迎回来",
|
||||
loginDesc: "请输入您的凭据以访问系统",
|
||||
emailLabel: "电子邮箱",
|
||||
passwordLabel: "登录密码",
|
||||
emailPlaceholder: "name@company.com",
|
||||
passwordPlaceholder: "最小 8 位字符",
|
||||
confirmPasswordLabel: "确认密码",
|
||||
confirmPasswordPlaceholder: "再次输入密码",
|
||||
fullNameLabel: "姓名",
|
||||
fullNamePlaceholder: "请输入姓名",
|
||||
departmentLabel: "部门",
|
||||
departmentPlaceholder: "请输入部门",
|
||||
remember: "记住我",
|
||||
forgot: "忘记密码?",
|
||||
forgotTitle: "找回密码",
|
||||
forgotDesc: "请联系系统管理员重置密码",
|
||||
loginButton: "登录",
|
||||
registerPrompt: "还没有账号?",
|
||||
registerNow: "立即注册",
|
||||
footer: "© 2025 CTMS Enterprise. 专业临床研究团队支持。",
|
||||
authFailed: "认证失败,请检查您的凭据",
|
||||
registerTitle: "创建账号",
|
||||
registerDesc: "提交后需管理员审核通过方可登录",
|
||||
registerButton: "提交注册",
|
||||
registerSuccess: "注册成功,等待管理员审核",
|
||||
backToLogin: "已有账号?返回登录",
|
||||
passwordRuleMin: "密码至少 8 位",
|
||||
passwordRuleMix: "需包含字母和数字",
|
||||
passwordMismatch: "两次输入的密码不一致",
|
||||
},
|
||||
projectOverview: {
|
||||
title: "项目总览",
|
||||
subtitle: "当前项目运行状态与关键指标",
|
||||
kpiProgress: "节点完成率",
|
||||
kpiOverdueAes: "逾期 AE",
|
||||
kpiOverdueAesHint: "需尽快跟进处理",
|
||||
kpiFinanceTotal: "费用总额",
|
||||
kpiFinancePaid: "累计已支付",
|
||||
alertWarning: "存在逾期未处理事项,请及时关注项目的合规性与进度。",
|
||||
alertOk: "项目运行状态良好,所有关键指标均在可控范围内。",
|
||||
noMilestones: "暂无节点",
|
||||
progressLabel: "总体进度: {rate}",
|
||||
},
|
||||
financeContracts: {
|
||||
title: "合同费用",
|
||||
subtitle: "维护分中心合同费用与附件",
|
||||
empty: "暂无合同费用记录",
|
||||
newTitle: "新增合同费用",
|
||||
editTitle: "编辑合同费用",
|
||||
formSubtitle: "填写分中心合同信息并保存",
|
||||
uploadHint: "保存后可上传合同附件",
|
||||
detailTitle: "合同费用详情",
|
||||
detailSubtitle: "查看合同费用与附件",
|
||||
},
|
||||
financeSpecials: {
|
||||
title: "特殊费用",
|
||||
subtitle: "记录差旅、住宿等特殊费用",
|
||||
empty: "暂无特殊费用记录",
|
||||
newTitle: "新增特殊费用",
|
||||
editTitle: "编辑特殊费用",
|
||||
uploadHint: "保存后可上传凭证附件",
|
||||
detailTitle: "特殊费用详情",
|
||||
detailSubtitle: "查看费用记录与附件",
|
||||
},
|
||||
drugShipments: {
|
||||
title: "药品运输/流向",
|
||||
subtitle: "登记寄送与回收信息",
|
||||
recordLabel: "运输记录",
|
||||
empty: "暂无运输记录",
|
||||
confirmDelete: "确认删除该运输记录?",
|
||||
newTitle: "新增运输记录",
|
||||
editTitle: "编辑运输记录",
|
||||
formSubtitle: "记录寄送/回收流向",
|
||||
uploadHint: "保存后可上传交接材料",
|
||||
detailTitle: "运输记录详情",
|
||||
detailSubtitle: "查看运输/流向信息与附件",
|
||||
},
|
||||
startupFeasibilityEthics: {
|
||||
title: "立项与伦理",
|
||||
subtitle: "立项与伦理记录管理",
|
||||
feasibilityTab: "立项记录",
|
||||
ethicsTab: "伦理记录",
|
||||
emptyFeasibility: "暂无立项记录",
|
||||
emptyEthics: "暂无伦理记录",
|
||||
feasibilityNewTitle: "新增立项记录",
|
||||
feasibilityEditTitle: "编辑立项记录",
|
||||
feasibilityFormSubtitle: "维护立项递交与审批信息",
|
||||
feasibilityUploadHint: "保存后可上传立项附件",
|
||||
feasibilityDetailTitle: "立项记录详情",
|
||||
feasibilityDetailSubtitle: "查看立项记录与附件",
|
||||
ethicsNewTitle: "新增伦理记录",
|
||||
ethicsEditTitle: "编辑伦理记录",
|
||||
ethicsFormSubtitle: "维护伦理递交与审批信息",
|
||||
ethicsUploadHint: "保存后可上传伦理附件",
|
||||
ethicsDetailTitle: "伦理记录详情",
|
||||
ethicsDetailSubtitle: "查看伦理记录与附件",
|
||||
},
|
||||
startupMeetingAuth: {
|
||||
title: "启动与授权",
|
||||
subtitle: "启动会与培训授权管理",
|
||||
kickoffTab: "启动会记录",
|
||||
trainingTab: "培训与授权",
|
||||
emptyKickoff: "暂无启动会记录",
|
||||
emptyTraining: "暂无培训授权人员",
|
||||
kickoffNewTitle: "新增启动会",
|
||||
kickoffEditTitle: "编辑启动会",
|
||||
kickoffFormSubtitle: "记录启动会日期与参训人员",
|
||||
kickoffUploadHint: "保存后可上传会议附件",
|
||||
kickoffDetailTitle: "启动会详情",
|
||||
kickoffDetailSubtitle: "查看启动会信息与附件",
|
||||
attendeesPlaceholder: "每行一个姓名",
|
||||
trainingNewTitle: "新增人员",
|
||||
trainingEditTitle: "编辑培训授权",
|
||||
trainingFormSubtitle: "维护人员培训与授权状态",
|
||||
trainingUploadHint: "保存后可上传授权附件",
|
||||
trainingDetailTitle: "培训授权详情",
|
||||
trainingDetailSubtitle: "查看人员培训与授权信息",
|
||||
},
|
||||
subjectManagement: {
|
||||
title: "受试者管理",
|
||||
subtitle: "受试者、病史、访视、AE 综合管理",
|
||||
subjectLabel: "受试者",
|
||||
empty: "暂无受试者记录",
|
||||
},
|
||||
subjectForm: {
|
||||
titleEdit: "编辑受试者",
|
||||
titleNew: "新增受试者",
|
||||
subtitle: "维护受试者基本信息",
|
||||
},
|
||||
subjectDetail: {
|
||||
title: "受试者详情",
|
||||
subtitle: "查看病史、访视与 AE 信息",
|
||||
tabs: {
|
||||
history: "病史",
|
||||
visits: "访视",
|
||||
ae: "AE 信息",
|
||||
},
|
||||
emptyHistory: "暂无病史记录",
|
||||
emptyVisits: "暂无访视记录",
|
||||
emptyAe: "暂无 AE 记录",
|
||||
dialogHistory: "病史记录",
|
||||
dialogVisit: "访视记录",
|
||||
dialogAe: "AE 记录",
|
||||
},
|
||||
knowledgeMedicalConsult: {
|
||||
title: "医学咨询",
|
||||
subtitle: "常见问题与答复",
|
||||
detailTitle: "医学咨询详情",
|
||||
newCategory: "新增分类",
|
||||
editCategory: "编辑分类",
|
||||
newItem: "新建咨询",
|
||||
editItem: "编辑咨询",
|
||||
confirmDeleteCategory: "确认删除分类「{name}」?",
|
||||
confirmDeleteItem: "确认删除咨询「{name}」?",
|
||||
confirmToggleItem: "确认{action}此咨询?",
|
||||
questionDesc: "问题描述",
|
||||
editQuestion: "编辑问题",
|
||||
confirmResolved: "确认解决",
|
||||
asker: "提问人",
|
||||
askedAt: "提问时间",
|
||||
bestAnswer: "最佳答案",
|
||||
adopted: "已采纳",
|
||||
replyDeleted: "回复已删除",
|
||||
replies: "回复",
|
||||
replyCount: "共 {count} 条",
|
||||
noReplyPermission: "当前角色无权限回复",
|
||||
emptyReplies: "暂无回复",
|
||||
setBest: "设为最佳",
|
||||
cancelBest: "取消最佳",
|
||||
confirmBestReply: "设为最佳答案并确认已解决?",
|
||||
bestReplySet: "已设为最佳答案",
|
||||
bestReplyCleared: "已取消最佳答案",
|
||||
resolvedSuccess: "已设置为已解决",
|
||||
replyRequired: "请输入回复内容",
|
||||
replySubmitted: "回复已提交",
|
||||
replyFailed: "回复失败",
|
||||
replyLoadFailed: "回复加载失败",
|
||||
clearQuote: "取消引用",
|
||||
quoteDeleted: "内容已删除",
|
||||
},
|
||||
knowledgeNotes: {
|
||||
title: "注意事项",
|
||||
subtitle: "分中心特殊要求记录",
|
||||
empty: "暂无注意事项记录",
|
||||
confirmDelete: "确认删除该注意事项?",
|
||||
newTitle: "新增注意事项",
|
||||
editTitle: "编辑注意事项",
|
||||
formSubtitle: "记录分中心特殊问题",
|
||||
uploadHint: "保存后可上传附件",
|
||||
detailTitle: "注意事项详情",
|
||||
detailSubtitle: "查看注意事项与附件",
|
||||
},
|
||||
monitoring: {
|
||||
title: "监查",
|
||||
subtitle: "功能建设中",
|
||||
listTitle: "监查记录列表",
|
||||
emptyDescription: "监查功能正在搭建基础能力,敬请期待。",
|
||||
},
|
||||
audit: {
|
||||
title: "稽查",
|
||||
subtitle: "功能建设中",
|
||||
listTitle: "稽查记录列表",
|
||||
emptyDescription: "稽查模块正在准备基础框架,敬请期待。",
|
||||
},
|
||||
adminUserApproval: {
|
||||
title: "注册审核",
|
||||
subtitle: "仅显示待审核账号,审核通过后即可登录",
|
||||
createdAt: "注册时间",
|
||||
approve: "审核通过",
|
||||
reject: "拒绝",
|
||||
loadFailed: "加载待审核用户失败",
|
||||
approveSuccess: "已通过审核",
|
||||
approveFailed: "审核失败",
|
||||
rejectSuccess: "已拒绝",
|
||||
rejectFailed: "拒绝失败",
|
||||
},
|
||||
adminUsers: {
|
||||
title: "账号治理(系统登录账号)",
|
||||
subtitle: "账号用于登录系统,本身不具备项目或角色权限",
|
||||
userLabel: "用户",
|
||||
createdAt: "创建时间",
|
||||
newTitle: "新建用户",
|
||||
editTitle: "编辑用户",
|
||||
resetPassword: "重置密码",
|
||||
accountStatus: "账号状态",
|
||||
passwordOptional: "新密码(可选)",
|
||||
passwordInitial: "初始密码",
|
||||
passwordOptionalHint: "留空则不修改密码",
|
||||
passwordInitialHint: "请输入初始密码",
|
||||
passwordInitialRequired: "请输入初始密码",
|
||||
keepAdminWarning: "至少保留一个管理员账号,不能禁用该账号",
|
||||
loadFailed: "用户列表加载失败",
|
||||
updateSuccess: "用户已更新",
|
||||
createSuccess: "用户已创建",
|
||||
deleteConfirm: "删除后账号将无法登录且无法恢复,需先在各项目成员配置中移除该账号。请输入当前管理员密码确认删除。",
|
||||
deleteTitle: "危险操作:删除账号",
|
||||
deletePasswordPlaceholder: "请输入 admin 密码",
|
||||
deleteConfirmButton: "确认删除",
|
||||
deleteSuccess: "账号已删除",
|
||||
deleteFailed: "删除失败",
|
||||
enableConfirm: "确认启用该用户账号?",
|
||||
disableConfirm: "该操作将影响用户账号,请确认是否继续",
|
||||
enableTitle: "启用账号",
|
||||
disableTitle: "禁用账号",
|
||||
enableSuccess: "已启用",
|
||||
disableSuccess: "已禁用",
|
||||
resetTitle: "重置密码",
|
||||
resetWarningTitle: "该操作将影响用户账号,请确认是否继续",
|
||||
resetWarningDesc: "系统将生成临时密码,需再次输入用户名才能提交。",
|
||||
resetTarget: "重置对象:",
|
||||
confirmUsername: "确认用户名",
|
||||
confirmUsernameHint: "请输入用户名以确认",
|
||||
passwordMode: "密码方式",
|
||||
passwordAuto: "系统生成临时密码",
|
||||
passwordManual: "手动输入新密码",
|
||||
tempPassword: "临时密码",
|
||||
newPassword: "新密码",
|
||||
regenerate: "重新生成",
|
||||
manualPasswordHint: "请输入新密码",
|
||||
passwordHint: "不会明文展示,请通过安全渠道告知用户",
|
||||
tempPasswordError: "临时密码生成异常",
|
||||
manualPasswordRequired: "请输入新密码",
|
||||
confirmMismatch: "确认用户名不匹配,无法提交",
|
||||
resetConfirmPrompt: "该操作将影响用户账号,请确认是否继续",
|
||||
resetConfirmTitle: "确认重置密码",
|
||||
resetConfirm: "确认重置",
|
||||
resetSuccess: "密码已重置",
|
||||
resetFailed: "重置失败",
|
||||
},
|
||||
adminProjects: {
|
||||
title: "项目治理",
|
||||
subtitle: "项目是独立业务实体,账号加入项目后才成为项目成员(角色在成员配置中设置)",
|
||||
projectLabel: "项目",
|
||||
members: "项目账号/成员配置",
|
||||
sites: "中心管理",
|
||||
enter: "进入项目",
|
||||
createdAt: "创建时间",
|
||||
newTitle: "新建项目",
|
||||
editTitle: "编辑项目",
|
||||
sponsorPlaceholder: "申办方(可选)",
|
||||
protocolPlaceholder: "方案号(可选)",
|
||||
phasePlaceholder: "研究分期(可选)",
|
||||
loadFailed: "项目列表加载失败",
|
||||
createSuccess: "项目已创建",
|
||||
updateSuccess: "项目已更新",
|
||||
detailTitle: "项目详情",
|
||||
basicInfo: "项目基本信息",
|
||||
filesTitle: "项目通用文件",
|
||||
},
|
||||
adminProjectMembers: {
|
||||
title: "项目成员配置(角色仅在当前项目内生效)",
|
||||
subtitle: "同一账号在不同项目可拥有不同角色,角色授权仅在本页进行",
|
||||
projectPrefix: "项目:",
|
||||
memberLabel: "成员",
|
||||
username: "用户名",
|
||||
projectRole: "项目角色",
|
||||
addedAt: "加入时间",
|
||||
disabledGlobal: "全局已禁用",
|
||||
disabled: "已禁用",
|
||||
disabledGlobalHint: "请前往【账号治理】启用该账号",
|
||||
disabledGlobalDesc: "该账号已在系统级被禁用,无法在任何项目中使用",
|
||||
newTitle: "新增成员",
|
||||
user: "用户",
|
||||
userPlaceholder: "请选择用户",
|
||||
rolePlaceholder: "请选择角色",
|
||||
addSuccess: "成员已添加",
|
||||
addFailed: "添加失败",
|
||||
loadFailed: "成员列表加载失败",
|
||||
roleUpdated: "角色已更新",
|
||||
disableConfirm: "该操作将影响项目成员,请确认是否继续",
|
||||
disableTitle: "停用成员",
|
||||
disableSuccess: "成员已停用",
|
||||
enableSuccess: "成员已启用",
|
||||
removeConfirm: "确认将该账号从本项目移除?账号本身不会被删除。",
|
||||
removeTitle: "删除成员",
|
||||
removeSuccess: "已从项目移除",
|
||||
},
|
||||
adminSites: {
|
||||
title: "中心管理",
|
||||
projectPrefix: "项目:",
|
||||
siteLabel: "中心",
|
||||
piLabel: "PI",
|
||||
loadFailed: "中心列表加载失败",
|
||||
newTitle: "新建中心",
|
||||
editTitle: "编辑中心",
|
||||
cityPlaceholder: "所在城市(可选)",
|
||||
piPlaceholder: "PI(可选)",
|
||||
phonePlaceholder: "电话(可选)",
|
||||
contactPlaceholder: "选择项目成员作为负责人,可多选",
|
||||
updateSuccess: "中心已更新",
|
||||
createSuccess: "中心已创建",
|
||||
disableConfirm: "该操作将影响中心数据,请确认是否继续",
|
||||
disableTitle: "停用中心",
|
||||
disableSuccess: "已停用",
|
||||
enableSuccess: "已启用",
|
||||
craBindTitle: "CRA 绑定",
|
||||
sitePrefix: "中心:",
|
||||
craSelect: "选择 CRA",
|
||||
craSelectPlaceholder: "请选择 CRA 账号",
|
||||
craHint: "支持多选,保存后将以用户名写入中心联系人字段",
|
||||
craSave: "保存绑定",
|
||||
craSaveSuccess: "绑定已保存",
|
||||
},
|
||||
adminAuditLogs: {
|
||||
filterEvent: "操作类型",
|
||||
filterOperator: "操作人",
|
||||
filterResult: "结果",
|
||||
rangeStart: "开始",
|
||||
rangeEnd: "结束",
|
||||
exportSystem: "导出系统审计",
|
||||
exportProject: "导出项目审计",
|
||||
loadFailed: "审计日志加载失败",
|
||||
exportLoadFailed: "导出数据加载失败",
|
||||
exportConfirm: "该文件包含敏感审计信息,仅限授权人员使用。确认导出吗?",
|
||||
exportConfirmTitle: "审计导出确认",
|
||||
exportEmpty: "无可导出的审计记录",
|
||||
exportSystemName: "系统审计",
|
||||
exportProjectName: "项目审计",
|
||||
columns: {
|
||||
time: "时间",
|
||||
actor: "操作人",
|
||||
event: "操作类型",
|
||||
action: "操作内容",
|
||||
target: "操作对象",
|
||||
diff: "变更详情",
|
||||
result: "结果",
|
||||
},
|
||||
},
|
||||
workbench: {
|
||||
title: "我的工作台",
|
||||
subtitle: "欢迎回来,这是基于您的角色 {role} 聚合的待办事项",
|
||||
selectProject: "请先在顶部导航选择一个项目,以开启完整的工作台功能。",
|
||||
todayTitle: "今日待办",
|
||||
overdueTitle: "已逾期事项",
|
||||
actionTitle: "需要我处理的事项",
|
||||
todayEmpty: "今天暂时没有需要紧急处理的业务事项",
|
||||
overdueEmpty: "太棒了!目前没有任何逾期待处理的事项",
|
||||
actionEmpty: "暂无需要您处理的事项",
|
||||
quickEntryTitle: "快捷入口",
|
||||
enterProject: "进入项目",
|
||||
quickEmpty: "暂无需要你处理的事项",
|
||||
aeList: "AE 列表",
|
||||
contractList: "合同费用",
|
||||
medicalConsult: "医学咨询",
|
||||
feasibilityEthics: "立项与伦理",
|
||||
aeTitleFallback: "不良事件",
|
||||
contractTitleFallback: "合同费用",
|
||||
todoTitle: "个人备忘 (本地存储)",
|
||||
todoCount: "{count} 条记录",
|
||||
todoPlaceholder: "有什么需要记录的事项吗?(回车添加)",
|
||||
todoAdd: "添加",
|
||||
todoEmpty: "暂无待办事项,记录一些备忘吧",
|
||||
todoStatusFallback: "待处理",
|
||||
loadFailed: "工作台数据加载失败",
|
||||
},
|
||||
permissions: {
|
||||
subjectEnroll: "仅 PM/CRA 可更新受试者状态",
|
||||
subjectComplete: "仅 PM/CRA 可更新受试者状态",
|
||||
subjectDrop: "仅 PM/CRA 可更新受试者状态",
|
||||
aeClose: "仅 PM/PV/ADMIN 可关闭 AE",
|
||||
faqEdit: "仅 PM/ADMIN 可维护 FAQ",
|
||||
faqCreate: "仅项目成员可新建 FAQ",
|
||||
faqReply: "仅项目成员可回复 FAQ",
|
||||
projectMembersManage: "仅 ADMIN / PM 可调整项目成员",
|
||||
siteManage: "仅 ADMIN / PM 可管理中心",
|
||||
siteCraBind: "仅 ADMIN / PM 可绑定 CRA",
|
||||
default: "当前角色无权执行该操作",
|
||||
},
|
||||
profile: {
|
||||
title: "个人设置",
|
||||
subtitle: "更新个人信息或修改登录密码",
|
||||
uploadAvatar: "上传头像",
|
||||
currentPassword: "当前密码",
|
||||
currentPasswordHint: "修改密码时必填",
|
||||
newPassword: "新密码",
|
||||
newPasswordHint: "不修改请留空",
|
||||
confirmPassword: "确认新密码",
|
||||
confirmPasswordHint: "再次输入新密码",
|
||||
passwordRequired: "修改密码需输入当前密码",
|
||||
passwordRule: "至少 8 位且包含字母与数字",
|
||||
passwordMismatch: "两次输入的密码不一致",
|
||||
updateSuccess: "已更新",
|
||||
updateFailed: "更新失败",
|
||||
avatarUpdated: "头像已更新",
|
||||
avatarUploadFailed: "头像上传失败",
|
||||
},
|
||||
},
|
||||
enums: {
|
||||
userRole: {
|
||||
ADMIN: "管理员",
|
||||
PM: "项目负责人",
|
||||
CRA: "CRA",
|
||||
PV: "PV",
|
||||
IMP: "药品管理员",
|
||||
},
|
||||
userStatus: {
|
||||
PENDING: "待审核",
|
||||
ACTIVE: "启用",
|
||||
REJECTED: "已拒绝",
|
||||
DISABLED: "已禁用",
|
||||
},
|
||||
subjectStatus: {
|
||||
SCREENING: "筛选中",
|
||||
ENROLLED: "已入组",
|
||||
COMPLETED: "已完成",
|
||||
DROPPED: "已脱落",
|
||||
},
|
||||
visitStatus: {
|
||||
PLANNED: "计划中",
|
||||
DONE: "已完成",
|
||||
MISSED: "未完成",
|
||||
CANCELLED: "已取消",
|
||||
},
|
||||
visitName: {
|
||||
Screening: "筛选访视",
|
||||
Baseline: "基线访视",
|
||||
"Follow-up 1": "随访 1",
|
||||
"Follow-up 2": "随访 2",
|
||||
},
|
||||
aeSeriousness: {
|
||||
SERIOUS: "严重",
|
||||
NON_SERIOUS: "一般",
|
||||
},
|
||||
aeSeverity: {
|
||||
MILD: "轻度",
|
||||
MODERATE: "中度",
|
||||
SEVERE: "重度",
|
||||
},
|
||||
aeStatus: {
|
||||
NEW: "新建",
|
||||
FOLLOW_UP: "随访中",
|
||||
CLOSED: "已关闭",
|
||||
},
|
||||
shipmentDirection: {
|
||||
寄送: "寄送",
|
||||
回收: "回收",
|
||||
SEND: "寄送",
|
||||
RETURN: "回收",
|
||||
},
|
||||
shipmentStatus: {
|
||||
待寄出: "待寄出",
|
||||
运输中: "运输中",
|
||||
已签收: "已签收",
|
||||
已回收: "已回收",
|
||||
异常: "异常",
|
||||
PENDING: "待寄出",
|
||||
IN_TRANSIT: "运输中",
|
||||
SIGNED: "已签收",
|
||||
RETURNED: "已回收",
|
||||
EXCEPTION: "异常",
|
||||
},
|
||||
financeSpecialType: {
|
||||
差旅: "差旅",
|
||||
住宿: "住宿",
|
||||
交通: "交通",
|
||||
其他: "其他",
|
||||
TRAVEL: "差旅",
|
||||
HOTEL: "住宿",
|
||||
TRANSPORT: "交通",
|
||||
OTHER: "其他",
|
||||
},
|
||||
projectStatus: {
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
},
|
||||
faqStatus: {
|
||||
PENDING: "待回答",
|
||||
PROCESSING: "处理中",
|
||||
RESOLVED: "已解决",
|
||||
},
|
||||
generalStatus: {
|
||||
TODO: "待处理",
|
||||
DOING: "进行中",
|
||||
DONE: "已完成",
|
||||
BLOCKED: "阻塞",
|
||||
DRAFT: "草稿",
|
||||
SUBMITTED: "已提交",
|
||||
APPROVED: "已审批",
|
||||
REJECTED: "已驳回",
|
||||
PAID: "已支付",
|
||||
CLOSED: "已关闭",
|
||||
OPEN: "开放",
|
||||
NEW: "新建",
|
||||
FOLLOW_UP: "随访中",
|
||||
OVERDUE: "已逾期",
|
||||
SCREENING: "筛选中",
|
||||
ENROLLED: "已入组",
|
||||
COMPLETED: "已完成",
|
||||
DROPPED: "已脱落",
|
||||
},
|
||||
trainingFlag: {
|
||||
true: "是",
|
||||
false: "否",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -4,6 +4,7 @@ import { useStudyStore } from "../store/study";
|
||||
import Layout from "../components/Layout.vue";
|
||||
import Login from "../views/Login.vue";
|
||||
import Register from "../views/Register.vue";
|
||||
import ForgotPassword from "../views/ForgotPassword.vue";
|
||||
import StudyHome from "../views/StudyHome.vue";
|
||||
import MyWorkbench from "../views/workbench/MyWorkbench.vue";
|
||||
import FaqDetail from "../views/FaqDetail.vue";
|
||||
@@ -44,19 +45,26 @@ import NoteForm from "../views/knowledge/NoteForm.vue";
|
||||
import NoteDetail from "../views/knowledge/NoteDetail.vue";
|
||||
import SubjectForm from "../views/subjects/SubjectForm.vue";
|
||||
import SubjectDetail from "../views/subjects/SubjectDetail.vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: { public: true, title: "登录" },
|
||||
meta: { public: true, title: TEXT.common.actions.login },
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
component: Register,
|
||||
meta: { public: true, title: "注册" },
|
||||
meta: { public: true, title: TEXT.common.actions.register },
|
||||
},
|
||||
{
|
||||
path: "/forgot-password",
|
||||
name: "ForgotPassword",
|
||||
component: ForgotPassword,
|
||||
meta: { public: true, title: TEXT.modules.auth.forgotTitle },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
@@ -67,247 +75,247 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "workbench",
|
||||
name: "MyWorkbench",
|
||||
component: MyWorkbench,
|
||||
meta: { title: "我的工作台" },
|
||||
meta: { title: TEXT.menu.workbench },
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
name: "ProfileSettings",
|
||||
component: ProfileSettings,
|
||||
meta: { title: "个人设置" },
|
||||
meta: { title: TEXT.menu.profile },
|
||||
},
|
||||
{
|
||||
path: "project/overview",
|
||||
name: "ProjectOverview",
|
||||
component: ProjectOverview,
|
||||
meta: { title: "项目总览", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.projectOverview, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/contracts",
|
||||
name: "FinanceContracts",
|
||||
component: FinanceContracts,
|
||||
meta: { title: "合同费用", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.financeContracts, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/contracts/new",
|
||||
name: "FinanceContractNew",
|
||||
component: ContractForm,
|
||||
meta: { title: "新增合同费用", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.menu.financeContracts, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/contracts/:contractId",
|
||||
name: "FinanceContractDetail",
|
||||
component: ContractDetail,
|
||||
meta: { title: "合同费用详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.financeContracts.detailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/contracts/:contractId/edit",
|
||||
name: "FinanceContractEdit",
|
||||
component: ContractForm,
|
||||
meta: { title: "编辑合同费用", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.menu.financeContracts, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/special",
|
||||
name: "FinanceSpecial",
|
||||
component: FinanceSpecial,
|
||||
meta: { title: "特殊费用", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.financeSpecials, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/special/new",
|
||||
name: "FinanceSpecialNew",
|
||||
component: SpecialForm,
|
||||
meta: { title: "新增特殊费用", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.menu.financeSpecials, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/special/:specialId",
|
||||
name: "FinanceSpecialDetail",
|
||||
component: SpecialDetail,
|
||||
meta: { title: "特殊费用详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.financeSpecials.detailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "finance/special/:specialId/edit",
|
||||
name: "FinanceSpecialEdit",
|
||||
component: SpecialForm,
|
||||
meta: { title: "编辑特殊费用", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.menu.financeSpecials, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "drug/shipments",
|
||||
name: "DrugShipments",
|
||||
component: DrugShipments,
|
||||
meta: { title: "药品流向", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.drugShipments, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "drug/shipments/new",
|
||||
name: "DrugShipmentNew",
|
||||
component: ShipmentForm,
|
||||
meta: { title: "新增运输记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.menu.drugShipments, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "drug/shipments/:shipmentId",
|
||||
name: "DrugShipmentDetail",
|
||||
component: ShipmentDetail,
|
||||
meta: { title: "运输记录详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.drugShipments.detailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "drug/shipments/:shipmentId/edit",
|
||||
name: "DrugShipmentEdit",
|
||||
component: ShipmentForm,
|
||||
meta: { title: "编辑运输记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.menu.drugShipments, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/feasibility-ethics",
|
||||
name: "StartupFeasibilityEthics",
|
||||
component: StartupFeasibilityEthics,
|
||||
meta: { title: "立项与伦理", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.startupFeasibilityEthics, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/feasibility/new",
|
||||
name: "StartupFeasibilityNew",
|
||||
component: FeasibilityForm,
|
||||
meta: { title: "新增立项记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/feasibility/:recordId",
|
||||
name: "StartupFeasibilityDetail",
|
||||
component: FeasibilityDetail,
|
||||
meta: { title: "立项记录详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/feasibility/:recordId/edit",
|
||||
name: "StartupFeasibilityEdit",
|
||||
component: FeasibilityForm,
|
||||
meta: { title: "编辑立项记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/ethics/new",
|
||||
name: "StartupEthicsNew",
|
||||
component: EthicsForm,
|
||||
meta: { title: "新增伦理记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/ethics/:recordId",
|
||||
name: "StartupEthicsDetail",
|
||||
component: EthicsDetail,
|
||||
meta: { title: "伦理记录详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/ethics/:recordId/edit",
|
||||
name: "StartupEthicsEdit",
|
||||
component: EthicsForm,
|
||||
meta: { title: "编辑伦理记录", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/meeting-auth",
|
||||
name: "StartupMeetingAuth",
|
||||
component: StartupMeetingAuth,
|
||||
meta: { title: "启动与授权", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.startupMeetingAuth, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/kickoff/new",
|
||||
name: "StartupKickoffNew",
|
||||
component: KickoffForm,
|
||||
meta: { title: "新增启动会", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.modules.startupMeetingAuth.kickoffTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/kickoff/:meetingId",
|
||||
name: "StartupKickoffDetail",
|
||||
component: KickoffDetail,
|
||||
meta: { title: "启动会详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.startupMeetingAuth.kickoffDetailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/kickoff/:meetingId/edit",
|
||||
name: "StartupKickoffEdit",
|
||||
component: KickoffForm,
|
||||
meta: { title: "编辑启动会", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupMeetingAuth.kickoffTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/training/new",
|
||||
name: "StartupTrainingNew",
|
||||
component: TrainingForm,
|
||||
meta: { title: "新增人员", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.common.fields.person, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/training/:recordId",
|
||||
name: "StartupTrainingDetail",
|
||||
component: TrainingDetail,
|
||||
meta: { title: "培训授权详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.startupMeetingAuth.trainingDetailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "startup/training/:recordId/edit",
|
||||
name: "StartupTrainingEdit",
|
||||
component: TrainingForm,
|
||||
meta: { title: "编辑培训授权", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupMeetingAuth.trainingTab, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "subjects",
|
||||
name: "SubjectManagement",
|
||||
component: SubjectManagement,
|
||||
meta: { title: "受试者管理", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.subjects, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "subjects/new",
|
||||
name: "SubjectNew",
|
||||
component: SubjectForm,
|
||||
meta: { title: "新增受试者", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.menu.subjects, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "subjects/:subjectId",
|
||||
name: "SubjectDetail",
|
||||
component: SubjectDetail,
|
||||
meta: { title: "受试者详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.subjectDetail.title, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "subjects/:subjectId/edit",
|
||||
name: "SubjectEdit",
|
||||
component: SubjectForm,
|
||||
meta: { title: "编辑受试者", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.menu.subjects, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "monitoring",
|
||||
name: "MonitoringPlaceholder",
|
||||
component: MonitoringPlaceholder,
|
||||
meta: { title: "监查", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.monitoring, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "audit",
|
||||
name: "AuditPlaceholder",
|
||||
component: AuditPlaceholder,
|
||||
meta: { title: "稽查", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.audit, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/medical-consult",
|
||||
name: "KnowledgeMedicalConsult",
|
||||
component: KnowledgeMedicalConsult,
|
||||
meta: { title: "医学咨询", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.knowledgeMedicalConsult, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/medical-consult/:itemId",
|
||||
name: "KnowledgeMedicalConsultDetail",
|
||||
component: FaqDetail,
|
||||
meta: { title: "医学咨询详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.knowledgeMedicalConsult.detailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/notes",
|
||||
name: "KnowledgeNotes",
|
||||
component: KnowledgeNotes,
|
||||
meta: { title: "注意事项", requiresStudy: true },
|
||||
meta: { title: TEXT.menu.knowledgeNotes, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/notes/new",
|
||||
name: "KnowledgeNoteNew",
|
||||
component: NoteForm,
|
||||
meta: { title: "新增注意事项", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.add + TEXT.menu.knowledgeNotes, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/notes/:noteId",
|
||||
name: "KnowledgeNoteDetail",
|
||||
component: NoteDetail,
|
||||
meta: { title: "注意事项详情", requiresStudy: true },
|
||||
meta: { title: TEXT.modules.knowledgeNotes.detailTitle, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/notes/:noteId/edit",
|
||||
name: "KnowledgeNoteEdit",
|
||||
component: NoteForm,
|
||||
meta: { title: "编辑注意事项", requiresStudy: true },
|
||||
meta: { title: TEXT.common.actions.edit + TEXT.menu.knowledgeNotes, requiresStudy: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -320,37 +328,37 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "users",
|
||||
name: "AdminUsers",
|
||||
component: AdminUsers,
|
||||
meta: { title: "账号管理", requiresAdmin: true },
|
||||
meta: { title: TEXT.menu.accountManagement, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "user-approval",
|
||||
name: "AdminUserApproval",
|
||||
component: AdminUserApproval,
|
||||
meta: { title: "注册审核", requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminUserApproval.title, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
name: "AdminProjects",
|
||||
component: AdminProjects,
|
||||
meta: { title: "项目管理", requiresAdmin: true },
|
||||
meta: { title: TEXT.menu.projectManagement, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "projects/:projectId/members",
|
||||
name: "AdminProjectMembers",
|
||||
component: ProjectMembers,
|
||||
meta: { title: "项目成员", requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminProjectMembers.memberLabel, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "projects/:projectId/sites",
|
||||
name: "AdminSites",
|
||||
component: AdminSites,
|
||||
meta: { title: "中心管理", requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminSites.title, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "audit-logs",
|
||||
name: "AdminAuditLogs",
|
||||
component: AuditLogs,
|
||||
meta: { title: "审计日志", requiresAdmin: true },
|
||||
meta: { title: TEXT.menu.auditLogs, requiresAdmin: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -358,7 +366,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "/projects/:projectId",
|
||||
name: "ProjectDetail",
|
||||
component: ProjectDetail,
|
||||
meta: { title: "项目详情", requiresAdmin: true },
|
||||
meta: { title: TEXT.modules.adminProjects.detailTitle, requiresAdmin: true },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -370,8 +378,8 @@ const router = createRouter({
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const auth = useAuthStore();
|
||||
const studyStore = useStudyStore();
|
||||
const token = auth.token;
|
||||
if (!auth.user && token) {
|
||||
const getToken = () => auth.token;
|
||||
if (!auth.user && getToken()) {
|
||||
try {
|
||||
await auth.fetchMe();
|
||||
} catch {
|
||||
@@ -379,6 +387,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
auth.logout();
|
||||
}
|
||||
}
|
||||
const token = getToken();
|
||||
if (token && !studyStore.currentStudy) {
|
||||
await studyStore.ensureDefaultStudy();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ export const displayEnum = (enumMap: Record<string, string>, value?: string | nu
|
||||
return enumMap[value] || displayFallback;
|
||||
};
|
||||
|
||||
export const displayText = (value?: string | null, map?: Record<string, string>) => {
|
||||
if (!value) return displayFallback;
|
||||
if (map && map[value]) return map[value];
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getUserDisplayName = (user?: any) => {
|
||||
if (!user) return null;
|
||||
return user.display_name || user.full_name || user.username || user.email || null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed } from "vue";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const PERMISSIONS: Record<string, string[]> = {
|
||||
"subject.create": ["ADMIN", "PM", "CRA"],
|
||||
@@ -18,16 +19,16 @@ const PERMISSIONS: Record<string, string[]> = {
|
||||
};
|
||||
|
||||
const REASONS: Record<string, string> = {
|
||||
"subject.enroll": "仅 PM/CRA 可更新受试者状态",
|
||||
"subject.complete": "仅 PM/CRA 可更新受试者状态",
|
||||
"subject.drop": "仅 PM/CRA 可更新受试者状态",
|
||||
"ae.close": "仅 PM/PV/ADMIN 可关闭 AE",
|
||||
"faq.edit": "仅 PM/ADMIN 可维护 FAQ",
|
||||
"faq.create": "仅项目成员可新建 FAQ",
|
||||
"faq.reply": "仅项目成员可回复 FAQ",
|
||||
"project.members.manage": "仅 ADMIN / PM 可调整项目成员",
|
||||
"site.manage": "仅 ADMIN / PM 可管理中心",
|
||||
"site.cra.bind": "仅 ADMIN / PM 可绑定 CRA",
|
||||
"subject.enroll": TEXT.modules.permissions.subjectEnroll,
|
||||
"subject.complete": TEXT.modules.permissions.subjectComplete,
|
||||
"subject.drop": TEXT.modules.permissions.subjectDrop,
|
||||
"ae.close": TEXT.modules.permissions.aeClose,
|
||||
"faq.edit": TEXT.modules.permissions.faqEdit,
|
||||
"faq.create": TEXT.modules.permissions.faqCreate,
|
||||
"faq.reply": TEXT.modules.permissions.faqReply,
|
||||
"project.members.manage": TEXT.modules.permissions.projectMembersManage,
|
||||
"site.manage": TEXT.modules.permissions.siteManage,
|
||||
"site.cra.bind": TEXT.modules.permissions.siteCraBind,
|
||||
};
|
||||
|
||||
export const usePermission = () => {
|
||||
@@ -49,7 +50,7 @@ export const usePermission = () => {
|
||||
|
||||
const reason = (action: string): string => {
|
||||
if (userRole.value === "ADMIN") return "";
|
||||
return REASONS[action] || "当前角色无权执行该操作";
|
||||
return REASONS[action] || TEXT.modules.permissions.default;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索关键词"
|
||||
:placeholder="TEXT.common.placeholders.keyword"
|
||||
clearable
|
||||
@keyup.enter="loadFaqs"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-switch v-model="onlyActive" active-text="仅启用" @change="loadFaqs" />
|
||||
<el-switch v-model="onlyActive" :active-text="TEXT.common.labels.activeOnly" @change="loadFaqs" />
|
||||
<div class="spacer" />
|
||||
<PermissionAction action="faq.create">
|
||||
<el-button type="primary" @click="openForm()">新建咨询</el-button>
|
||||
<el-button type="primary" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newItem }}</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -59,6 +59,7 @@ import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import PermissionAction from "../components/PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -86,7 +87,7 @@ const loadCategories = async () => {
|
||||
const { data } = await fetchFaqCategories(params);
|
||||
categories.value = data.items || data || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "分类加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,7 +112,7 @@ const loadFaqs = async () => {
|
||||
total.value = data.total || 0;
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "咨询加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -3,44 +3,44 @@
|
||||
<el-card v-loading="loading">
|
||||
<div class="content">
|
||||
<div class="question-header">
|
||||
<div class="question-label">问题描述</div>
|
||||
<div class="question-label">{{ TEXT.modules.knowledgeMedicalConsult.questionDesc }}</div>
|
||||
<div class="question-actions">
|
||||
<el-button v-if="canEditQuestion" type="primary" size="small" class="edit-btn" @click="openEdit">
|
||||
编辑问题
|
||||
{{ TEXT.modules.knowledgeMedicalConsult.editQuestion }}
|
||||
</el-button>
|
||||
<el-button v-if="canConfirmResolved" type="success" size="small" @click="confirmResolved">
|
||||
确认解决
|
||||
{{ TEXT.modules.knowledgeMedicalConsult.confirmResolved }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="question-meta">
|
||||
<span class="meta-pill meta-user"
|
||||
>提问人:{{ displayUser(item?.created_by, { members: memberMap, users: userMap }) }}</span
|
||||
>{{ TEXT.modules.knowledgeMedicalConsult.asker }}:{{ displayUser(item?.created_by, { members: memberMap, users: userMap }) }}</span
|
||||
>
|
||||
<span class="meta-pill meta-time">提问时间:{{ displayDateTime(item?.created_at) }}</span>
|
||||
<span class="meta-pill meta-category">分类:{{ categoryName }}</span>
|
||||
<span class="meta-pill meta-status">状态:{{ statusLabel }}</span>
|
||||
<span class="meta-pill meta-time">{{ TEXT.modules.knowledgeMedicalConsult.askedAt }}:{{ displayDateTime(item?.created_at) }}</span>
|
||||
<span class="meta-pill meta-category">{{ TEXT.common.labels.category }}:{{ categoryName }}</span>
|
||||
<span class="meta-pill meta-status">{{ TEXT.common.fields.status }}:{{ statusLabel }}</span>
|
||||
</div>
|
||||
<pre class="question-text">{{ item?.question }}</pre>
|
||||
</div>
|
||||
<div v-if="bestReply" class="best-answer">
|
||||
<div class="best-title">
|
||||
<span>最佳答案</span>
|
||||
<el-tag type="success" size="small">已采纳</el-tag>
|
||||
<span>{{ TEXT.modules.knowledgeMedicalConsult.bestAnswer }}</span>
|
||||
<el-tag type="success" size="small">{{ TEXT.modules.knowledgeMedicalConsult.adopted }}</el-tag>
|
||||
</div>
|
||||
<div class="best-meta">
|
||||
{{ displayUser(bestReply.created_by, { members: memberMap, users: userMap }) }}
|
||||
· {{ displayDateTime(bestReply.created_at) }}
|
||||
</div>
|
||||
<div class="best-content">{{ bestReply.is_deleted ? "回复已删除" : bestReply.content }}</div>
|
||||
<div class="best-content">{{ bestReply.is_deleted ? TEXT.modules.knowledgeMedicalConsult.replyDeleted : bestReply.content }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt-12" v-loading="repliesLoading">
|
||||
<template #header>
|
||||
<div class="reply-header">
|
||||
<span>回复</span>
|
||||
<span class="reply-count">共 {{ replies.length }} 条</span>
|
||||
<span>{{ TEXT.modules.knowledgeMedicalConsult.replies }}</span>
|
||||
<span class="reply-count">{{ TEXT.modules.knowledgeMedicalConsult.replyCount.replace("{count}", String(replies.length)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="canReply" class="reply-form">
|
||||
@@ -56,7 +56,7 @@
|
||||
@clear-quote="clearQuote"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="reply-disabled">当前角色无权限回复</div>
|
||||
<div v-else class="reply-disabled">{{ TEXT.modules.knowledgeMedicalConsult.noReplyPermission }}</div>
|
||||
<el-divider v-if="replies.length" />
|
||||
<ThreadList
|
||||
v-if="replies.length"
|
||||
@@ -71,13 +71,13 @@
|
||||
@delete="removeReply"
|
||||
>
|
||||
<template #actions="{ item: r }">
|
||||
<el-tag v-if="item?.best_reply_id === r.id" type="success" size="small">最佳答案</el-tag>
|
||||
<el-tag v-if="item?.best_reply_id === r.id" type="success" size="small">{{ TEXT.modules.knowledgeMedicalConsult.bestAnswer }}</el-tag>
|
||||
<el-button v-if="canSelectBest && !r.is_deleted" type="text" size="small" @click="toggleBest(r)">
|
||||
{{ item?.best_reply_id === r.id ? "取消最佳" : "设为最佳" }}
|
||||
{{ item?.best_reply_id === r.id ? TEXT.modules.knowledgeMedicalConsult.cancelBest : TEXT.modules.knowledgeMedicalConsult.setBest }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ThreadList>
|
||||
<div v-else class="reply-empty">暂无回复</div>
|
||||
<div v-else class="reply-empty">{{ TEXT.modules.knowledgeMedicalConsult.emptyReplies }}</div>
|
||||
</el-card>
|
||||
|
||||
<FaqItemForm
|
||||
@@ -111,6 +111,7 @@ import { usePermission } from "../utils/permission";
|
||||
import FaqItemForm from "../components/FaqItemForm.vue";
|
||||
import ThreadComposer from "../components/ThreadComposer.vue";
|
||||
import ThreadList from "../components/ThreadList.vue";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
@@ -163,9 +164,7 @@ const canConfirmResolved = computed(() => {
|
||||
});
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (item.value?.status === "RESOLVED") return "已解决";
|
||||
if (item.value?.status === "PROCESSING") return "处理中";
|
||||
return "待回答";
|
||||
return TEXT.enums.faqStatus[item.value?.status as keyof typeof TEXT.enums.faqStatus] || TEXT.enums.faqStatus.PENDING;
|
||||
});
|
||||
|
||||
const bestReply = computed(() => {
|
||||
@@ -227,7 +226,7 @@ const loadReplies = async () => {
|
||||
replies.value = Array.isArray(data) ? data : data.items || [];
|
||||
await loadReplyAttachments();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "回复加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.knowledgeMedicalConsult.replyLoadFailed);
|
||||
} finally {
|
||||
repliesLoading.value = false;
|
||||
}
|
||||
@@ -247,7 +246,7 @@ const loadData = async () => {
|
||||
categories.value = catData.items || catData || [];
|
||||
await Promise.all([loadReplies(), loadMembers(faqData.study_id)]);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -263,7 +262,7 @@ const clearQuote = () => {
|
||||
|
||||
const submitReply = async () => {
|
||||
if (!replyContent.value.trim()) {
|
||||
ElMessage.warning("请输入回复内容");
|
||||
ElMessage.warning(TEXT.modules.knowledgeMedicalConsult.replyRequired);
|
||||
return;
|
||||
}
|
||||
if (!item.value) return;
|
||||
@@ -281,16 +280,16 @@ const submitReply = async () => {
|
||||
await uploadAttachment(item.value.study_id, "faq_replies", created.id, file.raw as File);
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "附件上传失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.uploadFailed);
|
||||
}
|
||||
}
|
||||
ElMessage.success("回复已提交");
|
||||
ElMessage.success(TEXT.modules.knowledgeMedicalConsult.replySubmitted);
|
||||
replyContent.value = "";
|
||||
quoteReply.value = null;
|
||||
replyFiles.value = [];
|
||||
loadReplies();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "回复失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.knowledgeMedicalConsult.replyFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -298,14 +297,14 @@ const submitReply = async () => {
|
||||
|
||||
const removeReply = async (reply: any) => {
|
||||
if (!item.value) 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 deleteFaqReply(item.value.id, reply.id);
|
||||
ElMessage.success("回复已删除");
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
loadReplies();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,14 +313,14 @@ const toggleBest = async (reply: any) => {
|
||||
try {
|
||||
const target = item.value.best_reply_id === reply.id ? null : reply.id;
|
||||
if (target) {
|
||||
const ok = await ElMessageBox.confirm("设为最佳答案并确认已解决?", "提示").catch(() => null);
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.knowledgeMedicalConsult.confirmBestReply, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
const { data } = await setFaqBestReply(item.value.id, { best_reply_id: target });
|
||||
item.value = data;
|
||||
ElMessage.success(target ? "已设为最佳答案" : "已取消最佳答案");
|
||||
ElMessage.success(target ? TEXT.modules.knowledgeMedicalConsult.bestReplySet : TEXT.modules.knowledgeMedicalConsult.bestReplyCleared);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -330,9 +329,9 @@ const confirmResolved = async () => {
|
||||
try {
|
||||
const { data } = await setFaqStatus(item.value.id, { status: "RESOLVED" });
|
||||
item.value = data;
|
||||
ElMessage.success("已设置为已解决");
|
||||
ElMessage.success(TEXT.modules.knowledgeMedicalConsult.resolvedSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="forgot-container">
|
||||
<ModulePlaceholder
|
||||
:list-title="TEXT.modules.auth.forgotTitle"
|
||||
:empty-title="TEXT.modules.auth.forgotTitle"
|
||||
:empty-description="TEXT.modules.auth.forgotDesc"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../locales";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forgot-container {
|
||||
min-height: 100vh;
|
||||
background-color: var(--ctms-bg-base);
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="login-container">
|
||||
<div class="login-content">
|
||||
<div class="login-brand">
|
||||
<h1 class="brand-title">CTMS</h1>
|
||||
<p class="brand-subtitle">临床试验管理系统</p>
|
||||
<h1 class="brand-title">{{ TEXT.common.appName }}</h1>
|
||||
<p class="brand-subtitle">{{ TEXT.modules.auth.brandSubtitle }}</p>
|
||||
</div>
|
||||
|
||||
<el-card class="login-card">
|
||||
<div class="login-header">
|
||||
<h2 class="form-title">欢迎回来</h2>
|
||||
<p class="form-desc">请输入您的凭据以访问系统</p>
|
||||
<h2 class="form-title">{{ TEXT.modules.auth.loginTitle }}</h2>
|
||||
<p class="form-desc">{{ TEXT.modules.auth.loginDesc }}</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
@@ -20,10 +20,10 @@
|
||||
@keyup.enter.native="onSubmit"
|
||||
class="elegant-form"
|
||||
>
|
||||
<el-form-item label="电子邮箱" prop="email">
|
||||
<el-form-item :label="TEXT.modules.auth.emailLabel" prop="email">
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="name@company.com"
|
||||
:placeholder="TEXT.modules.auth.emailPlaceholder"
|
||||
autocomplete="email"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -31,11 +31,11 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录密码" prop="password">
|
||||
<el-form-item :label="TEXT.modules.auth.passwordLabel" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="最小 8 位字符"
|
||||
:placeholder="TEXT.modules.auth.passwordPlaceholder"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
>
|
||||
@@ -46,8 +46,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||
<RouterLink to="/forgot-password" class="forgot-link">忘记密码?</RouterLink>
|
||||
<el-checkbox v-model="form.remember">{{ TEXT.modules.auth.remember }}</el-checkbox>
|
||||
<RouterLink to="/forgot-password" class="forgot-link">{{ TEXT.modules.auth.forgot }}</RouterLink>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
@@ -56,18 +56,18 @@
|
||||
class="submit-btn"
|
||||
@click="onSubmit"
|
||||
>
|
||||
登 录
|
||||
{{ TEXT.modules.auth.loginButton }}
|
||||
</el-button>
|
||||
|
||||
<div class="register-prompt">
|
||||
<span>还没有账号?</span>
|
||||
<RouterLink to="/register">立即注册</RouterLink>
|
||||
<span>{{ TEXT.modules.auth.registerPrompt }}</span>
|
||||
<RouterLink to="/register">{{ TEXT.modules.auth.registerNow }}</RouterLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2025 CTMS Enterprise. Powered by Professional Clinical Research.</p>
|
||||
<p>{{ TEXT.modules.auth.footer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,6 +80,7 @@ import { ElCheckbox, ElMessage, type FormInstance, type FormRules } from "elemen
|
||||
import { Message, Lock } from "@element-plus/icons-vue";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const router = useRouter();
|
||||
@@ -93,10 +94,10 @@ const form = reactive({
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
email: [
|
||||
{ required: true, message: "请输入有效的邮箱地址", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.emailLabel), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
password: [{ required: true, message: "请输入您的密码", trigger: "blur" }],
|
||||
password: [{ required: true, message: requiredMessage(TEXT.modules.auth.passwordLabel), trigger: "blur" }],
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -125,7 +126,7 @@ const onSubmit = async () => {
|
||||
router.push("/");
|
||||
} catch (error: any) {
|
||||
const detail = error?.response?.data?.detail || error?.response?.data?.message;
|
||||
ElMessage.error(detail || "认证失败,请检查您的凭据");
|
||||
ElMessage.error(detail || TEXT.modules.auth.authFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<h3 class="title">个人设置</h3>
|
||||
<p class="subtitle">更新个人信息或修改登录密码</p>
|
||||
<h3 class="title">{{ TEXT.modules.profile.title }}</h3>
|
||||
<p class="subtitle">{{ TEXT.modules.profile.subtitle }}</p>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
|
||||
<el-form-item label="头像">
|
||||
<el-form-item :label="TEXT.common.fields.avatar">
|
||||
<div class="avatar-row">
|
||||
<el-avatar :size="64" :src="avatarPreview" :alt="form.full_name || form.email">
|
||||
{{ form.full_name?.charAt(0)?.toUpperCase() || form.email?.charAt(0)?.toUpperCase() }}
|
||||
@@ -18,31 +18,31 @@
|
||||
:on-success="onAvatarUploaded"
|
||||
:on-error="onAvatarError"
|
||||
>
|
||||
<el-button>上传头像</el-button>
|
||||
<el-button>{{ TEXT.modules.profile.uploadAvatar }}</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-form-item :label="TEXT.common.fields.email">
|
||||
<el-input v-model="form.email" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.common.fields.department" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.department" />
|
||||
</el-form-item>
|
||||
<el-divider />
|
||||
<el-form-item label="当前密码" prop="current_password">
|
||||
<el-input v-model="form.current_password" type="password" show-password placeholder="修改密码时必填" />
|
||||
<el-form-item :label="TEXT.modules.profile.currentPassword" prop="current_password">
|
||||
<el-input v-model="form.current_password" type="password" show-password :placeholder="TEXT.modules.profile.currentPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="不修改请留空" />
|
||||
<el-form-item :label="TEXT.modules.profile.newPassword" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="TEXT.modules.profile.newPasswordHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="再次输入新密码" />
|
||||
<el-form-item :label="TEXT.modules.profile.confirmPassword" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" show-password :placeholder="TEXT.modules.profile.confirmPasswordHint" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -56,6 +56,7 @@ import { ElMessage } from "element-plus";
|
||||
import { updateProfile, fetchMe } from "../api/auth";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { getToken } from "../utils/auth";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -74,13 +75,13 @@ const uploadHeaders = computed<Record<string, string>>(() => ({
|
||||
}));
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.common.fields.department), trigger: "blur" }],
|
||||
current_password: [
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && !value) {
|
||||
cb(new Error("修改密码需输入当前密码"));
|
||||
cb(new Error(TEXT.modules.profile.passwordRequired));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -93,7 +94,7 @@ const rules: FormRules<typeof form> = {
|
||||
validator: (_r, value, cb) => {
|
||||
if (!value) return cb();
|
||||
if (value.length < 8 || !/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
|
||||
cb(new Error("至少 8 位且包含字母与数字"));
|
||||
cb(new Error(TEXT.modules.profile.passwordRule));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -105,7 +106,7 @@ const rules: FormRules<typeof form> = {
|
||||
{
|
||||
validator: (_r, value, cb) => {
|
||||
if (form.password && value !== form.password) {
|
||||
cb(new Error("两次输入的密码不一致"));
|
||||
cb(new Error(TEXT.modules.profile.passwordMismatch));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
@@ -136,13 +137,13 @@ const onSubmit = async () => {
|
||||
password: form.password || undefined,
|
||||
});
|
||||
await auth.fetchMe();
|
||||
ElMessage.success("已更新");
|
||||
ElMessage.success(TEXT.modules.profile.updateSuccess);
|
||||
form.current_password = "";
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
avatarPreview.value = auth.user?.avatar_url || avatarPreview.value;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "更新失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.profile.updateFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -152,11 +153,11 @@ const onSubmit = async () => {
|
||||
const onAvatarUploaded = async () => {
|
||||
await auth.fetchMe();
|
||||
avatarPreview.value = auth.user?.avatar_url || undefined;
|
||||
ElMessage.success("头像已更新");
|
||||
ElMessage.success(TEXT.modules.profile.avatarUpdated);
|
||||
};
|
||||
|
||||
const onAvatarError = (err: any) => {
|
||||
ElMessage.error(err?.response?.data?.message || "头像上传失败");
|
||||
ElMessage.error(err?.response?.data?.message || TEXT.modules.profile.avatarUploadFailed);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="register-container">
|
||||
<div class="register-card">
|
||||
<div class="header">
|
||||
<h2>创建账号</h2>
|
||||
<p>提交后需管理员审核通过方可登录</p>
|
||||
<h2>{{ TEXT.modules.auth.registerTitle }}</h2>
|
||||
<p>{{ TEXT.modules.auth.registerDesc }}</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
@@ -12,36 +12,36 @@
|
||||
label-position="top"
|
||||
@keyup.enter.native="onSubmit"
|
||||
>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="name@example.com" autocomplete="email" />
|
||||
<el-form-item :label="TEXT.modules.auth.emailLabel" prop="email">
|
||||
<el-input v-model="form.email" :placeholder="TEXT.modules.auth.emailPlaceholder" autocomplete="email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-form-item :label="TEXT.modules.auth.passwordLabel" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="至少 8 位且包含字母与数字"
|
||||
:placeholder="TEXT.modules.auth.passwordRuleMix"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-form-item :label="TEXT.modules.auth.confirmPasswordLabel" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
:placeholder="TEXT.modules.auth.confirmPasswordPlaceholder"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.modules.auth.fullNameLabel" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.modules.auth.fullNamePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.modules.auth.departmentLabel" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.modules.auth.departmentPlaceholder" />
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交注册</el-button>
|
||||
<RouterLink to="/login">已有账号?返回登录</RouterLink>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.modules.auth.registerButton }}</el-button>
|
||||
<RouterLink to="/login">{{ TEXT.modules.auth.backToLogin }}</RouterLink>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -55,6 +55,7 @@ import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { register as apiRegister } from "../api/auth";
|
||||
import type { RegisterRequest } from "../types/api";
|
||||
import { TEXT, requiredMessage } from "../locales";
|
||||
|
||||
const router = useRouter();
|
||||
const defaultRole: RegisterRequest["role"] = "CRA";
|
||||
@@ -72,11 +73,11 @@ const submitting = ref(false);
|
||||
const passwordRule = {
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (!value || value.length < 8) {
|
||||
callback(new Error("密码至少 8 位"));
|
||||
callback(new Error(TEXT.modules.auth.passwordRuleMin));
|
||||
return;
|
||||
}
|
||||
if (!/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
|
||||
callback(new Error("需包含字母和数字"));
|
||||
callback(new Error(TEXT.modules.auth.passwordRuleMix));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -86,19 +87,19 @@ const passwordRule = {
|
||||
|
||||
const rules: FormRules<typeof form> = {
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.emailLabel), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.passwordLabel), trigger: "blur" },
|
||||
passwordRule,
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请确认密码", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.modules.auth.confirmPasswordLabel), trigger: "blur" },
|
||||
{
|
||||
validator: (_: any, value: string, callback: (err?: Error) => void) => {
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
callback(new Error(TEXT.modules.auth.passwordMismatch));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
@@ -106,8 +107,8 @@ const rules: FormRules<typeof form> = {
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.modules.auth.fullNameLabel), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.modules.auth.departmentLabel), trigger: "blur" }],
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
@@ -123,7 +124,7 @@ const onSubmit = async () => {
|
||||
role: defaultRole,
|
||||
department: form.department,
|
||||
});
|
||||
ElMessage.success("注册成功,等待管理员审核");
|
||||
ElMessage.success(TEXT.modules.auth.registerSuccess);
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
}, 3000);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<!-- 概览头部 -->
|
||||
<div class="home-header">
|
||||
<div class="header-content">
|
||||
<h1 class="welcome-title">项目总览</h1>
|
||||
<p class="page-subtitle">当前项目运行状态与关键指标</p>
|
||||
<h1 class="welcome-title">{{ TEXT.modules.projectOverview.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.projectOverview.subtitle }}</p>
|
||||
<p class="study-info">
|
||||
<span class="study-name">{{ study.currentStudy.name }}</span>
|
||||
<span class="study-divider">/</span>
|
||||
@@ -12,7 +12,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<el-tag effect="plain" class="role-tag">角色: {{ auth.user?.role }}</el-tag>
|
||||
<el-tag effect="plain" class="role-tag">{{ TEXT.common.labels.role }}: {{ roleLabel }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="stats-grid">
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="节点完成率"
|
||||
:title="TEXT.modules.projectOverview.kpiProgress"
|
||||
:value="progress?.milestones_done ?? 0"
|
||||
:unit="` / ${progress?.milestones_total ?? 0}`"
|
||||
:subtext="milestoneCompletionRate"
|
||||
@@ -30,19 +30,19 @@
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="逾期 AE"
|
||||
:title="TEXT.modules.projectOverview.kpiOverdueAes"
|
||||
:value="overdueAes"
|
||||
unit="例"
|
||||
subtext="需尽快跟进处理"
|
||||
:unit="TEXT.common.units.case"
|
||||
:subtext="TEXT.modules.projectOverview.kpiOverdueAesHint"
|
||||
:loading="loading.overdueAes"
|
||||
:icon="Warning"
|
||||
/>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<KpiCard
|
||||
title="费用总额"
|
||||
:title="TEXT.modules.projectOverview.kpiFinanceTotal"
|
||||
:value="formatAmount(financeSummary?.total_amount ?? 0)"
|
||||
:subtext="`累计已支付:¥${formatAmount(financeSummary?.paid_amount ?? 0)}`"
|
||||
:subtext="`${TEXT.modules.projectOverview.kpiFinancePaid}:¥${formatAmount(financeSummary?.paid_amount ?? 0)}`"
|
||||
:loading="loading.finance"
|
||||
:icon="Money"
|
||||
/>
|
||||
@@ -53,7 +53,7 @@
|
||||
<el-alert
|
||||
v-if="overdueAes > 0"
|
||||
type="warning"
|
||||
title="存在逾期未处理事项,请及时关注项目的合规性与进度。"
|
||||
:title="TEXT.modules.projectOverview.alertWarning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="home-alert"
|
||||
@@ -61,7 +61,7 @@
|
||||
<el-alert
|
||||
v-else
|
||||
type="success"
|
||||
title="项目运行状态良好,所有关键指标均在可控范围内。"
|
||||
:title="TEXT.modules.projectOverview.alertOk"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="home-alert"
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state-container">
|
||||
<StateEmpty description="请先在顶部选择一个当前项目" />
|
||||
<StateEmpty :description="TEXT.common.empty.selectProject" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,6 +87,8 @@ import KpiCard from "../components/KpiCard.vue";
|
||||
import QuickActions from "../components/QuickActions.vue";
|
||||
import { List, Warning, Money } from "@element-plus/icons-vue";
|
||||
import StateEmpty from "../components/StateEmpty.vue";
|
||||
import { displayEnum } from "../utils/display";
|
||||
import { TEXT } from "../locales";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -103,11 +105,13 @@ const loading = ref({
|
||||
|
||||
const milestoneCompletionRate = computed(() => {
|
||||
const total = progress.value?.milestones_total ?? 0;
|
||||
if (!total) return "暂无节点";
|
||||
if (!total) return TEXT.modules.projectOverview.noMilestones;
|
||||
const rate = (progress.value?.milestones_done ?? 0) / total;
|
||||
return `总体进度: ${(rate * 100).toFixed(0)}%`;
|
||||
return TEXT.modules.projectOverview.progressLabel.replace("{rate}", `${(rate * 100).toFixed(0)}%`);
|
||||
});
|
||||
|
||||
const roleLabel = computed(() => displayEnum(TEXT.enums.userRole, auth.user?.role));
|
||||
|
||||
const formatAmount = (val: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2 }).format(val);
|
||||
};
|
||||
|
||||
@@ -3,30 +3,32 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>注册审核</h3>
|
||||
<p>仅显示待审核账号,审核通过后即可登录</p>
|
||||
<h3>{{ TEXT.modules.adminUserApproval.title }}</h3>
|
||||
<p>{{ TEXT.modules.adminUserApproval.subtitle }}</p>
|
||||
</div>
|
||||
<el-select v-model="status" size="small" style="width: 180px" @change="loadUsers">
|
||||
<el-option label="待审核" value="PENDING" />
|
||||
<el-option label="已通过" value="ACTIVE" />
|
||||
<el-option label="已拒绝" value="REJECTED" />
|
||||
<el-option label="已禁用" value="DISABLED" />
|
||||
<el-option :label="TEXT.enums.userStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.userStatus.ACTIVE" value="ACTIVE" />
|
||||
<el-option :label="TEXT.enums.userStatus.REJECTED" value="REJECTED" />
|
||||
<el-option :label="TEXT.enums.userStatus.DISABLED" value="DISABLED" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="users" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="full_name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column prop="department" label="部门" min-width="160" />
|
||||
<el-table-column prop="created_at" label="注册时间" width="200">
|
||||
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
|
||||
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="role" :label="TEXT.common.labels.role" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.userRole, scope.row.role) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="160" />
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUserApproval.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
|
||||
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
@@ -35,7 +37,7 @@
|
||||
size="small"
|
||||
@click="onApprove(scope.row.id)"
|
||||
>
|
||||
审核通过
|
||||
{{ TEXT.modules.adminUserApproval.approve }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
@@ -44,9 +46,9 @@
|
||||
size="small"
|
||||
@click="onReject(scope.row.id)"
|
||||
>
|
||||
拒绝
|
||||
{{ TEXT.modules.adminUserApproval.reject }}
|
||||
</el-button>
|
||||
<span v-else>—</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -59,7 +61,8 @@ import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { approveUser, listPendingUsers, rejectUser } from "../../api/admin";
|
||||
import type { UserInfo, UserStatus } from "../../types/api";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { displayDateTime, displayEnum } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -84,7 +87,7 @@ const loadUsers = async () => {
|
||||
const { data } = await listPendingUsers(status.value);
|
||||
users.value = data.items || [];
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "加载待审核用户失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -93,20 +96,20 @@ const loadUsers = async () => {
|
||||
const onApprove = async (id: string) => {
|
||||
try {
|
||||
await approveUser(id);
|
||||
ElMessage.success("已通过审核");
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.approveSuccess);
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "审核失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.approveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const onReject = async (id: string) => {
|
||||
try {
|
||||
await rejectUser(id);
|
||||
ElMessage.success("已拒绝");
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.rejectSuccess);
|
||||
loadUsers();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || "拒绝失败");
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.rejectFailed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
<el-card>
|
||||
<div class="ctms-table-toolbar">
|
||||
<div class="ctms-filter-group">
|
||||
<el-select v-model="filters.eventType" clearable placeholder="操作类型" @change="loadLogs" class="ctms-filter-item">
|
||||
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="loadLogs" class="ctms-filter-item">
|
||||
<el-option v-for="opt in eventTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.operatorId" clearable placeholder="操作人" filterable @change="loadLogs" class="ctms-filter-item">
|
||||
<el-select v-model="filters.operatorId" clearable :placeholder="TEXT.modules.adminAuditLogs.filterOperator" filterable @change="loadLogs" class="ctms-filter-item">
|
||||
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.result" clearable placeholder="结果" @change="loadLogs" style="width: 100px">
|
||||
<el-option label="成功" value="SUCCESS" />
|
||||
<el-option label="失败" value="FAIL" />
|
||||
<el-select v-model="filters.result" clearable :placeholder="TEXT.modules.adminAuditLogs.filterResult" @change="loadLogs" style="width: 100px">
|
||||
<el-option :label="TEXT.audit.resultSuccess" value="SUCCESS" />
|
||||
<el-option :label="TEXT.audit.resultFail" value="FAIL" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="filters.range"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
:start-placeholder="TEXT.modules.adminAuditLogs.rangeStart"
|
||||
:end-placeholder="TEXT.modules.adminAuditLogs.rangeEnd"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 240px"
|
||||
@@ -34,7 +34,7 @@
|
||||
:loading="exportLoading"
|
||||
@click="confirmExport('system')"
|
||||
>
|
||||
导出系统审计
|
||||
{{ TEXT.modules.adminAuditLogs.exportSystem }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canProjectExport"
|
||||
@@ -44,36 +44,36 @@
|
||||
:loading="exportLoading"
|
||||
@click="confirmExport('project')"
|
||||
>
|
||||
导出项目审计
|
||||
{{ TEXT.modules.adminAuditLogs.exportProject }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="timestamp" label="时间" width="180">
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.timestamp) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actorName" label="操作人" width="140" />
|
||||
<el-table-column prop="actorRoleLabel" label="角色" width="120" />
|
||||
<el-table-column prop="eventLabel" label="操作类型" min-width="160" />
|
||||
<el-table-column prop="actionText" label="操作内容" min-width="180" />
|
||||
<el-table-column label="操作对象" min-width="180">
|
||||
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="140" />
|
||||
<el-table-column prop="actorRoleLabel" :label="TEXT.common.labels.role" width="120" />
|
||||
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" min-width="160" />
|
||||
<el-table-column prop="actionText" :label="TEXT.modules.adminAuditLogs.columns.action" min-width="180" />
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" min-width="180">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.targetTypeLabel">
|
||||
{{ scope.row.targetTypeLabel }}({{ scope.row.targetName || "—" }})
|
||||
{{ scope.row.targetTypeLabel }}({{ scope.row.targetName || TEXT.common.fallback }})
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变更详情" min-width="220">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="220">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.diffText?.length">
|
||||
<div v-for="(line, idx) in scope.row.diffText" :key="idx">{{ line }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
<span v-else>{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="120">
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'">
|
||||
{{ scope.row.resultLabel }}
|
||||
@@ -107,6 +107,7 @@ import { roleDict, getDictLabel } from "../../dictionaries";
|
||||
import { exportAuditCsv } from "../../audit/export/auditExportService";
|
||||
import { logAudit } from "../../audit";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const buildLogKey = (log: any) => {
|
||||
if (log.id) return `id:${log.id}`;
|
||||
@@ -197,7 +198,7 @@ const loadLogs = async () => {
|
||||
total.value = (data as any).total || merged.length;
|
||||
enrichLogs();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "审计日志加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -278,7 +279,7 @@ const fetchAllForExport = async () => {
|
||||
});
|
||||
return filtered.map((log) => normalizeAuditEvent(log, userMap));
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "导出数据加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.exportLoadFailed);
|
||||
return [];
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
@@ -287,20 +288,20 @@ const fetchAllForExport = async () => {
|
||||
|
||||
const confirmExport = async (scope: "system" | "project") => {
|
||||
const ok = await ElMessageBox.confirm(
|
||||
"该文件包含敏感审计信息,仅限授权人员使用。确认导出吗?",
|
||||
"审计导出确认",
|
||||
{ type: "warning", confirmButtonText: "确认", cancelButtonText: "取消" }
|
||||
TEXT.modules.adminAuditLogs.exportConfirm,
|
||||
TEXT.modules.adminAuditLogs.exportConfirmTitle,
|
||||
{ type: "warning", confirmButtonText: TEXT.common.actions.confirm, cancelButtonText: TEXT.common.actions.cancel }
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
const events = await fetchAllForExport();
|
||||
if (!events.length) {
|
||||
ElMessage.warning("无可导出的审计记录");
|
||||
ElMessage.warning(TEXT.modules.adminAuditLogs.exportEmpty);
|
||||
return;
|
||||
}
|
||||
const fileName =
|
||||
scope === "system"
|
||||
? `系统审计_${new Date().toISOString().slice(0, 10)}`
|
||||
: `项目审计_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
|
||||
? `${TEXT.modules.adminAuditLogs.exportSystemName}_${new Date().toISOString().slice(0, 10)}`
|
||||
: `${TEXT.modules.adminAuditLogs.exportProjectName}_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
|
||||
exportAuditCsv(events, { fileName });
|
||||
logAudit(scope === "system" ? "AUDIT_EXPORT_SYSTEM" : "AUDIT_EXPORT_PROJECT", {
|
||||
targetId: study.currentStudy?.id,
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<div class="ctms-page">
|
||||
<div class="ctms-page-header">
|
||||
<div>
|
||||
<h1 class="ctms-page-title">项目详情</h1>
|
||||
<h1 class="ctms-page-title">{{ TEXT.modules.adminProjects.detailTitle }}</h1>
|
||||
</div>
|
||||
<div class="ctms-page-actions">
|
||||
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
|
||||
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
|
||||
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
|
||||
<el-button type="primary" @click="enterProject" :disabled="!project">{{ TEXT.modules.adminProjects.enter }}</el-button>
|
||||
<el-button @click="goMembers" :disabled="!project">{{ TEXT.modules.adminProjects.members }}</el-button>
|
||||
<el-button @click="goSites" :disabled="!project">{{ TEXT.modules.adminProjects.sites }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="ctms-section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="ctms-section-title">项目基本信息</div>
|
||||
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.basicInfo }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions v-if="project" :column="2" border class="detail-descriptions">
|
||||
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectCode">{{ project.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectName">{{ project.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.sponsor">{{ project.sponsor || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.adminProjects.createdAt">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<StateLoading v-else :rows="4" />
|
||||
</el-card>
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-card class="ctms-section-card">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="ctms-section-title">项目通用文件</div>
|
||||
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.filesTitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<AttachmentList
|
||||
@@ -55,6 +55,7 @@ import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateLoading from "../../components/StateLoading.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -67,16 +68,12 @@ const loadProject = async () => {
|
||||
const { data } = await fetchStudyDetail(route.params.projectId as string);
|
||||
project.value = data as Study;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "项目加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
}[status] || status);
|
||||
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
|
||||
|
||||
const enterProject = () => {
|
||||
if (!project.value) return;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<el-dialog :title="project ? '编辑项目' : '新建项目'" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="project ? TEXT.modules.adminProjects.editTitle : TEXT.modules.adminProjects.newTitle" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="项目编号" prop="code">
|
||||
<el-input v-model="form.code" :disabled="!!project" placeholder="请输入项目编号" />
|
||||
<el-form-item :label="TEXT.common.fields.projectCode" prop="code">
|
||||
<el-input v-model="form.code" :disabled="!!project" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称" />
|
||||
<el-form-item :label="TEXT.common.fields.projectName" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申办方" prop="sponsor">
|
||||
<el-input v-model="form.sponsor" placeholder="申办方(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.sponsor" prop="sponsor">
|
||||
<el-input v-model="form.sponsor" :placeholder="TEXT.modules.adminProjects.sponsorPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方案号" prop="protocol_no">
|
||||
<el-input v-model="form.protocol_no" placeholder="方案号(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.protocolNo" prop="protocol_no">
|
||||
<el-input v-model="form.protocol_no" :placeholder="TEXT.modules.adminProjects.protocolPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分期" prop="phase">
|
||||
<el-input v-model="form.phase" placeholder="研究分期(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.phase" prop="phase">
|
||||
<el-input v-model="form.phase" :placeholder="TEXT.modules.adminProjects.phasePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="进行中" value="ACTIVE" />
|
||||
<el-option label="已关闭" value="CLOSED" />
|
||||
<el-form-item :label="TEXT.common.fields.status" prop="status">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.projectStatus.DRAFT" value="DRAFT" />
|
||||
<el-option :label="TEXT.enums.projectStatus.ACTIVE" value="ACTIVE" />
|
||||
<el-option :label="TEXT.enums.projectStatus.CLOSED" value="CLOSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -36,6 +36,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createStudy, updateStudy } from "../../api/studies";
|
||||
import type { Study } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -64,9 +65,9 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
code: [{ required: true, message: "请输入项目编号", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择项目状态", trigger: "change" }],
|
||||
code: [{ required: true, message: requiredMessage(TEXT.common.fields.projectCode), trigger: "blur" }],
|
||||
name: [{ required: true, message: requiredMessage(TEXT.common.fields.projectName), trigger: "blur" }],
|
||||
status: [{ required: true, message: requiredMessage(TEXT.common.fields.status), trigger: "change" }],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -108,7 +109,7 @@ const onSubmit = async () => {
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已更新");
|
||||
ElMessage.success(TEXT.modules.adminProjects.updateSuccess);
|
||||
} else {
|
||||
await createStudy({
|
||||
code: form.code,
|
||||
@@ -118,12 +119,12 @@ const onSubmit = async () => {
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已创建");
|
||||
ElMessage.success(TEXT.modules.adminProjects.createSuccess);
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>项目成员配置(角色仅在当前项目内生效)</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
<div class="sub">同一账号在不同项目可拥有不同角色,角色授权仅在本页进行</div>
|
||||
<h3>{{ TEXT.modules.adminProjectMembers.title }}</h3>
|
||||
<div class="sub" v-if="project">{{ TEXT.modules.adminProjectMembers.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
|
||||
<div class="sub">{{ TEXT.modules.adminProjectMembers.subtitle }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openAdd">新增成员</el-button>
|
||||
<el-button type="primary" @click="openAdd">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjectMembers.memberLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="memberRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="username" label="用户名" min-width="160" />
|
||||
<el-table-column prop="role_in_study" label="项目角色" width="140">
|
||||
<el-table-column prop="username" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
|
||||
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" width="140">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.role_in_study }}</el-tag>
|
||||
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="160">
|
||||
<el-table-column :label="TEXT.common.fields.status" width="160">
|
||||
<template #default="scope">
|
||||
<el-tooltip
|
||||
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
content="请前往【账号治理】启用该账号"
|
||||
:content="TEXT.modules.adminProjectMembers.disabledGlobalHint"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag type="danger">全局已禁用</el-tag>
|
||||
<el-tag type="danger">{{ TEXT.modules.adminProjectMembers.disabledGlobal }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="scope.row.is_active ? 'success' : 'danger'">
|
||||
{{ scope.row.is_active ? "启用" : "已禁用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.enable : TEXT.modules.adminProjectMembers.disabled }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="added_at" label="加入时间" width="200">
|
||||
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="360" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="360" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.role_in_study"
|
||||
@@ -42,14 +42,14 @@
|
||||
@change="(val) => updateRole(scope.row.id, val)"
|
||||
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
>
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
<el-option label="PV" value="PV" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
<el-option label="ADMIN" value="ADMIN" />
|
||||
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
|
||||
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
|
||||
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
|
||||
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
|
||||
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
|
||||
</el-select>
|
||||
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">该账号已在系统级被禁用,无法在任何项目中使用</span>
|
||||
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
|
||||
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">{{ TEXT.modules.adminProjectMembers.disabledGlobalDesc }}</span>
|
||||
<el-button link type="danger" size="small" @click="onDelete(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
@@ -57,17 +57,17 @@
|
||||
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
@click="toggleActive(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="新增成员" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
<el-dialog :title="TEXT.modules.adminProjectMembers.newTitle" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
<el-form :model="newMember" label-width="120px" ref="addFormRef" :rules="addRules">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<el-select v-model="newMember.user_id" filterable placeholder="请选择用户">
|
||||
<el-form-item :label="TEXT.modules.adminProjectMembers.user" prop="user_id">
|
||||
<el-select v-model="newMember.user_id" filterable :placeholder="TEXT.modules.adminProjectMembers.userPlaceholder">
|
||||
<el-option
|
||||
v-for="user in availableUsers"
|
||||
:key="user.id"
|
||||
@@ -77,19 +77,19 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role_in_study">
|
||||
<el-select v-model="newMember.role_in_study" placeholder="请选择角色">
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
<el-option label="PV" value="PV" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
<el-option label="ADMIN" value="ADMIN" />
|
||||
<el-form-item :label="TEXT.modules.adminProjectMembers.projectRole" prop="role_in_study">
|
||||
<el-select v-model="newMember.role_in_study" :placeholder="TEXT.modules.adminProjectMembers.rolePlaceholder">
|
||||
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
|
||||
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
|
||||
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
|
||||
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
|
||||
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="submitAdd">保存</el-button>
|
||||
<el-button @click="addVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="submitAdd">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -106,7 +106,8 @@ import type { Study, StudyMember, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { displayDateTime, displayEnum } from "../../utils/display";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
@@ -125,8 +126,8 @@ const newMember = reactive({
|
||||
});
|
||||
|
||||
const addRules = reactive<FormRules>({
|
||||
user_id: [{ required: true, message: "请选择用户", trigger: "change" }],
|
||||
role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
|
||||
user_id: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.user), trigger: "change" }],
|
||||
role_in_study: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.projectRole), trigger: "change" }],
|
||||
});
|
||||
|
||||
const loadProject = async () => {
|
||||
@@ -146,7 +147,7 @@ const loadMembers = async () => {
|
||||
const { data } = await listMembers(projectId.value, { limit: 500 });
|
||||
members.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "成员列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -187,7 +188,7 @@ const submitAdd = async () => {
|
||||
target: { projectId: projectId.value },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -200,7 +201,7 @@ const submitAdd = async () => {
|
||||
adding.value = true;
|
||||
try {
|
||||
await addMember(projectId.value, newMember);
|
||||
ElMessage.success("成员已添加");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.addSuccess);
|
||||
addVisible.value = false;
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
@@ -210,7 +211,7 @@ const submitAdd = async () => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "添加失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.addFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -231,7 +232,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
target: { projectId: projectId.value, memberId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -242,7 +243,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
}
|
||||
try {
|
||||
await updateMember(projectId.value, memberId, { role_in_study: role });
|
||||
ElMessage.success("角色已更新");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.roleUpdated);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -251,7 +252,7 @@ const updateRole = async (memberId: string, role: string) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "更新失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.updateFailed);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -271,7 +272,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
target: { projectId: projectId.value, memberId: row.id },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -281,14 +282,14 @@ const toggleActive = async (row: StudyMember) => {
|
||||
return;
|
||||
}
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响项目成员,请确认是否继续", "停用成员", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.disableConfirm, TEXT.modules.adminProjectMembers.disableTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateMember(projectId.value, row.id, { is_active: false });
|
||||
ElMessage.success("成员已停用");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.disableSuccess);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -298,7 +299,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -311,7 +312,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
} else {
|
||||
try {
|
||||
await updateMember(projectId.value, row.id, { is_active: true });
|
||||
ElMessage.success("成员已启用");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.enableSuccess);
|
||||
loadMembers();
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
@@ -321,7 +322,7 @@ const toggleActive = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -342,7 +343,7 @@ const onDelete = async (row: StudyMember) => {
|
||||
target: { projectId: projectId.value, memberId: row.id },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -351,16 +352,16 @@ const onDelete = async (row: StudyMember) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm("确认将该账号从本项目移除?账号本身不会被删除。", "删除成员", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.removeConfirm, TEXT.modules.adminProjectMembers.removeTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await removeMember(projectId.value, row.id);
|
||||
members.value = members.value.filter((m) => m.id !== row.id);
|
||||
ElMessage.success("已从项目移除");
|
||||
ElMessage.success(TEXT.modules.adminProjectMembers.removeSuccess);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
@@ -369,7 +370,7 @@ const onDelete = async (row: StudyMember) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||
targetId: projectId.value,
|
||||
targetName: project.value?.name,
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>项目治理</h3>
|
||||
<p class="sub">项目是独立业务实体,账号加入项目后才成为项目成员(角色在成员配置中设置)</p>
|
||||
<h3>{{ TEXT.modules.adminProjects.title }}</h3>
|
||||
<p class="sub">{{ TEXT.modules.adminProjects.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="projects" v-loading="loading" stripe>
|
||||
<el-table-column prop="code" label="项目编号" width="140">
|
||||
<el-table-column prop="code" :label="TEXT.modules.adminProjects.projectCode" width="140">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.code }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="项目名称" min-width="200">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsor" label="申办方" min-width="160" />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<el-table-column prop="sponsor" :label="TEXT.common.fields.sponsor" min-width="160" />
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goMembers(scope.row)">项目账号/成员配置</el-button>
|
||||
<el-button link type="primary" size="small" @click="goSites(scope.row)">中心管理</el-button>
|
||||
<el-button link type="success" size="small" @click="enterStudy(scope.row)">进入项目</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goMembers(scope.row)">{{ TEXT.modules.adminProjects.members }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goSites(scope.row)">{{ TEXT.modules.adminProjects.sites }}</el-button>
|
||||
<el-button link type="success" size="small" @click="enterStudy(scope.row)">{{ TEXT.modules.adminProjects.enter }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -51,6 +51,7 @@ import type { Study } from "../../types/api";
|
||||
import ProjectForm from "./ProjectForm.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const projects = ref<Study[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -66,7 +67,7 @@ const loadProjects = async () => {
|
||||
const items = (data as any).items || [];
|
||||
projects.value = items;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "项目列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -100,11 +101,7 @@ const goDetail = (row: Study) => {
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
}[status] || status);
|
||||
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
|
||||
|
||||
const statusTag = (status: string) =>
|
||||
({
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<el-dialog title="CRA 绑定" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<div class="tip">中心:{{ site?.name }}</div>
|
||||
<el-dialog :title="TEXT.modules.adminSites.craBindTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<div class="tip">{{ TEXT.modules.adminSites.sitePrefix }}{{ site?.name }}</div>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择 CRA">
|
||||
<el-select v-model="selectedCras" multiple filterable placeholder="请选择 CRA 账号" style="width: 100%">
|
||||
<el-form-item :label="TEXT.modules.adminSites.craSelect">
|
||||
<el-select v-model="selectedCras" multiple filterable :placeholder="TEXT.modules.adminSites.craSelectPlaceholder" style="width: 100%">
|
||||
<el-option v-for="user in craUsers" :key="user.id" :label="user.username" :value="user.id" />
|
||||
</el-select>
|
||||
<div class="hint">支持多选,保存后将以用户名写入中心联系人字段</div>
|
||||
<div class="hint">{{ TEXT.modules.adminSites.craHint }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存绑定</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">{{ TEXT.modules.adminSites.craSave }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -24,6 +24,7 @@ import type { Site, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -83,7 +84,7 @@ const onSave = async () => {
|
||||
target: { siteId: props.site.id, studyId: props.studyId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
@@ -98,7 +99,7 @@ const onSave = async () => {
|
||||
.map((id) => props.craUsers.find((u) => u.id === id)?.username || id)
|
||||
.filter(Boolean);
|
||||
await updateSite(props.studyId, props.site.id, { contact: names.join(",") });
|
||||
ElMessage.success("绑定已保存");
|
||||
ElMessage.success(TEXT.modules.adminSites.craSaveSuccess);
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
logAudit("SITE_CRA_BOUND", {
|
||||
@@ -109,7 +110,7 @@ const onSave = async () => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
logAudit("SITE_CRA_BOUND", {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<el-dialog :title="site ? '编辑中心' : '新建中心'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="site ? TEXT.modules.adminSites.editTitle : TEXT.modules.adminSites.newTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="中心名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入中心名称" />
|
||||
<el-form-item :label="TEXT.common.fields.siteName" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.siteName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="城市" prop="city">
|
||||
<el-input v-model="form.city" placeholder="所在城市(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.city" prop="city">
|
||||
<el-input v-model="form.city" :placeholder="TEXT.modules.adminSites.cityPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="PI" prop="pi_name">
|
||||
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
|
||||
<el-form-item :label="TEXT.modules.adminSites.piLabel" prop="pi_name">
|
||||
<el-input v-model="form.pi_name" :placeholder="TEXT.modules.adminSites.piPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="电话(可选)" />
|
||||
<el-form-item :label="TEXT.common.fields.phone" prop="phone">
|
||||
<el-input v-model="form.phone" :placeholder="TEXT.modules.adminSites.phonePlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="contact">
|
||||
<el-form-item :label="TEXT.common.fields.contact" prop="contact">
|
||||
<el-select
|
||||
v-model="form.craSelections"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="选择项目成员作为负责人,可多选"
|
||||
:placeholder="TEXT.modules.adminSites.contactPlaceholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in craOptions"
|
||||
@@ -28,13 +28,13 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="site" label="状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="停用" />
|
||||
<el-form-item v-if="site" :label="TEXT.common.fields.status" prop="is_active">
|
||||
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -47,6 +47,7 @@ import type { Site } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -80,7 +81,7 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
|
||||
name: [{ required: true, message: requiredMessage(TEXT.common.fields.siteName), trigger: "blur" }],
|
||||
});
|
||||
|
||||
const craOptions = computed(() => {
|
||||
@@ -138,7 +139,7 @@ const onSubmit = async () => {
|
||||
target: { siteId: props.site?.id, studyId: props.studyId },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: props.site?.id || props.studyId,
|
||||
targetName: props.site?.name,
|
||||
@@ -159,7 +160,7 @@ const onSubmit = async () => {
|
||||
contact,
|
||||
is_active: form.is_active,
|
||||
});
|
||||
ElMessage.success("中心已更新");
|
||||
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.site.id,
|
||||
targetName: props.site.name,
|
||||
@@ -173,7 +174,7 @@ const onSubmit = async () => {
|
||||
pi_name: form.pi_name,
|
||||
contact,
|
||||
});
|
||||
ElMessage.success("中心已创建");
|
||||
ElMessage.success(TEXT.modules.adminSites.createSuccess);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.studyId,
|
||||
targetName: form.name,
|
||||
@@ -184,7 +185,7 @@ const onSubmit = async () => {
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: props.site?.id || props.studyId,
|
||||
targetName: props.site?.name || form.name,
|
||||
|
||||
@@ -3,41 +3,41 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>中心管理</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
<h3>{{ TEXT.modules.adminSites.title }}</h3>
|
||||
<div class="sub" v-if="project">{{ TEXT.modules.adminSites.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建中心</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminSites.siteLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="sites" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="中心名称" min-width="180" />
|
||||
<el-table-column prop="city" label="城市" width="140" />
|
||||
<el-table-column prop="pi_name" label="PI" width="160" />
|
||||
<el-table-column label="电话" width="160">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="180" />
|
||||
<el-table-column prop="city" :label="TEXT.common.fields.city" width="140" />
|
||||
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="160" />
|
||||
<el-table-column :label="TEXT.common.fields.phone" width="160">
|
||||
<template #default="scope">
|
||||
{{ phoneText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contact" label="负责人" min-width="180">
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="180">
|
||||
<template #default="scope">
|
||||
{{ contactLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<el-table-column :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||
<el-tag type="danger" v-else>停用</el-tag>
|
||||
<el-tag type="success" v-if="scope.row.is_active">{{ TEXT.common.actions.enable }}</el-tag>
|
||||
<el-tag type="danger" v-else>{{ TEXT.common.actions.disable }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleSite(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -68,6 +68,7 @@ import type { Site, Study, UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { evaluateAction } from "../../guards/actionGuard";
|
||||
import { logAudit } from "../../audit";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
@@ -98,7 +99,7 @@ const loadSites = async () => {
|
||||
const { data } = await fetchSites(projectId.value);
|
||||
sites.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "中心列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminSites.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -135,16 +136,16 @@ const memberNameMap = computed(() => {
|
||||
});
|
||||
|
||||
const contactLabel = (row: any) => {
|
||||
if (!row?.contact) return "—";
|
||||
if (!row?.contact) return TEXT.common.fallback;
|
||||
return String(row.contact)
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean)
|
||||
.map((c) => memberNameMap.value[c] || c)
|
||||
.join("、") || "—";
|
||||
.join("、") || TEXT.common.fallback;
|
||||
};
|
||||
|
||||
const phoneText = (row: any) => row?.phone || row?.contact_phone || "—";
|
||||
const phoneText = (row: any) => row?.phone || row?.contact_phone || TEXT.common.fallback;
|
||||
|
||||
const openCreate = () => {
|
||||
loadUsers();
|
||||
@@ -168,7 +169,7 @@ const toggleSite = async (row: Site) => {
|
||||
target: { siteId: row.id, studyId: projectId.value },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
|
||||
logAudit(decision.auditType, {
|
||||
targetId: row.id,
|
||||
targetName: row.name,
|
||||
@@ -178,15 +179,15 @@ const toggleSite = async (row: Site) => {
|
||||
return;
|
||||
}
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响中心数据,请确认是否继续", "停用中心", {
|
||||
const ok = await ElMessageBox.confirm(TEXT.modules.adminSites.disableConfirm, TEXT.modules.adminSites.disableTitle, {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
await updateSite(projectId.value, row.id, { is_active: !row.is_active });
|
||||
ElMessage.success(row.is_active ? "已停用" : "已启用");
|
||||
ElMessage.success(row.is_active ? TEXT.modules.adminSites.disableSuccess : TEXT.modules.adminSites.enableSuccess);
|
||||
loadSites();
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: row.id,
|
||||
@@ -196,7 +197,7 @@ const toggleSite = async (row: Site) => {
|
||||
severity: "normal",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
targetId: row.id,
|
||||
targetName: row.name,
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<el-dialog :title="user ? '编辑用户' : '新建用户'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="user ? TEXT.modules.adminUsers.editTitle : TEXT.modules.adminUsers.newTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" :disabled="!!user" placeholder="请输入邮箱" />
|
||||
<el-form-item :label="TEXT.common.fields.email" prop="email">
|
||||
<el-input v-model="form.email" :disabled="!!user" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="full_name">
|
||||
<el-input v-model="form.full_name" placeholder="请输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
|
||||
<el-input v-model="form.full_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入部门" />
|
||||
<el-form-item :label="TEXT.common.fields.department" prop="department">
|
||||
<el-input v-model="form.department" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.department" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="user ? '新密码(可选)' : '初始密码'" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? '留空则不修改密码' : '请输入初始密码'" />
|
||||
<el-form-item :label="user ? TEXT.modules.adminUsers.passwordOptional : TEXT.modules.adminUsers.passwordInitial" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? TEXT.modules.adminUsers.passwordOptionalHint : TEXT.modules.adminUsers.passwordInitialHint" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="user" label="账号状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" :disabled="isLastAdmin" />
|
||||
<el-form-item v-if="user" :label="TEXT.modules.adminUsers.accountStatus" prop="is_active">
|
||||
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" :disabled="isLastAdmin" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -29,6 +29,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createUser, updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -60,16 +61,16 @@ const defaultRole = "PV";
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
email: [
|
||||
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
|
||||
{ required: true, message: requiredMessage(TEXT.common.fields.email), trigger: "blur" },
|
||||
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
|
||||
],
|
||||
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
|
||||
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
|
||||
department: [{ required: true, message: requiredMessage(TEXT.common.fields.department), trigger: "blur" }],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!props.user && !value) {
|
||||
callback(new Error("请输入初始密码"));
|
||||
callback(new Error(TEXT.modules.adminUsers.passwordInitialRequired));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -113,7 +114,7 @@ const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate();
|
||||
if (props.user?.role === "ADMIN" && (props.adminCount || 0) <= 1 && !form.is_active) {
|
||||
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
|
||||
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
@@ -125,7 +126,7 @@ const onSubmit = async () => {
|
||||
department: form.department,
|
||||
password: form.password || undefined,
|
||||
});
|
||||
ElMessage.success("用户已更新");
|
||||
ElMessage.success(TEXT.modules.adminUsers.updateSuccess);
|
||||
} else {
|
||||
await createUser({
|
||||
email: form.email,
|
||||
@@ -134,12 +135,12 @@ const onSubmit = async () => {
|
||||
role: defaultRole,
|
||||
password: form.password,
|
||||
});
|
||||
ElMessage.success("用户已创建");
|
||||
ElMessage.success(TEXT.modules.adminUsers.createSuccess);
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<el-dialog title="重置密码" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-dialog :title="TEXT.modules.adminUsers.resetTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
title="该操作将影响用户账号,请确认是否继续"
|
||||
description="系统将生成临时密码,需再次输入用户名才能提交。"
|
||||
:title="TEXT.modules.adminUsers.resetWarningTitle"
|
||||
:description="TEXT.modules.adminUsers.resetWarningDesc"
|
||||
class="mb-12"
|
||||
/>
|
||||
<div class="info">重置对象:<strong>{{ user?.username }}</strong></div>
|
||||
<div class="info">{{ TEXT.modules.adminUsers.resetTarget }}<strong>{{ user?.username }}</strong></div>
|
||||
<el-form ref="formRef" :model="form" label-width="140px" :rules="rules">
|
||||
<el-form-item label="确认用户名" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" placeholder="请输入用户名以确认" />
|
||||
<el-form-item :label="TEXT.modules.adminUsers.confirmUsername" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" :placeholder="TEXT.modules.adminUsers.confirmUsernameHint" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码方式" prop="mode">
|
||||
<el-form-item :label="TEXT.modules.adminUsers.passwordMode" prop="mode">
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio label="auto">系统生成临时密码</el-radio>
|
||||
<el-radio label="manual">手动输入新密码</el-radio>
|
||||
<el-radio label="auto">{{ TEXT.modules.adminUsers.passwordAuto }}</el-radio>
|
||||
<el-radio label="manual">{{ TEXT.modules.adminUsers.passwordManual }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="form.mode === 'auto' ? '临时密码' : '新密码'" prop="passwordValue">
|
||||
<el-form-item :label="form.mode === 'auto' ? TEXT.modules.adminUsers.tempPassword : TEXT.modules.adminUsers.newPassword" prop="passwordValue">
|
||||
<el-input
|
||||
v-if="form.mode === 'auto'"
|
||||
v-model="form.tempPassword"
|
||||
@@ -27,7 +27,7 @@
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="regenerate">重新生成</el-button>
|
||||
<el-button @click="regenerate">{{ TEXT.modules.adminUsers.regenerate }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
@@ -35,14 +35,14 @@
|
||||
v-model="form.manualPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入新密码"
|
||||
:placeholder="TEXT.modules.adminUsers.manualPasswordHint"
|
||||
/>
|
||||
<div class="hint">不会明文展示,请通过安全渠道告知用户</div>
|
||||
<div class="hint">{{ TEXT.modules.adminUsers.passwordHint }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">确认重置</el-button>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">{{ TEXT.modules.adminUsers.resetConfirm }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -52,6 +52,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import { TEXT, requiredMessage } from "../../locales";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
@@ -78,17 +79,17 @@ const form = reactive({
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
confirmUsername: [{ required: true, message: "请再次输入用户名", trigger: "blur" }],
|
||||
mode: [{ required: true, message: "请选择密码方式", trigger: "change" }],
|
||||
confirmUsername: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.confirmUsername), trigger: "blur" }],
|
||||
mode: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.passwordMode), trigger: "change" }],
|
||||
passwordValue: [
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (form.mode === "auto" && !form.tempPassword) {
|
||||
callback(new Error("临时密码生成异常"));
|
||||
callback(new Error(TEXT.modules.adminUsers.tempPasswordError));
|
||||
return;
|
||||
}
|
||||
if (form.mode === "manual" && !form.manualPassword) {
|
||||
callback(new Error("请输入新密码"));
|
||||
callback(new Error(TEXT.modules.adminUsers.manualPasswordRequired));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -127,16 +128,16 @@ const onSubmit = async () => {
|
||||
if (!props.user) return;
|
||||
await formRef.value?.validate();
|
||||
if (form.confirmUsername !== props.user.username) {
|
||||
ElMessage.error("确认用户名不匹配,无法提交");
|
||||
ElMessage.error(TEXT.modules.adminUsers.confirmMismatch);
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm(
|
||||
"该操作将影响用户账号,请确认是否继续",
|
||||
"确认重置密码",
|
||||
TEXT.modules.adminUsers.resetConfirmPrompt,
|
||||
TEXT.modules.adminUsers.resetConfirmTitle,
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确认重置",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.modules.adminUsers.resetConfirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
@@ -144,11 +145,11 @@ const onSubmit = async () => {
|
||||
try {
|
||||
const password = form.mode === "manual" ? form.manualPassword : form.tempPassword;
|
||||
await updateUser(props.user.id, { password });
|
||||
ElMessage.success("密码已重置");
|
||||
ElMessage.success(TEXT.modules.adminUsers.resetSuccess);
|
||||
emit("reset");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "重置失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.resetFailed);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>账号治理(系统登录账号)</h3>
|
||||
<p class="sub">账号用于登录系统,本身不具备项目或角色权限</p>
|
||||
<h3>{{ TEXT.modules.adminUsers.title }}</h3>
|
||||
<p class="sub">{{ TEXT.modules.adminUsers.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
|
||||
</div>
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column prop="full_name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="department" label="部门" min-width="140" />
|
||||
<el-table-column label="状态" width="140">
|
||||
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
|
||||
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.status" width="140">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'danger' : 'primary'"
|
||||
@@ -32,9 +32,9 @@
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'ACTIVE' ? "禁用" : "启用" }}
|
||||
{{ scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">重置密码</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">{{ TEXT.modules.adminUsers.resetPassword }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@@ -42,7 +42,7 @@
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -72,6 +72,7 @@ import UserForm from "./UserForm.vue";
|
||||
import UserResetPassword from "./UserResetPassword.vue";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { displayDateTime } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const total = ref(0);
|
||||
@@ -98,7 +99,7 @@ const loadUsers = async () => {
|
||||
total.value = (data as any).total || items.length;
|
||||
activeAdminCount.value = allItems.filter((u: UserInfo) => u.role === "ADMIN" && u.status === "ACTIVE").length;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "用户列表加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -116,26 +117,26 @@ const openEdit = (row: UserInfo) => {
|
||||
|
||||
const toggleStatus = async (row: UserInfo) => {
|
||||
if (isLastAdmin(row)) {
|
||||
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
|
||||
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
|
||||
return;
|
||||
}
|
||||
const disable = row.status === "ACTIVE";
|
||||
const ok = await ElMessageBox.confirm(
|
||||
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
|
||||
disable ? "禁用账号" : "启用账号",
|
||||
disable ? TEXT.modules.adminUsers.disableConfirm : TEXT.modules.adminUsers.enableConfirm,
|
||||
disable ? TEXT.modules.adminUsers.disableTitle : TEXT.modules.adminUsers.enableTitle,
|
||||
{
|
||||
type: disable ? "warning" : "info",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateUser(row.id, { status: disable ? "DISABLED" : "ACTIVE", is_active: !disable });
|
||||
ElMessage.success(disable ? "已禁用" : "已启用");
|
||||
ElMessage.success(disable ? TEXT.modules.adminUsers.disableSuccess : TEXT.modules.adminUsers.enableSuccess);
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,28 +147,28 @@ const openReset = (row: UserInfo) => {
|
||||
|
||||
const onDelete = async (row: UserInfo) => {
|
||||
const { value, action } = await ElMessageBox.prompt(
|
||||
"删除后账号将无法登录且无法恢复,需先在各项目成员配置中移除该账号。请输入当前管理员密码确认删除。",
|
||||
"危险操作:删除账号",
|
||||
TEXT.modules.adminUsers.deleteConfirm,
|
||||
TEXT.modules.adminUsers.deleteTitle,
|
||||
{
|
||||
inputType: "password",
|
||||
inputPlaceholder: "请输入 admin 密码",
|
||||
confirmButtonText: "确认删除",
|
||||
inputPlaceholder: TEXT.modules.adminUsers.deletePasswordPlaceholder,
|
||||
confirmButtonText: TEXT.modules.adminUsers.deleteConfirmButton,
|
||||
confirmButtonClass: "el-button--danger",
|
||||
cancelButtonText: "取消",
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
}
|
||||
).catch(() => ({ action: "cancel", value: "" }));
|
||||
if (action !== "confirm" || !value) return;
|
||||
try {
|
||||
await deleteUser(row.id, { admin_password: value });
|
||||
ElMessage.success("账号已删除");
|
||||
ElMessage.success(TEXT.modules.adminUsers.deleteSuccess);
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
const detail = e?.response?.data?.detail || e?.response?.data?.message;
|
||||
if (detail && detail.includes("项目成员")) {
|
||||
if (detail && detail.includes(TEXT.modules.adminProjectMembers.memberLabel)) {
|
||||
ElMessage.closeAll();
|
||||
ElMessage.error(detail);
|
||||
} else {
|
||||
ElMessage.error(detail || "删除失败");
|
||||
ElMessage.error(detail || TEXT.modules.adminUsers.deleteFailed);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -192,15 +193,15 @@ const statusType = (status: string) => {
|
||||
const statusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE":
|
||||
return "启用";
|
||||
return TEXT.enums.userStatus.ACTIVE;
|
||||
case "PENDING":
|
||||
return "待审核";
|
||||
return TEXT.enums.userStatus.PENDING;
|
||||
case "REJECTED":
|
||||
return "已拒绝";
|
||||
return TEXT.enums.userStatus.REJECTED;
|
||||
case "DISABLED":
|
||||
return "已停用";
|
||||
return TEXT.enums.userStatus.DISABLED;
|
||||
default:
|
||||
return status || "未知";
|
||||
return status || TEXT.common.fallback;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">运输记录详情</h1>
|
||||
<p class="page-subtitle">查看运输/流向信息与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.drugShipments.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="类型">{{ detail.direction || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ detail.status || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="日期">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递公司">{{ detail.carrier || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运单号">{{ detail.tracking_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发出方">{{ detail.from_party || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="接收方">{{ detail.to_party || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="物品描述" :span="2">{{ detail.drug_desc || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.direction">{{ displayEnum(TEXT.enums.shipmentDirection, detail.direction) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.shipDate">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.carrier">{{ detail.carrier || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trackingNo">{{ detail.tracking_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.fromParty">{{ detail.from_party || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.toParty">{{ detail.to_party || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.drugDesc" :span="2">{{ detail.drug_desc || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -41,8 +41,9 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { getDrugShipment } from "../../api/drugShipments";
|
||||
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();
|
||||
@@ -71,7 +72,7 @@ const load = async () => {
|
||||
const { data } = await getDrugShipment(studyId, shipmentId);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,56 +2,56 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑运输记录" : "新增运输记录" }}</h1>
|
||||
<p class="page-subtitle">记录寄送/回收流向</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.drugShipments.editTitle : TEXT.modules.drugShipments.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="类型" required>
|
||||
<el-select v-model="form.direction" placeholder="选择类型">
|
||||
<el-option label="寄送" value="寄送" />
|
||||
<el-option label="回收" value="回收" />
|
||||
<el-form-item :label="TEXT.common.fields.direction" required>
|
||||
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
|
||||
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物品描述" required>
|
||||
<el-input v-model="form.drug_desc" type="textarea" :rows="3" placeholder="药品名称/规格/数量等" />
|
||||
<el-form-item :label="TEXT.common.fields.drugDesc" required>
|
||||
<el-input v-model="form.drug_desc" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.shipDate">
|
||||
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="快递公司">
|
||||
<el-input v-model="form.carrier" placeholder="输入快递公司" />
|
||||
<el-form-item :label="TEXT.common.fields.carrier">
|
||||
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="form.tracking_no" placeholder="输入运单号" />
|
||||
<el-form-item :label="TEXT.common.fields.trackingNo">
|
||||
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发出方">
|
||||
<el-input v-model="form.from_party" placeholder="输入发出方" />
|
||||
<el-form-item :label="TEXT.common.fields.fromParty">
|
||||
<el-input v-model="form.from_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.fromParty" />
|
||||
</el-form-item>
|
||||
<el-form-item label="接收方">
|
||||
<el-input v-model="form.to_party" placeholder="输入接收方" />
|
||||
<el-form-item :label="TEXT.common.fields.toParty">
|
||||
<el-input v-model="form.to_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.toParty" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" required>
|
||||
<el-select v-model="form.status" placeholder="选择状态">
|
||||
<el-option label="待寄出" value="待寄出" />
|
||||
<el-option label="运输中" value="运输中" />
|
||||
<el-option label="已签收" value="已签收" />
|
||||
<el-option label="已回收" value="已回收" />
|
||||
<el-option label="异常" value="异常" />
|
||||
<el-form-item :label="TEXT.common.fields.status" required>
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -63,7 +63,7 @@
|
||||
:entity-id="shipmentId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传交接材料" />
|
||||
<StateEmpty :description="TEXT.modules.drugShipments.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,6 +76,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { createDrugShipment, getDrugShipment, updateDrugShipment } from "../../api/drugShipments";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -87,7 +88,7 @@ const isEdit = computed(() => !!shipmentId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
|
||||
const form = reactive({
|
||||
direction: "寄送",
|
||||
direction: "SEND",
|
||||
site_name: "",
|
||||
drug_desc: "",
|
||||
ship_date: "",
|
||||
@@ -95,7 +96,7 @@ const form = reactive({
|
||||
tracking_no: "",
|
||||
from_party: "",
|
||||
to_party: "",
|
||||
status: "待寄出",
|
||||
status: "PENDING",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
@@ -104,7 +105,7 @@ const load = async () => {
|
||||
try {
|
||||
const { data } = await getDrugShipment(studyId.value, shipmentId.value);
|
||||
Object.assign(form, {
|
||||
direction: data.direction || "寄送",
|
||||
direction: data.direction || "SEND",
|
||||
site_name: data.site_name || "",
|
||||
drug_desc: data.drug_desc || "",
|
||||
ship_date: data.ship_date || "",
|
||||
@@ -112,11 +113,11 @@ const load = async () => {
|
||||
tracking_no: data.tracking_no || "",
|
||||
from_party: data.from_party || "",
|
||||
to_party: data.to_party || "",
|
||||
status: data.status || "待寄出",
|
||||
status: data.status || "PENDING",
|
||||
remark: data.remark || "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "加载失败");
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,15 +139,15 @@ const submit = async () => {
|
||||
};
|
||||
if (isEdit.value && shipmentId.value) {
|
||||
await updateDrugShipment(studyId.value, shipmentId.value, payload);
|
||||
ElMessage.success("已保存");
|
||||
ElMessage.success(TEXT.common.messages.saveSuccess);
|
||||
router.push(`/drug/shipments/${shipmentId.value}`);
|
||||
} else {
|
||||
const { data } = await createDrugShipment(studyId.value, payload);
|
||||
ElMessage.success("已创建");
|
||||
ElMessage.success(TEXT.common.messages.createSuccess);
|
||||
router.push(`/drug/shipments/${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;
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">合同费用详情</h1>
|
||||
<p class="page-subtitle">查看合同费用与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeContracts.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同编号">{{ detail.contract_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签署日期">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同金额">
|
||||
{{ detail.amount }} {{ detail.currency || "CNY" }}
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.contractNo">{{ detail.contract_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.signedDate">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.amount">
|
||||
{{ detail.amount }} {{ detail.currency || TEXT.common.fields.currencyDefault }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useStudyStore } from "../../store/study";
|
||||
import { getFinanceContract } from "../../api/financeContracts";
|
||||
import { displayDate } from "../../utils/display";
|
||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -64,7 +65,7 @@ const load = async () => {
|
||||
const { data } = await getFinanceContract(studyId, contractId);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,39 +2,39 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑合同费用" : "新增合同费用" }}</h1>
|
||||
<p class="page-subtitle">填写分中心合同信息并保存</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeContracts.editTitle : TEXT.modules.financeContracts.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="110px">
|
||||
<el-form-item label="分中心" prop="site_name" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" prop="site_name" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号" prop="contract_no" required>
|
||||
<el-input v-model="form.contract_no" placeholder="输入合同编号" />
|
||||
<el-form-item :label="TEXT.common.fields.contractNo" prop="contract_no" required>
|
||||
<el-input v-model="form.contract_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="签署日期">
|
||||
<el-date-picker v-model="form.signed_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.signedDate">
|
||||
<el-date-picker v-model="form.signed_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额" prop="amount" required>
|
||||
<el-form-item :label="TEXT.common.fields.amount" prop="amount" required>
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" :step="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" placeholder="选择币种">
|
||||
<el-form-item :label="TEXT.common.fields.currency">
|
||||
<el-select v-model="form.currency" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option label="CNY" value="CNY" />
|
||||
<el-option label="USD" value="USD" />
|
||||
<el-option label="EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -46,7 +46,7 @@
|
||||
:entity-id="contractId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传合同附件" />
|
||||
<StateEmpty :description="TEXT.modules.financeContracts.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">特殊费用详情</h1>
|
||||
<p class="page-subtitle">查看费用记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeSpecials.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类型">{{ detail.fee_type || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发生日期">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人员">{{ detail.staff_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">{{ detail.amount || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.type">{{ displayEnum(TEXT.enums.financeSpecialType, detail.fee_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.occurDate">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.staff">{{ detail.staff_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.amount">{{ detail.amount || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,40 +2,40 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑特殊费用" : "新增特殊费用" }}</h1>
|
||||
<p class="page-subtitle">记录差旅、住宿等特殊费用</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeSpecials.editTitle : TEXT.modules.financeSpecials.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.subtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="110px">
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用类型" required>
|
||||
<el-select v-model="form.fee_type" placeholder="选择类型">
|
||||
<el-option label="差旅" value="差旅" />
|
||||
<el-option label="住宿" value="住宿" />
|
||||
<el-option label="交通" value="交通" />
|
||||
<el-option label="其他" value="其他" />
|
||||
<el-form-item :label="TEXT.common.fields.type" required>
|
||||
<el-select v-model="form.fee_type" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="金额" required>
|
||||
<el-form-item :label="TEXT.common.fields.amount" required>
|
||||
<el-input-number v-model="form.amount" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发生日期">
|
||||
<el-date-picker v-model="form.occur_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.occurDate">
|
||||
<el-date-picker v-model="form.occur_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="人员">
|
||||
<el-input v-model="form.staff_name" placeholder="输入人员姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.staff">
|
||||
<el-input v-model="form.staff_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.staff" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -47,7 +47,7 @@
|
||||
:entity-id="specialId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传凭证附件" />
|
||||
<StateEmpty :description="TEXT.modules.financeSpecials.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
title="稽查"
|
||||
subtitle="功能建设中"
|
||||
list-title="稽查记录列表"
|
||||
empty-description="稽查模块正在准备基础框架,敬请期待。"
|
||||
:title="TEXT.modules.audit.title"
|
||||
:subtitle="TEXT.modules.audit.subtitle"
|
||||
:list-title="TEXT.modules.audit.listTitle"
|
||||
:empty-description="TEXT.modules.audit.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
|
||||
@@ -2,54 +2,58 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">药品运输/流向</h1>
|
||||
<p class="page-subtitle">登记寄送与回收信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.drugShipments.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.drugShipments.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增运输记录</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.drugShipments.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="运单号">
|
||||
<el-input v-model="filters.tracking_no" placeholder="输入运单号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.trackingNo">
|
||||
<el-input v-model="filters.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="选择状态" clearable>
|
||||
<el-option label="待寄出" value="待寄出" />
|
||||
<el-option label="运输中" value="运输中" />
|
||||
<el-option label="已签收" value="已签收" />
|
||||
<el-option label="已回收" value="已回收" />
|
||||
<el-option label="异常" value="异常" />
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
|
||||
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="direction" label="类型" width="100" />
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="tracking_no" label="运单号" min-width="160" />
|
||||
<el-table-column prop="ship_date" label="日期" width="140">
|
||||
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="100">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="tracking_no" :label="TEXT.common.fields.trackingNo" min-width="160" />
|
||||
<el-table-column prop="ship_date" :label="TEXT.common.fields.shipDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.ship_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无运输记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.drugShipments.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,55 +2,55 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">合同费用</h1>
|
||||
<p class="page-subtitle">维护分中心合同费用与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeContracts.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeContracts.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增合同费用</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.financeContracts.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="filters.contract_no" placeholder="输入合同编号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.contractNo">
|
||||
<el-input v-model="filters.contract_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="contract_no" label="合同编号" min-width="160" />
|
||||
<el-table-column prop="signed_date" label="签署日期" width="140">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="contract_no" :label="TEXT.common.fields.contractNo" min-width="160" />
|
||||
<el-table-column prop="signed_date" :label="TEXT.common.fields.signedDate" width="140">
|
||||
<template #default="scope">
|
||||
{{ displayDate(scope.row.signed_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="160">
|
||||
<el-table-column :label="TEXT.common.fields.amount" width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.amount }} {{ scope.row.currency || "CNY" }}
|
||||
{{ scope.row.amount }} {{ scope.row.currency || TEXT.common.fields.currencyDefault }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ displayDateTime(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无合同费用记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.financeContracts.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,61 +2,65 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">特殊费用</h1>
|
||||
<p class="page-subtitle">记录差旅、住宿等特殊费用</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.financeSpecials.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增特殊费用</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.financeSpecials.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用类型">
|
||||
<el-select v-model="filters.fee_type" placeholder="选择类型" clearable>
|
||||
<el-option label="差旅" value="差旅" />
|
||||
<el-option label="住宿" value="住宿" />
|
||||
<el-option label="交通" value="交通" />
|
||||
<el-option label="其他" value="其他" />
|
||||
<el-form-item :label="TEXT.common.fields.type">
|
||||
<el-select v-model="filters.fee_type" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
|
||||
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="fee_type" label="费用类型" width="120" />
|
||||
<el-table-column prop="occur_date" label="发生日期" width="140">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="fee_type" :label="TEXT.common.fields.type" width="120">
|
||||
<template #default="scope">
|
||||
{{ displayEnum(TEXT.enums.financeSpecialType, scope.row.fee_type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="occur_date" :label="TEXT.common.fields.occurDate" width="140">
|
||||
<template #default="scope">
|
||||
{{ displayDate(scope.row.occur_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="staff_name" label="人员" min-width="120" />
|
||||
<el-table-column label="金额" width="140">
|
||||
<el-table-column prop="staff_name" :label="TEXT.common.fields.staff" min-width="120" />
|
||||
<el-table-column :label="TEXT.common.fields.amount" width="140">
|
||||
<template #default="scope">
|
||||
{{ scope.row.amount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">
|
||||
{{ displayDateTime(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无特殊费用记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.financeSpecials.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,44 +2,44 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">注意事项</h1>
|
||||
<p class="page-subtitle">记录分中心特殊问题与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.knowledgeNotes.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增注意事项</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.modules.knowledgeNotes.newTitle }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="filters.site_name" placeholder="输入分中心名称" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="filters.keyword" placeholder="标题关键词" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.keyword">
|
||||
<el-input v-model="filters.keyword" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.keyword" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="site_name" label="分中心" min-width="160" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" />
|
||||
<el-table-column prop="level" label="重要等级" width="120" />
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
|
||||
<el-table-column prop="title" :label="TEXT.common.fields.title" min-width="200" />
|
||||
<el-table-column prop="level" :label="TEXT.common.fields.level" width="120" />
|
||||
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无注意事项记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.knowledgeNotes.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<ModulePlaceholder
|
||||
title="监查"
|
||||
subtitle="功能建设中"
|
||||
list-title="监查记录列表"
|
||||
empty-description="监查功能正在搭建基础能力,敬请期待。"
|
||||
:title="TEXT.modules.monitoring.title"
|
||||
:subtitle="TEXT.modules.monitoring.subtitle"
|
||||
:list-title="TEXT.modules.monitoring.listTitle"
|
||||
:empty-description="TEXT.modules.monitoring.emptyDescription"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
</script>
|
||||
|
||||
@@ -2,63 +2,63 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">立项与伦理</h1>
|
||||
<p class="page-subtitle">记录立项与伦理递交信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="立项记录" name="feasibility">
|
||||
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.feasibilityTab" name="feasibility">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewFeasibility">新增立项记录</el-button>
|
||||
<el-button type="primary" @click="goNewFeasibility">{{ TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="feasibilityItems" v-loading="loadingFeasibility" style="width: 100%">
|
||||
<el-table-column prop="submit_date" label="递交日期" width="140">
|
||||
<el-table-column prop="submit_date" :label="TEXT.common.fields.submitDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.submit_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accept_date" label="受理日期" width="140">
|
||||
<el-table-column prop="accept_date" :label="TEXT.common.fields.acceptDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.accept_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approved_date" label="同意日期" width="140">
|
||||
<el-table-column prop="approved_date" :label="TEXT.common.fields.approvedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.approved_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="project_no" label="项目编号" min-width="160" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="project_no" :label="TEXT.common.fields.projectNo" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goFeasibilityEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingFeasibility && feasibilityItems.length === 0" description="暂无立项记录" />
|
||||
<StateEmpty v-if="!loadingFeasibility && feasibilityItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyFeasibility" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="伦理记录" name="ethics">
|
||||
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.ethicsTab" name="ethics">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewEthics">新增伦理记录</el-button>
|
||||
<el-button type="primary" @click="goNewEthics">{{ TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="ethicsItems" v-loading="loadingEthics" style="width: 100%">
|
||||
<el-table-column prop="submit_date" label="递交日期" width="140">
|
||||
<el-table-column prop="submit_date" :label="TEXT.common.fields.submitDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.submit_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accept_date" label="受理日期" width="140">
|
||||
<el-table-column prop="accept_date" :label="TEXT.common.fields.acceptDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.accept_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="meeting_date" label="会议日期" width="140">
|
||||
<el-table-column prop="meeting_date" :label="TEXT.common.fields.meetingDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.meeting_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approved_date" label="批准日期" width="140">
|
||||
<el-table-column prop="approved_date" :label="TEXT.common.fields.approvedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.approved_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approval_no" label="批件号" min-width="160" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="approval_no" :label="TEXT.common.fields.approvalNo" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goEthicsDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEthicsEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingEthics && ethicsItems.length === 0" description="暂无伦理记录" />
|
||||
<StateEmpty v-if="!loadingEthics && ethicsItems.length === 0" :description="TEXT.modules.startupFeasibilityEthics.emptyEthics" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,61 +2,61 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">启动与授权</h1>
|
||||
<p class="page-subtitle">管理启动会记录与人员培训授权</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="启动会记录" name="kickoff">
|
||||
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.kickoffTab" name="kickoff">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewKickoff">新增启动会</el-button>
|
||||
<el-button type="primary" @click="goNewKickoff">{{ TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="kickoffItems" v-loading="loadingKickoff" style="width: 100%">
|
||||
<el-table-column prop="kickoff_date" label="启动会日期" width="160">
|
||||
<el-table-column prop="kickoff_date" :label="TEXT.common.fields.kickoffDate" width="160">
|
||||
<template #default="scope">{{ displayDate(scope.row.kickoff_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attendees" label="参训人员" min-width="200">
|
||||
<el-table-column prop="attendees" :label="TEXT.common.fields.attendees" min-width="200">
|
||||
<template #default="scope">
|
||||
{{ (scope.row.attendees || []).join("、") || "—" }}
|
||||
{{ (scope.row.attendees || []).join("、") || TEXT.common.fallback }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goKickoffDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goKickoffEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingKickoff && kickoffItems.length === 0" description="暂无启动会记录" />
|
||||
<StateEmpty v-if="!loadingKickoff && kickoffItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="培训与授权" name="training">
|
||||
<el-tab-pane :label="TEXT.modules.startupMeetingAuth.trainingTab" name="training">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="goNewTraining">新增人员</el-button>
|
||||
<el-button type="primary" @click="goNewTraining">{{ TEXT.modules.startupMeetingAuth.trainingNewTitle }}</el-button>
|
||||
</div>
|
||||
<el-table :data="trainingItems" v-loading="loadingTraining" style="width: 100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" min-width="120" />
|
||||
<el-table-column prop="site_name" label="分中心" min-width="140" />
|
||||
<el-table-column label="已培训" width="120">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.name" min-width="140" />
|
||||
<el-table-column prop="role" :label="TEXT.common.labels.role" min-width="120" />
|
||||
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.trained" width="120">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.trained" @change="toggleTraining(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已授权" width="120">
|
||||
<el-table-column :label="TEXT.common.fields.authorized" width="120">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.authorized" @change="toggleTraining(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goTrainingDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goTrainingEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingTraining && trainingItems.length === 0" description="暂无培训授权人员" />
|
||||
<StateEmpty v-if="!loadingTraining && trainingItems.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyTraining" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,50 +2,52 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">受试者管理</h1>
|
||||
<p class="page-subtitle">受试者、访视与 AE 统一管理</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.subjectManagement.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectManagement.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="goNew">新增受试者</el-button>
|
||||
<el-button type="primary" @click="goNew">{{ TEXT.common.actions.add }}{{ TEXT.modules.subjectManagement.subjectLabel }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="filter-card">
|
||||
<el-form :inline="true" :model="filters">
|
||||
<el-form-item label="编号">
|
||||
<el-input v-model="filters.subject_no" placeholder="输入受试者编号" clearable />
|
||||
<el-form-item :label="TEXT.common.fields.subjectNo">
|
||||
<el-input v-model="filters.subject_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.subjectNo" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" placeholder="选择状态" clearable>
|
||||
<el-option label="筛选" value="SCREENING" />
|
||||
<el-option label="入组" value="ENROLLED" />
|
||||
<el-option label="完成" value="COMPLETED" />
|
||||
<el-option label="退出" value="DROPPED" />
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable>
|
||||
<el-option :label="TEXT.enums.subjectStatus.SCREENING" value="SCREENING" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.ENROLLED" value="ENROLLED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.COMPLETED" value="COMPLETED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.DROPPED" value="DROPPED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table :data="items" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="subject_no" label="受试者编号" min-width="140" />
|
||||
<el-table-column label="分中心" min-width="160">
|
||||
<template #default="scope">{{ siteMap[scope.row.site_id] || "—" }}</template>
|
||||
<el-table-column prop="subject_no" :label="TEXT.common.fields.subjectNo" min-width="140" />
|
||||
<el-table-column :label="TEXT.common.fields.site" min-width="160">
|
||||
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column prop="enrollment_date" label="入组日期" width="140">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.subjectStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enrollment_date" :label="TEXT.common.fields.enrollmentDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.enrollment_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="160">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">详情</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loading && items.length === 0" description="暂无受试者记录" />
|
||||
<StateEmpty v-if="!loading && items.length === 0" :description="TEXT.modules.subjectManagement.empty" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">注意事项详情</h1>
|
||||
<p class="page-subtitle">查看注意事项与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.knowledgeNotes.detailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.detailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重要等级">{{ detail.level || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{ detail.title || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="内容" :span="2">{{ detail.content || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.level">{{ detail.level || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.title" :span="2">{{ detail.title || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.content" :span="2">{{ detail.content || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑注意事项" : "新增注意事项" }}</h1>
|
||||
<p class="page-subtitle">记录分中心特殊问题</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.knowledgeNotes.editTitle : TEXT.modules.knowledgeNotes.newTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.formSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="分中心" required>
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心/机构名称" />
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="form.title" placeholder="输入标题" />
|
||||
<el-form-item :label="TEXT.common.fields.title" required>
|
||||
<el-input v-model="form.title" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="重要等级">
|
||||
<el-input v-model="form.level" placeholder="例如 高/中/低" />
|
||||
<el-form-item :label="TEXT.common.fields.level">
|
||||
<el-input v-model="form.level" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" required>
|
||||
<el-input v-model="form.content" type="textarea" :rows="4" placeholder="输入内容" />
|
||||
<el-form-item :label="TEXT.common.fields.content" required>
|
||||
<el-input v-model="form.content" type="textarea" :rows="4" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -36,7 +36,7 @@
|
||||
:entity-id="noteId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传附件" />
|
||||
<StateEmpty :description="TEXT.modules.knowledgeNotes.uploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">伦理记录详情</h1>
|
||||
<p class="page-subtitle">查看伦理记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="递交日期">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="受理日期">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会议日期">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="批准日期">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="批件号">{{ detail.approval_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.meetingDate">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvalNo">{{ detail.approval_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑伦理记录" : "新增伦理记录" }}</h1>
|
||||
<p class="page-subtitle">维护伦理递交与审批信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.ethicsEditTitle : TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="递交日期">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.submitDate">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受理日期">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.acceptDate">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="会议日期">
|
||||
<el-date-picker v-model="form.meeting_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.meetingDate">
|
||||
<el-date-picker v-model="form.meeting_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批准日期">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.approvedDate">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="批件号">
|
||||
<el-input v-model="form.approval_no" placeholder="输入批件号" />
|
||||
<el-form-item :label="TEXT.common.fields.approvalNo">
|
||||
<el-input v-model="form.approval_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.approvalNo" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -39,7 +39,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传伦理附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.ethicsUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">立项记录详情</h1>
|
||||
<p class="page-subtitle">查看立项记录与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="递交日期">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="受理日期">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="同意日期">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目编号">{{ detail.project_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.projectNo">{{ detail.project_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑立项记录" : "新增立项记录" }}</h1>
|
||||
<p class="page-subtitle">维护立项递交与审批信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.feasibilityEditTitle : TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="递交日期">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.submitDate">
|
||||
<el-date-picker v-model="form.submit_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受理日期">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.acceptDate">
|
||||
<el-date-picker v-model="form.accept_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="同意日期">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.approvedDate">
|
||||
<el-date-picker v-model="form.approved_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号">
|
||||
<el-input v-model="form.project_no" placeholder="输入项目编号" />
|
||||
<el-form-item :label="TEXT.common.fields.projectNo">
|
||||
<el-input v-model="form.project_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectNo" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -36,7 +36,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传立项附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.feasibilityUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">启动会详情</h1>
|
||||
<p class="page-subtitle">查看启动会信息与附件</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.kickoffDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="启动会日期">{{ displayDate(detail.kickoff_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参训人员">
|
||||
{{ (detail.attendees || []).join("、") || "—" }}
|
||||
<el-descriptions-item :label="TEXT.common.fields.kickoffDate">{{ displayDate(detail.kickoff_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.attendees">
|
||||
{{ (detail.attendees || []).join("、") || TEXT.common.fallback }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑启动会" : "新增启动会" }}</h1>
|
||||
<p class="page-subtitle">记录启动会日期与参训人员</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.kickoffEditTitle : TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="启动会日期">
|
||||
<el-date-picker v-model="form.kickoff_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.kickoffDate">
|
||||
<el-date-picker v-model="form.kickoff_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参训人员">
|
||||
<el-form-item :label="TEXT.common.fields.attendees">
|
||||
<el-input
|
||||
v-model="form.attendees"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="每行一个姓名"
|
||||
:placeholder="TEXT.modules.startupMeetingAuth.attendeesPlaceholder"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -35,7 +35,7 @@
|
||||
:entity-id="meetingId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传会议附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupMeetingAuth.kickoffUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">培训授权详情</h1>
|
||||
<p class="page-subtitle">查看人员培训与授权信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.trainingDetailTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingDetailSubtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="姓名">{{ detail.name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">{{ detail.role || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ detail.site_name || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已培训">{{ detail.trained ? "是" : "否" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="培训日期">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已授权">{{ detail.authorized ? "是" : "否" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="授权日期">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detail.remark || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.name">{{ detail.name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.labels.role">{{ displayEnum(TEXT.enums.userRole, detail.role) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trained">{{ detail.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.trainedDate">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.authorized">{{ detail.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.authorizedDate">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,41 +2,41 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑培训授权" : "新增人员" }}</h1>
|
||||
<p class="page-subtitle">维护人员培训与授权状态</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.trainingEditTitle : TEXT.modules.startupMeetingAuth.trainingNewTitle }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingFormSubtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input v-model="form.name" placeholder="输入姓名" />
|
||||
<el-form-item :label="TEXT.common.fields.name" required>
|
||||
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-input v-model="form.role" placeholder="输入角色" />
|
||||
<el-form-item :label="TEXT.common.labels.role">
|
||||
<el-input v-model="form.role" :placeholder="TEXT.common.placeholders.input + TEXT.common.labels.role" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心">
|
||||
<el-input v-model="form.site_name" placeholder="输入分中心" />
|
||||
<el-form-item :label="TEXT.common.fields.site">
|
||||
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已培训">
|
||||
<el-form-item :label="TEXT.common.fields.trained">
|
||||
<el-switch v-model="form.trained" />
|
||||
</el-form-item>
|
||||
<el-form-item label="培训日期">
|
||||
<el-date-picker v-model="form.trained_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.trainedDate">
|
||||
<el-date-picker v-model="form.trained_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="已授权">
|
||||
<el-form-item :label="TEXT.common.fields.authorized">
|
||||
<el-switch v-model="form.authorized" />
|
||||
</el-form-item>
|
||||
<el-form-item label="授权日期">
|
||||
<el-date-picker v-model="form.authorized_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item :label="TEXT.common.fields.authorizedDate">
|
||||
<el-date-picker v-model="form.authorized_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="补充说明" />
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -48,7 +48,7 @@
|
||||
:entity-id="recordId"
|
||||
/>
|
||||
<el-card v-else class="hint-card">
|
||||
<StateEmpty description="保存后可上传授权附件" />
|
||||
<StateEmpty :description="TEXT.modules.startupMeetingAuth.trainingUploadHint" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,187 +2,195 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">受试者详情</h1>
|
||||
<p class="page-subtitle">查看病史、访视与 AE 信息</p>
|
||||
<h1 class="page-title">{{ TEXT.modules.subjectDetail.title }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectDetail.subtitle }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goEdit">编辑</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="loading">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="受试者编号">{{ detail.subject_no || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分中心">{{ siteMap[detail.site_id] || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ detail.status || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛选日期">{{ displayDate(detail.screening_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入组日期">{{ displayDate(detail.enrollment_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成日期">{{ displayDate(detail.completion_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="退出原因" :span="2">{{ detail.drop_reason || "—" }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.subjectNo">{{ detail.subject_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.site">{{ siteMap[detail.site_id] || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.status">{{ displayEnum(TEXT.enums.subjectStatus, detail.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.screeningDate">{{ displayDate(detail.screening_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.enrollmentDate">{{ displayDate(detail.enrollment_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.completionDate">{{ displayDate(detail.completion_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.common.fields.dropReason" :span="2">{{ detail.drop_reason || TEXT.common.fallback }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="病史" name="history">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.history" name="history">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openHistoryDialog()">新增病史</el-button>
|
||||
<el-button type="primary" @click="openHistoryDialog()">{{ TEXT.common.actions.newHistory }}</el-button>
|
||||
</div>
|
||||
<el-table :data="historyItems" v-loading="loadingHistory" style="width: 100%">
|
||||
<el-table-column prop="record_date" label="日期" width="140">
|
||||
<el-table-column prop="record_date" :label="TEXT.common.fields.recordDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.record_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="240" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="content" :label="TEXT.common.fields.content" min-width="240" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openHistoryDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeHistory(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openHistoryDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeHistory(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingHistory && historyItems.length === 0" description="暂无病史记录" />
|
||||
<StateEmpty v-if="!loadingHistory && historyItems.length === 0" :description="TEXT.modules.subjectDetail.emptyHistory" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="访视" name="visits">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.visits" name="visits">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openVisitDialog()">新增访视</el-button>
|
||||
<el-button type="primary" @click="openVisitDialog()">{{ TEXT.common.actions.newVisit }}</el-button>
|
||||
</div>
|
||||
<el-table :data="visitItems" v-loading="loadingVisits" style="width: 100%">
|
||||
<el-table-column prop="visit_code" label="访视编号" width="120" />
|
||||
<el-table-column prop="visit_name" label="访视名称" min-width="140" />
|
||||
<el-table-column prop="planned_date" label="计划日期" width="140">
|
||||
<el-table-column prop="visit_code" :label="TEXT.common.fields.visitCode" width="120" />
|
||||
<el-table-column prop="visit_name" :label="TEXT.common.fields.visitName" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ displayText(scope.row.visit_name, TEXT.enums.visitName) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planned_date" :label="TEXT.common.fields.plannedDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.planned_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actual_date" label="实际日期" width="140">
|
||||
<el-table-column prop="actual_date" :label="TEXT.common.fields.actualDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.actual_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.visitStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openVisitDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeVisit(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openVisitDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeVisit(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingVisits && visitItems.length === 0" description="暂无访视记录" />
|
||||
<StateEmpty v-if="!loadingVisits && visitItems.length === 0" :description="TEXT.modules.subjectDetail.emptyVisits" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="AE 信息" name="ae">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.ae" name="ae">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openAeDialog()">新增 AE</el-button>
|
||||
<el-button type="primary" @click="openAeDialog()">{{ TEXT.common.actions.newAe }}</el-button>
|
||||
</div>
|
||||
<el-table :data="aeItems" v-loading="loadingAes" style="width: 100%">
|
||||
<el-table-column prop="onset_date" label="发生日期" width="140">
|
||||
<el-table-column prop="onset_date" :label="TEXT.common.fields.occurDate" width="140">
|
||||
<template #default="scope">{{ displayDate(scope.row.onset_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="term" label="事件" min-width="140" />
|
||||
<el-table-column prop="seriousness" label="严重程度" width="120" />
|
||||
<el-table-column prop="causality" label="关联性" width="120" />
|
||||
<el-table-column prop="outcome" label="结局" width="120" />
|
||||
<el-table-column prop="description" label="备注" min-width="200" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="term" :label="TEXT.common.fields.title" min-width="140" />
|
||||
<el-table-column prop="seriousness" :label="TEXT.common.fields.seriousness" width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.aeSeriousness, scope.row.seriousness) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="causality" :label="TEXT.common.fields.causality" width="120" />
|
||||
<el-table-column prop="outcome" :label="TEXT.common.fields.outcome" width="120" />
|
||||
<el-table-column prop="description" :label="TEXT.common.fields.remark" min-width="200" />
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openAeDialog(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeAe(scope.row)">删除</el-button>
|
||||
<el-button link type="primary" size="small" @click="openAeDialog(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeAe(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingAes && aeItems.length === 0" description="暂无 AE 记录" />
|
||||
<StateEmpty v-if="!loadingAes && aeItems.length === 0" :description="TEXT.modules.subjectDetail.emptyAe" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="historyDialogVisible" title="病史记录" width="520px">
|
||||
<el-dialog v-model="historyDialogVisible" :title="TEXT.modules.subjectDetail.dialogHistory" width="520px">
|
||||
<el-form :model="historyForm" label-width="90px">
|
||||
<el-form-item label="日期">
|
||||
<el-form-item :label="TEXT.common.fields.recordDate">
|
||||
<el-date-picker v-model="historyForm.record_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" required>
|
||||
<el-form-item :label="TEXT.common.fields.content" required>
|
||||
<el-input v-model="historyForm.content" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveHistory">保存</el-button>
|
||||
<el-button @click="historyDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="visitDialogVisible" title="访视记录" width="560px">
|
||||
<el-dialog v-model="visitDialogVisible" :title="TEXT.modules.subjectDetail.dialogVisit" width="560px">
|
||||
<el-form :model="visitForm" label-width="100px">
|
||||
<template v-if="!visitEditingId">
|
||||
<el-form-item label="访视编号" required>
|
||||
<el-form-item :label="TEXT.common.fields.visitCode" required>
|
||||
<el-input v-model="visitForm.visit_code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="访视名称" required>
|
||||
<el-form-item :label="TEXT.common.fields.visitName" required>
|
||||
<el-input v-model="visitForm.visit_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期">
|
||||
<el-form-item :label="TEXT.common.fields.plannedDate">
|
||||
<el-date-picker v-model="visitForm.planned_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口开始">
|
||||
<el-form-item :label="TEXT.common.fields.windowStart">
|
||||
<el-date-picker v-model="visitForm.window_start" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="窗口结束">
|
||||
<el-form-item :label="TEXT.common.fields.windowEnd">
|
||||
<el-date-picker v-model="visitForm.window_end" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="实际日期">
|
||||
<el-form-item :label="TEXT.common.fields.actualDate">
|
||||
<el-date-picker v-model="visitForm.actual_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-form-item :label="TEXT.common.fields.status">
|
||||
<el-select v-model="visitForm.status">
|
||||
<el-option label="计划中" value="PLANNED" />
|
||||
<el-option label="已完成" value="DONE" />
|
||||
<el-option label="未完成" value="MISSED" />
|
||||
<el-option :label="TEXT.enums.visitStatus.PLANNED" value="PLANNED" />
|
||||
<el-option :label="TEXT.enums.visitStatus.DONE" value="DONE" />
|
||||
<el-option :label="TEXT.enums.visitStatus.MISSED" value="MISSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="visitForm.notes" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visitDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveVisit">保存</el-button>
|
||||
<el-button @click="visitDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="aeDialogVisible" title="AE 记录" width="560px">
|
||||
<el-dialog v-model="aeDialogVisible" :title="TEXT.modules.subjectDetail.dialogAe" width="560px">
|
||||
<el-form :model="aeForm" label-width="100px">
|
||||
<el-form-item label="事件" required>
|
||||
<el-form-item :label="TEXT.common.fields.title" required>
|
||||
<el-input v-model="aeForm.term" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发生日期">
|
||||
<el-form-item :label="TEXT.common.fields.occurDate">
|
||||
<el-date-picker v-model="aeForm.onset_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" required>
|
||||
<el-form-item :label="TEXT.common.fields.seriousness" required>
|
||||
<el-select v-model="aeForm.seriousness">
|
||||
<el-option label="严重" value="SERIOUS" />
|
||||
<el-option label="一般" value="NON_SERIOUS" />
|
||||
<el-option :label="TEXT.enums.aeSeriousness.SERIOUS" value="SERIOUS" />
|
||||
<el-option :label="TEXT.enums.aeSeriousness.NON_SERIOUS" value="NON_SERIOUS" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重等级" required>
|
||||
<el-form-item :label="TEXT.common.fields.severity" required>
|
||||
<el-select v-model="aeForm.severity">
|
||||
<el-option label="轻度" value="MILD" />
|
||||
<el-option label="中度" value="MODERATE" />
|
||||
<el-option label="重度" value="SEVERE" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.MILD" value="MILD" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.MODERATE" value="MODERATE" />
|
||||
<el-option :label="TEXT.enums.aeSeverity.SEVERE" value="SEVERE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联性">
|
||||
<el-form-item :label="TEXT.common.fields.causality">
|
||||
<el-input v-model="aeForm.causality" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结局">
|
||||
<el-form-item :label="TEXT.common.fields.outcome">
|
||||
<el-input v-model="aeForm.outcome" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-form-item :label="TEXT.common.fields.remark">
|
||||
<el-input v-model="aeForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="aeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveAe">保存</el-button>
|
||||
<el-button @click="aeDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,51 +2,51 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">{{ isEdit ? "编辑受试者" : "新增受试者" }}</h1>
|
||||
<p class="page-subtitle">维护受试者基本信息</p>
|
||||
<h1 class="page-title">{{ isEdit ? TEXT.modules.subjectForm.titleEdit : TEXT.modules.subjectForm.titleNew }}</h1>
|
||||
<p class="page-subtitle">{{ TEXT.modules.subjectForm.subtitle }}</p>
|
||||
</div>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="受试者编号" required>
|
||||
<el-input v-model="form.subject_no" :disabled="isEdit" placeholder="输入受试者编号" />
|
||||
<el-form-item :label="TEXT.common.fields.subjectNo" required>
|
||||
<el-input v-model="form.subject_no" :disabled="isEdit" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.subjectNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分中心" required>
|
||||
<el-select v-model="form.site_id" :disabled="isEdit" placeholder="选择分中心">
|
||||
<el-form-item :label="TEXT.common.fields.site" required>
|
||||
<el-select v-model="form.site_id" :disabled="isEdit" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option v-for="site in sites" :key="site.id" :label="site.name" :value="site.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="筛选日期">
|
||||
<el-form-item :label="TEXT.common.fields.screeningDate">
|
||||
<el-date-picker
|
||||
v-model="form.screening_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="状态">
|
||||
<el-select v-model="form.status" placeholder="选择状态">
|
||||
<el-option label="筛选" value="SCREENING" />
|
||||
<el-option label="入组" value="ENROLLED" />
|
||||
<el-option label="完成" value="COMPLETED" />
|
||||
<el-option label="退出" value="DROPPED" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.status">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option :label="TEXT.enums.subjectStatus.SCREENING" value="SCREENING" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.ENROLLED" value="ENROLLED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.COMPLETED" value="COMPLETED" />
|
||||
<el-option :label="TEXT.enums.subjectStatus.DROPPED" value="DROPPED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="入组日期">
|
||||
<el-date-picker v-model="form.enrollment_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.enrollmentDate">
|
||||
<el-date-picker v-model="form.enrollment_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="完成日期">
|
||||
<el-date-picker v-model="form.completion_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.completionDate">
|
||||
<el-date-picker v-model="form.completion_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" label="退出原因">
|
||||
<el-input v-model="form.drop_reason" type="textarea" :rows="3" placeholder="输入退出原因" />
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.dropReason">
|
||||
<el-input v-model="form.drop_reason" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user