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 IconMapper } from "./IconMapper";
|
||||||
export { default as SettingMapper } from "./SettingMapper";
|
export { default as SettingMapper } from "./SettingMapper";
|
||||||
|
|
||||||
|
export { default as VCan } from "./directives/Access";
|
||||||
export { default as VDraggable } from "./directives/Draggable";
|
export { default as VDraggable } from "./directives/Draggable";
|
||||||
export { default as VPopup } from "./directives/Popup";
|
export { default as VPopup } from "./directives/Popup";
|
||||||
|
|
||||||
export * from "./MethodLocker";
|
export * from "./MethodLocker";
|
||||||
export * from "./Network";
|
export * from "./Network";
|
||||||
export * from "./Prismjs";
|
export * from "./Prismjs";
|
||||||
|
export * from "./directives/Access";
|
||||||
export * from "./directives/Draggable";
|
export * from "./directives/Draggable";
|
||||||
export * from "./directives/Popup";
|
export * from "./directives/Popup";
|
||||||
|
|||||||
Reference in New Issue
Block a user