---初始化后台管理web页面项目

This commit is contained in:
2025-08-20 14:39:30 +08:00
parent ad49711a7e
commit 87545a8baf
2057 changed files with 282864 additions and 213 deletions

View File

@ -0,0 +1,61 @@
import type { UnwrapRef, Ref, WritableComputedRef, DeepReadonly } from 'vue';
import {
reactive,
readonly,
computed,
getCurrentInstance,
watchEffect,
unref,
// nextTick,
toRaw,
} from 'vue';
import { isEqual } from 'lodash-es';
export function useRuleFormItem<T extends Recordable, K extends keyof T, V = UnwrapRef<T[K]>>(
props: T,
key?: K,
changeEvent?,
emitData?: Ref<any[]>,
): [WritableComputedRef<V>, (val: V) => void, DeepReadonly<V>];
export function useRuleFormItem<T extends Recordable>(
props: T,
key: keyof T = 'value',
changeEvent = 'change',
emitData?: Ref<any[]>,
) {
const instance = getCurrentInstance();
const emit = instance?.emit;
const innerState = reactive({
value: props[key],
});
const defaultState = readonly(innerState);
const setState = (val: UnwrapRef<T[keyof T]>): void => {
innerState.value = val as T[keyof T];
};
watchEffect(() => {
innerState.value = props[key];
});
const state: any = computed({
get() {
return innerState.value;
},
set(value) {
if (isEqual(value, defaultState.value)) return;
innerState.value = value as T[keyof T];
// nextTick(() => {
// emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []));
// });
emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []));
},
});
return [state, setState, defaultState];
}

View File

@ -0,0 +1,18 @@
import type { InjectionKey, ComputedRef, Ref } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface PageContextProps {
contentHeight: ComputedRef<number>;
pageHeight: Ref<number>;
setPageHeight: (height: number) => Promise<void>;
}
const key: InjectionKey<PageContextProps> = Symbol();
export function createPageContext(context: PageContextProps) {
return createContext<PageContextProps>(context, key, { native: true });
}
export function usePageContext() {
return useContext<PageContextProps>(key);
}

View File

@ -0,0 +1,18 @@
import { nextTick, onMounted, onActivated } from 'vue';
export function onMountedOrActivated(hook: Fn) {
let mounted: boolean;
onMounted(() => {
hook();
nextTick(() => {
mounted = true;
});
});
onActivated(() => {
if (mounted) {
hook();
}
});
}

View File

@ -0,0 +1,40 @@
import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
import type { Ref } from 'vue';
interface Params {
excludeListeners?: boolean;
excludeKeys?: string[];
excludeDefaultKeys?: boolean;
}
const DEFAULT_EXCLUDE_KEYS = ['class', 'style'];
const LISTENER_PREFIX = /^on[A-Z]/;
export function entries<T>(obj: Recordable<T>): [string, T][] {
return Object.keys(obj).map((key: string) => [key, obj[key]]);
}
export function useAttrs(params: Params = {}): Ref<Recordable> | {} {
const instance = getCurrentInstance();
if (!instance) return {};
const { excludeListeners = false, excludeKeys = [], excludeDefaultKeys = true } = params;
const attrs = shallowRef({});
const allExcludeKeys = excludeKeys.concat(excludeDefaultKeys ? DEFAULT_EXCLUDE_KEYS : []);
// Since attrs are not reactive, make it reactive instead of doing in `onUpdated` hook for better performance
instance.attrs = reactive(instance.attrs);
watchEffect(() => {
const res = entries(instance.attrs).reduce((acm, [key, val]) => {
if (!allExcludeKeys.includes(key) && !(excludeListeners && LISTENER_PREFIX.test(key))) {
acm[key] = val;
}
return acm;
}, {} as Recordable);
attrs.value = res;
});
return attrs;
}

View File

@ -0,0 +1,45 @@
import {
InjectionKey,
provide,
inject,
reactive,
readonly as defineReadonly,
// defineComponent,
UnwrapRef,
} from 'vue';
export interface CreateContextOptions {
readonly?: boolean;
createProvider?: boolean;
native?: boolean;
}
type ShallowUnwrap<T> = {
[P in keyof T]: UnwrapRef<T[P]>;
};
export function createContext<T>(
context: any,
key: InjectionKey<T> = Symbol(),
options: CreateContextOptions = {},
) {
const { readonly = true, createProvider = false, native = false } = options;
const state = reactive(context);
const provideData = readonly ? defineReadonly(state) : state;
!createProvider && provide(key, native ? context : provideData);
return {
state,
};
}
export function useContext<T>(key: InjectionKey<T>, native?: boolean): T;
export function useContext<T>(key: InjectionKey<T>, defaultValue?: any, native?: boolean): T;
export function useContext<T>(
key: InjectionKey<T> = Symbol(),
defaultValue?: any,
): ShallowUnwrap<T> {
return inject(key, defaultValue || {});
}

View File

@ -0,0 +1,22 @@
import { ref } from 'vue';
const isEditorOpen = ref(false);
const hasAjaxError = ref(false);
const isSingleLogin = ref(false);
function ajaxError() {
hasAjaxError.value = true;
setTimeout(() => {
hasAjaxError.value = false;
}, 10000);
}
export default function() {
return {
isEditorOpen,
hasAjaxError,
ajaxError,
isSingleLogin
}
}

View File

@ -0,0 +1,17 @@
import { ref, unref } from 'vue';
export function useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {
const lockRef = ref(false);
return async function (...args: P) {
if (unref(lockRef)) return;
lockRef.value = true;
try {
const ret = await fn(...args);
lockRef.value = false;
return ret;
} catch (e) {
lockRef.value = false;
throw e;
}
};
}

16
src/hooks/core/useRefs.ts Normal file
View File

@ -0,0 +1,16 @@
import type { Ref } from 'vue';
import { ref, onBeforeUpdate } from 'vue';
export function useRefs(): [Ref<HTMLElement[]>, (index: number) => (el: HTMLElement) => void] {
const refs = ref([]) as Ref<HTMLElement[]>;
onBeforeUpdate(() => {
refs.value = [];
});
const setRefs = (index: number) => (el: HTMLElement) => {
refs.value[index] = el;
};
return [refs, setRefs];
}

View File

@ -0,0 +1,45 @@
import { ref, watch } from 'vue';
import { tryOnUnmounted } from '@vueuse/core';
import { isFunction } from '/@/utils/is';
export function useTimeoutFn(handle: Fn<any>, wait: number, native = false) {
if (!isFunction(handle)) {
throw new Error('handle is not Function!');
}
const { readyRef, stop, start } = useTimeoutRef(wait);
if (native) {
handle();
} else {
watch(
readyRef,
(maturity) => {
maturity && handle();
},
{ immediate: false },
);
}
return { readyRef, stop, start };
}
export function useTimeoutRef(wait: number) {
const readyRef = ref(false);
let timer: TimeoutHandle;
function stop(): void {
readyRef.value = false;
timer && window.clearTimeout(timer);
}
function start(): void {
stop();
timer = setTimeout(() => {
readyRef.value = true;
}, wait);
}
start();
tryOnUnmounted(stop);
return { readyRef, stop, start };
}

View File

@ -0,0 +1,45 @@
import { httpRequest } from '/@/api/sys/api';
import { getToken } from '/@/utils/auth';
export default function () {
async function changeApiOptions(val) {
if (val.path) {
try {
const headers = { Authorization: `Bearer ${getToken()}` };
if (val.requestHeaderConfigs.length > 0) {
val.requestHeaderConfigs.forEach((element) => {
if (element.name) headers[element.name] = element.value;
});
}
let path = val.path;
if (val.requestParamsConfigs.length > 0) {
path += '?';
val.requestParamsConfigs.forEach((element) => {
if (element.name) path += `${element.name}=${element.value}&`;
});
}
const apiData = {};
if (val.requestBodyConfigs.length > 0) {
val.requestBodyConfigs.forEach((element) => {
if (element.name) apiData[element.name] = element.value;
});
}
const res = await httpRequest(
{
requestUrl: path,
requestType: val.method,
},
{
headers,
data: apiData,
},
);
return res;
} catch (error) {}
}
}
return {
changeApiOptions,
};
}

View File

@ -0,0 +1,89 @@
import { ref, computed, ComputedRef, unref } from 'vue';
import { useEventListener } from '/@/hooks/event/useEventListener';
import { screenMap, sizeEnum, screenEnum } from '/@/enums/breakpointEnum';
let globalScreenRef: ComputedRef<sizeEnum | undefined>;
let globalWidthRef: ComputedRef<number>;
let globalRealWidthRef: ComputedRef<number>;
export interface CreateCallbackParams {
screen: ComputedRef<sizeEnum | undefined>;
width: ComputedRef<number>;
realWidth: ComputedRef<number>;
screenEnum: typeof screenEnum;
screenMap: Map<sizeEnum, number>;
sizeEnum: typeof sizeEnum;
}
export function useBreakpoint() {
return {
screenRef: computed(() => unref(globalScreenRef)),
widthRef: globalWidthRef,
screenEnum,
realWidthRef: globalRealWidthRef,
};
}
// Just call it once
export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void) {
const screenRef = ref<sizeEnum>(sizeEnum.XL);
const realWidthRef = ref(window.innerWidth);
function getWindowWidth() {
const width = document.body.clientWidth;
const xs = screenMap.get(sizeEnum.XS)!;
const sm = screenMap.get(sizeEnum.SM)!;
const md = screenMap.get(sizeEnum.MD)!;
const lg = screenMap.get(sizeEnum.LG)!;
const xl = screenMap.get(sizeEnum.XL)!;
if (width < xs) {
screenRef.value = sizeEnum.XS;
} else if (width < sm) {
screenRef.value = sizeEnum.SM;
} else if (width < md) {
screenRef.value = sizeEnum.MD;
} else if (width < lg) {
screenRef.value = sizeEnum.LG;
} else if (width < xl) {
screenRef.value = sizeEnum.XL;
} else {
screenRef.value = sizeEnum.XXL;
}
realWidthRef.value = width;
}
useEventListener({
el: window,
name: 'resize',
listener: () => {
getWindowWidth();
resizeFn();
},
// wait: 100,
});
getWindowWidth();
globalScreenRef = computed(() => unref(screenRef));
globalWidthRef = computed((): number => screenMap.get(unref(screenRef)!)!);
globalRealWidthRef = computed((): number => unref(realWidthRef));
function resizeFn() {
fn?.({
screen: globalScreenRef,
width: globalWidthRef,
realWidth: globalRealWidthRef,
screenEnum,
screenMap,
sizeEnum,
});
}
resizeFn();
return {
screenRef: globalScreenRef,
screenEnum,
widthRef: globalWidthRef,
realWidthRef: globalRealWidthRef,
};
}

View File

