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

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
+36 -12
View File
@@ -134,6 +134,8 @@ const replyFiles = ref<UploadUserFile[]>([]);
const replyAttachmentsMap = ref<Record<string, any[]>>({});
const { can } = usePermission();
const canReadCategories = computed(() => can("faq.category.read"));
const canReadMembers = computed(() => can("project.members.list"));
const canReply = computed(() => {
if (!item.value) return false;
@@ -150,29 +152,24 @@ const canUploadReplyAttachment = computed(() => {
const canDeleteReply = (reply: any) => {
if (reply.is_deleted) return false;
if (auth.user?.is_admin) return true;
if (reply.created_by === auth.user?.id) return true;
return item.value?.study_id ? can("faq.edit") : false;
if (!item.value?.study_id) return false;
return can("faq.reply.delete");
};
const canSelectBest = computed(() => {
if (!item.value) return false;
return can("faq.reply");
return can("faq.update");
});
const canEditQuestion = computed(() => {
if (!item.value) return false;
if (auth.user?.is_admin) return true;
if (item.value.created_by === auth.user?.id) return true;
return can("faq.edit");
return can("faq.update");
});
const canConfirmResolved = computed(() => {
if (!item.value) return false;
if (item.value.status === "RESOLVED") return false;
if (auth.user?.is_admin) return true;
if (item.value.created_by === auth.user?.id) return true;
return can("faq.edit");
return can("faq.update");
});
const statusLabel = computed(() => {
@@ -218,6 +215,10 @@ const categoryName = computed(() => {
const loadMembers = async (studyId?: string | null) => {
if (!canReadMembers.value) {
members.value = [];
return;
}
if (!studyId) {
members.value = [];
return;
@@ -250,12 +251,19 @@ const loadData = async () => {
try {
const { data: faqData } = await fetchFaqItem(id);
item.value = faqData;
const questionTitle = String(faqData.question || "").trim();
study.setViewContext({
pageTitle: questionTitle ? questionTitle.slice(0, 24) : TEXT.modules.knowledgeMedicalConsult.detailTitle,
});
const categoryParams: Record<string, any> = {};
if (faqData.study_id) {
categoryParams.study_id = faqData.study_id;
}
const { data: catData } = await fetchFaqCategories(categoryParams);
categories.value = catData.items || catData || [];
categories.value = [];
if (canReadCategories.value) {
const { data: catData } = await fetchFaqCategories(categoryParams);
categories.value = catData.items || catData || [];
}
await Promise.all([loadReplies(), loadMembers(faqData.study_id)]);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
@@ -309,6 +317,10 @@ const submitReply = async () => {
const removeReply = async (reply: any) => {
if (!item.value) return;
if (!canDeleteReply(reply)) {
ElMessage.warning("权限不足");
return;
}
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
if (!ok) return;
try {
@@ -322,6 +334,10 @@ const removeReply = async (reply: any) => {
const toggleBest = async (reply: any) => {
if (!item.value) return;
if (!canSelectBest.value) {
ElMessage.warning("权限不足");
return;
}
try {
const target = item.value.best_reply_id === reply.id ? null : reply.id;
if (target) {
@@ -338,6 +354,10 @@ const toggleBest = async (reply: any) => {
const confirmResolved = async () => {
if (!item.value) return;
if (!canConfirmResolved.value) {
ElMessage.warning("权限不足");
return;
}
try {
const { data } = await setFaqStatus(item.value.id, { status: "RESOLVED" });
item.value = data;
@@ -348,6 +368,10 @@ const confirmResolved = async () => {
};
const openEdit = () => {
if (!canEditQuestion.value) {
ElMessage.warning("权限不足");
return;
}
editing.value = item.value;
showForm.value = true;
};