first commit

This commit is contained in:
zc
2026-02-26 17:31:18 +08:00
commit f1b87df6ca
803 changed files with 297148 additions and 0 deletions

13
src/stores/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export * from './modules/app'
export * from './modules/route'
export * from './modules/tabs'
export * from './modules/dict'
export * from './modules/user'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

144
src/stores/modules/app.ts Normal file
View File

@@ -0,0 +1,144 @@
import { defineStore } from 'pinia'
import { computed, reactive, toRefs } from 'vue'
import { generate, getRgbStr } from '@arco-design/color'
import { type BasicConfig, listSiteOptionDict } from '@/apis'
import { getSettings } from '@/config/setting'
const storeSetup = () => {
// App配置
const settingConfig = reactive({ ...getSettings() }) as App.AppSettings
// 页面切换动画类名
const transitionName = computed(() => (settingConfig.animate ? settingConfig.animateMode : ''))
// 深色菜单主题色变量
const themeCSSVar = computed<Record<string, string>>(() => {
const obj: Record<string, string> = {}
const list = generate(settingConfig.themeColor, { list: true, dark: true }) as string[]
list.forEach((color, index) => {
obj[`--primary-${index + 1}`] = getRgbStr(color)
})
return obj
})
// 设置主题色
const setThemeColor = (color: string) => {
if (!color) return
settingConfig.themeColor = color
const list = generate(settingConfig.themeColor, { list: true, dark: settingConfig.theme === 'dark' }) as string[]
list.forEach((color, index) => {
const rgbStr = getRgbStr(color)
document.body.style.setProperty(`--primary-${index + 1}`, rgbStr)
})
}
// 切换主题 暗黑模式|简白模式
const toggleTheme = (dark: boolean) => {
if (dark) {
settingConfig.theme = 'dark'
document.body.setAttribute('arco-theme', 'dark')
} else {
settingConfig.theme = 'light'
document.body.removeAttribute('arco-theme')
}
setThemeColor(settingConfig.themeColor)
}
// 初始化主题
const initTheme = () => {
if (!settingConfig.themeColor) return
setThemeColor(settingConfig.themeColor)
}
// 设置左侧菜单折叠状态
const setMenuCollapse = (collapsed: boolean) => {
settingConfig.menuCollapse = collapsed
}
// 系统配置配置
const siteConfig = reactive({}) as BasicConfig
// 初始化系统配置
const initSiteConfig = () => {
listSiteOptionDict().then((res) => {
const resMap = new Map()
res.data.forEach((item) => {
resMap.set(item.label, item.value)
})
siteConfig.SITE_FAVICON = resMap.get('SITE_FAVICON')
siteConfig.SITE_LOGO = resMap.get('SITE_LOGO')
siteConfig.SITE_TITLE = resMap.get('SITE_TITLE')
siteConfig.SITE_COPYRIGHT = resMap.get('SITE_COPYRIGHT')
siteConfig.SITE_BEIAN = resMap.get('SITE_BEIAN')
document.title = resMap.get('SITE_TITLE')
document
.querySelector('link[rel="shortcut icon"]')
?.setAttribute('href', resMap.get('SITE_FAVICON') || '/favicon.ico')
})
}
// 设置系统配置
const setSiteConfig = (config: BasicConfig) => {
Object.assign(siteConfig, config)
document.title = config.SITE_TITLE || ''
document.querySelector('link[rel="shortcut icon"]')?.setAttribute('href', config.SITE_FAVICON || '/favicon.ico')
}
// 监听 色弱模式 和 哀悼模式
watch([
() => settingConfig.enableMourningMode,
() => settingConfig.enableColorWeaknessMode,
], ([mourningMode, colorWeaknessMode]) => {
const filters = [] as string[]
if (mourningMode) {
filters.push('grayscale(100%)')
}
if (colorWeaknessMode) {
filters.push('invert(80%)')
}
// 如果没有任何滤镜条件,移除 `filter` 样式
if (filters.length === 0) {
document.documentElement.style.removeProperty('filter')
} else {
document.documentElement.style.setProperty('filter', filters.join(' '))
}
}, {
immediate: true,
})
const getFavicon = () => {
return siteConfig.SITE_FAVICON
}
const getLogo = () => {
return siteConfig.SITE_LOGO
}
const getTitle = () => {
return siteConfig.SITE_TITLE
}
const getCopyright = () => {
return siteConfig.SITE_COPYRIGHT
}
const getForRecord = () => {
return siteConfig.SITE_BEIAN
}
return {
...toRefs(settingConfig),
...toRefs(siteConfig),
transitionName,
themeCSSVar,
toggleTheme,
setThemeColor,
initTheme,
setMenuCollapse,
initSiteConfig,
setSiteConfig,
getFavicon,
getLogo,
getTitle,
getCopyright,
getForRecord,
}
}
export const useAppStore = defineStore('app', storeSetup, { persist: true })

