优化文档管理与导航体验
1、为文档增加编辑接口和前端编辑抽屉,文档列表与详情页统一按创建、更新、删除权限展示操作。 2、将文档版本上传和分发改为抽屉交互,并在详情页补充版本删除权限检查。 3、增强全局面包屑上下文和详情页标题同步,避免详情页重复或缺失导航信息。 4、按项目权限过滤快捷入口、首页费用指标和 eTMF 新建入口,并补充对应前端测试。
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</div>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" :disabled="!canCreate" @click="openCreate" class="header-action-btn">
|
||||
<el-button v-if="canCreate" type="primary" @click="openCreate" class="header-action-btn">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.newDocument }}
|
||||
</el-button>
|
||||
@@ -73,8 +73,11 @@
|
||||
<span class="text-secondary">{{ formatDate(row.updated_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="120" align="center">
|
||||
<el-table-column v-if="canUpdate || canDelete" :label="TEXT.modules.fileVersionManagement.columns.actions" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="canUpdate" link type="primary" :disabled="isInactiveSite(row.site_id)" @click.stop="openEdit(row)">
|
||||
{{ TEXT.common.actions.edit }}
|
||||
</el-button>
|
||||
<el-button v-if="canDelete" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
@@ -87,35 +90,70 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog append-to=".layout-main .content-wrapper" v-model="createVisible" :title="TEXT.modules.fileVersionManagement.dialog.createTitle" width="520px" destroy-on-close class="ctms-dialog">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px" class="ctms-form">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docNo" prop="doc_no">
|
||||
<el-input v-model="createForm.doc_no" placeholder="e.g. SOP-001" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.title" prop="title">
|
||||
<el-input v-model="createForm.title" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.scope" prop="scope_type">
|
||||
<el-select v-model="createForm.scope_type" style="width: 100%">
|
||||
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="createForm.scope_type === 'SITE'" :label="TEXT.modules.fileVersionManagement.fields.site" prop="site_id">
|
||||
<el-select v-model="createForm.site_id" filterable style="width: 100%">
|
||||
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docType" prop="doc_type">
|
||||
<el-select v-model="createForm.doc_type" style="width: 100%">
|
||||
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-drawer
|
||||
v-if="editorVisible"
|
||||
v-model="editorVisible"
|
||||
direction="rtl"
|
||||
size="620px"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
class="document-editor-drawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="editor-header">
|
||||
<div class="editor-title">{{ editingDocumentId ? TEXT.common.actions.edit : TEXT.modules.fileVersionManagement.dialog.createTitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form ref="editorFormRef" :model="editorForm" :rules="editorRules" label-position="top" class="document-form">
|
||||
<div class="form-group">
|
||||
<div class="form-group-title">
|
||||
<span class="group-dot group-dot-basic"></span>
|
||||
{{ TEXT.common.labels.basicInfo }}
|
||||
</div>
|
||||
<el-row :gutter="16">
|
||||
<el-col v-if="!editingDocumentId" :span="12">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docNo" prop="doc_no">
|
||||
<el-input v-model="editorForm.doc_no" placeholder="e.g. SOP-001" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="editingDocumentId ? 24 : 12">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.title" prop="title">
|
||||
<el-input v-model="editorForm.title" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.scope" prop="scope_type">
|
||||
<el-select v-model="editorForm.scope_type" class="full-width">
|
||||
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.docType" prop="doc_type">
|
||||
<el-select v-model="editorForm.doc_type" class="full-width">
|
||||
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item v-if="editorForm.scope_type === 'SITE'" :label="TEXT.modules.fileVersionManagement.fields.site" prop="site_id">
|
||||
<el-select v-model="editorForm.site_id" filterable class="full-width">
|
||||
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="submitCreate">{{ TEXT.common.actions.confirm }}</el-button>
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="editorVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitEditor">{{ TEXT.common.actions.confirm }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -124,7 +162,7 @@ import { onMounted, reactive, ref, computed, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { fetchDocuments, createDocument, deleteDocument } from "../../api/documents";
|
||||
import { fetchDocuments, createDocument, deleteDocument, updateDocument } from "../../api/documents";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
@@ -142,9 +180,10 @@ const { can } = usePermission();
|
||||
const loading = ref(false);
|
||||
const items = ref<DocumentSummary[]>([]);
|
||||
const sites = ref<Site[]>([]);
|
||||
const createVisible = ref(false);
|
||||
const creating = ref(false);
|
||||
const createFormRef = ref<FormInstance>();
|
||||
const editorVisible = ref(false);
|
||||
const saving = ref(false);
|
||||
const editingDocumentId = ref("");
|
||||
const editorFormRef = ref<FormInstance>();
|
||||
|
||||
const trialId = computed(() => (route.params.trialId as string) || "");
|
||||
|
||||
@@ -176,6 +215,7 @@ const isAdmin = computed(() => {
|
||||
return !!auth.user?.is_admin;
|
||||
});
|
||||
const canCreate = computed(() => can("documents.create"));
|
||||
const canUpdate = computed(() => can("documents.update"));
|
||||
const canDelete = computed(() => can("documents.delete"));
|
||||
const isInactiveSite = (siteId?: string | null) => !!siteId && siteActiveMap.value[siteId] === false;
|
||||
const documentRowClass = ({ row }: { row: DocumentSummary }) =>
|
||||
@@ -192,7 +232,7 @@ const sortedItems = computed(() =>
|
||||
[...filteredItems.value].sort((a, b) => Number(isInactiveSite(a?.site_id)) - Number(isInactiveSite(b?.site_id)))
|
||||
);
|
||||
|
||||
const createForm = reactive({
|
||||
const editorForm = reactive({
|
||||
doc_no: "",
|
||||
title: "",
|
||||
scope_type: "GLOBAL",
|
||||
@@ -200,14 +240,14 @@ const createForm = reactive({
|
||||
doc_type: "",
|
||||
});
|
||||
|
||||
const createRules: FormRules = {
|
||||
const editorRules: FormRules = {
|
||||
doc_no: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
|
||||
title: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
|
||||
scope_type: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
|
||||
site_id: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (createForm.scope_type === "SITE" && !value) {
|
||||
if (editorForm.scope_type === "SITE" && !value) {
|
||||
callback(new Error(TEXT.common.messages.required));
|
||||
return;
|
||||
}
|
||||
@@ -262,49 +302,88 @@ const resetFilters = () => {
|
||||
filters.doc_type = "";
|
||||
};
|
||||
|
||||
const resetEditorForm = () => {
|
||||
editorForm.doc_no = "";
|
||||
editorForm.title = "";
|
||||
editorForm.scope_type = "GLOBAL";
|
||||
editorForm.site_id = "";
|
||||
editorForm.doc_type = "";
|
||||
editorFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
if (!canCreate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
createForm.doc_no = "";
|
||||
createForm.title = "";
|
||||
createForm.scope_type = "GLOBAL";
|
||||
createForm.site_id = "";
|
||||
createForm.doc_type = "";
|
||||
createVisible.value = true;
|
||||
editingDocumentId.value = "";
|
||||
resetEditorForm();
|
||||
editorVisible.value = true;
|
||||
};
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!canCreate.value) {
|
||||
const openEdit = (row: DocumentSummary) => {
|
||||
if (!canUpdate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (!createFormRef.value) return;
|
||||
await createFormRef.value.validate(async (valid) => {
|
||||
if (isInactiveSite(row.site_id)) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
editingDocumentId.value = row.id;
|
||||
Object.assign(editorForm, {
|
||||
doc_no: "",
|
||||
title: row.title || "",
|
||||
scope_type: row.scope_type || "GLOBAL",
|
||||
site_id: row.site_id || "",
|
||||
doc_type: row.doc_type || "",
|
||||
});
|
||||
editorFormRef.value?.clearValidate();
|
||||
editorVisible.value = true;
|
||||
};
|
||||
|
||||
const submitEditor = async () => {
|
||||
if (editingDocumentId.value && !canUpdate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (!editingDocumentId.value && !canCreate.value) {
|
||||
ElMessage.warning("权限不足");
|
||||
return;
|
||||
}
|
||||
if (!editorFormRef.value) return;
|
||||
await editorFormRef.value.validate(async (valid) => {
|
||||
if (!valid || !trialId.value) return;
|
||||
creating.value = true;
|
||||
saving.value = true;
|
||||
try {
|
||||
if (createForm.scope_type === "SITE" && isInactiveSite(createForm.site_id)) {
|
||||
if (editorForm.scope_type === "SITE" && isInactiveSite(editorForm.site_id)) {
|
||||
ElMessage.warning("中心已停用");
|
||||
creating.value = false;
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
await createDocument({
|
||||
trial_id: trialId.value,
|
||||
site_id: createForm.scope_type === "SITE" ? createForm.site_id : undefined,
|
||||
doc_no: createForm.doc_no,
|
||||
title: createForm.title,
|
||||
doc_type: createForm.doc_type,
|
||||
scope_type: createForm.scope_type,
|
||||
});
|
||||
createVisible.value = false;
|
||||
const payload = {
|
||||
site_id: editorForm.scope_type === "SITE" ? editorForm.site_id : null,
|
||||
title: editorForm.title.trim(),
|
||||
doc_type: editorForm.doc_type,
|
||||
scope_type: editorForm.scope_type,
|
||||
};
|
||||
if (editingDocumentId.value) {
|
||||
await updateDocument(editingDocumentId.value, payload);
|
||||
} else {
|
||||
await createDocument({
|
||||
...payload,
|
||||
trial_id: trialId.value,
|
||||
doc_no: editorForm.doc_no.trim(),
|
||||
site_id: editorForm.scope_type === "SITE" ? editorForm.site_id : undefined,
|
||||
});
|
||||
}
|
||||
editorVisible.value = false;
|
||||
await load();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.createSuccess);
|
||||
ElMessage.success(editingDocumentId.value ? TEXT.common.messages.updateSuccess : TEXT.modules.fileVersionManagement.messages.createSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
creating.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -322,6 +401,10 @@ const confirmDelete = async (row: DocumentSummary) => {
|
||||
return;
|
||||
}
|
||||
if (!row?.id) return;
|
||||
if (isInactiveSite(row.site_id)) {
|
||||
ElMessage.warning("中心已停用");
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
@@ -392,6 +475,55 @@ watch(
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.document-form {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.group-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.group-dot-basic {
|
||||
background: var(--ctms-primary);
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user