前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,51 @@
import type { UnwrapRef, Ref, WritableComputedRef, DeepReadonly } from 'vue';
import { reactive, readonly, computed, getCurrentInstance, watchEffect, unref, nextTick, toRaw } from 'vue';
import { Form } from 'ant-design-vue';
import { FormItemContext } from 'ant-design-vue/es/form/FormItemContext';
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[] | undefined>
): [WritableComputedRef<V>, (val: V) => void, DeepReadonly<V>, FormItemContext];
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 formItemContext = Form.useInjectFormItemContext();
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() {
//修复多选时空值显示问题兼容值为0的情况
return innerState.value == null || innerState.value === '' ? [] : innerState.value;
},
set(value) {
if (isEqual(value, defaultState.value)) return;
innerState.value = value as T[keyof T];
nextTick(() => {
emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []));
// https://antdv.com/docs/vue/migration-v3-cn
// antDv3升级后需要调用这个方法更新校验的值
nextTick(() => formItemContext.onFieldChange());
});
},
});
return [state, setState, defaultState, formItemContext];
}

View File

@ -0,0 +1,50 @@
import type { UnwrapRef, Ref, WritableComputedRef, DeepReadonly } from 'vue';
import { reactive, readonly, computed, getCurrentInstance, watchEffect, unref, nextTick, toRaw } from 'vue';
import { Form } from 'ant-design-vue';
import { FormItemContext } from 'ant-design-vue/es/form/FormItemContext';
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[] | undefined>
): [WritableComputedRef<V>, (val: V) => void, DeepReadonly<V>, FormItemContext];
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 formItemContext = Form.useInjectFormItemContext();
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 == null ? "" : innerState.value;
},
set(value) {
if (isEqual(value, defaultState.value)) return;
innerState.value = value as T[keyof T];
nextTick(() => {
emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []));
// https://antdv.com/docs/vue/migration-v3-cn
// antDv3升级后需要调用这个方法更新校验的值
nextTick(() => formItemContext.onFieldChange());
});
},
});
return [state, setState, defaultState, formItemContext];
}

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,22 @@
import { nextTick, onMounted, onActivated } from 'vue';
type HookArgs = {
type: 'mounted' | 'activated';
}
export function onMountedOrActivated(hook: Fn<HookArgs, any>) {
let mounted: boolean;
onMounted(() => {
hook({type: 'mounted'});
nextTick(() => {
mounted = true;
});
});
onActivated(() => {
if (mounted) {
hook({type: 'activated'});
}
});
}

View File

