新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,190 @@
import { ComputedRef, isRef, nextTick, Ref, ref, unref, watch } from 'vue';
import { onMountedOrActivated } from '@jeesite/core/hooks/core/onMountedOrActivated';
import { useWindowSizeFn } from '@jeesite/core/hooks/event/useWindowSizeFn';
import { useLayoutHeight } from '@jeesite/core/layouts/default/content/useContentViewHeight';
import { getViewportOffset } from '@jeesite/core/utils/domUtils';
import { isNumber, isString } from '@jeesite/core/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();
}
const fixHeight = -1;
contentHeight.value = height + fixHeight;
}
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 '@jeesite/core/components/ContextMenu';
import type { ContextMenuItem } from '@jeesite/core/components/ContextMenu';
export type { ContextMenuItem };
export function useContextMenu(authRemove = true) {
if (getCurrentInstance() && authRemove) {
onUnmounted(() => {
destroyContextMenu();
});
}
return [createContextMenu, destroyContextMenu];
}

View File

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

View File

@@ -0,0 +1,26 @@
import { useAppProviderContext } from '@jeesite/core/components/Application';
import { theme } from 'ant-design-vue';
// import { computed } from 'vue';
// import { lowerFirst } from 'lodash-es';
export function useDesign(scope: string) {
const values = useAppProviderContext();
const token = theme.useToken();
// 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,
hashId: token.hashId.value,
// style,
};
}

View File

