first commit

This commit is contained in:
2026-01-26 09:32:17 +08:00
commit 9429a700d0
2928 changed files with 30330 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
// 用户信息类型
const defaultUser = {
id:'',
account: '',
name: '',
group_id: '',
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 || userInfo.id) || null,
account: userInfo.account || userInfo.account,
name: userInfo.name || userInfo.name,
group_id: userInfo.group_id || userInfo.group_id,
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
}
})
+179
View File
@@ -0,0 +1,179 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getDictItemsByCode } from '@/api/dict'
/**
* 字典 Store
*
* 用于全局管理系统字典数据
* 缓存字典数据避免重复请求,提高性能
*
* 使用示例:
* const dictStore = useDictStore()
* const statusDict = await dictStore.getDictItems('user_status')
* const roleDict = dictStore.getDictItemsSync('user_role') // 已加载则同步返回
*/
export const useDictStore = defineStore('dict', () => {
// 字典缓存:{ dictCode: [...items] }
const dictCache = ref({})
// 正在加载的字典代码集合
const loadingCodes = ref(new Set())
/**
* 获取字典项(异步)
* @param {string} code - 字典编码,如 'user_status'
* @returns {Promise<Array>} 字典项数组
*/
async function getDictItems(code) {
// 如果缓存中已有,直接返回
if (dictCache.value[code]) {
return dictCache.value[code]
}
// 避免重复请求:如果已在加载中,等待
if (loadingCodes.value.has(code)) {
// 等待加载完成(最多 5 秒)
return await new Promise((resolve) => {
let count = 0
const timer = setInterval(() => {
if (dictCache.value[code]) {
clearInterval(timer)
resolve(dictCache.value[code])
}
count++
if (count > 50) {
clearInterval(timer)
resolve([])
}
}, 100)
})
}
// 标记为正在加载
loadingCodes.value.add(code)
try {
const res = await getDictItemsByCode(code)
let items = []
// 兼容不同的 API 响应格式
if (res?.data && Array.isArray(res.data)) {
items = res.data
} else if (Array.isArray(res)) {
items = res
} else if (res?.data?.data && Array.isArray(res.data.data)) {
items = res.data.data
}
// 缓存字典项
dictCache.value[code] = items
// console.log(`✅ 字典 [${code}] 已加载,共 ${items.length} 项`)
return items
} catch (error) {
console.error(`❌ 加载字典 [${code}] 失败:`, error)
dictCache.value[code] = []
return []
} finally {
// 移除加载标记
loadingCodes.value.delete(code)
}
}
/**
* 同步获取字典项(如果已缓存)
* @param {string} code - 字典编码
* @returns {Array} 字典项数组,未缓存则返回空数组
*/
function getDictItemsSync(code) {
return dictCache.value[code] || []
}
/**
* 预加载字典(在应用启动时调用)
* @param {Array<string>} codes - 字典编码数组
* @returns {Promise<void>}
*/
async function preloadDicts(codes) {
const promises = codes.map(code => getDictItems(code))
await Promise.all(promises)
}
/**
* 根据字典编码和值获取标签
* @param {string} code - 字典编码
* @param {any} value - 字典值
* @returns {string} 字典标签
*/
function getDictLabel(code, value) {
const items = getDictItemsSync(code)
if (!items.length) {
console.warn(`⚠️ 字典 [${code}] 未加载,无法获取标签`)
return String(value)
}
const item = items.find(i => String(i.dict_value) === String(value) || i.dict_value === value)
return item ? item.dict_label : String(value)
}
/**
* 根据字典编码和标签获取值
* @param {string} code - 字典编码
* @param {string} label - 字典标签
* @returns {any} 字典值
*/
function getDictValue(code, label) {
const items = getDictItemsSync(code)
if (!items.length) {
console.warn(`⚠️ 字典 [${code}] 未加载,无法获取值`)
return null
}
const item = items.find(i => i.dict_label === label)
return item ? item.dict_value : null
}
/**
* 清空缓存
* @param {string} code - 字典编码,不指定则清空所有
*/
function clearCache(code) {
if (code) {
delete dictCache.value[code]
// console.log(`✅ 已清除字典 [${code}] 缓存`)
} else {
dictCache.value = {}
// console.log(`✅ 已清除所有字典缓存`)
}
}
/**
* 刷新字典
* @param {string} code - 字典编码
* @returns {Promise<Array>}
*/
async function refreshDict(code) {
clearCache(code)
return getDictItems(code)
}
/**
* 获取所有已缓存的字典
* @returns {Object}
*/
const allDicts = computed(() => dictCache.value)
return {
dictCache,
loadingCodes,
getDictItems,
getDictItemsSync,
preloadDicts,
getDictLabel,
getDictValue,
clearCache,
refreshDict,
allDicts,
}
})
+197
View File
@@ -0,0 +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 = '/dashboard';
// 从 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 hasDashboard = tabs.some(t => t.fullPath === defaultDashboardPath);
if (!hasDashboard) {
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' });
}
return tabs;
}
} catch (e) {
console.warn('恢复 tabs 失败:', e);
}
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
}
// 保存 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';
+234
View File
@@ -0,0 +1,234 @@
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.role || 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;
// 注释掉后台更新缓存的逻辑,避免重复请求
// fetchMenus(true).catch(err => {
// console.warn('后台更新菜单失败:', err);
// });
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.role || 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
});
// 如果出错,尝试使用缓存数据
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,
};
});
View File