@ -0,0 +1,41 @@
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,38 @@
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,17 @@
import { ref, unref } from 'vue';
export function useLockFn<P extends any[] = any[], V extends any = 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;
}
};
}

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,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,52 @@
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,42 @@
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,36 @@
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
import { 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,88 @@
/**
* 自适应宽度构造器
*
* @time 2022-4-8
* @author sunjianlei
*/
import { ref } from 'vue';
import { useDebounceFn, tryOnUnmounted } from '@vueuse/core';
import { useEventListener } from '/@/hooks/event/useEventListener';
// key = js运算符+数字
const defWidthConfig: configType = {
'<=565': '100%',
'<=1366': '800px',
'<=1600': '600px',
'<=1920': '600px',
'>1920': '500px',
};
type configType = Record<string, string | number>;
/**
* 自适应宽度
*
* @param widthConfig 宽度配置,可参考 defWidthConfig 配置
* @param assign 是否合并默认配置
* @param debounce 去抖毫秒数
*/
export function useAdaptiveWidth(widthConfig = defWidthConfig, assign = true, debounce = 50) {
const widthConfigAssign = assign ? Object.assign({}, defWidthConfig, widthConfig) : widthConfig;
const configKeys = Object.keys(widthConfigAssign);
const adaptiveWidth = ref<string | number>();
/**
* 进行计算宽度
* @param innerWidth
*/
function calcWidth(innerWidth) {
let width;
for (const key of configKeys) {
try {
// 通过js运算
let flag = new Function(`return ${innerWidth} ${key}`)();
if (flag) {
width = widthConfigAssign[key];
break;
}
} catch (e) {
console.error(e);
}
}
if (width) {
adaptiveWidth.value = width;
} else {
console.warn('没有找到匹配的自适应宽度');
}
}
// 初始计算
calcWidth(window.innerWidth);
// 监听 resize 事件
const { removeEvent } = useEventListener({
el: window,
name: 'resize',
listener: useDebounceFn(() => calcWidth(window.innerWidth), debounce),
});
// 卸载组件时取消监听事件
tryOnUnmounted(() => removeEvent());
return { adaptiveWidth };
}
/**
* 抽屉自适应宽度
*/
export function useDrawerAdaptiveWidth() {
return useAdaptiveWidth(
{
'<=620': '100%',
'<=1600': 600,
'<=1920': 650,
'>1920': 700,
},
false
);
}

View File

@ -0,0 +1,42 @@
import type { GlobConfig } from '/#/config';
import { getAppEnvConfig } from '/@/utils/env';
export const useGlobSetting = (): Readonly<GlobConfig> => {
const {
VITE_GLOB_APP_TITLE,
VITE_GLOB_API_URL,
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_APP_CAS_BASE_URL,
VITE_GLOB_APP_OPEN_SSO,
VITE_GLOB_APP_OPEN_QIANKUN,
VITE_GLOB_DOMAIN_URL,
VITE_GLOB_ONLINE_VIEW_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.`
// );
// }
// 短标题替换shortName的下划线为空格
const shortTitle = VITE_GLOB_APP_SHORT_NAME.replace(/_/g, " ");
// Take global configuration
const glob: Readonly<GlobConfig> = {
title: VITE_GLOB_APP_TITLE,
domainUrl: VITE_GLOB_DOMAIN_URL,
apiUrl: VITE_GLOB_API_URL,
shortName: VITE_GLOB_APP_SHORT_NAME,
shortTitle: shortTitle,
openSso: VITE_GLOB_APP_OPEN_SSO,
openQianKun: VITE_GLOB_APP_OPEN_QIANKUN,
casBaseUrl: VITE_GLOB_APP_CAS_BASE_URL,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_DOMAIN_URL,
viewUrl: VITE_GLOB_ONLINE_VIEW_URL,
};
window._CONFIG['domianURL'] = VITE_GLOB_DOMAIN_URL;
return glob as Readonly<GlobConfig>;
};

View File

@ -0,0 +1,90 @@
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 getShowBreadTitle = 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,
getShowBreadTitle
};
}

View File

@ -0,0 +1,157 @@
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)) {
// update-begin--author:liaozhiyang---date:20240407---for【QQYUN-8774】侧边混合导航菜单宽度调整
return unref(getCollapsed) && !unref(getMixSideFixed) ? unref(getMiniWidthNumber) : unref(getMenuWidth) - 60;
// update-end--author:liaozhiyang---date:20240407---for【QQYUN-8774】侧边混合导航菜单宽度调整
}
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,32 @@
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);
// 获取标签页样式
const getTabsTheme = computed(() => appStore.getMultiTabsSetting.theme);
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
appStore.setProjectConfig({ multiTabsSetting });
}
return {
setMultipleTabSetting,
getShowMultipleTab,
getShowQuick,
getShowRedo,
getShowFold,
getTabsTheme,
};
}

View File

@ -0,0 +1,88 @@
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,51 @@
import { ref } from 'vue';
import { ScreenSizeEnum } from '/@/enums/sizeEnum';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
// 定义 useAdapt 方法参数
interface AdaptOptions {
// xl>1200
xl?: string | number;
// xl>992
lg?: string | number;
// xl>768
md?: string | number;
// xl>576
sm?: string | number;
// xl>480
xs?: string | number;
//xl<480默认值
mindef?: string | number;
//默认值
def?: string | number;
}
export function useAdapt(props?: AdaptOptions) {
//默认宽度
const width = ref<string | number>(props?.def || '600px');
//获取宽度
useWindowSizeFn(calcWidth, 100, { immediate: true });
//计算宽度
function calcWidth() {
let windowWidth = document.documentElement.clientWidth;
switch (true) {
case windowWidth > ScreenSizeEnum.XL:
width.value = props?.xl || '600px';
break;
case windowWidth > ScreenSizeEnum.LG:
width.value = props?.lg || '600px';
break;
case windowWidth > ScreenSizeEnum.MD:
width.value = props?.md || '600px';
break;
case windowWidth > ScreenSizeEnum.SM:
width.value = props?.sm || '500px';
break;
case windowWidth > ScreenSizeEnum.XS:
width.value = props?.xs || '400px';
break;
default:
width.value = props?.mindef || '300px';
break;
}
}
return { width, calcWidth };
}

View File

@ -0,0 +1,188 @@
import { defHttp } from '/@/utils/http/axios';
import { ref, unref } from 'vue';
import { VALIDATE_FAILED, validateFormModelAndTables } from '/@/utils/common/vxeUtils';
export function useJvxeMethod(requestAddOrEdit, classifyIntoFormData, tableRefs, activeKey, refKeys, validateSubForm?) {
const formRef = ref();
/** 查询某个tab的数据 */
function requestSubTableData(url, params, tab, success) {
tab.loading = true;
defHttp
.get({ url, params }, { isTransformResponse: false })
.then((res) => {
let { result } = res;
if (res.success && result) {
if (Array.isArray(result)) {
tab.dataSource = result;
} else if (Array.isArray(result.records)) {
tab.dataSource = result.records;
}
}
typeof success === 'function' ? success(res) : '';
})
.finally(() => {
tab.loading = false;
});
}
/* --- handle 事件 --- */
/** ATab 选项卡切换事件 */
function handleChangeTabs(key) {
// 自动重置scrollTop状态防止出现白屏
tableRefs[key]?.value?.resetScrollTop(0);
}
/** 获取所有的editableTable实例*/
function getAllTable() {
let values = Object.values(tableRefs);
return Promise.all(values);
}
/** 确定按钮点击事件 */
function handleSubmit() {
/** 触发表单验证 */
getAllTable()
.then((tables) => {
let values = formRef.value.getFieldsValue();
return validateFormModelAndTables(formRef.value.validate, values, tables, formRef.value.getProps, false);
})
.then((allValues) => {
/** 一次性验证一对一的所有子表 */
return validateSubForm && typeof validateSubForm === 'function' ? validateSubForm(allValues) : validateAllSubOne(allValues);
})
.then((allValues) => {
if (typeof classifyIntoFormData !== 'function') {
throw throwNotFunction('classifyIntoFormData');
}
let formData = classifyIntoFormData(allValues);
// 发起请求
return requestAddOrEdit(formData);
})
.catch((e) => {
if (e.error === VALIDATE_FAILED) {
// 如果有未通过表单验证的子表就自动跳转到它所在的tab
//update-begin-author:taoyan date:2022-11-22 for: VUEN-2866【代码生成】Tab风格 一对多子表校验不通过时,点击提交表单空白了,流程附加页面也有此问题
if(e.paneKey){
activeKey.value = e.paneKey
}else{
//update-begin-author:liusq date:2024-06-12 for: TV360X-478 一对多tab校验未通过时tab没有跳转
activeKey.value = e.subIndex == null ? (e.index == null ? unref(activeKey) : refKeys.value[e.index]) : Object.keys(tableRefs)[e.subIndex];
//update-end-author:liusq date:2024-06-12 for: TV360X-478 一对多tab校验未通过时tab没有跳转
}
//update-end-author:taoyan date:2022-11-22 for: VUEN-2866【代码生成】Tab风格 一对多子表校验不通过时,点击提交表单空白了,流程附加页面也有此问题
} else {
console.error(e);
}
});
}
//校验所有子表表单
function validateAllSubOne(allValues) {
return new Promise((resolve) => {
resolve(allValues);
});
}
/* --- throw --- */
/** not a function */
function throwNotFunction(name) {
return `${name} 未定义或不是一个函数`;
}
/** not a array */
function throwNotArray(name) {
return `${name} 未定义或不是一个数组`;
}
return [handleChangeTabs, handleSubmit, requestSubTableData, formRef];
}
//update-begin-author:taoyan date:2022-6-16 for: 代码生成-原生表单用
/**
* 校验多个表单和子表table用于原生的antd-vue的表单
* @param activeKey 子表表单/vxe-table 所在tabs的 activeKey
* @param refMap 子表表单/vxe-table对应的ref对象 map结构
* 示例:
* useValidateAntFormAndTable(activeKey, {
* 'tableA': tableARef,
* 'formB': formBRef
* })
*/
export function useValidateAntFormAndTable(activeKey, refMap) {
/**
* 获取所有子表数据
*/
async function getSubFormAndTableData() {
let formData = {};
let all = Object.keys(refMap);
let key = '';
for (let i = 0; i < all.length; i++) {
key = all[i];
let instance = refMap[key].value;
if (instance.isForm) {
let subFormData = await validateFormAndGetData(instance, key);
if (subFormData) {
formData[key + 'List'] = [subFormData];
}
} else {
let arr = await validateTableAndGetData(instance, key);
if (arr && arr.length > 0) {
formData[key + 'List'] = arr;
}
}
}
return formData;
}
/**
* 转换数据用 如果有数组转成逗号分割的格式
* @param data
*/
function transformData(data) {
if (data) {
Object.keys(data).map((k) => {
if (data[k] instanceof Array) {
data[k] = data[k].join(',');
}
});
}
return data;
}
/**
* 子表table
* @param instance
* @param key
*/
async function validateTableAndGetData(instance, key) {
const errors = await instance.validateTable();
if (!errors) {
return instance.getTableData();
} else {
activeKey.value = key;
// 自动重置scrollTop状态防止出现白屏
instance.resetScrollTop(0);
return Promise.reject(1);
}
}
/**
* 子表表单
* @param instance
* @param key
*/
async function validateFormAndGetData(instance, key) {
try {
let data = await instance.getFormData();
transformData(data);
return data;
} catch (e) {
activeKey.value = key;
return Promise.reject(e);
}
}
return {
getSubFormAndTableData,
transformData,
};
}
//update-end-author:taoyan date:2022-6-16 for: 代码生成-原生表单用

View File

@ -0,0 +1,355 @@
import { reactive, ref, Ref, unref } from 'vue';
import { merge } from 'lodash-es';
import { DynamicProps } from '/#/utils';
import { BasicTableProps, TableActionType, useTable } from '/@/components/Table';
import { ColEx } from '/@/components/Form/src/types';
import { FormActionType } from '/@/components/Form';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { useDesign } from '/@/hooks/web/useDesign';
import { filterObj } from '/@/utils/common/compUtils';
const { handleExportXls, handleImportXls } = useMethods();
// 定义 useListPage 方法所需参数
interface ListPageOptions {
// 样式作用域范围
designScope?: string;
// 【必填】表格参数配置
tableProps: TableProps;
// 是否分页
pagination?: boolean;
// 导出配置
exportConfig?: {
url: string | (() => string);
// 导出文件名
name?: string | (() => string);
//导出参数
params?: object;
};
// 导入配置
importConfig?: {
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导入地址是动态的
url: string | (() => string);
//update-end-author:taoyan date:20220507 for: erp代码生成 子表 导入地址是动态的
// 导出成功后的回调
success?: (fileInfo?: any) => void;
};
}
interface IDoRequestOptions {
// 是否显示确认对话框,默认 true
confirm?: boolean;
// 是否自动刷新表格,默认 true
reload?: boolean;
// 是否自动清空选择,默认 true
clearSelection?: boolean;
}
/**
* listPage页面公共方法
*
* @param options
*/
export function useListPage(options: ListPageOptions) {
const $message = useMessage();
let $design = {} as ReturnType<typeof useDesign>;
if (options.designScope) {
$design = useDesign(options.designScope);
}
const tableContext = useListTable(options.tableProps);
const [, { getForm, reload, setLoading }, { selectedRowKeys }] = tableContext;
// 导出 excel
async function onExportXls() {
//update-begin---author:wangshuai ---date:20220411 for导出新增自定义参数------------
let { url, name, params } = options?.exportConfig ?? {};
let realUrl = typeof url === 'function' ? url() : url;
if (realUrl) {
let title = typeof name === 'function' ? name() : name;
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导出报错,原因未知-
let paramsForm:any = {};
try {
paramsForm = await getForm().validate();
} catch (e) {
console.error(e);
}
//update-end-author:taoyan date:20220507 for: erp代码生成 子表 导出报错,原因未知-
//update-begin-author:liusq date:20230410 for:[/issues/409]导出功能没有按排序结果导出,设置导出默认排序,创建时间倒序
if(!paramsForm?.column){
Object.assign(paramsForm,{column:'createTime',order:'desc'});
}
//update-begin-author:liusq date:20230410 for: [/issues/409]导出功能没有按排序结果导出,设置导出默认排序,创建时间倒序
//如果参数不为空,则整合到一起
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导出动态设置mainId
if (params) {
Object.keys(params).map((k) => {
let temp = (params as object)[k];
if (temp) {
paramsForm[k] = unref(temp);
}
});
}
//update-end-author:taoyan date:20220507 for: erp代码生成 子表 导出动态设置mainId
if (selectedRowKeys.value && selectedRowKeys.value.length > 0) {
paramsForm['selections'] = selectedRowKeys.value.join(',');
}
console.log()
return handleExportXls(title as string, realUrl, filterObj(paramsForm));
//update-end---author:wangshuai ---date:20220411 for导出新增自定义参数--------------
} else {
$message.createMessage.warn('没有传递 exportConfig.url 参数');
return Promise.reject();
}
}
// 导入 excel
function onImportXls(file) {
let { url, success } = options?.importConfig ?? {};
//update-begin-author:taoyan date:20220507 for: erp代码生成 子表 导入地址是动态的
let realUrl = typeof url === 'function' ? url() : url;
if (realUrl) {
return handleImportXls(file, realUrl, success || reload);
//update-end-author:taoyan date:20220507 for: erp代码生成 子表 导入地址是动态的
} else {
$message.createMessage.warn('没有传递 importConfig.url 参数');
return Promise.reject();
}
}
/**
* 通用请求处理方法,可自动刷新表格,自动清空选择
* @param api 请求api
* @param options 是否显示确认框
*/
function doRequest(api: () => Promise<any>, options?: IDoRequestOptions) {
return new Promise((resolve, reject) => {
const execute = async () => {
try {
setLoading(true);
const res = await api();
if (options?.reload ?? true) {
reload();
}
if (options?.clearSelection ?? true) {
selectedRowKeys.value = [];
}
resolve(res);
} catch (e) {
reject(e);
} finally {
setLoading(false);
}
};
if (options?.confirm ?? true) {
$message.createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => execute(),
onCancel: () => reject(),
});
} else {
execute();
}
});
}
/** 执行单个删除操作 */
function doDeleteRecord(api: () => Promise<any>) {
return doRequest(api, { confirm: false, clearSelection: false });
}
return {
...$design,
...$message,
onExportXls,
onImportXls,
doRequest,
doDeleteRecord,
tableContext,
};
}
// 定义表格所需参数
type TableProps = Partial<DynamicProps<BasicTableProps>>;
type UseTableMethod = TableActionType & {
getForm: () => FormActionType;
};
/**
* useListTable 列表页面标准表格参数
*
* @param tableProps 表格参数
*/
export function useListTable(tableProps: TableProps): [
(instance: TableActionType, formInstance: UseTableMethod) => void,
TableActionType & {
getForm: () => FormActionType;
},
{
rowSelection: any;
selectedRows: Ref<Recordable[]>;
selectedRowKeys: Ref<any[]>;
}
] {
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 12, // ≥576px
md: 12, // ≥768px
lg: 8, // ≥992px
xl: 8, // ≥1200px
xxl: 6, // ≥1600px
};
const defaultTableProps: TableProps = {
rowKey: 'id',
// 使用查询条件区域
useSearchForm: true,
// 查询条件区域配置
formConfig: {
// 紧凑模式
compact: true,
// label默认宽度
// labelWidth: 120,
// 按下回车后自动提交
autoSubmitOnEnter: true,
// 默认 row 配置
rowProps: { gutter: 8 },
// 默认 col 配置
baseColProps: {
...adaptiveColProps,
},
labelCol: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 6,
},
wrapperCol: {},
// 是否显示 展开/收起 按钮
showAdvancedButton: true,
// 超过指定列数默认折叠
autoAdvancedCol: 3,
// 操作按钮配置
actionColOptions: {
...adaptiveColProps,
style: { textAlign: 'left' },
},
},
// 斑马纹
striped: false,
// 是否可以自适应高度
canResize: true,
// 表格最小高度
// update-begin--author:liaozhiyang---date:20240603---for【TV360X-861】列表查询区域不可往上滚动
minHeight: 300,
// update-end--author:liaozhiyang---date:20240603---for【TV360X-861】列表查询区域不可往上滚动
// 点击行选中
clickToRowSelect: false,
// 是否显示边框
bordered: true,
// 是否显示序号列
showIndexColumn: false,
// 显示表格设置
showTableSetting: true,
// 表格全屏设置
tableSetting: {
fullScreen: false,
},
// 是否显示操作列
showActionColumn: true,
// 操作列
actionColumn: {
width: 120,
title: '操作',
//是否锁定操作列取值 right ,left,false
fixed: false,
dataIndex: 'action',
slots: { customRender: 'action' },
},
};
// 合并用户个性化配置
if (tableProps) {
//update-begin---author:wangshuai---date:2024-04-28---for:【issues/6180】前端代码配置表变查询条件显示列不生效---
if(tableProps.formConfig){
setTableProps(tableProps.formConfig);
}
//update-end---author:wangshuai---date:2024-04-28---for:【issues/6180】前端代码配置表变查询条件显示列不生效---
// merge 方法可深度合并对象
merge(defaultTableProps, tableProps);
}
// 发送请求之前调用的方法
function beforeFetch(params) {
// 默认以 createTime 降序排序
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
// 合并方法
Object.assign(defaultTableProps, { beforeFetch });
if (typeof tableProps.beforeFetch === 'function') {
defaultTableProps.beforeFetch = function (params) {
params = beforeFetch(params);
// @ts-ignore
tableProps.beforeFetch(params);
return params;
};
}
// 当前选择的行
const selectedRowKeys = ref<any[]>([]);
// 选择的行记录
const selectedRows = ref<Recordable[]>([]);
// 表格选择列配置
const rowSelection: any = tableProps?.rowSelection ?? {};
const defaultRowSelection = reactive({
...rowSelection,
type: rowSelection.type ?? 'checkbox',
// 选择列宽度,默认 50
columnWidth: rowSelection.columnWidth ?? 50,
selectedRows: selectedRows,
selectedRowKeys: selectedRowKeys,
onChange(...args) {
selectedRowKeys.value = args[0];
selectedRows.value = args[1];
if (typeof rowSelection.onChange === 'function') {
rowSelection.onChange(...args);
}
},
});
delete defaultTableProps.rowSelection;
/**
* 设置表格参数
*
* @param formConfig
*/
function setTableProps(formConfig: any) {
const replaceAttributeArray: string[] = ['baseColProps','labelCol'];
for (let item of replaceAttributeArray) {
if(formConfig && formConfig[item]){
if(defaultTableProps.formConfig){
let defaultFormConfig:any = defaultTableProps.formConfig;
defaultFormConfig[item] = formConfig[item];
}
formConfig[item] = {};
}
}
}
return [
...useTable(defaultTableProps),
{
selectedRows,
selectedRowKeys,
rowSelection: defaultRowSelection,
},
];
}

View File

@ -0,0 +1,126 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import { useGlobSetting } from '/@/hooks/setting';
const { createMessage, createWarningModal } = useMessage();
const glob = useGlobSetting();
/**
* 导出文件xlsx的mime-type
*/
export const XLSX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
/**
* 导出文件xlsx的文件后缀
*/
export const XLSX_FILE_SUFFIX = '.xlsx';
export function useMethods() {
/**
* 导出xls
* @param name
* @param url
*/
async function exportXls(name, url, params, isXlsx = false) {
//update-begin---author:wangshuai---date:2024-01-25---for:【QQYUN-8118】导出超时时间设置长点---
const data = await defHttp.get({ url: url, params: params, responseType: 'blob', timeout: 60000 }, { isTransformResponse: false });
//update-end---author:wangshuai---date:2024-01-25---for:【QQYUN-8118】导出超时时间设置长点---
if (!data) {
createMessage.warning('文件下载失败');
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for: 导出excel失败提示不进行导出---
let reader = new FileReader()
reader.readAsText(data, 'utf-8')
reader.onload = async () => {
if(reader.result){
if(reader.result.toString().indexOf("success") !=-1){
const { success, message } = JSON.parse(reader.result.toString());
if (!success) {
createMessage.warning("导出失败,失败原因:"+ message);
}else{
exportExcel(name, isXlsx, data);
}
return;
}
}
exportExcel(name, isXlsx, data);
//update-end---author:wangshuai---date:2024-04-18---for: 导出excel失败提示不进行导出---
}
}
/**
* 导入xls
* @param data 导入的数据
* @param url
* @param success 成功后的回调
*/
async function importXls(data, url, success) {
const isReturn = (fileInfo) => {
try {
if (fileInfo.code === 201) {
let {
message,
result: { msg, fileUrl, fileName },
} = fileInfo;
let href = glob.uploadUrl + fileUrl;
createWarningModal({
title: message,
centered: false,
content: `<div>
<span>${msg}</span><br/>
<span>具体详情请<a href = ${href} download = ${fileName}> 点击下载 </a> </span>
</div>`,
});
//update-begin---author:wangshuai ---date:20221121 for[VUEN-2827]导入无权限,提示图标错误------------
} else if (fileInfo.code === 500 || fileInfo.code === 510) {
createMessage.error(fileInfo.message || `${data.file.name} 导入失败`);
//update-end---author:wangshuai ---date:20221121 for[VUEN-2827]导入无权限,提示图标错误------------
} else {
createMessage.success(fileInfo.message || `${data.file.name} 文件上传成功`);
}
} catch (error) {
console.log('导入的数据异常', error);
} finally {
typeof success === 'function' ? success(fileInfo) : '';
}
};
await defHttp.uploadFile({ url }, { file: data.file }, { success: isReturn });
}
return {
handleExportXls: (name: string, url: string, params?: object) => exportXls(name, url, params),
handleImportXls: (data, url, success) => importXls(data, url, success),
handleExportXlsx: (name: string, url: string, params?: object) => exportXls(name, url, params, true),
};
/**
* 导出excel
* @param name
* @param isXlsx
* @param data
*/
function exportExcel(name, isXlsx, data) {
if (!name || typeof name != 'string') {
name = '导出文件';
}
let blobOptions = { type: 'application/vnd.ms-excel' };
let fileSuffix = '.xls';
if (isXlsx) {
blobOptions['type'] = XLSX_MIME_TYPE;
fileSuffix = XLSX_FILE_SUFFIX;
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data], blobOptions), name + fileSuffix);
} else {
let url = window.URL.createObjectURL(new Blob([data], blobOptions));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', name + fileSuffix);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); //下载完成移除元素
window.URL.revokeObjectURL(url); //释放掉blob对象
}
}
}

View File

@ -0,0 +1,201 @@
import { ref, unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useGlobSetting } from '/@/hooks/setting';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { setThirdCaptcha, getCaptcha } from '/@/api/sys/user';
import { useI18n } from '/@/hooks/web/useI18n';
export function useThirdLogin() {
const { createMessage, notification } = useMessage();
const { t } = useI18n();
const glob = useGlobSetting();
const userStore = useUserStore();
//第三方类型
const thirdType = ref('');
//第三方登录相关信息
const thirdLoginInfo = ref<any>({});
//状态
const thirdLoginState = ref(false);
//绑定手机号弹窗
const bindingPhoneModal = ref(false);
//第三方用户UUID
const thirdUserUuid = ref('');
//提示窗
const thirdConfirmShow = ref(false);
//绑定密码弹窗
const thirdPasswordShow = ref(false);
//绑定密码
const thirdLoginPassword = ref('');
//绑定用户
const thirdLoginUser = ref('');
//加载中
const thirdCreateUserLoding = ref(false);
//绑定手机号
const thirdPhone = ref('');
//验证码
const thirdCaptcha = ref('');
//第三方登录
function onThirdLogin(source) {
let url = `${glob.uploadUrl}/sys/thirdLogin/render/${source}`;
window.open(
url,
`login ${source}`,
'height=500, width=500, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=n o, status=no'
);
thirdType.value = source;
thirdLoginInfo.value = {};
thirdLoginState.value = false;
let receiveMessage = function (event) {
let token = event.data;
if (typeof token === 'string') {
//如果是字符串类型 说明是token信息
if (token === '登录失败') {
createMessage.warning(token);
} else if (token.includes('绑定手机号')) {
bindingPhoneModal.value = true;
let strings = token.split(',');
thirdUserUuid.value = strings[1];
} else {
doThirdLogin(token);
}
} else if (typeof token === 'object') {
//对象类型 说明需要提示是否绑定现有账号
if (token['isObj'] === true) {
thirdConfirmShow.value = true;
thirdLoginInfo.value = { ...token };
}
} else {
createMessage.warning('不识别的信息传递');
}
//update-begin---author:wangshuai---date:2024-02-20---for:【QQYUN-8156】连续登录失败导致失败提醒累加---
window.removeEventListener('message', unref(receiveMessage),false);
//update-end---author:wangshuai---date:2024-02-20---for:【QQYUN-8156】连续登录失败导致失败提醒累加---
};
window.addEventListener('message', receiveMessage, false);
}
// 根据token执行登录
function doThirdLogin(token) {
if (unref(thirdLoginState) === false) {
thirdLoginState.value = true;
userStore.ThirdLogin({ token, thirdType: unref(thirdType) }).then((res) => {
console.log('res====>doThirdLogin', res);
if (res && res.userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${res.userInfo.realname}`,
duration: 3,
});
} else {
requestFailed(res);
}
});
}
}
function requestFailed(err) {
notification.error({
message: '登录失败',
description: ((err.response || {}).data || {}).message || err.message || '请求出现错误,请稍后再试',
duration: 4,
});
}
// 绑定已有账号 需要输入密码
function thirdLoginUserBind() {
thirdLoginPassword.value = '';
thirdLoginUser.value = thirdLoginInfo.value.uuid;
thirdConfirmShow.value = false;
thirdPasswordShow.value = true;
}
//创建新账号
function thirdLoginUserCreate() {
thirdCreateUserLoding.value = true;
// 账号名后面添加两位随机数
thirdLoginInfo.value.suffix = parseInt(Math.random() * 98 + 1);
defHttp
.post({ url: '/sys/third/user/create', params: { thirdLoginInfo: unref(thirdLoginInfo) } }, { isTransformResponse: false })
.then((res) => {
if (res.success) {
let token = res.result;
doThirdLogin(token);
thirdConfirmShow.value = false;
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
thirdCreateUserLoding.value = false;
});
}
// 核实密码
function thirdLoginCheckPassword() {
let params = Object.assign({}, unref(thirdLoginInfo), { password: unref(thirdLoginPassword) });
defHttp.post({ url: '/sys/third/user/checkPassword', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
thirdLoginNoPassword();
doThirdLogin(res.result);
} else {
createMessage.warning(res.message);
}
});
}
// 没有密码 取消操作
function thirdLoginNoPassword() {
thirdPasswordShow.value = false;
thirdLoginPassword.value = '';
thirdLoginUser.value = '';
}
//倒计时执行前的函数
function sendCodeApi() {
//return setThirdCaptcha({mobile:unref(thirdPhone)});
return getCaptcha({ mobile: unref(thirdPhone), smsmode: '0' });
}
//绑定手机号点击确定按钮
function thirdHandleOk() {
if (!unref(thirdPhone)) {
cmsFailed('请输入手机号');
}
if (!unref(thirdCaptcha)) {
cmsFailed('请输入验证码');
}
let params = {
mobile: unref(thirdPhone),
captcha: unref(thirdCaptcha),
thirdUserUuid: unref(thirdUserUuid),
};
defHttp.post({ url: '/sys/thirdLogin/bindingThirdPhone', params }, { isTransformResponse: false }).then((res) => {
if (res.success) {
bindingPhoneModal.value = false;
doThirdLogin(res.result);
} else {
createMessage.warning(res.message);
}
});
}
function cmsFailed(err) {
notification.error({
message: '登录失败',
description: err,
duration: 4,
});
return;
}
//返回数据和方法
return {
thirdPasswordShow,
thirdLoginCheckPassword,
thirdLoginNoPassword,
thirdLoginPassword,
thirdConfirmShow,
thirdCreateUserLoding,
thirdLoginUserCreate,
thirdLoginUserBind,
bindingPhoneModal,
thirdHandleOk,
thirdPhone,
thirdCaptcha,
onThirdLogin,
sendCodeApi,
};
}

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,183 @@
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,64 @@
import { isRef, unref, watch, Ref, ComputedRef } from 'vue';
import Clipboard from 'clipboard';
import { ModalOptionsEx, useMessage } from '/@/hooks/web/useMessage';
/** 带复制按钮的弹窗 */
interface IOptions extends ModalOptionsEx {
// 要复制的文本,可以是一个 ref 对象,动态更新
copyText: string | Ref<string> | ComputedRef<string>;
}
const COPY_CLASS = 'copy-this-text';
const CLIPBOARD_TEXT = 'data-clipboard-text';
export function useCopyModal() {
return { createCopyModal };
}
const { createMessage, createConfirm } = useMessage();
/** 创建复制弹窗 */
function createCopyModal(options: Partial<IOptions>) {
let modal = createConfirm({
...options,
iconType: options.iconType ?? 'info',
width: options.width ?? 500,
title: options.title ?? '复制',
maskClosable: options.maskClosable ?? true,
okText: options.okText ?? '复制',
okButtonProps: {
...options.okButtonProps,
class: COPY_CLASS,
[CLIPBOARD_TEXT]: unref(options.copyText),
} as any,
onOk() {
return new Promise((resolve: any) => {
const clipboard = new Clipboard('.' + COPY_CLASS);
clipboard.on('success', () => {
clipboard.destroy();
createMessage.success('复制成功');
resolve();
});
clipboard.on('error', () => {
createMessage.error('该浏览器不支持自动复制');
clipboard.destroy();
resolve();
});
});
},
});
// 动态更新 copyText
if (isRef(options.copyText)) {
watch(options.copyText, (copyText) => {
modal.update({
okButtonProps: {
...options.okButtonProps,
class: COPY_CLASS,
[CLIPBOARD_TEXT]: copyText,
} as any,
});
});
}
return modal;
}

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) {
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,
};
}

