From a4ad087a0a5466ff9ade86654f74a9e129285d1f Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Thu, 18 Dec 2025 16:24:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=A6=E7=90=86=E4=B8=8E=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86-=E7=AE=80=E5=8D=95=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/schemas/member.py | 2 +- frontend/src/api/members.ts | 4 +- frontend/src/components/MilestoneForm.vue | 56 ++++++- frontend/src/dictionaries/milestone.dict.ts | 41 +++++ frontend/src/router/index.ts | 7 + frontend/src/views/MilestoneDetail.vue | 158 ++++++++++++++++++++ frontend/src/views/Milestones.vue | 132 ++++++++++++---- frontend/src/views/Tasks.vue | 2 +- frontend/src/views/admin/ProjectMembers.vue | 4 +- pg_data/base/16384/16476 | Bin 8192 -> 8192 bytes pg_data/base/16384/16489 | Bin 8192 -> 8192 bytes pg_data/global/pg_control | Bin 8192 -> 8192 bytes pg_data/pg_wal/000000010000000000000001 | Bin 16777216 -> 16777216 bytes pg_data/pg_xact/0000 | Bin 8192 -> 8192 bytes 14 files changed, 366 insertions(+), 40 deletions(-) create mode 100644 frontend/src/dictionaries/milestone.dict.ts create mode 100644 frontend/src/views/MilestoneDetail.vue diff --git a/backend/app/schemas/member.py b/backend/app/schemas/member.py index 0fa7935c..35654de1 100644 --- a/backend/app/schemas/member.py +++ b/backend/app/schemas/member.py @@ -4,7 +4,7 @@ from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field -StudyRole = Literal["PM", "CRA", "PV", "IMP"] +StudyRole = Literal["PM", "CRA", "PV", "IMP", "ADMIN"] class StudyMemberCreate(BaseModel): diff --git a/frontend/src/api/members.ts b/frontend/src/api/members.ts index b9a1a29b..4ab83dcb 100644 --- a/frontend/src/api/members.ts +++ b/frontend/src/api/members.ts @@ -1,8 +1,8 @@ import { apiDelete, apiGet, apiPatch, apiPost } from "./axios"; import type { StudyMember } from "../types/api"; -export const listMembers = (studyId: string) => - apiGet(`/api/v1/studies/${studyId}/members/`); +export const listMembers = (studyId: string, params?: Record) => + apiGet(`/api/v1/studies/${studyId}/members/`, { params }); export const addMember = (studyId: string, payload: { user_id: string; role_in_study: string; is_active?: boolean }) => apiPost(`/api/v1/studies/${studyId}/members/`, payload); diff --git a/frontend/src/components/MilestoneForm.vue b/frontend/src/components/MilestoneForm.vue index c868176c..e2c5e5fd 100644 --- a/frontend/src/components/MilestoneForm.vue +++ b/frontend/src/components/MilestoneForm.vue @@ -6,7 +6,17 @@ - + + + + + + + + + + + @@ -17,7 +27,7 @@ - + @@ -37,10 +47,14 @@ import type { FormInstance, FormRules } from "element-plus"; import { ElMessage } from "element-plus"; import { createMilestone, updateMilestone } from "../api/milestones"; import { useStudyStore } from "../store/study"; +import { milestoneStatusOptions, milestoneTypeOptions } from "../dictionaries/milestone.dict"; interface Props { modelValue: boolean; milestone?: Record; + sites?: any[]; + members?: any[]; + users?: any[]; } const props = defineProps(); @@ -53,12 +67,14 @@ const visible = computed({ set: (v: boolean) => emit("update:modelValue", v), }); -const types = ["ETHICS_SUBMISSION", "ETHICS_APPROVAL", "SIV", "FPI", "LPI", "DBL", "CSR"]; -const statuses = ["NOT_STARTED", "IN_PROGRESS", "DONE", "BLOCKED"]; +const types = milestoneTypeOptions; +const statuses = milestoneStatusOptions; const form = reactive({ name: "", type: "", + site_id: "", + owner_id: "", planned_date: "", actual_date: "", status: "NOT_STARTED", @@ -68,6 +84,8 @@ const form = reactive({ const rules: FormRules = { name: [{ required: true, message: "请输入名称", trigger: "blur" }], type: [{ required: true, message: "请选择类型", trigger: "change" }], + site_id: [{ required: true, message: "请选择中心", trigger: "change" }], + owner_id: [{ required: true, message: "请选择负责人", trigger: "change" }], planned_date: [{ required: true, message: "请选择计划日期", trigger: "change" }], }; @@ -75,12 +93,32 @@ const submitting = ref(false); const isEdit = computed(() => !!props.milestone); +const memberOptions = computed(() => { + const userMap = (props.users || []).reduce>((acc, cur: any) => { + if (cur?.id) acc[cur.id] = cur.username || cur.id; + return acc; + }, {}); + const memberList = (props.members || []).filter((m: any) => m.is_active !== false); + if (memberList.length) { + return memberList.map((m: any) => ({ + value: m.user_id, + label: userMap[m.user_id] || m.username || m.user_id, + })); + } + return (props.users || []).map((u: any) => ({ + value: u.id, + label: u.username || u.id, + })); +}); + watch( () => props.milestone, (val) => { if (val) { form.name = val.name || ""; form.type = val.type || ""; + form.site_id = val.site_id || ""; + form.owner_id = val.owner_id || ""; form.planned_date = val.planned_date || ""; form.actual_date = val.actual_date || ""; form.status = val.status || "NOT_STARTED"; @@ -88,6 +126,8 @@ watch( } else { form.name = ""; form.type = ""; + form.site_id = ""; + form.owner_id = ""; form.planned_date = ""; form.actual_date = ""; form.status = "NOT_STARTED"; @@ -111,10 +151,14 @@ const onSubmit = async () => { } submitting.value = true; try { + const payload = { + ...form, + actual_date: form.actual_date || null, + }; if (isEdit.value && props.milestone) { - await updateMilestone(study.currentStudy.id, props.milestone.id, form); + await updateMilestone(study.currentStudy.id, props.milestone.id, payload); } else { - await createMilestone(study.currentStudy.id, form); + await createMilestone(study.currentStudy.id, payload); } ElMessage.success("提交成功"); emit("success"); diff --git a/frontend/src/dictionaries/milestone.dict.ts b/frontend/src/dictionaries/milestone.dict.ts new file mode 100644 index 00000000..b0882679 --- /dev/null +++ b/frontend/src/dictionaries/milestone.dict.ts @@ -0,0 +1,41 @@ +import { createDict, getDictColor, getDictLabel } from "./utils"; +import type { Dict } from "./types"; + +export const milestoneTypeDict: Record = { + ETHICS_SUBMISSION: "伦理递交", + ETHICS_APPROVAL: "伦理批件", + SITE_INITIATION: "中心启动", + SIV: "启动会", + FPI: "首例入组", + LPI: "末例入组", + DBL: "揭盲", + CSR: "总结报告", +}; + +const milestoneStatusItems = [ + { value: "NOT_STARTED", label: "未开始", color: "info", order: 1 }, + { value: "IN_PROGRESS", label: "进行中", color: "warning", order: 2 }, + { value: "DONE", label: "已完成", color: "success", order: 3 }, + { value: "COMPLETED", label: "已完成", color: "success", order: 3 }, + { value: "BLOCKED", label: "受阻", color: "danger", order: 4 }, +]; + +export const milestoneStatusDict: Dict = createDict(milestoneStatusItems); + +export const milestoneTypeOptions = Object.entries(milestoneTypeDict).map(([value, label]) => ({ + value, + label, +})); + +export const milestoneStatusOptions = ["NOT_STARTED", "IN_PROGRESS", "DONE", "BLOCKED"].map((value) => ({ + value, + label: getDictLabel(milestoneStatusDict, value) || value, + color: getDictColor(milestoneStatusDict, value), +})); + +export const getMilestoneTypeLabel = (value?: string | null) => milestoneTypeDict[value || ""] || value || "—"; + +export const getMilestoneStatusLabel = (value?: string | null) => + getDictLabel(milestoneStatusDict, value) || value || "—"; + +export const getMilestoneStatusColor = (value?: string | null) => getDictColor(milestoneStatusDict, value) || "info"; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 33838cdd..06ca101e 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -7,6 +7,7 @@ import StudyList from "../views/StudyList.vue"; import StudyHome from "../views/StudyHome.vue"; import Tasks from "../views/Tasks.vue"; import Milestones from "../views/Milestones.vue"; +import MilestoneDetail from "../views/MilestoneDetail.vue"; import Subjects from "../views/Subjects.vue"; import SubjectDetail from "../views/SubjectDetail.vue"; import MyWorkbench from "../views/workbench/MyWorkbench.vue"; @@ -68,6 +69,12 @@ const routes: RouteRecordRaw[] = [ component: Milestones, meta: { title: "里程碑", requiresStudy: true }, }, + { + path: "study/milestones/:milestoneId", + name: "StudyMilestoneDetail", + component: MilestoneDetail, + meta: { title: "节点详情", requiresStudy: true }, + }, { path: "study/subjects", name: "StudySubjects", diff --git a/frontend/src/views/MilestoneDetail.vue b/frontend/src/views/MilestoneDetail.vue new file mode 100644 index 00000000..1de15829 --- /dev/null +++ b/frontend/src/views/MilestoneDetail.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/frontend/src/views/Milestones.vue b/frontend/src/views/Milestones.vue index 3e002679..53adcd37 100644 --- a/frontend/src/views/Milestones.vue +++ b/frontend/src/views/Milestones.vue @@ -10,48 +10,89 @@ - - + + + + - - - + + + + + + + + + + + + + - - +