/** * 书籍后台管理菜单创建脚本 * * 使用方式: * 1. 登录后台管理前端。 * 2. 打开浏览器控制台。 * 3. 如前端接口代理不是 /api,先执行: * window.__XUANZHI_BASE_API__ = 'http://127.0.0.1:8888' * 4. 粘贴本文件全部内容并执行。 * * 说明: * - 脚本会先读取现有菜单树,已存在的菜单不会重复创建。 * - /menu/addBaseMenu 不返回新菜单 ID,所以每次创建后会重新读取菜单树定位 ID。 */ (async () => { const BASE_API = window.__XUANZHI_BASE_API__ || '/api' const TOKEN = window.__XUANZHI_TOKEN__ || localStorage.getItem('token') || getCookie('x-token') let userId = window.__XUANZHI_USER_ID__ || localStorage.getItem('userId') || '' const menus = [ { key: 'bookRoot', parentKey: null, path: 'book', name: 'book', component: 'view/book/index.vue', sort: 60, hidden: false, meta: { title: '书籍管理', icon: 'Reading', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookManage', parentKey: 'bookRoot', path: 'bookManage', name: 'bookManage', component: 'view/book/book/index.vue', sort: 10, hidden: false, meta: { title: '书籍管理', icon: 'Notebook', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookChapterManage', parentKey: 'bookRoot', path: 'bookChapterManage', name: 'bookChapterManage', component: 'view/book/chapter/index.vue', sort: 20, hidden: false, meta: { title: '章节管理', icon: 'Tickets', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookAuthorManage', parentKey: 'bookRoot', path: 'bookAuthorManage', name: 'bookAuthorManage', component: 'view/book/author/index.vue', sort: 30, hidden: false, meta: { title: '作者管理', icon: 'User', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookSeriesManage', parentKey: 'bookRoot', path: 'bookSeriesManage', name: 'bookSeriesManage', component: 'view/book/series/index.vue', sort: 40, hidden: false, meta: { title: '系列管理', icon: 'Collection', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookCommentManage', parentKey: 'bookRoot', path: 'bookCommentManage', name: 'bookCommentManage', component: 'view/book/comment/index.vue', sort: 50, hidden: false, meta: { title: '评论管理', icon: 'ChatLineSquare', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookReadRecordManage', parentKey: 'bookRoot', path: 'bookReadRecordManage', name: 'bookReadRecordManage', component: 'view/book/readRecord/index.vue', sort: 60, hidden: false, meta: { title: '阅读记录管理', icon: 'View', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookFavoriteRecordManage', parentKey: 'bookRoot', path: 'bookFavoriteRecordManage', name: 'bookFavoriteRecordManage', component: 'view/book/favoriteRecord/index.vue', sort: 70, hidden: false, meta: { title: '收藏记录管理', icon: 'Star', keepAlive: false, closeTab: false, defaultMenu: false } }, { key: 'bookCommentLikeRecordManage', parentKey: 'bookRoot', path: 'bookCommentLikeRecordManage', name: 'bookCommentLikeRecordManage', component: 'view/book/commentLikeRecord/index.vue', sort: 80, hidden: false, meta: { title: '评论点赞记录管理', icon: 'Pointer', keepAlive: false, closeTab: false, defaultMenu: false } } ] if (!TOKEN) { throw new Error('未找到 token,请先登录后台,或手动设置 window.__XUANZHI_TOKEN__') } userId = userId || await getCurrentUserId() const createdOrFound = new Map() for (const item of menus) { const parentId = item.parentKey ? getMenuId(createdOrFound.get(item.parentKey)) : 0 const menu = await ensureMenu({ ...item, parentId }) createdOrFound.set(item.key, menu) console.log(`[menu] ${item.meta.title} -> ID=${getMenuId(menu)} (${menu.component || item.component})`) } const result = Object.fromEntries( [...createdOrFound.entries()].map(([key, menu]) => [key, getMenuId(menu)]) ) localStorage.setItem('xuanzhi-book-menu-ids', JSON.stringify(result)) console.log('书籍后台菜单创建/确认完成,菜单 ID 已写入 localStorage.xuanzhi-book-menu-ids:', result) console.log('下一步:执行 02-assign-book-admin-menus-to-roles.browser-console.js 给角色分配菜单。') async function ensureMenu(menu) { const existed = await findMenu(menu) if (existed) return existed const payload = { path: menu.path, name: menu.name, hidden: menu.hidden, parentId: menu.parentId, component: menu.component, sort: menu.sort, meta: menu.meta, parameters: [], menuBtn: [] } await apiPost('/menu/addBaseMenu', payload) const created = await findMenu(menu) if (!created) { throw new Error(`菜单已提交但未在菜单树中找到:${menu.name} / ${menu.component}`) } return created } async function findMenu(menu) { const tree = await getBaseMenuTree() const flat = flattenMenus(tree) return flat.find((item) => item.name === menu.name || item.path === menu.path || item.component === menu.component ) } async function getBaseMenuTree() { const res = await apiPost('/menu/getBaseMenuTree', {}) return res.data?.menus || [] } async function getCurrentUserId() { try { const res = await apiGet('/user/getUserInfo') return res.data?.userInfo?.ID || res.data?.userInfo?.id || '' } catch (error) { console.warn('读取当前用户 ID 失败,将不带 x-user-id 调用菜单接口。必要时可手动设置 window.__XUANZHI_USER_ID__。', error) return '' } } async function apiGet(path) { return apiRequest(path, { method: 'GET' }) } async function apiPost(path, data) { return apiRequest(path, { method: 'POST', body: JSON.stringify(data) }) } async function apiRequest(path, options = {}) { const res = await fetch(`${BASE_API}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'x-token': TOKEN, 'x-user-id': String(userId || ''), ...(options.headers || {}) } }) const json = await res.json().catch(() => ({})) if (!res.ok || json.code !== 0) { throw new Error(`${path} 调用失败:${json.msg || res.statusText}`) } return json } function flattenMenus(list) { const result = [] const walk = (items) => { ;(items || []).forEach((item) => { result.push(item) walk(item.children) }) } walk(list) return result } function getMenuId(menu) { return menu?.ID || menu?.id || menu?.menuId } function getCookie(name) { return document.cookie .split('; ') .find((row) => row.startsWith(`${name}=`)) ?.split('=')[1] || '' } })()