View File

@@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
const storeSetup = () => {
const dictData = ref<Record<string, App.DictItem[]>>({})
// 设置字典
const setDict = (code: string, items: App.DictItem[]) => {
if (code) {
dictData.value[code] = items
}
}
// 获取字典
const getDict = (code: string) => {
if (!code) {
return null
}
return dictData.value[code] || null
}
// 删除字典
const deleteDict = (code: string) => {
if (!code || !(code in dictData.value)) {
return false
}
delete dictData.value[code]
return true
}
// 清空字典
const cleanDict = () => {
dictData.value = {}
}
return {
dictData,
setDict,
getDict,
deleteDict,
cleanDict,
}
}
export const useDictStore = defineStore('dict', storeSetup)

109
src/stores/modules/route.ts Normal file
View File

@@ -0,0 +1,109 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { mapTree, toTreeArray } from 'xe-utils'
import { cloneDeep, omit } from 'lodash-es'
import { constantRoutes, systemRoutes } from '@/router/route'
import { type RouteItem, getUserRoute } from '@/apis'
import { transformPathToName } from '@/utils'
import { asyncRouteModules } from '@/router/asyncModules'
const layoutComponentMap = {
Layout: () => import('@/layout/index.vue'),
ParentView: () => import('@/components/ParentView/index.vue'),
}
/** 将component由字符串转成真正的模块 */
const transformComponentView = (component: string) => {
return layoutComponentMap[component as keyof typeof layoutComponentMap] || asyncRouteModules[component]
}
/**
* @description 前端来做排序、格式化
* @params {menus} 后端返回的路由数据,已经根据当前用户角色过滤掉了没权限的路由
* 1. 对后端返回的路由数据进行排序,格式化
* 2. 同时将component由字符串转成真正的模块
*/
const formatAsyncRoutes = (menus: RouteItem[]) => {
if (!menus.length) return []
const pathMap = new Map()
return mapTree(menus, (item) => {
pathMap.set(item.id, item.path)
if (item.children?.length) {
item.children.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0))
}
// 部分子菜单,例如:通知公告新增、查看详情,需要选中其父菜单
if (item.parentId && item.type === 2 && item.permission) {
item.activeMenu = pathMap.get(item.parentId)
}
return {
path: item.path,
name: item.name ?? transformPathToName(item.path),
component: transformComponentView(item.component),
redirect: item.redirect,
meta: {
title: item.title,
hidden: item.isHidden,
keepAlive: item.isCache,
icon: item.icon,
showInTabs: item.showInTabs,
activeMenu: item.activeMenu,
// 在meta配置中添加affix属性处理
affix: item.path === '/dashboard/analysis',
},
}
}) as unknown as RouteRecordRaw[]
}
/** 判断路由层级是否大于 2 */
export const isMultipleRoute = (route: RouteRecordRaw) => {
return route.children?.some((child) => child.children?.length) ?? false
}
/** 路由降级(把三级及其以上的路由转化为二级路由) */
export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
return cloneDeep(routes).map((route) => {
if (!isMultipleRoute(route)) return route
return {
...route,
children: toTreeArray(route.children).map((item) => omit(item, 'children')) as RouteRecordRaw[],
}
})
}
const storeSetup = () => {
// 所有路由(常驻路由 + 动态路由)
const routes = ref<RouteRecordRaw[]>([])
// 动态路由(异步路由)
const asyncRoutes = ref<RouteRecordRaw[]>([])
// 合并路由
const setRoutes = (data: RouteRecordRaw[]) => {
// 合并路由并排序
routes.value = [...constantRoutes, ...systemRoutes].concat(data)
.sort((a, b) => (a.meta?.sort ?? 0) - (b.meta?.sort ?? 0))
asyncRoutes.value = data
}
// 生成路由
const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
const { data } = await getUserRoute()
const asyncRoutes = formatAsyncRoutes(data)
const flatRoutes = flatMultiLevelRoutes(cloneDeep(asyncRoutes))
setRoutes(asyncRoutes)
return flatRoutes
}
return {
routes,
asyncRoutes,
generateRoutes,
}
}
export const useRouteStore = defineStore('route', storeSetup, { persist: true })

