feat: 重建 Bell GoAdmin 产品骨架 (#62)

This commit is contained in:
QiuSW
2026-08-27 18:20:40 +08:00
parent b37c350096
commit f5a3dd796b
715 changed files with 75278 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop',
size: Cookies.get('size') || 'medium'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
},
SET_SIZE: (state, size) => {
state.size = size
Cookies.set('size', size)
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
},
setSize({ commit }, size) {
commit('SET_SIZE', size)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+28
View File
@@ -0,0 +1,28 @@
const state = {
logs: []
}
const mutations = {
ADD_ERROR_LOG: (state, log) => {
state.logs.push(log)
},
CLEAR_ERROR_LOG: (state) => {
state.logs.splice(0)
}
}
const actions = {
addErrorLog({ commit }, log) {
commit('ADD_ERROR_LOG', log)
},
clearErrorLog({ commit }) {
commit('CLEAR_ERROR_LOG')
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+186
View File
@@ -0,0 +1,186 @@
import { constantRoutes } from '@/router'
import { getRoutes } from '@/api/admin/sys-role'
import Layout from '@/layout'
// import sysuserindex from '@/views/sysuser/index'
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* Use names to determine if the current user has permission
* @param names
* @param route
*/
function hasPathPermission(paths, route) {
if (route.path) {
return paths.some(path => route.path === path.path)
} else {
return true
}
}
/**
* 后台查询的菜单数据拼装成路由格式的数据
* @param routes
*/
export function generaMenu(routes, data) {
data.forEach(item => {
const menu = {
path: item.path,
component: item.component === 'Layout' ? Layout : loadView(item.component),
// eslint-disable-next-line eqeqeq
hidden: item.visible != '0',
children: [],
name: item.menuName,
meta: {
title: item.title,
icon: item.icon,
noCache: item.noCache
}
}
if (item.children) {
generaMenu(menu.children, item.children)
}
routes.push(menu)
})
}
// 使用 require.context 预建 views 目录索引,
// 绕过 babel-plugin-dynamic-import-node 在开发模式下将 import() 转换为
// 同步 require() 导致 webpack 上下文失效的问题。
const viewsContext = require.context('@/views', true, /\.vue$/)
export const loadView = (view) => { // 路由懒加载
const key = `.${view}.vue`
if (viewsContext.keys().includes(key)) {
// require.context 返回的是 { default: Component, __esModule: true },
// Vue Router 4 需要直接拿到组件对象(.default),否则渲染空白
const mod = viewsContext(key)
return mod.default || mod
}
// 兜底:动态 import(生产构建不受 dynamic-import-node 影响)
return () => import(`@/views${view}.vue`)
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param components
*/
export function filterAsyncPathRoutes(routes, paths) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPathPermission(paths, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncPathRoutes(tmp.children, paths)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: [],
defaultRoutes: [],
topbarRouters: [],
sidebarRouters: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
},
SET_DEFAULT_ROUTES: (state, routes) => {
state.defaultRoutes = constantRoutes.concat(routes)
},
SET_TOPBAR_ROUTES: (state, routes) => {
// 顶部导航菜单默认添加统计报表栏指向首页
// const index = [{
// path: 'dashboard',
// meta: { title: '统计报表', icon: 'dashboard' }
// }]
state.topbarRouters = routes // .concat(index)
},
SET_SIDEBAR_ROUTERS: (state, routes) => {
state.sidebarRouters = routes
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
const loadMenuData = []
getRoutes().then(response => {
// console.log(JSON.stringify(response))
let data = response
if (response.code !== 200) {
this.$message({
message: '菜单数据加载异常',
type: 0
})
} else {
data = response.data
Object.assign(loadMenuData, data)
const dynamicRoutes = []
generaMenu(dynamicRoutes, loadMenuData)
dynamicRoutes.push({ path: '/:pathMatch(.*)*', redirect: '/', hidden: true })
commit('SET_ROUTES', dynamicRoutes)
const sidebarRoutes = []
generaMenu(sidebarRoutes, loadMenuData)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(dynamicRoutes)
}
}).catch(error => {
console.log(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+37
View File
@@ -0,0 +1,37 @@
import variables from '@/styles/element-variables.scss'
import defaultSettings from '@/settings'
const { showSettings, topNav, tagsView, fixedHeader, sidebarLogo, themeStyle } = defaultSettings
const state = {
theme: variables.theme,
showSettings: showSettings,
topNav: topNav,
tagsView: tagsView,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo,
themeStyle: themeStyle
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
// eslint-disable-next-line no-prototype-builtins
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+32
View File
@@ -0,0 +1,32 @@
import { getSetting } from '@/api/login'
import storage from '@/utils/storage'
const state = {
info: storage.get('app_info') || {}
}
const mutations = {
SET_INFO: (state, data) => {
state.info = data
storage.set('app_info', data)
}
}
const actions = {
settingDetail({ commit }) {
return new Promise((resolve, reject) => {
getSetting().then(response => {
commit('SET_INFO', response.data)
resolve(response.data)
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+162
View File
@@ -0,0 +1,162 @@
const state = {
visitedViews: [],
cachedViews: []
}
const mutations = {
ADD_VISITED_VIEW: (state, view) => {
if (state.visitedViews.some(v => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
ADD_CACHED_VIEW: (state, view) => {
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
},
DEL_VISITED_VIEW: (state, view) => {
for (const [i, v] of state.visitedViews.entries()) {
if (v.path === view.path) {
state.visitedViews.splice(i, 1)
break
}
}
},
DEL_CACHED_VIEW: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
DEL_OTHERS_VISITED_VIEWS: (state, view) => {
state.visitedViews = state.visitedViews.filter(v => {
return v.meta.affix || v.path === view.path
})
},
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
if (state.cachedViews.length > 0) {
const index = state.cachedViews.indexOf(view.name)
if (index > -1) {
state.cachedViews = state.cachedViews.slice(index, index + 1)
} else {
// if index = -1, there is no cached tags
state.cachedViews = []
}
}
},
DEL_ALL_VISITED_VIEWS: state => {
// keep affix tags
const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
state.visitedViews = affixTags
},
DEL_ALL_CACHED_VIEWS: state => {
state.cachedViews = []
},
UPDATE_VISITED_VIEW: (state, view) => {
for (let v of state.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
}
}
const actions = {
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delVisitedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_VISITED_VIEW', view)
resolve([...state.visitedViews])
})
},
delCachedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delOthersVisitedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_VISITED_VIEWS', view)
resolve([...state.visitedViews])
})
},
delOthersCachedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delAllVisitedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_VISITED_VIEWS')
resolve([...state.visitedViews])
})
},
delAllCachedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+153
View File
@@ -0,0 +1,153 @@
import { login, logout, getInfo, refreshtoken } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'
import storage from '@/utils/storage'
const state = {
token: getToken(),
name: '',
avatar: '',
introduction: '',
roles: [],
permissions: [],
permisaction: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
if (avatar.indexOf('http') !== -1) {
state.avatar = avatar
} else {
state.avatar = process.env.VUE_APP_BASE_API + avatar
}
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_PERMISSIONS: (state, permisaction) => {
state.permisaction = permisaction
}
}
const actions = {
// user login
login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const { token } = response
commit('SET_TOKEN', token)
setToken(token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
if (!response || !response.data) {
commit('SET_TOKEN', '')
removeToken()
return resolve()
}
const { roles, name, avatar, introduction, permissions } = response.data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
return reject('getInfo: roles must be a non-null array!')
}
commit('SET_PERMISSIONS', permissions)
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
storage.clear()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 刷新token
refreshToken({ commit, state }) {
return new Promise((resolve, reject) => {
refreshtoken({ token: state.token }).then(response => {
const { token } = response
commit('SET_TOKEN', token)
setToken(token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
},
// dynamically modify permissions
changeRoles({ commit, dispatch }, role) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const token = role + '-token'
commit('SET_TOKEN', token)
setToken(token)
const { roles } = await dispatch('getInfo')
resetRouter()
// generate accessible routes map based on roles
const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
// dynamically add accessible routes
accessRoutes.forEach(route => router.addRoute(route))
// reset visited views and cached views
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}