基础项目
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
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:
@@ -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] || ''
|
||||
}
|
||||
})()
|
||||
@@ -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] || ''
|
||||
}
|
||||
})()
|
||||
Reference in New Issue
Block a user