View File

@ -0,0 +1,115 @@
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') {
console.log("---useECharts---初始化加载---")
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,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 };
};

View File

@ -0,0 +1,55 @@
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 '';
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,158 @@
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';
import { h } from 'vue';
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) {
try {
if (iconType === 'warning') {
return h(InfoCircleFilled,{"class":"modal-icon-warning"})
} else if (iconType === 'success') {
return h(CheckCircleFilled,{"class": "modal-icon-success"});
} else if (iconType === 'info') {
return h(InfoCircleFilled,{"class": "modal-icon-info"});
} else {
return h(CloseCircleFilled,{"class":"modal-icon-error"});
}
} catch (e) {
console.log(e);
}
}
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
try {
if (isString(content)) {
return h('div', h('div', {'innerHTML':content as string}));
} else {
return content;
}
} catch (e) {
console.log(e);
return content;
}
}
/**
* @description: Create confirmation box
*/
function createConfirm(options: ModalOptionsEx): ReturnType<ModalFunc> {
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);
}
const getBaseOptions = () => {
const { t } = useI18n();
return {
okText: t('common.okText'),
centered: true,
};
};
function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial {
//update-begin-author:taoyan date:2023-1-10 for: 可以自定义图标
let titleIcon:any = ''
if(options.icon){
titleIcon = options.icon;
}else{
titleIcon = getIcon(icon)
}
//update-end-author:taoyan date:2023-1-10 for: 可以自定义图标
return {
...getBaseOptions(),
...options,
content: renderContent(options),
icon: titleIcon
};
}
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'));
}
interface MOE extends Omit<ModalOptionsEx, 'iconType'> {
iconType?: ModalOptionsEx['iconType'];
}
// 提示框无需传入iconType默认为warning
function createConfirmSync(options: MOE) {
return new Promise((resolve) => {
createConfirm({
iconType: 'warning',
...options,
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
});
}
notification.config({
placement: 'topRight',
duration: 3,
});
/**
* @description: message
*/
export function useMessage() {
return {
createMessage: Message,
notification: notification as NotifyApi,
createConfirm: createConfirm,
createConfirmSync,
createSuccessModal,
createErrorModal,
createInfoModal,
createWarningModal,
};
}

View File

@ -0,0 +1,132 @@
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) {
try {
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" />;
}
} catch (e) {
console.log(e);
}
}
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
try {
if (isString(content)) {
return <div innerHTML={`<div>${content as string}</div>`}></div>;
} else {
return content;
}
} catch (e) {
console.log(e);
return content;
}
}
/**
* @description: Create confirmation box
*/
function createConfirm(options: ModalOptionsEx): ReturnType<ModalFunc> {
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);
}
const getBaseOptions = () => {
const { t } = useI18n();
return {
okText: t('common.okText'),
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,
};
}

