task-49(账号/权限页面): 实现用户菜单授权数据加载
新增 user-menu-auth.ts 纯解析:parseMenuOptionList 归一 permission-menus 扁平项、
buildMenuOptionTree 按 parent_id 组装并按 (sort,key) 排序、parseUserGrantedColumnIds/
normalizeMenuIds 解包用户已授权 id 去重升序。user-menu-auth-api.ts 走 GET
/api/admin/permission-menus(menuType=admin) 与 /permission-users/{id}/columns 加载授权树与已勾选。
8 用例全过,355 单测 + build 绿。
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/** 用户菜单授权数据加载适配(任务 49):拉取可授权菜单树 + 用户已授权 id。 */
|
||||
import { http } from '@/api/http'
|
||||
import {
|
||||
ADMIN_MENU_TYPE,
|
||||
buildMenuOptionTree,
|
||||
parseMenuOptionList,
|
||||
parseUserGrantedColumnIds,
|
||||
type MenuOptionNode,
|
||||
} from './user-menu-auth'
|
||||
|
||||
export const GRANT_MENUS_ENDPOINT = '/api/admin/permission-menus'
|
||||
export const USER_PERMISSION_COLUMNS_ENDPOINT = '/api/admin/permission-users'
|
||||
|
||||
/** 拉取后台可授权菜单树(扁平列表组装嵌套、按 sort 排序)。 */
|
||||
export async function fetchGrantableMenus(): Promise<MenuOptionNode[]> {
|
||||
const { data } = await http.get<unknown>(GRANT_MENUS_ENDPOINT, { params: { menuType: ADMIN_MENU_TYPE } })
|
||||
return buildMenuOptionTree(parseMenuOptionList(data))
|
||||
}
|
||||
|
||||
/** 拉取某用户当前直接授权菜单 id 列表。 */
|
||||
export async function fetchUserGrantedColumnIds(userId: number): Promise<number[]> {
|
||||
const { data } = await http.get<unknown>(`${USER_PERMISSION_COLUMNS_ENDPOINT}/${userId}/columns`, {
|
||||
params: { menuType: ADMIN_MENU_TYPE },
|
||||
})
|
||||
return parseUserGrantedColumnIds(data)
|
||||
}
|
||||
|
||||
/** 编辑用户菜单授权所需数据:可选菜单树 + 已勾选 id。 */
|
||||
export interface UserMenuAuthData {
|
||||
tree: MenuOptionNode[]
|
||||
checkedIds: number[]
|
||||
}
|
||||
|
||||
export async function loadUserMenuAuth(userId: number): Promise<UserMenuAuthData> {
|
||||
const [tree, checkedIds] = await Promise.all([fetchGrantableMenus(), fetchUserGrantedColumnIds(userId)])
|
||||
return { tree, checkedIds }
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/** 用户菜单授权数据解析模型(任务 49):permission-menus 扁平项 → 嵌套树 + 用户已授权 id。 */
|
||||
import { unwrap } from '../../api/envelope.ts'
|
||||
|
||||
/** 后台 Vue 控制台菜单类型(与 /api/admin/current-user/menus 一致)。 */
|
||||
export const ADMIN_MENU_TYPE = 'admin'
|
||||
|
||||
/** 授权用可点选菜单节点(携带后端 numeric column id,用于提交授权)。 */
|
||||
export interface MenuOptionNode {
|
||||
id: number
|
||||
/** column_key,稳定权限标识;缺省用 id 字符串兜底。 */
|
||||
key: string
|
||||
name: string
|
||||
sort: number
|
||||
parentId: number | null
|
||||
children?: MenuOptionNode[]
|
||||
}
|
||||
|
||||
function numberOrNull(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
|
||||
}
|
||||
|
||||
/** 把单条 PermissionMenuItemVo(snake_case) 归一为授权节点;缺 id 视为无效。 */
|
||||
export function parsePermissionMenuItem(raw: unknown): MenuOptionNode | null {
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const record = raw as Record<string, unknown>
|
||||
const id = numberOrNull(record.id)
|
||||
if (id === null) return null
|
||||
const parentId = numberOrNull(record.parent_id)
|
||||
const sortRaw = numberOrNull(record.sort_order)
|
||||
const columnKey = typeof record.column_key === 'string' ? record.column_key.trim() : ''
|
||||
return {
|
||||
id,
|
||||
key: columnKey || String(id),
|
||||
name: typeof record.name === 'string' && record.name.trim() ? record.name.trim() : '未命名菜单',
|
||||
sort: sortRaw === null ? 0 : sortRaw,
|
||||
parentId: parentId === null ? null : parentId,
|
||||
}
|
||||
}
|
||||
|
||||
/** 归一化 permission-menus 响应(信封或裸数组)为扁平授权节点列表。 */
|
||||
export function parseMenuOptionList(payload: unknown): MenuOptionNode[] {
|
||||
const data = unwrap<unknown>(payload)
|
||||
if (!Array.isArray(data)) return []
|
||||
return data
|
||||
.map((raw) => parsePermissionMenuItem(raw))
|
||||
.filter((node): node is MenuOptionNode => node !== null)
|
||||
}
|
||||
|
||||
/** 按 parent_id 组装嵌套树;同级按 (sort,key) 升序,父缺失的节点按根处理。 */
|
||||
export function buildMenuOptionTree(items: MenuOptionNode[]): MenuOptionNode[] {
|
||||
const nodes = new Map<number, MenuOptionNode>()
|
||||
for (const item of items) {
|
||||
nodes.set(item.id, { ...item, children: [] })
|
||||
}
|
||||
const roots: MenuOptionNode[] = []
|
||||
for (const item of items) {
|
||||
const node = nodes.get(item.id)
|
||||
if (!node) continue
|
||||
const parent = item.parentId === null ? undefined : nodes.get(item.parentId)
|
||||
if (parent) {
|
||||
parent.children!.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
const order = (a: MenuOptionNode, b: MenuOptionNode) =>
|
||||
a.sort - b.sort || a.key.localeCompare(b.key)
|
||||
const sortRecursive = (nodes: MenuOptionNode[]) => {
|
||||
nodes.sort(order)
|
||||
for (const node of nodes) node.children?.length && sortRecursive(node.children)
|
||||
}
|
||||
sortRecursive(roots)
|
||||
for (const item of items) {
|
||||
const node = nodes.get(item.id)
|
||||
if (node && !node.children?.length) delete node.children
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
/** 解包用户已授权 columnIds(data.columnIds)→ 去重升序正整数。 */
|
||||
export function parseUserGrantedColumnIds(payload: unknown): number[] {
|
||||
const data = unwrap<unknown>(payload)
|
||||
const record = data && typeof data === 'object' ? (data as Record<string, unknown>) : null
|
||||
if (!record || !Array.isArray(record.columnIds)) return []
|
||||
return normalizeMenuIds(record.columnIds)
|
||||
}
|
||||
|
||||
/** 任意输入 id 集合归一为去重升序正整数(镜像后端 normalizeColumnIds)。 */
|
||||
export function normalizeMenuIds(raw: unknown): number[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
const ids = new Set<number>()
|
||||
for (const value of raw) {
|
||||
const id = numberOrNull(value)
|
||||
if (id !== null && id >= 1) ids.add(id)
|
||||
}
|
||||
return [...ids].sort((a, b) => a - b)
|
||||
}
|
||||
Reference in New Issue
Block a user