@ -0,0 +1,11 @@
import mitt from '../../utils/mitt';
const bus = new mitt();
export default function () {
return {
bus,
FLOW_PROCESSED: 'flow_processed',
FORM_LIST_MODIFIED: 'form_list_modified',
CREATE_FLOW: 'create_flow'
};
}

View File

@ -0,0 +1,58 @@
import type { Ref } from 'vue';
import { ref, watch, unref } from 'vue';
import { useThrottleFn, useDebounceFn } from '@vueuse/core';
export type RemoveEventFn = () => void;
export interface UseEventParams {
el?: Element | Ref<Element | undefined> | Window | any;
name: string;
listener: EventListener;
options?: boolean | AddEventListenerOptions;
autoRemove?: boolean;
isDebounce?: boolean;
wait?: number;
}
export function useEventListener({
el = window,
name,
listener,
options,
autoRemove = true,
isDebounce = true,
wait = 80,
}: UseEventParams): { removeEvent: RemoveEventFn } {
/* eslint-disable-next-line */
let remove: RemoveEventFn = () => {};
const isAddRef = ref(false);
if (el) {
const element = ref(el as Element) as Ref<Element>;
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
const realHandler = wait ? handler : listener;
const removeEventListener = (e: Element) => {
isAddRef.value = true;
e.removeEventListener(name, realHandler, options);
};
const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options);
const removeWatch = watch(
element,
(v, _ov, cleanUp) => {
if (v) {
!unref(isAddRef) && addEventListener(v);
cleanUp(() => {
autoRemove && removeEventListener(v);
});
}
},
{ immediate: true },
);
remove = () => {
removeEventListener(element.value);
removeWatch();
};
}
return { removeEvent: remove };
}

View File

@ -0,0 +1,48 @@
import { Ref, watchEffect, ref } from 'vue';
interface IntersectionObserverProps {
target: Ref<Element | null | undefined>;
root?: Ref<any>;
onIntersect: IntersectionObserverCallback;
rootMargin?: string;
threshold?: number;
}
export function useIntersectionObserver({
target,
root,
onIntersect,
rootMargin = '0px',
threshold = 0.1,
}: IntersectionObserverProps) {
let cleanup = () => {};
const observer: Ref<Nullable<IntersectionObserver>> = ref(null);
const stopEffect = watchEffect(() => {
cleanup();
observer.value = new IntersectionObserver(onIntersect, {
root: root ? root.value : null,
rootMargin,
threshold,
});
const current = target.value;
current && observer.value.observe(current);
cleanup = () => {
if (observer.value) {
observer.value.disconnect();
target.value && observer.value.unobserve(target.value);
}
};
});
return {
observer,
stop: () => {
cleanup();
stopEffect();
},
};
}

View File

@ -0,0 +1,65 @@
import type { Ref } from 'vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { isWindow, isObject } from '/@/utils/is';
import { useThrottleFn } from '@vueuse/core';
export function useScroll(
refEl: Ref<Element | Window | null>,
options?: {
wait?: number;
leading?: boolean;
trailing?: boolean;
},
) {
const refX = ref(0);
const refY = ref(0);
let handler = () => {
if (isWindow(refEl.value)) {
refX.value = refEl.value.scrollX;
refY.value = refEl.value.scrollY;
} else if (refEl.value) {
refX.value = (refEl.value as Element).scrollLeft;
refY.value = (refEl.value as Element).scrollTop;
}
};
if (isObject(options)) {
let wait = 0;
if (options.wait && options.wait > 0) {
wait = options.wait;
Reflect.deleteProperty(options, 'wait');
}
handler = useThrottleFn(handler, wait);
}
let stopWatch: () => void;
onMounted(() => {
stopWatch = watch(
refEl,
(el, prevEl, onCleanup) => {
if (el) {
el.addEventListener('scroll', handler);
} else if (prevEl) {
prevEl.removeEventListener('scroll', handler);
}
onCleanup(() => {
refX.value = refY.value = 0;
el && el.removeEventListener('scroll', handler);
});
},
{ immediate: true },
);
});
onUnmounted(() => {
refEl.value && refEl.value.removeEventListener('scroll', handler);
});
function stop() {
stopWatch && stopWatch();
}
return { refX, refY, stop };
}

View File

@ -0,0 +1,59 @@
import { isFunction, isUnDef } from '/@/utils/is';
import { ref, unref } from 'vue';
export interface ScrollToParams {
el: any;
to: number;
duration?: number;
callback?: () => any;
}
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
t /= d / 2;
if (t < 1) {
return (c / 2) * t * t + b;
}
t--;
return (-c / 2) * (t * (t - 2) - 1) + b;
};
const move = (el: HTMLElement, amount: number) => {
el.scrollTop = amount;
};
const position = (el: HTMLElement) => {
return el.scrollTop;
};
export function useScrollTo({ el, to, duration = 500, callback }: ScrollToParams) {
const isActiveRef = ref(false);
const start = position(el);
const change = to - start;
const increment = 20;
let currentTime = 0;
duration = isUnDef(duration) ? 500 : duration;
const animateScroll = function () {
if (!unref(isActiveRef)) {
return;
}
currentTime += increment;
const val = easeInOutQuad(currentTime, start, change, duration);
move(el, val);
if (currentTime < duration && unref(isActiveRef)) {
requestAnimationFrame(animateScroll);
} else {
if (callback && isFunction(callback)) {
callback();
}
}
};
const run = () => {
isActiveRef.value = true;
animateScroll();
};
const stop = () => {
isActiveRef.value = false;
};
return { start: run, stop };
}

View File

@ -0,0 +1,35 @@
import { tryOnMounted, tryOnUnmounted, useDebounceFn } from '@vueuse/core';
interface WindowSizeOptions {
once?: boolean;
immediate?: boolean;
listenerOptions?: AddEventListenerOptions | boolean;
}
export function useWindowSizeFn<T>(fn: Fn<T>, wait = 150, options?: WindowSizeOptions) {
let handler = () => {
fn();
};
const handleSize = useDebounceFn(handler, wait);
handler = handleSize;
const start = () => {
if (options && options.immediate) {
handler();
}
window.addEventListener('resize', handler);
};
const stop = () => {
window.removeEventListener('resize', handler);
};
tryOnMounted(() => {
start();
});
tryOnUnmounted(() => {
stop();
});
return [start, stop];
}

View File

@ -0,0 +1,36 @@
import type { GlobConfig } from '/#/config';
import { warn } from '/@/utils/log';
import { getAppEnvConfig } from '/@/utils/env';
export const useGlobSetting = (): Readonly<GlobConfig> => {
const {
VITE_GLOB_APP_TITLE,
VITE_GLOB_API_URL,
VITE_GLOB_REQUEST_TIMEOUT,
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_OUT_LINK_URL,
VITE_GLOB_PRINT_BASE_URL,
} = getAppEnvConfig();
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
warn(
`VITE_GLOB_APP_SHORT_NAME Variables can only be characters/underscores, please modify in the environment variables and re-running.`,
);
}
// Take global configuration
const glob: Readonly<GlobConfig> = {
title: VITE_GLOB_APP_TITLE,
apiUrl: VITE_GLOB_API_URL,
requestTimeout:VITE_GLOB_REQUEST_TIMEOUT,
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
outLink: VITE_GLOB_OUT_LINK_URL,
printBaseUrl: VITE_GLOB_PRINT_BASE_URL,
};
return glob as Readonly<GlobConfig>;
};

View File

@ -0,0 +1,105 @@
import type { HeaderSetting } from '/#/config';
import { computed, unref } from 'vue';
import { useAppStore } from '/@/store/modules/app';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useFullContent } from '/@/hooks/web/useFullContent';
import { MenuModeEnum } from '/@/enums/menuEnum';
export function useHeaderSetting() {
const { getFullContent } = useFullContent();
const appStore = useAppStore();
const getShowFullHeaderRef = computed(() => {
return (
!unref(getFullContent) &&
unref(getShowMixHeaderRef) &&
unref(getShowHeader) &&
!unref(getIsTopMenu) &&
!unref(getIsMixSidebar)
);
});
const getUnFixedAndFull = computed(() => !unref(getFixed) && !unref(getShowFullHeaderRef));
const getShowInsetHeaderRef = computed(() => {
const need = !unref(getFullContent) && unref(getShowHeader);
return (
(need && !unref(getShowMixHeaderRef)) ||
(need && unref(getIsTopMenu)) ||
(need && unref(getIsMixSidebar))
);
});
const {
getMenuMode,
getSplit,
getShowHeaderTrigger,
getIsSidebarType,
getIsMixSidebar,
getIsTopMenu,
} = useMenuSetting();
const { getShowBreadCrumb, getShowLogo } = useRootSetting();
const getShowMixHeaderRef = computed(() => !unref(getIsSidebarType) && unref(getShowHeader));
const getShowDoc = computed(() => appStore.getHeaderSetting.showDoc);
const getHeaderTheme = computed(() => appStore.getHeaderSetting.theme);
const getShowHeader = computed(() => appStore.getHeaderSetting.show);
const getFixed = computed(() => appStore.getHeaderSetting.fixed);
const getHeaderBgColor = computed(() => appStore.getHeaderSetting.bgColor);
const getShowSearch = computed(() => appStore.getHeaderSetting.showSearch);
const getUseLockPage = computed(() => appStore.getHeaderSetting.useLockPage);
const getShowFullScreen = computed(() => appStore.getHeaderSetting.showFullScreen);
const getShowNotice = computed(() => appStore.getHeaderSetting.showNotice);
const getShowBread = computed(() => {
return (
unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && unref(getShowBreadCrumb) && !unref(getSplit)
);
});
const getShowHeaderLogo = computed(() => {
return unref(getShowLogo) && !unref(getIsSidebarType) && !unref(getIsMixSidebar);
});
const getShowContent = computed(() => {
return unref(getShowBread) || unref(getShowHeaderTrigger);
});
// Set header configuration
function setHeaderSetting(headerSetting: Partial<HeaderSetting>) {
appStore.setProjectConfig({ headerSetting });
}
return {
setHeaderSetting,
getShowDoc,
getShowSearch,
getHeaderTheme,
getUseLockPage,
getShowFullScreen,
getShowNotice,
getShowBread,
getShowContent,
getShowHeaderLogo,
getShowHeader,
getFixed,
getShowMixHeaderRef,
getShowFullHeaderRef,
getShowInsetHeaderRef,
getUnFixedAndFull,
getHeaderBgColor,
};
}

View File