172
src/stores/modules/tabs.ts Normal file
View File

@@ -0,0 +1,172 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { type RouteLocationNormalized, type RouteRecordName, useRouter } from 'vue-router'
import _XEUtils_ from 'xe-utils'
import { useRouteStore } from '@/stores'
const storeSetup = () => {
const router = useRouter()
const tabList = ref<RouteLocationNormalized[]>([]) // 保存页签tab的数组
const cacheList = ref<RouteRecordName[]>([]) // keep-alive缓存的数组元素是组件名
// 添加一个页签,如果当前路由已经打开,则不再重复添加
const addTabItem = (item: RouteLocationNormalized) => {
const index = tabList.value.findIndex((i) => i.path === item.path)
if (index >= 0) {
tabList.value[index].fullPath !== item.fullPath && (tabList.value[index] = item)
} else {
if (item.meta?.showInTabs ?? true) {
tabList.value.push(item)
}
}
}
// 删除一个页签
const deleteTabItem = (path: string) => {
const index = tabList.value.findIndex((item) => item.path === path && !item.meta?.affix)
if (index < 0) return
const isActive = router.currentRoute.value.path === tabList.value[index].path
tabList.value.splice(index, 1)
if (isActive) {
const lastObj = tabList.value[tabList.value.length - 1]
router.push(lastObj.fullPath || lastObj.path)
}
}
// 清空页签
const clearTabList = () => {
const routeStore = useRouteStore()
const arr: RouteLocationNormalized[] = []
_XEUtils_.eachTree(routeStore.routes, (item) => {
if (item.meta?.affix ?? false) {
arr.push(item as unknown as RouteLocationNormalized)
}
})
tabList.value = arr
}
// 设置当前tab页签名称
const setTabTitle = (title: string) => {
if (!title) return false
const route = router.currentRoute.value
const path = route?.fullPath || route.path
const index = tabList.value.findIndex((i) => i.fullPath === path)
if (index >= 0) {
tabList.value[index].meta.title = title
}
}
// 添加缓存页
const addCacheItem = (item: RouteLocationNormalized) => {
if (!item.name) return
if (cacheList.value.includes(item.name)) return
if (item.meta?.keepAlive) {
cacheList.value.push(item.name)
}
}
// 删除一个缓存页
const deleteCacheItem = (name: RouteRecordName) => {
const index = cacheList.value.findIndex((i) => i === name)
if (index >= 0) {
cacheList.value.splice(index, 1)
}
}
// 清空缓存页
const clearCacheList = () => {
cacheList.value = []
}
// 关闭当前
const closeCurrent = (path: string) => {
const item = tabList.value.find((i) => i.path === path)
item?.name && deleteCacheItem(item.name)
deleteTabItem(path)
}
// 关闭其他
const closeOther = (path: string) => {
const arr = tabList.value.filter((i) => i.path !== path)
arr.forEach((item) => {
deleteTabItem(item.path)
item?.name && deleteCacheItem(item.name)
})
}
// 关闭左侧
const closeLeft = (path: string) => {
const index = tabList.value.findIndex((i) => i.path === path)
if (index < 0) return
const arr = tabList.value.filter((i, n) => n < index)
arr.forEach((item) => {
deleteTabItem(item.path)
item?.name && deleteCacheItem(item.name)
})
}
// 关闭右侧
const closeRight = (path: string) => {
const index = tabList.value.findIndex((i) => i.path === path)
if (index < 0) return
const arr = tabList.value.filter((i, n) => n > index)
arr.forEach((item) => {
deleteTabItem(item.path)
item?.name && deleteCacheItem(item.name)
})
}
// 关闭全部
const closeAll = () => {
clearTabList()
clearCacheList()
router.push({ path: '/' })
}
// 重置
const reset = () => {
clearTabList()
clearCacheList()
}
// 初始化
const init = () => {
if (tabList.value.some((i) => !i?.meta.affix)) return
reset()
}
// Tabs页签右侧刷新按钮-页面重新加载
const reloadFlag = ref(true)
const reloadPage = () => {
const route = router.currentRoute.value
deleteCacheItem(route.name as string) // 修复点击刷新图标,无法重新触发生命周期钩子函数问题
reloadFlag.value = false
nextTick(() => {
reloadFlag.value = true
addCacheItem(route)
})
}
return {
tabList,
cacheList,
addTabItem,
deleteTabItem,
clearTabList,
setTabTitle,
addCacheItem,
deleteCacheItem,
clearCacheList,
closeCurrent,
closeOther,
closeLeft,
closeRight,
closeAll,
reset,
init,
reloadFlag,
reloadPage,
}
}
export const useTabsStore = defineStore('tabs', storeSetup, { persist: { storage: sessionStorage } })

