Merge pull request #26 from huanghong1125/sticky

add  Sticky like Affix
This commit is contained in:
Archer 2022-04-06 08:26:51 +08:00 committed by GitHub
commit 326bba8002
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 329 additions and 39 deletions

View File

@ -257,6 +257,14 @@ const adminList = [
meta: { meta: {
title: 'router.inputPassword' title: 'router.inputPassword'
} }
},
{
path: 'sticky',
component: 'views/Components/Sticky',
name: 'Sticky',
meta: {
title: 'router.sticky'
}
} }
] ]
}, },
@ -477,6 +485,7 @@ const testList: string[] = [
'/components/highlight', '/components/highlight',
'/components/infotip', '/components/infotip',
'/Components/InputPassword', '/Components/InputPassword',
'/Components/Sticky',
'/hooks', '/hooks',
'/hooks/useWatermark', '/hooks/useWatermark',
'/level', '/level',

View File

@ -0,0 +1,3 @@
import ContentDetailWrap from './src/ContentDetailWrap.vue'
export { ContentDetailWrap }

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { ElCard, ElButton } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ref, onMounted, defineEmits } from 'vue'
import { Sticky } from '@/components/Sticky'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('content-detail-wrap')
defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def('')
})
const emit = defineEmits(['back'])
const offset = ref(85)
const contentDetailWrap = ref()
onMounted(() => {
offset.value = contentDetailWrap.value.getBoundingClientRect().top
})
</script>
<template>
<div :class="[`${prefixCls}-container`, 'relative']" ref="contentDetailWrap">
<Sticky :offset="offset">
<div
:class="[
`${prefixCls}-header`,
'flex border-bottom-1 h-50px items-center text-center bg-white pr-10px '
]"
>
<div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
<el-button @click="emit('back')">
<Icon icon="ep:arrow-left" class="mr-5px" />
{{ t('common.back') }}
</el-button>
</div>
<div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']">
<slot name="title">
<label class="text-16px font-700">{{ title }}</label>
</slot>
</div>
<div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']">
<slot name="right"></slot>
</div>
</div>
</Sticky>
<div style="padding: var(--app-content-padding)">
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
<div>
<slot></slot>
</div>
</ElCard>
</div>
</div>
</template>

View File

@ -14,6 +14,7 @@ defineProps({
</script> </script>
<template> <template>
<div style="padding: var(--app-content-padding)">
<ElCard :class="[prefixCls, 'mb-20px']" shadow="never"> <ElCard :class="[prefixCls, 'mb-20px']" shadow="never">
<template v-if="title" #header> <template v-if="title" #header>
<div class="flex items-center"> <div class="flex items-center">
@ -30,4 +31,5 @@ defineProps({
<slot></slot> <slot></slot>
</div> </div>
</ElCard> </ElCard>
</div>
</template> </template>

View File

@ -0,0 +1,3 @@
import Sticky from './src/Sticky.vue'
export { Sticky }

View File

@ -0,0 +1,141 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ref, onMounted, onActivated, shallowRef } from 'vue'
import { useEventListener, useWindowSize, isClient } from '@vueuse/core'
import type { CSSProperties } from 'vue'
const props = defineProps({
// (px)
offset: propTypes.number.def(0),
//
zIndex: propTypes.number.def(999),
// class
className: propTypes.string.def(''),
// (top)topbottom
position: {
type: String,
validator: function (value: string) {
return ['top', 'bottom'].indexOf(value) !== -1
},
default: 'top'
}
})
const width = ref('auto' as string)
const height = ref('auto' as string)
const isSticky = ref(false)
const refSticky = shallowRef<HTMLElement>()
const scrollContainer = shallowRef<HTMLElement | Window>()
const { height: windowHeight } = useWindowSize()
onMounted(() => {
height.value = refSticky.value?.getBoundingClientRect().height + 'px'
scrollContainer.value = getScrollContainer(refSticky.value!, true)
useEventListener(scrollContainer, 'scroll', handleScroll)
useEventListener('resize', handleReize)
handleScroll()
})
onActivated(() => {
handleScroll()
})
const camelize = (str: string): string => {
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
if (!isClient || !element || !styleName) return ''
let key = camelize(styleName)
if (key === 'float') key = 'cssFloat'
try {
const style = element.style[styleName]
if (style) return style
const computed = document.defaultView?.getComputedStyle(element, '')
return computed ? computed[styleName] : ''
} catch {
return element.style[styleName]
}
}
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
if (!isClient) return false
const key = (
{
undefined: 'overflow',
true: 'overflow-y',
false: 'overflow-x'
} as const
)[String(isVertical)]!
const overflow = getStyle(el, key)
return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
}
const getScrollContainer = (
el: HTMLElement,
isVertical: boolean
): Window | HTMLElement | undefined => {
if (!isClient) return
let parent = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) return window
if (isScroll(parent, isVertical)) return parent
parent = parent.parentNode as HTMLElement
}
return parent
}
const handleScroll = () => {
width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
if (props.position === 'top') {
const offsetTop = refSticky.value?.getBoundingClientRect().top
if (offsetTop !== undefined && offsetTop < props.offset) {
sticky()
return
}
reset()
} else {
const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
sticky()
return
}
reset()
}
}
const handleReize = () => {
if (isSticky.value && refSticky.value) {
width.value = refSticky.value.getBoundingClientRect().width + 'px'
}
}
const sticky = () => {
if (isSticky.value) {
return
}
isSticky.value = true
}
const reset = () => {
if (!isSticky.value) {
return
}
width.value = 'auto'
isSticky.value = false
}
</script>
<template>
<div :style="{ height: height, zIndex: zIndex }" ref="refSticky">
<div
:class="className"
:style="{
top: position === 'top' ? offset + 'px' : '',
bottom: position !== 'top' ? offset + 'px' : '',
zIndex: zIndex,
position: isSticky ? 'fixed' : 'static',
width: width,
height: height
}"
>
<slot>
<div>sticky</div>
</slot>
</div>
</div>
</template>