@ -0,0 +1,166 @@
import type { MenuSetting } from '/#/config';
import { computed, unref, ref } from 'vue';
import { useAppStore } from '/@/store/modules/app';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
import { useFullContent } from '/@/hooks/web/useFullContent';
const mixSideHasChildren = ref(false);
export function useMenuSetting() {
const { getFullContent: fullContent } = useFullContent();
const appStore = useAppStore();
const getShowSidebar = computed(() => {
return (
unref(getSplit) ||
(unref(getShowMenu) && unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && !unref(fullContent))
);
});
const getCollapsed = computed(() => appStore.getMenuSetting.collapsed);
const getMenuType = computed(() => appStore.getMenuSetting.type);
const getMenuMode = computed(() => appStore.getMenuSetting.mode);
const getMenuFixed = computed(() => appStore.getMenuSetting.fixed);
const getShowMenu = computed(() => appStore.getMenuSetting.show);
const getMenuHidden = computed(() => appStore.getMenuSetting.hidden);
const getMenuWidth = computed(() => appStore.getMenuSetting.menuWidth);
const getTrigger = computed(() => appStore.getMenuSetting.trigger);
const getMenuTheme = computed(() => appStore.getMenuSetting.theme);
const getSplit = computed(() => appStore.getMenuSetting.split);
const getMenuBgColor = computed(() => appStore.getMenuSetting.bgColor);
const getMixSideTrigger = computed(() => appStore.getMenuSetting.mixSideTrigger);
const getCanDrag = computed(() => appStore.getMenuSetting.canDrag);
const getAccordion = computed(() => appStore.getMenuSetting.accordion);
const getMixSideFixed = computed(() => appStore.getMenuSetting.mixSideFixed);
const getTopMenuAlign = computed(() => appStore.getMenuSetting.topMenuAlign);
const getCloseMixSidebarOnChange = computed(
() => appStore.getMenuSetting.closeMixSidebarOnChange,
);
const getIsSidebarType = computed(() => unref(getMenuType) === MenuTypeEnum.SIDEBAR);
const getIsTopMenu = computed(() => unref(getMenuType) === MenuTypeEnum.TOP_MENU);
const getCollapsedShowTitle = computed(() => appStore.getMenuSetting.collapsedShowTitle);
const getShowTopMenu = computed(() => {
return unref(getMenuMode) === MenuModeEnum.HORIZONTAL || unref(getSplit);
});
const getShowHeaderTrigger = computed(() => {
if (
unref(getMenuType) === MenuTypeEnum.TOP_MENU ||
!unref(getShowMenu) ||
unref(getMenuHidden)
) {
return false;
}
return unref(getTrigger) === TriggerEnum.HEADER;
});
const getIsHorizontal = computed(() => {
return unref(getMenuMode) === MenuModeEnum.HORIZONTAL;
});
const getIsMixSidebar = computed(() => {
return unref(getMenuType) === MenuTypeEnum.MIX_SIDEBAR;
});
const getIsMixMode = computed(() => {
return unref(getMenuMode) === MenuModeEnum.INLINE && unref(getMenuType) === MenuTypeEnum.MIX;
});
const getRealWidth = computed(() => {
if (unref(getIsMixSidebar)) {
return unref(getCollapsed) && !unref(getMixSideFixed)
? unref(getMiniWidthNumber)
: unref(getMenuWidth);
}
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
});
const getMiniWidthNumber = computed(() => {
const { collapsedShowTitle } = appStore.getMenuSetting;
return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH;
});
const getCalcContentWidth = computed(() => {
const width =
unref(getIsTopMenu) || !unref(getShowMenu) || (unref(getSplit) && unref(getMenuHidden))
? 0
: unref(getIsMixSidebar)
? (unref(getCollapsed) ? SIDE_BAR_MINI_WIDTH : SIDE_BAR_SHOW_TIT_MINI_WIDTH) +
(unref(getMixSideFixed) && unref(mixSideHasChildren) ? unref(getRealWidth) : 0)
: unref(getRealWidth);
return `calc(100% - ${unref(width)}px)`;
});
// Set menu configuration
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
appStore.setProjectConfig({ menuSetting });
}
function toggleCollapsed() {
setMenuSetting({
collapsed: !unref(getCollapsed),
});
}
return {
setMenuSetting,
toggleCollapsed,
getMenuFixed,
getRealWidth,
getMenuType,
getMenuMode,
getShowMenu,
getCollapsed,
getMiniWidthNumber,
getCalcContentWidth,
getMenuWidth,
getTrigger,
getSplit,
getMenuTheme,
getCanDrag,
getCollapsedShowTitle,
getIsHorizontal,
getIsSidebarType,
getAccordion,
getShowTopMenu,
getShowHeaderTrigger,
getTopMenuAlign,
getMenuHidden,
getIsTopMenu,
getMenuBgColor,
getShowSidebar,
getIsMixMode,
getIsMixSidebar,
getCloseMixSidebarOnChange,
getMixSideTrigger,
getMixSideFixed,
mixSideHasChildren,
};
}

View File

@ -0,0 +1,28 @@
import type { MultiTabsSetting } from '/#/config';
import { computed } from 'vue';
import { useAppStore } from '/@/store/modules/app';
export function useMultipleTabSetting() {
const appStore = useAppStore();
const getShowMultipleTab = computed(() => appStore.getMultiTabsSetting.show);
const getShowQuick = computed(() => appStore.getMultiTabsSetting.showQuick);
const getShowRedo = computed(() => appStore.getMultiTabsSetting.showRedo);
const getShowFold = computed(() => appStore.getMultiTabsSetting.showFold);
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
appStore.setProjectConfig({ multiTabsSetting });
}
return {
setMultipleTabSetting,
getShowMultipleTab,
getShowQuick,
getShowRedo,
getShowFold,
};
}

View File

@ -0,0 +1,95 @@
import type { ProjectConfig } from '/#/config';
import { computed } from 'vue';
import { useAppStore } from '/@/store/modules/app';
import { ContentEnum, ThemeEnum } from '/@/enums/appEnum';
type RootSetting = Omit<
ProjectConfig,
'locale' | 'headerSetting' | 'menuSetting' | 'multiTabsSetting'
>;
export function useRootSetting() {
const appStore = useAppStore();
const getPageLoading = computed(() => appStore.getPageLoading);
const getOpenKeepAlive = computed(() => appStore.getProjectConfig.openKeepAlive);
const getSettingButtonPosition = computed(() => appStore.getProjectConfig.settingButtonPosition);
const getCanEmbedIFramePage = computed(() => appStore.getProjectConfig.canEmbedIFramePage);
const getPermissionMode = computed(() => appStore.getProjectConfig.permissionMode);
const getShowLogo = computed(() => appStore.getProjectConfig.showLogo);
const getContentMode = computed(() => appStore.getProjectConfig.contentMode);
const getUseOpenBackTop = computed(() => appStore.getProjectConfig.useOpenBackTop);
const getShowSettingButton = computed(() => appStore.getProjectConfig.showSettingButton);
const getUseErrorHandle = computed(() => appStore.getProjectConfig.useErrorHandle);
const getShowFooter = computed(() => appStore.getProjectConfig.showFooter);
const getShowBreadCrumb = computed(() => appStore.getProjectConfig.showBreadCrumb);
const getThemeColor = computed(() => appStore.getProjectConfig.themeColor);
const getShowBreadCrumbIcon = computed(() => appStore.getProjectConfig.showBreadCrumbIcon);
const getFullContent = computed(() => appStore.getProjectConfig.fullContent);
const getColorWeak = computed(() => appStore.getProjectConfig.colorWeak);
const getGrayMode = computed(() => appStore.getProjectConfig.grayMode);
const getLockTime = computed(() => appStore.getProjectConfig.lockTime);
const getShowDarkModeToggle = computed(() => appStore.getProjectConfig.showDarkModeToggle);
const getDarkMode = computed(() => appStore.getDarkMode);
const getLayoutContentMode = computed(() =>
appStore.getProjectConfig.contentMode === ContentEnum.FULL
? ContentEnum.FULL
: ContentEnum.FIXED,
);
function setRootSetting(setting: Partial<RootSetting>) {
appStore.setProjectConfig(setting);
}
function setDarkMode(mode: ThemeEnum) {
appStore.setDarkMode(mode);
}
return {
setRootSetting,
getSettingButtonPosition,
getFullContent,
getColorWeak,
getGrayMode,
getLayoutContentMode,
getPageLoading,
getOpenKeepAlive,
getCanEmbedIFramePage,
getPermissionMode,
getShowLogo,
getUseErrorHandle,
getShowBreadCrumb,
getShowBreadCrumbIcon,
getUseOpenBackTop,
getShowSettingButton,
getShowFooter,
getContentMode,
getLockTime,
getThemeColor,
getDarkMode,
setDarkMode,
getShowDarkModeToggle,
};
}

View File

@ -0,0 +1,31 @@
import type { TransitionSetting } from '/#/config';
import { computed } from 'vue';
import { useAppStore } from '/@/store/modules/app';
export function useTransitionSetting() {
const appStore = useAppStore();
const getEnableTransition = computed(() => appStore.getTransitionSetting?.enable);
const getOpenNProgress = computed(() => appStore.getTransitionSetting?.openNProgress);
const getOpenPageLoading = computed((): boolean => {
return !!appStore.getTransitionSetting?.openPageLoading;
});
const getBasicTransition = computed(() => appStore.getTransitionSetting?.basicTransition);
function setTransitionSetting(transitionSetting: Partial<TransitionSetting>) {
appStore.setProjectConfig({ transitionSetting });
}
return {
setTransitionSetting,
getEnableTransition,
getOpenNProgress,
getOpenPageLoading,
getBasicTransition,
};
}

View File

@ -0,0 +1,26 @@
import { reactive, nextTick } from 'vue';
export default function () {
const tableOptions: {
scrollHeight: number;
} = reactive({
scrollHeight: 600,
});
async function scrollHeight() {
await nextTick();
const conTable = document.getElementsByClassName('vben-layout-content');
if (conTable && conTable.length > 0 && conTable[0]) {
const searchTable = document.getElementsByClassName('search');
if (searchTable && searchTable.length > 0 && searchTable[0]) {
tableOptions.scrollHeight = conTable[0].clientHeight - 220 - searchTable[0].clientHeight;
} else {
tableOptions.scrollHeight = conTable[0].clientHeight - 180;
}
} else {
tableOptions.scrollHeight = 400;
}
}
return {
tableOptions,
scrollHeight,
};
}

View File

@ -0,0 +1,10 @@
import { useAppProviderContext } from '/@/components/Application';
import { computed, unref } from 'vue';
export function useAppInject() {
const values = useAppProviderContext();
return {
getIsMobile: computed(() => unref(values.isMobile)),
};
}

View File

