Files
xk-admin/apps/web-antd/src/utils/vip.ts

147 lines
4.9 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.
/**
* VIP 权限判断(全局方法)
* - 默认读登录态 userInfo.vip
* - 接诊/复诊等场景务必传入「履约诊所」vip避免用错门店权益
* - 模板显隐优先用 <VipGate code="xxx" :vip="storeVip" />
*/
import { computed, type ComputedRef, type Ref } from 'vue';
import { useUserStore } from '@vben/stores';
/** 与 xk_vip_feature.code / 等级 permissions 对齐的常用功能码 */
export const VIP_FEATURE = {
MEDICAL_RECORD: 'medical_record',
AI_MEDICAL_RECORD: 'ai_medical_record',
AI_PRESCRIPTION: 'ai_prescription',
GOLDEN_FORMULA: 'golden_formula',
} as const;
export type VipFeatureCode = (typeof VIP_FEATURE)[keyof typeof VIP_FEATURE] | string;
export interface StoreVipInfo {
level_code?: string;
level_name?: string;
level_weight?: number;
badge_url?: string;
duration_type?: string;
duration_label?: string;
duration_badge_url?: string;
permissions?: string[];
expire_at?: number;
is_lifetime?: boolean;
}
/** 读取当前登录用户 VIP 信息 */
export function getCurrentVip(): StoreVipInfo | null {
const userStore = useUserStore();
return ((userStore.userInfo as any)?.vip as StoreVipInfo) || null;
}
/**
* 从 get-current-store-type / 同类接口响应中取出 vip 对象
* 兼容 result / data / 已解包
*/
export function pickVipFromApiPayload(payload: unknown): StoreVipInfo | null {
if (!payload || typeof payload !== 'object') return null;
const raw = payload as Record<string, any>;
const data = raw.result ?? raw.data ?? raw;
if (!data || typeof data !== 'object') return null;
const vip = (data as any).vip;
return vip && typeof vip === 'object' ? (vip as StoreVipInfo) : null;
}
/** 规范化 permissions 列表 */
export function normalizeVipPermissions(
vipOrPermissions?: StoreVipInfo | string[] | null,
): string[] {
if (Array.isArray(vipOrPermissions)) {
return vipOrPermissions.filter((x) => typeof x === 'string' && x);
}
if (vipOrPermissions && typeof vipOrPermissions === 'object') {
const list = vipOrPermissions.permissions;
return Array.isArray(list) ? list.filter((x) => typeof x === 'string' && x) : [];
}
const cur = getCurrentVip()?.permissions;
return Array.isArray(cur) ? cur.filter((x) => typeof x === 'string' && x) : [];
}
/**
* 是否拥有指定权限码(全局判断方法)
* @param code 权限码,如 VIP_FEATURE.MEDICAL_RECORD
* @param vipOrPermissions 挂号侧 vip / permissions
* - 不传:用登录态 userInfo.vip
* - 显式传 null视为无履约诊所权益不回落登录态避免接诊未就绪时误放行
*/
export function hasVipPermission(
code: string,
vipOrPermissions?: StoreVipInfo | string[] | null,
): boolean {
if (!code) return false;
if (arguments.length < 2) {
return normalizeVipPermissions().includes(code);
}
if (vipOrPermissions == null) {
return false;
}
return normalizeVipPermissions(vipOrPermissions).includes(code);
}
/**
* 等级是否不低于目标(按 level_weight若无 weight 则仅精确匹配编码时返回 true
*/
export function levelAtLeast(minCode: string, vip?: StoreVipInfo | null): boolean {
const current = vip ?? getCurrentVip();
if (!current?.level_code) return false;
if (current.level_code === minCode) return true;
const weight = Number(current.level_weight ?? 0);
const minWeight = parseLevelWeight(minCode);
if (minWeight >= 0) {
return weight >= minWeight;
}
return false;
}
/** 粗略解析 V0-V11 / BLACK_GOLD / CUSTOM 权重 */
function parseLevelWeight(code: string): number {
if (/^V\d+$/i.test(code)) {
return Number(code.slice(1));
}
if (code === 'BLACK_GOLD') return 12;
if (code === 'CUSTOM') return 13;
return -1;
}
/**
* 组合式:基于可选的门店 vip ref 做权限判断(接诊台推荐)
* @example
* const { has, canUseMedicalRecord } = useVipPermission(receptionVip)
* const ok = has(VIP_FEATURE.MEDICAL_RECORD)
*/
export function useVipPermission(
vipSource?: Ref<StoreVipInfo | null | undefined> | ComputedRef<StoreVipInfo | null | undefined>,
) {
/** 传入了 vipSource 时始终走履约诊所分支(含 null=无权益) */
const has = (code: string) => {
if (vipSource) {
return hasVipPermission(code, vipSource.value ?? null);
}
return hasVipPermission(code);
};
return {
vip: computed(() => (vipSource ? vipSource.value : getCurrentVip())),
has,
hasVipPermission: has,
levelAtLeast: (minCode: string) =>
levelAtLeast(minCode, vipSource ? vipSource.value ?? null : getCurrentVip()),
canUseMedicalRecord: computed(() => has(VIP_FEATURE.MEDICAL_RECORD)),
canUseAiMedicalRecord: computed(() => has(VIP_FEATURE.AI_MEDICAL_RECORD)),
canUseAiPrescription: computed(() => has(VIP_FEATURE.AI_PRESCRIPTION)),
canUseGoldenFormula: computed(() => has(VIP_FEATURE.GOLDEN_FORMULA)),
};
}
/** @deprecated 请用 useVipPermission保留兼容旧调用 */
export function useVipGate() {
return useVipPermission();
}