front/src/store/modules/rbac-user.ts
zzh c4979a1fb4 feat: 面单标签设计与仓内操作页优化,接通模板打印链路
完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 17:49:18 +08:00

361 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import {
AuthService,
setAuthToken,
clearAuthToken,
OmsLoginType,
type LoginResultDto,
type UserInfoDto,
} from '@/api/rbac'
import { handleApiError } from '@/utils/rbac-error-handler'
import { getLoginRouteNameByType } from '@/utils/login-portal'
// Token 存储 key
const TOKEN_KEY = 'rbac_access_token'
const REFRESH_TOKEN_KEY = 'rbac_refresh_token'
const TOKEN_EXPIRES_KEY = 'rbac_token_expires_at'
const USER_INFO_KEY = 'rbac_userInfo'
// 兼容旧代码的 key
const OLD_TOKEN_KEY = 'rbac_token'
const LOGIN_TYPE_KEY = 'rbac_login_type'
const SSO_SESSION_FLAG = 'rbac_sso_session'
function isSsoSessionActive(): boolean {
return sessionStorage.getItem(SSO_SESSION_FLAG) === '1'
}
function getAuthStorage(): Storage {
return isSsoSessionActive() ? sessionStorage : localStorage
}
function readAuthItem(key: string): string {
if (isSsoSessionActive()) {
return sessionStorage.getItem(key) || ''
}
return localStorage.getItem(key) || ''
}
// 1小时无操作则认为会话应过期(前端主导的“空闲超时”,用于控制是否允许续期)
const KEEP_ALIVE_IDLE_MS = 60 * 60 * 1000
// 注意:tokenExpiresAt 在 saveTokens 里会提前 5 分钟到期刷新(当前实现是 -300s)
// 因此“真实 accessToken 过期时间”= tokenExpiresAt + 5min
const ACCESS_TOKEN_REAL_EXPIRES_AT_OFFSET_MS = 5 * 60 * 1000
export const useRbacUserStore = defineStore('rbacUser', () => {
const persistInSession = ref(isSsoSessionActive())
const accessToken = ref<string>(readAuthItem(TOKEN_KEY))
const refreshToken = ref<string>(readAuthItem(REFRESH_TOKEN_KEY))
const tokenExpiresAt = ref<number>(Number(readAuthItem(TOKEN_EXPIRES_KEY)) || 0)
// 仅在用户发生交互后才会开启;开启后允许续期,并在倒计时到达时登出
const keepAliveUntil = ref<number>(0)
const savedUserInfo = getAuthStorage().getItem(USER_INFO_KEY)
const userInfo = ref<UserInfoDto | null>(savedUserInfo ? JSON.parse(savedUserInfo) : null)
const loginPortalType = ref<OmsLoginType>(readStoredLoginType())
const permissions = computed(() => userInfo.value?.permissions || [])
const menus = computed(() => userInfo.value?.menus || [])
const isLoggedIn = computed(() => !!accessToken.value)
// 兼容旧代码
const token = computed(() => accessToken.value)
function readStoredLoginType(): OmsLoginType {
const raw = getAuthStorage().getItem(LOGIN_TYPE_KEY) ?? localStorage.getItem(LOGIN_TYPE_KEY)
if (raw === String(OmsLoginType.Customer)) return OmsLoginType.Customer
return OmsLoginType.User
}
function setLoginPortalType(type: OmsLoginType, options?: { sessionOnly?: boolean }) {
loginPortalType.value = type
if (options?.sessionOnly) {
persistInSession.value = true
sessionStorage.setItem(SSO_SESSION_FLAG, '1')
sessionStorage.setItem(LOGIN_TYPE_KEY, String(type))
return
}
persistInSession.value = false
sessionStorage.removeItem(SSO_SESSION_FLAG)
localStorage.setItem(LOGIN_TYPE_KEY, String(type))
}
function getLoginRouteName(): string {
return getLoginRouteNameByType(loginPortalType.value)
}
function isCustomerPortal(): boolean {
return loginPortalType.value === OmsLoginType.Customer
}
// 保存 Token 信息
function saveTokens(result: LoginResultDto, options?: { sessionOnly?: boolean }) {
if (!result.accessToken) {
return
}
accessToken.value = result.accessToken
refreshToken.value = result.refreshToken || ''
const expiresInSeconds =
typeof result.expiresIn === 'number' && result.expiresIn > 0 ? result.expiresIn : 3600
tokenExpiresAt.value = Date.now() + (expiresInSeconds - 300) * 1000
const storage = options?.sessionOnly ? sessionStorage : localStorage
if (options?.sessionOnly) {
persistInSession.value = true
sessionStorage.setItem(SSO_SESSION_FLAG, '1')
} else {
persistInSession.value = false
sessionStorage.removeItem(SSO_SESSION_FLAG)
}
storage.setItem(TOKEN_KEY, accessToken.value)
if (refreshToken.value) {
storage.setItem(REFRESH_TOKEN_KEY, refreshToken.value)
}
storage.setItem(TOKEN_EXPIRES_KEY, String(tokenExpiresAt.value))
setAuthToken(accessToken.value)
if (!options?.sessionOnly) {
// 兼容旧代码及 auth utils 的 isLogin 检查
localStorage.setItem(OLD_TOKEN_KEY, accessToken.value)
localStorage.setItem('token', accessToken.value)
}
}
// 真实 accessToken 过期时间(用于“无操作也要按时间跳登录”的兜底)
function getAccessTokenRealExpiresAt(): number {
if (!tokenExpiresAt.value) return 0
return tokenExpiresAt.value + ACCESS_TOKEN_REAL_EXPIRES_AT_OFFSET_MS
}
function isKeepAliveActive(): boolean {
return keepAliveUntil.value > 0 && Date.now() < keepAliveUntil.value
}
function enableKeepAlive() {
keepAliveUntil.value = Date.now() + KEEP_ALIVE_IDLE_MS
}
function disableKeepAlive() {
keepAliveUntil.value = 0
}
function getKeepAliveUntil(): number {
return keepAliveUntil.value
}
// 检查 Token 是否需要刷新
function isTokenExpiringSoon(): boolean {
return tokenExpiresAt.value > 0 && Date.now() >= tokenExpiresAt.value
}
function normalizeRefreshToken(raw: string): string | null {
let rt = raw
if (rt.startsWith('Bearer ')) {
rt = rt.slice('Bearer '.length)
}
const parts = rt.split('.')
if (parts.length !== 3 || parts.some((p) => !p || p.length < 8)) {
return null
}
if (rt.includes('[') || rt.includes(']') || rt.includes('{') || rt.includes('}')) {
return null
}
return rt
}
// 刷新 Token
async function refreshAccessToken(): Promise<boolean> {
if (!isKeepAliveActive()) {
return false
}
if (!refreshToken.value) {
return false
}
const rt = normalizeRefreshToken(refreshToken.value)
if (!rt) {
console.warn('[rbac] refreshToken format invalid, skip refresh')
logout()
return false
}
try {
const result = await AuthService.postRbacAuthRefreshToken({ refreshToken: rt })
if (result?.accessToken) {
saveTokens(result, { sessionOnly: persistInSession.value })
return true
}
return false
} catch {
logout()
return false
}
}
// 登录(OMS 管理端默认 type=User)
async function login(account: string, password: string, loginType: OmsLoginType = OmsLoginType.User) {
try {
const result = await AuthService.postRbacAuthLogin({
account,
password,
type: loginType,
})
saveTokens(result)
setLoginPortalType(loginType)
await getCurrentUser()
return true
} catch (error) {
handleApiError(error)
return false
}
}
/** 微信扫码登录成功后写入 Token 并拉取用户信息 */
async function loginByWxResult(result: LoginResultDto): Promise<boolean> {
try {
if (!result?.accessToken) {
return false
}
saveTokens(result)
setLoginPortalType(OmsLoginType.Customer)
await getCurrentUser()
return true
} catch (error) {
handleApiError(error)
return false
}
}
/** 管理员代登客户门户:Token 仅存 sessionStorage,避免覆盖管理端登录态 */
async function loginBySsoResult(result: LoginResultDto): Promise<boolean> {
try {
if (!result?.accessToken) {
return false
}
saveTokens(result, { sessionOnly: true })
setLoginPortalType(OmsLoginType.Customer, { sessionOnly: true })
await getCurrentUser()
return true
} catch (error) {
handleApiError(error)
return false
}
}
// 获取当前用户信息(基于 Token,无需 userId 参数)
async function getCurrentUser() {
try {
if (isTokenExpiringSoon()) {
await refreshAccessToken()
}
const data = await AuthService.getRbacAuthGetCurrentUser()
userInfo.value = data
getAuthStorage().setItem(USER_INFO_KEY, JSON.stringify(data))
return data
} catch (error) {
logout()
throw error
}
}
// 刷新用户信息(强制从服务器获取最新数据)
async function refreshUserInfo() {
try {
const data = await AuthService.getRbacAuthGetCurrentUser()
userInfo.value = data
getAuthStorage().setItem(USER_INFO_KEY, JSON.stringify(data))
return data
} catch (error) {
handleApiError(error)
throw error
}
}
// 登出
function logout() {
accessToken.value = ''
refreshToken.value = ''
tokenExpiresAt.value = 0
keepAliveUntil.value = 0
userInfo.value = null
persistInSession.value = false
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(REFRESH_TOKEN_KEY)
localStorage.removeItem(TOKEN_EXPIRES_KEY)
localStorage.removeItem(USER_INFO_KEY)
// 兼容旧代码
localStorage.removeItem(OLD_TOKEN_KEY)
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
sessionStorage.removeItem(SSO_SESSION_FLAG)
sessionStorage.removeItem(TOKEN_KEY)
sessionStorage.removeItem(REFRESH_TOKEN_KEY)
sessionStorage.removeItem(TOKEN_EXPIRES_KEY)
sessionStorage.removeItem(USER_INFO_KEY)
sessionStorage.removeItem(LOGIN_TYPE_KEY)
clearAuthToken()
}
// 初始化:恢复登录状态
function initAuth() {
if (accessToken.value) {
setAuthToken(accessToken.value)
}
}
initAuth()
// 检查权限(与 @/utils/rbac-permission 规则一致:资源码 ↔ Query 互通)
function hasPermission(permission: string): boolean {
if (!permission?.trim()) return true
const code = permission.trim()
const granted = permissions.value
if (granted.includes(code)) return true
if (/\.Query$/i.test(code)) {
const resource = code.replace(/\.Query$/i, '')
if (resource && granted.includes(resource)) return true
} else if (granted.includes(`${code}.Query`)) {
return true
}
return false
}
// 检查角色
function hasRole(roleCode: string): boolean {
return userInfo.value?.roles?.some((r) => r.code === roleCode) || false
}
return {
token,
accessToken,
refreshToken,
userInfo,
permissions,
menus,
isLoggedIn,
loginPortalType,
getLoginRouteName,
isCustomerPortal,
getAccessTokenRealExpiresAt,
isKeepAliveActive,
enableKeepAlive,
disableKeepAlive,
getKeepAliveUntil,
login,
loginByWxResult,
loginBySsoResult,
getCurrentUser,
refreshUserInfo,
logout,
hasPermission,
hasRole,
refreshAccessToken,
isTokenExpiringSoon,
}
})