UX-2(操作级权限控制)

This commit is contained in:
Cheng Zhou
2025-12-17 15:11:02 +08:00
parent dd51f56707
commit 9ca30d12f8
29 changed files with 288 additions and 131 deletions
+9 -7
View File
@@ -6,9 +6,11 @@
<h3>AE 详情</h3>
<p>{{ ae.term }}</p>
</div>
<el-button v-if="canClose" :type="ae.status === 'CLOSED' ? 'primary' : 'danger'" size="small" @click="toggleClose">
{{ ae.status === "CLOSED" ? "重新打开" : "关闭" }}
</el-button>
<PermissionAction action="ae.close">
<el-button :type="ae.status === 'CLOSED' ? 'primary' : 'danger'" size="small" @click="toggleClose">
{{ ae.status === "CLOSED" ? "重新打开" : "关闭" }}
</el-button>
</PermissionAction>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="受试者ID">{{ ae.subject_id }}</el-descriptions-item>
@@ -43,6 +45,8 @@ import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import CommentList from "../components/CommentList.vue";
import AttachmentList from "../components/AttachmentList.vue";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
const route = useRoute();
const study = useStudyStore();
@@ -51,10 +55,8 @@ const auth = useAuthStore();
const ae = ref<any | null>(null);
const studyId = computed(() => study.currentStudy?.id || "");
const canClose = computed(() => {
// CRA 也显示按钮,后端会校验权限
return true;
});
const { can } = usePermission();
const canClose = computed(() => can("ae.close"));
const enrichOverdue = (item: any) => {
if (!item) return item;
+7 -5
View File
@@ -25,7 +25,9 @@
</el-select>
<el-switch v-model="onlyActive" active-text="仅启用" @change="loadFaqs" />
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="openForm()">新建 FAQ</el-button>
<PermissionAction action="faq.edit">
<el-button type="primary" @click="openForm()">新建 FAQ</el-button>
</PermissionAction>
</div>
</el-card>
@@ -61,6 +63,8 @@ import FaqList from "../components/FaqList.vue";
import FaqItemForm from "../components/FaqItemForm.vue";
import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
const auth = useAuthStore();
const study = useStudyStore();
@@ -78,10 +82,8 @@ const activeCategory = ref("");
const showForm = ref(false);
const editing = ref<any | null>(null);
const canEdit = computed(() => {
const role = auth.user?.role;
return role === "ADMIN" || role === "PM";
});
const { can } = usePermission();
const canEdit = computed(() => can("faq.edit"));
const loadCategories = async () => {
try {
+7 -5
View File
@@ -25,7 +25,9 @@
@change="loadList"
/>
<div class="spacer" />
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建费用</el-button>
<PermissionAction action="finance.create">
<el-button type="primary" @click="showForm = true">新建费用</el-button>
</PermissionAction>
</div>
</el-card>
@@ -71,6 +73,8 @@ import FinanceForm from "../components/FinanceForm.vue";
import { fetchFinanceItems, fetchFinanceSummary } from "../api/finance";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
const study = useStudyStore();
const auth = useAuthStore();
@@ -95,10 +99,8 @@ const categories = ["SITE_FEE", "SUBJECT_STIPEND", "TRAVEL", "OTHER"];
const showForm = ref(false);
const canCreate = computed(() => {
const role = auth.user?.role;
return role === "ADMIN" || role === "PM" || role === "CRA";
});
const { can } = usePermission();
const canCreate = computed(() => can("finance.create"));
const formatAmount = (num?: number | string) => {
const n = Number(num);
+7 -5
View File
@@ -7,7 +7,9 @@
<el-tag :type="statusTag(item?.status)">{{ item?.status }}</el-tag>
</div>
<div class="header-actions" v-if="item">
<el-button v-if="canEditDraft" size="small" @click="showEdit = true">编辑</el-button>
<PermissionAction action="finance.create">
<el-button v-if="canEditDraft" size="small" @click="showEdit = true">编辑</el-button>
</PermissionAction>
<FinanceStatusActions :item="item" @success="loadItem" />
</div>
</div>
@@ -36,9 +38,11 @@ import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import FinanceStatusActions from "../components/FinanceStatusActions.vue";
import FinanceForm from "../components/FinanceForm.vue";
import PermissionAction from "../components/PermissionAction.vue";
import { fetchFinanceItem } from "../api/finance";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import { usePermission } from "../utils/permission";
const study = useStudyStore();
const auth = useAuthStore();
@@ -47,10 +51,8 @@ const route = useRoute();
const item = ref<any>(null);
const loading = ref(false);
const showEdit = ref(false);
const canEditDraft = computed(() => {
const role = auth.user?.role;
return item.value?.status === "DRAFT" && (role === "ADMIN" || role === "PM" || role === "CRA");
});
const { can } = usePermission();
const canEditDraft = computed(() => item.value?.status === "DRAFT" && can("finance.create"));
const loadItem = async () => {
if (!study.currentStudy) return;
+7 -5
View File
@@ -22,7 +22,9 @@
@change="load"
/>
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="showForm = true">新增交易</el-button>
<PermissionAction action="imp.transaction">
<el-button type="primary" @click="showForm = true">新增交易</el-button>
</PermissionAction>
</div>
</el-card>
<el-card>
@@ -55,6 +57,8 @@ import { fetchImpTransactions } from "../api/impTransactions";
import { fetchImpBatches } from "../api/impBatches";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import ImpTransactionForm from "../components/ImpTransactionForm.vue";
const study = useStudyStore();
@@ -70,10 +74,8 @@ const loading = ref(false);
const showForm = ref(false);
const batches = ref<any[]>([]);
const canEdit = computed(() => {
const role = auth.user?.role;
return role === "ADMIN" || role === "PM" || role === "IMP";
});
const { can } = usePermission();
const canEdit = computed(() => can("imp.transaction"));
const loadBatches = async () => {
if (!study.currentStudy) return;
+7 -7
View File
@@ -6,7 +6,9 @@
<h3>Issue 详情</h3>
<p>{{ issue.title }}</p>
</div>
<el-button v-if="canEdit" type="danger" size="small" @click="closeIssue">关闭</el-button>
<PermissionAction action="issue.close">
<el-button type="danger" size="small" @click="closeIssue">关闭</el-button>
</PermissionAction>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="类别">{{ issue.category }}</el-descriptions-item>
@@ -38,20 +40,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { fetchIssues, updateIssue } from "../api/issues";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import CommentList from "../components/CommentList.vue";
import AttachmentList from "../components/AttachmentList.vue";
const route = useRoute();
const study = useStudyStore();
const auth = useAuthStore();
const issue = ref<any | null>(null);
const studyId = computed(() => study.currentStudy?.id || "");
const canEdit = computed(() => {
const role = auth.user?.role;
return role === "ADMIN" || role === "PM" || role === "PV";
});
const { can } = usePermission();
const canEdit = computed(() => can("issue.close"));
const load = async () => {
if (!study.currentStudy) return;
+11 -8
View File
@@ -3,7 +3,9 @@
<el-card class="mb-12">
<div class="actions">
<h3>里程碑</h3>
<el-button type="primary" v-if="canEdit" @click="openCreate">新增里程碑</el-button>
<PermissionAction action="milestone.create">
<el-button type="primary" @click="openCreate">新增里程碑</el-button>
</PermissionAction>
</div>
</el-card>
@@ -15,9 +17,11 @@
<el-table-column prop="actual_date" label="实际日期" width="140" />
<el-table-column prop="status" label="状态" width="140" />
<el-table-column prop="owner_id" label="负责人" width="160" />
<el-table-column label="操作" width="120" v-if="canEdit">
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button type="primary" link size="small" @click="openEdit(scope.row)">编辑</el-button>
<PermissionAction action="milestone.create">
<el-button type="primary" link size="small" @click="openEdit(scope.row)">编辑</el-button>
</PermissionAction>
</template>
</el-table-column>
</el-table>
@@ -28,23 +32,22 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { fetchMilestones } from "../api/milestones";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import MilestoneForm from "../components/MilestoneForm.vue";
const study = useStudyStore();
const auth = useAuthStore();
const { can } = usePermission();
const milestones = ref<any[]>([]);
const loading = ref(false);
const showForm = ref(false);
const editingMilestone = ref<any | null>(null);
const canEdit = computed(() => auth.user?.role === "ADMIN" || auth.user?.role === "PM");
const loadMilestones = async () => {
if (!study.currentStudy) return;
loading.value = true;
+15 -9
View File
@@ -4,14 +4,20 @@
<h3>受试者 {{ subject.subject_no }}</h3>
<p>中心{{ subject.site_id }}</p>
<p>状态{{ subject.status }}</p>
<div v-if="canEdit" class="actions">
<el-button size="small" type="primary" @click="setStatus('ENROLLED')">标记入组</el-button>
<el-button size="small" type="success" @click="setStatus('COMPLETED')">标记完成</el-button>
<el-button size="small" type="danger" @click="setStatus('DROPPED')">标记脱落</el-button>
<div class="actions">
<PermissionAction action="subject.enroll">
<el-button size="small" type="primary" @click="setStatus('ENROLLED')">标记入组</el-button>
</PermissionAction>
<PermissionAction action="subject.complete">
<el-button size="small" type="success" @click="setStatus('COMPLETED')">标记完成</el-button>
</PermissionAction>
<PermissionAction action="subject.drop">
<el-button size="small" type="danger" @click="setStatus('DROPPED')">标记脱落</el-button>
</PermissionAction>
</div>
</el-card>
<VisitTable :visits="visits" :subject-id="subject.id" :can-edit="canEdit" :on-refresh="loadVisits" />
<VisitTable :visits="visits" :subject-id="subject.id" :can-edit="canVisitEdit" :on-refresh="loadVisits" />
</div>
<div v-else class="page">
<el-skeleton rows="3" animated />
@@ -26,6 +32,8 @@ import { fetchSubjects, updateSubject } from "../api/subjects";
import { fetchVisits } from "../api/visits";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import VisitTable from "../components/VisitTable.vue";
const route = useRoute();
@@ -35,10 +43,8 @@ const auth = useAuthStore();
const subject = ref<any | null>(null);
const visits = ref<any[]>([]);
const canEdit = computed(() => {
const role = auth.user?.role;
return role === "ADMIN" || role === "PM" || role === "CRA";
});
const { can } = usePermission();
const canVisitEdit = computed(() => can("subject.enroll"));
const loadSubject = async () => {
if (!study.currentStudy) return;
+11 -9
View File
@@ -10,7 +10,9 @@
</el-select>
<el-input v-model="filters.assignee_id" placeholder="负责人ID" style="width: 180px" @change="loadTasks" />
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="openCreate">新增任务</el-button>
<PermissionAction action="task.create">
<el-button type="primary" @click="openCreate">新增任务</el-button>
</PermissionAction>
</div>
</el-card>
@@ -22,9 +24,11 @@
<el-table-column prop="priority" label="优先级" width="100" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column prop="due_date" label="截止日期" width="140" />
<el-table-column label="操作" width="120" v-if="canEdit">
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button type="primary" link size="small" @click="openEdit(scope.row)">编辑</el-button>
<PermissionAction action="task.edit">
<el-button type="primary" link size="small" @click="openEdit(scope.row)">编辑</el-button>
</PermissionAction>
</template>
</el-table-column>
</el-table>
@@ -43,12 +47,14 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { fetchTasks } from "../api/tasks";
import { fetchMilestones } from "../api/milestones";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import PermissionAction from "../components/PermissionAction.vue";
import { usePermission } from "../utils/permission";
import TaskForm from "../components/TaskForm.vue";
const study = useStudyStore();
@@ -72,11 +78,7 @@ const filters = ref({
const showForm = ref(false);
const editingTask = ref<any | null>(null);
const canEdit = computed(() => {
const role = auth.user?.role;
// 简化:ADMIN/PM 可编辑
return role === "ADMIN" || role === "PM";
});
const { can } = usePermission();
const loadMilestones = async () => {
if (!study.currentStudy) return;