瀏覽代碼

Merge pull request #26 from huanghong1125/sticky

add  Sticky like Affix
Archer 3 年之前
父節點
當前提交
326bba8002

+ 9 - 0
mock/role/index.ts

@@ -257,6 +257,14 @@ const adminList = [
         meta: {
           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/infotip',
   '/Components/InputPassword',
+  '/Components/Sticky',
   '/hooks',
   '/hooks/useWatermark',
   '/level',

+ 3 - 0
src/components/ContentDetailWrap/index.ts

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

+ 59 - 0
src/components/ContentDetailWrap/src/ContentDetailWrap.vue

@@ -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>

+ 17 - 15
src/components/ContentWrap/src/ContentWrap.vue

@@ -14,20 +14,22 @@ defineProps({
 </script>
 
 <template>
-  <ElCard :class="[prefixCls, 'mb-20px']" shadow="never">
-    <template v-if="title" #header>
-      <div class="flex items-center">
-        <span class="text-16px font-700">{{ title }}</span>
-        <ElTooltip v-if="message" effect="dark" placement="right">
-          <template #content>
-            <div class="max-w-200px">{{ message }}</div>
-          </template>
-          <Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" />
-        </ElTooltip>
+  <div style="padding: var(--app-content-padding)">
+    <ElCard :class="[prefixCls, 'mb-20px']" shadow="never">
+      <template v-if="title" #header>
+        <div class="flex items-center">
+          <span class="text-16px font-700">{{ title }}</span>
+          <ElTooltip v-if="message" effect="dark" placement="right">
+            <template #content>
+              <div class="max-w-200px">{{ message }}</div>
+            </template>
+            <Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" />
+          </ElTooltip>
+        </div>
+      </template>
+      <div>
+        <slot></slot>
       </div>
-    </template>
-    <div>
-      <slot></slot>
-    </div>
-  </ElCard>
+    </ElCard>
+  </div>
 </template>

+ 3 - 0
src/components/Sticky/index.ts

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

+ 141 - 0
src/components/Sticky/src/Sticky.vue

@@ -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),表示距离顶部位置,可以设置为top或者bottom
+  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>

+ 1 - 1
src/layout/components/AppView.vue

@@ -22,7 +22,7 @@ const getCaches = computed((): string[] => {
 <template>
   <section
     :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))]':
           fixedHeader && (layout === 'classic' || layout === 'topLeft') && footer,

+ 7 - 2
src/locales/en.ts

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

+ 6 - 1
src/locales/zh-CN.ts

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

+ 8 - 0
src/router/index.ts

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

+ 62 - 0
src/views/Components/Sticky.vue

@@ -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>

+ 5 - 6
src/views/Example/Page/ExampleAdd.vue

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

+ 3 - 8
src/views/Example/Page/ExampleDetail.vue

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

+ 5 - 6
src/views/Example/Page/ExampleEdit.vue

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