release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
@@ -2,7 +2,11 @@
|
||||
<div class="api-permissions">
|
||||
<div class="api-permissions-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-title">项目级权限矩阵</span>
|
||||
<div class="toolbar-heading">
|
||||
<span class="toolbar-title">有效权限矩阵</span>
|
||||
<el-tag size="small" effect="plain" type="info">只读</el-tag>
|
||||
</div>
|
||||
<span class="toolbar-result">显示 {{ filteredPermissionCount }} / {{ totalPermissionCount }} 项权限</span>
|
||||
</div>
|
||||
<div class="toolbar-filters">
|
||||
<el-input
|
||||
@@ -29,6 +33,13 @@
|
||||
<el-option label="删除" value="delete" />
|
||||
<el-option label="导出" value="export" />
|
||||
</el-select>
|
||||
<el-select v-model="filterAuthorization" placeholder="授权状态" style="width: 140px">
|
||||
<el-option label="全部授权状态" value="" />
|
||||
<el-option label="存在角色差异" value="different" />
|
||||
<el-option label="至少一项授权" value="granted" />
|
||||
<el-option label="全部未授权" value="ungranted" />
|
||||
</el-select>
|
||||
<el-button :disabled="!hasActiveFilters" @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,10 +48,12 @@
|
||||
:data="filteredOperations"
|
||||
border
|
||||
stripe
|
||||
max-height="calc(100dvh - 330px)"
|
||||
empty-text="未找到匹配的权限"
|
||||
:span-method="spanMethod"
|
||||
class="perm-matrix-table"
|
||||
>
|
||||
<el-table-column prop="permission_category" label="权限类型" width="120">
|
||||
<el-table-column prop="permission_category" label="权限类型" width="120" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="hierarchy-cell hierarchy-cell--category" :data-category="getPermissionCategory(row)">
|
||||
<span class="hierarchy-bar" />
|
||||
@@ -49,7 +62,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="module" label="模块" width="160">
|
||||
<el-table-column prop="module" label="模块" width="160" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="hierarchy-cell hierarchy-cell--module">
|
||||
<span class="module-dot" />
|
||||
@@ -94,7 +107,7 @@
|
||||
:title="isOperationAllowed(row.operation_key, role) ? '已授权' : '未授权'"
|
||||
>
|
||||
<template v-if="isOperationAllowed(row.operation_key, role)">✓</template>
|
||||
<CircleCloseFilled v-else />
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -105,7 +118,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { CircleCloseFilled, Search } from "@element-plus/icons-vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { ApiEndpointPermissionsResponse } from "@/types/api";
|
||||
import { fetchApiOperations } from "@/api/projectPermissions";
|
||||
@@ -144,8 +157,20 @@ const { roleLabel, compareRolesByTemplateOrder, loadRoleTemplates } = useRoleTem
|
||||
const searchText = ref("");
|
||||
const filterModule = ref("");
|
||||
const filterAction = ref("");
|
||||
const filterAuthorization = ref("");
|
||||
const operations = ref<Operation[]>([]);
|
||||
|
||||
const hasActiveFilters = computed(() => Boolean(
|
||||
searchText.value || filterModule.value || filterAction.value || filterAuthorization.value
|
||||
));
|
||||
|
||||
const resetFilters = () => {
|
||||
searchText.value = "";
|
||||
filterModule.value = "";
|
||||
filterAction.value = "";
|
||||
filterAuthorization.value = "";
|
||||
};
|
||||
|
||||
const moduleLabel = projectPermissionModuleLabel;
|
||||
const getPermissionCategory = (row: Operation): "common" | "business" => projectPermissionCategory(row.module);
|
||||
const getPermissionCategoryLabel = (row: Operation): string => projectPermissionCategoryLabel(row.module);
|
||||
@@ -216,8 +241,14 @@ const filteredOperations = computed(() => {
|
||||
|
||||
const matchModule = !filterModule.value || operation.module === filterModule.value;
|
||||
const matchAction = !filterAction.value || getOperationVerb(operation) === filterAction.value;
|
||||
const roleStates = roles.value.map((role) => isOperationAllowed(operation.operation_key, role));
|
||||
const matchAuthorization =
|
||||
!filterAuthorization.value ||
|
||||
(filterAuthorization.value === "different" && roleStates.some(Boolean) && roleStates.some((allowed) => !allowed)) ||
|
||||
(filterAuthorization.value === "granted" && roleStates.some(Boolean)) ||
|
||||
(filterAuthorization.value === "ungranted" && roleStates.every((allowed) => !allowed));
|
||||
|
||||
return matchSearch && matchModule && matchAction;
|
||||
return matchSearch && matchModule && matchAction && matchAuthorization;
|
||||
});
|
||||
|
||||
// 按权限类型、模块分组排序
|
||||
@@ -235,6 +266,12 @@ const filteredOperations = computed(() => {
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const countUniquePermissions = (rows: DisplayOperation[]): number =>
|
||||
new Set(rows.map((operation) => operation.operation_key)).size;
|
||||
|
||||
const totalPermissionCount = computed(() => countUniquePermissions(displayOperations.value));
|
||||
const filteredPermissionCount = computed(() => countUniquePermissions(filteredOperations.value));
|
||||
|
||||
// 权限类型、模块与模块细分列合并行
|
||||
const spanMethod = ({ row, rowIndex, columnIndex }: { row: DisplayOperation; rowIndex: number; column: any; columnIndex: number }) => {
|
||||
if (columnIndex === 0) {
|
||||
@@ -353,12 +390,13 @@ const SECTION_LABELS: Record<string, string> = {
|
||||
faq_category: "医学咨询/FAQ",
|
||||
faq_reply: "医学咨询/FAQ",
|
||||
faq_attachments: "医学咨询/FAQ",
|
||||
collaboration: "在线协作",
|
||||
};
|
||||
|
||||
const SECTION_ORDER_BY_MODULE: Record<string, string[]> = {
|
||||
subjects: ["参与者基础信息", "访视", "AE", "PD", "病史"],
|
||||
risk_issues: ["AE/SAE", "PD", "监查访视问题"],
|
||||
shared_library: ["医学咨询/FAQ", "注意事项"],
|
||||
shared_library: ["医学咨询/FAQ", "注意事项", "在线协作"],
|
||||
};
|
||||
|
||||
const getOperationSection = (row: Operation): string => {
|
||||
@@ -394,19 +432,19 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
|
||||
<style scoped>
|
||||
.api-permissions {
|
||||
padding: 20px 0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.api-permissions-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 18px;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
@@ -414,23 +452,35 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1a2332;
|
||||
}
|
||||
|
||||
.toolbar-result {
|
||||
color: #7a8a9a;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.toolbar-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.api-permissions-table-wrapper {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
@@ -442,7 +492,7 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
.hierarchy-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -458,7 +508,7 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 36px;
|
||||
height: 24px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
background: linear-gradient(180deg, #94a3b8 0%, #cbd5e1 100%);
|
||||
}
|
||||
@@ -472,7 +522,7 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
}
|
||||
|
||||
.category-label {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: 0.3px;
|
||||
@@ -481,15 +531,15 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
/* 模块:实心圆点 + 中等加粗字 */
|
||||
.module-dot {
|
||||
flex-shrink: 0;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.module-label {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
@@ -498,11 +548,11 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
.section-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 12px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
@@ -511,17 +561,17 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
.operation-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-tag {
|
||||
flex-shrink: 0;
|
||||
min-width: 40px;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.operation-name {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
@@ -529,10 +579,10 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -544,11 +594,46 @@ defineExpose({ searchText, getOperationSection, getPermissionCategory, getPermis
|
||||
}
|
||||
|
||||
.permission-state--disabled {
|
||||
color: #94a3b8;
|
||||
color: #c3ccd5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.permission-state--disabled :deep(svg) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
:deep(.api-permissions .el-input__wrapper),
|
||||
:deep(.api-permissions .el-select__wrapper) {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
:deep(.api-permissions .el-button) {
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
:deep(.perm-matrix-table .el-table__header-wrapper th.el-table__cell) {
|
||||
padding: 6px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:deep(.perm-matrix-table .el-table__body-wrapper td.el-table__cell) {
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.api-permissions-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar-filters {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.toolbar-filters > * {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user