Files
crawler-plugin/admin-frontend-vue/src/pages/account/menu-manage-model.ts
T

289 lines
10 KiB
TypeScript
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.
/** 菜单管理 DTO/模型(任务 53):扁平 PermissionMenuItemVo → 可编辑节点 + 创建/更新请求体。 */
import { unwrap } from '../../api/envelope.ts'
/** 后台 Vue 控制台菜单类型。 */
export const ADMIN_MENU_TYPE = 'admin'
export interface MenuManageNode {
id: number
name: string
/** column_key,稳定权限标识。 */
columnKey: string
parentId: number | null
rootColumnKey?: string
menuType: string
routePath: string
sortOrder: number
createdAt?: string
children?: MenuManageNode[]
}
/** 菜单类型展示文案(对齐 admin.js:6824admin → 后台,其余一律 → 软件)。 */
export function menuTypeLabel(type: string | null | undefined): string {
return (type || '').toLowerCase() === 'admin' ? '后台' : '软件'
}
/** 菜单类型下拉选项(对齐 admin.html:5931-5934 后台/软件)。 */
export const MENU_TYPE_OPTIONS: Array<{ value: 'admin' | 'app'; label: string }> = [
{ value: 'admin', label: '后台' },
{ value: 'app', label: '软件' },
]
export interface MenuManageForm {
name: string
columnKey: string
parentId: number | null
menuType: string
routePath: string
sortOrder: number
}
/** 请求体字段与 Java PermissionMenuCreate/UpdateRequest 对齐(camelCase)。
* 权限标识/路由由后台自动生成,不随表单提交,故为可选字段。 */
export interface MenuManagePayload {
name: string
columnKey?: string
parentId: number | null
menuType: string
routePath?: string
sortOrder: number
}
export interface MenuManageFormErrors {
name?: string
columnKey?: string
menuType?: string
routePath?: string
}
function numberOrNull(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
}
function text(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
/** 排序归一:非负整数,非法回 0。 */
export function normalizeMenuSort(value: unknown): number {
const n = numberOrNull(value)
return n !== null && n >= 0 ? n : 0
}
/** 把单条 PermissionMenuItemVo(snake_case) 归一为可编辑节点;缺 id 视为无效。 */
export function parseMenuManageItem(raw: unknown): MenuManageNode | 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 node: MenuManageNode = {
id,
name: text(record.name),
columnKey: text(record.column_key),
parentId: parentId === null ? null : parentId,
menuType: text(record.menu_type) || ADMIN_MENU_TYPE,
routePath: text(record.route_path),
sortOrder: normalizeMenuSort(record.sort_order),
}
const rootColumnKey = text(record.root_column_key)
if (rootColumnKey) node.rootColumnKey = rootColumnKey
const createdAt = text(record.created_at)
if (createdAt) node.createdAt = createdAt
return node
}
/** 归一化 permission-menus 响应(信封或裸数组)为可编辑节点列表。 */
export function parseMenuManageList(payload: unknown): MenuManageNode[] {
const data = unwrap<unknown>(payload)
if (!Array.isArray(data)) return []
return data
.map((raw) => parseMenuManageItem(raw))
.filter((node): node is MenuManageNode => node !== null)
}
/** 按 parentId 组装展示树;同级按 (sortOrder,name) 升序,父缺失按根处理。 */
export function buildMenuManageTree(items: MenuManageNode[]): MenuManageNode[] {
const nodes = new Map<number, MenuManageNode>()
for (const item of items) {
nodes.set(item.id, { ...item, children: [] })
}
const roots: MenuManageNode[] = []
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: MenuManageNode, b: MenuManageNode) =>
a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)
const sortRecursive = (nodes: MenuManageNode[]) => {
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
}
/** 展平树为列表(前序),供父级下拉/统计复用。 */
export function flattenMenuNodes(nodes: MenuManageNode[]): MenuManageNode[] {
const out: MenuManageNode[] = []
const walk = (list: MenuManageNode[]) => {
for (const node of list) {
out.push(node)
if (node.children?.length) walk(node.children)
}
}
walk(nodes)
return out
}
/** 同级建议排序:该父下最大 sortOrder + 1;无同级返回 0。 */
export function nextSiblingSort(items: MenuManageNode[], parentId: number | null): number {
let max = -1
for (const item of items) {
if (item.parentId === parentId && item.sortOrder > max) max = item.sortOrder
}
return max + 1
}
/** 新建菜单表单工厂:指定父节点与建议排序,其余默认(menuType=admin)。 */
export function createMenuForm(parentId: number | null, sortOrder: number): MenuManageForm {
return {
...emptyMenuManageForm(),
parentId: typeof parentId === 'number' ? parentId : null,
sortOrder: normalizeMenuSort(sortOrder),
}
}
/** 编辑表单回填:从可编辑节点还原表单字段。 */
export function menuFormFromNode(node: MenuManageNode): MenuManageForm {
return {
name: node.name,
columnKey: node.columnKey,
parentId: node.parentId,
menuType: node.menuType,
routePath: node.routePath,
sortOrder: node.sortOrder,
}
}
/** 编辑时父节点防环校验:不可选自身或其子孙为父;返回可操作原因,否则 undefined。 */
export function menuParentChangeReason(node: MenuManageNode, parentId: number | null): string | undefined {
if (parentId === null || parentId === undefined) return undefined
if (parentId === node.id) return '不能选择自身作为父菜单'
const descendants = flattenMenuNodes(node.children ?? [])
if (descendants.some((item) => item.id === parentId)) return '不能选择自己的子菜单作为父菜单'
return undefined
}
/** 删除前置校验(镜像后端:仅叶子可删,避免孤儿层级);可删返回 undefined。 */
export function menuDeleteReason(node: MenuManageNode | null | undefined): string | undefined {
const children = node?.children
if (Array.isArray(children) && children.length > 0) return '请先删除该菜单下的子菜单'
return undefined
}
/** 同级拖拽/上下移:把 targetId 移动 dir 位(±1),返回新的同级 id 顺序;越界/缺失则原序。 */
export function siblingOrderAfterMove(group: number[], targetId: number, dir: number): number[] {
if (!Array.isArray(group) || group.length === 0) return []
const next = [...group]
const idx = next.indexOf(targetId)
if (idx < 0) return next
const to = idx + (dir === -1 || dir === 1 ? dir : 0)
if (to < 0 || to >= next.length) return next
const [moved] = next.splice(idx, 1)
next.splice(to, 0, moved)
return next
}
/** 拖拽分组键:同"菜单类型 + 父级"才算同级(对齐 admin.js columnSiblingGroupKey)。 */
export function menuSiblingGroupKey(node: MenuManageNode): string {
return `${node.menuType || 'app'}|${node.parentId ?? ''}`
}
/** 拖放排序:把 draggedId 插到 targetId 前/后,返回新的同级 id 顺序;任一 id 缺失或同 id 则原序。 */
export function siblingOrderAfterDrop(
group: number[],
draggedId: number,
targetId: number,
placeAfter: boolean,
): number[] {
if (!Array.isArray(group) || group.length === 0) return []
if (draggedId === targetId) return [...group]
const next = [...group]
const from = next.indexOf(draggedId)
if (from < 0) return next
const target = next.indexOf(targetId)
if (target < 0) return next
next.splice(from, 1)
const insertAt = next.indexOf(targetId) + (placeAfter ? 1 : 0)
next.splice(insertAt, 0, draggedId)
return next
}
/** 父菜单候选:仅同菜单类型的节点(对齐 admin 的类型联动父级下拉)。 */
export function menuParentCandidates(nodes: MenuManageNode[], menuType: string): MenuManageNode[] {
const type = (menuType || '').trim()
return flattenMenuNodes(nodes).filter((node) => (node.menuType || '') === type)
}
/** 同级重排请求体(对齐 admin.js persistColumnOrder{ menu_type, ordered_ids })。 */
export function toMenuReorderRequest(
menuType: string,
orderedIds: number[],
): { menu_type: string; ordered_ids: number[] } {
return { menu_type: text(menuType) || 'app', ordered_ids: dedupeOrderIds(orderedIds) }
}
function dedupeOrderIds(ids: unknown[]): number[] {
if (!Array.isArray(ids)) return []
const set = new Set<number>()
for (const v of ids) {
const n = numberOrNull(v)
if (n !== null && n > 0) set.add(n)
}
return [...set]
}
/** 新建/编辑表单空值工厂:默认菜单类型为 admin、排序 0、根父节点。 */
export function emptyMenuManageForm(): MenuManageForm {
return { name: '', columnKey: '', parentId: null, menuType: ADMIN_MENU_TYPE, routePath: '', sortOrder: 0 }
}
/** 表单校验(镜像后端 NotBlank 文案);返回逐字段错误。权限标识/路由由后台自动生成,不参与校验。 */
export function validateMenuManageForm(form: MenuManageForm): { valid: boolean; errors: MenuManageFormErrors } {
const errors: MenuManageFormErrors = {}
if (!text(form.name)) errors.name = '菜单名称不能为空'
if (!text(form.menuType)) errors.menuType = '菜单类型不能为空'
return { valid: Object.keys(errors).length === 0, errors }
}
function toMenuManagePayload(form: MenuManageForm): MenuManagePayload {
// 权限标识(column_key)/路由(route_path) 不随表单提交:由后台自动生成(创建)或保持原值(编辑)。
return {
name: text(form.name),
parentId: typeof form.parentId === 'number' ? form.parentId : null,
menuType: text(form.menuType),
sortOrder: normalizeMenuSort(form.sortOrder),
}
}
/** 表单 → POST /api/admin/permission-menus 请求体。 */
export function toMenuCreateRequest(form: MenuManageForm): MenuManagePayload {
return toMenuManagePayload(form)
}
/** 表单 → PUT /api/admin/permission-menus/{id} 请求体。 */
export function toMenuUpdateRequest(form: MenuManageForm): MenuManagePayload {
return toMenuManagePayload(form)
}