View File

@ -0,0 +1,78 @@
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';
import { useUserStore } from '/@/store/modules/user';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
function handleError(e: Error) {
console.error(e);
}
// page switch
export function useGo(_router?: Router) {
// update-begin--author:liaozhiyang---date:20230908---for【issues/694】404返回首页问题
const userStore = useUserStore();
const homePath = userStore.getUserInfo.homePath || PageEnum.BASE_HOME;
// update-end--author:liaozhiyang---date:20230908---for【issues/694】404返回首页问题
let router;
if (!_router) {
router = useRouter();
}
const { push, replace } = _router || router;
function go(opt: PageEnum | RouteLocationRawEx | string = homePath, 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;
}
// update-begin--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
const tabStore = useMultipleTabStore();
if (name && Object.keys(params).length > 0) {
tabStore.setRedirectPageParam({
redirect_type: 'name',
name: String(name),
params,
query,
});
params['path'] = String(name);
} else {
tabStore.setRedirectPageParam({
redirect_type: 'path',
path: fullPath,
query,
});
params['path'] = fullPath;
}
// update-end--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
push({ name: REDIRECT_NAME, params, query }).then(() => resolve(true));
});
}
return redo;
};

View File

@ -0,0 +1,31 @@
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,174 @@
import type { 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';
// User permissions related operations
export function usePermission() {
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
//动态加载流程节点表单权限
let formData: any = {};
function initBpmFormData(_bpmFormData) {
formData = _bpmFormData;
}
const { closeAll } = useTabs(router);
//==================================工作流权限判断-begin=========================================
function hasBpmPermission(code, type) {
// 禁用-type=2
// 显示-type=1
let codeList: string[] = [];
let permissionList = formData.permissionList;
if (permissionList && permissionList.length > 0) {
for (let item of permissionList) {
if (item.type == type) {
codeList.push(item.action);
}
}
}
return codeList.indexOf(code) >= 0;
}
//==================================工作流权限判断-end=========================================
/**
* 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();
}
/**
* 确定是否存在权限
*/
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 allCodeList = permissionStore.getPermCodeList as string[];
if (!isArray(value) && allCodeList && allCodeList.length > 0) {
//=============================工作流权限判断-显示-begin==============================================
if (formData) {
let code = value as string;
if (hasBpmPermission(code, '1') === true) {
return true;
}
}
//=============================工作流权限判断-显示-end==============================================
return allCodeList.includes(value);
}
return (intersection(value, allCodeList) as string[]).length > 0;
}
return true;
}
/**
* 是否禁用组件
*/
function isDisabledAuth(value?: RoleEnum | RoleEnum[] | string | string[], def = true): boolean {
//=============================工作流权限判断-禁用-begin==============================================
if (formData) {
let code = value as string;
if (hasBpmPermission(code, '2') === true) {
return true;
}
//update-begin-author:taoyan date:2022-6-17 for: VUEN-1342【流程】编码方式 节点权限配置好后,未生效
if (isCodingButNoConfig(code) == true) {
return false;
}
//update-end-author:taoyan date:2022-6-17 for: VUEN-1342【流程】编码方式 节点权限配置好后,未生效
}
//=============================工作流权限判断-禁用-end==============================================
return !hasPermission(value);
}
/**
* 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();
}
//update-begin-author:taoyan date:2022-6-17 for: VUEN-1342【流程】编码方式 节点权限配置好后,未生效
/**
* 判断是不是 代码里写了逻辑但是没有配置权限这种情况
*/
function isCodingButNoConfig(code) {
let all = permissionStore.allAuthList;
if (all && all instanceof Array) {
let temp = all.filter((item) => item.action == code);
if (temp && temp.length > 0) {
if (temp[0].status == '0') {
return true;
}
} else {
return true;
}
}
return false;
}
//update-end-author:taoyan date:2022-6-17 for: VUEN-1342【流程】编码方式 节点权限配置好后,未生效
return { changeRole, hasPermission, togglePermissionMode, refreshMenu, isDisabledAuth, initBpmFormData };
}