@@ -0,0 +1,117 @@
import type { EChartsOption } from 'echarts';
import { onActivated, Ref } from 'vue';
import { useTimeoutFn } from '@jeesite/core/hooks/core/useTimeout';
import { tryOnUnmounted } from '@vueuse/core';
import { unref, nextTick, watch, computed, ref } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { useEventListener } from '@jeesite/core/hooks/event/useEventListener';
import { useBreakpoint } from '@jeesite/core/hooks/event/useBreakpoint';
import echarts from '@jeesite/core/utils/lib/echarts';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
export function useECharts(elRef: Ref<HTMLDivElement>, theme: 'light' | 'dark' | 'default' = 'default') {
const { getDarkMode: getSysDarkMode } = useRootSetting();
const getDarkMode = computed(() => {
return theme === 'default' ? getSysDarkMode.value : theme;
});
let chartInstance: echarts.ECharts | null = null;
let resizeFn: Fn = resize;
const cacheOptions = ref({}) as Ref<EChartsOption>;
let removeResizeFn: Fn = () => {};
resizeFn = useDebounceFn(resize, 200);
const getOptions = computed(() => {
if (getDarkMode.value !== 'dark') {
return cacheOptions.value as EChartsOption;
}
return {
backgroundColor: 'transparent',
...cacheOptions.value,
} as EChartsOption;
});
function initCharts(t = theme) {
const el = unref(elRef);
if (!el || !unref(el)) {
return;
}
chartInstance = echarts.init(el, t);
const { removeEvent } = useEventListener({
el: window,
name: 'resize',
listener: resizeFn,
});
removeResizeFn = removeEvent;
const { widthRef, screenEnum } = useBreakpoint();
if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
useTimeoutFn(() => {
resizeFn();
}, 30);
}
}
function setOptions(options: EChartsOption, clear = true) {
cacheOptions.value = options;
if (unref(elRef)?.offsetHeight === 0) {
useTimeoutFn(() => {
setOptions(unref(getOptions));
}, 30);
return;
}
nextTick(() => {
useTimeoutFn(() => {
if (!chartInstance) {
initCharts(getDarkMode.value as 'default');
if (!chartInstance) return;
}
clear && chartInstance?.clear();
chartInstance?.setOption(unref(getOptions));
}, 30);
});
}
function resize() {
chartInstance?.resize();
}
onActivated(() => {
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 '@jeesite/core/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,57 @@
import { i18n } from '@jeesite/core/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 `${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 tt = t as (arg0: string, ...arg: I18nTranslationRestParameters) => string;
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) return '';
if (!key.includes('.') && !namespace) return key;
return tt(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,77 @@
import { computed, onUnmounted, unref, watchEffect } from 'vue';
import { useThrottleFn } from '@vueuse/core';
import { useAppStore } from '@jeesite/core/store/modules/app';
import { useLockStore } from '@jeesite/core/store/modules/lock';
import { useUserStore } from '@jeesite/core/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) {
if (userStore.getSessionTimeout) {
clear();
return;
}
const lockTime = appStore.getProjectConfig.lockTime;
if (!lockTime || lockTime < 1 || lockTime > 99999) {
clear();
return;
}
clear();
timeId = setTimeout(
() => {
lockPage();
},
lockTime * 60 * 1000,
);
}
function lockPage(): void {
lockStore.setLockInfo({
isLock: true,
pwd: undefined,
});
}
watchEffect((onClean) => {
// if (userStore.getToken) {
if (!userStore.getSessionTimeout) {
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,197 @@
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 '@jeesite/core/utils/is';
import { Icon } from '@jeesite/core/components/Icon';
import type { ConfigOnClose, MessageType } from 'ant-design-vue/lib/message';
import type { JointContent } from 'ant-design-vue/es/message/interface';
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';
icon?: any;
title?: any;
content?: any;
}
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) {
const iconType = options.iconType || 'info';
Reflect.deleteProperty(options, 'iconType');
const opt: ModalFuncProps = {
maskClosable: true,
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 | any {
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'));
}
function contains(str, searchs) {
if (typeof str === 'object' && str.content) {
str = str.content;
}
if (typeof str === 'object' && str.props?.innerHTML) {
str = str.props?.innerHTML;
}
if (typeof str === 'string' && searchs) {
const ss = searchs.split(',');
for (let i = 0; i < ss.length; i++) {
if (str.indexOf(ss[i]) >= 0) {
return true;
}
}
}
return false;
}
function showMessageModal(options: ModalOptionsPartial | string, type?: string) {
const { t } = useI18n();
if (typeof options === 'string') options = { content: options };
if (typeof options.content === 'string' && options.content.startsWith('posfull:')) {
options.content = '<div class="modal-posfull-content">' + options.content.substring(8) + '</div>';
options.width = '80%';
}
if (type === 'error' || contains(options.content, t('sys.message.error'))) {
return Modal.error(createModalOptions(options, 'close'));
} else if (type === 'warning' || contains(options.content, t('sys.message.warning'))) {
return Modal.warning(createModalOptions(options, 'warning'));
} else if (type === 'success' || contains(options.content, t('sys.message.success'))) {
return Modal.success(createModalOptions(options, 'success'));
} else {
return Modal.info(createModalOptions(options, 'info'));
}
}
function showMessage(content: JointContent, type?: string, duration?: number, onClose?: ConfigOnClose) {
const { t } = useI18n();
let messageRemove = () => {};
if (typeof content === 'string' && content.startsWith('posfull:')) {
content = {
content: (
<div style="position: relative;" onMouseout={() => setTimeout(messageRemove, 1000)}>
<div style="position: absolute; right: -6px; top: -20px;" onClick={() => messageRemove()}>
<Icon icon="i-ant-design:close-outlined" color="#555" class="cursor-pointer" />
</div>
<div class="text-left" innerHTML={content.substring(8)}></div>
</div>
),
duration,
};
}
if (type === 'error' || contains(content, t('sys.message.error'))) {
messageRemove = Message.error(content, duration, onClose);
} else if (type === 'warning' || contains(content, t('sys.message.warning'))) {
messageRemove = Message.warning(content, duration, onClose);
} else if (type === 'success' || contains(content, t('sys.message.success'))) {
messageRemove = Message.success(content, duration, onClose);
} else {
messageRemove = Message.info(content, duration, onClose);
}
return messageRemove;
}
notification.config({
placement: 'topRight',
duration: 3,
});
/**
* @description: message
*/
export function useMessage() {
return {
createMessage: Message,
notification: notification as NotifyApi,
createConfirm: createConfirm,
createSuccessModal,
createErrorModal,
createInfoModal,
createWarningModal,
showMessageModal,
showMessage,
};
}

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author Vben、ThinkGem
*/
import type { RouteRecordRaw } from 'vue-router';
import { useAppStore } from '@jeesite/core/store/modules/app';
import { usePermissionStore } from '@jeesite/core/store/modules/permission';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { useTabs } from './useTabs';
import { router, resetRouter } from '@jeesite/core/router';
// import { RootRoute } from '@jeesite/core/router/routes';
import projectSetting from '@jeesite/core/settings/projectSetting';
import { PermissionModeEnum } from '@jeesite/core/enums/appEnum';
import { RoleEnum } from '@jeesite/core/enums/roleEnum';
import { intersection } from 'lodash-es';
import { isArray } from '@jeesite/core/utils/is';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
// User permissions related operations
export function usePermission() {
const userStore = useUserStore();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const { closeAll } = useTabs(router);
/**
* Change permission mode
*/
async function togglePermissionMode() {
appStore.setProjectConfig({
permissionMode:
projectSetting.permissionMode === PermissionModeEnum.BACK
? PermissionModeEnum.ROUTE_MAPPING
: PermissionModeEnum.BACK,
});
location.reload();
}
/**
* Reset and regain authority resource information
* @param id
*/
async function resume() {
const tabStore = useMultipleTabStore();
tabStore.clearCacheTabs();
resetRouter();
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
permissionStore.setLastBuildMenuTime();
closeAll();
}
/**
* Determine whether there is permission
*/
function hasPermission(value?: RoleEnum | RoleEnum[] | string | string[], def = true): boolean {
// Open 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 permiCodeList = permissionStore.getPermCodeList;
// if (!isArray(value)) {
// return permiCodeList.includes(value);
// }
// return (intersection(value, permiCodeList) as string[]).length > 0;
if (value) {
const values = !isArray(value) ? [value] : value;
for (const val of values) {
if (val && val !== '') {
const currPermi = val.split(':');
for (const permi of permiCodeList) {
if (isPermitted(permi, currPermi)) {
return true;
}
}
}
}
}
return false;
}
return true;
}
function isPermitted(permi: string[], currPermi: string[]) {
for (const i in permi) {
if (permi[i] !== currPermi[i]) {
return false;
}
}
return true;
}
/**
* Change roles
* @param roles
*/
async function changeRole(roles: RoleEnum | RoleEnum[]): Promise<void> {
if (projectSetting.permissionMode !== PermissionModeEnum.ROUTE_MAPPING) {
throw new Error('Please switch PermissionModeEnum to ROUTE_MAPPING mode in the configuration to operate!');
}
if (!isArray(roles)) {
roles = [roles];
}
userStore.setRoleList(roles);
await resume();
}
/**
* refresh menu data
*/
async function refreshMenu() {
resume();
}
return { changeRole, hasPermission, togglePermissionMode, refreshMenu };
}

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,106 @@
import type { RouteLocationNormalized, Router } from 'vue-router';
import { useRouter } from 'vue-router';
import { unref } from 'vue';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
import { useAppStore } from '@jeesite/core/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) => {
return (item.fullPath || item.path) === route.fullPath;
})!;
}
async function updateTabTitle(title: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const targetTab = tab || getCurrentTab();
await tabStore.setTabTitle(title, targetTab);
}
async function updateTabPath(path: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const targetTab = tab || getCurrentTab();
await tabStore.updateTabPath(path, targetTab);
}
async function handleTabAction(action: TableActionEnum, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs;
if (!canIUse) {
return;
}
const currentTab = getCurrentTab();
switch (action) {
case TableActionEnum.REFRESH:
await tabStore.refreshPage(router);
break;
case TableActionEnum.CLOSE_ALL:
await tabStore.closeAllTab(router);
break;
case TableActionEnum.CLOSE_LEFT:
await tabStore.closeLeftTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_RIGHT:
await tabStore.closeRightTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_OTHER:
await tabStore.closeOtherTabs(currentTab, router);
break;
case TableActionEnum.CLOSE_CURRENT:
case TableActionEnum.CLOSE:
await tabStore.closeTab(tab || currentTab, router);
break;
}
}
return {
tabStore,
refreshPage: () => handleTabAction(TableActionEnum.REFRESH),
closeAll: () => handleTabAction(TableActionEnum.CLOSE_ALL),
closeLeft: () => handleTabAction(TableActionEnum.CLOSE_LEFT),
closeRight: () => handleTabAction(TableActionEnum.CLOSE_RIGHT),
closeOther: () => handleTabAction(TableActionEnum.CLOSE_OTHER),
closeCurrent: () => handleTabAction(TableActionEnum.CLOSE_CURRENT),
close: (tab?: RouteLocationNormalized) => handleTabAction(TableActionEnum.CLOSE, tab),
setTitle: (title: string, tab?: RouteLocationNormalized) => updateTabTitle(title, tab),
updatePath: (fullPath: string, tab?: RouteLocationNormalized) => updateTabPath(fullPath, tab),
};
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author ThinkGem
*/
import { watch, unref } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useTitle as usePageTitle } from '@vueuse/core';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { useRouter } from 'vue-router';
import { REDIRECT_NAME } from '@jeesite/core/router/constant';
/**
* Listening to page changes and dynamically changing site titles
*/
export function useTitle() {
const { title } = useGlobSetting();
const { t } = useI18n();
const { currentRoute } = useRouter();
const pageTitle = usePageTitle();
watch(
() => currentRoute.value.path,
() => {
const route = unref(currentRoute);
if (route.name === REDIRECT_NAME) {
return;
}
const tTitle = t(route?.meta?.title as string);
pageTitle.value = tTitle ? ` ${tTitle} - ${title} ` : `${title}`;
},
{ immediate: true },
);
}