@ -0,0 +1,190 @@
import { ComputedRef, isRef, nextTick, Ref, ref, unref, watch } from 'vue';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useLayoutHeight } from '/@/layouts/default/content/useContentViewHeight';
import { getViewportOffset } from '/@/utils/domUtils';
import { isNumber, isString } from '/@/utils/is';
export interface CompensationHeight {
// 使用 layout Footer 高度作为判断补偿高度的条件
useLayoutFooter: boolean;
// refs HTMLElement
elements?: Ref[];
}
type Upward = number | string | null | undefined;
/**
* 动态计算内容高度根据锚点dom最下坐标到屏幕最下坐标根据传入dom的高度、padding、margin等值进行动态计算
* 最终获取合适的内容高度
*
* @param flag 用于开启计算的响应式标识
* @param anchorRef 锚点组件 Ref<ElRef | ComponentRef>
* @param subtractHeightRefs 待减去高度的组件列表 Ref<ElRef | ComponentRef>
* @param substractSpaceRefs 待减去空闲空间(margins/paddings)的组件列表 Ref<ElRef | ComponentRef>
* @param offsetHeightRef 计算偏移的响应式高度,计算高度时将直接减去此值
* @param upwardSpace 向上递归减去空闲空间的 层级 或 直到指定class为止 数值为2代表向上递归两次|数值为ant-layout表示向上递归直到碰见.ant-layout为止
* @returns 响应式高度
*/
export function useContentHeight(
flag: ComputedRef<Boolean>,
anchorRef: Ref,
subtractHeightRefs: Ref[],
substractSpaceRefs: Ref[],
upwardSpace: Ref<Upward> | ComputedRef<Upward> | Upward = 0,
offsetHeightRef: Ref<number> = ref(0),
) {
const contentHeight: Ref<Nullable<number>> = ref(null);
const { footerHeightRef: layoutFooterHeightRef } = useLayoutHeight();
let compensationHeight: CompensationHeight = {
useLayoutFooter: true,
};
const setCompensation = (params: CompensationHeight) => {
compensationHeight = params;
};
function redoHeight() {
nextTick(() => {
calcContentHeight();
});
}
function calcSubtractSpace(
element: Element | null | undefined,
direction: 'all' | 'top' | 'bottom' = 'all',
): number {
function numberPx(px: string) {
return Number(px.replace(/[^\d]/g, ''));
}
let subtractHeight = 0;
const ZERO_PX = '0px';
if (element) {
const cssStyle = getComputedStyle(element);
const marginTop = numberPx(cssStyle?.marginTop ?? ZERO_PX);
const marginBottom = numberPx(cssStyle?.marginBottom ?? ZERO_PX);
const paddingTop = numberPx(cssStyle?.paddingTop ?? ZERO_PX);
const paddingBottom = numberPx(cssStyle?.paddingBottom ?? ZERO_PX);
if (direction === 'all') {
subtractHeight += marginTop;
subtractHeight += marginBottom;
subtractHeight += paddingTop;
subtractHeight += paddingBottom;
} else if (direction === 'top') {
subtractHeight += marginTop;
subtractHeight += paddingTop;
} else {
subtractHeight += marginBottom;
subtractHeight += paddingBottom;
}
}
return subtractHeight;
}
function getEl(element: any): Nullable<HTMLDivElement> {
if (element == null) {
return null;
}
return (element instanceof HTMLDivElement ? element : element.$el) as HTMLDivElement;
}
async function calcContentHeight() {
if (!flag.value) {
return;
}
// Add a delay to get the correct height
await nextTick();
const anchorEl = getEl(unref(anchorRef));
if (!anchorEl) {
return;
}
const { bottomIncludeBody } = getViewportOffset(anchorEl);
// substract elements height
let substractHeight = 0;
subtractHeightRefs.forEach((item) => {
substractHeight += getEl(unref(item))?.offsetHeight ?? 0;
});
// subtract margins / paddings
let substractSpaceHeight = calcSubtractSpace(anchorEl) ?? 0;
substractSpaceRefs.forEach((item) => {
substractSpaceHeight += calcSubtractSpace(getEl(unref(item)));
});
// upwardSpace
let upwardSpaceHeight = 0;
function upward(element: Element | null, upwardLvlOrClass: number | string | null | undefined) {
if (element && upwardLvlOrClass) {
const parent = element.parentElement;
if (parent) {
if (isString(upwardLvlOrClass)) {
if (!parent.classList.contains(upwardLvlOrClass)) {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
upward(parent, upwardLvlOrClass);
} else {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
}
} else if (isNumber(upwardLvlOrClass)) {
if (upwardLvlOrClass > 0) {
upwardSpaceHeight += calcSubtractSpace(parent, 'bottom');
upward(parent, --upwardLvlOrClass);
}
}
}
}
}
if (isRef(upwardSpace)) {
upward(anchorEl, unref(upwardSpace));
} else {
upward(anchorEl, upwardSpace);
}
let height =
bottomIncludeBody -
unref(layoutFooterHeightRef) -
unref(offsetHeightRef) -
substractHeight -
substractSpaceHeight -
upwardSpaceHeight;
// compensation height
const calcCompensationHeight = () => {
compensationHeight.elements?.forEach((item) => {
height += getEl(unref(item))?.offsetHeight ?? 0;
});
};
if (compensationHeight.useLayoutFooter && unref(layoutFooterHeightRef) > 0) {
calcCompensationHeight();
} else {
calcCompensationHeight();
}
contentHeight.value = height;
}
onMountedOrActivated(() => {
nextTick(() => {
calcContentHeight();
});
});
useWindowSizeFn(
() => {
calcContentHeight();
},
50,
{ immediate: true },
);
watch(
() => [layoutFooterHeightRef.value],
() => {
calcContentHeight();
},
{
flush: 'post',
immediate: true,
},
);
return { redoHeight, setCompensation, contentHeight };
}

View File

@ -0,0 +1,12 @@
import { onUnmounted, getCurrentInstance } from 'vue';
import { createContextMenu, destroyContextMenu } from '/@/components/ContextMenu';
import type { ContextMenuItem } from '/@/components/ContextMenu';
export type { ContextMenuItem };
export function useContextMenu(authRemove = true) {
if (getCurrentInstance() && authRemove) {
onUnmounted(() => {
destroyContextMenu();
});
}
return [createContextMenu, destroyContextMenu];
}

View File

@ -0,0 +1,69 @@
import { ref, watch } from 'vue';
import { isDef } from '/@/utils/is';
interface Options {
target?: HTMLElement;
}
export function useCopyToClipboard(initial?: string) {
const clipboardRef = ref(initial || '');
const isSuccessRef = ref(false);
const copiedRef = ref(false);
watch(
clipboardRef,
(str?: string) => {
if (isDef(str)) {
copiedRef.value = true;
isSuccessRef.value = copyTextToClipboard(str);
}
},
{ immediate: !!initial, flush: 'sync' },
);
return { clipboardRef, isSuccessRef, copiedRef };
}
export function copyTextToClipboard(input: string, { target = document.body }: Options = {}) {
const element = document.createElement('textarea');
const previouslyFocusedElement = document.activeElement;
element.value = input;
element.setAttribute('readonly', '');
(element.style as any).contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';
element.style.fontSize = '12pt';
const selection = document.getSelection();
let originalRange;
if (selection && selection.rangeCount > 0) {
originalRange = selection.getRangeAt(0);
}
target.append(element);
element.select();
element.selectionStart = 0;
element.selectionEnd = input.length;
let isSuccess = false;
try {
isSuccess = document.execCommand('copy');
} catch (e: any) {
throw new Error(e);
}
element.remove();
if (originalRange && selection) {
selection.removeAllRanges();
selection.addRange(originalRange);
}
if (previouslyFocusedElement) {
(previouslyFocusedElement as HTMLElement).focus();
}
return isSuccess;
}

View File

@ -0,0 +1,22 @@
import { useAppProviderContext } from '/@/components/Application';
// import { computed } from 'vue';
// import { lowerFirst } from 'lodash-es';
export function useDesign(scope: string) {
const values = useAppProviderContext();
// const $style = cssModule ? useCssModule() : {};
// const style: Record<string, string> = {};
// if (cssModule) {
// Object.keys($style).forEach((key) => {
// // const moduleCls = $style[key];
// const k = key.replace(new RegExp(`^${values.prefixCls}-?`, 'ig'), '');
// style[lowerFirst(k)] = $style[key];
// });
// }
return {
// prefixCls: computed(() => `${values.prefixCls}-${scope}`),
prefixCls: `${values.prefixCls}-${scope}`,
prefixVar: values.prefixCls,
// style,
};
}

116
src/hooks/web/useECharts.ts Normal file
View File

@ -0,0 +1,116 @@
import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { tryOnUnmounted } from '@vueuse/core';
import { unref, nextTick, watch, computed, ref } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { useEventListener } from '/@/hooks/event/useEventListener';
import { useBreakpoint } from '/@/hooks/event/useBreakpoint';
import echarts from '/@/utils/lib/echarts';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
export function useECharts(
elRef: Ref<HTMLDivElement>,
theme: 'light' | 'dark' | 'default' = 'default',
) {
const { getDarkMode: getSysDarkMode } = useRootSetting();
const getDarkMode = computed(() => {
return theme === 'default' ? getSysDarkMode.value : theme;
});
let chartInstance: echarts.ECharts | null = null;
let resizeFn: Fn = resize;
const cacheOptions = ref({}) as Ref<EChartsOption>;
let removeResizeFn: Fn = () => {};
resizeFn = useDebounceFn(resize, 200);
const getOptions = computed(() => {
if (getDarkMode.value !== 'dark') {
return cacheOptions.value as EChartsOption;
}
return {
backgroundColor: 'transparent',
...cacheOptions.value,
} as EChartsOption;
});
function initCharts(t = theme) {
const el = unref(elRef);
if (!el || !unref(el)) {
return;
}
chartInstance = echarts.init(el, t);
const { removeEvent } = useEventListener({
el: window,
name: 'resize',
listener: resizeFn,
});
removeResizeFn = removeEvent;
const { widthRef, screenEnum } = useBreakpoint();
if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
useTimeoutFn(() => {
resizeFn();
}, 30);
}
}
function setOptions(options: EChartsOption, clear = true) {
cacheOptions.value = options;
if (unref(elRef)?.offsetHeight === 0) {
useTimeoutFn(() => {
setOptions(unref(getOptions));
}, 30);
return;
}
nextTick(() => {
useTimeoutFn(() => {
if (!chartInstance) {
initCharts(getDarkMode.value as 'default');
if (!chartInstance) return;
}
clear && chartInstance?.clear();
chartInstance?.setOption(unref(getOptions));
}, 30);
});
}
function resize() {
chartInstance?.resize();
}
watch(
() => getDarkMode.value,
(theme) => {
if (chartInstance) {
chartInstance.dispose();
initCharts(theme as 'default');
setOptions(cacheOptions.value);
}
},
);
tryOnUnmounted(() => {
if (!chartInstance) return;
removeResizeFn();
chartInstance.dispose();
chartInstance = null;
});
function getInstance(): echarts.ECharts | null {
if (!chartInstance) {
initCharts(getDarkMode.value as 'default');
}
return chartInstance;
}
return {
setOptions,
resize,
echarts,
getInstance,
};
}

