初始版本提交

This commit is contained in:
yaoyn
2024-02-05 09:15:37 +08:00
parent b52d4414be
commit 445292105f
1848 changed files with 236859 additions and 75 deletions

View File

@ -0,0 +1,6 @@
import { withInstall } from '/@/utils';
import basicDrawer from './src/BasicDrawer.vue';
export const BasicDrawer = withInstall(basicDrawer);
export * from './src/typing';
export { useDrawer, useDrawerInner } from './src/useDrawer';

View File

@ -0,0 +1,269 @@
<template>
<Drawer
:class="prefixCls"
@close="onClose"
v-bind="getBindValues"
:contentWrapperStyle="contentWrapperStyle"
>
<template #title v-if="!$slots.title">
<DrawerHeader
:title="getMergeProps.title"
:isDetail="isDetail"
:showDetailBack="showDetailBack"
:canFullscreen="getMergeProps.canFullscreen"
@close="onClose"
@fullscreen="handleFullScreen"
>
<template #titleToolbar>
<slot name="titleToolbar"></slot>
</template>
</DrawerHeader>
</template>
<template v-else #title>
<slot name="title"></slot>
</template>
<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">
import type { DrawerInstance, DrawerProps } from './typing';
import type { CSSProperties } from 'vue';
import {
defineComponent,
ref,
computed,
watch,
unref,
nextTick,
toRaw,
getCurrentInstance,
} from 'vue';
import { Drawer } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { isFunction, isNumber } from '/@/utils/is';
import { deepMerge } from '/@/utils';
import DrawerFooter from './components/DrawerFooter.vue';
import DrawerHeader from './components/DrawerHeader.vue';
import { ScrollContainer } from '/@/components/Container';
import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign';
import { useAttrs } from '/@/hooks/core/useAttrs';
export default defineComponent({
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader },
inheritAttrs: false,
props: basicProps,
emits: ['visible-change', 'ok', 'close', 'register'],
setup(props, { emit }) {
const visibleRef = ref(false);
const attrs = useAttrs();
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
const { t } = useI18n();
const { prefixVar, prefixCls } = useDesign('basic-drawer');
const contentWrapperStyle = ref({});
const drawerInstance: DrawerInstance = {
setDrawerProps: setDrawerProps,
emitVisible: undefined,
};
const instance = getCurrentInstance();
instance && emit('register', drawerInstance, instance.uid);
const getMergeProps = computed((): DrawerProps => {
return deepMerge(toRaw(props), unref(propsRef));
});
const getProps = computed((): DrawerProps => {
const opt = {
placement: 'right',
...unref(attrs),
...unref(getMergeProps),
visible: unref(visibleRef),
};
opt.title = undefined;
const { isDetail, width, 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;
}
}
return opt as DrawerProps;
});
const getBindValues = computed((): DrawerProps => {
return {
...attrs,
...unref(getProps),
maskClosable: false,
};
});
// 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;
});
watch(
() => props.visible,
(newVal, oldVal) => {
if (newVal !== oldVal) visibleRef.value = newVal;
},
{ deep: true },
);
watch(
() => visibleRef.value,
(visible) => {
nextTick(() => {
emit('visible-change', visible);
instance && drawerInstance.emitVisible?.(visible, instance.uid);
});
},
);
// Cancel event
async function onClose(e: Recordable) {
const { closeFunc } = unref(getProps);
emit('close', e);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
visibleRef.value = !res;
return;
}
visibleRef.value = false;
}
function setDrawerProps(props: Partial<DrawerProps>): void {
// Keep the last setDrawerProps
propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
if (Reflect.has(props, 'visible')) {
visibleRef.value = !!props.visible;
}
}
function handleFullScreen(fullScreen) {
contentWrapperStyle.value = fullScreen ? { width: '100%' } : {};
}
function handleOk() {
emit('ok');
}
return {
onClose,
t,
prefixCls,
getMergeProps: getMergeProps as any,
getScrollContentStyle,
getProps: getProps as any,
getLoading,
getBindValues,
getFooterHeight,
handleOk,
handleFullScreen,
contentWrapperStyle,
};
},
});
</script>
<style lang="less">
@header-height: 60px;
@detail-header-height: 40px;
@prefix-cls: ~'@{namespace}-basic-drawer';
@prefix-cls-detail: ~'@{namespace}-basic-drawer__detail';
.@{prefix-cls} {
.ant-drawer-wrapper-body {
overflow: hidden;
}
.ant-drawer-close {
&:hover {
color: @error-color;
}
}
.ant-drawer-body {
height: calc(100% - @header-height);
padding: 0;
background-color: @component-background;
.scrollbar__wrap {
padding: 16px !important;
margin-bottom: 0 !important;
}
> .scrollbar > .scrollbar__bar.is-horizontal {
display: none;
}
}
}
.@{prefix-cls-detail} {
position: absolute;
.ant-drawer-header {
width: 100%;
height: @detail-header-height;
padding: 0;
border-top: 1px solid @border-color-base;
box-sizing: border-box;
}
.ant-drawer-title {
height: 100%;
}
.ant-drawer-close {
height: @detail-header-height;
line-height: @detail-header-height;
}
.scrollbar__wrap {
padding: 0 !important;
}
.ant-drawer-body {
height: calc(100% - @detail-header-height);
}
}
</style>