View File

@@ -0,0 +1,205 @@
import { getCurrentInstance, onBeforeUnmount, ref, Ref, shallowRef, unref } from 'vue';
import { useRafThrottle } from '@jeesite/core/utils/domUtils';
import { addResizeListener, removeResizeListener } from '@jeesite/core/utils/event';
import { isDef } from '@jeesite/core/utils/is';
const watermarkSymbol = 'watermark-dom';
const updateWatermarkText = ref<string | null>(null);
type UseWatermarkRes = {
setWatermark: (str: string) => void;
clear: () => void;
clearAll: () => void;
waterMarkOptions?: waterMarkOptionsType;
obInstance?: MutationObserver;
targetElement?: HTMLElement;
parentElement?: HTMLElement;
};
type waterMarkOptionsType = {
// 自定义水印的文字大小
fontSize?: number;
// 自定义水印的文字颜色
fontColor?: string;
// 自定义水印的文字字体
fontFamily?: string;
// 自定义水印的文字对齐方式
textAlign?: CanvasTextAlign;
// 自定义水印的文字基线
textBaseline?: CanvasTextBaseline;
// 自定义水印的文字倾斜角度
rotate?: number;
};
const sourceMap = new Map<symbol, Omit<UseWatermarkRes, 'clearAll'>>();
function findTargetNode(el) {
return Array.from(sourceMap.values()).find((item) => item.targetElement === el);
}
function createBase64(str: string, waterMarkOptions: waterMarkOptionsType) {
const can = document.createElement('canvas');
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext('2d');
if (cans) {
const fontFamily = waterMarkOptions?.fontFamily || 'Vedana';
const fontSize = waterMarkOptions?.fontSize || 15;
const fontColor = waterMarkOptions?.fontColor || 'rgba(0, 0, 0, 0.15)';
const textAlign = waterMarkOptions?.textAlign || 'left';
const textBaseline = waterMarkOptions?.textBaseline || 'middle';
const rotate = waterMarkOptions?.rotate || 20;
cans.rotate((-rotate * Math.PI) / 180);
cans.font = `${fontSize}px ${fontFamily}`;
cans.fillStyle = fontColor;
cans.textAlign = textAlign;
cans.textBaseline = textBaseline;
cans.fillText(str, width / 20, height);
}
return can.toDataURL('image/png');
}
const resetWatermarkStyle = (element: HTMLElement, watermarkText: string, waterMarkOptions: waterMarkOptionsType) => {
element.className = '__' + watermarkSymbol;
element.style.pointerEvents = 'none';
element.style.display = 'block';
element.style.visibility = 'visible';
element.style.top = '0px';
element.style.left = '0px';
element.style.position = 'absolute';
element.style.zIndex = '100000';
element.style.height = '100%';
element.style.width = '100%';
element.style.background = `url(${createBase64(
unref(updateWatermarkText) || watermarkText,
waterMarkOptions,
)}) left top repeat`;
};
const obFn = () => {
const obInstance = new MutationObserver((mutationRecords) => {
for (const mutation of mutationRecords) {
for (const node of Array.from(mutation.removedNodes)) {
const target = findTargetNode(node);
if (!target) return;
const { targetElement, parentElement } = target;
// 父元素的子元素水印如果被删除 重新插入被删除的水印(防篡改,插入通过控制台删除的水印)
if (!parentElement?.contains(targetElement as Node | null)) {
target?.parentElement?.appendChild(node as HTMLElement);
}
}
if (mutation.type === 'attributes' && mutation.target) {
// 修复控制台可以”Hide element” 的问题
const _target = mutation.target as HTMLElement;
const target = findTargetNode(_target);
if (target) {
// 禁止改属性 包括class 修改以后 mutation.type 也等于 'attributes'
// 先解除监听 再加一下
clearAll();
target.setWatermark(target.targetElement?.['data-watermark-text']);
}
}
}
});
return obInstance;
};
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
waterMarkOptions: waterMarkOptionsType = {},
): UseWatermarkRes {
const domSymbol = Symbol(watermarkSymbol);
const appendElRaw = unref(appendEl);
if (appendElRaw && sourceMap.has(domSymbol)) {
const { setWatermark, clear } = sourceMap.get(domSymbol) as UseWatermarkRes;
return { setWatermark, clear, clearAll };
}
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
sourceMap.has(domSymbol) && sourceMap.get(domSymbol)?.obInstance?.disconnect();
sourceMap.delete(domSymbol);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
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, waterMarkOptions)}) left top repeat`;
}
}
const createWatermark = (str: string) => {
if (unref(watermarkEl) && sourceMap.has(domSymbol)) {
updateWatermarkText.value = str;
updateWatermark({ str });
return;
}
const div = document.createElement('div');
div['data-watermark-text'] = str; //自定义属性 用于恢复水印
updateWatermarkText.value = str;
watermarkEl.value = div;
resetWatermarkStyle(div, str, waterMarkOptions);
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height });
el.appendChild(div);
sourceMap.set(domSymbol, {
setWatermark,
clear,
parentElement: el,
targetElement: div,
obInstance: obFn(),
waterMarkOptions,
});
sourceMap.get(domSymbol)?.obInstance?.observe(el, {
childList: true, // 子节点的变动(指新增,删除或者更改)
subtree: true, // 该观察器应用于该节点的所有后代节点
attributes: true, // 属性的变动
});
};
function setWatermark(str: string) {
createWatermark(str);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear, clearAll };
}
function clearAll() {
Array.from(sourceMap.values()).forEach((item) => {
item?.obInstance?.disconnect();
item.clear();
});
}