View File

@ -0,0 +1,42 @@
import { nextTick } from 'vue';
import $printJS, { Configuration } from 'print-js';
import Print from 'vue-print-nb-jeecg/src/printarea';
/**
* 调用 printJS如果type = html就走 printNB 的方法
*/
export function printJS(configuration: Configuration) {
if (configuration?.type === 'html') {
printNb(configuration.printable);
} else {
return $printJS(configuration);
}
}
/** 调用 printNB 打印 */
export function printNb(domId) {
if (domId) {
localPrint(domId);
} else {
window.print();
}
}
let closeBtn = true;
function localPrint(domId) {
if (typeof domId === 'string' && !domId.startsWith('#')) {
domId = '#' + domId;
}
nextTick(() => {
if (closeBtn) {
closeBtn = false;
new Print({
el: domId,
endCallback() {
closeBtn = true;
},
});
}
});
}

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 };
}

View File

@ -0,0 +1,45 @@
// 单点登录核心类
import { getToken } from '/@/utils/auth';
import { getUrlParam } from '/@/utils';
import { useGlobSetting } from '/@/hooks/setting';
import { validateCasLogin } from '/@/api/sys/user';
import { useUserStore } from '/@/store/modules/user';
const globSetting = useGlobSetting();
const openSso = globSetting.openSso;
export function useSso() {
//update-begin---author:wangshuai---date:2024-01-03---for:【QQYUN-7805】SSO登录强制用http #957---
let locationUrl = document.location.protocol +"//" + window.location.host + '/';
//update-end---author:wangshuai---date:2024-01-03---for:【QQYUN-7805】SSO登录强制用http #957---
/**
* 单点登录
*/
async function ssoLogin() {
if (openSso == 'true') {
let token = getToken();
let ticket = getUrlParam('ticket');
if (!token) {
if (ticket) {
await validateCasLogin({
ticket: ticket,
service: locationUrl,
}).then((res) => {
const userStore = useUserStore();
userStore.setToken(res.token);
return userStore.afterLoginAction(true, {});
});
} else {
window.location.href = globSetting.casBaseUrl + '/login?service=' + encodeURIComponent(locationUrl);
}
}
}
}
/**
* 退出登录
*/
async function ssoLoginOut() {
window.location.href = globSetting.casBaseUrl + '/logout?service=' + encodeURIComponent(locationUrl);
}
return { ssoLogin, ssoLoginOut };
}

