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
+8
View File
@@ -9,6 +9,14 @@
> 其余历史示例账号已被停用,仅保留以上 5 个。若需新增,可用管理员在 `/api/v1/users` 创建,并在项目成员中赋予角色。
## 角色权限概要(前端操作级提示,后端仍最终裁决)
- ADMIN:全权限
- PM:里程碑/任务维护、受试者状态、AE 创建/关闭、Issue 创建/关闭、Finance 创建/审批/支付、IMP 交易、FAQ 维护
- CRA:任务/受试者/AE/Data Query 创建,受试者状态更新,Finance 创建/提交,其他只读
- PVAE 创建/关闭,Issue 创建/关闭,其余只读
- IMP:IMP 交易/产品/批次维护,其余只读
- 普通成员(无项目角色):仅浏览
## 访问方式
- 前端:`http://localhost`Nginx 反代到 Vite 开发容器)
- 后端 API:同域 `/api/v1/*`(已在前端代理)
+19 -12
View File
@@ -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) => {
+14 -10
View File
@@ -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>
+22
View File
@@ -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,
};
+61 -5
View File
@@ -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,
};
};
+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;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.