参与者管理- PD模块内容补充
This commit is contained in:
@@ -36,8 +36,8 @@
|
||||
</div>
|
||||
<div class="setup-view-mode">
|
||||
<el-radio-group v-model="setupViewMode" size="small">
|
||||
<el-radio-button label="draft">草稿视图</el-radio-button>
|
||||
<el-radio-button label="published">发布预览</el-radio-button>
|
||||
<el-radio-button label="draft">草稿</el-radio-button>
|
||||
<el-radio-button label="published">预览</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3623,7 +3623,7 @@ const startEdit = () => {
|
||||
if (!project.value) return;
|
||||
if (activeStep.value !== 0 && activeStep.value !== 2 && activeStep.value !== 4) return;
|
||||
if (isPublishedView.value) {
|
||||
ElMessage.info("当前为发布预览,请先切换到草稿视图后再编辑");
|
||||
ElMessage.info("当前为预览,请先切换到草稿后再编辑");
|
||||
return;
|
||||
}
|
||||
if (!canManageSetup.value) {
|
||||
@@ -4128,7 +4128,7 @@ const addProjectMilestone = () => {
|
||||
const ensureStep2Editable = (): boolean => {
|
||||
if (!project.value) return false;
|
||||
if (isPublishedView.value) {
|
||||
ElMessage.info("当前为发布预览,请先切换到草稿视图后再操作");
|
||||
ElMessage.info("当前为预览,请先切换到草稿后再操作");
|
||||
return false;
|
||||
}
|
||||
if (!canManageSetup.value) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="filter-container unified-action-bar">
|
||||
<el-form :inline="true" class="filter-form">
|
||||
<el-form-item class="filter-item-form">
|
||||
<el-input v-model="filters.keyword" :placeholder="TEXT.modules.subjectManagement.screeningNo" clearable class="filter-input-comp">
|
||||
<el-input v-model="filters.keyword" :placeholder="TEXT.common.fields.keyword" clearable class="filter-input-comp">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
@@ -34,29 +34,20 @@
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="unified-section table-section">
|
||||
<el-table :data="filteredItems" v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="subject_no" :label="TEXT.modules.subjectManagement.screeningNo" min-width="160" />
|
||||
<el-table-column :label="TEXT.common.fields.site" min-width="160">
|
||||
<el-table :data="filteredItems" v-loading="loading" style="width: 100%" table-layout="fixed">
|
||||
<el-table-column prop="pd_no" :label="TEXT.common.fields.pdNo" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="subject_no" :label="TEXT.modules.subjectManagement.screeningNo" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column :label="TEXT.common.fields.site" min-width="160" show-overflow-tooltip>
|
||||
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<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 prop="status" :label="TEXT.common.fields.status" min-width="160">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.pdStatus, 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 prop="updated_at" :label="TEXT.common.labels.updatedAt" min-width="180">
|
||||
<template #default="scope">{{ displayDate(scope.row.updated_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.riskIssuePd.syncStatus" width="120">
|
||||
<template #default>
|
||||
<el-tag size="small" type="success" effect="plain">{{ TEXT.modules.riskIssuePd.synced }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="120">
|
||||
<el-table-column prop="description" :label="TEXT.common.fields.pdDescription" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="160" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="goSubject(scope.row.id)">
|
||||
{{ TEXT.modules.riskIssuePd.toSubject }}
|
||||
<el-button link type="primary" size="small" @click="goSubject(scope.row.subject_id)">
|
||||
{{ TEXT.common.actions.view }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -73,9 +64,9 @@ import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { CircleCheck, Location, Search } from "@element-plus/icons-vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { fetchSubjects } from "../../api/subjects";
|
||||
import { listStudySubjectPds } from "../../api/subjectPds";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { displayDate, displayEnum } from "../../utils/display";
|
||||
import { displayEnum } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
@@ -91,7 +82,7 @@ const filters = ref({
|
||||
status: "",
|
||||
});
|
||||
|
||||
const statusOptions = Object.entries(TEXT.enums.subjectStatus).map(([value, label]) => ({ value, label }));
|
||||
const statusOptions = Object.entries(TEXT.enums.pdStatus).map(([value, label]) => ({ value, label }));
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.value.keyword = "";
|
||||
@@ -120,10 +111,16 @@ const load = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadSites(studyId);
|
||||
const { data } = await fetchSubjects(studyId);
|
||||
items.value = (Array.isArray(data) ? data : data.items || []).sort((a: any, b: any) =>
|
||||
String(b.updated_at || "").localeCompare(String(a.updated_at || ""))
|
||||
);
|
||||
const { data } = await listStudySubjectPds(studyId);
|
||||
const list = Array.isArray(data) ? data : data.items || [];
|
||||
const getPdSeq = (pdNo: string) => {
|
||||
const text = String(pdNo || "");
|
||||
const matched = text.match(/^PD(\d+)$/i);
|
||||
if (!matched) return Number.POSITIVE_INFINITY;
|
||||
const num = Number(matched[1]);
|
||||
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
|
||||
};
|
||||
items.value = list.sort((a: any, b: any) => getPdSeq(a?.pd_no) - getPdSeq(b?.pd_no));
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
@@ -134,8 +131,10 @@ const load = async () => {
|
||||
const filteredItems = computed(() => {
|
||||
const keyword = filters.value.keyword.trim().toLowerCase();
|
||||
return items.value.filter((item) => {
|
||||
const pdNo = String(item?.pd_no || "").toLowerCase();
|
||||
const subjectNo = String(item?.subject_no || "").toLowerCase();
|
||||
if (keyword && !subjectNo.includes(keyword)) return false;
|
||||
const description = String(item?.description || "").toLowerCase();
|
||||
if (keyword && !pdNo.includes(keyword) && !subjectNo.includes(keyword) && !description.includes(keyword)) return false;
|
||||
if (filters.value.siteId && item?.site_id !== filters.value.siteId) return false;
|
||||
if (filters.value.status && item?.status !== filters.value.status) return false;
|
||||
return true;
|
||||
|
||||
@@ -253,7 +253,47 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.pd" name="pd">
|
||||
<StateEmpty :description="TEXT.modules.subjectDetail.emptyPd" />
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="openPdDialog()">
|
||||
{{ TEXT.modules.subjectDetail.newPd }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="pdItems" v-loading="loadingPds" style="width: 100%">
|
||||
<el-table-column prop="pd_no" :label="TEXT.common.fields.pdNo" width="130" />
|
||||
<el-table-column prop="pd_type" :label="TEXT.common.fields.pdType" min-width="140" />
|
||||
<el-table-column prop="pd_level" :label="TEXT.common.fields.pdLevel" min-width="120">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.pdLevel, scope.row.pd_level) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" :label="TEXT.common.fields.pdDescription" min-width="240">
|
||||
<template #default="scope">{{ scope.row.description || TEXT.common.fallback }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" min-width="100">
|
||||
<template #default="scope">{{ displayEnum(TEXT.enums.pdStatus, scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="150">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="openPdDialog(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="removePd(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<StateEmpty v-if="!loadingPds && pdItems.length === 0" :description="TEXT.modules.subjectDetail.emptyPd" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
@@ -342,6 +382,36 @@
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog append-to=".layout-main .content-wrapper" v-model="pdDialogVisible" :title="TEXT.modules.subjectDetail.dialogPd" width="560px">
|
||||
<el-form :model="pdForm" label-width="100px">
|
||||
<el-form-item :label="TEXT.common.fields.pdNo">
|
||||
<el-input v-model="pdForm.pd_no" disabled :placeholder="TEXT.modules.subjectDetail.pdNoAuto" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.pdType" required>
|
||||
<el-select v-model="pdForm.pd_type" filterable allow-create default-first-option clearable>
|
||||
<el-option v-for="item in pdTypeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.pdLevel" required>
|
||||
<el-select v-model="pdForm.pd_level">
|
||||
<el-option v-for="option in pdLevelOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.status" required>
|
||||
<el-select v-model="pdForm.status">
|
||||
<el-option v-for="option in pdStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.pdDescription">
|
||||
<el-input v-model="pdForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pdDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="savePd">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -355,6 +425,7 @@ 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 { listSubjectPds, createSubjectPd, updateSubjectPd, deleteSubjectPd } from "../../api/subjectPds";
|
||||
import { displayDate, displayEnum, displayText } from "../../utils/display";
|
||||
import StateEmpty from "../../components/StateEmpty.vue";
|
||||
import { TEXT } from "../../locales";
|
||||
@@ -461,6 +532,22 @@ const aeSeriousnessOptions = [
|
||||
{ value: "IV", label: TEXT.enums.aeSeriousness.IV },
|
||||
{ value: "V", label: TEXT.enums.aeSeriousness.V },
|
||||
];
|
||||
|
||||
const pdItems = ref<any[]>([]);
|
||||
const loadingPds = ref(false);
|
||||
const pdDialogVisible = ref(false);
|
||||
const pdEditingId = ref<string | null>(null);
|
||||
const pdTypeOptions = ["知情同意", "排除标准", "药物使用", "入排标准", "访视流程", "检验检查", "其他"];
|
||||
const pdLevelOptions = Object.entries(TEXT.enums.pdLevel).map(([value, label]) => ({ value, label }));
|
||||
const pdStatusOptions = Object.entries(TEXT.enums.pdStatus).map(([value, label]) => ({ value, label }));
|
||||
const pdForm = reactive({
|
||||
pd_no: "",
|
||||
pd_type: "",
|
||||
pd_level: "GENERAL",
|
||||
description: "",
|
||||
status: "OPEN",
|
||||
});
|
||||
|
||||
const normalizeSeriousness = (value?: string | null) => {
|
||||
if (!value) return "I";
|
||||
const normalized = String(value).trim().toUpperCase();
|
||||
@@ -905,6 +992,88 @@ const removeAe = async (row: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPds = async () => {
|
||||
if (!studyId || !subjectId) return;
|
||||
loadingPds.value = true;
|
||||
try {
|
||||
const { data } = (await listSubjectPds(studyId, subjectId)) as any;
|
||||
const list = Array.isArray(data) ? data : data.items || [];
|
||||
const getPdSeq = (pdNo: string) => {
|
||||
const text = String(pdNo || "");
|
||||
const matched = text.match(/^PD(\d+)$/i);
|
||||
if (!matched) return Number.POSITIVE_INFINITY;
|
||||
const num = Number(matched[1]);
|
||||
return Number.isFinite(num) ? num : Number.POSITIVE_INFINITY;
|
||||
};
|
||||
pdItems.value = list.sort((a: any, b: any) => getPdSeq(a?.pd_no) - getPdSeq(b?.pd_no));
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
loadingPds.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openPdDialog = (row?: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
pdEditingId.value = row?.id || null;
|
||||
Object.assign(pdForm, {
|
||||
pd_no: row?.pd_no || "",
|
||||
pd_type: row?.pd_type || "",
|
||||
pd_level: row?.pd_level || "GENERAL",
|
||||
description: row?.description || "",
|
||||
status: row?.status || "OPEN",
|
||||
});
|
||||
pdDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const savePd = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
if (!pdForm.pd_type?.trim()) {
|
||||
ElMessage.error(TEXT.common.messages.required);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const basePayload = {
|
||||
pd_type: pdForm.pd_type.trim(),
|
||||
pd_level: pdForm.pd_level,
|
||||
description: pdForm.description?.trim() || null,
|
||||
status: pdForm.status,
|
||||
};
|
||||
if (pdEditingId.value) {
|
||||
await updateSubjectPd(studyId, subjectId, pdEditingId.value, basePayload);
|
||||
} else {
|
||||
await createSubjectPd(studyId, subjectId, { subject_id: subjectId, ...basePayload });
|
||||
}
|
||||
pdDialogVisible.value = false;
|
||||
loadPds();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const removePd = async (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteSubjectPd(studyId, subjectId, row.id);
|
||||
loadPds();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => router.push("/subjects");
|
||||
|
||||
watch(
|
||||
@@ -920,6 +1089,7 @@ onMounted(async () => {
|
||||
loadHistories();
|
||||
loadVisits();
|
||||
loadAes();
|
||||
loadPds();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user