Files
ctms/frontend/src/views/admin/ApiPermissions.vue
T
2026-06-11 11:06:59 +08:00

230 lines
5.6 KiB
Vue

<template>
<div class="permission-page">
<div class="permission-shell unified-shell" v-loading="loading">
<!-- 顶部操作栏 -->
<div class="permission-header unified-action-bar">
<div class="permission-title">
<el-icon><Key /></el-icon>
<h2>权限管理</h2>
</div>
<div class="permission-project-meta">
<span class="meta-item">
<span class="meta-label">项目编号</span>
<strong>{{ project?.code || "-" }}</strong>
</span>
<span class="meta-separator" />
<span class="meta-item">
<span class="meta-label">项目名称</span>
<strong>{{ project?.name || "-" }}</strong>
</span>
</div>
<div class="permission-actions">
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon>
保存
</el-button>
</div>
</div>
<!-- 标签页 -->
<el-tabs v-model="activeTab">
<!-- 接口级权限 -->
<el-tab-pane label="角色权限" name="api">
<PermissionTemplateSelector
:study-id="studyId"
:current-permissions="currentPermissionsForTemplate"
@applied="onTemplateApplied"
/>
<el-divider />
<ApiEndpointPermissions
:project="project"
:matrix="apiMatrix"
@update="onApiMatrixUpdate"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { Key, Check } from "@element-plus/icons-vue";
import type {
ApiEndpointPermissionsResponse,
} from "@/types/api";
import {
fetchApiEndpointPermissions,
updateApiEndpointPermissions,
} from "@/api/projectPermissions";
import { useStudyStore } from "@/store/study";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";
const route = useRoute();
const studyStore = useStudyStore();
const activeTab = ref<"api">("api");
const loading = ref(false);
const saving = ref(false);
const project = computed(() => studyStore.currentStudy);
const studyId = computed(() => {
const id = route.params.id;
return typeof id === "string" ? id : (id as string[])[0];
});
// 权限数据
const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null);
// 脏值检测
const dirty = ref(false);
const loadPermissionData = async () => {
if (!studyId.value) return;
loading.value = true;
try {
const apiRes = await fetchApiEndpointPermissions(studyId.value);
apiMatrix.value = apiRes.data;
dirty.value = false;
} catch (error) {
ElMessage.error("加载权限数据失败");
console.error(error);
} finally {
loading.value = false;
}
};
const onApiMatrixUpdate = (newMatrix: ApiEndpointPermissionsResponse) => {
apiMatrix.value = newMatrix;
dirty.value = true;
};
// 将当前 apiMatrix 转换为模板所需的 {role: {endpoint_key: bool}} 格式
const currentPermissionsForTemplate = computed(() => {
if (!apiMatrix.value) return undefined;
const result: Record<string, Record<string, boolean>> = {};
for (const [role, endpoints] of Object.entries(apiMatrix.value)) {
result[role] = {};
for (const [key, val] of Object.entries(endpoints)) {
result[role][key] = typeof val === "boolean" ? val : val.allowed;
}
}
return result;
});
const onTemplateApplied = async (permissions: Record<string, Record<string, { allowed: boolean }>>) => {
// 模板应用后刷新权限矩阵
await loadPermissionData();
ElMessage.success("权限已更新");
};
const flattenMatrix = (matrix: ApiEndpointPermissionsResponse): Record<string, Record<string, boolean>> => {
const result: Record<string, Record<string, boolean>> = {};
for (const [role, endpoints] of Object.entries(matrix)) {
result[role] = {};
for (const [key, val] of Object.entries(endpoints)) {
result[role][key] = typeof val === "boolean" ? val : val.allowed;
}
}
return result;
};
const save = async () => {
if (!studyId.value || !dirty.value) return;
saving.value = true;
try {
if (apiMatrix.value) {
const res = await updateApiEndpointPermissions(studyId.value, flattenMatrix(apiMatrix.value));
apiMatrix.value = res.data;
}
dirty.value = false;
ElMessage.success("权限已保存");
await loadPermissionData();
} catch (error) {
ElMessage.error("保存权限失败");
console.error(error);
} finally {
saving.value = false;
}
};
onMounted(() => {
loadPermissionData();
});
</script>
<style scoped lang="scss">
.permission-page {
padding: 20px;
}
.permission-shell {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.permission-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #ebeef5;
gap: 20px;
}
.permission-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
h2 {
margin: 0;
font-size: 18px;
}
}
.permission-project-meta {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
font-size: 14px;
color: #606266;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
}
.meta-label {
color: #909399;
}
.meta-separator {
width: 1px;
height: 20px;
background: #dcdfe6;
}
.permission-actions {
display: flex;
gap: 10px;
}
:deep(.el-tabs) {
padding: 20px;
}
</style>