View File

@ -0,0 +1,152 @@
import { FormSchema } from '/@/components/Form';
import { BasicColumn } from "/@/components/Table";
import { getFormTemplateUsingCache } from '/@/api/form/design';
import { getMenuByIdComponentPath } from '/@/api/system/menu';
import { deepMerge } from '/@/utils';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
const { t } = useI18n();
const { notification } = useMessage();
export function useFormConfig() {
async function mergeFormSchemas(obj:{formSchema: FormSchema[],formId:String,formPath:String}){
try{
let formProps;
if(obj.formId){
formProps=await queryConfig(obj.formId,'formProps');
}else{
formProps=await queryConfigByFormPath(obj.formPath,'formProps');
}
let fschemas=formProps?.schemas;
if(fschemas&&fschemas.length>0){
return deepMerge(obj.formSchema,fschemas);
}else{
return formSchema;
}
}catch(e){
return [];
}
}
async function mergeFormEventConfigs(obj:{formEventConfigs,formId:String,formPath:String}){
try{
let fEConfigs;
if(obj.formId){
fEConfigs=await queryConfig(obj.formId,'formEventConfigs');
}else{
fEConfigs=await queryConfigByFormPath(obj.formPath,'formEventConfigs');
}
return deepMerge(obj.formEventConfigs,fEConfigs);
}catch(e){
return {};
}
}
async function mergeColumns(columns: BasicColumn[],formId:String){
try{
const cols=await queryConfig(formId,'columns');
if(cols&&cols.length>0){
const filteredCol = cols.filter(colItem =>
columns.some(column => column.dataIndex === colItem.dataIndex)
);
return filteredCol;
}else{
return columns;
}
}catch(e){
return [];
}
}
async function mergeSearchFormSchema(searchFormSchema: FormSchema[],formId:String){
try{
const sFormSchema=await queryConfig(formId,'searchFormSchema');
if(sFormSchema&&sFormSchema.length>0){
const filteredsFormSchema = sFormSchema.filter(sItem =>
searchFormSchema.some( searchItem => searchItem.field === sItem.field)
);
return filteredsFormSchema;
}else{
return searchFormSchema;
}
}catch(e){
return [];
}
}
async function mergeButtons(buttons,formId:String){
try{
const btns=await queryConfig(formId,'buttons');
if(btns&&btns.length>0){
return btns;
}else{
return buttons;
}
}catch(e){
return[];
}
}
async function queryConfigByFormPath(componentPath:String,configName:String){
if(componentPath==null){
notification.error({
message: t('提示'),
description: `发生异常,组件路径不能为空!`,
});
throw new Error('发生异常,组件路径不能为空!');
}
const menu=await getMenuByIdComponentPath("/"+componentPath+"/index");
if(menu==null){
notification.error({
message: t('提示'),
description: `发生异常,根据组件路径找不到菜单!`,
});
throw new Error('发生异常,根据组件路径找不到菜单!');
}
const formId=menu.formId;
return await queryConfig(formId,configName);
}
async function queryConfig(formId:String,configName:String){
const formTemplate = await getFormTemplateUsingCache(formId);
const renderConfig=formTemplate.renderConfig;
if(renderConfig==null){
return null;
}
const exportMatches = renderConfig.matchAll(/export\s+const\s+(\w+)\s*(?::\s*\w+(?:\[\])?)?\s*=\s*([\s\S]*?)(?=\n\s*export\s+const|\n\s*export\s+function|\n\s*function|$)/g);
for (const match of exportMatches) {
const varName = match[1];
const valueCode = match[2].trim();
const cleanCode = valueCode.endsWith(';') ? valueCode.slice(0, -1) : valueCode;
if(varName==configName) {
try {
const value = new Function(`return ${cleanCode}`)();
return value;
} catch (e) {
console.error(`Failed to parse ${varName}:`, e);
notification.error({
message: t('提示'),
description: `解析表单渲染覆盖配置出错[解析${configName}发生异常]`,
});
throw new Error('解析表单渲染覆盖配置出错,请联系管理员处理');
}
}
}
return null;
}
return {
mergeFormSchemas,
mergeColumns,
mergeSearchFormSchema,
mergeButtons,
mergeFormEventConfigs
};
}

View File

@ -0,0 +1,129 @@
import { apiConfigFunc } from '/@/utils/event/design';
import { execute } from '/@/api/liteflow';
export enum EventType {
CREATE = 0, //初始化表单
GET, // 获取表单数据
LOAD, //加载表单
SUBMIT, //提交表单
}
//初始化表单
export async function createFormEvent(
formEventConfig,
formModels,
formRef,
schemas,
isCustomForm = false,
) {
try {
if (
formEventConfig &&
formEventConfig[EventType.CREATE] &&
Array.isArray(formEventConfig[EventType.CREATE])
) {
await executeFormEvent(
formEventConfig[EventType.CREATE],
formModels,
formRef,
schemas,
isCustomForm,
);
}
} catch (error) {}
}
//获取表单数据
export async function getFormDataEvent(
formEventConfig,
formModels,
formRef,
schemas,
isCustomForm = false,
) {
try {
if (
formEventConfig &&
formEventConfig[EventType.GET] &&
Array.isArray(formEventConfig[EventType.GET])
) {
await executeFormEvent(
formEventConfig[EventType.GET],
formModels,
formRef,
schemas,
isCustomForm,
);
}
} catch (error) {}
}
//加载表单
export async function loadFormEvent(
formEventConfig,
formModels,
formRef,
schemas,
isCustomForm = false,
) {
try {
if (
formEventConfig &&
formEventConfig[EventType.LOAD] &&
Array.isArray(formEventConfig[EventType.LOAD])
) {
await executeFormEvent(
formEventConfig[EventType.LOAD],
formModels,
formRef,
schemas,
isCustomForm,
);
}
} catch (error) {}
}
//提交表单
export async function submitFormEvent(
formEventConfig,
formModels,
formRef,
schemas,
isCustomForm = false,
) {
try {
if (
formEventConfig &&
formEventConfig[EventType.SUBMIT] &&
Array.isArray(formEventConfig[EventType.SUBMIT])
) {
await executeFormEvent(
formEventConfig[EventType.SUBMIT],
formModels,
formRef,
schemas,
isCustomForm,
);
}
} catch (error) {}
}
export async function executeFormEvent(
formEventConfig,
formModel,
formRef,
schemas,
isCustomForm = false,
) {
if (!formEventConfig.length) return;
formEventConfig.map((x) => {
x.nodeInfo?.processEvent?.map(async (config) => {
if (config.operateType === 'api') {
await apiConfigFunc(config.operateConfig, isCustomForm, formModel);
} else if (config.operateType === 'liteflow') {
await execute(config.operateConfig, formModel);
} else if (config.operateType === 'js') {
const event = new Function('schema', 'formModel', 'formActionType', config.operateConfig);
event(schemas, formModel, formRef);
}
});
});
}

View File

@ -0,0 +1,28 @@
import { computed, unref } from 'vue';
import { useAppStore } from '/@/store/modules/app';
import { useRouter } from 'vue-router';
/**
* @description: Full screen display content
*/
export const useFullContent = () => {
const appStore = useAppStore();
const router = useRouter();
const { currentRoute } = router;
// Whether to display the content in full screen without displaying the menu
const getFullContent = computed(() => {
// Query parameters, the full screen is displayed when the address bar has a full parameter
const route = unref(currentRoute);
const query = route.query;
if (query && Reflect.has(query, '__full__')) {
return true;
}
// Return to the configuration in the configuration file
return appStore.getProjectConfig.fullContent;
});
return { getFullContent };
};

56
src/hooks/web/useI18n.ts Normal file
View File

@ -0,0 +1,56 @@
import { i18n } from '/@/locales/setupI18n';
type I18nGlobalTranslation = {
(key: string): string;
(key: string, locale: string): string;
(key: string, locale: string, list: unknown[]): string;
(key: string, locale: string, named: Record<string, unknown>): string;
(key: string, list: unknown[]): string;
(key: string, named: Record<string, unknown>): string;
};
type I18nTranslationRestParameters = [string, any];
function getKey(namespace: string | undefined, key: string) {
if (!namespace) {
return key;
}
if (key.startsWith(namespace)) {
return key;
}
return `${namespace}.${key}`;
}
export function useI18n(namespace?: string): {
t: I18nGlobalTranslation;
} {
const normalFn = {
t: (key: string) => {
return getKey(namespace, key);
},
};
if (!i18n) {
return normalFn;
}
const { t, ...methods } = i18n.global;
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) return '';
//TODO 后端获取翻译 注释下一行代码
// if (!key.includes('.') && !namespace) return key;
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters));
};
return {
...methods,
t: tFn,
};
}
// Why write this function
// Mainly to configure the vscode i18nn ally plugin. This function is only used for routing and menus. Please use useI18n for other places
// 为什么要编写此函数?
// 主要用于配合vscode i18nn ally插件。此功能仅用于路由和菜单。请在其他地方使用useI18n
export const t = (key: string) => key;

View File

@ -0,0 +1,72 @@
import { computed, onUnmounted, unref, watchEffect } from 'vue';
import { useThrottleFn } from '@vueuse/core';
import { useAppStore } from '/@/store/modules/app';
import { useLockStore } from '/@/store/modules/lock';
import { useUserStore } from '/@/store/modules/user';
import { useRootSetting } from '../setting/useRootSetting';
export function useLockPage() {
const { getLockTime } = useRootSetting();
const lockStore = useLockStore();
const userStore = useUserStore();
const appStore = useAppStore();
let timeId: TimeoutHandle;
function clear(): void {
window.clearTimeout(timeId);
}
function resetCalcLockTimeout(): void {
// not login
if (!userStore.getToken) {
clear();
return;
}
const lockTime = appStore.getProjectConfig.lockTime;
if (!lockTime || lockTime < 1) {
clear();
return;
}
clear();
timeId = setTimeout(() => {
lockPage();
}, lockTime * 60 * 1000);
}
function lockPage(): void {
lockStore.setLockInfo({
isLock: true,
pwd: undefined,
});
}
watchEffect((onClean) => {
if (userStore.getToken) {
resetCalcLockTimeout();
} else {
clear();
}
onClean(() => {
clear();
});
});
onUnmounted(() => {
clear();
});
const keyupFn = useThrottleFn(resetCalcLockTimeout, 2000);
return computed(() => {
if (unref(getLockTime)) {
return { onKeyup: keyupFn, onMousemove: keyupFn };
} else {
clear();
return {};
}
});
}

View File

