项目初始化

This commit is contained in:
2026-03-19 10:57:24 +08:00
commit ee94d420ad
3822 changed files with 582614 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
<template>
<Drawer v-bind="getBindValues" :closable="false" @close="onClose">
<template #title v-if="!$slots.title">
<DrawerHeader :title="getMergeProps.title" :isDetail="isDetail" :showDetailBack="showDetailBack" @close="onClose">
<template #titleToolbar>
<slot name="titleToolbar"></slot>
</template>
</DrawerHeader>
</template>
<template v-else #title>
<slot name="title"></slot>
</template>
<template #extra>
<Tooltip :title="t('component.drawer.cancelText')" placement="bottom">
<Icon icon="i-ant-design:close-outlined" class="anticon-close cursor-pointer" @click="onClose" />
</Tooltip>
</template>
<div v-if="widthResize" class="ew-resize" @mousedown="onMousedown"></div>
<ScrollContainer
:style="getScrollContentStyle"
v-loading="getLoading"
:loading-tip="loadingText || t('common.loadingText')"
>
<slot></slot>
</ScrollContainer>
<DrawerFooter v-bind="getProps" @close="onClose" @ok="handleOk" :height="getFooterHeight">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</DrawerFooter>
</Drawer>
</template>
<script lang="ts" setup name="BasicDrawer">
import type { DrawerInstance, DrawerProps } from './typing';
import { ref, computed, watch, unref, toRaw, getCurrentInstance, CSSProperties, watchEffect } from 'vue';
import { Drawer } from 'ant-design-vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { isFunction, isNumber } from '@jeesite/core/utils/is';
import { deepMerge } from '@jeesite/core/utils';
import { Tooltip } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import DrawerFooter from './components/DrawerFooter.vue';
import DrawerHeader from './components/DrawerHeader.vue';
import { ScrollContainer } from '@jeesite/core/components/Container';
import { basicProps } from './props';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useAttrs } from '@jeesite/core/hooks/core/useAttrs';
import { useBreakpoint } from '@jeesite/core/hooks/event/useBreakpoint';
defineOptions({
inheritAttrs: false,
});
const props = defineProps(basicProps);
const emit = defineEmits(['open-change', 'ok', 'close', 'register', 'update:open']);
const attrs = useAttrs();
const openRef = ref(false);
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
const { t } = useI18n();
const { prefixVar, prefixCls } = useDesign('basic-drawer');
const { realWidthRef, screenEnum } = useBreakpoint();
const drawerInstance: DrawerInstance = {
getDrawerProps,
setDrawerProps,
emitOpen: undefined,
};
const instance = getCurrentInstance();
if (instance) {
emit('register', drawerInstance, instance.uid);
}
const getMergeProps = computed((): DrawerProps => {
return deepMerge(toRaw(props), unref(propsRef));
});
const getWrapClassName = computed(() => {
return `${prefixCls} ${props.wrapClassName || ''}`;
});
const getProps = computed((): DrawerProps => {
const opt = {
placement: 'right',
...unref(attrs),
...deepMerge(toRaw(props), unref(propsRef)),
open: unref(openRef),
};
opt.title = undefined;
const { isDetail, width, class: wrapClassName, getContainer } = opt;
if (isDetail) {
if (!width) {
opt.width = '100%';
}
const detailCls = `${prefixCls}__detail`;
opt.class = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
if (!getContainer) {
// TODO type error?
opt.getContainer = `.${prefixVar}-layout-content` as any;
}
} else {
opt.class = unref(getWrapClassName);
}
// 小屏幕直接全屏抽屉
if (unref(realWidthRef) < screenEnum.SM) {
opt.width = '100%';
}
return opt as DrawerProps;
});
const getBindValues = computed((): DrawerProps => {
const values = {
...attrs,
...unref(getProps),
open: unref(openRef),
} as any;
if (typeof values?.width === 'string') {
let width = Number(values.width);
if (!isNaN(width)) values.width = width;
}
delete values['wrapClassName'];
return values;
});
// Custom implementation of the bottom button,
const getFooterHeight = computed(() => {
const { footerHeight, showFooter } = unref(getProps);
if (showFooter && footerHeight) {
return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`;
}
return `0px`;
});
const getScrollContentStyle = computed((): CSSProperties => {
const footerHeight = unref(getFooterHeight);
return {
position: 'relative',
height: `calc(100% - ${footerHeight})`,
};
});
const getLoading = computed(() => {
return !!unref(getProps)?.loading;
});
watchEffect(() => {
openRef.value = !!props.open;
});
watch(
() => unref(openRef),
(v) => {
emit('open-change', v);
emit('update:open', v);
instance && drawerInstance.emitOpen?.(v, instance.uid);
},
{
immediate: false,
},
);
// Cancel event
async function onClose(e: Recordable) {
const { closeFunc } = unref(getProps);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
openRef.value = !res;
return;
}
openRef.value = false;
emit('close', e);
}
function handleOk(e: Event) {
emit('ok', e);
}
const onMousedown = function (e) {
const wrapper = e.target.closest('.ant-drawer-content-wrapper') as HTMLElement;
if (!wrapper) return;
wrapper.style.transition = 'none';
const w = wrapper.clientWidth;
const x = e.clientX;
const l = e.target.offsetLeft;
let isDown = true;
window.onmousemove = (e) => {
if (!isDown) {
return;
}
const nl = e.clientX - (x - l);
wrapper.style.width = w - nl + 'px';
};
window.onmouseup = () => {
window.onmousemove = null;
};
};
function getDrawerProps(): Partial<DrawerProps> {
return getProps.value;
}
function setDrawerProps(props: Partial<DrawerProps>): void {
if (typeof props.loading != 'undefined') {
props.confirmLoading = props.loading;
}
// Keep the last setDrawerProps
// propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
propsRef.value = { ...(unref(propsRef) as Recordable), ...props } as Recordable;
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
}
defineExpose({
open: (loading = false) => {
setDrawerProps({ open: true, loading });
},
close: () => {
setDrawerProps({ open: false });
},
loading: () => {
setDrawerProps({ loading: true });
},
closeLoading: () => {
setDrawerProps({ loading: false });
},
confirmLoading: () => {
setDrawerProps({ confirmLoading: true });
},
closeConfirmLoading: () => {
setDrawerProps({ confirmLoading: false });
},
getProps: getDrawerProps,
setProps: setDrawerProps,
});
</script>
<style lang="less">
@header-height: 60px;
@detail-header-height: 40px;
@prefix-cls: ~'jeesite-basic-drawer';
@prefix-cls-detail: ~'jeesite-basic-drawer__detail';
.ant-drawer .@{prefix-cls} {
overflow: hidden;
.ew-resize {
position: absolute;
left: 0;
height: 90%;
width: 3px;
z-index: 1000;
user-select: none;
&:hover {
cursor: ew-resize;
background: @border-color-light;
}
}
.ant-drawer {
&-body {
height: calc(100% - @header-height);
padding: 0;
background-color: @component-background;
> .scrollbar {
> .scrollbar__wrap {
> .scrollbar__view {
margin: 16px 16px 5px;
> .ant-form,
> .ant-tabs {
margin-right: 25px;
&:first-child {
margin-top: 20px;
}
.jeesite-form-group {
.title {
margin-right: -12px;
}
}
}
}
}
> .is-horizontal {
display: none;
}
}
}
&-title {
font-weight: normal;
.anticon {
color: @primary-color;
}
}
&-extra {
.anticon-close {
opacity: 0.6;
color: @text-color-base;
&:hover {
color: @error-color;
opacity: 1;
}
}
}
}
}
.@{prefix-cls-detail} {
position: absolute;
overflow: hidden;
.ant-drawer {
&-body {
height: calc(100% - @detail-header-height);
padding: 0 !important;
> .scrollbar {
> .scrollbar__wrap {
> .scrollbar__view {
margin: 10px;
}
}
.is-horizontal {
display: none;
}
}
}
&-header {
width: 100%;
height: @detail-header-height;
padding: 0;
border-top: 1px solid @border-color-base;
box-sizing: border-box;
}
&-title {
height: 100%;
}
&-close {
height: @detail-header-height;
line-height: @detail-header-height;
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer">
<template v-if="!$slots.footer">
<slot name="insertFooter"></slot>
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-2" v-if="showCancelBtn">
<Icon icon="i-ant-design:close-outlined" />
{{ cancelText || (getOkAuth && showOkBtn ? t('common.cancelText') : t('common.closeText')) }}
</a-button>
<slot name="centerFooter"></slot>
<a-button
:type="okType"
@click="handleOk"
v-bind="okButtonProps"
class="mr-2"
:loading="confirmLoading"
v-if="showOkBtn && getOkAuth"
>
<Icon icon="i-ant-design:check-outlined" />
{{ okText || t('common.okText') }}
</a-button>
<slot name="appendFooter"></slot>
</template>
<template v-else>
<slot name="footer"></slot>
</template>
</div>
</template>
<script lang="ts">
import type { CSSProperties } from 'vue';
import { defineComponent, computed } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { usePermission } from '@jeesite/core/hooks/web/usePermission';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { Icon } from '@jeesite/core/components/Icon';
import { footerProps } from '../props';
export default defineComponent({
name: 'BasicDrawerFooter',
components: { Icon },
props: {
...footerProps,
height: {
type: String,
default: '60px',
},
},
emits: ['ok', 'close'],
setup(props, { emit }) {
const { t } = useI18n();
const { hasPermission } = usePermission();
const { prefixCls } = useDesign('basic-drawer-footer');
const getStyle = computed((): CSSProperties => {
const heightStr = `${props.height}`;
return {
height: heightStr,
lineHeight: heightStr,
};
});
const getOkAuth = computed(() => {
return hasPermission(props.okAuth);
});
function handleOk() {
emit('ok');
}
function handleClose() {
emit('close');
}
return { t, prefixCls, getStyle, getOkAuth, handleOk, handleClose };
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-basic-drawer-footer';
.@{prefix-cls} {
position: absolute;
bottom: 0;
width: 100%;
padding-right: 8px;
text-align: right;
background-color: @component-background;
// border-top: 1px solid @border-color-base;
z-index: 100; // 设置下,否则 BasicTable 空白图标会覆盖 actions 上边框线
> * {
margin-right: 8px;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<BasicTitle v-if="!isDetail" :class="prefixCls">
<slot name="title"></slot>
{{ !$slots.title ? title : '' }}
</BasicTitle>
<div :class="[prefixCls, `${prefixCls}--detail`]" v-else>
<span :class="`${prefixCls}__twrap`">
<span @click="handleClose" v-if="showDetailBack">
<Icon icon="i-ant-design:arrow-left-outlined" :class="`${prefixCls}__back`" />
</span>
<span v-if="title">{{ title }}</span>
</span>
<span :class="`${prefixCls}__toolbar`">
<slot name="titleToolbar"></slot>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicTitle } from '@jeesite/core/components/Basic';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/utils/propTypes';
export default defineComponent({
name: 'BasicDrawerHeader',
components: { Icon, BasicTitle },
props: {
isDetail: propTypes.bool,
showDetailBack: propTypes.bool,
title: propTypes.string,
},
emits: ['close'],
setup(_, { emit }) {
const { prefixCls } = useDesign('basic-drawer-header');
function handleClose() {
emit('close');
}
return { prefixCls, handleClose };
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-basic-drawer-header';
@footer-height: 60px;
.@{prefix-cls} {
display: flex;
height: 100%;
align-items: center;
&__back {
padding: 0 12px;
cursor: pointer;
&:hover {
color: @primary-color;
}
}
&__twrap {
flex: 1;
}
&__toolbar {
padding-right: 50px;
}
}
</style>

View File

@@ -0,0 +1,50 @@
import type { PropType } from 'vue';
export const footerProps = {
confirmLoading: { type: Boolean },
/**
* @description: Show close button
*/
showCancelBtn: { type: Boolean, default: true },
cancelButtonProps: Object as PropType<Recordable>,
cancelText: { type: String },
/**
* @description: Show confirmation button
*/
showOkBtn: { type: Boolean, default: true },
okButtonProps: Object as PropType<Recordable>,
okText: { type: String },
okType: { type: String, default: 'primary' },
okAuth: { type: String },
showFooter: { type: Boolean },
footerHeight: {
type: [String, Number] as PropType<string | number>,
default: 60,
},
};
export const basicProps = {
isDetail: { type: Boolean },
title: { type: String, default: '' },
loadingText: { type: String },
showDetailBack: { type: Boolean, default: true },
open: { type: Boolean },
loading: { type: Boolean },
maskClosable: { type: Boolean, default: true },
getContainer: {
type: [Object, String] as PropType<any>,
},
closeFunc: {
type: [Function, Object] as PropType<any>,
default: null,
},
destroyOnClose: { type: Boolean },
wrapClassName: { type: String },
// 是否允许拖拽调整抽屉宽度
widthResize: { type: Boolean, default: true },
...footerProps,
// eslint check
width: { type: [Number, String] },
mask: { type: Boolean, default: true },
maskStyle: { type: Object },
};

View File

@@ -0,0 +1,198 @@
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
import type { ScrollContainerOptions } from '@jeesite/core/components/Container';
export interface DrawerInstance {
getDrawerProps: () => Partial<DrawerProps>;
setDrawerProps: (props: Partial<DrawerProps>) => void;
emitOpen?: (open: boolean, uid: number) => void;
}
export interface ReturnMethods extends DrawerInstance {
openDrawer: <T = any>(open?: boolean, data?: T, openOnSet?: boolean) => void;
closeDrawer: () => void;
getOpen?: ComputedRef<boolean>;
setDrawerData: (data: any) => void;
}
export type RegisterFn = (drawerInstance: DrawerInstance, uuid: number) => void;
export interface ReturnInnerMethods extends DrawerInstance {
closeDrawer: () => void;
changeLoading: (loading: boolean) => void;
changeOkLoading: (loading: boolean) => void;
getOpen?: ComputedRef<boolean>;
}
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
export type UseDrawerInnerReturnType = [RegisterFn, ReturnInnerMethods];
export interface DrawerFooterProps {
showOkBtn: boolean;
showCancelBtn: boolean;
/**
* Text of the Cancel button
* @default 'cancel'
* @type string
*/
cancelText: string;
/**
* Text of the OK button
* @default 'OK'
* @type string
*/
okText: string;
/**
* Button type of the OK button
* @default 'primary'
* @type string
*/
okType: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
okAuth: string;
/**
* The ok button props, follow jsx rules
* @type object
*/
okButtonProps: { props: ButtonProps; on: any };
/**
* The cancel button props, follow jsx rules
* @type object
*/
cancelButtonProps: { props: ButtonProps; on: any };
/**
* Whether to apply loading visual effect for OK button or not
* @default false
* @type boolean
*/
confirmLoading: boolean;
showFooter: boolean;
footerHeight: string | number;
}
export interface DrawerProps extends DrawerFooterProps {
isDetail?: boolean;
loading?: boolean;
showDetailBack?: boolean;
open?: boolean;
/**
* Built-in ScrollContainer component configuration
* @type ScrollContainerOptions
*/
scrollOptions?: ScrollContainerOptions;
closeFunc?: () => Promise<any>;
triggerWindowResize?: boolean;
/**
* Whether a close (x) button is open on top right of the Drawer dialog or not.
* @default true
* @type boolean
*/
closable?: boolean;
/**
* Whether to unmount child components on closing drawer or not.
* @default false
* @type boolean
*/
destroyOnClose?: boolean;
/**
* Return the mounted node for Drawer.
* @default 'body'
* @type any ( HTMLElement| () => HTMLElement | string)
*/
getContainer?: string | false | HTMLElement | (() => HTMLElement);
/**
* Whether to show mask or not.
* @default true
* @type boolean
*/
mask?: boolean;
/**
* Clicking on the mask (area outside the Drawer) to close the Drawer or not.
* @default true
* @type boolean
*/
maskClosable?: boolean;
/**
* Style for Drawer's mask element.
* @default {}
* @type object
*/
maskStyle?: CSSProperties;
/**
* The title for Drawer.
* @type any (string | slot)
*/
title?: VNodeChild | JSX.Element | any;
/**
* The class name of the container of the Drawer dialog.
* @type string
*/
//wrapClassName?: string;
class?: string;
/**
* Style of wrapper element which **contains mask** compare to `drawerStyle`
* @type object
*/
wrapStyle?: CSSProperties;
/**
* Style of the popup layer element
* @type object
*/
drawerStyle?: CSSProperties;
/**
* Style of floating layer, typically used for adjusting its position.
* @type object
*/
bodyStyle?: CSSProperties;
headerStyle?: CSSProperties;
/**
* Width of the Drawer dialog.
* @default 256
* @type string | number
*/
width?: string | number;
/**
* placement is top or bottom, height of the Drawer dialog.
* @type string | number
*/
height?: string | number;
/**
* The z-index of the Drawer.
* @default 1000
* @type number
*/
zIndex?: number;
/**
* The placement of the Drawer.
* @default 'right'
* @type string
*/
placement?: 'top' | 'right' | 'bottom' | 'left';
afterOpenChange?: (open?: boolean) => void;
keyboard?: boolean;
/**
* Specify a callback that will be called when a user clicks mask, close button or Cancel button.
*/
onClose?: (e?: Event) => void;
}
export interface DrawerActionType {
scrollBottom: () => void;
scrollTo: (to: number) => void;
getScrollWrap: () => Element | null;
}

View File

@@ -0,0 +1,170 @@
import type {
UseDrawerReturnType,
DrawerInstance,
ReturnMethods,
DrawerProps,
UseDrawerInnerReturnType,
} from './typing';
import { ref, onUnmounted, unref, getCurrentInstance, reactive, watchEffect, nextTick, toRaw, computed } from 'vue';
import { isProdMode } from '@jeesite/core/utils/env';
import { isFunction } from '@jeesite/core/utils/is';
import { isEqual } from 'lodash-es';
import { tryOnUnmounted } from '@vueuse/core';
import { error } from '@jeesite/core/utils/log';
const dataTransfer = reactive<any>({});
const openData = reactive<{ [key: number]: boolean }>({});
/**
* @description: Applicable to separate drawer and call outside
*/
export function useDrawer(): UseDrawerReturnType {
const drawer = ref<DrawerInstance | null>(null);
const loaded = ref<Nullable<boolean>>(false);
const uid = ref<number>(0);
function register(drawerInstance: DrawerInstance, uuid: number) {
if (!getCurrentInstance()) {
throw new Error('useDrawer() can only be used inside setup() or functional components!');
}
uid.value = uuid;
isProdMode() &&
onUnmounted(() => {
drawer.value = null;
loaded.value = false;
dataTransfer[unref(uid)] = null;
});
if (unref(loaded) && isProdMode() && drawerInstance === unref(drawer)) return;
drawer.value = drawerInstance;
loaded.value = true;
drawerInstance.emitOpen = (open: boolean, uid: number) => {
openData[uid] = open;
};
}
const getInstance = () => {
const instance = unref(drawer);
if (!instance) {
error('useDrawer instance is undefined!');
}
return instance;
};
const methods: ReturnMethods = {
getDrawerProps: (): Partial<DrawerProps> => {
return getInstance()?.getDrawerProps() || {};
},
setDrawerProps: (props: Partial<DrawerProps>): void => {
getInstance()?.setDrawerProps(props);
},
getOpen: computed((): boolean => {
return openData[~~unref(uid)];
}),
openDrawer: <T = any>(open = true, data?: T, openOnSet = true): void => {
getInstance()?.setDrawerProps({
open: open,
});
if (!data) return;
const id = unref(uid);
if (openOnSet) {
dataTransfer[id] = null;
dataTransfer[id] = toRaw(data);
return;
}
const equal = isEqual(toRaw(dataTransfer[id]), toRaw(data));
if (!equal) {
dataTransfer[id] = toRaw(data);
}
},
closeDrawer: () => {
getInstance()?.setDrawerProps({ open: false });
},
setDrawerData: (data: any) => {
if (!data) return;
nextTick(() => {
setTimeout(() => {
const id = unref(uid);
dataTransfer[id] = null;
dataTransfer[id] = toRaw(data);
return;
}, 100);
});
},
};
return [register, methods];
}
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
const currentInstance = getCurrentInstance();
const uidRef = ref<number>(0);
const getInstance = () => {
const instance = unref(drawerInstanceRef);
if (!instance) {
error('useDrawerInner instance is undefined!');
return;
}
return instance;
};
const register = (modalInstance: DrawerInstance, uuid: number) => {
isProdMode() &&
tryOnUnmounted(() => {
drawerInstanceRef.value = null;
});
uidRef.value = uuid;
drawerInstanceRef.value = modalInstance;
currentInstance?.emit('register', modalInstance, uuid);
};
watchEffect(() => {
const data = dataTransfer[unref(uidRef)];
if (!data) return;
if (!callbackFn || !isFunction(callbackFn)) return;
nextTick(() => {
callbackFn(data);
});
});
return [
register,
{
changeLoading: (loading = true) => {
getInstance()?.setDrawerProps({ loading });
},
changeOkLoading: (loading = true) => {
getInstance()?.setDrawerProps({ confirmLoading: loading });
},
getOpen: computed((): boolean => {
return openData[~~unref(uidRef)];
}),
closeDrawer: () => {
getInstance()?.setDrawerProps({ open: false });
},
getDrawerProps: (): Partial<DrawerProps> => {
return getInstance()?.getDrawerProps() || {};
},
setDrawerProps: (props: Partial<DrawerProps>) => {
getInstance()?.setDrawerProps(props);
},
},
];
};