View File

@ -0,0 +1,83 @@
<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">
{{ cancelText }}
</a-button>
<slot name="centerFooter"></slot>
<a-button
:type="okType"
@click="handleOk"
v-bind="okButtonProps"
class="mr-2"
:loading="confirmLoading"
:disabled="confirmLoading"
v-if="showOkBtn"
>
{{ 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 '/@/hooks/web/useDesign';
import { footerProps } from '../props';
export default defineComponent({
name: 'BasicDrawerFooter',
props: {
...footerProps,
height: {
type: String,
default: '60px',
},
},
emits: ['ok', 'close'],
setup(props, { emit }) {
const { prefixCls } = useDesign('basic-drawer-footer');
const getStyle = computed((): CSSProperties => {
const heightStr = `${props.height}`;
return {
height: heightStr,
lineHeight: heightStr,
};
});
function handleOk() {
emit('ok');
}
function handleClose() {
emit('close');
}
return { handleOk, prefixCls, handleClose, getStyle };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-drawer-footer';
@footer-height: 60px;
.@{prefix-cls} {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 12px 0 20px;
text-align: right;
background-color: @component-background;
border-top: 1px solid @border-color-base;
> * {
margin-right: 8px;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<BasicTitle v-if="!isDetail" :class="prefixCls">
<slot name="title"></slot>
{{ !$slots.title ? title : '' }}
<div v-if="canFullscreen" class="fullscreen-icon">
<Tooltip :title="t('还原')" placement="bottom" v-if="fullScreen">
<FullscreenExitOutlined role="full" @click="handleFullScreen" />
</Tooltip>
<Tooltip :title="t('最大化')" placement="bottom" v-else>
<FullscreenOutlined role="close" @click="handleFullScreen" />
</Tooltip>
</div>
</BasicTitle>
<div :class="[prefixCls, `${prefixCls}--detail`]" v-else>
<span :class="`${prefixCls}__twrap`">
<span @click="handleClose" v-if="showDetailBack">
<ArrowLeftOutlined :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, ref } from 'vue';
import { BasicTitle } from '/@/components/Basic';
import {
FullscreenExitOutlined,
FullscreenOutlined,
ArrowLeftOutlined,
} from '@ant-design/icons-vue';
import { Tooltip } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'BasicDrawerHeader',
components: {
BasicTitle,
ArrowLeftOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
Tooltip,
},
props: {
isDetail: propTypes.bool,
showDetailBack: propTypes.bool,
title: propTypes.string,
canFullscreen: { type: Boolean, default: false },
},
emits: ['close', 'fullscreen'],
setup(_, { emit }) {
const { prefixCls } = useDesign('basic-drawer-header');
const { t } = useI18n();
const fullScreen = ref(false);
function handleClose() {
emit('close');
}
function handleFullScreen(e: Event) {
e?.stopPropagation();
e?.preventDefault();
fullScreen.value = !fullScreen.value;
emit('fullscreen', fullScreen.value);
}
return { prefixCls, handleClose, t, fullScreen, handleFullScreen };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-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;
}
}
.fullscreen-icon {
position: absolute;
right: 0;
}
</style>

View File

@ -0,0 +1,44 @@
import type { PropType } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export const footerProps = {
confirmLoading: { type: Boolean },
/**
* @description: Show close button
*/
showCancelBtn: { type: Boolean, default: true },
cancelButtonProps: Object as PropType<Recordable>,
cancelText: { type: String, default: t('取消') },
/**
* @description: Show confirmation button
*/
showOkBtn: { type: Boolean, default: true },
okButtonProps: Object as PropType<Recordable>,
okText: { type: String, default: t('确认') },
okType: { type: String, default: 'primary' },
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 },
visible: { 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 },
...footerProps,
};

View File

@ -0,0 +1,195 @@
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
import type { ScrollContainerOptions } from '/@/components/Container/index';
export interface DrawerInstance {
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
emitVisible?: (visible: boolean, uid: number) => void;
}
export interface ReturnMethods extends DrawerInstance {
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void;
closeDrawer: () => void;
getVisible?: ComputedRef<boolean>;
}
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
export interface ReturnInnerMethods extends DrawerInstance {
closeDrawer: () => void;
changeLoading: (loading: boolean) => void;
changeOkLoading: (loading: boolean) => void;
getVisible?: 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';
/**
* The ok button props, follow jsx rules
* @type object
*/
okButtonProps: { props: ButtonProps; on: {} };
/**
* The cancel button props, follow jsx rules
* @type object
*/
cancelButtonProps: { props: ButtonProps; on: {} };
/**
* 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;
visible?: boolean;
/**
* Built-in ScrollContainer component configuration
* @type ScrollContainerOptions
*/
scrollOptions?: ScrollContainerOptions;
closeFunc?: () => Promise<any>;
triggerWindowResize?: boolean;
/**
* Whether a close (x) button is visible 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?: () => HTMLElement | string;
/**
* 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;
/**
* 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';
afterVisibleChange?: (visible?: 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;
canFullscreen?: boolean;
contentWrapperStyle?: any;
}
export interface DrawerActionType {
scrollBottom: () => void;
scrollTo: (to: number) => void;
getScrollWrap: () => Element | null;
}

View File

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