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

147 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-08-04 14:53:11 +08:00
/**
* VIP
* - userInfo.vip
* - /vip
* - <VipGate code="xxx" :vip="storeVip" />
2026-08-04 14:53:11 +08:00
*/
import { computed, type ComputedRef, type Ref } from 'vue';
2026-08-04 14:53:11 +08:00
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;
2026-08-04 14:53:11 +08:00
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;
}
2026-08-07 20:24:49 +08:00
/**
* 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
2026-08-07 20:24:49 +08:00
*/
export function hasVipPermission(
code: string,
vipOrPermissions?: StoreVipInfo | string[] | null,
): boolean {
2026-08-04 14:53:11 +08:00
if (!code) return false;
if (arguments.length < 2) {
return normalizeVipPermissions().includes(code);
2026-08-07 20:24:49 +08:00
}
if (vipOrPermissions == null) {
return false;
}
return normalizeVipPermissions(vipOrPermissions).includes(code);
2026-08-04 14:53:11 +08:00
}
/**
* level_weight weight true
*/
2026-08-07 20:24:49 +08:00
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);
2026-08-04 14:53:11 +08:00
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);
};
2026-08-04 14:53:11 +08:00
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)),
2026-08-04 14:53:11 +08:00
};
}
/** @deprecated 请用 useVipPermission保留兼容旧调用 */
export function useVipGate() {
return useVipPermission();
}