41 changed files with 1100 additions and 874 deletions
@ -1,246 +0,0 @@ |
|||||
import './index.less'; |
|
||||
|
|
||||
import type { DrawerInstance, DrawerProps } from './types'; |
|
||||
import type { CSSProperties } from 'vue'; |
|
||||
|
|
||||
import { defineComponent, ref, computed, watchEffect, watch, unref, nextTick, toRaw } from 'vue'; |
|
||||
import { Drawer, Row, Col, Button } from 'ant-design-vue'; |
|
||||
|
|
||||
import { BasicTitle } from '/@/components/Basic'; |
|
||||
import { Loading } from '/@/components/Loading'; |
|
||||
import { LeftOutlined } from '@ant-design/icons-vue'; |
|
||||
|
|
||||
import { useI18n } from '/@/hooks/web/useI18n'; |
|
||||
|
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|
||||
import { isFunction, isNumber } from '/@/utils/is'; |
|
||||
import { deepMerge } from '/@/utils'; |
|
||||
import { tryTsxEmit } from '/@/utils/helper/vueHelper'; |
|
||||
|
|
||||
import { basicProps } from './props'; |
|
||||
|
|
||||
const prefixCls = 'basic-drawer'; |
|
||||
export default defineComponent({ |
|
||||
inheritAttrs: false, |
|
||||
props: basicProps, |
|
||||
emits: ['visible-change', 'ok', 'close', 'register'], |
|
||||
setup(props, { slots, emit, attrs }) { |
|
||||
const scrollRef = ref<ElRef>(null); |
|
||||
const visibleRef = ref(false); |
|
||||
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null); |
|
||||
|
|
||||
const { t } = useI18n(); |
|
||||
|
|
||||
const getMergeProps = computed( |
|
||||
(): DrawerProps => { |
|
||||
return deepMerge(toRaw(props), unref(propsRef)); |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const getProps = computed( |
|
||||
(): DrawerProps => { |
|
||||
const opt = { |
|
||||
placement: 'right', |
|
||||
...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.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls; |
|
||||
|
|
||||
if (!getContainer) { |
|
||||
// TODO type error?
|
|
||||
opt.getContainer = '.layout-content' as any; |
|
||||
} |
|
||||
} |
|
||||
return opt as DrawerProps; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const getBindValues = computed( |
|
||||
(): DrawerProps => { |
|
||||
return { |
|
||||
...attrs, |
|
||||
...unref(getProps), |
|
||||
}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
// 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})`, |
|
||||
overflow: 'auto', |
|
||||
padding: '16px', |
|
||||
paddingBottom: '30px', |
|
||||
}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const getLoading = computed(() => { |
|
||||
return !!unref(getProps)?.loading; |
|
||||
}); |
|
||||
|
|
||||
watchEffect(() => { |
|
||||
visibleRef.value = props.visible; |
|
||||
}); |
|
||||
|
|
||||
watch( |
|
||||
() => visibleRef.value, |
|
||||
(visible) => { |
|
||||
nextTick(() => { |
|
||||
emit('visible-change', visible); |
|
||||
}); |
|
||||
}, |
|
||||
{ |
|
||||
immediate: false, |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
// Cancel event
|
|
||||
async function onClose(e: ChangeEvent) { |
|
||||
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) || {}, props); |
|
||||
|
|
||||
if (Reflect.has(props, 'visible')) { |
|
||||
visibleRef.value = !!props.visible; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function renderFooter() { |
|
||||
if (slots?.footer) { |
|
||||
return getSlot(slots, 'footer'); |
|
||||
} |
|
||||
const { |
|
||||
showCancelBtn, |
|
||||
cancelButtonProps, |
|
||||
cancelText, |
|
||||
showOkBtn, |
|
||||
okType, |
|
||||
okText, |
|
||||
okButtonProps, |
|
||||
confirmLoading, |
|
||||
showFooter, |
|
||||
} = unref(getProps); |
|
||||
if (!showFooter) { |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div class={`${prefixCls}__footer`}> |
|
||||
{getSlot(slots, 'insertFooter')} |
|
||||
{showCancelBtn && ( |
|
||||
<Button {...cancelButtonProps} onClick={onClose} class="mr-2"> |
|
||||
{() => cancelText} |
|
||||
</Button> |
|
||||
)} |
|
||||
{getSlot(slots, 'centerFooter')} |
|
||||
{showOkBtn && ( |
|
||||
<Button |
|
||||
type={okType} |
|
||||
onClick={() => { |
|
||||
emit('ok'); |
|
||||
}} |
|
||||
{...okButtonProps} |
|
||||
loading={confirmLoading} |
|
||||
> |
|
||||
{() => okText} |
|
||||
</Button> |
|
||||
)} |
|
||||
{getSlot(slots, 'appendFooter')} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
function renderHeader() { |
|
||||
if (slots?.title) { |
|
||||
return getSlot(slots, 'title'); |
|
||||
} |
|
||||
const { title } = unref(getMergeProps); |
|
||||
|
|
||||
if (!props.isDetail) { |
|
||||
return <BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle>; |
|
||||
} |
|
||||
return ( |
|
||||
<Row type="flex" align="middle" class={`${prefixCls}__detail-header`}> |
|
||||
{() => ( |
|
||||
<> |
|
||||
{props.showDetailBack && ( |
|
||||
<Button size="small" type="link" onClick={onClose}> |
|
||||
{() => <LeftOutlined />} |
|
||||
</Button> |
|
||||
)} |
|
||||
{title && ( |
|
||||
<Col style="flex:1" class={[`${prefixCls}__detail-title`, 'ellipsis', 'px-2']}> |
|
||||
{() => title} |
|
||||
</Col> |
|
||||
)} |
|
||||
{getSlot(slots, 'titleToolbar')} |
|
||||
</> |
|
||||
)} |
|
||||
</Row> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
const drawerInstance: DrawerInstance = { |
|
||||
setDrawerProps: setDrawerProps, |
|
||||
}; |
|
||||
|
|
||||
tryTsxEmit((instance) => { |
|
||||
emit('register', drawerInstance, instance.uid); |
|
||||
}); |
|
||||
|
|
||||
return () => { |
|
||||
return ( |
|
||||
<Drawer class={prefixCls} onClose={onClose} {...unref(getBindValues)}> |
|
||||
{{ |
|
||||
title: () => renderHeader(), |
|
||||
default: () => ( |
|
||||
<> |
|
||||
<div ref={scrollRef} style={unref(getScrollContentStyle)}> |
|
||||
<Loading |
|
||||
absolute |
|
||||
tip={t('component.drawer.loadingText')} |
|
||||
loading={unref(getLoading)} |
|
||||
/> |
|
||||
{getSlot(slots)} |
|
||||
</div> |
|
||||
{renderFooter()} |
|
||||
</> |
|
||||
), |
|
||||
}} |
|
||||
</Drawer> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -0,0 +1,259 @@ |
|||||
|
<template> |
||||
|
<Drawer :class="prefixCls" @close="onClose" v-bind="getBindValues"> |
||||
|
<template #title v-if="!$slots.title"> |
||||
|
<DrawerHeader |
||||
|
:title="getMergeProps.title" |
||||
|
:isDetail="isDetail" |
||||
|
:showDetailBack="showDetailBack" |
||||
|
@close="onClose" |
||||
|
> |
||||
|
<template #titleToolbar> |
||||
|
<slot name="titleToolbar" /> |
||||
|
</template> |
||||
|
</DrawerHeader> |
||||
|
</template> |
||||
|
|
||||
|
<ScrollContainer |
||||
|
:style="getScrollContentStyle" |
||||
|
v-loading="getLoading" |
||||
|
:loading-tip="loadingText || t('component.drawer.loadingText')" |
||||
|
> |
||||
|
<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" /> |
||||
|
</template> |
||||
|
</DrawerFooter> |
||||
|
</Drawer> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { DrawerInstance, DrawerProps } from './types'; |
||||
|
import type { CSSProperties } from 'vue'; |
||||
|
|
||||
|
import { |
||||
|
defineComponent, |
||||
|
ref, |
||||
|
computed, |
||||
|
watchEffect, |
||||
|
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({ |
||||
|
inheritAttrs: false, |
||||
|
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader }, |
||||
|
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 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.wrapClassName = 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), |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 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(() => { |
||||
|
visibleRef.value = props.visible; |
||||
|
}); |
||||
|
|
||||
|
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) || {}, props); |
||||
|
|
||||
|
if (Reflect.has(props, 'visible')) { |
||||
|
visibleRef.value = !!props.visible; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleOk() { |
||||
|
emit('ok'); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onClose, |
||||
|
t, |
||||
|
prefixCls, |
||||
|
getMergeProps, |
||||
|
getScrollContentStyle, |
||||
|
getProps, |
||||
|
getLoading, |
||||
|
getBindValues, |
||||
|
getFooterHeight, |
||||
|
handleOk, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
@import (reference) '../../../design/index.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: @background-color-dark; |
||||
|
|
||||
|
.scrollbar__wrap { |
||||
|
padding: 16px !important; |
||||
|
margin-bottom: 0 !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.@{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> |
||||
@ -0,0 +1,85 @@ |
|||||
|
<template> |
||||
|
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer"> |
||||
|
<template v-if="!$slots.footer"> |
||||
|
<slot name="insertFooter" /> |
||||
|
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-2" v-if="showCancelBtn"> |
||||
|
{{ cancelText }} |
||||
|
</a-button> |
||||
|
<slot name="centerFooter" /> |
||||
|
<a-button |
||||
|
:type="okType" |
||||
|
@click="handleOk" |
||||
|
v-bind="okButtonProps" |
||||
|
class="mr-2" |
||||
|
:loading="confirmLoading" |
||||
|
v-if="showOkBtn" |
||||
|
> |
||||
|
{{ okText }} |
||||
|
</a-button> |
||||
|
<slot name="appendFooter" /> |
||||
|
</template> |
||||
|
|
||||
|
<template v-else> |
||||
|
<slot name="footer" /> |
||||
|
</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"> |
||||
|
@import (reference) '../../../../design/index.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: #fff; |
||||
|
border-top: 1px solid @border-color-base; |
||||
|
|
||||
|
> * { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,73 @@ |
|||||
|
<template> |
||||
|
<BasicTitle v-if="!isDetail" :class="prefixCls"> |
||||
|
<slot name="title" /> |
||||
|
{{ !$slots.title ? title : '' }} |
||||
|
</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" /> |
||||
|
</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { defineComponent } from 'vue'; |
||||
|
import { BasicTitle } from '/@/components/Basic'; |
||||
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue'; |
||||
|
|
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
|
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
export default defineComponent({ |
||||
|
name: 'BasicDrawerHeader', |
||||
|
components: { BasicTitle, ArrowLeftOutlined }, |
||||
|
props: { |
||||
|
isDetail: propTypes.bool, |
||||
|
showDetailBack: propTypes.bool, |
||||
|
title: propTypes.string, |
||||
|
}, |
||||
|
setup(_, { emit }) { |
||||
|
const { prefixCls } = useDesign('basic-drawer-header'); |
||||
|
|
||||
|
function handleClose() { |
||||
|
emit('close'); |
||||
|
} |
||||
|
return { prefixCls, handleClose }; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less"> |
||||
|
@import (reference) '../../../../design/index.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; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,66 +0,0 @@ |
|||||
@import (reference) '../../../design/index.less'; |
|
||||
@header-height: 40px; |
|
||||
@footer-height: 60px; |
|
||||
|
|
||||
.basic-drawer { |
|
||||
.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: @background-color-dark; |
|
||||
|
|
||||
.scrollbar__wrap { |
|
||||
padding: 16px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&__detail { |
|
||||
position: absolute; |
|
||||
|
|
||||
&-header { |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.ant-drawer-header { |
|
||||
width: 100%; |
|
||||
height: @header-height; |
|
||||
padding: 0; |
|
||||
border-top: 1px solid @border-color-base; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
|
|
||||
.ant-drawer-title { |
|
||||
height: 100%; |
|
||||
} |
|
||||
|
|
||||
.ant-drawer-close { |
|
||||
height: @header-height; |
|
||||
line-height: @header-height; |
|
||||
} |
|
||||
|
|
||||
.scrollbar__wrap { |
|
||||
padding: 0; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&__footer { |
|
||||
position: absolute; |
|
||||
bottom: 0; |
|
||||
width: 100%; |
|
||||
height: @footer-height; |
|
||||
padding: 0 26px; |
|
||||
line-height: @footer-height; |
|
||||
text-align: right; |
|
||||
background: #fff; |
|
||||
border-top: 1px solid @border-color-base; |
|
||||
} |
|
||||
} |
|
||||
@ -1,10 +1,10 @@ |
|||||
import './src/index.less'; |
import './src/index.less'; |
||||
import { withInstall } from '../util'; |
import { withInstall } from '../util'; |
||||
import BasicModal from './src/BasicModal'; |
import BasicModal from './src/BasicModal.vue'; |
||||
|
|
||||
withInstall(BasicModal); |
withInstall(BasicModal); |
||||
|
|
||||
export { BasicModal }; |
export { BasicModal }; |
||||
export { useModalContext } from './src/useModalContext'; |
export { useModalContext } from './src/hooks/useModalContext'; |
||||
export { useModal, useModalInner } from './src/useModal'; |
export { useModal, useModalInner } from './src/hooks/useModal'; |
||||
export * from './src/types'; |
export * from './src/types'; |
||||
|
|||||
@ -1,232 +0,0 @@ |
|||||
import type { ModalProps, ModalMethods } from './types'; |
|
||||
|
|
||||
import { defineComponent, computed, ref, watch, unref, watchEffect, toRef } from 'vue'; |
|
||||
|
|
||||
import Modal from './Modal'; |
|
||||
import { Button } from '/@/components/Button'; |
|
||||
import ModalWrapper from './ModalWrapper'; |
|
||||
import { BasicTitle } from '/@/components/Basic'; |
|
||||
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue'; |
|
||||
|
|
||||
import { getSlot, extendSlots } from '/@/utils/helper/tsxHelper'; |
|
||||
import { isFunction } from '/@/utils/is'; |
|
||||
import { deepMerge } from '/@/utils'; |
|
||||
import { tryTsxEmit } from '/@/utils/helper/vueHelper'; |
|
||||
|
|
||||
import { basicProps } from './props'; |
|
||||
import { useFullScreen } from './useFullScreen'; |
|
||||
export default defineComponent({ |
|
||||
name: 'BasicModal', |
|
||||
props: basicProps, |
|
||||
emits: ['visible-change', 'height-change', 'cancel', 'ok', 'register'], |
|
||||
setup(props, { slots, emit, attrs }) { |
|
||||
const visibleRef = ref(false); |
|
||||
const propsRef = ref<Partial<ModalProps> | null>(null); |
|
||||
const modalWrapperRef = ref<ComponentRef>(null); |
|
||||
// modal Bottom and top height
|
|
||||
const extHeightRef = ref(0); |
|
||||
// Unexpanded height of the popup
|
|
||||
|
|
||||
// Custom title component: get title
|
|
||||
const getMergeProps = computed( |
|
||||
(): ModalProps => { |
|
||||
return { |
|
||||
...props, |
|
||||
...(unref(propsRef) as any), |
|
||||
}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({ |
|
||||
modalWrapperRef, |
|
||||
extHeightRef, |
|
||||
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'), |
|
||||
}); |
|
||||
|
|
||||
// modal component does not need title
|
|
||||
const getProps = computed( |
|
||||
(): ModalProps => { |
|
||||
const opt = { |
|
||||
...unref(getMergeProps), |
|
||||
visible: unref(visibleRef), |
|
||||
title: undefined, |
|
||||
}; |
|
||||
|
|
||||
return { |
|
||||
...opt, |
|
||||
wrapClassName: unref(getWrapClassName), |
|
||||
}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const getModalBindValue = computed((): any => { |
|
||||
return { ...attrs, ...unref(getProps) }; |
|
||||
}); |
|
||||
|
|
||||
watchEffect(() => { |
|
||||
visibleRef.value = !!props.visible; |
|
||||
}); |
|
||||
|
|
||||
watch( |
|
||||
() => unref(visibleRef), |
|
||||
(v) => { |
|
||||
emit('visible-change', v); |
|
||||
}, |
|
||||
{ |
|
||||
immediate: false, |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
/** |
|
||||
* @description: 渲染标题 |
|
||||
*/ |
|
||||
function renderTitle() { |
|
||||
const { helpMessage } = unref(getProps); |
|
||||
const { title } = unref(getMergeProps); |
|
||||
return ( |
|
||||
<BasicTitle helpMessage={helpMessage}> |
|
||||
{() => (slots.title ? getSlot(slots, 'title') : title)} |
|
||||
</BasicTitle> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// 取消事件
|
|
||||
async function handleCancel(e: Event) { |
|
||||
e?.stopPropagation(); |
|
||||
|
|
||||
if (props.closeFunc && isFunction(props.closeFunc)) { |
|
||||
const isClose: boolean = await props.closeFunc(); |
|
||||
visibleRef.value = !isClose; |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
visibleRef.value = false; |
|
||||
emit('cancel'); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: 设置modal参数 |
|
||||
*/ |
|
||||
function setModalProps(props: Partial<ModalProps>): void { |
|
||||
// Keep the last setModalProps
|
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, props); |
|
||||
if (!Reflect.has(props, 'visible')) return; |
|
||||
visibleRef.value = !!props.visible; |
|
||||
} |
|
||||
|
|
||||
function renderContent() { |
|
||||
type OmitWrapperType = Omit< |
|
||||
ModalProps, |
|
||||
'fullScreen' | 'modalFooterHeight' | 'visible' | 'loading' |
|
||||
>; |
|
||||
const { useWrapper, loading, wrapperProps } = unref(getProps); |
|
||||
if (!useWrapper) return getSlot(slots); |
|
||||
|
|
||||
const showFooter = props.footer !== undefined && !props.footer ? 0 : undefined; |
|
||||
return ( |
|
||||
<ModalWrapper |
|
||||
footerOffset={props.wrapperFooterOffset} |
|
||||
fullScreen={unref(fullScreenRef)} |
|
||||
ref={modalWrapperRef} |
|
||||
loading={loading} |
|
||||
visible={unref(visibleRef)} |
|
||||
modalFooterHeight={showFooter} |
|
||||
{...((wrapperProps as unknown) as OmitWrapperType)} |
|
||||
onGetExtHeight={(height: number) => { |
|
||||
extHeightRef.value = height; |
|
||||
}} |
|
||||
onHeightChange={(height: string) => { |
|
||||
emit('height-change', height); |
|
||||
}} |
|
||||
> |
|
||||
{() => getSlot(slots)} |
|
||||
</ModalWrapper> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// 底部按钮自定义实现,
|
|
||||
function renderFooter() { |
|
||||
const { |
|
||||
showCancelBtn, |
|
||||
cancelButtonProps, |
|
||||
cancelText, |
|
||||
showOkBtn, |
|
||||
okType, |
|
||||
okText, |
|
||||
okButtonProps, |
|
||||
confirmLoading, |
|
||||
} = unref(getProps); |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
{getSlot(slots, 'insertFooter')} |
|
||||
{showCancelBtn && ( |
|
||||
<Button {...cancelButtonProps} onClick={handleCancel}> |
|
||||
{() => cancelText} |
|
||||
</Button> |
|
||||
)} |
|
||||
{getSlot(slots, 'centerdFooter')} |
|
||||
{showOkBtn && ( |
|
||||
<Button |
|
||||
type={okType as any} |
|
||||
loading={confirmLoading} |
|
||||
onClick={() => { |
|
||||
emit('ok'); |
|
||||
}} |
|
||||
{...okButtonProps} |
|
||||
> |
|
||||
{() => okText} |
|
||||
</Button> |
|
||||
)} |
|
||||
{getSlot(slots, 'appendFooter')} |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: 关闭按钮 |
|
||||
*/ |
|
||||
function renderClose() { |
|
||||
const { canFullscreen } = unref(getProps); |
|
||||
|
|
||||
const fullScreen = unref(fullScreenRef) ? ( |
|
||||
<FullscreenExitOutlined role="full" onClick={handleFullScreen} /> |
|
||||
) : ( |
|
||||
<FullscreenOutlined role="close" onClick={handleFullScreen} /> |
|
||||
); |
|
||||
|
|
||||
const cls = [ |
|
||||
'custom-close-icon', |
|
||||
{ |
|
||||
'can-full': canFullscreen, |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
return ( |
|
||||
<div class={cls}> |
|
||||
{canFullscreen && fullScreen} |
|
||||
<CloseOutlined onClick={handleCancel} /> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
const modalMethods: ModalMethods = { |
|
||||
setModalProps, |
|
||||
}; |
|
||||
|
|
||||
tryTsxEmit((instance) => { |
|
||||
emit('register', modalMethods, instance.uid); |
|
||||
}); |
|
||||
return () => ( |
|
||||
<Modal onCancel={handleCancel} {...unref(getModalBindValue)}> |
|
||||
{{ |
|
||||
footer: () => renderFooter(), |
|
||||
closeIcon: () => renderClose(), |
|
||||
title: () => renderTitle(), |
|
||||
...extendSlots(slots, ['default']), |
|
||||
default: () => renderContent(), |
|
||||
}} |
|
||||
</Modal> |
|
||||
); |
|
||||
}, |
|
||||
}); |
|
||||
@ -0,0 +1,184 @@ |
|||||
|
<template> |
||||
|
<Modal @cancel="handleCancel" v-bind="getBindValue"> |
||||
|
<template #closeIcon v-if="!$slots.closeIcon"> |
||||
|
<ModalClose |
||||
|
:canFullscreen="getProps.canFullscreen" |
||||
|
:fullScreen="fullScreenRef" |
||||
|
@cancel="handleCancel" |
||||
|
@fullscreen="handleFullScreen" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<template #title v-if="!$slots.title"> |
||||
|
<ModalHeader :helpMessage="getProps.helpMessage" :title="getMergeProps.title" /> |
||||
|
</template> |
||||
|
|
||||
|
<template #footer v-if="!$slots.footer"> |
||||
|
<ModalFooter v-bind="getProps" @ok="handleOk" @cancel="handleCancel" /> |
||||
|
</template> |
||||
|
<ModalWrapper |
||||
|
:useWrapper="getProps.useWrapper" |
||||
|
:footerOffset="wrapperFooterOffset" |
||||
|
:fullScreen="fullScreenRef" |
||||
|
ref="modalWrapperRef" |
||||
|
:loading="getProps.loading" |
||||
|
:visible="visibleRef" |
||||
|
:modalFooterHeight="footer !== undefined && !footer ? 0 : undefined" |
||||
|
v-bind="omit(getProps.wrapperProps, 'visible')" |
||||
|
@ext-height="handleExtHeight" |
||||
|
@height-change="handleHeightChange" |
||||
|
> |
||||
|
<slot /> |
||||
|
</ModalWrapper> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { ModalProps, ModalMethods } from './types'; |
||||
|
|
||||
|
import { |
||||
|
defineComponent, |
||||
|
computed, |
||||
|
ref, |
||||
|
watch, |
||||
|
unref, |
||||
|
watchEffect, |
||||
|
toRef, |
||||
|
getCurrentInstance, |
||||
|
} from 'vue'; |
||||
|
|
||||
|
import Modal from './components/Modal'; |
||||
|
import ModalWrapper from './components/ModalWrapper.vue'; |
||||
|
import ModalClose from './components/ModalClose.vue'; |
||||
|
import ModalFooter from './components/ModalFooter.vue'; |
||||
|
import ModalHeader from './components/ModalHeader.vue'; |
||||
|
|
||||
|
import { isFunction } from '/@/utils/is'; |
||||
|
import { deepMerge } from '/@/utils'; |
||||
|
|
||||
|
import { basicProps } from './props'; |
||||
|
import { useFullScreen } from './hooks/useModalFullScreen'; |
||||
|
import { omit } from 'lodash-es'; |
||||
|
export default defineComponent({ |
||||
|
name: 'BasicModal', |
||||
|
components: { Modal, ModalWrapper, ModalClose, ModalFooter, ModalHeader }, |
||||
|
props: basicProps, |
||||
|
emits: ['visible-change', 'height-change', 'cancel', 'ok', 'register'], |
||||
|
setup(props, { emit, attrs }) { |
||||
|
const visibleRef = ref(false); |
||||
|
const propsRef = ref<Partial<ModalProps> | null>(null); |
||||
|
const modalWrapperRef = ref<ComponentRef>(null); |
||||
|
// modal Bottom and top height |
||||
|
const extHeightRef = ref(0); |
||||
|
const modalMethods: ModalMethods = { |
||||
|
setModalProps, |
||||
|
emitVisible: undefined, |
||||
|
}; |
||||
|
const instance = getCurrentInstance(); |
||||
|
if (instance) { |
||||
|
emit('register', modalMethods, instance.uid); |
||||
|
} |
||||
|
|
||||
|
// Custom title component: get title |
||||
|
const getMergeProps = computed( |
||||
|
(): ModalProps => { |
||||
|
return { |
||||
|
...props, |
||||
|
...(unref(propsRef) as any), |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({ |
||||
|
modalWrapperRef, |
||||
|
extHeightRef, |
||||
|
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'), |
||||
|
}); |
||||
|
|
||||
|
// modal component does not need title |
||||
|
const getProps = computed( |
||||
|
(): ModalProps => { |
||||
|
const opt = { |
||||
|
...unref(getMergeProps), |
||||
|
visible: unref(visibleRef), |
||||
|
title: undefined, |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
...opt, |
||||
|
wrapClassName: unref(getWrapClassName), |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const getBindValue = computed((): any => { |
||||
|
return { ...attrs, ...unref(getProps) }; |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
visibleRef.value = !!props.visible; |
||||
|
}); |
||||
|
|
||||
|
watch( |
||||
|
() => unref(visibleRef), |
||||
|
(v) => { |
||||
|
emit('visible-change', v); |
||||
|
instance && modalMethods.emitVisible?.(v, instance.uid); |
||||
|
}, |
||||
|
{ |
||||
|
immediate: false, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 取消事件 |
||||
|
async function handleCancel(e: Event) { |
||||
|
e?.stopPropagation(); |
||||
|
|
||||
|
if (props.closeFunc && isFunction(props.closeFunc)) { |
||||
|
const isClose: boolean = await props.closeFunc(); |
||||
|
visibleRef.value = !isClose; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
visibleRef.value = false; |
||||
|
emit('cancel'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description: 设置modal参数 |
||||
|
*/ |
||||
|
function setModalProps(props: Partial<ModalProps>): void { |
||||
|
// Keep the last setModalProps |
||||
|
propsRef.value = deepMerge(unref(propsRef) || {}, props); |
||||
|
if (!Reflect.has(props, 'visible')) return; |
||||
|
visibleRef.value = !!props.visible; |
||||
|
} |
||||
|
|
||||
|
function handleOk() { |
||||
|
emit('ok'); |
||||
|
} |
||||
|
|
||||
|
function handleHeightChange(height: string) { |
||||
|
emit('height-change', height); |
||||
|
} |
||||
|
|
||||
|
function handleExtHeight(height: number) { |
||||
|
extHeightRef.value = height; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
handleCancel, |
||||
|
getBindValue, |
||||
|
getProps, |
||||
|
handleFullScreen, |
||||
|
fullScreenRef, |
||||
|
getMergeProps, |
||||
|
handleOk, |
||||
|
visibleRef, |
||||
|
omit, |
||||
|
modalWrapperRef, |
||||
|
handleExtHeight, |
||||
|
handleHeightChange, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -1,161 +0,0 @@ |
|||||
import type { ModalWrapperProps } from './types'; |
|
||||
import type { CSSProperties } from 'vue'; |
|
||||
|
|
||||
import { |
|
||||
defineComponent, |
|
||||
computed, |
|
||||
ref, |
|
||||
watchEffect, |
|
||||
unref, |
|
||||
watch, |
|
||||
onMounted, |
|
||||
nextTick, |
|
||||
onUnmounted, |
|
||||
} from 'vue'; |
|
||||
import { Spin } from 'ant-design-vue'; |
|
||||
|
|
||||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
|
||||
|
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|
||||
import { useElResize } from '/@/hooks/event/useElResize'; |
|
||||
import { propTypes } from '/@/utils/propTypes'; |
|
||||
import { createModalContext } from './useModalContext'; |
|
||||
|
|
||||
export default defineComponent({ |
|
||||
name: 'ModalWrapper', |
|
||||
props: { |
|
||||
loading: propTypes.bool, |
|
||||
modalHeaderHeight: propTypes.number.def(50), |
|
||||
modalFooterHeight: propTypes.number.def(54), |
|
||||
minHeight: propTypes.number.def(200), |
|
||||
footerOffset: propTypes.number.def(0), |
|
||||
visible: propTypes.bool, |
|
||||
fullScreen: propTypes.bool, |
|
||||
}, |
|
||||
emits: ['heightChange', 'getExtHeight'], |
|
||||
setup(props: ModalWrapperProps, { slots, emit }) { |
|
||||
const wrapperRef = ref<ElRef>(null); |
|
||||
const spinRef = ref<ComponentRef>(null); |
|
||||
const realHeightRef = ref(0); |
|
||||
|
|
||||
let stopElResizeFn: Fn = () => {}; |
|
||||
|
|
||||
useWindowSizeFn(setModalHeight); |
|
||||
|
|
||||
createModalContext({ |
|
||||
redoModalHeight: setModalHeight, |
|
||||
}); |
|
||||
|
|
||||
const wrapStyle = computed( |
|
||||
(): CSSProperties => { |
|
||||
return { |
|
||||
minHeight: `${props.minHeight}px`, |
|
||||
height: `${unref(realHeightRef)}px`, |
|
||||
overflow: 'auto', |
|
||||
}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
watchEffect(() => { |
|
||||
setModalHeight(); |
|
||||
}); |
|
||||
|
|
||||
watch( |
|
||||
() => props.fullScreen, |
|
||||
(v) => { |
|
||||
!v && setModalHeight(); |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
onMounted(() => { |
|
||||
const { modalHeaderHeight, modalFooterHeight } = props; |
|
||||
emit('getExtHeight', modalHeaderHeight + modalFooterHeight); |
|
||||
listenElResize(); |
|
||||
}); |
|
||||
|
|
||||
onUnmounted(() => { |
|
||||
stopElResizeFn && stopElResizeFn(); |
|
||||
}); |
|
||||
|
|
||||
async function setModalHeight() { |
|
||||
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
|
|
||||
// 加上这个,就必须在使用的时候传递父级的visible
|
|
||||
if (!props.visible) return; |
|
||||
const wrapperRefDom = unref(wrapperRef); |
|
||||
if (!wrapperRefDom) return; |
|
||||
const bodyDom = wrapperRefDom.parentElement; |
|
||||
if (!bodyDom) return; |
|
||||
bodyDom.style.padding = '0'; |
|
||||
await nextTick(); |
|
||||
|
|
||||
try { |
|
||||
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement; |
|
||||
if (!modalDom) return; |
|
||||
|
|
||||
const modalRect = getComputedStyle(modalDom).top; |
|
||||
const modalTop = Number.parseInt(modalRect); |
|
||||
let maxHeight = |
|
||||
window.innerHeight - |
|
||||
modalTop * 2 + |
|
||||
(props.footerOffset! || 0) - |
|
||||
props.modalFooterHeight - |
|
||||
props.modalHeaderHeight; |
|
||||
|
|
||||
// 距离顶部过进会出现滚动条
|
|
||||
if (modalTop < 40) { |
|
||||
maxHeight -= 26; |
|
||||
} |
|
||||
await nextTick(); |
|
||||
const spinEl = unref(spinRef); |
|
||||
|
|
||||
if (!spinEl) return; |
|
||||
|
|
||||
const spinContainerEl = spinEl.$el.querySelector('.ant-spin-container') as HTMLElement; |
|
||||
if (!spinContainerEl) return; |
|
||||
|
|
||||
const realHeight = spinContainerEl.scrollHeight; |
|
||||
|
|
||||
if (props.fullScreen) { |
|
||||
realHeightRef.value = |
|
||||
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight; |
|
||||
} else { |
|
||||
realHeightRef.value = realHeight > maxHeight ? maxHeight : realHeight + 16 + 30; |
|
||||
} |
|
||||
emit('heightChange', unref(realHeightRef)); |
|
||||
|
|
||||
nextTick(() => { |
|
||||
const el = spinEl.$el; |
|
||||
if (el) { |
|
||||
el.style.height = `${unref(realHeightRef)}px`; |
|
||||
} |
|
||||
}); |
|
||||
} catch (error) { |
|
||||
console.log(error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function listenElResize() { |
|
||||
const wrapper = unref(wrapperRef); |
|
||||
if (!wrapper) return; |
|
||||
|
|
||||
const container = wrapper.querySelector('.ant-spin-container'); |
|
||||
if (!container) return; |
|
||||
|
|
||||
const [start, stop] = useElResize(container, () => { |
|
||||
setModalHeight(); |
|
||||
}); |
|
||||
stopElResizeFn = stop; |
|
||||
start(); |
|
||||
} |
|
||||
|
|
||||
return () => { |
|
||||
return ( |
|
||||
<div ref={wrapperRef} style={unref(wrapStyle)}> |
|
||||
<Spin ref={spinRef} spinning={props.loading}> |
|
||||
{() => getSlot(slots)} |
|
||||
</Spin> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -0,0 +1,98 @@ |
|||||
|
<template> |
||||
|
<div :class="getClass"> |
||||
|
<template v-if="canFullscreen"> |
||||
|
<FullscreenExitOutlined role="full" @click="handleFullScreen" v-if="fullScreen" /> |
||||
|
|
||||
|
<FullscreenOutlined role="close" @click="handleFullScreen" v-else /> |
||||
|
</template> |
||||
|
<CloseOutlined @click="handleCancel" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { defineComponent, computed } from 'vue'; |
||||
|
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'ModalClose', |
||||
|
components: { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined }, |
||||
|
props: { |
||||
|
canFullscreen: propTypes.bool.def(true), |
||||
|
fullScreen: propTypes.bool, |
||||
|
}, |
||||
|
emits: ['cancel', 'fullscreen'], |
||||
|
setup(props, { emit }) { |
||||
|
const { prefixCls } = useDesign('basic-modal-close'); |
||||
|
|
||||
|
const getClass = computed(() => { |
||||
|
return [ |
||||
|
prefixCls, |
||||
|
`${prefixCls}--custom`, |
||||
|
{ |
||||
|
[`${prefixCls}--can-full`]: props.canFullscreen, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
function handleCancel() { |
||||
|
emit('cancel'); |
||||
|
} |
||||
|
function handleFullScreen(e: Event) { |
||||
|
e?.stopPropagation(); |
||||
|
e?.preventDefault(); |
||||
|
emit('fullscreen'); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
getClass, |
||||
|
prefixCls, |
||||
|
handleCancel, |
||||
|
handleFullScreen, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
@import (reference) '../../../../design/index.less'; |
||||
|
@prefix-cls: ~'@{namespace}-basic-modal-close'; |
||||
|
.@{prefix-cls} { |
||||
|
display: flex; |
||||
|
height: 95%; |
||||
|
align-items: center; |
||||
|
|
||||
|
> span { |
||||
|
margin-left: 48px; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
&--can-full { |
||||
|
> span { |
||||
|
margin-left: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&:not(&--can-full) { |
||||
|
> span:nth-child(1) { |
||||
|
&:hover { |
||||
|
font-weight: 700; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
& span:nth-child(1) { |
||||
|
display: inline-block; |
||||
|
padding: 10px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: @primary-color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
& span:nth-child(2) { |
||||
|
&:hover { |
||||
|
color: @error-color; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,39 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<slot name="insertFooter" /> |
||||
|
<a-button v-bind="cancelButtonProps" @click="handleCancel" v-if="showCancelBtn"> |
||||
|
{{ cancelText }} |
||||
|
</a-button> |
||||
|
<slot name="centerFooter" /> |
||||
|
<a-button |
||||
|
:type="okType" |
||||
|
@click="handleOk" |
||||
|
:loading="confirmLoading" |
||||
|
v-bind="okButtonProps" |
||||
|
v-if="showOkBtn" |
||||
|
> |
||||
|
{{ okText }} |
||||
|
</a-button> |
||||
|
<slot name="appendFooter" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { defineComponent } from 'vue'; |
||||
|
|
||||
|
import { basicProps } from '../props'; |
||||
|
export default defineComponent({ |
||||
|
name: 'BasicModalFooter', |
||||
|
props: basicProps, |
||||
|
emits: ['ok', 'cancel'], |
||||
|
setup(_, { emit }) { |
||||
|
function handleOk() { |
||||
|
emit('ok'); |
||||
|
} |
||||
|
|
||||
|
function handleCancel() { |
||||
|
emit('cancel'); |
||||
|
} |
||||
|
return { handleOk, handleCancel }; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,22 @@ |
|||||
|
<template> |
||||
|
<BasicTitle :helpMessage="helpMessage"> |
||||
|
{{ title }} |
||||
|
</BasicTitle> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { PropType } from 'vue'; |
||||
|
import { defineComponent } from 'vue'; |
||||
|
import { BasicTitle } from '/@/components/Basic'; |
||||
|
|
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
export default defineComponent({ |
||||
|
name: 'BasicModalHeader', |
||||
|
components: { BasicTitle }, |
||||
|
props: { |
||||
|
helpMessage: { |
||||
|
type: [String, Array] as PropType<string | string[]>, |
||||
|
}, |
||||
|
title: propTypes.string, |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,152 @@ |
|||||
|
<template> |
||||
|
<ScrollContainer ref="wrapperRef" :style="wrapStyle"> |
||||
|
<div ref="spinRef" :style="spinStyle" v-loading="loading" :loading-tip="loadingTip"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</ScrollContainer> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { ModalWrapperProps } from '../types'; |
||||
|
import type { CSSProperties } from 'vue'; |
||||
|
|
||||
|
import { |
||||
|
defineComponent, |
||||
|
computed, |
||||
|
ref, |
||||
|
watchEffect, |
||||
|
unref, |
||||
|
watch, |
||||
|
onMounted, |
||||
|
nextTick, |
||||
|
onUnmounted, |
||||
|
} from 'vue'; |
||||
|
import { Spin } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; |
||||
|
import { ScrollContainer } from '/@/components/Container'; |
||||
|
|
||||
|
// import { useElResize } from '/@/hooks/event/useElResize'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { createModalContext } from '../hooks/useModalContext'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'ModalWrapper', |
||||
|
components: { Spin, ScrollContainer }, |
||||
|
props: { |
||||
|
loading: propTypes.bool, |
||||
|
useWrapper: propTypes.bool.def(true), |
||||
|
modalHeaderHeight: propTypes.number.def(50), |
||||
|
modalFooterHeight: propTypes.number.def(54), |
||||
|
minHeight: propTypes.number.def(200), |
||||
|
footerOffset: propTypes.number.def(0), |
||||
|
visible: propTypes.bool, |
||||
|
fullScreen: propTypes.bool, |
||||
|
loadingTip: propTypes.string, |
||||
|
}, |
||||
|
emits: ['height-change', 'ext-height'], |
||||
|
setup(props: ModalWrapperProps, { emit }) { |
||||
|
const wrapperRef = ref<ComponentRef>(null); |
||||
|
const spinRef = ref<ElRef>(null); |
||||
|
const realHeightRef = ref(0); |
||||
|
|
||||
|
let stopElResizeFn: Fn = () => {}; |
||||
|
|
||||
|
useWindowSizeFn(setModalHeight); |
||||
|
|
||||
|
createModalContext({ |
||||
|
redoModalHeight: setModalHeight, |
||||
|
}); |
||||
|
|
||||
|
const wrapStyle = computed( |
||||
|
(): CSSProperties => { |
||||
|
return { |
||||
|
minHeight: `${props.minHeight}px`, |
||||
|
height: `${unref(realHeightRef)}px`, |
||||
|
// overflow: 'auto', |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const spinStyle = computed( |
||||
|
(): CSSProperties => { |
||||
|
return { |
||||
|
// padding 28 |
||||
|
height: `${unref(realHeightRef) - 28}px`, |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
props.useWrapper && setModalHeight(); |
||||
|
}); |
||||
|
|
||||
|
watch( |
||||
|
() => props.fullScreen, |
||||
|
() => { |
||||
|
setTimeout(() => { |
||||
|
setModalHeight(); |
||||
|
}, 0); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
const { modalHeaderHeight, modalFooterHeight } = props; |
||||
|
emit('ext-height', modalHeaderHeight + modalFooterHeight); |
||||
|
// listenElResize(); |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
stopElResizeFn && stopElResizeFn(); |
||||
|
}); |
||||
|
|
||||
|
async function setModalHeight() { |
||||
|
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度 |
||||
|
// 加上这个,就必须在使用的时候传递父级的visible |
||||
|
if (!props.visible) return; |
||||
|
const wrapperRefDom = unref(wrapperRef); |
||||
|
if (!wrapperRefDom) return; |
||||
|
const bodyDom = wrapperRefDom.$el.parentElement; |
||||
|
if (!bodyDom) return; |
||||
|
bodyDom.style.padding = '0'; |
||||
|
await nextTick(); |
||||
|
|
||||
|
try { |
||||
|
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement; |
||||
|
if (!modalDom) return; |
||||
|
|
||||
|
const modalRect = getComputedStyle(modalDom).top; |
||||
|
const modalTop = Number.parseInt(modalRect); |
||||
|
let maxHeight = |
||||
|
window.innerHeight - |
||||
|
modalTop * 2 + |
||||
|
(props.footerOffset! || 0) - |
||||
|
props.modalFooterHeight - |
||||
|
props.modalHeaderHeight; |
||||
|
|
||||
|
// 距离顶部过进会出现滚动条 |
||||
|
if (modalTop < 40) { |
||||
|
maxHeight -= 26; |
||||
|
} |
||||
|
await nextTick(); |
||||
|
const spinEl = unref(spinRef); |
||||
|
|
||||
|
if (!spinEl) return; |
||||
|
|
||||
|
const realHeight = spinEl.scrollHeight; |
||||
|
|
||||
|
if (props.fullScreen) { |
||||
|
realHeightRef.value = |
||||
|
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight; |
||||
|
} else { |
||||
|
realHeightRef.value = realHeight > maxHeight ? maxHeight : realHeight + 16 + 30; |
||||
|
} |
||||
|
emit('height-change', unref(realHeightRef)); |
||||
|
} catch (error) { |
||||
|
console.log(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { wrapStyle, wrapperRef, spinRef, spinStyle }; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
Loading…
Reference in new issue