133
src/stores/modules/user.ts Normal file
View File

@@ -0,0 +1,133 @@
import { defineStore } from 'pinia'
import { computed, reactive, ref } from 'vue'
import { resetRouter } from '@/router'
import {
type AccountLoginReq,
AuthTypeConstants,
type EmailLoginReq,
type PhoneLoginReq,
type UserInfo,
accountLogin as accountLoginApi,
emailLogin as emailLoginApi,
getUserInfo as getUserInfoApi,
logout as logoutApi,
phoneLogin as phoneLoginApi,
socialLogin as socialLoginApi,
} from '@/apis'
import { clearToken, getToken, setToken } from '@/utils/auth'
import { resetHasRouteFlag } from '@/router/guard'
const storeSetup = () => {
const userInfo = reactive<UserInfo>({
id: '',
username: '',
nickname: '',
gender: 0,
email: '',
phone: '',
avatar: '',
pwdResetTime: '',
pwdExpired: false,
registrationDate: '',
deptName: '',
roles: [],
permissions: [],
})
const nickname = computed(() => userInfo.nickname)
const username = computed(() => userInfo.username)
const avatar = computed(() => userInfo.avatar)
const token = ref(getToken() || '')
const pwdExpiredShow = ref<boolean>(true)
const roles = ref<string[]>([]) // 当前用户角色
const permissions = ref<string[]>([]) // 当前角色权限标识集合
// 重置token
const resetToken = () => {
token.value = ''
clearToken()
resetHasRouteFlag()
}
// 登录
const accountLogin = async (req: AccountLoginReq) => {
const res = await accountLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.ACCOUNT })
setToken(res.data.token)
token.value = res.data.token
}
// 邮箱登录
const emailLogin = async (req: EmailLoginReq) => {
const res = await emailLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.EMAIL })
setToken(res.data.token)
token.value = res.data.token
}
// 手机号登录
const phoneLogin = async (req: PhoneLoginReq) => {
const res = await phoneLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE })
setToken(res.data.token)
token.value = res.data.token
}
// 三方账号登录
const socialLogin = async (source: string, req: any) => {
const res = await socialLoginApi({ ...req, source, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.SOCIAL })
setToken(res.data.token)
token.value = res.data.token
}
// 退出登录回调
const logoutCallBack = async () => {
roles.value = []
permissions.value = []
pwdExpiredShow.value = true
resetToken()
resetRouter()
}
// 退出登录
const logout = async () => {
try {
await logoutApi()
await logoutCallBack()
return true
} catch (error) {
return false
}
}
// 获取用户信息
const getInfo = async () => {
const res = await getUserInfoApi()
Object.assign(userInfo, res.data)
userInfo.avatar = res.data.avatar
if (res.data.roles && res.data.roles.length) {
roles.value = res.data.roles
permissions.value = res.data.permissions
}
}
return {
userInfo,
nickname,
username,
avatar,
token,
roles,
permissions,
pwdExpiredShow,
accountLogin,
emailLogin,
phoneLogin,
socialLogin,
logout,
logoutCallBack,
getInfo,
resetToken,
}
}
export const useUserStore = defineStore('user', storeSetup, {
persist: { paths: ['token', 'roles', 'permissions', 'pwdExpiredShow'], storage: localStorage },
})