You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

200 lines
6.2 KiB

import { defineStore } from 'pinia'
import { DEFAULT_ADMIN_ACCESS, ROLE_ACCESS_CEILING } from '~/utils/menus'
export const IDENTITY_LABELS: Record<number, string> = {
1: '超管',
2: '管理员',
4: '运营',
5: '渠道商',
}
// 身份常量(与后端 pb.Identity 一致)
export const IDENTITY_ADMIN = 1
export const IDENTITY_MANAGER = 2
// ⚠️ identity=3(品牌商/代理)已于 2026-09-12 随 brand 表下线:后端不再接受新建,
// 存量账号由 migrateBrandAccountsToOperator 禁用并降级为运营。这里不再导出对应常量。
export const IDENTITY_OPERATOR = 4
export const IDENTITY_DEALER = 5 // 渠道商账号
interface AppBind {
id: number
name: string // 部署注册名
app_name?: string // 所属应用名(应用切换按它去重)
region: string
}
interface LoginData {
account: string
account_id?: number
identity: number
token: string
access: string
apps: string
regions: string
products: string
channelid?: string
appbinds: AppBind[]
}
const STORAGE_KEY = 'auth_state'
function parseComma(val: string): string[] {
if (!val) return []
return val.split(',').map((s) => s.trim()).filter(Boolean)
}
function saveToStorage(data: object) {
if (import.meta.client) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
}
}
function loadFromStorage(): Record<string, unknown> | null {
if (!import.meta.client) return null
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
return JSON.parse(raw)
} catch {
return null
}
}
export const useAuthStore = defineStore('auth', {
state: () => ({
token: '' as string,
account: '' as string,
accountId: 0 as number, // 账号表 id(0 = yaml 里的引导超管,库里没有记录)
identity: 0 as number,
access: [] as string[],
apps: [] as string[],
regions: [] as string[],
products: [] as string[],
// 渠道分成体系的归属绑定:渠道商账号有 channelid。
// 仅用于界面收敛(隐藏无关筛选/按钮),真正的数据隔离由后端 scope 强制执行。
channelid: '' as string,
appbinds: [] as AppBind[],
currentAppId: 0 as number,
siteTitle: '' as string,
}),
getters: {
isSuperAdmin(): boolean {
return this.identity === 1
},
isAdmin(): boolean {
return this.identity === IDENTITY_ADMIN || this.identity === IDENTITY_MANAGER
},
// 渠道商账号:锁定到单个渠道商
isDealer(): boolean {
return this.identity === IDENTITY_DEALER
},
identityLabel(): string {
return IDENTITY_LABELS[this.identity] ?? ''
},
isLoggedIn(): boolean {
return !!this.token
},
},
actions: {
// 登录态落盘(三处写入共用一份,加字段只改这里)
persist() {
saveToStorage({
token: this.token,
account: this.account,
accountId: this.accountId,
identity: this.identity,
access: this.access,
apps: this.apps,
regions: this.regions,
products: this.products,
channelid: this.channelid,
appbinds: this.appbinds,
currentAppId: this.currentAppId,
siteTitle: this.siteTitle,
})
},
setLoginData(data: LoginData) {
this.token = data.token
this.account = data.account
this.accountId = data.account_id ?? 0
this.identity = data.identity
this.access = parseComma(data.access)
this.apps = parseComma(data.apps)
this.regions = parseComma(data.regions)
this.products = parseComma(data.products)
this.channelid = data.channelid ?? ''
this.appbinds = data.appbinds ?? []
if (this.appbinds.length > 0 && !this.currentAppId) {
this.currentAppId = this.appbinds[0].id
}
this.persist()
},
setCurrentApp(id: number) {
this.currentAppId = id
this.persist()
},
setSiteTitle(title: string) {
this.siteTitle = title
this.persist()
},
logout() {
this.token = ''
this.account = ''
this.accountId = 0
this.identity = 0
this.access = []
this.apps = []
this.regions = []
this.products = []
this.channelid = ''
this.appbinds = []
this.currentAppId = 0
this.siteTitle = ''
if (import.meta.client) {
localStorage.removeItem(STORAGE_KEY)
}
},
restore() {
const stored = loadFromStorage()
if (!stored) return
this.token = (stored.token as string) ?? ''
this.account = (stored.account as string) ?? ''
this.accountId = (stored.accountId as number) ?? 0
this.identity = (stored.identity as number) ?? 0
this.access = (stored.access as string[]) ?? []
this.apps = (stored.apps as string[]) ?? []
this.regions = (stored.regions as string[]) ?? []
this.products = (stored.products as string[]) ?? []
this.channelid = (stored.channelid as string) ?? ''
this.appbinds = (stored.appbinds as AppBind[]) ?? []
this.currentAppId = (stored.currentAppId as number) ?? 0
this.siteTitle = (stored.siteTitle as string) ?? ''
},
hasAccess(pageId: string): boolean {
if (this.identity === IDENTITY_ADMIN) return true // 超管全量
// 2026-09-12「应用环境配置」并入「服务与环境配置」(svcconfig):只勾过旧 id 的存量账号
// 不该因为一次合并就失去入口。反向不成立——勾了 svcconfig 本来就包含这一页。
if (pageId === 'svcconfig' && this.access.includes('appconfig')) return true
// 管理员未显式配置权限时,按默认权限集放行(除 SaaS 管理 / 系统管理 外全部)。
if (this.identity === IDENTITY_MANAGER && this.access.length === 0) {
return DEFAULT_ADMIN_ACCESS.includes(pageId)
}
// 渠道商账号:可见页面不允许超出角色上限(仪表盘/用户查询/月结算)。
// 未显式勾权限时上限即默认集,免得新开的账号空白一片。
const ceiling = ROLE_ACCESS_CEILING[this.identity]
if (ceiling) {
if (!ceiling.includes(pageId)) return false
if (this.access.length === 0) return true
}
return this.access.includes(pageId)
},
},
})