20260803225403
This commit is contained in:
1 parent
efd5a2bc93
commit
e17bbe9db3
40 files changed
+5516
-3910
No files matched your search
@@ -0,0 +1,219 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const props = defineProps({
|
||||
functionNodes: { type: Array, default: () => [] },
|
||||
pageNodes: { type: Array, default: () => [] },
|
||||
actions: { type: Array, default: () => [] },
|
||||
functionIds: { type: Array, default: () => [] },
|
||||
pageIds: { type: Array, default: () => [] },
|
||||
actionIds: { type: Array, default: () => [] },
|
||||
});
|
||||
const emit = defineEmits(["update:functionIds", "update:pageIds", "update:actionIds"]);
|
||||
const appStore = useAppStore();
|
||||
|
||||
const functionEntries = computed(() => {
|
||||
const byId = new Map(props.functionNodes.map((item) => [String(item.b_id), item]));
|
||||
return props.functionNodes
|
||||
.filter((item) => item.b_node_type === "entry")
|
||||
.map((item) => {
|
||||
const path = [];
|
||||
let parent = byId.get(String(item.b_parent_id || ""));
|
||||
while (parent) {
|
||||
path.unshift(appStore.t(parent.b_i18n, parent.b_name));
|
||||
parent = byId.get(String(parent.b_parent_id || ""));
|
||||
}
|
||||
return { ...item, path: path.join(" / ") };
|
||||
});
|
||||
});
|
||||
|
||||
const pageGroups = computed(() => {
|
||||
const groups = new Map();
|
||||
for (const item of props.pageNodes) {
|
||||
const key = String(item.b_page_module_id);
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
key,
|
||||
name: appStore.t(item.b_page_i18n, item.b_page_name || item.b_page_code),
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
groups.get(key).items.push(item);
|
||||
}
|
||||
return [...groups.values()];
|
||||
});
|
||||
|
||||
const actionGroups = computed(() => {
|
||||
const groups = new Map();
|
||||
for (const item of props.actions) {
|
||||
const key = String(item.b_module_id);
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
key,
|
||||
name: appStore.t(item.b_module_i18n, item.b_module_name || item.b_module_code),
|
||||
items: [],
|
||||
});
|
||||
}
|
||||
groups.get(key).items.push(item);
|
||||
}
|
||||
return [...groups.values()];
|
||||
});
|
||||
|
||||
function toggle(model, id, checked) {
|
||||
const values = new Set(props[model].map(String));
|
||||
if (checked) values.add(String(id));
|
||||
else values.delete(String(id));
|
||||
emit(`update:${model}`, [...values]);
|
||||
}
|
||||
|
||||
function setAll(model, rows, checked) {
|
||||
const values = new Set(props[model].map(String));
|
||||
for (const row of rows) {
|
||||
if (checked) values.add(String(row.b_id));
|
||||
else values.delete(String(row.b_id));
|
||||
}
|
||||
emit(`update:${model}`, [...values]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-tabs class="grant-tabs">
|
||||
<a-tab-pane key="functions" tab="功能入口">
|
||||
<div class="grant-toolbar">
|
||||
<span>已选 {{ functionIds.length }} 项</span>
|
||||
<a-button type="link" size="small" @click="setAll('functionIds', functionEntries, true)"
|
||||
>全选</a-button
|
||||
>
|
||||
<a-button type="link" size="small" @click="setAll('functionIds', functionEntries, false)"
|
||||
>清空</a-button
|
||||
>
|
||||
</div>
|
||||
<div class="grant-list">
|
||||
<label v-for="item in functionEntries" :key="item.b_id" class="grant-row">
|
||||
<a-checkbox
|
||||
:checked="functionIds.map(String).includes(String(item.b_id))"
|
||||
@change="toggle('functionIds', item.b_id, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ appStore.t(item.b_i18n, item.b_name) }}</span>
|
||||
<small>{{ item.path }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="pages" tab="页面区域">
|
||||
<div class="grant-toolbar">
|
||||
<span>已选 {{ pageIds.length }} 项</span>
|
||||
<a-button type="link" size="small" @click="setAll('pageIds', pageNodes, true)"
|
||||
>全选</a-button
|
||||
>
|
||||
<a-button type="link" size="small" @click="setAll('pageIds', pageNodes, false)"
|
||||
>清空</a-button
|
||||
>
|
||||
</div>
|
||||
<div class="grant-list">
|
||||
<section v-for="group in pageGroups" :key="group.key" class="grant-group">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<label v-for="item in group.items" :key="item.b_id" class="grant-row">
|
||||
<a-checkbox
|
||||
:checked="pageIds.map(String).includes(String(item.b_id))"
|
||||
@change="toggle('pageIds', item.b_id, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ appStore.t(item.b_i18n, item.b_name) }}</span>
|
||||
<small>{{ item.b_code }}</small>
|
||||
</label>
|
||||
</section>
|
||||
<a-empty v-if="!pageGroups.length" description="没有需要单独授权的页面区域" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="actions" tab="模块操作">
|
||||
<div class="grant-toolbar">
|
||||
<span>已选 {{ actionIds.length }} 项</span>
|
||||
<a-button type="link" size="small" @click="setAll('actionIds', actions, true)"
|
||||
>全选</a-button
|
||||
>
|
||||
<a-button type="link" size="small" @click="setAll('actionIds', actions, false)"
|
||||
>清空</a-button
|
||||
>
|
||||
</div>
|
||||
<div class="grant-list">
|
||||
<section v-for="group in actionGroups" :key="group.key" class="grant-group">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<label v-for="item in group.items" :key="item.b_id" class="grant-row">
|
||||
<a-checkbox
|
||||
:checked="actionIds.map(String).includes(String(item.b_id))"
|
||||
@change="toggle('actionIds', item.b_id, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ appStore.t(item.b_i18n, item.b_name) }}</span>
|
||||
<small>{{ item.b_code }}</small>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grant-tabs :deep(.ant-tabs-content-holder),
|
||||
.grant-tabs :deep(.ant-tabs-content),
|
||||
.grant-tabs :deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.grant-tabs :deep(.ant-tabs-tabpane) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grant-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
color: var(--fms-text-secondary, #666);
|
||||
}
|
||||
|
||||
.grant-toolbar span {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.grant-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.grant-group > strong {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
background: var(--fms-bg, #f5f7fa);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.grant-row {
|
||||
display: grid;
|
||||
min-height: 38px;
|
||||
grid-template-columns: 24px minmax(140px, 1fr) minmax(120px, 1fr);
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grant-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.grant-row small {
|
||||
overflow: hidden;
|
||||
color: var(--fms-text-secondary, #8c8c8c);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Plus, Save, Trash2 } from "@lucide/vue";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import PermissionGrantEditor from "./PermissionGrantEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
controller: { type: Object, required: true },
|
||||
});
|
||||
const emit = defineEmits(["update:open"]);
|
||||
const permissionStore = usePermissionStore();
|
||||
const createOpen = ref(false);
|
||||
const createForm = reactive({ b_code: "", b_name: "", b_xh: 0 });
|
||||
const editForm = reactive({ b_id: "", b_name: "", b_canuse: true, b_xh: 0 });
|
||||
|
||||
const selectedRole = computed(() =>
|
||||
props.controller.roles.value.find(
|
||||
(item) => String(item.b_id) === String(props.controller.selectedRoleId.value),
|
||||
),
|
||||
);
|
||||
|
||||
watch(selectedRole, (role) => {
|
||||
Object.assign(editForm, {
|
||||
b_id: String(role?.b_id || ""),
|
||||
b_name: role?.b_name || "",
|
||||
b_canuse: Number(role?.b_canuse) === 1,
|
||||
b_xh: Number(role?.b_xh) || 0,
|
||||
});
|
||||
});
|
||||
|
||||
async function selectRole(id) {
|
||||
try {
|
||||
await props.controller.loadRole(id);
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
function showCreate() {
|
||||
Object.assign(createForm, { b_code: "", b_name: "", b_xh: 0 });
|
||||
createOpen.value = true;
|
||||
}
|
||||
|
||||
async function createRole() {
|
||||
if (!/^[a-z][a-z0-9_]*$/.test(createForm.b_code) || !createForm.b_name.trim()) {
|
||||
message.warning("请填写小写技术编码和角色名称");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await props.controller.createRole(createForm);
|
||||
createOpen.value = false;
|
||||
message.success("角色已创建");
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRole() {
|
||||
try {
|
||||
await props.controller.saveRole(editForm);
|
||||
permissionStore.clear();
|
||||
await permissionStore.load();
|
||||
message.success("角色与授权已保存");
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
function removeRole() {
|
||||
if (!selectedRole.value) return;
|
||||
Modal.confirm({
|
||||
title: `确定删除角色「${selectedRole.value.b_name}」?`,
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await props.controller.removeRole(selectedRole.value.b_id);
|
||||
permissionStore.clear();
|
||||
await permissionStore.load();
|
||||
message.success("角色已删除");
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
title="角色与主授权"
|
||||
width="1080px"
|
||||
:footer="null"
|
||||
:body-style="{ height: '650px', padding: 0 }"
|
||||
@cancel="emit('update:open', false)"
|
||||
>
|
||||
<div class="role-layout">
|
||||
<aside class="role-list-pane">
|
||||
<div class="role-toolbar">
|
||||
<strong>角色</strong>
|
||||
<a-button type="text" title="新增角色" @click="showCreate"><Plus :size="16" /></a-button>
|
||||
</div>
|
||||
<button
|
||||
v-for="role in controller.roles.value"
|
||||
:key="role.b_id"
|
||||
type="button"
|
||||
class="role-item"
|
||||
:class="{ active: String(role.b_id) === controller.selectedRoleId.value }"
|
||||
@click="selectRole(role.b_id)"
|
||||
>
|
||||
<span>{{ role.b_name }}</span>
|
||||
<small
|
||||
>{{ role.b_code
|
||||
}}<template v-if="Number(role.b_is_super) === 1"> · 超级角色</template></small
|
||||
>
|
||||
</button>
|
||||
</aside>
|
||||
<main v-if="selectedRole" class="role-detail">
|
||||
<div class="role-detail-toolbar">
|
||||
<a-input v-model:value="editForm.b_name" aria-label="角色名称" />
|
||||
<a-input-number v-model:value="editForm.b_xh" aria-label="排序" :precision="0" />
|
||||
<a-switch
|
||||
v-model:checked="editForm.b_canuse"
|
||||
checked-children="启用"
|
||||
un-checked-children="停用"
|
||||
/>
|
||||
<a-button danger :disabled="Number(selectedRole.b_is_super) === 1" @click="removeRole">
|
||||
<Trash2 :size="15" />删除
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="controller.saving.value" @click="saveRole">
|
||||
<Save :size="15" />保存
|
||||
</a-button>
|
||||
</div>
|
||||
<a-alert
|
||||
v-if="Number(selectedRole.b_is_super) === 1"
|
||||
type="info"
|
||||
show-icon
|
||||
message="超级角色自动拥有全部启用权限,无需逐项配置。"
|
||||
/>
|
||||
<PermissionGrantEditor
|
||||
v-else
|
||||
class="role-grants"
|
||||
:function-nodes="controller.functionNodes.value"
|
||||
:page-nodes="controller.pageNodes.value"
|
||||
:actions="controller.actions.value"
|
||||
:function-ids="controller.roleFunctionIds.value"
|
||||
:page-ids="controller.rolePageIds.value"
|
||||
:action-ids="controller.roleActionIds.value"
|
||||
@update:function-ids="controller.roleFunctionIds.value = $event"
|
||||
@update:page-ids="controller.rolePageIds.value = $event"
|
||||
@update:action-ids="controller.roleActionIds.value = $event"
|
||||
/>
|
||||
</main>
|
||||
<a-empty v-else class="role-empty" description="请选择角色" />
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="createOpen" title="新增角色" @ok="createRole">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="技术编码" required
|
||||
><a-input v-model:value="createForm.b_code"
|
||||
/></a-form-item>
|
||||
<a-form-item label="名称" required
|
||||
><a-input v-model:value="createForm.b_name"
|
||||
/></a-form-item>
|
||||
<a-form-item label="排序"
|
||||
><a-input-number v-model:value="createForm.b_xh" :precision="0"
|
||||
/></a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-layout {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-template-columns: 230px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.role-list-pane {
|
||||
overflow: auto;
|
||||
border-right: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.role-toolbar,
|
||||
.role-detail-toolbar {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.role-toolbar strong {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.role-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 9px 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-item:hover,
|
||||
.role-item.active {
|
||||
background: var(--fms-bg, #f5f7fa);
|
||||
}
|
||||
|
||||
.role-item small {
|
||||
color: var(--fms-text-secondary, #8c8c8c);
|
||||
}
|
||||
|
||||
.role-detail {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.role-detail-toolbar {
|
||||
margin: 0 -16px 12px;
|
||||
}
|
||||
|
||||
.role-detail-toolbar :deep(.ant-input) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.role-detail-toolbar :deep(.ant-input-number) {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.role-detail-toolbar :deep(.ant-switch) {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.role-grants {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.role-empty {
|
||||
margin-top: 180px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
import { ref } from "vue";
|
||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
|
||||
function idSet(rows, field) {
|
||||
return new Set(rows.map((item) => String(item[field])));
|
||||
}
|
||||
|
||||
function grantDescriptor(
|
||||
table,
|
||||
ownerField,
|
||||
targetField,
|
||||
ownerId,
|
||||
selectedIds,
|
||||
existingRows,
|
||||
extra,
|
||||
) {
|
||||
const selected = new Set(selectedIds.map(String));
|
||||
const existing = idSet(existingRows, targetField);
|
||||
return {
|
||||
table,
|
||||
ownerField,
|
||||
targetField,
|
||||
ownerId: String(ownerId),
|
||||
extra,
|
||||
inserts: [...selected].filter((id) => !existing.has(id)),
|
||||
deletes: existingRows
|
||||
.filter((row) => !selected.has(String(row[targetField])))
|
||||
.map((row) => ({ b_id: row.b_id })),
|
||||
};
|
||||
}
|
||||
|
||||
async function saveGrantDescriptors(descriptors, leadingRequests = []) {
|
||||
const insertCount = descriptors.reduce((total, item) => total + item.inserts.length, 0);
|
||||
let ids = [];
|
||||
if (insertCount) {
|
||||
const response = await nextIdApi(insertCount);
|
||||
ids = Array.isArray(response.data) ? response.data.map(String) : [String(response.data)];
|
||||
}
|
||||
let idIndex = 0;
|
||||
const requests = [...leadingRequests];
|
||||
for (const descriptor of descriptors) {
|
||||
const inserts = descriptor.inserts.map((targetId) => ({
|
||||
b_id: ids[idIndex++],
|
||||
[descriptor.ownerField]: descriptor.ownerId,
|
||||
[descriptor.targetField]: targetId,
|
||||
...(descriptor.extra || {}),
|
||||
}));
|
||||
if (inserts.length || descriptor.deletes.length) {
|
||||
requests.push({
|
||||
table: descriptor.table,
|
||||
key_field: "b_id",
|
||||
inserts,
|
||||
deletes: descriptor.deletes,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (requests.length) await saveObjectApi(requests);
|
||||
}
|
||||
|
||||
export function usePermissionAdministration(operatorId) {
|
||||
const roles = ref([]);
|
||||
const functionNodes = ref([]);
|
||||
const pageNodes = ref([]);
|
||||
const actions = ref([]);
|
||||
const selectedRoleId = ref("");
|
||||
const roleFunctionIds = ref([]);
|
||||
const rolePageIds = ref([]);
|
||||
const roleActionIds = ref([]);
|
||||
const selectedUserId = ref("");
|
||||
const userRoleIds = ref([]);
|
||||
const userFunctionIds = ref([]);
|
||||
const userPageIds = ref([]);
|
||||
const userActionIds = ref([]);
|
||||
const roleRows = ref({ functions: [], pages: [], actions: [] });
|
||||
const userRows = ref({ roles: [], functions: [], pages: [], actions: [] });
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
async function loadCatalogs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("b_role", "", "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("v_s_function_node", "b_canuse = 1", "b_xh ASC, b_id ASC"),
|
||||
loadDataApi(
|
||||
"v_s_page_node",
|
||||
"b_canuse = 1 AND b_permission_required = 1",
|
||||
"b_page_module_id ASC, b_xh ASC, b_id ASC",
|
||||
),
|
||||
loadDataApi("v_s_module_action", "b_canuse = 1", "b_module_id ASC, b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
roles.value = responses[0].data || [];
|
||||
functionNodes.value = responses[1].data || [];
|
||||
pageNodes.value = responses[2].data || [];
|
||||
actions.value = responses[3].data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRole(roleId) {
|
||||
selectedRoleId.value = String(roleId || "");
|
||||
if (!selectedRoleId.value) return;
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("b_role_function_node", `b_role_id = ${selectedRoleId.value}`),
|
||||
loadDataApi("b_role_page_node", `b_role_id = ${selectedRoleId.value}`),
|
||||
loadDataApi("b_role_action", `b_role_id = ${selectedRoleId.value}`),
|
||||
]);
|
||||
roleRows.value = {
|
||||
functions: responses[0].data || [],
|
||||
pages: responses[1].data || [],
|
||||
actions: responses[2].data || [],
|
||||
};
|
||||
roleFunctionIds.value = [...idSet(roleRows.value.functions, "b_function_node_id")];
|
||||
rolePageIds.value = [...idSet(roleRows.value.pages, "b_page_node_id")];
|
||||
roleActionIds.value = [...idSet(roleRows.value.actions, "b_action_id")];
|
||||
}
|
||||
|
||||
async function createRole(values) {
|
||||
const response = await nextIdApi();
|
||||
const id = String(response.data);
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_role",
|
||||
key_field: "b_id",
|
||||
inserts: [
|
||||
{
|
||||
b_id: id,
|
||||
b_code: String(values.b_code || "").trim(),
|
||||
b_name: String(values.b_name || "").trim(),
|
||||
b_i18n: `meta.role.${id}`,
|
||||
b_is_super: values.b_is_super ? 1 : 0,
|
||||
b_canuse: 1,
|
||||
b_xh: Number(values.b_xh) || 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
await loadCatalogs();
|
||||
await loadRole(id);
|
||||
}
|
||||
|
||||
async function saveRole(values) {
|
||||
const roleId = selectedRoleId.value;
|
||||
if (!roleId) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveGrantDescriptors(
|
||||
[
|
||||
grantDescriptor(
|
||||
"b_role_function_node",
|
||||
"b_role_id",
|
||||
"b_function_node_id",
|
||||
roleId,
|
||||
roleFunctionIds.value,
|
||||
roleRows.value.functions,
|
||||
),
|
||||
grantDescriptor(
|
||||
"b_role_page_node",
|
||||
"b_role_id",
|
||||
"b_page_node_id",
|
||||
roleId,
|
||||
rolePageIds.value,
|
||||
roleRows.value.pages,
|
||||
),
|
||||
grantDescriptor(
|
||||
"b_role_action",
|
||||
"b_role_id",
|
||||
"b_action_id",
|
||||
roleId,
|
||||
roleActionIds.value,
|
||||
roleRows.value.actions,
|
||||
),
|
||||
],
|
||||
[
|
||||
{
|
||||
table: "b_role",
|
||||
key_field: "b_id",
|
||||
updates: [
|
||||
{
|
||||
b_id: roleId,
|
||||
b_name: String(values.b_name || "").trim(),
|
||||
b_canuse: values.b_canuse ? 1 : 0,
|
||||
b_xh: Number(values.b_xh) || 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
await loadCatalogs();
|
||||
await loadRole(roleId);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRole(roleId) {
|
||||
const role = roles.value.find((item) => String(item.b_id) === String(roleId));
|
||||
if (Number(role?.b_is_super) === 1) throw new Error("超级管理员角色不能删除");
|
||||
const userRoles = await loadDataApi("b_user_role", `b_role_id = ${roleId}`);
|
||||
if ((userRoles.data || []).length) throw new Error("该角色仍分配给用户");
|
||||
await loadRole(roleId);
|
||||
const dataScopes = await loadDataApi("b_role_data_scope", `b_role_id = ${roleId}`);
|
||||
await saveObjectApi([
|
||||
{ table: "b_role_function_node", key_field: "b_id", deletes: roleRows.value.functions },
|
||||
{ table: "b_role_page_node", key_field: "b_id", deletes: roleRows.value.pages },
|
||||
{ table: "b_role_action", key_field: "b_id", deletes: roleRows.value.actions },
|
||||
{ table: "b_role_data_scope", key_field: "b_id", deletes: dataScopes.data || [] },
|
||||
{ table: "b_role", key_field: "b_id", deletes: [{ b_id: String(roleId) }] },
|
||||
]);
|
||||
selectedRoleId.value = "";
|
||||
await loadCatalogs();
|
||||
}
|
||||
|
||||
async function loadUser(userId) {
|
||||
selectedUserId.value = String(userId || "");
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("b_user_role", `b_user_id = ${selectedUserId.value}`),
|
||||
loadDataApi("b_user_function_node", `b_user_id = ${selectedUserId.value}`),
|
||||
loadDataApi("b_user_page_node", `b_user_id = ${selectedUserId.value}`),
|
||||
loadDataApi("b_user_action", `b_user_id = ${selectedUserId.value}`),
|
||||
]);
|
||||
userRows.value = {
|
||||
roles: responses[0].data || [],
|
||||
functions: responses[1].data || [],
|
||||
pages: responses[2].data || [],
|
||||
actions: responses[3].data || [],
|
||||
};
|
||||
userRoleIds.value = [...idSet(userRows.value.roles, "b_role_id")];
|
||||
userFunctionIds.value = [...idSet(userRows.value.functions, "b_function_node_id")];
|
||||
userPageIds.value = [...idSet(userRows.value.pages, "b_page_node_id")];
|
||||
userActionIds.value = [...idSet(userRows.value.actions, "b_action_id")];
|
||||
}
|
||||
|
||||
async function saveUserPermissions() {
|
||||
const userId = selectedUserId.value;
|
||||
if (!userId) return;
|
||||
const extra = { b_inputuser_id: String(operatorId.value || operatorId) };
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveGrantDescriptors([
|
||||
grantDescriptor(
|
||||
"b_user_role",
|
||||
"b_user_id",
|
||||
"b_role_id",
|
||||
userId,
|
||||
userRoleIds.value,
|
||||
userRows.value.roles,
|
||||
extra,
|
||||
),
|
||||
grantDescriptor(
|
||||
"b_user_function_node",
|
||||
"b_user_id",
|
||||
"b_function_node_id",
|
||||
userId,
|
||||
userFunctionIds.value,
|
||||
userRows.value.functions,
|
||||
extra,
|
||||
),
|
||||
grantDescriptor(
|
||||
"b_user_page_node",
|
||||
"b_user_id",
|
||||
"b_page_node_id",
|
||||
userId,
|
||||
userPageIds.value,
|
||||
userRows.value.pages,
|
||||
extra,
|
||||
),
|
||||
grantDescriptor(
|
||||
"b_user_action",
|
||||
"b_user_id",
|
||||
"b_action_id",
|
||||
userId,
|
||||
userActionIds.value,
|
||||
userRows.value.actions,
|
||||
extra,
|
||||
),
|
||||
]);
|
||||
await loadUser(userId);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
roles,
|
||||
functionNodes,
|
||||
pageNodes,
|
||||
actions,
|
||||
selectedRoleId,
|
||||
roleFunctionIds,
|
||||
rolePageIds,
|
||||
roleActionIds,
|
||||
selectedUserId,
|
||||
userRoleIds,
|
||||
userFunctionIds,
|
||||
userPageIds,
|
||||
userActionIds,
|
||||
loading,
|
||||
saving,
|
||||
loadCatalogs,
|
||||
loadRole,
|
||||
createRole,
|
||||
saveRole,
|
||||
removeRole,
|
||||
loadUser,
|
||||
saveUserPermissions,
|
||||
};
|
||||
}
|
||||
@@ -1,428 +1,124 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Plus, ChevronRight, ChevronDown } from "@lucide/vue";
|
||||
import { ShieldCheck } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import PermissionGrantEditor from "./components/PermissionGrantEditor.vue";
|
||||
import RoleManagerModal from "./components/RoleManagerModal.vue";
|
||||
import { usePermissionAdministration } from "./composables/usePermissionAdministration";
|
||||
|
||||
const listRef = ref(null);
|
||||
const authStore = useAuthStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const operatorId = computed(() => String(authStore.user?.id || ""));
|
||||
const permissions = usePermissionAdministration(operatorId);
|
||||
const roleManagerOpen = ref(false);
|
||||
const userPermissionOpen = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
|
||||
// ── Mock data ──
|
||||
const mockUser = {
|
||||
b_id: "1001",
|
||||
b_account: "zhangsan",
|
||||
b_name: "张三",
|
||||
b_phone: "13800138000",
|
||||
b_department: "技术部",
|
||||
b_canuse: "1",
|
||||
b_salesman: "0",
|
||||
b_docman: "0",
|
||||
b_dealman: "1",
|
||||
b_othermachinelogin: "0",
|
||||
b_visible_customer: "1",
|
||||
b_ismanager: "0",
|
||||
b_purview: "1",
|
||||
b_spview: "0",
|
||||
b_subcompany: "",
|
||||
b_inputuser: "admin",
|
||||
b_inputdatetime: "2025-01-15 10:30:00",
|
||||
};
|
||||
async function ensureCatalogs() {
|
||||
if (!permissions.roles.value.length) await permissions.loadCatalogs();
|
||||
}
|
||||
|
||||
// 模块+权限合并的树数据:每个模块挂载自己的 power 列表
|
||||
const mockModuleTree = [
|
||||
{
|
||||
b_id: "g1",
|
||||
b_name: "基础资料",
|
||||
children: [
|
||||
{
|
||||
b_id: "m11", b_code: "m_customer", b_name: "客户管理",
|
||||
powers: ["查看", "新增", "修改", "删除", "导出"],
|
||||
},
|
||||
{
|
||||
b_id: "m12", b_code: "m_supplier", b_name: "供应商管理",
|
||||
powers: ["查看", "新增", "修改", "删除"],
|
||||
},
|
||||
{
|
||||
b_id: "m13", b_code: "m_product", b_name: "产品管理",
|
||||
powers: ["查看", "新增", "修改"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: "g2",
|
||||
b_name: "业务操作",
|
||||
children: [
|
||||
{
|
||||
b_id: "m21", b_code: "m_order", b_name: "订单管理",
|
||||
powers: ["查看", "新增", "修改", "删除", "导出", "审核"],
|
||||
},
|
||||
{
|
||||
b_id: "m22", b_code: "m_ship", b_name: "发货管理",
|
||||
powers: ["查看", "新增", "发货", "确认"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: "g3",
|
||||
b_name: "报表与分析",
|
||||
children: [
|
||||
{
|
||||
b_id: "m31", b_code: "m_sales_report", b_name: "销售报表",
|
||||
powers: ["查看", "导出"],
|
||||
},
|
||||
{
|
||||
b_id: "m32", b_code: "m_stock_report", b_name: "库存报表",
|
||||
powers: ["查看"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 该用户当前的全部权限状态:模块授权 + 每个模块的操作权限
|
||||
// key = moduleId, value = Set of power names(含 "module" 表示模块已授权)
|
||||
const mockUserGrants = {
|
||||
m11: new Set(["module", "查看", "新增", "修改"]),
|
||||
m13: new Set(["module", "查看"]),
|
||||
m21: new Set(["module", "查看", "新增", "修改", "审核"]),
|
||||
};
|
||||
|
||||
// ── Modal state ──
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("新增用户");
|
||||
const saving = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
b_account: "",
|
||||
b_name: "",
|
||||
b_phone: "",
|
||||
b_department: "",
|
||||
b_canuse: true,
|
||||
b_salesman: false,
|
||||
b_docman: false,
|
||||
b_dealman: false,
|
||||
b_othermachinelogin: false,
|
||||
b_visible_customer: false,
|
||||
b_ismanager: false,
|
||||
b_purview: true,
|
||||
b_spview: false,
|
||||
});
|
||||
const password = ref("");
|
||||
const passwordConfirm = ref("");
|
||||
|
||||
// 合并后的权限状态:{ [moduleId]: Set<powerName> },"module" 表示已授权该模块
|
||||
const moduleGrants = ref({});
|
||||
|
||||
// 左侧菜单树:当前选中模块及分组展开状态
|
||||
const selectedModId = ref(null);
|
||||
const collapsedGroups = ref(new Set(["g3"])); // 默认折叠"报表与分析"
|
||||
const selectedModule = computed(() => {
|
||||
if (!selectedModId.value) return null;
|
||||
for (const group of mockModuleTree) {
|
||||
const found = group.children.find((m) => m.b_id === selectedModId.value);
|
||||
if (found) return found;
|
||||
async function openRoleManager() {
|
||||
try {
|
||||
await ensureCatalogs();
|
||||
roleManagerOpen.value = true;
|
||||
if (!permissions.selectedRoleId.value && permissions.roles.value.length) {
|
||||
await permissions.loadRole(permissions.roles.value[0].b_id);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
function selectModule(modId) {
|
||||
selectedModId.value = modId;
|
||||
}
|
||||
function toggleGroupExpand(gid) {
|
||||
const next = new Set(collapsedGroups.value);
|
||||
if (next.has(gid)) next.delete(gid);
|
||||
else next.add(gid);
|
||||
collapsedGroups.value = next;
|
||||
}
|
||||
|
||||
function isModuleGranted(modId) {
|
||||
return moduleGrants.value[modId]?.has("module") ?? false;
|
||||
}
|
||||
function isPowerGranted(modId, powerName) {
|
||||
return moduleGrants.value[modId]?.has(powerName) ?? false;
|
||||
}
|
||||
function toggleModuleGrant(modId) {
|
||||
const next = { ...moduleGrants.value };
|
||||
if (isModuleGranted(modId)) {
|
||||
// 取消模块授权 → 清除该模块所有权限
|
||||
delete next[modId];
|
||||
} else {
|
||||
next[modId] = new Set(["module"]);
|
||||
async function openUserPermissions(record) {
|
||||
try {
|
||||
selectedUser.value = record;
|
||||
await ensureCatalogs();
|
||||
await permissions.loadUser(record.b_id);
|
||||
userPermissionOpen.value = true;
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
function togglePower(modId, powerName) {
|
||||
const next = { ...moduleGrants.value };
|
||||
const current = next[modId];
|
||||
if (!current) return;
|
||||
const nextSet = new Set(current);
|
||||
if (nextSet.has(powerName)) nextSet.delete(powerName);
|
||||
else nextSet.add(powerName);
|
||||
next[modId] = nextSet;
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
function toggleAllPowers(mod) {
|
||||
const modId = mod.b_id;
|
||||
const current = moduleGrants.value[modId];
|
||||
if (!current) return;
|
||||
const next = { ...moduleGrants.value };
|
||||
const nextSet = new Set(current);
|
||||
const allGranted = mod.powers.every((p) => nextSet.has(p));
|
||||
if (allGranted) {
|
||||
for (const p of mod.powers) nextSet.delete(p);
|
||||
} else {
|
||||
for (const p of mod.powers) nextSet.add(p);
|
||||
|
||||
async function saveUserPermissions() {
|
||||
try {
|
||||
await permissions.saveUserPermissions();
|
||||
permissionStore.clear();
|
||||
await permissionStore.load();
|
||||
userPermissionOpen.value = false;
|
||||
message.success("用户角色与附加授权已保存");
|
||||
} catch (error) {
|
||||
message.error(error.message || String(error));
|
||||
}
|
||||
next[modId] = nextSet;
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function resetForm() {
|
||||
formData.b_account = "";
|
||||
formData.b_name = "";
|
||||
formData.b_phone = "";
|
||||
formData.b_department = "";
|
||||
formData.b_canuse = true;
|
||||
formData.b_salesman = false;
|
||||
formData.b_docman = false;
|
||||
formData.b_dealman = false;
|
||||
formData.b_othermachinelogin = false;
|
||||
formData.b_visible_customer = false;
|
||||
formData.b_ismanager = false;
|
||||
formData.b_purview = true;
|
||||
formData.b_spview = false;
|
||||
password.value = "";
|
||||
passwordConfirm.value = "";
|
||||
moduleGrants.value = {};
|
||||
selectedModId.value = null;
|
||||
collapsedGroups.value = new Set(["g3"]);
|
||||
}
|
||||
|
||||
function fillEditForm(user) {
|
||||
formData.b_account = user.b_account || "";
|
||||
formData.b_name = user.b_name || "";
|
||||
formData.b_phone = user.b_phone || "";
|
||||
formData.b_department = user.b_department || "";
|
||||
formData.b_canuse = user.b_canuse === "1" || user.b_canuse === 1;
|
||||
formData.b_salesman = user.b_salesman === "1" || user.b_salesman === 1;
|
||||
formData.b_docman = user.b_docman === "1" || user.b_docman === 1;
|
||||
formData.b_dealman = user.b_dealman === "1" || user.b_dealman === 1;
|
||||
formData.b_othermachinelogin = user.b_othermachinelogin === "1" || user.b_othermachinelogin === 1;
|
||||
formData.b_visible_customer = user.b_visible_customer === "1" || user.b_visible_customer === 1;
|
||||
formData.b_ismanager = user.b_ismanager === "1" || user.b_ismanager === 1;
|
||||
formData.b_purview = user.b_purview === "1" || user.b_purview === 1;
|
||||
formData.b_spview = user.b_spview === "1" || user.b_spview === 1;
|
||||
password.value = "";
|
||||
passwordConfirm.value = "";
|
||||
}
|
||||
|
||||
// ── Handlers ──
|
||||
function handleAdd() {
|
||||
modalTitle.value = "新增用户";
|
||||
activeTab.value = "basic";
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
modalTitle.value = `编辑用户 - ${row.b_name || row.b_account || ""}`;
|
||||
activeTab.value = "basic";
|
||||
fillEditForm(mockUser);
|
||||
// 深拷贝 mock user grants
|
||||
const grants = {};
|
||||
for (const [modId, powers] of Object.entries(mockUserGrants)) {
|
||||
grants[modId] = new Set(powers);
|
||||
}
|
||||
moduleGrants.value = grants;
|
||||
selectedModId.value = mockModuleTree[0].children[0].b_id;
|
||||
collapsedGroups.value = new Set();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
const rowActions = [
|
||||
{ key: "edit", type: "action", label: "编辑", onClick: handleEdit },
|
||||
{
|
||||
key: "permissions",
|
||||
permissionCode: "update",
|
||||
type: "action",
|
||||
label: "授权",
|
||||
onClick: openUserPermissions,
|
||||
},
|
||||
];
|
||||
|
||||
// Save (demo only)
|
||||
async function handleSave() {
|
||||
saving.value = true;
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
saving.value = false;
|
||||
modalVisible.value = false;
|
||||
message.success("(Demo)保存成功 — 未真实调用接口");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<FmsModuleListPage
|
||||
ref="listRef"
|
||||
data-code="b_user"
|
||||
:row-actions="rowActions"
|
||||
@row-action="({ record }) => { if (record?.action?.key === 'edit') handleEdit(record.record) }"
|
||||
>
|
||||
<FmsModuleListPage module-code="b_user" :row-actions="rowActions">
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<Plus :size="15" />
|
||||
新增用户
|
||||
<a-button v-if="permissionStore.canAction('b_user', 'update')" @click="openRoleManager">
|
||||
<ShieldCheck :size="15" />角色维护
|
||||
</a-button>
|
||||
</template>
|
||||
</FmsModuleListPage>
|
||||
|
||||
<!-- Demo Edit Modal -->
|
||||
<RoleManagerModal v-model:open="roleManagerOpen" :controller="permissions" />
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:width="1000"
|
||||
:confirm-loading="saving"
|
||||
:body-style="{ padding: '0 24px 16px', maxHeight: '70vh', overflow: 'auto' }"
|
||||
@ok="handleSave"
|
||||
@cancel="modalVisible = false"
|
||||
v-model:open="userPermissionOpen"
|
||||
:title="`用户授权 - ${selectedUser?.b_name || selectedUser?.b_account || selectedUser?.b_id || ''}`"
|
||||
width="1000px"
|
||||
:confirm-loading="permissions.saving.value"
|
||||
:body-style="{ height: '640px', padding: '12px 20px' }"
|
||||
@ok="saveUserPermissions"
|
||||
>
|
||||
<a-tabs v-model:active-key="activeTab">
|
||||
<!-- Tab 1: 基本信息(含密码) -->
|
||||
<a-tab-pane key="basic" tab="基本信息">
|
||||
<div class="demo-basic-info">
|
||||
<a-row :gutter="[24, 16]">
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">登录账号 <span class="demo-required">*</span></label>
|
||||
<a-input v-model:value="formData.b_account" placeholder="请输入登录账号" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">用户姓名 <span class="demo-required">*</span></label>
|
||||
<a-input v-model:value="formData.b_name" placeholder="请输入用户姓名" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">手机号</label>
|
||||
<a-input v-model:value="formData.b_phone" placeholder="请输入手机号" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">部门</label>
|
||||
<a-input v-model:value="formData.b_department" placeholder="请输入部门" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">新密码</label>
|
||||
<a-input-password v-model:value="password" placeholder="留空则不修改" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">确认密码</label>
|
||||
<a-input-password v-model:value="passwordConfirm" placeholder="请再次输入" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标志位 -->
|
||||
<div class="demo-flags-card">
|
||||
<div class="demo-card-title">标志位</div>
|
||||
<a-row :gutter="[16, 12]">
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_canuse">启用</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_salesman">销售员</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_docman">单据主管</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_dealman">可处理事务</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_othermachinelogin">允许异地登录</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_visible_customer">可见客户</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_ismanager">管理员</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_purview">操作权限</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_spview">审批权限</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div class="user-permission-layout">
|
||||
<section class="role-section">
|
||||
<strong>所属角色</strong>
|
||||
<div class="role-options">
|
||||
<a-checkbox-group v-model:value="permissions.userRoleIds.value">
|
||||
<a-checkbox
|
||||
v-for="role in permissions.roles.value"
|
||||
:key="role.b_id"
|
||||
:value="String(role.b_id)"
|
||||
:disabled="Number(role.b_canuse) !== 1"
|
||||
>
|
||||
{{ role.b_name }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 2: 菜单权限(左树右操作列表) -->
|
||||
<a-tab-pane key="module-power" tab="菜单权限">
|
||||
<div class="perm-layout">
|
||||
<!-- 左侧:菜单树 -->
|
||||
<div class="perm-tree">
|
||||
<div v-for="group in mockModuleTree" :key="group.b_id" class="tree-group">
|
||||
<div class="tree-group-hd" @click="toggleGroupExpand(group.b_id)">
|
||||
<span class="tree-group-ic">
|
||||
<ChevronRight v-if="collapsedGroups.has(group.b_id)" :size="14" />
|
||||
<ChevronDown v-else :size="14" />
|
||||
</span>
|
||||
<span class="tree-group-label">{{ group.b_name }}</span>
|
||||
<span class="tree-group-count">{{ group.children.length }}</span>
|
||||
</div>
|
||||
<div v-show="!collapsedGroups.has(group.b_id)" class="tree-group-body">
|
||||
<div
|
||||
v-for="mod in group.children"
|
||||
:key="mod.b_id"
|
||||
class="tree-item"
|
||||
:class="{ 'is-active': selectedModId === mod.b_id }"
|
||||
@click="selectModule(mod.b_id)"
|
||||
>
|
||||
<a-checkbox
|
||||
:checked="isModuleGranted(mod.b_id)"
|
||||
@click.stop
|
||||
@change="toggleModuleGrant(mod.b_id)"
|
||||
/>
|
||||
<span class="tree-item-name">{{ mod.b_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:操作权限列表 -->
|
||||
<div class="perm-detail">
|
||||
<template v-if="selectedModule">
|
||||
<div class="perm-detail-hd">
|
||||
<span class="perm-detail-title">{{ selectedModule.b_name }}</span>
|
||||
<span class="perm-detail-sub">{{ selectedModule.b_code }}</span>
|
||||
</div>
|
||||
<div class="perm-detail-body">
|
||||
<template v-if="isModuleGranted(selectedModule.b_id)">
|
||||
<label class="perm-detail-all">
|
||||
<a-checkbox
|
||||
:checked="selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||||
:indeterminate="selectedModule.powers.some((p) => isPowerGranted(selectedModule.b_id, p)) && !selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||||
@change="toggleAllPowers(selectedModule)"
|
||||
/>
|
||||
<span>全选操作权限</span>
|
||||
</label>
|
||||
<div class="perm-pow-list">
|
||||
<label
|
||||
v-for="pow in selectedModule.powers"
|
||||
:key="pow"
|
||||
class="perm-pow-item"
|
||||
>
|
||||
<a-checkbox
|
||||
:checked="isPowerGranted(selectedModule.b_id, pow)"
|
||||
@change="togglePower(selectedModule.b_id, pow)"
|
||||
/>
|
||||
<span>{{ pow }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="perm-detail-tip">
|
||||
勾选左侧「{{ selectedModule.b_name }}」以授权访问,再配置具体操作权限
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="perm-detail-empty">
|
||||
请选择左侧菜单
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</section>
|
||||
<section class="additive-section">
|
||||
<strong>附加授权</strong>
|
||||
<PermissionGrantEditor
|
||||
class="user-grants"
|
||||
:function-nodes="permissions.functionNodes.value"
|
||||
:page-nodes="permissions.pageNodes.value"
|
||||
:actions="permissions.actions.value"
|
||||
:function-ids="permissions.userFunctionIds.value"
|
||||
:page-ids="permissions.userPageIds.value"
|
||||
:action-ids="permissions.userActionIds.value"
|
||||
@update:function-ids="permissions.userFunctionIds.value = $event"
|
||||
@update:page-ids="permissions.userPageIds.value = $event"
|
||||
@update:action-ids="permissions.userActionIds.value = $event"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -442,207 +138,45 @@ async function handleSave() {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// ── Modal internal styles ──
|
||||
.demo-basic-info {
|
||||
padding-top: 4px;
|
||||
.user-permission-layout {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.demo-field-label {
|
||||
.role-section,
|
||||
.additive-section {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.role-section > strong,
|
||||
.additive-section > strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.demo-required {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.demo-flags-card {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.demo-card-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// ── Menu permission: left tree + right list ──
|
||||
.perm-layout {
|
||||
display: flex;
|
||||
height: 440px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 左侧:菜单树
|
||||
.perm-tree {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
padding: 6px 0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow-y: auto;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.tree-group-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f1f3;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-group-ic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tree-group-label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.tree-group-count {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.tree-group-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 8px 30px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #ebedf0;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #e6f0ff;
|
||||
|
||||
&:hover {
|
||||
background: #dcebff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// 右侧:操作权限列表
|
||||
.perm-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.perm-detail-hd {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.perm-detail-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.perm-detail-sub {
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.perm-detail-body {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.perm-detail-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.role-options {
|
||||
max-height: 92px;
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 6px;
|
||||
background: #f6f7f9;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.perm-pow-list {
|
||||
.role-options :deep(.ant-checkbox-group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.additive-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.perm-pow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-detail-tip {
|
||||
margin-top: 8px;
|
||||
padding: 36px 16px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
background: #fafbfc;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.perm-detail-empty {
|
||||
.user-grants {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #bbb;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user