@ -0,0 +1,123 @@
import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
import { Modal, message as Message, notification } from 'ant-design-vue';
import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons-vue';
import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification';
import { useI18n } from './useI18n';
import { isString } from '/@/utils/is';
export interface NotifyApi {
info(config: NotificationArgsProps): void;
success(config: NotificationArgsProps): void;
error(config: NotificationArgsProps): void;
warn(config: NotificationArgsProps): void;
warning(config: NotificationArgsProps): void;
open(args: NotificationArgsProps): void;
close(key: String): void;
config(options: ConfigProps): void;
destroy(): void;
}
export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
export declare type IconType = 'success' | 'info' | 'error' | 'warning';
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
iconType: 'warning' | 'success' | 'error' | 'info';
}
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
interface ConfirmOptions {
info: ModalFunc;
success: ModalFunc;
error: ModalFunc;
warn: ModalFunc;
warning: ModalFunc;
}
function getIcon(iconType: string) {
if (iconType === 'warning') {
return <InfoCircleFilled class="modal-icon-warning" />;
} else if (iconType === 'success') {
return <CheckCircleFilled class="modal-icon-success" />;
} else if (iconType === 'info') {
return <InfoCircleFilled class="modal-icon-info" />;
} else {
return <CloseCircleFilled class="modal-icon-error" />;
}
}
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
if (isString(content)) {
return <div innerHTML={`<div>${content as string}</div>`}></div>;
} else {
return content;
}
}
/**
* @description: Create confirmation box
*/
function createConfirm(options: ModalOptionsEx): ConfirmOptions {
const iconType = options.iconType || 'warning';
Reflect.deleteProperty(options, 'iconType');
const opt: ModalFuncProps = {
centered: true,
icon: getIcon(iconType),
...options,
content: renderContent(options),
};
return Modal.confirm(opt) as unknown as ConfirmOptions;
}
const getBaseOptions = () => {
const { t } = useI18n();
return {
okText: t('确认'),
centered: true,
};
};
function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial {
return {
...getBaseOptions(),
...options,
content: renderContent(options),
icon: getIcon(icon),
};
}
function createSuccessModal(options: ModalOptionsPartial) {
return Modal.success(createModalOptions(options, 'success'));
}
function createErrorModal(options: ModalOptionsPartial) {
return Modal.error(createModalOptions(options, 'close'));
}
function createInfoModal(options: ModalOptionsPartial) {
return Modal.info(createModalOptions(options, 'info'));
}
function createWarningModal(options: ModalOptionsPartial) {
return Modal.warning(createModalOptions(options, 'warning'));
}
notification.config({
placement: 'topRight',
duration: 3,
});
/**
* @description: message
*/
export function useMessage() {
return {
createMessage: Message,
notification: notification as NotifyApi,
createConfirm: createConfirm,
createSuccessModal,
createErrorModal,
createInfoModal,
createWarningModal,
};
}

62
src/hooks/web/usePage.ts Normal file
View File

@ -0,0 +1,62 @@
import type { RouteLocationRaw, Router } from 'vue-router';
import { PageEnum } from '/@/enums/pageEnum';
import { isString } from '/@/utils/is';
import { unref } from 'vue';
import { useRouter } from 'vue-router';
import { REDIRECT_NAME } from '/@/router/constant';
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
function handleError(e: Error) {
console.error(e);
}
// page switch
export function useGo(_router?: Router) {
let router;
if (!_router) {
router = useRouter();
}
const { push, replace } = _router || router;
function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
if (!opt) {
return;
}
if (isString(opt)) {
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError);
} else {
const o = opt as RouteLocationRaw;
isReplace ? replace(o).catch(handleError) : push(o).catch(handleError);
}
}
return go;
}
/**
* @description: redo current page
*/
export const useRedo = (_router?: Router) => {
const { push, currentRoute } = _router || useRouter();
const { query, params = {}, name, fullPath } = unref(currentRoute.value);
function redo(): Promise<boolean> {
return new Promise((resolve) => {
if (name === REDIRECT_NAME) {
resolve(false);
return;
}
if (name && Object.keys(params).length > 0) {
params['_redirect_type'] = 'name';
params['path'] = String(name);
} else {
params['_redirect_type'] = 'path';
params['path'] = fullPath;
}
push({ name: REDIRECT_NAME, params, query }).then(() => resolve(true));
});
}
return redo;
};

View File

@ -0,0 +1,34 @@
import type { Ref } from 'vue';
import { ref, unref, computed } from 'vue';
function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
const offset = (pageNo - 1) * Number(pageSize);
const ret =
offset + Number(pageSize) >= list.length
? list.slice(offset, list.length)
: list.slice(offset, offset + Number(pageSize));
return ret;
}
export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
const currentPage = ref(1);
const pageSizeRef = ref(pageSize);
const getPaginationList = computed(() => {
return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
});
const getTotal = computed(() => {
return unref(list).length;
});
function setCurrentPage(page: number) {
currentPage.value = page;
}
function setPageSize(pageSize: number) {
pageSizeRef.value = pageSize;
}
return { setCurrentPage, getTotal, setPageSize, getPaginationList };
}

View File

@ -0,0 +1,279 @@
import { RouteRecordRaw } from 'vue-router';
import { useAppStore } from '/@/store/modules/app';
import { usePermissionStore } from '/@/store/modules/permission';
import { useUserStore } from '/@/store/modules/user';
import { useTabs } from './useTabs';
import { router, resetRouter } from '/@/router';
// import { RootRoute } from '/@/router/routes';
import projectSetting from '/@/settings/projectSetting';
import { PermissionModeEnum } from '/@/enums/appEnum';
import { RoleEnum } from '/@/enums/roleEnum';
import { intersection } from 'lodash-es';
import { isArray } from '/@/utils/is';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { MenuAuthModel } from '/@/api/system/login/model';
import { BasicColumn, FormSchema } from '/@/components/Table';
import { toRaw } from 'vue';
import { camelCaseString } from '/@/utils/event/design';
import { ButtonConfig } from '/@/model/generator/listConfig';
// User permissions related operations
export function usePermission() {
const userStore = useUserStore();
const appStore = useAppStore();
//获取到当前路由
const currentRoute = router.currentRoute;
const permissionStore = usePermissionStore();
const { closeAll } = useTabs(router);
/**
* Change permission mode
*/
async function togglePermissionMode() {
appStore.setProjectConfig({
permissionMode:
projectSetting.permissionMode === PermissionModeEnum.BACK
? PermissionModeEnum.ROUTE_MAPPING
: PermissionModeEnum.BACK,
});
location.reload();
}
/**
* Reset and regain authority resource information
* @param id
*/
async function resume() {
const tabStore = useMultipleTabStore();
tabStore.clearCacheTabs();
resetRouter();
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
permissionStore.setLastBuildMenuTime();
closeAll();
}
/**
* Determine whether there is permission
*/
function hasPermission(value?: RoleEnum | RoleEnum[] | string | string[], def = true): boolean {
// Visible by default
if (!value) {
return def;
}
const permMode = projectSetting.permissionMode;
if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) {
if (!isArray(value)) {
return userStore.getRoleList?.includes(value as RoleEnum);
}
return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0;
}
if (PermissionModeEnum.BACK === permMode) {
const allPerm = permissionStore.getPermCodeList as MenuAuthModel[];
const { menuId } = currentRoute.value.meta;
const thisMenuAuth = allPerm.find((p) => p.menuId === menuId);
return thisMenuAuth?.buttonAuthCode?.includes(value as string) || false;
// if (!isArray(allPerm)) {
// const { menuId } = currentRoute.value.meta;
// const auth = allPerm.find((p) => p.menuId === menuId);
// return allCodeList.includes(value);
// }
// return (intersection(value, allCodeList) as string[]).length > 0;
}
return true;
}
/**
* Change roles
* @param roles
*/
async function changeRole(roles: RoleEnum | RoleEnum[]): Promise<void> {
if (projectSetting.permissionMode !== PermissionModeEnum.ROUTE_MAPPING) {
throw new Error(
'Please switch PermissionModeEnum to ROUTE_MAPPING mode in the configuration to operate!',
);
}
if (!isArray(roles)) {
roles = [roles];
}
userStore.setRoleList(roles);
await resume();
}
/**
* refresh menu data
*/
async function refreshMenu() {
resume();
}
/**
* refresh menu data
*/
async function changeMenu() {}
/**
* button
* @param buttonList 未过滤的
* @returns 过滤后的
*/
function filterButtonAuth(buttonList: ButtonConfig[]) {
//获取到当前菜单id
const menuId = currentRoute.value.meta.menuId;
const perm = permissionStore.getPermCodeList.find((x) => x.menuId === menuId);
if (!perm) return buttonList;
let buttonAuth = perm?.buttonAuthCode || [];
if (!buttonAuth || buttonAuth?.length === 0) {
return [];
}
buttonAuth = buttonAuth.map((x) => {
const btnList = x.split(':');
btnList.shift();
return btnList.join('');
});
const result = buttonList.filter((btn) => buttonAuth.includes(btn.code));
return result;
}
/**
* BasicColumn
* @param columns 未过滤的
* @param convertCamel 是否需要转驼峰
* @returns 过滤后的
*/
function filterColumnAuth(columns: BasicColumn[], convertCamel = false): BasicColumn[] {
//获取到当前菜单id
const menuId = currentRoute.value.meta.menuId;
const perm = permissionStore.getPermCodeList.find((x) => x.menuId === menuId);
if (!perm) return columns;
let columnAuth = perm?.columnAuthCode || [];
if (!columnAuth || columnAuth?.length === 0) {
return [];
}
if (convertCamel) {
columnAuth = columnAuth.map((x) => camelCaseString(x)!);
}
const result = columns.filter(
(col) => col.dataIndex && toRaw(columnAuth).includes(col.dataIndex as string),
);
return result;
}
/**
* FormSchema权限过滤
* @param formSchema 未过滤的FormSchema
* @param convertCamel 是否需要转换驼峰
* @returns 返回过滤过的FormSchema
*/
function filterFormSchemaAuth(formSchema: FormSchema[], convertCamel = false): FormSchema[] {
const menuId = currentRoute.value.meta.menuId;
const perm = permissionStore.getPermCodeList.find((x) => x.menuId === menuId);
if (!perm) return formSchema;
let formAuth: any[] = perm?.formAuthCode || [];
if (!formAuth || formAuth?.length === 0) {
return [];
}
if (convertCamel) {
formAuth = formAuth.map((x) => {
if (typeof x === 'object' && !!Object.keys(x)[0]) {
const subformField = camelCaseString(Object.keys(x)[0])!;
x[subformField!] = x[subformField!]?.map((field) => camelCaseString(field));
return {
[subformField]: x[subformField!],
};
} else {
return camelCaseString(x)!;
}
});
}
const result = formSchema.filter((schema) => {
return findLayoutComponent(schema, formAuth);
});
return result;
}
function findLayoutComponent(schema, formAuth) {
if (['card', 'tab', 'grid'].includes(schema.type)) {
if (schema.children && schema.children.length > 0) {
return schema.children.filter((children) => {
children.list = children.list.filter((x) => findLayoutComponent(x, formAuth));
return children.list;
});
}
} else if (schema.type === 'table-layout') {
if (schema.children && schema.children.length > 0) {
return schema.children.filter((children) => {
children.list = children.list.filter((chil) => {
chil.children = chil.children.filter((x) => findLayoutComponent(x, formAuth));
return chil.children;
});
return children.list;
});
}
} else if (schema.type === 'form') {
if (schema.componentProps.columns && schema.componentProps.columns.length) {
const subformField = schema.field.substring(0, schema.field.length - 4);
const currentSubFormAuth = formAuth.find((x) => {
return typeof x === 'object' && Object.keys(x)[0] === subformField;
});
if (!currentSubFormAuth) {
return false;
} else {
schema.componentProps.columns = schema.componentProps.columns.filter((x) => {
return x.componentType
? findLayoutComponent(x, Object.values(currentSubFormAuth)[0])
: true;
});
return true;
}
}
} else if (schema.type === 'one-for-one') {
if (schema.componentProps.childSchemas && schema.componentProps.childSchemas.length) {
const subformField = schema.field.substring(0, schema.field.length - 4);
const currentSubFormAuth = formAuth.find((x) => {
return typeof x === 'object' && Object.keys(x)[0] === subformField;
});
if (!currentSubFormAuth) {
return false;
} else {
schema.componentProps.childSchemas = schema.componentProps.childSchemas.filter((x) => {
return x.component
? findLayoutComponent(x, Object.values(currentSubFormAuth)[0])
: true;
});
return true;
}
}
} else {
const field = schema.field || schema.dataIndex || schema.key;
return formAuth?.includes(field) || schema.ignorePermission;
}
}
return {
changeRole,
hasPermission,
togglePermissionMode,
refreshMenu,
changeMenu,
filterButtonAuth,
filterColumnAuth,
filterFormSchemaAuth,
};
}