View File

@ -0,0 +1,126 @@
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.path === route.path)!;
}
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:
// update-begin--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
await tabStore.closeLeftTabs(tab || currentTab, router);
// update-end--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
case TableActionEnum.CLOSE_RIGHT:
// update-begin--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
await tabStore.closeRightTabs(tab || currentTab, router);
// update-end--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
case TableActionEnum.CLOSE_OTHER:
// update-begin--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
await tabStore.closeOtherTabs(tab || currentTab, router);
// update-end--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
break;
case TableActionEnum.CLOSE_CURRENT:
case TableActionEnum.CLOSE:
await tabStore.closeTab(tab || currentTab, router);
break;
}
}
/**
* 关闭相同的路由
* @param path
*/
function closeSameRoute(path) {
if(path.indexOf('?')>0){
path = path.split('?')[0];
}
let tab = tabStore.getTabList.find((item) => item.path.indexOf(path)>=0)!;
if(tab){
tabStore.closeTab(tab, router);
}
}
return {
refreshPage: () => handleTabAction(TableActionEnum.REFRESH),
// update-begin--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
closeAll: (tab) => handleTabAction(TableActionEnum.CLOSE_ALL, tab),
closeLeft: (tab) => handleTabAction(TableActionEnum.CLOSE_LEFT, tab),
closeRight: (tab) => handleTabAction(TableActionEnum.CLOSE_RIGHT, tab),
closeOther: (tab) => handleTabAction(TableActionEnum.CLOSE_OTHER, tab),
// update-end--author:liaozhiyang---date:20240605---for【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
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),
closeSameRoute
};
}

