初始化
This commit is contained in:
86
src/store/dict.ts
Normal file
86
src/store/dict.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { getSimpleDictDataList } from '@/api/system/dict/data'
|
||||
|
||||
/** 字典项 */
|
||||
export interface DictItem {
|
||||
label: string
|
||||
value: string
|
||||
colorType?: string
|
||||
cssClass?: string
|
||||
}
|
||||
|
||||
/** 字典缓存类型 */
|
||||
export type DictCache = Record<string, DictItem[]>
|
||||
|
||||
export const useDictStore = defineStore(
|
||||
'dict',
|
||||
() => {
|
||||
const dictCache = ref<DictCache>({}) // 字典缓存
|
||||
const isLoaded = computed(() => Object.keys(dictCache.value).length > 0) // 是否已加载(基于 dictCache 非空判断)
|
||||
|
||||
/** 设置字典缓存 */
|
||||
const setDictCache = (dicts: DictCache) => {
|
||||
dictCache.value = dicts
|
||||
}
|
||||
|
||||
/** 通过 API 加载字典数据 */
|
||||
const loadDictCache = async () => {
|
||||
if (isLoaded.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const dicts = await getSimpleDictDataList()
|
||||
const dictCacheData: DictCache = {}
|
||||
dicts.forEach((dict: DictData) => {
|
||||
if (!dictCacheData[dict.dictType]) {
|
||||
dictCacheData[dict.dictType] = []
|
||||
}
|
||||
dictCacheData[dict.dictType].push({
|
||||
label: dict.label,
|
||||
value: dict.value,
|
||||
colorType: dict.colorType,
|
||||
cssClass: dict.cssClass,
|
||||
})
|
||||
})
|
||||
setDictCache(dictCacheData)
|
||||
} catch (error) {
|
||||
console.error('加载字典数据失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取字典选项列表 */
|
||||
const getDictOptions = (dictType: string): DictItem[] => {
|
||||
return dictCache.value[dictType] || []
|
||||
}
|
||||
|
||||
/** 获取字典数据对象 */
|
||||
const getDictData = (dictType: string, value: any): DictItem | undefined => {
|
||||
const dict = dictCache.value[dictType]
|
||||
if (!dict) {
|
||||
return undefined
|
||||
}
|
||||
return dict.find(d => d.value === value || d.value === String(value))
|
||||
}
|
||||
|
||||
/** 清空字典缓存 */
|
||||
const clearDictCache = () => {
|
||||
dictCache.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
dictCache,
|
||||
isLoaded,
|
||||
setDictCache,
|
||||
loadDictCache,
|
||||
getDictOptions,
|
||||
getDictData,
|
||||
clearDictCache,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
22
src/store/index.ts
Normal file
22
src/store/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化
|
||||
|
||||
const store = createPinia()
|
||||
store.use(
|
||||
createPersistedState({
|
||||
storage: {
|
||||
getItem: uni.getStorageSync,
|
||||
setItem: uni.setStorageSync,
|
||||
},
|
||||
}),
|
||||
)
|
||||
// 立即激活 Pinia 实例, 这样即使在 app.use(store)之前调用 store 也能正常工作 (解决APP端白屏问题)
|
||||
setActivePinia(store)
|
||||
|
||||
export default store
|
||||
|
||||
// 模块统一导出
|
||||
export * from './dict'
|
||||
export * from './theme'
|
||||
export * from './token'
|
||||
export * from './user'
|
||||
42
src/store/theme.ts
Normal file
42
src/store/theme.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ConfigProviderThemeVars } from 'wot-design-uni'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useThemeStore = defineStore(
|
||||
'theme-store',
|
||||
() => {
|
||||
/** 主题 */
|
||||
const theme = ref<'light' | 'dark'>('light')
|
||||
|
||||
/** 主题变量 */
|
||||
const themeVars = ref<ConfigProviderThemeVars>({
|
||||
// colorTheme: 'red',
|
||||
// buttonPrimaryBgColor: '#07c160',
|
||||
// buttonPrimaryColor: '#07c160',
|
||||
})
|
||||
|
||||
/** 设置主题变量 */
|
||||
const setThemeVars = (partialVars: Partial<ConfigProviderThemeVars>) => {
|
||||
themeVars.value = { ...themeVars.value, ...partialVars }
|
||||
}
|
||||
|
||||
/** 切换主题 */
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
return {
|
||||
/** 设置主题变量 */
|
||||
setThemeVars,
|
||||
/** 切换主题 */
|
||||
toggleTheme,
|
||||
/** 主题变量 */
|
||||
themeVars,
|
||||
/** 主题 */
|
||||
theme,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
355
src/store/token.ts
Normal file
355
src/store/token.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
/* eslint-disable brace-style */ // 原因:unibest 官方维护的代码,尽量不要大概,避免难以合并
|
||||
import type {
|
||||
AuthLoginReqVO,
|
||||
AuthRegisterReqVO,
|
||||
AuthSmsLoginReqVO,
|
||||
ILoginForm,
|
||||
} from '@/api/login'
|
||||
import type { IAuthLoginRes } from '@/api/types/login'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue' // 修复:导入 computed
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import {
|
||||
login as _login,
|
||||
logout as _logout,
|
||||
refreshToken as _refreshToken,
|
||||
wxLogin as _wxLogin,
|
||||
getWxCode,
|
||||
register,
|
||||
smsLogin,
|
||||
} from '@/api/login'
|
||||
import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
|
||||
import { isDoubleTokenMode } from '@/utils'
|
||||
import { useDictStore } from './dict'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
// 初始化状态
|
||||
const tokenInfoState = isDoubleTokenMode
|
||||
? {
|
||||
accessToken: '',
|
||||
// accessExpiresIn: 0,
|
||||
refreshToken: '',
|
||||
// refreshExpiresIn: 0,
|
||||
expiresTime: 0,
|
||||
}
|
||||
: {
|
||||
token: '',
|
||||
expiresIn: 0,
|
||||
}
|
||||
|
||||
export const useTokenStore = defineStore(
|
||||
'token',
|
||||
() => {
|
||||
const toast = useToast()
|
||||
// 定义用户信息
|
||||
const tokenInfo = ref<IAuthLoginRes>({ ...tokenInfoState })
|
||||
// 设置用户信息
|
||||
const setTokenInfo = (val: IAuthLoginRes) => {
|
||||
tokenInfo.value = val
|
||||
|
||||
// 计算并存储过期时间
|
||||
const now = Date.now()
|
||||
if (isSingleTokenRes(val)) {
|
||||
// 单token模式
|
||||
const expireTime = now + val.expiresIn * 1000
|
||||
uni.setStorageSync('accessTokenExpireTime', expireTime)
|
||||
}
|
||||
else if (isDoubleTokenRes(val)) {
|
||||
// 双token模式
|
||||
const accessExpireTime = val.expiresTime
|
||||
// const refreshExpireTime = now + val.refreshExpiresIn * 1000
|
||||
uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
|
||||
// uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
|
||||
// add by 芋艿:目前后端没有返回 refreshToken 的过期时间,所以这里暂时不存储 refreshToken 过期时间
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断token是否过期
|
||||
*/
|
||||
const isTokenExpired = computed(() => {
|
||||
if (!tokenInfo.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const expireTime = uni.getStorageSync('accessTokenExpireTime')
|
||||
|
||||
if (!expireTime)
|
||||
return true
|
||||
return now >= expireTime
|
||||
})
|
||||
|
||||
/**
|
||||
* 判断refreshToken是否过期
|
||||
*/
|
||||
const isRefreshTokenExpired = computed(() => {
|
||||
if (!isDoubleTokenMode)
|
||||
return true
|
||||
|
||||
// const now = Date.now()
|
||||
// const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
|
||||
//
|
||||
// if (!refreshExpireTime)
|
||||
// return true
|
||||
// return now >= refreshExpireTime
|
||||
// add by 芋艿:目前后端没有返回 refreshToken 的过期时间,所以这里暂时不做过期判断,先全部返回 false 非过期
|
||||
return false
|
||||
})
|
||||
|
||||
/**
|
||||
* 登录成功后处理逻辑
|
||||
* @param tokenInfo 登录返回的token信息
|
||||
*/
|
||||
async function _postLogin(tokenInfo: IAuthLoginRes) {
|
||||
// 设置认证信息
|
||||
setTokenInfo(tokenInfo)
|
||||
// 获取用户信息
|
||||
const userStore = useUserStore()
|
||||
await userStore.fetchUserInfo()
|
||||
// add by 芋艿:加载字典数据(异步)
|
||||
const dictStore = useDictStore()
|
||||
dictStore.loadDictCache().then()
|
||||
// ✅ 登录成功后查询门店并选择第一条数据存入缓存
|
||||
try {
|
||||
const { findByProductType } = await import('@/api/tire/store')
|
||||
const storeList = await findByProductType()
|
||||
if (storeList && storeList.length > 0) {
|
||||
// 选择第一条门店数据并存入缓存
|
||||
const firstStore = storeList[0]
|
||||
userStore.setStore(firstStore.id!, firstStore.storeName || null)
|
||||
uni.setStorageSync('storeId', firstStore.id)
|
||||
uni.setStorageSync('storeName', firstStore.storeName)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载门店列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录:账号登录、注册登录、短信登录、三方登录等
|
||||
* 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
|
||||
* (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
||||
* @param loginForm 登录参数
|
||||
* @returns 登录结果
|
||||
*/
|
||||
const login = async (loginForm: ILoginForm) => {
|
||||
let typeName = ''
|
||||
try {
|
||||
let res: IAuthLoginRes
|
||||
switch (loginForm.type) {
|
||||
case 'register': {
|
||||
res = await register(loginForm as AuthRegisterReqVO)
|
||||
typeName = '注册'
|
||||
break
|
||||
}
|
||||
case 'sms': {
|
||||
res = await smsLogin(loginForm as AuthSmsLoginReqVO)
|
||||
typeName = '注册'
|
||||
break
|
||||
}
|
||||
default: {
|
||||
res = await _login(loginForm as AuthLoginReqVO)
|
||||
typeName = '登录'
|
||||
}
|
||||
}
|
||||
// console.log('普通登录-res: ', res)
|
||||
await _postLogin(res)
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: `${typeName}成功`,
|
||||
// icon: 'success',
|
||||
// })
|
||||
toast.success(`${typeName}成功`)
|
||||
return res
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`${typeName}失败:`, error)
|
||||
// 注释 by 芋艿:避免覆盖 http.ts 中的错误提示
|
||||
// uni.showToast({
|
||||
// title: `${typeName}失败,请重试`,
|
||||
// icon: 'error',
|
||||
// })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
|
||||
* (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
||||
* @returns 登录结果
|
||||
*/
|
||||
const wxLogin = async () => {
|
||||
try {
|
||||
// 获取微信小程序登录的code
|
||||
const code = await getWxCode()
|
||||
console.log('微信登录-code: ', code)
|
||||
const res = await _wxLogin(code)
|
||||
console.log('微信登录-res: ', res)
|
||||
await _postLogin(res)
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: '登录成功',
|
||||
// icon: 'success',
|
||||
// })
|
||||
toast.success('登录成功')
|
||||
return res
|
||||
}
|
||||
catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: '微信登录失败,请重试',
|
||||
// icon: 'error',
|
||||
// })
|
||||
toast.error('微信登录失败,请重试')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录 并 删除用户信息
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
// TODO 实现自己的退出登录逻辑
|
||||
await _logout()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
}
|
||||
finally {
|
||||
// 无论成功失败,都需要清除本地token信息
|
||||
// 清除存储的过期时间
|
||||
uni.removeStorageSync('accessTokenExpireTime')
|
||||
// uni.removeStorageSync('refreshTokenExpireTime')
|
||||
console.log('退出登录-清除用户信息')
|
||||
tokenInfo.value = { ...tokenInfoState }
|
||||
uni.removeStorageSync('token')
|
||||
const userStore = useUserStore()
|
||||
userStore.clearUserInfo()
|
||||
// add by 芋艿:清空字典缓存
|
||||
const dictStore = useDictStore()
|
||||
dictStore.clearDictCache()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @returns 刷新结果
|
||||
*/
|
||||
const refreshToken = async () => {
|
||||
if (!isDoubleTokenMode) {
|
||||
console.error('单token模式不支持刷新token')
|
||||
throw new Error('单token模式不支持刷新token')
|
||||
}
|
||||
|
||||
try {
|
||||
// 安全检查,确保refreshToken存在
|
||||
if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
|
||||
throw new Error('无效的refreshToken')
|
||||
}
|
||||
|
||||
const refreshToken = tokenInfo.value.refreshToken
|
||||
const res = await _refreshToken(refreshToken)
|
||||
console.log('刷新token-res: ', res)
|
||||
setTokenInfo(res)
|
||||
return res
|
||||
}
|
||||
catch (error) {
|
||||
console.error('刷新token失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的token
|
||||
* 注意:在computed中不直接调用异步函数,只做状态判断
|
||||
* 实际的刷新操作应由调用方处理
|
||||
*/
|
||||
const getValidToken = computed(() => {
|
||||
// token已过期,返回空
|
||||
if (isTokenExpired.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!isDoubleTokenMode) {
|
||||
return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
|
||||
}
|
||||
else {
|
||||
return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查是否有登录信息(不考虑token是否过期)
|
||||
*/
|
||||
const hasLoginInfo = computed(() => {
|
||||
if (!tokenInfo.value) {
|
||||
return false
|
||||
}
|
||||
if (isDoubleTokenMode) {
|
||||
return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
|
||||
}
|
||||
else {
|
||||
return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查是否已登录且token有效
|
||||
*/
|
||||
const hasValidLogin = computed(() => {
|
||||
console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value)
|
||||
if (isDoubleTokenMode) {
|
||||
// add by 芋艿:双令牌场景下,以刷新令牌过期为准。而刷新令牌是否过期,通过请求时返回 401 来判断(由于后端 refreshToken 不返回过期时间)
|
||||
// 即相比下面的判断方式,去掉了“!isTokenExpired.value”
|
||||
// 如果不这么做:访问令牌过期时(刷新令牌没过期),会导致刷新界面时,直接认为是令牌过期,导致跳转到登录界面
|
||||
return hasLoginInfo.value
|
||||
}
|
||||
return hasLoginInfo.value && !isTokenExpired.value
|
||||
})
|
||||
|
||||
/**
|
||||
* 尝试获取有效的token,如果过期且可刷新,则刷新token
|
||||
* @returns 有效的token或空字符串
|
||||
*/
|
||||
const tryGetValidToken = async (): Promise<string> => {
|
||||
if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) {
|
||||
try {
|
||||
await refreshToken()
|
||||
return getValidToken.value
|
||||
}
|
||||
catch (error) {
|
||||
console.error('尝试刷新token失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return getValidToken.value
|
||||
}
|
||||
|
||||
return {
|
||||
// 核心API方法
|
||||
login,
|
||||
wxLogin,
|
||||
logout,
|
||||
|
||||
// 认证状态判断(最常用的)
|
||||
hasLogin: hasValidLogin,
|
||||
|
||||
// 内部系统使用的方法
|
||||
refreshToken,
|
||||
tryGetValidToken,
|
||||
validToken: getValidToken,
|
||||
|
||||
// 调试或特殊场景可能需要直接访问的信息
|
||||
tokenInfo,
|
||||
setTokenInfo,
|
||||
}
|
||||
},
|
||||
{
|
||||
// 添加持久化配置,确保刷新页面后token信息不丢失
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
113
src/store/user.ts
Normal file
113
src/store/user.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { AuthPermissionInfo, IUserInfoRes } from '@/api/types/login'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
// getUserInfo,
|
||||
getAuthPermissionInfo,
|
||||
} from '@/api/login'
|
||||
|
||||
// 初始化状态
|
||||
const userInfoState: IUserInfoRes = {
|
||||
userId: -1,
|
||||
username: '',
|
||||
nickname: '',
|
||||
avatar: '/static/images/default-avatar.png', // TODO @芋艿:CDN 化
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'user',
|
||||
() => {
|
||||
// 定义用户信息
|
||||
const userInfo = ref<IUserInfoRes>({ ...userInfoState })
|
||||
const tenantId = ref<number | null>(null) // 租户编号
|
||||
const roles = ref<string[]>([]) // 角色标识列表
|
||||
const permissions = ref<string[]>([]) // 权限标识列表
|
||||
const favoriteMenus = ref<string[]>([]) // 常用菜单 key 列表
|
||||
const storeId = ref<number | null>(null) // 门店ID
|
||||
const storeName = ref<string | null>(null) // 门店名称
|
||||
|
||||
/** 设置用户信息 */
|
||||
const setUserInfo = (val: AuthPermissionInfo) => {
|
||||
// console.log('设置用户信息', val)
|
||||
// 若头像为空 则使用默认头像
|
||||
if (!val.user) {
|
||||
val.user.avatar = userInfoState.avatar
|
||||
}
|
||||
userInfo.value = val.user
|
||||
roles.value = val.roles
|
||||
permissions.value = val.permissions
|
||||
}
|
||||
|
||||
const setUserAvatar = (avatar: string) => {
|
||||
userInfo.value.avatar = avatar
|
||||
// console.log('设置用户头像', avatar)
|
||||
// console.log('userInfo', userInfo.value)
|
||||
}
|
||||
|
||||
/** 删除用户信息 */
|
||||
const clearUserInfo = () => {
|
||||
userInfo.value = { ...userInfoState }
|
||||
roles.value = []
|
||||
permissions.value = []
|
||||
storeId.value = null
|
||||
storeName.value = null
|
||||
uni.removeStorageSync('user')
|
||||
uni.removeStorageSync('storeId')
|
||||
uni.removeStorageSync('storeName')
|
||||
}
|
||||
|
||||
/** 设置门店ID和名称(缓存) */
|
||||
const setStore = (id: number | string | null, name?: string | null) => {
|
||||
const num = id === null || id === undefined || id === '' ? null : Number(id)
|
||||
storeId.value = num
|
||||
storeName.value = name ?? null
|
||||
if (num !== null) {
|
||||
uni.setStorageSync('storeId', num)
|
||||
} else {
|
||||
uni.removeStorageSync('storeId')
|
||||
}
|
||||
if (name) {
|
||||
uni.setStorageSync('storeName', name)
|
||||
} else {
|
||||
uni.removeStorageSync('storeName')
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置租户编号 */
|
||||
const setTenantId = (id: number) => {
|
||||
tenantId.value = id
|
||||
}
|
||||
|
||||
/** 设置常用菜单 */
|
||||
const setFavoriteMenus = (keys: string[]) => {
|
||||
favoriteMenus.value = keys
|
||||
}
|
||||
|
||||
/** 获取用户信息 */
|
||||
const fetchUserInfo = async () => {
|
||||
const res = await getAuthPermissionInfo()
|
||||
setUserInfo(res)
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
tenantId,
|
||||
roles,
|
||||
permissions,
|
||||
favoriteMenus,
|
||||
storeId,
|
||||
storeName,
|
||||
clearUserInfo,
|
||||
fetchUserInfo,
|
||||
setUserInfo,
|
||||
setUserAvatar,
|
||||
setTenantId,
|
||||
setFavoriteMenus,
|
||||
setStore,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user