管理后台前后端逻辑、UI美化
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
<el-button @click="cancelSubjectEdit">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="startSubjectEdit">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="startSubjectEdit">
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -72,7 +74,9 @@
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.history" name="history">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openHistoryDialog()">{{ TEXT.common.actions.newHistory }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @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="TEXT.common.fields.recordDate" width="140">
|
||||
@@ -81,8 +85,24 @@
|
||||
<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)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeHistory(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="openHistoryDialog(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="removeHistory(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -91,7 +111,9 @@
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.visits" name="visits">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openVisitDialog()">{{ TEXT.common.actions.newVisit }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @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="TEXT.common.fields.visitCode" width="120" />
|
||||
@@ -135,12 +157,32 @@
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="160">
|
||||
<template #default="scope">
|
||||
<template v-if="visitEditingRowId === scope.row.id">
|
||||
<el-button link type="primary" size="small" @click="saveVisitRow(scope.row)">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button link size="small" @click="cancelVisitRow">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button link type="primary" size="small" :disabled="isReadOnly" @click="saveVisitRow(scope.row)">
|
||||
{{ TEXT.common.actions.save }}
|
||||
</el-button>
|
||||
<el-button link size="small" :disabled="isReadOnly" @click="cancelVisitRow">
|
||||
{{ TEXT.common.actions.cancel }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button link type="primary" size="small" @click="startVisitRowEdit(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>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="startVisitRowEdit(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="removeVisit(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -150,7 +192,9 @@
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.ae" name="ae">
|
||||
<div class="tab-actions">
|
||||
<el-button type="primary" @click="openAeDialog()">{{ TEXT.common.actions.newAe }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @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="TEXT.common.fields.occurDate" width="140">
|
||||
@@ -165,8 +209,24 @@
|
||||
<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)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeAe(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="openAeDialog(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="isReadOnly"
|
||||
@click="removeAe(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -186,7 +246,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="historyDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveHistory">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -217,7 +277,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visitDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveVisit">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -254,14 +314,14 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="aeDialogVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :disabled="isReadOnly" @click="saveAe">{{ TEXT.common.actions.save }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
@@ -303,6 +363,8 @@ const subjectForm = reactive({
|
||||
});
|
||||
|
||||
const siteMap = ref<Record<string, string>>({});
|
||||
const siteActiveMap = ref<Record<string, boolean>>({});
|
||||
const isReadOnly = computed(() => !!detail.site_id && siteActiveMap.value[detail.site_id] === false);
|
||||
const activeTab = ref("history");
|
||||
|
||||
const historyItems = ref<any[]>([]);
|
||||
@@ -355,8 +417,13 @@ const loadSites = async () => {
|
||||
acc[site.id] = site.name;
|
||||
return acc;
|
||||
}, {});
|
||||
siteActiveMap.value = list.reduce((acc: Record<string, boolean>, site: any) => {
|
||||
acc[site.id] = !!site.is_active;
|
||||
return acc;
|
||||
}, {});
|
||||
} catch {
|
||||
siteMap.value = {};
|
||||
siteActiveMap.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -374,6 +441,10 @@ const loadSubject = async () => {
|
||||
};
|
||||
|
||||
const startSubjectEdit = () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
subjectForm.status = detail.status || "SCREENING";
|
||||
subjectForm.consent_date = detail.consent_date || "";
|
||||
subjectForm.enrollment_date = detail.enrollment_date || "";
|
||||
@@ -387,6 +458,10 @@ const cancelSubjectEdit = () => {
|
||||
};
|
||||
|
||||
const saveSubjectEdit = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
subjectSaving.value = true;
|
||||
try {
|
||||
@@ -422,6 +497,10 @@ const loadHistories = async () => {
|
||||
};
|
||||
|
||||
const openHistoryDialog = (row?: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
historyEditingId.value = row?.id || null;
|
||||
historyForm.record_date = row?.record_date || "";
|
||||
historyForm.content = row?.content || "";
|
||||
@@ -429,6 +508,10 @@ const openHistoryDialog = (row?: any) => {
|
||||
};
|
||||
|
||||
const saveHistory = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -449,6 +532,10 @@ const saveHistory = async () => {
|
||||
};
|
||||
|
||||
const removeHistory = 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;
|
||||
@@ -474,6 +561,10 @@ const loadVisits = async () => {
|
||||
};
|
||||
|
||||
const openVisitDialog = (row?: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
visitEditingId.value = row?.id || null;
|
||||
if (row) {
|
||||
visitForm.actual_date = row.actual_date || "";
|
||||
@@ -490,6 +581,10 @@ const openVisitDialog = (row?: any) => {
|
||||
};
|
||||
|
||||
const startVisitRowEdit = (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
visitEditingRowId.value = row?.id || null;
|
||||
visitRowDraft.actual_date = row?.actual_date || "";
|
||||
visitRowDraft.notes = row?.notes || "";
|
||||
@@ -500,6 +595,10 @@ const cancelVisitRow = () => {
|
||||
};
|
||||
|
||||
const saveVisitRow = async (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId || !row?.id) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -559,6 +658,10 @@ const getVisitStatusTagType = (status: string) => {
|
||||
};
|
||||
|
||||
const saveVisit = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
if (visitEditingId.value) {
|
||||
@@ -585,6 +688,10 @@ const saveVisit = async () => {
|
||||
};
|
||||
|
||||
const removeVisit = 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;
|
||||
@@ -610,6 +717,10 @@ const loadAes = async () => {
|
||||
};
|
||||
|
||||
const openAeDialog = (row?: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
aeEditingId.value = row?.id || null;
|
||||
Object.assign(aeForm, {
|
||||
term: row?.term || "",
|
||||
@@ -624,6 +735,10 @@ const openAeDialog = (row?: any) => {
|
||||
};
|
||||
|
||||
const saveAe = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId || !subjectId) return;
|
||||
try {
|
||||
const payload = {
|
||||
@@ -650,6 +765,10 @@ const saveAe = async () => {
|
||||
};
|
||||
|
||||
const removeAe = async (row: any) => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<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-input
|
||||
v-model="form.subject_no"
|
||||
:disabled="isEdit || isReadOnly"
|
||||
:placeholder="TEXT.common.placeholders.input + TEXT.common.fields.subjectNo"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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 v-model="form.site_id" :disabled="isEdit || isReadOnly" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-option v-for="site in sites" :key="site.id" :label="site.name" :value="site.id" :disabled="!site.is_active" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.screeningDate">
|
||||
@@ -24,7 +28,7 @@
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isEdit"
|
||||
:disabled="isEdit || isReadOnly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.consentDate">
|
||||
@@ -33,10 +37,11 @@
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isEdit" :label="TEXT.common.fields.status">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
|
||||
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select" :disabled="isReadOnly">
|
||||
<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" />
|
||||
@@ -44,16 +49,36 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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-date-picker
|
||||
v-model="form.enrollment_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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-date-picker
|
||||
v-model="form.completion_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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-input
|
||||
v-model="form.drop_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="TEXT.common.placeholders.input"
|
||||
:disabled="isReadOnly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
|
||||
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">
|
||||
{{ TEXT.common.actions.save }}
|
||||
</el-button>
|
||||
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -75,10 +100,18 @@ const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const saving = ref(false);
|
||||
const sites = ref<any[]>([]);
|
||||
const siteActiveMap = computed(() => {
|
||||
const map: Record<string, boolean> = {};
|
||||
sites.value.forEach((site) => {
|
||||
if (site?.id) map[site.id] = !!site.is_active;
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const subjectId = computed(() => route.params.subjectId as string | undefined);
|
||||
const isEdit = computed(() => !!subjectId.value);
|
||||
const studyId = computed(() => study.currentStudy?.id || "");
|
||||
const isReadOnly = computed(() => isEdit.value && !!form.site_id && siteActiveMap.value[form.site_id] === false);
|
||||
|
||||
const form = reactive({
|
||||
subject_no: "",
|
||||
@@ -121,6 +154,10 @@ const load = async () => {
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (isReadOnly.value) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
if (!studyId.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user