统一业务页面权限与抽屉编辑

1、将 FAQ 分类和问题维护改为抽屉交互,并按分类、问题、回复的细粒度权限控制入口。

2、将注意事项、可行性和伦理记录接入详情页抽屉编辑,创建时支持先保存主记录再上传附件。

3、将参与者基础信息编辑改为抽屉,详情页按可读权限显示病史、访视、AE 和 PD 标签页。

4、补充业务页面权限一致性测试,覆盖停用中心、无权限入口和面包屑上下文。
This commit is contained in:
Cheng Zhou
2026-06-04 11:10:20 +08:00
parent 6a2e43f829
commit 6e8494abd5
39 changed files with 2334 additions and 337 deletions
+19 -4
View File
@@ -102,9 +102,9 @@
<el-table-column prop="remark" :label="TEXT.modules.projectMilestones.columns.remark" show-overflow-tooltip>
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.modules.projectMilestones.columns.operations" width="100">
<el-table-column v-if="canUpdateMilestones" :label="TEXT.modules.projectMilestones.columns.operations" width="100">
<template #default="scope">
<el-button link type="primary" @click="openEditor(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button v-if="canUpdateMilestones" link type="primary" @click="openEditor(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
</template>
</el-table-column>
<template #empty>
@@ -120,7 +120,8 @@
v-model="editorVisible"
direction="rtl"
size="460px"
:close-on-click-modal="false"
:close-on-click-modal="true"
:before-close="editorDirtyGuard.beforeClose"
:show-close="false"
class="milestone-editor-drawer"
>
@@ -185,7 +186,7 @@
<template #footer>
<div class="time-editor-footer">
<el-button @click="editorVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" @click="saveEditor">{{ TEXT.common.actions.save }}</el-button>
<el-button v-if="canUpdateMilestones" type="primary" @click="saveEditor">{{ TEXT.common.actions.save }}</el-button>
</div>
</template>
</el-drawer>
@@ -200,6 +201,8 @@ import { listProjectMilestones, updateProjectMilestone } from "../../api/project
import { TEXT } from "../../locales";
import StateEmpty from "../../components/StateEmpty.vue";
import { displayDateTime } from "../../utils/display";
import { usePermission } from "../../utils/permission";
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
interface MilestoneRow {
id: string;
@@ -231,6 +234,7 @@ type MilestoneLocalEdit = {
};
const study = useStudyStore();
const { can } = usePermission();
const rows = ref<MilestoneRow[]>([]);
const loading = ref(false);
const dataSourceLabel = ref<string>(TEXT.modules.projectMilestones.sourcePublished);
@@ -244,8 +248,10 @@ const editorForm = reactive<MilestoneLocalEdit>({
actualEndDate: "",
remark: "",
});
const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);
const doneCount = computed(() => rows.value.filter((item) => getStatusView(item).completed).length);
const canUpdateMilestones = computed(() => can("project.milestones.update"));
const toDateOnly = (value?: string): Date | null => {
if (!value) return null;
@@ -398,18 +404,27 @@ const validateDateRange = (start?: string, end?: string, label?: string) => {
};
const openEditor = (row: MilestoneRow) => {
if (!canUpdateMilestones.value) {
ElMessage.warning("权限不足");
return;
}
editingRowId.value = row.id;
editorForm.adjustedStartDate = row.adjustedStartDate || "";
editorForm.adjustedEndDate = row.adjustedEndDate || "";
editorForm.actualStartDate = row.actualStartDate || "";
editorForm.actualEndDate = row.actualEndDate || "";
editorForm.remark = row.remark || "";
editorDirtyGuard.syncBaseline();
editorVisible.value = true;
};
const saveEditor = async () => {
const studyId = study.currentStudy?.id;
if (!studyId || !editingRowId.value) return;
if (!canUpdateMilestones.value) {
ElMessage.warning("权限不足");
return;
}
if (!validateDateRange(editorForm.adjustedStartDate, editorForm.adjustedEndDate, "调整计划时间")) return;
if (!validateDateRange(editorForm.actualStartDate, editorForm.actualEndDate, "实际时间")) return;
const patch = {