230 lines
5.6 KiB
Vue
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>
|