前端代码
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const device = ref('')
|
||||
const drawerSize = ref('')
|
||||
const operateMinWith = ref('240')
|
||||
// 侧栏折叠态(运行时态,不持久化),供 vertical 布局的固定侧栏与外壳共享
|
||||
const sideCollapse = ref(false)
|
||||
// 移动端(<640)左侧抽屉菜单开合态
|
||||
const mobileMenuOpen = ref(false)
|
||||
// 当前断点区间(由 useResponsive 维护):mobile(<640) / pad(640–1024) / desktop(>=1024)
|
||||
const layoutZone = ref('desktop')
|
||||
// 平板区间强制「通栏侧边」收缩布局(不改用户所选布局)
|
||||
const isPad = computed(() => layoutZone.value === 'pad')
|
||||
// 进入平板前的侧栏折叠态快照,离开平板时据此恢复(区分「系统收缩」与「用户收缩」)
|
||||
const prePadCollapse = ref(false)
|
||||
|
||||
const toggleMobileMenu = (v) => {
|
||||
mobileMenuOpen.value = typeof v === 'boolean' ? v : !mobileMenuOpen.value
|
||||
}
|
||||
|
||||
// 切换断点区间:进入平板时快照当前折叠态并强制收缩,离开平板时恢复进入前的折叠态,
|
||||
// 从而只撤销系统自己施加的收缩,不覆盖用户在桌面手动收缩的意图。
|
||||
const setLayoutZone = (zone) => {
|
||||
const prev = layoutZone.value
|
||||
if (zone === 'pad' && prev !== 'pad') {
|
||||
prePadCollapse.value = sideCollapse.value
|
||||
sideCollapse.value = true
|
||||
} else if (zone !== 'pad' && prev === 'pad') {
|
||||
sideCollapse.value = prePadCollapse.value
|
||||
}
|
||||
layoutZone.value = zone
|
||||
}
|
||||
|
||||
const toggleDevice = (e) => {
|
||||
if (e === 'mobile') {
|
||||
drawerSize.value = '100%'
|
||||
operateMinWith.value = '80'
|
||||
} else {
|
||||
drawerSize.value = '800'
|
||||
operateMinWith.value = '240'
|
||||
}
|
||||
device.value = e
|
||||
}
|
||||
|
||||
const toggleSideCollapse = (v) => {
|
||||
sideCollapse.value = typeof v === 'boolean' ? v : !sideCollapse.value
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
drawerSize,
|
||||
operateMinWith,
|
||||
sideCollapse,
|
||||
toggleSideCollapse,
|
||||
mobileMenuOpen,
|
||||
toggleMobileMenu,
|
||||
layoutZone,
|
||||
isPad,
|
||||
setLayoutZone,
|
||||
toggleDevice
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,252 @@
|
||||
import { findSysDictionary } from '@/api/sysDictionary'
|
||||
import { getDictionaryTreeListByType } from '@/api/sysDictionaryDetail'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useDictionaryStore = defineStore('dictionary', () => {
|
||||
const dictionaryMap = ref({})
|
||||
|
||||
const setDictionaryMap = (dictionaryRes) => {
|
||||
dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes }
|
||||
}
|
||||
|
||||
// 过滤树形数据的深度
|
||||
const filterTreeByDepth = (items, currentDepth, targetDepth) => {
|
||||
if (targetDepth === 0) {
|
||||
// depth=0 返回全部数据
|
||||
return items
|
||||
}
|
||||
|
||||
if (currentDepth >= targetDepth) {
|
||||
// 达到目标深度,移除children
|
||||
return items.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend
|
||||
}))
|
||||
}
|
||||
|
||||
// 递归处理子项
|
||||
return items.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend,
|
||||
children: item.children
|
||||
? filterTreeByDepth(item.children, currentDepth + 1, targetDepth)
|
||||
: undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 将树形结构扁平化为数组(用于兼容原有的平铺格式)
|
||||
const flattenTree = (items) => {
|
||||
const result = []
|
||||
|
||||
const traverse = (nodes) => {
|
||||
nodes.forEach((item) => {
|
||||
result.push({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend
|
||||
})
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
traverse(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(items)
|
||||
return result
|
||||
}
|
||||
|
||||
// 标准化树形数据,确保每个节点都包含标准的字段格式
|
||||
const normalizeTreeData = (items) => {
|
||||
return items.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend,
|
||||
children:
|
||||
item.children && item.children.length > 0
|
||||
? normalizeTreeData(item.children)
|
||||
: undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 根据value和depth查找指定节点并返回其children
|
||||
const findNodeByValue = (
|
||||
items,
|
||||
targetValue,
|
||||
currentDepth = 1,
|
||||
maxDepth = 0
|
||||
) => {
|
||||
for (const item of items) {
|
||||
// 如果找到目标value的节点
|
||||
if (item.value === targetValue) {
|
||||
// 如果maxDepth为0,返回所有children
|
||||
if (maxDepth === 0) {
|
||||
return item.children ? normalizeTreeData(item.children) : []
|
||||
}
|
||||
// 否则根据depth限制返回children
|
||||
if (item.children && item.children.length > 0) {
|
||||
return filterTreeByDepth(item.children, 1, maxDepth)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 如果当前深度小于最大深度,继续在children中查找
|
||||
if (
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
(maxDepth === 0 || currentDepth < maxDepth)
|
||||
) {
|
||||
const result = findNodeByValue(
|
||||
item.children,
|
||||
targetValue,
|
||||
currentDepth + 1,
|
||||
maxDepth
|
||||
)
|
||||
if (result !== null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getDictionary = async (type, depth = 0, value = null) => {
|
||||
// 如果传入了value参数,则查找指定节点的children
|
||||
if (value !== null) {
|
||||
// 构建缓存key,包含value和depth信息
|
||||
const cacheKey = `${type}_value_${value}_depth_${depth}`
|
||||
|
||||
if (
|
||||
dictionaryMap.value[cacheKey] &&
|
||||
dictionaryMap.value[cacheKey].length
|
||||
) {
|
||||
return dictionaryMap.value[cacheKey]
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取完整的树形结构数据
|
||||
const treeRes = await getDictionaryTreeListByType({ type })
|
||||
if (
|
||||
treeRes.code === 0 &&
|
||||
treeRes.data &&
|
||||
treeRes.data.list &&
|
||||
treeRes.data.list.length > 0
|
||||
) {
|
||||
// 查找指定value的节点并返回其children
|
||||
const targetNodeChildren = findNodeByValue(
|
||||
treeRes.data.list,
|
||||
value,
|
||||
1,
|
||||
depth
|
||||
)
|
||||
|
||||
if (targetNodeChildren !== null) {
|
||||
let resultData
|
||||
if (depth === 0) {
|
||||
// depth=0 时返回完整的children树形结构
|
||||
resultData = targetNodeChildren
|
||||
} else {
|
||||
// 其他depth值:扁平化children数据
|
||||
resultData = flattenTree(targetNodeChildren)
|
||||
}
|
||||
|
||||
const dictionaryRes = {}
|
||||
dictionaryRes[cacheKey] = resultData
|
||||
setDictionaryMap(dictionaryRes)
|
||||
return dictionaryMap.value[cacheKey]
|
||||
} else {
|
||||
// 如果没找到指定value的节点,返回空数组
|
||||
return []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('根据value获取字典数据失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 原有的逻辑:不传value参数时的处理
|
||||
// 构建缓存key,包含depth信息
|
||||
const cacheKey = depth === 0 ? `${type}_tree` : `${type}_depth_${depth}`
|
||||
|
||||
if (dictionaryMap.value[cacheKey] && dictionaryMap.value[cacheKey].length) {
|
||||
return dictionaryMap.value[cacheKey]
|
||||
} else {
|
||||
try {
|
||||
// 首先尝试获取树形结构数据
|
||||
const treeRes = await getDictionaryTreeListByType({ type })
|
||||
if (
|
||||
treeRes.code === 0 &&
|
||||
treeRes.data &&
|
||||
treeRes.data.list &&
|
||||
treeRes.data.list.length > 0
|
||||
) {
|
||||
// 使用树形结构数据
|
||||
const treeData = treeRes.data.list
|
||||
|
||||
let resultData
|
||||
if (depth === 0) {
|
||||
// depth=0 时返回完整的树形结构,但要确保字段格式标准化
|
||||
resultData = normalizeTreeData(treeData)
|
||||
} else {
|
||||
// 其他depth值:根据depth参数过滤数据,然后扁平化
|
||||
const filteredData = filterTreeByDepth(treeData, 1, depth)
|
||||
resultData = flattenTree(filteredData)
|
||||
}
|
||||
|
||||
const dictionaryRes = {}
|
||||
dictionaryRes[cacheKey] = resultData
|
||||
setDictionaryMap(dictionaryRes)
|
||||
return dictionaryMap.value[cacheKey]
|
||||
} else {
|
||||
// 如果没有树形数据,回退到原有的平铺方式
|
||||
const res = await findSysDictionary({ type })
|
||||
if (res.code === 0) {
|
||||
const dictionaryRes = {}
|
||||
const dict = []
|
||||
res.data.resysDictionary.sysDictionaryDetails &&
|
||||
res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
|
||||
dict.push({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend
|
||||
})
|
||||
})
|
||||
dictionaryRes[cacheKey] = dict
|
||||
setDictionaryMap(dictionaryRes)
|
||||
return dictionaryMap.value[cacheKey]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取字典数据失败:', error)
|
||||
// 发生错误时回退到原有方式
|
||||
const res = await findSysDictionary({ type })
|
||||
if (res.code === 0) {
|
||||
const dictionaryRes = {}
|
||||
const dict = []
|
||||
res.data.resysDictionary.sysDictionaryDetails &&
|
||||
res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
|
||||
dict.push({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
extend: item.extend
|
||||
})
|
||||
})
|
||||
dictionaryRes[cacheKey] = dict
|
||||
setDictionaryMap(dictionaryRes)
|
||||
return dictionaryMap.value[cacheKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dictionaryMap,
|
||||
setDictionaryMap,
|
||||
getDictionary
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getSysParam } from '@/api/sysParams'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useParamsStore = defineStore('params', () => {
|
||||
const paramsMap = ref({})
|
||||
|
||||
const setParamsMap = (paramsRes) => {
|
||||
paramsMap.value = { ...paramsMap.value, ...paramsRes }
|
||||
}
|
||||
|
||||
const getParams = async(key) => {
|
||||
if (paramsMap.value[key] && paramsMap.value[key].length) {
|
||||
return paramsMap.value[key]
|
||||
} else {
|
||||
const res = await getSysParam({ key })
|
||||
if (res.code === 0) {
|
||||
const paramsRes = {}
|
||||
paramsRes[key] = res.data.value
|
||||
setParamsMap(paramsRes)
|
||||
return paramsMap.value[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paramsMap,
|
||||
setParamsMap,
|
||||
getParams
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,211 @@
|
||||
import { asyncRouterHandle } from '@/utils/asyncRouter'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { asyncMenu } from '@/api/menu'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
import {useRoute} from "vue-router";
|
||||
import {config} from "@/core/config.js";
|
||||
|
||||
const notLayoutRouterArr = []
|
||||
const keepAliveRoutersArr = []
|
||||
const nameMap = {}
|
||||
|
||||
const formatRouter = (routes, routeMap, parent) => {
|
||||
routes &&
|
||||
routes.forEach((item) => {
|
||||
item.parent = parent
|
||||
item.meta.btns = item.btns
|
||||
item.meta.hidden = item.hidden
|
||||
if (item.meta.defaultMenu === true) {
|
||||
if (!parent) {
|
||||
item = { ...item, path: `/${item.path}` }
|
||||
notLayoutRouterArr.push(item)
|
||||
}
|
||||
}
|
||||
routeMap[item.name] = item
|
||||
if (item.children && item.children.length > 0) {
|
||||
formatRouter(item.children, routeMap, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const KeepAliveFilter = (routes) => {
|
||||
routes &&
|
||||
routes.forEach((item) => {
|
||||
// 子菜单中有 keep-alive 的,父菜单也必须 keep-alive,否则无效。这里将子菜单中有 keep-alive 的父菜单也加入。
|
||||
if (
|
||||
(item.children && item.children.some((ch) => ch.meta.keepAlive)) ||
|
||||
item.meta.keepAlive
|
||||
) {
|
||||
const path = item.meta.path
|
||||
keepAliveRoutersArr.push(pathInfo[path])
|
||||
nameMap[item.name] = pathInfo[path]
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
KeepAliveFilter(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useRouterStore = defineStore('router', () => {
|
||||
const keepAliveRouters = ref([])
|
||||
const asyncRouterFlag = ref(0)
|
||||
const setKeepAliveRouters = (history) => {
|
||||
const keepArrTemp = []
|
||||
|
||||
// 1. 首先添加原有的keepAlive配置
|
||||
keepArrTemp.push(...keepAliveRoutersArr)
|
||||
if (config.keepAliveTabs) {
|
||||
history.forEach((item) => {
|
||||
// 2. 为所有history中的路由强制启用keep-alive
|
||||
// 通过routeMap获取路由信息,然后通过pathInfo获取组件名
|
||||
const routeInfo = routeMap[item.name]
|
||||
if (routeInfo && routeInfo.meta && routeInfo.meta.path) {
|
||||
const componentName = pathInfo[routeInfo.meta.path]
|
||||
if (componentName) {
|
||||
keepArrTemp.push(componentName)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果子路由在tabs中打开,父路由也需要keepAlive
|
||||
if (nameMap[item.name]) {
|
||||
keepArrTemp.push(nameMap[item.name])
|
||||
}
|
||||
})
|
||||
}
|
||||
keepAliveRouters.value = Array.from(new Set(keepArrTemp))
|
||||
}
|
||||
|
||||
// 处理组件缓存
|
||||
const handleKeepAlive = async (to) => {
|
||||
if (!to.matched.some((item) => item.meta.keepAlive)) return
|
||||
|
||||
if (to.matched?.length > 2) {
|
||||
for (let i = 1; i < to.matched.length; i++) {
|
||||
const element = to.matched[i - 1]
|
||||
|
||||
if (element.name === 'layout') {
|
||||
to.matched.splice(i, 1)
|
||||
await handleKeepAlive(to)
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof element.components.default === 'function') {
|
||||
await element.components.default()
|
||||
await handleKeepAlive(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
emitter.on('setKeepAlive', setKeepAliveRouters)
|
||||
|
||||
const asyncRouters = ref([])
|
||||
|
||||
const topMenu = ref([])
|
||||
|
||||
const leftMenu = ref([])
|
||||
|
||||
const menuMap = {}
|
||||
|
||||
const topActive = ref('')
|
||||
|
||||
const setLeftMenu = (name) => {
|
||||
sessionStorage.setItem('topActive', name)
|
||||
topActive.value = name
|
||||
leftMenu.value = []
|
||||
if (menuMap[name]?.children) {
|
||||
leftMenu.value = menuMap[name].children
|
||||
}
|
||||
return menuMap[name]?.children
|
||||
}
|
||||
|
||||
const findTopActive = (menuMap, routeName) => {
|
||||
for (let topName in menuMap) {
|
||||
const topItem = menuMap[topName];
|
||||
if (topItem.children?.some(item => item.name === routeName)) {
|
||||
return topName;
|
||||
}
|
||||
const foundName = findTopActive(topItem.children || {}, routeName);
|
||||
if (foundName) {
|
||||
return topName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
let topActive = sessionStorage.getItem('topActive')
|
||||
// 初始化菜单内容,防止重复添加
|
||||
topMenu.value = [];
|
||||
asyncRouters.value[0]?.children.forEach((item) => {
|
||||
if (item.hidden) return
|
||||
menuMap[item.name] = item
|
||||
topMenu.value.push({ ...item, children: [] })
|
||||
})
|
||||
if (!topActive || topActive === 'undefined' || topActive === 'null') {
|
||||
topActive = findTopActive(menuMap, route.name);
|
||||
}
|
||||
setLeftMenu(topActive)
|
||||
})
|
||||
|
||||
const routeMap = {}
|
||||
// 从后台获取动态路由
|
||||
const SetAsyncRouter = async () => {
|
||||
asyncRouterFlag.value++
|
||||
const baseRouter = [
|
||||
{
|
||||
path: '/layout',
|
||||
name: 'layout',
|
||||
component: 'view/layout/index.vue',
|
||||
meta: {
|
||||
title: '底层layout'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
]
|
||||
const asyncRouterRes = await asyncMenu()
|
||||
const asyncRouter = asyncRouterRes.data.menus
|
||||
asyncRouter &&
|
||||
asyncRouter.push({
|
||||
path: 'reload',
|
||||
name: 'Reload',
|
||||
hidden: true,
|
||||
meta: {
|
||||
title: '',
|
||||
closeTab: true
|
||||
},
|
||||
component: 'view/error/reload.vue'
|
||||
})
|
||||
formatRouter(asyncRouter, routeMap)
|
||||
baseRouter[0].children = asyncRouter
|
||||
if (notLayoutRouterArr.length !== 0) {
|
||||
baseRouter.push(...notLayoutRouterArr)
|
||||
}
|
||||
asyncRouterHandle(baseRouter)
|
||||
KeepAliveFilter(asyncRouter)
|
||||
asyncRouters.value = baseRouter
|
||||
return true
|
||||
}
|
||||
|
||||
// 根级菜单(一级菜单树):各布局取菜单数据的单一入口,避免 asyncRouters[0].children 散点解构。
|
||||
const rootMenus = computed(() => asyncRouters.value[0]?.children || [])
|
||||
|
||||
return {
|
||||
topActive,
|
||||
setLeftMenu,
|
||||
topMenu,
|
||||
leftMenu,
|
||||
rootMenus,
|
||||
asyncRouters,
|
||||
keepAliveRouters,
|
||||
asyncRouterFlag,
|
||||
SetAsyncRouter,
|
||||
routeMap,
|
||||
handleKeepAlive
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
import { computed, reactive, watch, watchEffect } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useDark, useDebounceFn, usePreferredDark } from '@vueuse/core'
|
||||
import { setSelfSetting } from '@/api/user'
|
||||
import {
|
||||
buildThemeEnvelope,
|
||||
buildThemePreset,
|
||||
cacheThemeSettings,
|
||||
mergeThemeSettings,
|
||||
normalizeColor,
|
||||
normalizeThemePreset,
|
||||
normalizeThemeSettings,
|
||||
readCachedThemeSettings,
|
||||
toggleAuxiliaryColorModes,
|
||||
toggleCssDarkMode
|
||||
} from '@/theme/shared'
|
||||
import { cloneThemeSettings } from '@/theme/settings'
|
||||
import { getThemeColors } from '@/theme/token'
|
||||
import { resolveElementPlusConfig } from '@/theme/adapters/element-plus'
|
||||
import { dispatchTheme } from '@/theme/adapters/runtime'
|
||||
|
||||
// 轻量登录态判断:避免在登录页等未登录场景向 setSelfSetting 发请求。
|
||||
// 直接读 token,不依赖 userStore,规避 user <-> theme 的循环引用。
|
||||
const hasAuthToken = () =>
|
||||
Boolean(localStorage.getItem('token'))
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const settings = reactive(readCachedThemeSettings() || cloneThemeSettings())
|
||||
|
||||
const isDark = useDark({
|
||||
selector: 'html',
|
||||
attribute: 'class',
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
storageKey: null
|
||||
})
|
||||
const preferredDark = usePreferredDark()
|
||||
|
||||
const darkMode = computed(() => {
|
||||
if (settings.themeScheme === 'auto') return preferredDark.value
|
||||
return settings.themeScheme === 'dark'
|
||||
})
|
||||
|
||||
const themeColors = computed(() => getThemeColors(settings))
|
||||
const elConfig = computed(() => resolveElementPlusConfig(settings))
|
||||
const primaryColor = computed(() => themeColors.value.primary)
|
||||
const settingsJson = computed(() => JSON.stringify(settings))
|
||||
|
||||
// 远端同步基线:记录「已知与后端一致」的 settings 快照。用值比较而非时序标志,
|
||||
// 天然规避 watch 的异步触发顺序问题——应用后端 originSetting 时把基线设为该值,
|
||||
// 防抖保存触发时发现与基线一致即跳过,从而不会把刚拉取的服务端值再回写给服务端。
|
||||
let remoteBaseline = settingsJson.value
|
||||
let remoteSaving = false
|
||||
let remoteSavePending = false
|
||||
|
||||
// 远端保存串行化:任意时刻最多一个在途请求,完成后若期间又有改动则再保存一次。
|
||||
// 这样「最后一次配置」始终最后落库,避免旧请求晚返回覆盖新配置导致的静默丢配置。
|
||||
const saveSelfSettingToRemote = async () => {
|
||||
if (remoteSaving) {
|
||||
remoteSavePending = true // 已有请求在途,记下「完成后需再保存」
|
||||
return
|
||||
}
|
||||
|
||||
const json = settingsJson.value
|
||||
if (json === remoteBaseline) return // 与后端一致,无需保存
|
||||
if (!hasAuthToken()) return // 未登录,跳过(本地缓存已落地)
|
||||
|
||||
remoteSaving = true
|
||||
try {
|
||||
const res = await setSelfSetting(buildThemeEnvelope(settings))
|
||||
if (res?.code === 0) {
|
||||
remoteBaseline = json
|
||||
ElMessage.success({
|
||||
message: '保存成功'
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 静默失败:本地缓存仍是最新,下次任意改动会重新触发保存
|
||||
} finally {
|
||||
remoteSaving = false
|
||||
if (remoteSavePending) {
|
||||
remoteSavePending = false
|
||||
saveSelfSettingToRemote() // 在途期间又有改动 → 立即用最新值再保存一次
|
||||
}
|
||||
}
|
||||
}
|
||||
// 配置项常被滑块、调色板等高频修改,防抖后再保存,避免每次微调都打接口
|
||||
const debouncedRemoteSave = useDebounceFn(saveSelfSettingToRemote, 500)
|
||||
|
||||
const setThemeScheme = (themeScheme) => {
|
||||
settings.themeScheme = themeScheme
|
||||
}
|
||||
|
||||
const toggleTheme = (dark) => {
|
||||
settings.themeScheme = dark ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const updateThemeColors = (key, color) => {
|
||||
// 颜色统一小写后再写入,保证内部 themeColor / otherColor 始终是小写单一形态
|
||||
const value = normalizeColor(color)
|
||||
if (key === 'primary') {
|
||||
settings.themeColor = value
|
||||
return
|
||||
}
|
||||
|
||||
if (key in settings.otherColor) settings.otherColor[key] = value
|
||||
}
|
||||
|
||||
const applyPreset = (preset) => {
|
||||
const normalized = normalizeThemePreset(preset)
|
||||
if (!normalized?.theme) return
|
||||
|
||||
mergeThemeSettings(settings, cloneThemeSettings())
|
||||
mergeThemeSettings(settings, normalized.theme)
|
||||
}
|
||||
|
||||
// 应用后端返回的用户配置:normalizeThemeSettings 内部已对旧/未知数据做防御式回落,
|
||||
// 应用后把它设为远端基线,避免把服务端值再次回写。
|
||||
const applyRemoteSettings = (raw) => {
|
||||
// 防御:本地若有未同步到后端的改动(保存在途 / 与基线不一致),说明本地比服务端新,
|
||||
// 跳过覆盖,避免回退用户正在保存的配置。当前时序(登录 / 首次进入各调用一次)下恒不命中,
|
||||
// 仅为「GetUserInfo 未来若变成可重入(轮询 / 切回标签页刷新)」时保持 store 自洽。
|
||||
if (remoteSaving || remoteSavePending || settingsJson.value !== remoteBaseline) return
|
||||
mergeThemeSettings(settings, normalizeThemeSettings(raw))
|
||||
remoteBaseline = settingsJson.value
|
||||
}
|
||||
|
||||
const exportPreset = (name = 'current') => buildThemePreset(settings, name)
|
||||
|
||||
const resetConfig = () => {
|
||||
mergeThemeSettings(settings, cloneThemeSettings())
|
||||
}
|
||||
|
||||
watch(
|
||||
darkMode,
|
||||
(value) => {
|
||||
isDark.value = value
|
||||
toggleCssDarkMode(value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => settings.grayscale, () => settings.colourWeakness],
|
||||
([grayscale, colourWeakness]) => {
|
||||
toggleAuxiliaryColorModes(grayscale, colourWeakness)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
dispatchTheme({
|
||||
settings,
|
||||
themeColors: themeColors.value,
|
||||
isDark: isDark.value
|
||||
})
|
||||
})
|
||||
|
||||
// 统一持久化入口:任意 settings 变化 → 本地缓存(始终)+ 远端保存(登录态 & 与基线不同)。
|
||||
// 持久化不再依赖设置抽屉等组件挂载,store 在任何场景下都能自持久化。
|
||||
watch(settingsJson, () => {
|
||||
cacheThemeSettings(settings)
|
||||
debouncedRemoteSave()
|
||||
})
|
||||
|
||||
return {
|
||||
settings,
|
||||
elConfig,
|
||||
isDark,
|
||||
darkMode,
|
||||
themeColors,
|
||||
primaryColor,
|
||||
settingsJson,
|
||||
setThemeScheme,
|
||||
toggleTheme,
|
||||
updateThemeColors,
|
||||
applyPreset,
|
||||
applyRemoteSettings,
|
||||
exportPreset,
|
||||
resetConfig
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,150 @@
|
||||
import { login, getUserInfo } from '@/api/user'
|
||||
import { jsonInBlacklist } from '@/api/jwt'
|
||||
import router from '@/router/index'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRouterStore } from './router'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
import { useThemeStore } from '@/pinia'
|
||||
import { clearCachedThemeSettings } from '@/theme/shared'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const themeStore = useThemeStore()
|
||||
const loadingInstance = ref(null)
|
||||
|
||||
const userInfo = ref({
|
||||
uuid: '',
|
||||
nickName: '',
|
||||
headerImg: '',
|
||||
authority: {}
|
||||
})
|
||||
const token = useStorage('token', '')
|
||||
|
||||
const setUserInfo = (val) => {
|
||||
userInfo.value = val
|
||||
if (val.originSetting) {
|
||||
// 后端返回的用户主题:交给 themeStore 兼容解析并落地(本地缓存由 store 的 watch 接管)
|
||||
themeStore.applyRemoteSettings(val.originSetting)
|
||||
}
|
||||
}
|
||||
|
||||
const setToken = (val) => {
|
||||
token.value = val
|
||||
}
|
||||
|
||||
const NeedInit = async () => {
|
||||
await ClearStorage()
|
||||
await router.push({ name: 'Init', replace: true })
|
||||
}
|
||||
|
||||
const ResetUserInfo = (value = {}) => {
|
||||
userInfo.value = {
|
||||
...userInfo.value,
|
||||
...value
|
||||
}
|
||||
}
|
||||
/* 获取用户信息*/
|
||||
const GetUserInfo = async () => {
|
||||
const res = await getUserInfo()
|
||||
if (res.code === 0) {
|
||||
setUserInfo(res.data.userInfo)
|
||||
}
|
||||
return res
|
||||
}
|
||||
/* 登录*/
|
||||
const LoginIn = async (loginInfo) => {
|
||||
try {
|
||||
loadingInstance.value = ElLoading.service({
|
||||
fullscreen: true,
|
||||
text: '登录中,请稍候...'
|
||||
})
|
||||
|
||||
const res = await login(loginInfo)
|
||||
|
||||
if (res.code !== 0) {
|
||||
return false
|
||||
}
|
||||
// 登陆成功,设置用户信息和权限相关信息
|
||||
setUserInfo(res.data.user)
|
||||
setToken(res.data.token)
|
||||
|
||||
// 密码过期 强制跳转改密页
|
||||
if (res.data.needChangePassword) {
|
||||
await router.push({ name: 'ForceChangePassword' })
|
||||
return true
|
||||
}
|
||||
|
||||
// 初始化路由信息
|
||||
const routerStore = useRouterStore()
|
||||
await routerStore.SetAsyncRouter()
|
||||
const asyncRouters = routerStore.asyncRouters
|
||||
|
||||
// 注册到路由表里
|
||||
asyncRouters.forEach((asyncRouter) => {
|
||||
router.addRoute(asyncRouter)
|
||||
})
|
||||
|
||||
if(router.currentRoute.value.query.redirect) {
|
||||
await router.replace(router.currentRoute.value.query.redirect)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!router.hasRoute(userInfo.value.authority.defaultRouter)) {
|
||||
ElMessage.error('不存在可以登陆的首页,请联系管理员进行配置')
|
||||
} else {
|
||||
await router.replace({ name: userInfo.value.authority.defaultRouter })
|
||||
}
|
||||
|
||||
const isWindows = /windows/i.test(navigator.userAgent)
|
||||
window.localStorage.setItem('osType', isWindows ? 'WIN' : 'MAC')
|
||||
|
||||
// 全部操作均结束,关闭loading并返回
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('LoginIn error:', error)
|
||||
return false
|
||||
} finally {
|
||||
loadingInstance.value?.close()
|
||||
}
|
||||
}
|
||||
/* 登出*/
|
||||
const LoginOut = async () => {
|
||||
const res = await jsonInBlacklist()
|
||||
|
||||
// 登出失败
|
||||
if (res.code !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await ClearStorage()
|
||||
|
||||
// 把路由定向到登录页,无需等待直接reload
|
||||
router.push({ name: 'Login', replace: true })
|
||||
window.location.reload()
|
||||
}
|
||||
/* 清理数据 */
|
||||
const ClearStorage = async () => {
|
||||
token.value = ''
|
||||
sessionStorage.clear()
|
||||
// 清理所有相关的localStorage项
|
||||
localStorage.removeItem('originSetting')
|
||||
clearCachedThemeSettings()
|
||||
localStorage.removeItem('vueuse-color-scheme')
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
token,
|
||||
NeedInit,
|
||||
ResetUserInfo,
|
||||
GetUserInfo,
|
||||
LoginIn,
|
||||
LoginOut,
|
||||
setToken,
|
||||
loadingInstance,
|
||||
ClearStorage
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user