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
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)
|
|
},
|
|
},
|
|
})
|
|
|