392 lines
12 KiB
Vue
392 lines
12 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 角色管理:对接 Jeecg /sys/role 与菜单权限树。
|
||
* 数据权限按 Excel 线路目录树勾选,写入角色 description 的 [dataPerms:…],不另造 RBAC。
|
||
*/
|
||
import { computed, onMounted, reactive, ref } from "vue";
|
||
import { message, Modal } from "ant-design-vue";
|
||
import {
|
||
deleteRole,
|
||
listRoles,
|
||
mapSysRole,
|
||
queryRolePermission,
|
||
queryTreeListForRole,
|
||
saveOrUpdateRole,
|
||
saveRolePermission,
|
||
type RoleRow,
|
||
} from "/@/views/inquiry/role/role.api";
|
||
import {
|
||
DATA_PERM_TREE,
|
||
SYSTEM_EXCEPTION_NOTIFY_KEY,
|
||
SYSTEM_EXCEPTION_NOTIFY_TITLE,
|
||
dataPermTitle,
|
||
leafDataPermKeys,
|
||
normalizeDataPermKeys,
|
||
} from "/@/views/inquiry/shared/dataPermCatalog";
|
||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||
import { joinPermissionIds } from "/@/views/inquiry/role/rolePermission";
|
||
|
||
const rows = ref<RoleRow[]>([]);
|
||
const loading = ref(false);
|
||
const menuTree = ref<any[]>([]);
|
||
const permTitleMap = ref<Record<string, string>>({});
|
||
const modalOpen = ref(false);
|
||
const editing = ref<RoleRow | null>(null);
|
||
const form = reactive({
|
||
name: "",
|
||
roleCode: "",
|
||
status: "active" as RoleRow["status"],
|
||
checkedKeys: [] as string[],
|
||
/** 打开弹窗时已授权菜单,供 saveRolePermission 增量对比 */
|
||
lastPermissionIds: [] as string[],
|
||
dataPerms: [] as string[],
|
||
/** 旧后台独立项:系统异常消息通知 */
|
||
systemExceptionNotify: false,
|
||
});
|
||
|
||
const dataPermTree = DATA_PERM_TREE;
|
||
const systemExceptionNotifyTitle = SYSTEM_EXCEPTION_NOTIFY_TITLE;
|
||
|
||
const modalTitle = computed(() => (editing.value ? "编辑角色" : "新增角色"));
|
||
const modalOkText = computed(() => "保存");
|
||
|
||
function walkTree(nodes: any[], map: Record<string, string>) {
|
||
for (const n of nodes || []) {
|
||
const key = String(n.key ?? n.id ?? "");
|
||
const title = n.slotTitle || n.title || n.name || key;
|
||
if (key) map[key] = title;
|
||
if (n.children?.length) walkTree(n.children, map);
|
||
}
|
||
}
|
||
|
||
function normalizeTree(nodes: any[]): any[] {
|
||
return (nodes || []).map((n) => ({
|
||
key: String(n.key ?? n.id ?? ""),
|
||
title: n.slotTitle || n.title || n.name || String(n.key ?? n.id ?? ""),
|
||
children: n.children?.length ? normalizeTree(n.children) : undefined,
|
||
}));
|
||
}
|
||
|
||
function parseDataPerms(desc: string | undefined): string[] {
|
||
if (!desc) return [];
|
||
const m = desc.match(/\[dataPerms:(.+?)\]/);
|
||
if (!m) return [];
|
||
return normalizeDataPermKeys(
|
||
m[1]
|
||
.split("|")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
);
|
||
}
|
||
|
||
function buildDescription(dataPerms: string[], systemExceptionNotify: boolean) {
|
||
const keys = leafDataPermKeys(dataPerms);
|
||
if (systemExceptionNotify && !keys.includes(SYSTEM_EXCEPTION_NOTIFY_KEY)) {
|
||
keys.push(SYSTEM_EXCEPTION_NOTIFY_KEY);
|
||
}
|
||
if (!keys.length) return "";
|
||
return `[dataPerms:${keys.join("|")}]`;
|
||
}
|
||
|
||
function displayDataPerms(keys: string[]): string[] {
|
||
return keys.map((k) => dataPermTitle(k));
|
||
}
|
||
|
||
function routeDataPermsOnly(keys: string[]): string[] {
|
||
return keys.filter((k) => k !== SYSTEM_EXCEPTION_NOTIFY_KEY);
|
||
}
|
||
|
||
async function loadMenuTree() {
|
||
const res: any = await queryTreeListForRole();
|
||
const tree = res?.treeList || res?.result?.treeList || res?.result || res || [];
|
||
const list = Array.isArray(tree) ? tree : [];
|
||
menuTree.value = normalizeTree(list);
|
||
const map: Record<string, string> = {};
|
||
walkTree(menuTree.value, map);
|
||
permTitleMap.value = map;
|
||
}
|
||
|
||
async function enrichMenus(row: RoleRow) {
|
||
if (!row.editable && row.roleCode.toLowerCase() === "admin") {
|
||
row.menus = ["全部菜单"];
|
||
return;
|
||
}
|
||
try {
|
||
const perms: any = await queryRolePermission({ roleId: row.id });
|
||
const ids = Array.isArray(perms) ? perms : perms?.result || [];
|
||
const titles = (ids as any[])
|
||
.map((id) => permTitleMap.value[String(id)])
|
||
.filter(Boolean);
|
||
row.menus = titles.length ? Array.from(new Set(titles)).slice(0, 12) : ["未授权菜单"];
|
||
} catch {
|
||
row.menus = ["—"];
|
||
}
|
||
}
|
||
|
||
async function loadList() {
|
||
loading.value = true;
|
||
try {
|
||
if (!menuTree.value.length) await loadMenuTree();
|
||
const res: any = await listRoles({ pageNo: 1, pageSize: 200 });
|
||
const records = res?.records || res?.result?.records || [];
|
||
const mapped = (records as any[]).map((r) => {
|
||
const row = mapSysRole(r);
|
||
row.dataPerms = parseDataPerms(r.description);
|
||
return row;
|
||
});
|
||
await Promise.all(mapped.map((r) => enrichMenus(r)));
|
||
// 超级管理员固定置顶,其余按创建时间倒序(接口顺序兜底)
|
||
rows.value = mapped.sort((a, b) => {
|
||
const aSuper = a.roleCode.toLowerCase() === "admin" || a.name === "超级管理员";
|
||
const bSuper = b.roleCode.toLowerCase() === "admin" || b.name === "超级管理员";
|
||
if (aSuper && !bSuper) return -1;
|
||
if (!aSuper && bSuper) return 1;
|
||
return String(b.createdAt).localeCompare(String(a.createdAt));
|
||
});
|
||
} catch (e: any) {
|
||
message.error(e?.message || "加载角色失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => loadList());
|
||
|
||
function openCreate() {
|
||
editing.value = null;
|
||
Object.assign(form, {
|
||
name: "",
|
||
roleCode: "",
|
||
status: "active",
|
||
checkedKeys: [],
|
||
lastPermissionIds: [],
|
||
dataPerms: [],
|
||
systemExceptionNotify: false,
|
||
});
|
||
modalOpen.value = true;
|
||
}
|
||
|
||
async function openEdit(row: RoleRow) {
|
||
editing.value = row;
|
||
const allPerms = [...row.dataPerms];
|
||
Object.assign(form, {
|
||
name: row.name,
|
||
roleCode: row.roleCode,
|
||
status: row.status,
|
||
checkedKeys: [] as string[],
|
||
lastPermissionIds: [] as string[],
|
||
dataPerms: routeDataPermsOnly(allPerms),
|
||
systemExceptionNotify: allPerms.includes(SYSTEM_EXCEPTION_NOTIFY_KEY),
|
||
});
|
||
try {
|
||
const perms: any = await queryRolePermission({ roleId: row.id });
|
||
const ids = (Array.isArray(perms) ? perms : perms?.result || []).map(String);
|
||
form.checkedKeys = ids;
|
||
form.lastPermissionIds = [...ids];
|
||
} catch {
|
||
form.checkedKeys = [];
|
||
form.lastPermissionIds = [];
|
||
}
|
||
modalOpen.value = true;
|
||
}
|
||
|
||
async function save() {
|
||
if (!form.name.trim()) {
|
||
message.warning("请填写角色名称");
|
||
return;
|
||
}
|
||
const roleCode =
|
||
form.roleCode.trim() ||
|
||
`inq_${form.name.trim().replace(/\s+/g, "_").toLowerCase()}_${Date.now().toString().slice(-4)}`;
|
||
try {
|
||
const saved: any = await saveOrUpdateRole(
|
||
{
|
||
id: editing.value?.id,
|
||
roleName: form.name.trim(),
|
||
roleCode,
|
||
description: buildDescription(form.dataPerms, form.systemExceptionNotify),
|
||
},
|
||
Boolean(editing.value),
|
||
);
|
||
const roleId = String(editing.value?.id || saved?.id || saved?.result?.id || "");
|
||
if (roleId) {
|
||
await saveRolePermission({
|
||
roleId,
|
||
permissionIds: joinPermissionIds(form.checkedKeys),
|
||
lastpermissionIds: form.lastPermissionIds.join(","),
|
||
});
|
||
}
|
||
message.success(editing.value ? "角色已更新" : "角色已创建");
|
||
modalOpen.value = false;
|
||
await loadList();
|
||
} catch (e: any) {
|
||
message.error(e?.message || "保存失败");
|
||
}
|
||
}
|
||
|
||
function removeRole(row: RoleRow) {
|
||
Modal.confirm({
|
||
title: `删除角色 ${row.name}?`,
|
||
okType: "danger",
|
||
async onOk() {
|
||
try {
|
||
await deleteRole({ id: row.id }, () => undefined);
|
||
message.success("已删除");
|
||
await loadList();
|
||
} catch (e: any) {
|
||
message.error(e?.message || "删除失败");
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
/** a-tree 可能回传对象或数组;统一成叶子 key 列表 */
|
||
function onDataPermCheck(checked: string[] | { checked: string[] }) {
|
||
const keys = Array.isArray(checked) ? checked : checked?.checked || [];
|
||
form.dataPerms = leafDataPermKeys(keys.map(String));
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport defer to="#page-topbar-actions">
|
||
<button type="button" class="btn btn-primary" @click="openCreate">
|
||
<ProtoIcon name="plus" :size="14" />新增角色
|
||
</button>
|
||
</Teleport>
|
||
|
||
<div class="section-card is-list-page">
|
||
<div class="table-wrap">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>角色名称</th>
|
||
<th>菜单权限</th>
|
||
<th>数据权限</th>
|
||
<th>创建时间</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="loading">
|
||
<td colspan="6" style="text-align: center; color: #94a3b8">加载中…</td>
|
||
</tr>
|
||
<tr v-else-if="!rows.length">
|
||
<td colspan="6" style="text-align: center; color: #94a3b8">暂无角色</td>
|
||
</tr>
|
||
<tr v-for="row in rows" :key="row.id">
|
||
<td style="font-weight: 800">{{ row.name }}</td>
|
||
<td>
|
||
<div class="role-perm-summary">
|
||
<span v-for="m in row.menus" :key="m" class="tag">{{ m }}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="role-perm-summary">
|
||
<span v-for="d in displayDataPerms(row.dataPerms)" :key="d" class="tag">{{ d }}</span>
|
||
<span v-if="!row.dataPerms.length" style="color: #94a3b8">—</span>
|
||
</div>
|
||
</td>
|
||
<td>{{ row.createdAt }}</td>
|
||
<td>
|
||
<span class="tag" :class="row.status === 'active' ? 'tag-green' : 'tag-rose'">
|
||
{{ row.status === "active" ? "启用" : "禁用" }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<template v-if="row.editable">
|
||
<div class="role-row-actions">
|
||
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
|
||
<button class="link-action link-danger" type="button" @click="removeRole(row)">
|
||
删除
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<span v-else style="color: #94a3b8">—</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<AdminModal
|
||
v-model:open="modalOpen"
|
||
:title="modalTitle"
|
||
size="lg"
|
||
:ok-text="modalOkText"
|
||
@ok="save"
|
||
>
|
||
<div class="admin-modal-field">
|
||
<label class="admin-modal-label">角色名称 <span class="req">*</span></label>
|
||
<input v-model="form.name" class="field" type="text" placeholder="请输入角色名称" />
|
||
</div>
|
||
|
||
<div class="admin-modal-field">
|
||
<label class="admin-modal-label">菜单权限 <span class="req">*</span></label>
|
||
<div class="perm-tree-box">
|
||
<a-tree
|
||
v-model:checkedKeys="form.checkedKeys"
|
||
checkable
|
||
default-expand-all
|
||
:tree-data="menuTree"
|
||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-modal-field">
|
||
<label class="admin-modal-label">数据权限</label>
|
||
<div class="perm-tree-box">
|
||
<a-tree
|
||
:checkedKeys="form.dataPerms"
|
||
checkable
|
||
default-expand-all
|
||
:tree-data="dataPermTree"
|
||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||
@check="onDataPermCheck"
|
||
/>
|
||
</div>
|
||
<label class="notify-check">
|
||
<input v-model="form.systemExceptionNotify" type="checkbox" />
|
||
{{ systemExceptionNotifyTitle }}
|
||
</label>
|
||
</div>
|
||
|
||
<div class="admin-modal-field">
|
||
<label class="admin-modal-label">状态</label>
|
||
<div class="status-toggle">
|
||
<button type="button" :class="{ 'is-selected': form.status === 'active' }" @click="form.status = 'active'">
|
||
启用
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ 'is-selected': form.status === 'disabled' }"
|
||
@click="form.status = 'disabled'"
|
||
>
|
||
禁用
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</AdminModal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.role-row-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.notify-check {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
color: #0f172a;
|
||
cursor: pointer;
|
||
}
|
||
</style>
|