基础项目
Some checks failed
CI / init (pull_request) Has been cancelled
CI / Frontend node 18.16.0 (pull_request) Has been cancelled
CI / Backend go (1.22) (pull_request) Has been cancelled
CI / release-pr (pull_request) Has been cancelled
CI / devops-test (1.22, 18.16.0) (pull_request) Has been cancelled
CI / release-please (pull_request) Has been cancelled
CI / devops-prod (1.22, 18.x) (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled

This commit is contained in:
2026-04-26 15:32:46 +08:00
parent 78520be803
commit 392fbea7eb
26 changed files with 2018 additions and 25 deletions

View File

@@ -0,0 +1,241 @@
/**
* 书籍后台管理菜单创建脚本
*
* 使用方式:
* 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] || ''
}
})()

View File

@@ -0,0 +1,157 @@
/**
* 书籍后台管理菜单角色授权脚本
*
* 使用方式:
* 1. 先执行 01-create-book-admin-menus.browser-console.js。
* 2. 登录后台管理前端,打开浏览器控制台。
* 3. 如需给多个角色授权,先设置角色 ID
* window.__XUANZHI_BOOK_AUTHORITY_IDS__ = [888, 8881]
* 4. 如前端接口代理不是 /api先执行
* window.__XUANZHI_BASE_API__ = 'http://127.0.0.1:8888'
* 5. 粘贴本文件全部内容并执行。
*
* 说明:
* - /menu/addMenuAuthority 是全量覆盖角色菜单。
* - 本脚本会先读取角色已有菜单,再合并书籍菜单,避免覆盖原有权限。
*/
(async () => {
const BASE_API = window.__XUANZHI_BASE_API__ || '/api'
const TOKEN = window.__XUANZHI_TOKEN__ || localStorage.getItem('token') || getCookie('x-token')
const AUTHORITY_IDS = window.__XUANZHI_BOOK_AUTHORITY_IDS__ || [888]
const TARGET_MENU_NAMES = [
'book',
'bookManage',
'bookChapterManage',
'bookAuthorManage',
'bookSeriesManage',
'bookCommentManage',
'bookReadRecordManage',
'bookFavoriteRecordManage',
'bookCommentLikeRecordManage'
]
let userId = window.__XUANZHI_USER_ID__ || localStorage.getItem('userId') || ''
if (!TOKEN) {
throw new Error('未找到 token请先登录后台或手动设置 window.__XUANZHI_TOKEN__')
}
if (!Array.isArray(AUTHORITY_IDS) || AUTHORITY_IDS.length === 0) {
throw new Error('请设置 window.__XUANZHI_BOOK_AUTHORITY_IDS__例如[888, 8881]')
}
userId = userId || await getCurrentUserId()
const allMenus = flattenMenus(await getBaseMenuTree())
const targetMenus = TARGET_MENU_NAMES.map((name) => {
const menu = allMenus.find((item) => item.name === name)
if (!menu) throw new Error(`未找到菜单 ${name},请先执行创建菜单脚本。`)
return menu
})
for (const authorityId of AUTHORITY_IDS) {
const currentMenus = await getMenuAuthority(authorityId)
const merged = mergeMenusById(currentMenus, targetMenus)
await apiPost('/menu/addMenuAuthority', {
authorityId,
menus: merged
})
console.log(`[authority] ${authorityId} 已授权书籍后台菜单,合并后菜单数量:${merged.length}`)
}
console.log('书籍后台菜单角色授权完成。刷新页面或重新登录后可查看菜单。')
async function getBaseMenuTree() {
const res = await apiPost('/menu/getBaseMenuTree', {})
return res.data?.menus || []
}
async function getMenuAuthority(authorityId) {
const res = await apiPost('/menu/getMenuAuthority', { authorityId })
return res.data?.menus || []
}
function mergeMenusById(currentMenus, targetMenus) {
const map = new Map()
;[...(currentMenus || []), ...(targetMenus || [])].forEach((item) => {
const id = getMenuId(item)
if (!id) return
map.set(Number(id), normalizeMenu(item))
})
return [...map.values()]
}
function normalizeMenu(item) {
return {
ID: getMenuId(item),
path: item.path,
name: item.name,
hidden: item.hidden,
parentId: item.parentId,
component: item.component,
sort: item.sort,
meta: item.meta,
parameters: item.parameters || [],
menuBtn: item.menuBtn || []
}
}
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] || ''
}
})()