Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2025-12-29 03:06:49 +08:00
commit f43312a858
292 changed files with 103739 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import { mapState } from 'vuex'
const baseMixin = {
computed: {
...mapState({
layout: state => state.app.layout,
navTheme: state => state.app.theme,
primaryColor: state => state.app.color,
colorWeak: state => state.app.weak,
fixedHeader: state => state.app.fixedHeader,
fixedSidebar: state => state.app.fixedSidebar,
contentWidth: state => state.app.contentWidth,
autoHideHeader: state => state.app.autoHideHeader,
isMobile: state => state.app.isMobile,
sideCollapsed: state => state.app.sideCollapsed,
multiTab: state => state.app.multiTab
}),
isTopMenu () {
return this.layout === 'topmenu'
}
},
methods: {
isSideMenu () {
return !this.isTopMenu
}
}
}
export {
baseMixin
}
+11
View File
@@ -0,0 +1,11 @@
import { mapState } from 'vuex'
const deviceMixin = {
computed: {
...mapState({
isMobile: state => state.app.isMobile
})
}
}
export { deviceMixin }
+16
View File
@@ -0,0 +1,16 @@
const getters = {
isMobile: state => state.app.isMobile,
lang: state => state.app.lang,
theme: state => state.app.theme,
color: state => state.app.color,
token: state => state.user.token,
avatar: state => state.user.avatar,
nickname: state => state.user.name,
welcome: state => state.user.welcome,
roles: state => state.user.roles,
userInfo: state => state.user.info,
addRouters: state => state.permission.addRouters,
multiTab: state => state.app.multiTab
}
export default getters
+16
View File
@@ -0,0 +1,16 @@
import { mapState } from 'vuex'
const i18nMixin = {
computed: {
...mapState({
currentLang: state => state.app.lang
})
},
methods: {
setLang (lang) {
this.$store.dispatch('setLang', lang)
}
}
}
export default i18nMixin
+29
View File
@@ -0,0 +1,29 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
// default router permission control
// 默认路由模式为静态路由 (router.config.js)
import permission from './modules/static-router'
// dynamic router permission control (Experimental)
// 动态路由模式(api请求后端生成)
// import permission from './modules/async-router'
import getters from './getters'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
app,
user,
permission
},
state: {},
mutations: {},
actions: {},
getters
})
+99
View File
@@ -0,0 +1,99 @@
import storage from 'store'
import {
SIDEBAR_TYPE,
TOGGLE_MOBILE_TYPE,
TOGGLE_NAV_THEME,
TOGGLE_LAYOUT,
TOGGLE_FIXED_HEADER,
TOGGLE_FIXED_SIDEBAR,
TOGGLE_CONTENT_WIDTH,
TOGGLE_HIDE_HEADER,
TOGGLE_COLOR,
TOGGLE_WEAK,
TOGGLE_MULTI_TAB,
// i18n
APP_LANGUAGE
} from '@/store/mutation-types'
import { loadLanguageAsync } from '@/locales'
const app = {
state: {
sideCollapsed: false,
isMobile: false,
theme: storage.get(TOGGLE_NAV_THEME, 'light'), // 从 localStorage 读取主题,默认 'light'
layout: '',
contentWidth: '',
fixedHeader: false,
fixedSidebar: false,
autoHideHeader: false,
color: storage.get(TOGGLE_COLOR, '#13C2C2'), // 从 localStorage 读取主题色,默认 '#13C2C2'
weak: false,
multiTab: true,
lang: 'en-US',
_antLocale: {}
},
mutations: {
[SIDEBAR_TYPE]: (state, type) => {
state.sideCollapsed = type
storage.set(SIDEBAR_TYPE, type)
},
[TOGGLE_MOBILE_TYPE]: (state, isMobile) => {
state.isMobile = isMobile
},
[TOGGLE_NAV_THEME]: (state, theme) => {
state.theme = theme
storage.set(TOGGLE_NAV_THEME, theme)
},
[TOGGLE_LAYOUT]: (state, mode) => {
state.layout = mode
storage.set(TOGGLE_LAYOUT, mode)
},
[TOGGLE_FIXED_HEADER]: (state, mode) => {
state.fixedHeader = mode
storage.set(TOGGLE_FIXED_HEADER, mode)
},
[TOGGLE_FIXED_SIDEBAR]: (state, mode) => {
state.fixedSidebar = mode
storage.set(TOGGLE_FIXED_SIDEBAR, mode)
},
[TOGGLE_CONTENT_WIDTH]: (state, type) => {
state.contentWidth = type
storage.set(TOGGLE_CONTENT_WIDTH, type)
},
[TOGGLE_HIDE_HEADER]: (state, type) => {
state.autoHideHeader = type
storage.set(TOGGLE_HIDE_HEADER, type)
},
[TOGGLE_COLOR]: (state, color) => {
state.color = color
storage.set(TOGGLE_COLOR, color)
},
[TOGGLE_WEAK]: (state, mode) => {
state.weak = mode
storage.set(TOGGLE_WEAK, mode)
},
[APP_LANGUAGE]: (state, lang, antd = {}) => {
state.lang = lang
state._antLocale = antd
storage.set(APP_LANGUAGE, lang)
},
[TOGGLE_MULTI_TAB]: (state, bool) => {
storage.set(TOGGLE_MULTI_TAB, bool)
state.multiTab = bool
}
},
actions: {
setLang ({ commit }, lang) {
return new Promise((resolve, reject) => {
commit(APP_LANGUAGE, lang)
loadLanguageAsync(lang).then(() => {
resolve()
}).catch((e) => {
reject(e)
})
})
}
}
}
export default app
@@ -0,0 +1,33 @@
/**
* 向后端请求用户的菜单,动态生成路由
*/
import { constantRouterMap } from '@/config/router.config'
import { generatorDynamicRouter } from '@/router/generator-routers'
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes ({ commit }, data) {
return new Promise((resolve, reject) => {
const { token } = data
generatorDynamicRouter(token).then(routers => {
commit('SET_ROUTERS', routers)
resolve()
}).catch(e => {
reject(e)
})
})
}
}
}
export default permission
@@ -0,0 +1,54 @@
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
import cloneDeep from 'lodash.clonedeep'
/**
* 单账户多角色时,使用该方法可过滤角色不存在的菜单
*
* @param roles
* @param route
* @returns {*}
*/
// eslint-disable-next-line
function hasRole(roles, route) {
if (route.meta && route.meta.roles) {
return route.meta.roles.includes(roles.id)
} else {
return true
}
}
function filterAsyncRouter (routerMap, role) {
// 不进行权限过滤,直接返回所有路由(后端会验证token)
return routerMap.map(route => {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, role)
}
return route
})
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes ({ commit }, data) {
return new Promise(resolve => {
const routerMap = cloneDeep(asyncRouterMap)
// 不进行权限过滤,直接返回所有路由(后端会验证token)
const accessedRouters = filterAsyncRouter(routerMap, null)
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
}
}
export default permission
+410
View File
@@ -0,0 +1,410 @@
import storage from 'store'
import expirePlugin from 'store/plugins/expire'
import { login, emailLogin, mobileLogin, logout, getUserInfo, apiLogout, updateUserInfo, oauthCallback } from '@/api/login'
import { ACCESS_TOKEN, USER_INFO, USER_ROLES } from '@/store/mutation-types'
import { welcome } from '@/utils/util'
storage.addPlugin(expirePlugin)
const DEFAULT_ROLE = { id: 'default', permissionList: [] }
function normalizeRoles (roles) {
if (!roles) return []
if (Array.isArray(roles)) return roles
return [roles]
}
function getStoredInfo () {
const info = storage.get(USER_INFO) || {}
return (info && typeof info === 'object') ? info : {}
}
function getStoredRoles () {
const roles = storage.get(USER_ROLES) || []
return normalizeRoles(roles)
}
function getStoredToken () {
const token = storage.get(ACCESS_TOKEN)
return typeof token === 'string' ? token : (token && token.token) ? token.token : token
}
const initialInfo = getStoredInfo()
const initialRoles = getStoredRoles()
const initialToken = getStoredToken() || ''
const initialName = initialInfo.nickname || initialInfo.username || ''
const initialAvatar = initialInfo.avatar || ''
const initialWelcome = initialName ? welcome() : ''
const user = {
state: {
token: initialToken,
name: initialName,
welcome: initialWelcome,
avatar: initialAvatar,
roles: initialRoles,
info: initialInfo
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, { name, welcome }) => {
state.name = name
state.welcome = welcome
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_INFO: (state, info) => {
state.info = info
}
},
actions: {
// 登录
Login ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
// 适配 Python 后端响应格式
if (response && response.code === 1 && response.data) {
const result = response.data
const token = result.token
const info = result.userinfo || {}
const expiresAt = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
storage.set(ACCESS_TOKEN, token, expiresAt)
commit('SET_TOKEN', token)
commit('SET_INFO', info)
storage.set(USER_INFO, info, expiresAt)
// 设置基本信息
const name = info.nickname || info.username || 'User'
commit('SET_NAME', { name: name, welcome: welcome() })
commit('SET_AVATAR', info.avatar || '/avatar2.jpg')
// 设置默认角色,防止路由鉴权失败
const roles = [{ id: 'admin', permissionList: ['dashboard', 'exception', 'account'] }]
commit('SET_ROLES', roles)
storage.set(USER_ROLES, roles, expiresAt)
resolve(response)
} else {
reject(new Error((response && response.msg) || 'Login failed'))
}
}).catch(error => {
reject(error)
})
})
},
// 邮箱验证码登录
EmailLogin ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
emailLogin(userInfo).then(response => {
// 新API响应格式: { code: 1, msg: "登录成功", data: { token, userInfo } }
if (response.code === 1 && response.data) {
const token = response.data.token
storage.set(ACCESS_TOKEN, token, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', token)
// 保存用户信息(从登录接口返回的 userInfo)
if (response.data.userInfo) {
const userInfoData = response.data.userInfo
commit('SET_INFO', userInfoData)
// 设置用户名
if (userInfoData.nickname) {
commit('SET_NAME', { name: userInfoData.nickname, welcome: welcome() })
} else if (userInfoData.username) {
commit('SET_NAME', { name: userInfoData.username, welcome: welcome() })
}
// 设置头像
if (userInfoData.avatar) {
commit('SET_AVATAR', userInfoData.avatar)
}
// 设置角色(如果有)
if (userInfoData.role) {
commit('SET_ROLES', userInfoData.role)
} else if (userInfoData.roles) {
commit('SET_ROLES', userInfoData.roles)
} else {
// 如果没有角色信息,设置一个默认角色对象,避免路由守卫卡住
// 设置一个标记,表示已经初始化过用户信息
commit('SET_ROLES', [{ id: 'default', permissionList: [] }])
}
}
resolve(response)
} else {
reject(new Error(response.msg || '登录失败'))
}
}).catch(error => {
reject(error)
})
})
},
// 手机号验证码登录
MobileLogin ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
mobileLogin(userInfo).then(response => {
// 新API响应格式: { code: 1, msg: "登录成功", data: { token, userInfo } }
if (response.code === 1 && response.data) {
const token = response.data.token
storage.set(ACCESS_TOKEN, token, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', token)
// 保存用户信息(从登录接口返回的 userInfo)
if (response.data.userInfo) {
const userInfoData = response.data.userInfo
commit('SET_INFO', userInfoData)
// 设置用户名
if (userInfoData.nickname) {
commit('SET_NAME', { name: userInfoData.nickname, welcome: welcome() })
} else if (userInfoData.username) {
commit('SET_NAME', { name: userInfoData.username, welcome: welcome() })
}
// 设置头像
if (userInfoData.avatar) {
commit('SET_AVATAR', userInfoData.avatar)
}
// 设置角色(如果有)
if (userInfoData.role) {
commit('SET_ROLES', userInfoData.role)
} else if (userInfoData.roles) {
commit('SET_ROLES', userInfoData.roles)
} else {
// 如果没有角色信息,设置一个默认角色对象,避免路由守卫卡住
// 设置一个标记,表示已经初始化过用户信息
commit('SET_ROLES', [{ id: 'default', permissionList: [] }])
}
}
resolve(response)
} else {
reject(new Error(response.msg || '登录失败'))
}
}).catch(error => {
reject(error)
})
})
},
// Web3 登录完成后的统一处理
Web3LoginFinalize ({ commit }, payload) {
return new Promise((resolve, reject) => {
try {
const { token, userInfo } = payload
if (!token || !userInfo) {
reject(new Error('登录数据异常'))
return
}
storage.set(ACCESS_TOKEN, token, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', token)
commit('SET_INFO', userInfo)
if (userInfo.nickname) {
commit('SET_NAME', { name: userInfo.nickname, welcome: welcome() })
} else if (userInfo.username) {
commit('SET_NAME', { name: userInfo.username, welcome: welcome() })
}
if (userInfo.avatar) {
commit('SET_AVATAR', userInfo.avatar)
}
if (userInfo.role) {
commit('SET_ROLES', userInfo.role)
} else if (userInfo.roles) {
commit('SET_ROLES', userInfo.roles)
} else {
commit('SET_ROLES', [{ id: 'default', permissionList: [] }])
}
resolve()
} catch (e) {
reject(e)
}
})
},
// 刷新用户信息
FetchUserInfo ({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo().then(res => {
if (res && res.code === 1 && res.data) {
const info = res.data
commit('SET_INFO', info)
if (info.nickname) {
commit('SET_NAME', { name: info.nickname, welcome: welcome() })
} else if (info.username) {
commit('SET_NAME', { name: info.username, welcome: welcome() })
}
if (info.avatar) {
commit('SET_AVATAR', info.avatar)
}
resolve(info)
} else {
reject(new Error((res && res.msg) || '获取用户信息失败'))
}
}).catch(err => reject(err))
})
},
// 更新用户基本信息
UpdateUserInfo ({ commit, state }, payload) {
return new Promise((resolve, reject) => {
updateUserInfo(payload).then(res => {
if (res && res.code === 1) {
// 合并本地 store 的 info
const newInfo = { ...state.info, ...payload }
commit('SET_INFO', newInfo)
if (newInfo.nickname) {
commit('SET_NAME', { name: newInfo.nickname, welcome: welcome() })
}
resolve(res)
} else {
reject(new Error((res && res.msg) || '修改失败'))
}
}).catch(err => reject(err))
})
},
// 获取用户信息(从 store 中获取,不再请求接口)
GetInfo ({ commit, state }) {
return new Promise((resolve, reject) => {
// 用户信息已经在登录时保存到 store 中,直接返回
if (state.info && Object.keys(state.info).length > 0) {
// 补全 Roles
const info = state.info
if (info.role) {
const roles = normalizeRoles(info.role)
commit('SET_ROLES', roles)
storage.set(USER_ROLES, roles, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
} else if (info.roles) {
const roles = normalizeRoles(info.roles)
commit('SET_ROLES', roles)
storage.set(USER_ROLES, roles, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
} else {
commit('SET_ROLES', [DEFAULT_ROLE])
storage.set(USER_ROLES, [DEFAULT_ROLE], new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
}
resolve(state.info)
} else {
// 尝试主动拉取一次
getUserInfo().then(res => {
if (res && res.code === 1 && res.data) {
const info = res.data
commit('SET_INFO', info)
storage.set(USER_INFO, info, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
if (info.nickname) {
commit('SET_NAME', { name: info.nickname, welcome: welcome() })
} else if (info.username) {
commit('SET_NAME', { name: info.username, welcome: welcome() })
}
if (info.avatar) {
commit('SET_AVATAR', info.avatar)
}
// 关键修复:设置角色,防止路由守卫死循环
if (info.role) {
const roles = normalizeRoles(info.role)
commit('SET_ROLES', roles)
storage.set(USER_ROLES, roles, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
} else if (info.roles) {
const roles = normalizeRoles(info.roles)
commit('SET_ROLES', roles)
storage.set(USER_ROLES, roles, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
} else {
commit('SET_ROLES', [DEFAULT_ROLE])
storage.set(USER_ROLES, [DEFAULT_ROLE], new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
}
resolve(info)
} else {
reject(new Error((res && res.msg) || '用户信息不存在'))
}
}).catch(err => reject(err))
}
})
},
// OAuth 登录(Google/GitHub
OAuthLogin ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
oauthCallback(userInfo).then(response => {
// 新API响应格式: { code: 1, msg: "登录成功", data: { token, userInfo } }
if (response.code === 1 && response.data) {
const token = response.data.token
storage.set(ACCESS_TOKEN, token, new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
commit('SET_TOKEN', token)
// 保存用户信息(从登录接口返回的 userInfo)
if (response.data.userInfo) {
const userInfoData = response.data.userInfo
commit('SET_INFO', userInfoData)
// 设置用户名
if (userInfoData.nickname) {
commit('SET_NAME', { name: userInfoData.nickname, welcome: welcome() })
} else if (userInfoData.username) {
commit('SET_NAME', { name: userInfoData.username, welcome: welcome() })
}
// 设置头像
if (userInfoData.avatar) {
commit('SET_AVATAR', userInfoData.avatar)
}
// 设置角色(如果有)
if (userInfoData.role) {
commit('SET_ROLES', userInfoData.role)
} else if (userInfoData.roles) {
commit('SET_ROLES', userInfoData.roles)
} else {
// 如果没有角色信息,设置一个默认角色对象,避免路由守卫卡住
commit('SET_ROLES', [{ id: 'default', permissionList: [] }])
}
}
resolve(response)
} else {
reject(new Error(response.msg || '登录失败'))
}
}).catch(error => {
reject(error)
})
})
},
// 登出
Logout ({ commit, state }) {
return new Promise((resolve) => {
// 兼容旧登出与新后端登出
const req = typeof apiLogout === 'function' ? apiLogout() : logout(state.token)
req.then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_INFO', {})
commit('SET_NAME', { name: '', welcome: '' })
commit('SET_AVATAR', '')
storage.remove(ACCESS_TOKEN)
storage.remove(USER_INFO)
storage.remove(USER_ROLES)
resolve()
}).catch(() => {
// 登出失败时也继续执行,确保清理本地状态
storage.remove(ACCESS_TOKEN)
storage.remove(USER_INFO)
storage.remove(USER_ROLES)
resolve()
}).finally(() => {
})
})
}
}
}
export default user
@@ -0,0 +1,26 @@
export const ACCESS_TOKEN = 'Access-Token'
export const USER_INFO = 'User-Info'
export const USER_ROLES = 'User-Roles'
export const SIDEBAR_TYPE = 'sidebar_type'
export const TOGGLE_MOBILE_TYPE = 'is_mobile'
export const TOGGLE_NAV_THEME = 'nav_theme'
export const TOGGLE_LAYOUT = 'layout'
export const TOGGLE_FIXED_HEADER = 'fixed_header'
export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar'
export const TOGGLE_CONTENT_WIDTH = 'content_width'
export const TOGGLE_HIDE_HEADER = 'auto_hide_header'
export const TOGGLE_COLOR = 'color'
export const TOGGLE_WEAK = 'weak'
export const TOGGLE_MULTI_TAB = 'multi_tab'
export const APP_LANGUAGE = 'app_language'
export const CONTENT_WIDTH_TYPE = {
Fluid: 'Fluid',
Fixed: 'Fixed'
}
export const NAV_THEME = {
LIGHT: 'light',
DARK: 'dark'
}