update
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import type { Directive, DirectiveBinding, EffectScope } from "vue";
|
||||
import { effectScope, watch } from "vue";
|
||||
import { AccessMatchMode, useUserStore } from "../../store";
|
||||
|
||||
/** 访问控制类型 */
|
||||
export type AccessControlType = "permission" | "role";
|
||||
/** 指令结果生效方式 */
|
||||
export type AccessEffectMode = "if" | "show";
|
||||
|
||||
/** 自定义回调参数 */
|
||||
export type AccessDirectiveCallbackPayload = {
|
||||
type: AccessControlType;
|
||||
granted: boolean;
|
||||
valueList: string[];
|
||||
mode: AccessMatchMode;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问控制指令配置
|
||||
* `value` 支持单个值或值数组
|
||||
* 数组默认按 `or` 逻辑处理,需要 `and` 时显式传 `mode`
|
||||
* `callback` 存在时优先执行回调,不再使用 `effect`
|
||||
*/
|
||||
export type AccessDirectiveConfig = {
|
||||
value: string | string[];
|
||||
mode?: AccessMatchMode;
|
||||
effect?: AccessEffectMode;
|
||||
reverse?: boolean;
|
||||
display?: string;
|
||||
callback?: (payload: AccessDirectiveCallbackPayload) => void;
|
||||
}
|
||||
|
||||
type AccessDirectiveValue = string | string[] | AccessDirectiveConfig;
|
||||
|
||||
type AccessDirectiveState = {
|
||||
scope: EffectScope;
|
||||
placeholder: Comment | null;
|
||||
display: string;
|
||||
config: AccessDirectiveConfig;
|
||||
lastGranted?: boolean;
|
||||
}
|
||||
|
||||
type AccessMatcher = (store: ReturnType<typeof useUserStore>, valueList: string[], mode: AccessMatchMode) => boolean;
|
||||
|
||||
const ACCESS_STATE_KEY = "__vAccessState";
|
||||
|
||||
/** 过滤空字符串并统一裁剪空白 */
|
||||
function normalizeValueList(value: string | string[]): string[] {
|
||||
return (Array.isArray(value) ? value : [value])
|
||||
.map(item => item?.trim())
|
||||
.filter((item): item is string => !!item);
|
||||
}
|
||||
|
||||
/** 统一把指令值转成标准配置 */
|
||||
function normalizeConfig(value: AccessDirectiveValue): AccessDirectiveConfig {
|
||||
if (typeof value === "string" || Array.isArray(value)) {
|
||||
return {
|
||||
value,
|
||||
mode: "or",
|
||||
effect: "if",
|
||||
reverse: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: value?.value || [],
|
||||
mode: value?.mode || "or",
|
||||
effect: value?.effect || "if",
|
||||
reverse: !!value?.reverse,
|
||||
display: value?.display,
|
||||
callback: value?.callback
|
||||
};
|
||||
}
|
||||
|
||||
/** 恢复被 `if` 模式替换掉的真实元素 */
|
||||
function restoreElement(el: HTMLElement, state: AccessDirectiveState): void {
|
||||
if (state.placeholder?.parentNode) {
|
||||
state.placeholder.parentNode.replaceChild(el, state.placeholder);
|
||||
}
|
||||
state.placeholder = null;
|
||||
}
|
||||
|
||||
/** `if` 模式:无权限时直接移出 DOM */
|
||||
function applyIfEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
||||
if (granted) {
|
||||
restoreElement(el, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.placeholder && el.parentNode) {
|
||||
state.placeholder = document.createComment("v-access");
|
||||
el.parentNode.replaceChild(state.placeholder, el);
|
||||
}
|
||||
}
|
||||
|
||||
/** `show` 模式:保留 DOM,仅切换 display */
|
||||
function applyShowEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
||||
restoreElement(el, state);
|
||||
el.style.display = granted ? state.display : "none";
|
||||
}
|
||||
|
||||
/** 自定义模式:交给调用方自己处理样式或行为 */
|
||||
function applyCustomCallback(
|
||||
el: HTMLElement,
|
||||
state: AccessDirectiveState,
|
||||
granted: boolean,
|
||||
type: AccessControlType,
|
||||
valueList: string[],
|
||||
mode: AccessMatchMode
|
||||
): void {
|
||||
restoreElement(el, state);
|
||||
if (state.lastGranted === granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.config.callback?.({
|
||||
type,
|
||||
granted,
|
||||
valueList,
|
||||
mode,
|
||||
el
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据当前登录用户状态刷新指令效果 */
|
||||
function updateAccess(
|
||||
el: HTMLElement,
|
||||
state: AccessDirectiveState,
|
||||
type: AccessControlType,
|
||||
matcher: AccessMatcher
|
||||
): void {
|
||||
const store = useUserStore();
|
||||
const config = state.config;
|
||||
const mode = config.mode || "or";
|
||||
const valueList = normalizeValueList(config.value);
|
||||
const granted = config.reverse ? !matcher(store, valueList, mode) : matcher(store, valueList, mode);
|
||||
|
||||
if (config.callback) {
|
||||
applyCustomCallback(el, state, granted, type, valueList, mode);
|
||||
} else if (config.effect === "show") {
|
||||
applyShowEffect(el, state, granted);
|
||||
} else {
|
||||
applyIfEffect(el, state, granted);
|
||||
}
|
||||
|
||||
state.lastGranted = granted;
|
||||
}
|
||||
|
||||
function getState(el: HTMLElement): AccessDirectiveState | undefined {
|
||||
return (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
||||
}
|
||||
|
||||
function setState(el: HTMLElement, state: AccessDirectiveState): void {
|
||||
(el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY] = state;
|
||||
}
|
||||
|
||||
function clearState(el: HTMLElement): void {
|
||||
delete (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
||||
}
|
||||
|
||||
/** 卸载时清掉占位注释,避免遗留无用节点 */
|
||||
function clearPlaceholder(state: AccessDirectiveState): void {
|
||||
if (state.placeholder?.parentNode) {
|
||||
state.placeholder.parentNode.removeChild(state.placeholder);
|
||||
}
|
||||
state.placeholder = null;
|
||||
}
|
||||
|
||||
/** 创建角色/权限通用指令 */
|
||||
function createAccessDirective(type: AccessControlType, matcher: AccessMatcher): Directive<HTMLElement, AccessDirectiveValue> {
|
||||
return {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
||||
const state: AccessDirectiveState = {
|
||||
scope: effectScope(),
|
||||
placeholder: null,
|
||||
display: el.style.display,
|
||||
config: normalizeConfig(binding.value)
|
||||
};
|
||||
|
||||
setState(el, state);
|
||||
|
||||
state.scope.run(() => {
|
||||
const store = useUserStore();
|
||||
watch(
|
||||
() => {
|
||||
return type === "permission" ? store.getPermissionList().join("|") : store.getRoleList().join("|");
|
||||
},
|
||||
() => updateAccess(el, state, type, matcher),
|
||||
{ immediate: true }
|
||||
);
|
||||
});
|
||||
},
|
||||
updated(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
||||
const state = getState(el);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.config = normalizeConfig(binding.value);
|
||||
updateAccess(el, state, type, matcher);
|
||||
},
|
||||
unmounted(el: HTMLElement) {
|
||||
const state = getState(el);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.scope.stop();
|
||||
clearPlaceholder(state);
|
||||
clearState(el);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 权限指令:`v-can` */
|
||||
export const VCan = createAccessDirective("permission", (store, valueList, mode) => {
|
||||
return store.hasPermissions(valueList, mode);
|
||||
});
|
||||
|
||||
/** 角色指令:`v-role` */
|
||||
export const VRole = createAccessDirective("role", (store, valueList, mode) => {
|
||||
return store.hasRoles(valueList, mode);
|
||||
});
|
||||
|
||||
export default VCan;
|
||||
@@ -13,11 +13,13 @@ export { default as Toolkit } from "./Toolkit";
|
||||
export { default as IconMapper } from "./IconMapper";
|
||||
export { default as SettingMapper } from "./SettingMapper";
|
||||
|
||||
export { default as VCan } from "./directives/Access";
|
||||
export { default as VDraggable } from "./directives/Draggable";
|
||||
export { default as VPopup } from "./directives/Popup";
|
||||
|
||||
export * from "./MethodLocker";
|
||||
export * from "./Network";
|
||||
export * from "./Prismjs";
|
||||
export * from "./directives/Access";
|
||||
export * from "./directives/Draggable";
|
||||
export * from "./directives/Popup";
|
||||
|
||||
Reference in New Issue
Block a user