View File

@ -0,0 +1,46 @@
import { onMounted, onUnmounted, ref } from 'vue';
interface ScriptOptions {
src: string;
}
export function useScript(opts: ScriptOptions) {
const isLoading = ref(false);
const error = ref(false);
const success = ref(false);
let script: HTMLScriptElement;
const promise = new Promise((resolve, reject) => {
onMounted(() => {
script = document.createElement('script');
script.type = 'text/javascript';
script.onload = function () {
isLoading.value = false;
success.value = true;
error.value = false;
resolve('');
};
script.onerror = function (err) {
isLoading.value = false;
success.value = false;
error.value = true;
reject(err);
};
script.src = opts.src;
document.head.appendChild(script);
});
});
onUnmounted(() => {
script && script.remove();
});
return {
isLoading,
error,
success,
toPromise: () => promise,
};
}

View File

@ -0,0 +1,21 @@
import { nextTick, unref } from 'vue';
import type { Ref } from 'vue';
import type { Options } from 'sortablejs';
export function useSortable(el: HTMLElement | Ref<HTMLElement>, options?: Options) {
function initSortable() {
nextTick(async () => {
if (!el) return;
const Sortable = (await import('sortablejs')).default;
Sortable.create(unref(el), {
animation: 500,
delay: 400,
delayOnTouchOnly: true,
...options,
});
});
}
return { initSortable };
}

103
src/hooks/web/useTabs.ts Normal file
View File

@ -0,0 +1,103 @@
import type { RouteLocationNormalized, Router } from 'vue-router';
import { useRouter } from 'vue-router';
import { unref } from 'vue';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { useAppStore } from '/@/store/modules/app';
enum TableActionEnum {
REFRESH,
CLOSE_ALL,
CLOSE_LEFT,
CLOSE_RIGHT,
CLOSE_OTHER,
CLOSE_CURRENT,
CLOSE,
}
export function useTabs(_router?: Router) {
const appStore = useAppStore();
function canIUseTabs(): boolean {
const { show } = appStore.getMultiTabsSetting;
if (!show) {
throw new Error('The multi-tab page is currently not open, please open it in the settings');
}
return !!show;
}
const tabStore = useMultipleTabStore();
const router = _router || useRouter();
const { currentRoute } = router;
function getCurrentTab() {
const route = unref(currentRoute);
return tabStore.getTabList.find((item) => item.fullPath === route.fullPath)!;
}
async function updateTabTitle(title: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const targetTab = tab || getCurrentTab();
await tabStore.setTabTitle(title, targetTab);
}
async function updateTabPath(path: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const targetTab = tab || getCurrentTab();
await tabStore.updateTabPath(path, targetTab);
}
async function handleTabAction(action: TableActionEnum, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const currentTab = getCurrentTab();
switch (action) {
case TableActionEnum.REFRESH:
await tabStore.refreshPage(router);
break;
case TableActionEnum.CLOSE_ALL:
await tabStore.closeAllTab(router);
break;
case TableActionEnum.CLOSE_LEFT:
await tabStore.closeLeftTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_RIGHT:
await tabStore.closeRightTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_OTHER:
await tabStore.closeOtherTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_CURRENT:
case TableActionEnum.CLOSE:
await tabStore.closeTab(tab || currentTab, router);
break;
}
}
return {
refreshPage: () => handleTabAction(TableActionEnum.REFRESH),
closeAll: () => handleTabAction(TableActionEnum.CLOSE_ALL),
closeLeft: () => handleTabAction(TableActionEnum.CLOSE_LEFT),
closeRight: () => handleTabAction(TableActionEnum.CLOSE_RIGHT),
closeOther: () => handleTabAction(TableActionEnum.CLOSE_OTHER),
closeCurrent: () => handleTabAction(TableActionEnum.CLOSE_CURRENT),
close: (tab?: RouteLocationNormalized) => handleTabAction(TableActionEnum.CLOSE, tab),
setTitle: (title: string, tab?: RouteLocationNormalized) => updateTabTitle(title, tab),
updatePath: (fullPath: string, tab?: RouteLocationNormalized) => updateTabPath(fullPath, tab),
};
}

38
src/hooks/web/useTitle.ts Normal file
View File

@ -0,0 +1,38 @@
import { watch, unref, ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTitle as usePageTitle } from '@vueuse/core';
import { useGlobSetting } from '/@/hooks/setting';
import { useRouter } from 'vue-router';
import { useLocaleStore } from '/@/store/modules/locale';
import { useAppStore } from '/@/store/modules/app';
import { REDIRECT_NAME } from '/@/router/constant';
/**
* Listening to page changes and dynamically changing site titles
*/
export function useTitle() {
const title = import.meta.env.VITE_GLOB_APP_TITLE;
const { t } = useI18n();
const { currentRoute } = useRouter();
const localeStore = useLocaleStore();
const appStore = useAppStore();
const pageTitle = usePageTitle();
watch(
[() => currentRoute.value.path, () => localeStore.getLocale],
() => {
const route = unref(currentRoute);
if (route.name === REDIRECT_NAME) {
return;
}
const metaTitle=typeof route?.meta?.title=='function'?route.meta.title(route):route?.meta?.title;
const tTitle = t(metaTitle);
pageTitle.value = tTitle
? ` ${tTitle} - ${title} `
: `${title}`;
},
{ immediate: true },
);
}

View File

