commits
This commit is contained in:
+107
-107
@@ -1,107 +1,107 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
// 用户信息类型
|
||||
const defaultUser = {
|
||||
id:'',
|
||||
account: '',
|
||||
name: '',
|
||||
group_id: '',
|
||||
type: 'backend',
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const isLoggedIn = ref(!!token.value)
|
||||
const user = reactive({ ...defaultUser })
|
||||
|
||||
// 从缓存加载用户信息
|
||||
function loadUserFromCache() {
|
||||
const cachedUser = localStorage.getItem('userInfo')
|
||||
if (cachedUser) {
|
||||
try {
|
||||
const userInfo = JSON.parse(cachedUser)
|
||||
Object.assign(user, userInfo)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info from cache:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载用户信息
|
||||
loadUserFromCache()
|
||||
|
||||
// 保存登录信息(token 和用户信息)
|
||||
function setLoginInfo(loginData) {
|
||||
const userInfo = loginData.user || loginData
|
||||
|
||||
const normalizedUser = {
|
||||
id: parseInt(userInfo.id) || null,
|
||||
account: userInfo.account || '',
|
||||
name: userInfo.name || '',
|
||||
group_id: userInfo.group_id || '',
|
||||
type: 'backend',
|
||||
tid: userInfo.tid || '',
|
||||
avatar: userInfo.avatar || ''
|
||||
}
|
||||
|
||||
// 使用后端返回的真实 JWT token
|
||||
const accessToken = loginData.token || ''
|
||||
|
||||
token.value = accessToken
|
||||
isLoggedIn.value = !!accessToken
|
||||
localStorage.setItem('token', accessToken)
|
||||
|
||||
Object.assign(user, normalizedUser)
|
||||
localStorage.setItem('userInfo', JSON.stringify(normalizedUser))
|
||||
}
|
||||
|
||||
// 设置 token(兼容旧代码)
|
||||
function setToken(newToken) {
|
||||
token.value = newToken
|
||||
isLoggedIn.value = true
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 清除登录信息
|
||||
function clearToken() {
|
||||
token.value = ''
|
||||
isLoggedIn.value = false
|
||||
Object.assign(user, defaultUser)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
function checkAuth() {
|
||||
const storedToken = localStorage.getItem('token')
|
||||
if (storedToken) {
|
||||
token.value = storedToken
|
||||
isLoggedIn.value = true
|
||||
loadUserFromCache()
|
||||
} else {
|
||||
token.value = ''
|
||||
isLoggedIn.value = false
|
||||
Object.assign(user, defaultUser)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
function updateUserInfo(userInfo) {
|
||||
Object.assign(user, userInfo)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
isLoggedIn,
|
||||
user,
|
||||
setLoginInfo,
|
||||
setToken,
|
||||
clearToken,
|
||||
checkAuth,
|
||||
updateUserInfo
|
||||
}
|
||||
})
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
// 用户信息类型
|
||||
const defaultUser = {
|
||||
id:'',
|
||||
account: '',
|
||||
name: '',
|
||||
group_id: '',
|
||||
type: 'backend',
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const isLoggedIn = ref(!!token.value)
|
||||
const user = reactive({ ...defaultUser })
|
||||
|
||||
// 从缓存加载用户信息
|
||||
function loadUserFromCache() {
|
||||
const cachedUser = localStorage.getItem('userInfo')
|
||||
if (cachedUser) {
|
||||
try {
|
||||
const userInfo = JSON.parse(cachedUser)
|
||||
Object.assign(user, userInfo)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info from cache:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载用户信息
|
||||
loadUserFromCache()
|
||||
|
||||
// 保存登录信息(token 和用户信息)
|
||||
function setLoginInfo(loginData) {
|
||||
const userInfo = loginData.user || loginData
|
||||
|
||||
const normalizedUser = {
|
||||
id: parseInt(userInfo.id) || null,
|
||||
account: userInfo.account || '',
|
||||
name: userInfo.name || '',
|
||||
group_id: userInfo.group_id || '',
|
||||
type: 'backend',
|
||||
tid: userInfo.tid || '',
|
||||
avatar: userInfo.avatar || ''
|
||||
}
|
||||
|
||||
// 使用后端返回的真实 JWT token
|
||||
const accessToken = loginData.token || ''
|
||||
|
||||
token.value = accessToken
|
||||
isLoggedIn.value = !!accessToken
|
||||
localStorage.setItem('token', accessToken)
|
||||
|
||||
Object.assign(user, normalizedUser)
|
||||
localStorage.setItem('userInfo', JSON.stringify(normalizedUser))
|
||||
}
|
||||
|
||||
// 设置 token(兼容旧代码)
|
||||
function setToken(newToken) {
|
||||
token.value = newToken
|
||||
isLoggedIn.value = true
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 清除登录信息
|
||||
function clearToken() {
|
||||
token.value = ''
|
||||
isLoggedIn.value = false
|
||||
Object.assign(user, defaultUser)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
function checkAuth() {
|
||||
const storedToken = localStorage.getItem('token')
|
||||
if (storedToken) {
|
||||
token.value = storedToken
|
||||
isLoggedIn.value = true
|
||||
loadUserFromCache()
|
||||
} else {
|
||||
token.value = ''
|
||||
isLoggedIn.value = false
|
||||
Object.assign(user, defaultUser)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
function updateUserInfo(userInfo) {
|
||||
Object.assign(user, userInfo)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
isLoggedIn,
|
||||
user,
|
||||
setLoginInfo,
|
||||
setToken,
|
||||
clearToken,
|
||||
checkAuth,
|
||||
updateUserInfo
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+196
-196
@@ -1,197 +1,197 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
|
||||
// ========== 全局状态 Store ==========
|
||||
function initState() {
|
||||
return {
|
||||
isCollapse: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const useAllDataStore = defineStore('allData', () => {
|
||||
const state = reactive(initState());
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
return {
|
||||
state,
|
||||
count,
|
||||
doubleCount,
|
||||
increment,
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 多标签页 Tabs Store ==========
|
||||
import { defineStore as defineTabsStore } from 'pinia';
|
||||
import { ref as vueRef } from 'vue';
|
||||
|
||||
/**
|
||||
* 多标签页Tabs状态管理
|
||||
* tabList每个tab结构: {
|
||||
* title: 标签显示名,
|
||||
* fullPath: 路由路径(唯一key),
|
||||
* name: 路由name,
|
||||
* icon: 图标(可选)
|
||||
* }
|
||||
*/
|
||||
export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
// 固定首页tab
|
||||
const defaultDashboardPath = '/home';
|
||||
|
||||
// 从 localStorage 恢复 tabs 状态
|
||||
function loadTabsFromStorage() {
|
||||
try {
|
||||
const savedTabs = localStorage.getItem('tabs_list');
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
if (savedTabs) {
|
||||
const tabs = JSON.parse(savedTabs);
|
||||
// 确保至少包含首页
|
||||
const hasHome = tabs.some(t => t.fullPath === defaultDashboardPath);
|
||||
if (!hasHome) {
|
||||
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Home' });
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('恢复 tabs 失败:', e);
|
||||
}
|
||||
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Home' }];
|
||||
}
|
||||
|
||||
// 保存 tabs 到 localStorage
|
||||
function saveTabsToStorage(tabs, active) {
|
||||
try {
|
||||
localStorage.setItem('tabs_list', JSON.stringify(tabs));
|
||||
if (active) {
|
||||
localStorage.setItem('active_tab', active);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('保存 tabs 失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const tabList = vueRef(loadTabsFromStorage());
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
const activeTab = vueRef(savedActiveTab || defaultDashboardPath);
|
||||
|
||||
// 添加tab,若已存在则激活
|
||||
function addTab(tab) {
|
||||
const exist = tabList.value.find((t) => t.fullPath === tab.fullPath);
|
||||
if (!exist) {
|
||||
tabList.value.push(tab);
|
||||
}
|
||||
activeTab.value = tab.fullPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 删除指定tab并切换激活tab
|
||||
function removeTab(fullPath) {
|
||||
const idx = tabList.value.findIndex((t) => t.fullPath === fullPath);
|
||||
if (idx > -1) {
|
||||
tabList.value.splice(idx, 1);
|
||||
// 只在关闭当前激活tab时切换激活tab
|
||||
if (activeTab.value === fullPath) {
|
||||
if (tabList.value.length > 0) {
|
||||
// 优先激活右侧(如无则激活左侧)
|
||||
const newIdx = idx >= tabList.value.length ? tabList.value.length - 1 : idx;
|
||||
activeTab.value = tabList.value[newIdx].fullPath;
|
||||
} else {
|
||||
// 全部关闭,兜底首页
|
||||
activeTab.value = defaultDashboardPath;
|
||||
}
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭其他,只留首页和当前激活tab
|
||||
function closeOthers() {
|
||||
tabList.value = tabList.value.filter(
|
||||
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
|
||||
);
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 关闭左侧(关闭指定tab左侧的所有tab,保留首页和目标tab)
|
||||
function closeLeft(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其右侧的所有tab
|
||||
const beforeIndex = tabList.value.slice(0, targetIndex);
|
||||
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath);
|
||||
|
||||
if (hasCloseableLeft) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index >= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧(关闭指定tab右侧的所有tab,保留首页和目标tab)
|
||||
function closeRight(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其左侧的所有tab
|
||||
const afterIndex = tabList.value.slice(targetIndex + 1);
|
||||
const hasCloseableRight = afterIndex.length > 0;
|
||||
|
||||
if (hasCloseableRight) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index <= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部,只留首页
|
||||
function closeAll() {
|
||||
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
|
||||
activeTab.value = defaultDashboardPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 设置激活tab(不触发路由跳转,仅用于更新状态)
|
||||
function setActiveTab(fullPath) {
|
||||
activeTab.value = fullPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 重置 tabs store 到初始状态(登出时使用)
|
||||
function resetTabs() {
|
||||
tabList.value = [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
|
||||
activeTab.value = defaultDashboardPath;
|
||||
// 清除 localStorage 中的 tabs 数据
|
||||
localStorage.removeItem('tabs_list');
|
||||
localStorage.removeItem('active_tab');
|
||||
}
|
||||
|
||||
return {
|
||||
tabList,
|
||||
activeTab,
|
||||
addTab,
|
||||
removeTab,
|
||||
closeOthers,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeAll,
|
||||
setActiveTab,
|
||||
saveTabsToStorage,
|
||||
resetTabs,
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 菜单 Menu Store ==========
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
|
||||
// ========== 全局状态 Store ==========
|
||||
function initState() {
|
||||
return {
|
||||
isCollapse: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const useAllDataStore = defineStore('allData', () => {
|
||||
const state = reactive(initState());
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
return {
|
||||
state,
|
||||
count,
|
||||
doubleCount,
|
||||
increment,
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 多标签页 Tabs Store ==========
|
||||
import { defineStore as defineTabsStore } from 'pinia';
|
||||
import { ref as vueRef } from 'vue';
|
||||
|
||||
/**
|
||||
* 多标签页Tabs状态管理
|
||||
* tabList每个tab结构: {
|
||||
* title: 标签显示名,
|
||||
* fullPath: 路由路径(唯一key),
|
||||
* name: 路由name,
|
||||
* icon: 图标(可选)
|
||||
* }
|
||||
*/
|
||||
export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
// 固定首页tab
|
||||
const defaultDashboardPath = '/home';
|
||||
|
||||
// 从 localStorage 恢复 tabs 状态
|
||||
function loadTabsFromStorage() {
|
||||
try {
|
||||
const savedTabs = localStorage.getItem('tabs_list');
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
if (savedTabs) {
|
||||
const tabs = JSON.parse(savedTabs);
|
||||
// 确保至少包含首页
|
||||
const hasHome = tabs.some(t => t.fullPath === defaultDashboardPath);
|
||||
if (!hasHome) {
|
||||
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Home' });
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('恢复 tabs 失败:', e);
|
||||
}
|
||||
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Home' }];
|
||||
}
|
||||
|
||||
// 保存 tabs 到 localStorage
|
||||
function saveTabsToStorage(tabs, active) {
|
||||
try {
|
||||
localStorage.setItem('tabs_list', JSON.stringify(tabs));
|
||||
if (active) {
|
||||
localStorage.setItem('active_tab', active);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('保存 tabs 失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const tabList = vueRef(loadTabsFromStorage());
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
const activeTab = vueRef(savedActiveTab || defaultDashboardPath);
|
||||
|
||||
// 添加tab,若已存在则激活
|
||||
function addTab(tab) {
|
||||
const exist = tabList.value.find((t) => t.fullPath === tab.fullPath);
|
||||
if (!exist) {
|
||||
tabList.value.push(tab);
|
||||
}
|
||||
activeTab.value = tab.fullPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 删除指定tab并切换激活tab
|
||||
function removeTab(fullPath) {
|
||||
const idx = tabList.value.findIndex((t) => t.fullPath === fullPath);
|
||||
if (idx > -1) {
|
||||
tabList.value.splice(idx, 1);
|
||||
// 只在关闭当前激活tab时切换激活tab
|
||||
if (activeTab.value === fullPath) {
|
||||
if (tabList.value.length > 0) {
|
||||
// 优先激活右侧(如无则激活左侧)
|
||||
const newIdx = idx >= tabList.value.length ? tabList.value.length - 1 : idx;
|
||||
activeTab.value = tabList.value[newIdx].fullPath;
|
||||
} else {
|
||||
// 全部关闭,兜底首页
|
||||
activeTab.value = defaultDashboardPath;
|
||||
}
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭其他,只留首页和当前激活tab
|
||||
function closeOthers() {
|
||||
tabList.value = tabList.value.filter(
|
||||
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
|
||||
);
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 关闭左侧(关闭指定tab左侧的所有tab,保留首页和目标tab)
|
||||
function closeLeft(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其右侧的所有tab
|
||||
const beforeIndex = tabList.value.slice(0, targetIndex);
|
||||
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath);
|
||||
|
||||
if (hasCloseableLeft) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index >= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧(关闭指定tab右侧的所有tab,保留首页和目标tab)
|
||||
function closeRight(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其左侧的所有tab
|
||||
const afterIndex = tabList.value.slice(targetIndex + 1);
|
||||
const hasCloseableRight = afterIndex.length > 0;
|
||||
|
||||
if (hasCloseableRight) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index <= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = targetFullPath;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部,只留首页
|
||||
function closeAll() {
|
||||
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
|
||||
activeTab.value = defaultDashboardPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 设置激活tab(不触发路由跳转,仅用于更新状态)
|
||||
function setActiveTab(fullPath) {
|
||||
activeTab.value = fullPath;
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 重置 tabs store 到初始状态(登出时使用)
|
||||
function resetTabs() {
|
||||
tabList.value = [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
|
||||
activeTab.value = defaultDashboardPath;
|
||||
// 清除 localStorage 中的 tabs 数据
|
||||
localStorage.removeItem('tabs_list');
|
||||
localStorage.removeItem('active_tab');
|
||||
}
|
||||
|
||||
return {
|
||||
tabList,
|
||||
activeTab,
|
||||
addTab,
|
||||
removeTab,
|
||||
closeOthers,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeAll,
|
||||
setActiveTab,
|
||||
saveTabsToStorage,
|
||||
resetTabs,
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 菜单 Menu Store ==========
|
||||
export { useMenuStore } from './menu';
|
||||
+237
-237
@@ -1,237 +1,237 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
// import { getUserInfo } from '@/utils/auth'
|
||||
import { getMenus } from '@/api/menu';
|
||||
|
||||
export const useMenuStore = defineStore('menu', () => {
|
||||
// 菜单数据
|
||||
const menus = ref([]);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 加载错误
|
||||
const error = ref(null);
|
||||
|
||||
// 正在加载的 Promise(用于避免重复请求)
|
||||
let loadingPromise = null;
|
||||
|
||||
// 菜单缓存 key(基于用户类型和角色ID)
|
||||
const getCacheKey = () => {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.group_id || 0;
|
||||
return `menu_cache_${loginType}_${roleId}`;
|
||||
} catch (e) {
|
||||
return 'menu_cache_default';
|
||||
}
|
||||
};
|
||||
|
||||
// 从缓存加载菜单
|
||||
const loadFromCache = () => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
const cached = localStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
const menuData = JSON.parse(cached);
|
||||
// 检查缓存是否过期(5分钟过期)
|
||||
if (menuData.timestamp && Date.now() - menuData.timestamp < 5 * 60 * 1000) {
|
||||
return menuData.menus;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载菜单缓存失败:', e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 保存菜单到缓存
|
||||
const saveToCache = (menuData) => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
localStorage.setItem(cacheKey, JSON.stringify({
|
||||
menus: menuData,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('保存菜单缓存失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 清除菜单缓存
|
||||
const clearCache = () => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
localStorage.removeItem(cacheKey);
|
||||
// 也清除其他可能的缓存key(兼容旧代码)
|
||||
localStorage.removeItem('menu_cache');
|
||||
} catch (e) {
|
||||
console.warn('清除菜单缓存失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// 从 API 加载菜单(核心方法,确保只请求一次)
|
||||
const fetchMenus = async (forceRefresh = false) => {
|
||||
// 如果已经有正在加载的请求,直接返回该 Promise
|
||||
if (loadingPromise && !forceRefresh) {
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
// 如果不强制刷新,先尝试从缓存加载
|
||||
if (!forceRefresh) {
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
menus.value = cachedMenus;
|
||||
return Promise.resolve(cachedMenus);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果正在加载且不是强制刷新,返回现有的 Promise
|
||||
if (loading.value && !forceRefresh) {
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
// 创建新的加载 Promise
|
||||
loadingPromise = (async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userInfo = getUserInfo();
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.group_id || 0;
|
||||
|
||||
let res;
|
||||
|
||||
// 检查用户ID是否存在
|
||||
if (!userInfo.id) {
|
||||
throw new Error('用户ID不存在,请重新登录');
|
||||
}
|
||||
|
||||
// 用户登录,使用 getMenus 接口
|
||||
res = await getMenus(userInfo.id);
|
||||
|
||||
// 检查响应格式
|
||||
if (!res) {
|
||||
throw new Error('获取菜单失败:服务器无响应');
|
||||
}
|
||||
|
||||
// 检查后端返回的 code 字段
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '获取菜单失败');
|
||||
}
|
||||
|
||||
// 如果 code 为 200,检查 data
|
||||
if (res.code === 200) {
|
||||
// data 可能是空数组,这也是有效的
|
||||
if (res.data !== undefined && res.data !== null) {
|
||||
// 确保 data 是数组
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
// 直接使用后端返回的树形结构数据,不需要额外过滤
|
||||
menus.value = menuData;
|
||||
// 保存到缓存
|
||||
saveToCache(menuData);
|
||||
return menuData;
|
||||
} else {
|
||||
// data 为 null 或 undefined,使用空数组
|
||||
console.warn('菜单数据为空,使用空数组');
|
||||
menus.value = [];
|
||||
saveToCache([]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果响应格式不符合预期,尝试直接使用 res.data
|
||||
if (res.data !== undefined) {
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
const filtered = menuData.filter(m => (m.isShow ?? 1) !== 0);
|
||||
menus.value = filtered;
|
||||
saveToCache(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 如果都不符合,抛出错误
|
||||
throw new Error(res.message || '获取菜单失败:响应格式错误');
|
||||
} catch (err) {
|
||||
error.value = err.message || '获取菜单失败';
|
||||
console.error('获取菜单失败:', err);
|
||||
console.error('错误详情:', {
|
||||
message: err.message,
|
||||
response: err.response,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// 如果是 token 无效错误,不使用缓存,直接抛出
|
||||
if (err.message === 'token无效' || err.response?.status === 401) {
|
||||
clearCache();
|
||||
menus.value = [];
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 如果出错,尝试使用缓存数据
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
console.warn('使用缓存的菜单数据');
|
||||
menus.value = cachedMenus;
|
||||
return cachedMenus;
|
||||
}
|
||||
|
||||
// 如果连缓存都没有,设置空数组,避免页面崩溃
|
||||
menus.value = [];
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadingPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
// 刷新菜单(强制从 API 获取)
|
||||
const refreshMenus = async () => {
|
||||
clearCache();
|
||||
return await fetchMenus(true);
|
||||
};
|
||||
|
||||
// 重置菜单 store(登出时使用)
|
||||
const resetMenus = () => {
|
||||
menus.value = [];
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
loadingPromise = null;
|
||||
clearCache();
|
||||
};
|
||||
|
||||
// 计算属性:获取菜单列表
|
||||
const menuList = computed(() => menus.value);
|
||||
|
||||
// 计算属性:菜单是否已加载
|
||||
const isLoaded = computed(() => menus.value.length > 0);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
menus: menuList,
|
||||
loading,
|
||||
error,
|
||||
isLoaded,
|
||||
|
||||
// 方法
|
||||
fetchMenus,
|
||||
refreshMenus,
|
||||
resetMenus,
|
||||
clearCache,
|
||||
loadFromCache,
|
||||
};
|
||||
});
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
// import { getUserInfo } from '@/utils/auth'
|
||||
import { getMenus } from '@/api/menu';
|
||||
|
||||
export const useMenuStore = defineStore('menu', () => {
|
||||
// 菜单数据
|
||||
const menus = ref([]);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 加载错误
|
||||
const error = ref(null);
|
||||
|
||||
// 正在加载的 Promise(用于避免重复请求)
|
||||
let loadingPromise = null;
|
||||
|
||||
// 菜单缓存 key(基于用户类型和角色ID)
|
||||
const getCacheKey = () => {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.group_id || 0;
|
||||
return `menu_cache_${loginType}_${roleId}`;
|
||||
} catch (e) {
|
||||
return 'menu_cache_default';
|
||||
}
|
||||
};
|
||||
|
||||
// 从缓存加载菜单
|
||||
const loadFromCache = () => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
const cached = localStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
const menuData = JSON.parse(cached);
|
||||
// 检查缓存是否过期(5分钟过期)
|
||||
if (menuData.timestamp && Date.now() - menuData.timestamp < 5 * 60 * 1000) {
|
||||
return menuData.menus;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载菜单缓存失败:', e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 保存菜单到缓存
|
||||
const saveToCache = (menuData) => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
localStorage.setItem(cacheKey, JSON.stringify({
|
||||
menus: menuData,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('保存菜单缓存失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 清除菜单缓存
|
||||
const clearCache = () => {
|
||||
try {
|
||||
const cacheKey = getCacheKey();
|
||||
localStorage.removeItem(cacheKey);
|
||||
// 也清除其他可能的缓存key(兼容旧代码)
|
||||
localStorage.removeItem('menu_cache');
|
||||
} catch (e) {
|
||||
console.warn('清除菜单缓存失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// 从 API 加载菜单(核心方法,确保只请求一次)
|
||||
const fetchMenus = async (forceRefresh = false) => {
|
||||
// 如果已经有正在加载的请求,直接返回该 Promise
|
||||
if (loadingPromise && !forceRefresh) {
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
// 如果不强制刷新,先尝试从缓存加载
|
||||
if (!forceRefresh) {
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
menus.value = cachedMenus;
|
||||
return Promise.resolve(cachedMenus);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果正在加载且不是强制刷新,返回现有的 Promise
|
||||
if (loading.value && !forceRefresh) {
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
// 创建新的加载 Promise
|
||||
loadingPromise = (async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const userInfo = getUserInfo();
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.group_id || 0;
|
||||
|
||||
let res;
|
||||
|
||||
// 检查用户ID是否存在
|
||||
if (!userInfo.id) {
|
||||
throw new Error('用户ID不存在,请重新登录');
|
||||
}
|
||||
|
||||
// 用户登录,使用 getMenus 接口
|
||||
res = await getMenus(userInfo.id);
|
||||
|
||||
// 检查响应格式
|
||||
if (!res) {
|
||||
throw new Error('获取菜单失败:服务器无响应');
|
||||
}
|
||||
|
||||
// 检查后端返回的 code 字段
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '获取菜单失败');
|
||||
}
|
||||
|
||||
// 如果 code 为 200,检查 data
|
||||
if (res.code === 200) {
|
||||
// data 可能是空数组,这也是有效的
|
||||
if (res.data !== undefined && res.data !== null) {
|
||||
// 确保 data 是数组
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
// 直接使用后端返回的树形结构数据,不需要额外过滤
|
||||
menus.value = menuData;
|
||||
// 保存到缓存
|
||||
saveToCache(menuData);
|
||||
return menuData;
|
||||
} else {
|
||||
// data 为 null 或 undefined,使用空数组
|
||||
console.warn('菜单数据为空,使用空数组');
|
||||
menus.value = [];
|
||||
saveToCache([]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果响应格式不符合预期,尝试直接使用 res.data
|
||||
if (res.data !== undefined) {
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
const filtered = menuData.filter(m => (m.isShow ?? 1) !== 0);
|
||||
menus.value = filtered;
|
||||
saveToCache(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 如果都不符合,抛出错误
|
||||
throw new Error(res.message || '获取菜单失败:响应格式错误');
|
||||
} catch (err) {
|
||||
error.value = err.message || '获取菜单失败';
|
||||
console.error('获取菜单失败:', err);
|
||||
console.error('错误详情:', {
|
||||
message: err.message,
|
||||
response: err.response,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// 如果是 token 无效错误,不使用缓存,直接抛出
|
||||
if (err.message === 'token无效' || err.response?.status === 401) {
|
||||
clearCache();
|
||||
menus.value = [];
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 如果出错,尝试使用缓存数据
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
console.warn('使用缓存的菜单数据');
|
||||
menus.value = cachedMenus;
|
||||
return cachedMenus;
|
||||
}
|
||||
|
||||
// 如果连缓存都没有,设置空数组,避免页面崩溃
|
||||
menus.value = [];
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadingPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
// 刷新菜单(强制从 API 获取)
|
||||
const refreshMenus = async () => {
|
||||
clearCache();
|
||||
return await fetchMenus(true);
|
||||
};
|
||||
|
||||
// 重置菜单 store(登出时使用)
|
||||
const resetMenus = () => {
|
||||
menus.value = [];
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
loadingPromise = null;
|
||||
clearCache();
|
||||
};
|
||||
|
||||
// 计算属性:获取菜单列表
|
||||
const menuList = computed(() => menus.value);
|
||||
|
||||
// 计算属性:菜单是否已加载
|
||||
const isLoaded = computed(() => menus.value.length > 0);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
menus: menuList,
|
||||
loading,
|
||||
error,
|
||||
isLoaded,
|
||||
|
||||
// 方法
|
||||
fetchMenus,
|
||||
refreshMenus,
|
||||
resetMenus,
|
||||
clearCache,
|
||||
loadFromCache,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user