Files
inquiry_robot/inquiry-backend/src/views/inquiry/role/index.vue
T

392 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>