UX-2(操作级权限控制)
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
<div class="header">
|
||||
<span>分类</span>
|
||||
<div v-if="canMaintain">
|
||||
<el-button type="primary" size="small" @click="openForm()">新建</el-button>
|
||||
<PermissionAction action="faq.edit">
|
||||
<el-button type="primary" size="small" @click="openForm()">新建</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu :default-active="activeCategory" @select="onSelect">
|
||||
@@ -12,15 +14,16 @@
|
||||
<span>{{ c.name }}</span>
|
||||
<el-tag v-if="c.study_id" size="small" type="success" class="ml-4">项目</el-tag>
|
||||
<el-tag v-else size="small" class="ml-4">全局</el-tag>
|
||||
<el-button
|
||||
v-if="canEdit(c)"
|
||||
type="text"
|
||||
size="small"
|
||||
style="margin-left: auto"
|
||||
@click.stop="openForm(c)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<PermissionAction v-if="canEdit(c)" action="faq.edit">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
style="margin-left: auto"
|
||||
@click.stop="openForm(c)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<FaqCategoryForm v-model="showForm" :category="editing" :is-admin="isAdmin" @success="emit('refresh')" />
|
||||
@@ -33,6 +36,8 @@ import FaqCategoryForm from "./FaqCategoryForm.vue";
|
||||
import type { FaqCategory } from "../api/faqs";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
|
||||
const props = defineProps<{
|
||||
categories: FaqCategory[];
|
||||
@@ -46,8 +51,9 @@ const study = useStudyStore();
|
||||
const showForm = ref(false);
|
||||
const editing = ref<FaqCategory | null>(null);
|
||||
|
||||
const { can } = usePermission();
|
||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||
const canMaintain = computed(() => isAdmin.value || auth.user?.role === "PM");
|
||||
const canMaintain = computed(() => can("faq.edit"));
|
||||
|
||||
const sortedCategories = computed(() =>
|
||||
[...props.categories].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
|
||||
@@ -56,8 +62,9 @@ const sortedCategories = computed(() =>
|
||||
const activeCategory = computed(() => props.modelValue || "");
|
||||
|
||||
const canEdit = (c: FaqCategory) => {
|
||||
if (!canMaintain.value) return false;
|
||||
if (isAdmin.value) return true;
|
||||
return auth.user?.role === "PM" && c.study_id === study.currentStudy?.id;
|
||||
return c.study_id === study.currentStudy?.id;
|
||||
};
|
||||
|
||||
const onSelect = (id: string) => {
|
||||
|
||||
@@ -16,16 +16,19 @@
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button type="text" size="small" @click="view(scope.row)">查看</el-button>
|
||||
<el-button v-if="canEdit" type="text" size="small" @click="edit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="canEdit"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggle(scope.row)"
|
||||
:style="{ color: scope.row.is_active ? '#f56c6c' : '#67c23a' }"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<PermissionAction v-if="canEdit" action="faq.edit">
|
||||
<el-button type="text" size="small" @click="edit(scope.row)">编辑</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction v-if="canEdit" action="faq.edit">
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="toggle(scope.row)"
|
||||
:style="{ color: scope.row.is_active ? '#f56c6c' : '#67c23a' }"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -37,6 +40,7 @@ import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { updateFaqItem } from "../api/faqs";
|
||||
import type { FaqItem, FaqCategory } from "../api/faqs";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
items: FaqItem[];
|
||||
|
||||
@@ -1,63 +1,65 @@
|
||||
<template>
|
||||
<div class="actions">
|
||||
<el-button
|
||||
v-if="item.status === 'DRAFT' && canSubmit"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('SUBMITTED')"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.status === 'SUBMITTED' && canApprove"
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('APPROVED')"
|
||||
>
|
||||
审批通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.status === 'SUBMITTED' && canApprove"
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="reject"
|
||||
>
|
||||
驳回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.status === 'APPROVED' && canPay"
|
||||
type="warning"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('PAID')"
|
||||
>
|
||||
确认支付
|
||||
</el-button>
|
||||
<PermissionAction action="finance.create">
|
||||
<el-button
|
||||
v-if="item.status === 'DRAFT'"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('SUBMITTED')"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction action="finance.approve">
|
||||
<el-button
|
||||
v-if="item.status === 'SUBMITTED'"
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('APPROVED')"
|
||||
>
|
||||
审批通过
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction action="finance.approve">
|
||||
<el-button
|
||||
v-if="item.status === 'SUBMITTED'"
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="reject"
|
||||
>
|
||||
驳回
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
<PermissionAction action="finance.pay">
|
||||
<el-button
|
||||
v-if="item.status === 'APPROVED'"
|
||||
type="warning"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="change('PAID')"
|
||||
>
|
||||
确认支付
|
||||
</el-button>
|
||||
</PermissionAction>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { computed, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { changeFinanceStatus } from "../api/finance";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
|
||||
const props = defineProps<{ item: any }>();
|
||||
const emit = defineEmits(["success"]);
|
||||
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const loading = ref(false);
|
||||
|
||||
const role = computed(() => auth.user?.role);
|
||||
const canSubmit = computed(() => role.value === "ADMIN" || role.value === "PM" || role.value === "CRA");
|
||||
const canApprove = computed(() => role.value === "ADMIN" || role.value === "PM");
|
||||
const canPay = canApprove;
|
||||
|
||||
const change = async (status: string, reject_reason?: string) => {
|
||||
if (!study.currentStudy) return;
|
||||
loading.value = true;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<el-tooltip v-if="!allowed && tooltip" :content="reason" placement="top">
|
||||
<span class="perm-wrapper perm-disabled">
|
||||
<slot />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else-if="!allowed" class="perm-wrapper perm-disabled">
|
||||
<slot />
|
||||
</span>
|
||||
<span v-else class="perm-wrapper">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { usePermission } from "../utils/permission";
|
||||
|
||||
const props = defineProps<{
|
||||
action: string;
|
||||
tooltip?: boolean;
|
||||
}>();
|
||||
|
||||
const { can, reason: reasonFn } = usePermission();
|
||||
const allowed = computed(() => can(props.action));
|
||||
const reason = computed(() => reasonFn(props.action));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.perm-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
.perm-disabled {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,20 @@ import { ref } from "vue";
|
||||
import type { Study } from "../types/api";
|
||||
|
||||
const STUDY_KEY = "ctms_current_study";
|
||||
const STUDY_ROLE_KEY = "ctms_current_study_role";
|
||||
|
||||
export const useStudyStore = defineStore("study", () => {
|
||||
const currentStudy = ref<Study | null>(null);
|
||||
const currentStudyRole = ref<string | null>(null);
|
||||
|
||||
const setCurrentStudy = (study: Study) => {
|
||||
currentStudy.value = study;
|
||||
const role = (study as any).role_in_study || (study as any).role || null;
|
||||
currentStudyRole.value = role;
|
||||
localStorage.setItem(STUDY_KEY, JSON.stringify(study));
|
||||
if (role) {
|
||||
localStorage.setItem(STUDY_ROLE_KEY, role);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCurrentStudy = () => {
|
||||
@@ -21,16 +28,31 @@ export const useStudyStore = defineStore("study", () => {
|
||||
currentStudy.value = null;
|
||||
}
|
||||
}
|
||||
const savedRole = localStorage.getItem(STUDY_ROLE_KEY);
|
||||
currentStudyRole.value = savedRole || null;
|
||||
};
|
||||
|
||||
const clearCurrentStudy = () => {
|
||||
currentStudy.value = null;
|
||||
currentStudyRole.value = null;
|
||||
localStorage.removeItem(STUDY_KEY);
|
||||
localStorage.removeItem(STUDY_ROLE_KEY);
|
||||
};
|
||||
|
||||
const setCurrentStudyRole = (role: string | null) => {
|
||||
currentStudyRole.value = role;
|
||||
if (role) {
|
||||
localStorage.setItem(STUDY_ROLE_KEY, role);
|
||||
} else {
|
||||
localStorage.removeItem(STUDY_ROLE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentStudy,
|
||||
currentStudyRole,
|
||||
setCurrentStudy,
|
||||
setCurrentStudyRole,
|
||||
loadCurrentStudy,
|
||||
clearCurrentStudy,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
// 预留权限工具,可在后续基于 role 与项目内角色实现细粒度控制
|
||||
export const hasPermission = (requiredRoles: string[], userRole?: string): boolean => {
|
||||
if (!requiredRoles.length) return true;
|
||||
if (!userRole) return false;
|
||||
return requiredRoles.includes(userRole);
|
||||
import { computed } from "vue";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
|
||||
const PERMISSIONS: Record<string, string[]> = {
|
||||
"task.create": ["ADMIN", "PM"],
|
||||
"task.edit": ["ADMIN", "PM"],
|
||||
"milestone.create": ["ADMIN", "PM"],
|
||||
"subject.create": ["ADMIN", "PM", "CRA"],
|
||||
"subject.enroll": ["ADMIN", "PM", "CRA"],
|
||||
"subject.complete": ["ADMIN", "PM", "CRA"],
|
||||
"subject.drop": ["ADMIN", "PM", "CRA"],
|
||||
"ae.create": ["ADMIN", "PM", "CRA", "PV"],
|
||||
"ae.close": ["ADMIN", "PM", "PV", "CRA"],
|
||||
"issue.create": ["ADMIN", "PM", "PV"],
|
||||
"issue.close": ["ADMIN", "PM", "PV"],
|
||||
"finance.create": ["ADMIN", "PM", "CRA"],
|
||||
"finance.approve": ["ADMIN", "PM"],
|
||||
"finance.pay": ["ADMIN", "PM"],
|
||||
"imp.transaction": ["ADMIN", "PM", "IMP"],
|
||||
"faq.edit": ["ADMIN", "PM"],
|
||||
};
|
||||
|
||||
const REASONS: Record<string, string> = {
|
||||
"task.create": "仅项目负责人可以创建任务",
|
||||
"task.edit": "仅项目负责人可以编辑任务",
|
||||
"milestone.create": "仅项目负责人可以维护里程碑",
|
||||
"subject.enroll": "仅 PM/CRA 可更新受试者状态",
|
||||
"subject.complete": "仅 PM/CRA 可更新受试者状态",
|
||||
"subject.drop": "仅 PM/CRA 可更新受试者状态",
|
||||
"ae.close": "仅 PM/PV/ADMIN 可关闭 AE",
|
||||
"finance.approve": "审批需要 PM/ADMIN 权限",
|
||||
"finance.pay": "支付确认需要 PM/ADMIN 权限",
|
||||
"imp.transaction": "仅 IMP/PM/ADMIN 可操作台账",
|
||||
"faq.edit": "仅 PM/ADMIN 可维护 FAQ",
|
||||
};
|
||||
|
||||
export const usePermission = () => {
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
|
||||
const userRole = computed(() => auth.user?.role || null);
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || null);
|
||||
|
||||
const can = (action: string): boolean => {
|
||||
if (userRole.value === "ADMIN") return true;
|
||||
const allowList = PERMISSIONS[action];
|
||||
if (!allowList) return false;
|
||||
if (!projectRole.value) return false;
|
||||
return allowList.includes(projectRole.value);
|
||||
};
|
||||
|
||||
const reason = (action: string): string => {
|
||||
if (userRole.value === "ADMIN") return "";
|
||||
return REASONS[action] || "当前角色无权执行该操作";
|
||||
};
|
||||
|
||||
return {
|
||||
can,
|
||||
reason,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user