View File

@ -0,0 +1,72 @@
import type {Menu} from "@/router/types";
import { ref, watch, unref } 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 { REDIRECT_NAME } from '/@/router/constant';
import { getMenus } from '/@/router/menus';
/**
* Listening to page changes and dynamically changing site titles
*/
export function useTitle() {
const { title } = useGlobSetting();
const { t } = useI18n();
const { currentRoute } = useRouter();
const localeStore = useLocaleStore();
const pageTitle = usePageTitle();
const menus = ref<Menu[] | null>(null)
watch(
[() => currentRoute.value.path, () => localeStore.getLocale],
async () => {
const route = unref(currentRoute);
if (route.name === REDIRECT_NAME) {
return;
}
// update-begin--author:liaozhiyang---date:20231110---for【QQYUN-6938】online菜单名字和页面title不一致
if (route.params && Object.keys(route.params).length) {
if (!menus.value) {
menus.value = await getMenus();
}
const getTitle = getMatchingRouterName(menus.value, route.fullPath);
let tTitle = '';
if (getTitle) {
tTitle = t(getTitle);
} else {
tTitle = t(route?.meta?.title as string);
}
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
} else {
const tTitle = t(route?.meta?.title as string);
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
}
// update-end--author:liaozhiyang---date:20231110---for【QQYUN-6938】online菜单名字和页面title不一致
},
{ immediate: true }
);
}
/**
2023-11-09
liaozhiyang
获取路由匹配模式的真实页面名字
*/
function getMatchingRouterName(menus, path) {
for (let i = 0, len = menus.length; i < len; i++) {
const item = menus[i];
if (item.path === path && !item.redirect && !item.paramPath) {
return item.meta?.title;
} else if (item.children?.length) {
const result = getMatchingRouterName(item.children, path);
if (result) {
return result;
}
}
}
return '';
}