@ -0,0 +1,100 @@
import { getCurrentInstance, onBeforeUnmount, ref, Ref, shallowRef, unref } from 'vue';
import { useRafThrottle } from '/@/utils/domUtils';
import { addResizeListener, removeResizeListener } from '/@/utils/event';
import { isDef } from '/@/utils/is';
const domSymbol = Symbol('watermark-dom');
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
) {
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string) {
const can = document.createElement('canvas');
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext('2d');
if (cans) {
cans.rotate((-20 * Math.PI) / 120);
cans.font = '15px Vedana';
cans.fillStyle = 'rgba(0, 0, 0, 0.15)';
cans.textAlign = 'left';
cans.textBaseline = 'middle';
cans.fillText(str, width / 20, height);
}
return can.toDataURL('image/png');
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
} = {},
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(options.str)}) left top repeat`;
}
}
const createWatermark = (str: string) => {
if (unref(watermarkEl)) {
updateWatermark({ str });
return id;
}
const div = document.createElement('div');
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '0px';
div.style.left = '0px';
div.style.position = 'absolute';
div.style.zIndex = '100000';
const el = unref(appendEl);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height });
el.appendChild(div);
return id;
};
function setWatermark(str: string) {
createWatermark(str);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear };
}

View File

@ -0,0 +1,509 @@
import { buildOption } from '/@/utils/helper/designHelper';
import { FormProps } from '/@/components/Form';
import { separator } from '/@/views/workflow/design/bpmn/config/info';
import { TaskApproveOpinion, WorkFlowFormParams } from '/@/model/workflow/bpmnConfig';
import { FormConfigItem } from '/@/model/workflow/formSetting';
import { FormSchema } from '/@/components/Form';
import { TableItem } from '/@/model/workflow/formSetting';
import { disableTypes, hiddenComponentType, requiredDisabled } from '/@bpmn/config/formPermission';
/*
**根据权限 修改JSON配置**
data{
formJson 表单json配置
formConfigChildren 工作流节点表单权限配置
formConfigKey 唯一节点表单key
opinions 意见簿意见
opinionsComponents 意见簿组件
}
isViewProcess 是否查看流程
uploadComponentIds: 所有上传文件的文件夹id 【因为流程需要统计每个节点的附件汇总】
*/
export function changeFormJson(
data: {
formJson: any; // 表单json配置 FormJson | FormProps
formConfigChildren: Array<FormConfigItem>; // 工作流节点表单权限配置
formConfigKey: String; //唯一节点表单key
opinions?: Array<TaskApproveOpinion> | undefined; //意见簿意见
opinionsComponents?: Array<string> | undefined; //意见簿组件
},
isViewProcess: Boolean,
uploadComponentIds: Array<string>,
) {
const returnData = handlerFormPermission(
data.formJson.list ? buildOption(data.formJson, false) : data.formJson,
data.formConfigChildren,
data.formConfigKey,
isViewProcess,
{ uploadComponentIds, opinions: data.opinions, opinionsComponents: data.opinionsComponents },
);
// uploadComponent 带上所有上传文件的文件夹id
return returnData;
}
// 获取工作流表单权限map
/*
返回Map
[
字段唯一Key:{
{
edit:是否可编辑权限,
view:是否可查看权限,
disabled:是否禁用权限,
children:[] 子表数据
}
}
]
*/
export function getPermissionConfigMap(formConfigChildren) {
const map = new Map();
formConfigChildren.forEach((element) => {
map.set(element.key, element);
});
return map;
}
function handlerFormPermission(
buildOptionJson: FormProps,
formConfigChildren,
formKey,
isViewProcess,
otherParams,
) {
const permissionConfigMap = getPermissionConfigMap(formConfigChildren);
const formShow: Number = 0; //表单是否有显示的组件有则大于0
buildOptionJson.schemas = schemeList(
buildOptionJson.schemas,
permissionConfigMap,
formKey,
formShow,
isViewProcess,
otherParams,
);
return {
buildOptionJson,
uploadComponentIds: otherParams.uploadComponentIds,
};
}
function schemeList(
schemas,
permissionConfigMap,
formKey,
formShow,
isViewProcess,
otherParams,
tableName?,
) {
schemas = schemas.map((schema) => {
if (['Card', 'Grid', 'Tab'].includes(schema.component)) {
formShow += 1;
if (schema.children && schema.children.length > 0) {
schema.children.forEach((ele2) => {
if (ele2.list && ele2.list.length > 0) {
ele2.list = schemeList(
ele2.list,
permissionConfigMap,
formKey,
formShow,
isViewProcess,
otherParams,
tableName,
);
}
});
}
} else if (schema.component === 'TableLayout') {
formShow += 1;
if (schema.children && schema.children.length > 0) {
schema.children.forEach((ele2) => {
if (ele2.list && ele2.list.length > 0) {
ele2.list.forEach((ele3) => {
if (ele3.children && ele3.children.length > 0) {
ele3.children = schemeList(
ele3.children,
permissionConfigMap,
formKey,
formShow,
isViewProcess,
otherParams,
tableName,
);
}
});
}
});
}
} else if (schema.component == 'SubForm') {
if (
schema.componentProps &&
schema.componentProps.columns &&
schema.componentProps.columns.length > 0
) {
const permissionConfig = permissionConfigMap.has(schema.key)
? permissionConfigMap.get(schema.key)
: null;
// 查看流程
if (isViewProcess) {
schema.dynamicDisabled = true;
}
if ((permissionConfig?.children || []).find((item) => item.fieldId === '_row_ctrl_' && !item.edit)) {
schema.componentProps.disableAddRow = true;
}
if (!permissionConfig?.view) {
schema.show = false;
} else {
formShow += 1;
if (!permissionConfig?.edit) {
schema.dynamicDisabled = true;
}
}
schema.componentProps.columns = schema.componentProps.columns.map((ele3) => {
const tableItemPermissionConfig = permissionConfig.children.find((x) => {
if (x.key) {
return x.key === ele3.key;
}
if (x.fieldId) {
return x.fieldId === ele3.dataIndex;
}
});
return getSchemePermissionItem(
ele3,
tableItemPermissionConfig,
formKey,
isViewProcess,
otherParams,
);
});
}
} else if (schema.component == 'OneForOne') {
const permissionConfig = permissionConfigMap.has(schema.key)
? permissionConfigMap.get(schema.key)
: null;
if (!permissionConfig?.view) {
schema.show = false;
} else {
formShow += 1;
}
if (schema.componentProps?.childSchemas?.length > 0) {
const onePermissionConfigMap = getPermissionConfigMap(permissionConfig.children);
schema.componentProps.childSchemas = schemeList(
schema.componentProps.childSchemas,
onePermissionConfigMap,
formKey,
formShow,
isViewProcess,
otherParams,
schema.field,
);
}
} else {
const permissionConfig = permissionConfigMap.has(schema.key)
? permissionConfigMap.get(schema.key)
: null;
schema = getSchemePermissionItem(
schema,
permissionConfig,
formKey,
isViewProcess,
otherParams,
tableName,
);
if (permissionConfig?.view) {
formShow += 1;
}
}
return schema;
});
return formShow > 0 ? schemas : null;
}
function getSchemePermissionItem(
schema,
permissionConfig,
formKey,
isViewProcess,
otherParams,
tableName?,
) {
if (permissionConfig) {
//查看
schema.show = permissionConfig.view;
// 必填
if (schema.componentProps) schema.componentProps.required = permissionConfig.required;
schema.required = permissionConfig.required;
//编辑
if (schema.componentProps) schema.componentProps.disabled = !permissionConfig.edit;
schema.dynamicDisabled = !permissionConfig.edit;
// 查看流程
if (isViewProcess) {
schema.dynamicDisabled = true;
}
} else {
schema.show = false;
schema.dynamicDisabled = true;
}
// 修改意见簿
if (schema.component == 'Opinion') {
const key = formKey + separator + schema.key;
schema.defaultValue = [];
if (otherParams.opinionsComponents?.includes(key)) {
schema.defaultValue = otherParams.opinions;
}
}
// 上传组件集合
if (schema.component == 'Upload') {
let key = '';
if (tableName) {
key = formKey + separator + tableName + separator + schema.field;
} else {
key = formKey + separator + schema.field;
}
otherParams.uploadComponentIds.push(key);
}
if (permissionConfig?.isSubTable) {
// 子表单上传组件集合
if (schema.componentType == 'Upload') {
const key =
formKey + separator + permissionConfig.tableName + separator + permissionConfig.fieldId;
otherParams.uploadComponentIds.push(key);
}
}
return schema;
}
// 根据工作流页面权限,设置表单属性(必填,禁用,显示)
export function changeWorkFlowForm(formProps: FormProps, obj: WorkFlowFormParams) {
const {
formConfigChildren,
formConfigKey,
opinions,
opinionsComponents,
isViewProcess,
uploadIds,
formModels,
} = obj;
const returnData: {
buildOptionJson: FormProps;
uploadComponentIds: Array<string>;
formModels: any;
isViewProcess: boolean;
} = {
buildOptionJson: {}, //重新的配置Json文件
uploadComponentIds: [], //
formModels,
isViewProcess: false,
};
try {
const { buildOptionJson, uploadComponentIds } = changeFormJson(
{
formJson: formProps,
formConfigChildren,
formConfigKey: formConfigKey,
opinions,
opinionsComponents,
},
isViewProcess,
uploadIds,
);
returnData.buildOptionJson = buildOptionJson;
returnData.uploadComponentIds = uploadComponentIds;
return returnData;
} catch (error) {
return returnData;
}
}
// 根据表单json 生成权限配置
export function getWorkflowPermissionConfig(schema) {
let children: Array<TableItem> = [];
if (schema.length > 0) {
children = getSchemasConfig(schema);
}
return children;
}
function getSchemasConfig(list: FormSchema[]) {
const arr: Array<TableItem> = [];
if (list && list.length > 0) {
list.forEach((ele1) => {
if (['Card', 'Grid', 'Tab'].includes(ele1.component)) {
if (ele1.children && ele1.children.length > 0) {
ele1.children.forEach((ele2) => {
if (ele2.list && ele2.list.length > 0) {
arr.push(...getSchemasConfig(ele2.list));
}
});
}
} else if (ele1.component === 'TableLayout') {
if (ele1.children && ele1.children.length > 0) {
ele1.children.forEach((ele2) => {
if (ele2.list && ele2.list.length > 0) {
ele2.list.forEach((ele3) => {
if (ele3.children && ele3.children.length > 0) {
arr.push(...getSchemasConfig(ele3.children));
}
});
}
});
}
} else if (ele1.component == 'SubForm') {
arr.push(getTableConfig(ele1));
} else if (ele1.component === 'OneForOne') {
const obj: TableItem = getItemConfig(ele1);
obj.isSubTable = true;
obj.showChildren = false;
const tempList: FormSchema[] = ele1.componentProps?.childSchemas || [];
obj.children.push(...getSchemasConfig(tempList));
arr.push(obj);
} else {
arr.push(getItemConfig(ele1));
}
});
}
return arr;
}
function getItemConfig(element) {
const children: Array<TableItem> = [];
let required = true;
let view = true;
let edit = true;
let disabled = false;
let isSaveTable = false; //组件是否存表
const type = element.type;
if (disableTypes.includes(element.type)) {
required = false;
view = true;
edit = false;
disabled = true;
}
if (requiredDisabled.includes(element.type)) {
required = false;
}
// 隐藏组件权限不要设置
if (element.type === hiddenComponentType) {
required = false;
view = false;
edit = false;
disabled = true;
}
if (element.type === 'input' && element.componentProps.isSave) {
required = false;
view = true;
edit = false;
disabled = true;
isSaveTable = true;
}
return {
required,
view,
edit,
disabled,
isSaveTable,
tableName: '',
fieldName: element.label,
fieldId: element.field,
isSubTable: false,
showChildren: true,
type,
key: element.key,
children,
};
}
function getTableConfig(element) {
const children: Array<TableItem> = [];
if (
element.componentProps &&
element.componentProps.columns &&
element.componentProps.columns.length > 0
) {
element.componentProps.columns.forEach((ele2) => {
if (ele2.dataIndex) children.push(getTableItemConfig(element.field, ele2));
});
}
return {
required: true,
view: true,
edit: true,
disabled: false,
isSubTable: true,
showChildren: false,
tableName: element.field,
fieldName: element.label,
fieldId: element.field,
type: element.type,
key: element.key,
children,
};
}
function getTableItemConfig(tableName: string, element) {
let isSaveTable = false; //组件是否存表
if (element.type === 'input' && element.componentProps.isSave) {
isSaveTable = true;
}
return {
required: true,
view: true,
edit: true,
disabled: false,
isSubTable: true,
isSaveTable,
showChildren: false,
tableName: tableName,
fieldName: element.title,
fieldId: element.dataIndex,
type: element.type,
key: element.key,
children: [],
};
}
// 辅助设置表单Disabled
export function changeSchemaDisabled(schemas,isDisabled=true) {
const layoutComponents = ['tab', 'grid', 'card'];
schemas?.map((info) => {
if (layoutComponents.includes(info.type!)) {
info.children?.map((childInfo) => {
childInfo.list.map((com) => {
if (layoutComponents.includes(com.type)) {
changeSchemaDisabled(childInfo.list,isDisabled);
} else {
com.dynamicDisabled = isDisabled;
}
});
});
} else if (info.type == 'table-layout') {
info.children?.map((childInfo) => {
childInfo.list.map((com) => {
com.children.map((el) => {
if (layoutComponents.includes(el.type) || el.type == 'table-layout') {
changeSchemaDisabled(com.children,isDisabled);
} else {
el.dynamicDisabled = isDisabled;
}
});
});
});
} else if (info.type == 'one-for-one') {
changeSchemaDisabled(info.componentProps.childSchemas,isDisabled);
} else if (info.type == 'form') {
info.dynamicDisabled = isDisabled;
info.componentProps.disabled = isDisabled;
info.componentProps.columns?.forEach((column) => {
column.dynamicDisabled = isDisabled;
if(column?.componentProps) column.componentProps.disabled = isDisabled
})
} else {
info.dynamicDisabled = isDisabled;
}
});
return schemas;
}