View File

@ -22,7 +22,7 @@ const getCaches = computed((): string[] => {
<template> <template>
<section <section
:class="[ :class="[
'p-[var(--app-content-padding)] w-[100%] bg-[var(--app-contnet-bg-color)]', 'w-[100%] bg-[var(--app-contnet-bg-color)]',
{ {
'!min-h-[calc(100%-var(--app-footer-height))]': '!min-h-[calc(100%-var(--app-footer-height))]':
fixedHeader && (layout === 'classic' || layout === 'topLeft') && footer, fixedHeader && (layout === 'classic' || layout === 'topLeft') && footer,

View File

@ -9,7 +9,8 @@ export default {
loginOut: 'Login out', loginOut: 'Login out',
document: 'Document', document: 'Document',
reminder: 'Reminder', reminder: 'Reminder',
loginOutMessage: 'Exit the system', loginOutMessage: 'Exit the system?',
back: 'Back',
ok: 'OK', ok: 'OK',
cancel: 'Cancel', cancel: 'Cancel',
reload: 'Reload current', reload: 'Reload current',
@ -132,7 +133,8 @@ export default {
user: 'User management', user: 'User management',
role: 'Role management', role: 'Role management',
document: 'Document', document: 'Document',
inputPassword: 'InputPassword' inputPassword: 'InputPassword',
sticky: 'Sticky'
}, },
analysis: { analysis: {
newUser: 'New user', newUser: 'New user',
@ -334,6 +336,9 @@ export default {
center: 'center', center: 'center',
right: 'right' right: 'right'
}, },
stickyDemo: {
sticky: 'Sticky'
},
tableDemo: { tableDemo: {
table: 'Table', table: 'Table',
tableDes: 'Secondary packaging of Table components based on ElementPlus', tableDes: 'Secondary packaging of Table components based on ElementPlus',

View File

@ -10,6 +10,7 @@ export default {
document: '项目文档', document: '项目文档',
reminder: '温馨提示', reminder: '温馨提示',
loginOutMessage: '是否退出本系统?', loginOutMessage: '是否退出本系统?',
back: '返回',
ok: '确定', ok: '确定',
cancel: '取消', cancel: '取消',
reload: '重新加载', reload: '重新加载',
@ -132,7 +133,8 @@ export default {
user: '用户管理', user: '用户管理',
role: '角色管理', role: '角色管理',
document: '文档', document: '文档',
inputPassword: '密码输入框' inputPassword: '密码输入框',
sticky: '黏性'
}, },
analysis: { analysis: {
newUser: '新增用户', newUser: '新增用户',
@ -331,6 +333,9 @@ export default {
center: '中', center: '中',
right: '右' right: '右'
}, },
stickyDemo: {
sticky: '黏性'
},
tableDemo: { tableDemo: {
table: '表格', table: '表格',
tableDes: '基于 ElementPlus 的 Table 组件二次封装', tableDes: '基于 ElementPlus 的 Table 组件二次封装',

View File

@ -308,6 +308,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: { meta: {
title: t('router.inputPassword') title: t('router.inputPassword')
} }
},
{
path: 'sticky',
component: () => import('@/views/Components/Sticky.vue'),
name: 'Sticky',
meta: {
title: t('router.sticky')
}
} }
] ]
}, },

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Sticky } from '@/components/Sticky'
import { ElAffix } from 'element-plus'
const { t } = useI18n()
</script>
<template>
<ContentWrap :title="t('stickyDemo.sticky')">
<Sticky :offset="90">
<div style="padding: 10px; background-color: lightblue"> Sticky 距离顶部90px </div>
</Sticky>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<el-affix :offset="150">
<div style="padding: 10px; background-color: lightblue">Affix 距离顶部150px </div>
</el-affix>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<el-affix :offset="150" position="bottom">
<div style="padding: 10px; background-color: lightblue">Affix 距离底部150px </div>
</el-affix>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
<Sticky :offset="90" position="bottom">
<div style="padding: 10px; background-color: lightblue"> Sticky 距离底部90px </div>
</Sticky>
<p style="margin: 80px">Content</p>
<p style="margin: 80px">Content</p>
</ContentWrap>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Write from './components/Write.vue' import Write from './components/Write.vue'
import { ContentWrap } from '@/components/ContentWrap' import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref, unref } from 'vue' import { ref, unref } from 'vue'
import { ElButton } from 'element-plus' import { ElButton } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
@ -42,14 +42,13 @@ const save = async () => {
</script> </script>
<template> <template>
<ContentWrap :title="t('exampleDemo.add')"> <ContentDetailWrap :title="t('exampleDemo.add')" @back="push('/example/example-page')">
<Write ref="writeRef" /> <Write ref="writeRef" />
<div class="text-center"> <template #right>
<ElButton type="primary" :loading="loading" @click="save"> <ElButton type="primary" :loading="loading" @click="save">
{{ t('exampleDemo.save') }} {{ t('exampleDemo.save') }}
</ElButton> </ElButton>
<ElButton @click="push('/example/example-page')">{{ t('dialogDemo.close') }}</ElButton> </template>
</div> </ContentDetailWrap>
</ContentWrap>
</template> </template>

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Detail from './components/Detail.vue' import Detail from './components/Detail.vue'
import { ContentWrap } from '@/components/ContentWrap' import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref } from 'vue' import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { getTableDetApi } from '@/api/table' import { getTableDetApi } from '@/api/table'
@ -31,11 +30,7 @@ getTableDet()
</script> </script>
<template> <template>
<ContentWrap :title="t('exampleDemo.detail')"> <ContentDetailWrap :title="t('exampleDemo.detail')" @back="push('/example/example-page')">
<Detail :current-row="currentRow" /> <Detail :current-row="currentRow" />
</ContentDetailWrap>
<div class="text-center">
<ElButton @click="push('/example/example-page')">{{ t('dialogDemo.close') }}</ElButton>
</div>
</ContentWrap>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Write from './components/Write.vue' import Write from './components/Write.vue'
import { ContentWrap } from '@/components/ContentWrap' import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import { ref, unref } from 'vue' import { ref, unref } from 'vue'
import { ElButton } from 'element-plus' import { ElButton } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
@ -58,14 +58,13 @@ const save = async () => {
</script> </script>
<template> <template>
<ContentWrap :title="t('exampleDemo.edit')"> <ContentDetailWrap :title="t('exampleDemo.edit')" @back="push('/example/example-page')">
<Write ref="writeRef" :current-row="currentRow" /> <Write ref="writeRef" :current-row="currentRow" />
<div class="text-center"> <template #right>
<ElButton type="primary" :loading="loading" @click="save"> <ElButton type="primary" :loading="loading" @click="save">
{{ t('exampleDemo.save') }} {{ t('exampleDemo.save') }}
</ElButton> </ElButton>
<ElButton @click="push('/example/example-page')">{{ t('dialogDemo.close') }}</ElButton> </template>
</div> </ContentDetailWrap>
</ContentWrap>
</template> </template>