View File

@ -0,0 +1,98 @@
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,106 @@
// noinspection JSUnusedGlobalSymbols
import { unref } from 'vue';
import { useWebSocket, WebSocketResult } from '@vueuse/core';
import { getToken } from '/@/utils/auth';
let result: WebSocketResult<any>;
const listeners = new Map();
/**
* 开启 WebSocket 链接,全局只需执行一次
* @param url
*/
export function connectWebSocket(url: string) {
//update-begin-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
let token = (getToken() || '') as string;
result = useWebSocket(url, {
// 自动重连 (遇到错误最多重复连接10次)
autoReconnect: {
retries : 10,
delay : 5000
},
// 心跳检测
heartbeat: {
message: "ping",
interval: 55000
},
protocols: [token],
});
//update-end-author:taoyan date:2022-4-24 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
if (result) {
result.open = onOpen;
result.close = onClose;
const ws = unref(result.ws);
if(ws!=null){
ws.onerror = onError;
ws.onmessage = onMessage;
//update-begin---author:wangshuai---date:2024-04-30---for:【issues/1217】发送测试消息后铃铛数字没有变化---
ws.onopen = onOpen;
ws.onclose = onClose;
//update-end---author:wangshuai---date:2024-04-30---for:【issues/1217】发送测试消息后铃铛数字没有变化---
}
}
}
function onOpen() {
console.log('[WebSocket] 连接成功');
}
function onClose(e) {
console.log('[WebSocket] 连接断开:', e);
}
function onError(e) {
console.log('[WebSocket] 连接发生错误: ', e);
}
function onMessage(e) {
console.debug('[WebSocket] -----接收消息-------', e.data);
try {
//update-begin---author:wangshuai---date:2024-05-07---for:【issues/1161】前端websocket因心跳导致监听不起作用---
if(e==='ping'){
return;
}
//update-end---author:wangshuai---date:2024-05-07---for:【issues/1161】前端websocket因心跳导致监听不起作用---
const data = JSON.parse(e.data);
for (const callback of listeners.keys()) {
try {
callback(data);
} catch (err) {
console.error(err);
}
}
} catch (err) {
console.error('[WebSocket] data解析失败', err);
}
}
/**
* 添加 WebSocket 消息监听
* @param callback
*/
export function onWebSocket(callback: (data: object) => any) {
if (!listeners.has(callback)) {
if (typeof callback === 'function') {
listeners.set(callback, null);
} else {
console.debug('[WebSocket] 添加 WebSocket 消息监听失败:传入的参数不是一个方法');
}
}
}
/**
* 解除 WebSocket 消息监听
*
* @param callback
*/
export function offWebSocket(callback: (data: object) => any) {
listeners.delete(callback);
}
export function useMyWebSocket() {
return result;
}