first commit
This commit is contained in:
@@ -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)),
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 {};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { ModalFunc, ModalFuncProps } from 'ant-design-vue/lib/modal/Modal';
|
||||
|
||||
import { Modal, message as Message, notification } from 'ant-design-vue';
|
||||
import { InfoCircleFilled, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons-vue';
|
||||
|
||||
import { NotificationArgsProps, ConfigProps } from 'ant-design-vue/lib/notification';
|
||||
import { useI18n } from './useI18n';
|
||||
import { isString } from '/@/utils/is';
|
||||
|
||||
export interface NotifyApi {
|
||||
info(config: NotificationArgsProps): void;
|
||||
success(config: NotificationArgsProps): void;
|
||||
error(config: NotificationArgsProps): void;
|
||||
warn(config: NotificationArgsProps): void;
|
||||
warning(config: NotificationArgsProps): void;
|
||||
open(args: NotificationArgsProps): void;
|
||||
close(key: String): void;
|
||||
config(options: ConfigProps): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
export declare type IconType = 'success' | 'info' | 'error' | 'warning';
|
||||
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> {
|
||||
iconType: 'warning' | 'success' | 'error' | 'info';
|
||||
}
|
||||
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'>;
|
||||
|
||||
interface ConfirmOptions {
|
||||
info: ModalFunc;
|
||||
success: ModalFunc;
|
||||
error: ModalFunc;
|
||||
warn: ModalFunc;
|
||||
warning: ModalFunc;
|
||||
}
|
||||
|
||||
function getIcon(iconType: string) {
|
||||
if (iconType === 'warning') {
|
||||
return <InfoCircleFilled class="modal-icon-warning" />;
|
||||
} else if (iconType === 'success') {
|
||||
return <CheckCircleFilled class="modal-icon-success" />;
|
||||
} else if (iconType === 'info') {
|
||||
return <InfoCircleFilled class="modal-icon-info" />;
|
||||
} else {
|
||||
return <CloseCircleFilled class="modal-icon-error" />;
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) {
|
||||
if (isString(content)) {
|
||||
return <div innerHTML={`<div>${content as string}</div>`}></div>;
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Create confirmation box
|
||||
*/
|
||||
function createConfirm(options: ModalOptionsEx): 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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 {
|
||||
// update-begin--author:liaozhiyang---date:20240705---for:【TV360X-1604】按钮禁用权限在接口中查不到也禁用
|
||||
return false;
|
||||
// update-end--author:liaozhiyang---date:20240705---for:【TV360X-1604】按钮禁用权限在接口中查不到也禁用
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-6-17 for: VUEN-1342【流程】编码方式 节点权限配置好后,未生效
|
||||
|
||||
return { changeRole, hasPermission, togglePermissionMode, refreshMenu, isDisabledAuth, initBpmFormData };
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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 '';
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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
|
||||
const token = (getToken() || '') as string;
|
||||
result = useWebSocket(url, {
|
||||
// 自动重连 (遇到错误最多重复连接10次)
|
||||
autoReconnect: {
|
||||
retries: 10,
|
||||
// TODO 服务器压力暂改成50秒
|
||||
delay: 50000,
|
||||
},
|
||||
// 心跳检测
|
||||
heartbeat: {
|
||||
message: 'ping',
|
||||
// TODO 服务器压力暂改成50秒
|
||||
interval: 50000,
|
||||
},
|
||||
protocols: [token],
|
||||
// update-begin--author:liaozhiyang---date:20240726---for:[issues/6662] 演示系统socket总断,换一个写法
|
||||
onConnected: function (ws) {
|
||||
console.log('[WebSocket] 连接成功', ws);
|
||||
},
|
||||
onDisconnected: function (ws, event) {
|
||||
console.log('[WebSocket] 连接断开:', ws, event);
|
||||
},
|
||||
onError: function (ws, event) {
|
||||
console.log('[WebSocket] 连接发生错误: ', ws, event);
|
||||
},
|
||||
onMessage: function (_ws, e) {
|
||||
console.debug('[WebSocket] -----接收消息-------', e.data);
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2024-05-07---for:【issues/1161】前端websocket因心跳导致监听不起作用---
|
||||
if (e.data === '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);
|
||||
}
|
||||
},
|
||||
// update-end--author:liaozhiyang---date:20240726---for:[issues/6662] 演示系统socket总断,换一个写法
|
||||
});
|
||||
// update-begin--author:liaozhiyang---date:20240726---for:[issues/6662] 演示系统socket总断,换一个写法
|
||||
//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】发送测试消息后,铃铛数字没有变化---
|
||||
// }
|
||||
// }
|
||||
// update-end--author:liaozhiyang---date:20240726---for:[issues/6662] 演示系统socket总断,换一个写法
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user