refactor: ♻️ 索引栏组件兼容钉钉小程序

This commit is contained in:
xuqingkai 2024-05-18 16:01:59 +08:00
parent f84e9affb1
commit 0d18b39ff6
8 changed files with 218 additions and 86 deletions

View File

@ -8,11 +8,11 @@
使用一个固定高度的元素包裹`wd-index-bar`组件,组件的宽高会和包裹元素相同。
`wd-index-anchor`作为子组件使用,会根据anchor组件的`index`属性生成锚点以及侧边栏。
`wd-index-anchor`作为子组件使用,会根据 anchor 组件的`index`属性生成锚点以及侧边栏。
```vue
<template>
<view class="wrap" :style="{ height: wrapHeight + 'px' }">
<view class="wraper">
<wd-index-bar>
<view v-for="item in data" :key="item.index" class="city-wrap">
<wd-index-anchor :index="item.index" />
@ -22,50 +22,96 @@
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onMounted } from 'vue';
const wrapHeight = ref(400)
onMounted(() => {
const info = uni.getWindowInfo()
wrapHeight.value = info.windowHeight
})
import { ref } from 'vue'
import { onMounted } from 'vue'
const data = ref([
{
index: 'A',
data: ["阿坝", "阿拉善", "阿里", "安康", "安庆", "鞍山", "安顺", "安阳", "澳门",]
data: ['阿坝', '阿拉善', '阿里', '安康', '安庆', '鞍山', '安顺', '安阳', '澳门']
},
{
index: 'B',
data: ["北京", "白银", "保定", "宝鸡", "保山", "包头", "巴中", "北海", "蚌埠", "本溪", "毕节", "滨州", "百色", "亳州"]
data: ['北京', '白银', '保定', '宝鸡', '保山', '包头', '巴中', '北海', '蚌埠', '本溪', '毕节', '滨州', '百色', '亳州']
},
{
index: 'C',
data: ["重庆", "成都", "长沙", "长春", "沧州", "常德", "昌都", "长治", "常州", "巢湖", "潮州", "承德", "郴州", "赤峰", "池州", "崇左", "楚雄", "滁州", "朝阳"]
data: [
'重庆',
'成都',
'长沙',
'长春',
'沧州',
'常德',
'昌都',
'长治',
'常州',
'巢湖',
'潮州',
'承德',
'郴州',
'赤峰',
'池州',
'崇左',
'楚雄',
'滁州',
'朝阳'
]
},
{
index: 'D',
data: ["大连", "东莞", "大理", "丹东", "大庆", "大同", "大兴安岭", "德宏", "德阳", "德州", "定西", "迪庆", "东营"]
data: ['大连', '东莞', '大理', '丹东', '大庆', '大同', '大兴安岭', '德宏', '德阳', '德州', '定西', '迪庆', '东营']
},
{
index: 'E',
data: ["鄂尔多斯", "恩施", "鄂州"]
data: ['鄂尔多斯', '恩施', '鄂州']
},
{
index: 'F',
data: ["福州", "防城港", "佛山", "抚顺", "抚州", "阜新", "阜阳"]
data: ['福州', '防城港', '佛山', '抚顺', '抚州', '阜新', '阜阳']
},
{
index: 'G',
data: ["广州", "桂林", "贵阳", "甘南", "赣州", "甘孜", "广安", "广元", "贵港", "果洛"]
data: ['广州', '桂林', '贵阳', '甘南', '赣州', '甘孜', '广安', '广元', '贵港', '果洛']
},
{
index: 'H',
data: ["杭州", "哈尔滨", "合肥", "海口", "呼和浩特", "海北", "海东", "海南", "海西", "邯郸", "汉中", "鹤壁", "河池", "鹤岗", "黑河", "衡水", "衡阳", "河源", "贺州", "红河", "淮安", "淮北", "怀化", "淮南", "黄冈", "黄南", "黄山", "黄石", "惠州", "葫芦岛", "呼伦贝尔", "湖州", "菏泽"]
data: [
'杭州',
'哈尔滨',
'合肥',
'海口',
'呼和浩特',
'海北',
'海东',
'海南',
'海西',
'邯郸',
'汉中',
'鹤壁',
'河池',
'鹤岗',
'黑河',
'衡水',
'衡阳',
'河源',
'贺州',
'红河',
'淮安',
'淮北',
'怀化',
'淮南',
'黄冈',
'黄南',
'黄山',
'黄石',
'惠州',
'葫芦岛',
'呼伦贝尔',
'湖州',
'菏泽'
]
}
])
</script>
@ -78,10 +124,13 @@ const data = ref([
border-bottom: 1px solid #ddd;
}
.city-wrap .city:last-child {
margin-bottom: 10px;
.wraper {
height: calc(100vh - var(--window-top));
height: calc(100vh - var(--window-top) - constant(safe-area-inset-bottom));
height: calc(100vh - var(--window-top) - env(safe-area-inset-bottom));
}
.wot-theme-dark {
.city {
color: white;
@ -91,6 +140,12 @@ const data = ref([
</style>
```
## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
| ------ | ------------ | ------- | ------ | ------ | -------- |
| sticky | 索引是否吸顶 | boolean | - | false | - |
## IndexAnchor Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |

View File

@ -1,7 +1,7 @@
<template>
<wd-config-provider :theme="theme" :theme-vars="isRed ? themeVars : {}">
<wd-toast />
<view :class="['page-wraper', safeAreaInsetBottom ? 'is-safe' : '']">
<view class="page-wraper">
<wd-cell title="切换暗黑" title-width="240px" center v-if="showDarkMode">
<wd-switch v-model="isDark" />
</wd-cell>
@ -9,9 +9,9 @@
<wd-switch v-model="isRed" />
</wd-cell>
<slot />
<wd-gap height="0" v-if="safeAreaInsetBottom" safe-area-bottom></wd-gap>
</view>
<wd-notify />
<wd-gap height="0" safe-area-bottom></wd-gap>
</wd-config-provider>
</template>
<script lang="ts">
@ -67,10 +67,5 @@ onMounted(() => {
.page-wraper {
min-height: calc(100vh - var(--window-top));
box-sizing: border-box;
.is-safe {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<page-wraper>
<view class="wrap" :style="{ height: wrapHeight + 'px' }">
<wd-index-bar>
<view class="wraper">
<wd-index-bar sticky>
<view v-for="item in data" :key="item.index" class="city-wrap">
<wd-index-anchor :index="item.index" />
<view v-for="city in item.data" class="city" :key="city">{{ city }}</view>
<view v-for="city in item.data" class="city" :key="city" @click="handleClick(item.index, city)">{{ city }}</view>
</view>
</wd-index-bar>
</view>
@ -12,15 +12,9 @@
</template>
<script lang="ts" setup>
import { useToast } from '@/uni_modules/wot-design-uni'
import { ref } from 'vue'
import { onMounted } from 'vue'
const wrapHeight = ref(400)
onMounted(() => {
const info = uni.getWindowInfo()
wrapHeight.value = info.windowHeight
})
const { show: showToast } = useToast()
const data = ref([
{
@ -110,6 +104,10 @@ const data = ref([
]
}
])
function handleClick(index: string, city: string) {
showToast(`当前点击项:${index},城市:${city}`)
}
</script>
<style lang="scss">
@ -120,8 +118,10 @@ const data = ref([
border-bottom: 1px solid #ddd;
}
.city-wrap .city:last-child {
margin-bottom: 10px;
.wraper {
height: calc(100vh - var(--window-top));
height: calc(100vh - var(--window-top) - constant(safe-area-inset-bottom));
height: calc(100vh - var(--window-top) - env(safe-area-inset-bottom));
}
.wot-theme-dark {

View File

@ -123,7 +123,7 @@ import { useCell } from '../composables/useCell'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
import { useParent } from '../composables/useParent'
import { useTranslate } from '../composables/useTranslate'
import { calendarProps, CalendarExpose } from './types'
import { calendarProps, type CalendarExpose } from './types'
import type { CalendarType } from '../wd-calendar-view/types'
const { translate } = useTranslate('calendar')

View File

@ -8,9 +8,42 @@
}
}
// #ifdef MP-DINGTALK
@include b(index-anchor-ding) {
@include when(sticky){
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
}
// #endif
@include b(index-anchor) {
background-color: $-color-gray-3;
padding: 10px;
font-size: 14px;
color: $-color-title;
@include when(sticky){
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
}
@include b(index-anchor) {
background-color: $-color-gray-3;
padding: 10px;
font-size: 14px;
color: $-color-title;
@include when(sticky){
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
}

View File

@ -1,32 +1,49 @@
<template>
<view class="wd-index-anchor" :class="customClass" :style="customStyle">
<slot>
{{ index }}
</slot>
<!-- #ifdef MP-DINGTALK -->
<view :class="`wd-index-anchor-ding ${indexBar && indexBar.props.sticky && indexBar.anchorState.activeIndex === index ? 'is-sticky' : ''}`">
<!-- #endif -->
<view
:class="`wd-index-anchor ${indexBar && indexBar.props.sticky && indexBar.anchorState.activeIndex === index ? 'is-sticky' : ''} ${customClass}`"
:style="customStyle"
:id="indexAnchorId"
>
<slot>
{{ index }}
</slot>
</view>
<!-- #ifdef MP-DINGTALK -->
</view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { indexAnchorProps } from './type'
import { onMounted, getCurrentInstance, inject } from 'vue'
import { onMounted, getCurrentInstance, ref } from 'vue'
import { indexBarInjectionKey } from '../wd-index-bar/type'
import { getRect } from '../common/util'
import { getRect, isDef, uuid } from '../common/util'
import { useParent } from '../composables/useParent'
const props = defineProps(indexAnchorProps)
const indexBar = inject(indexBarInjectionKey)!
const { parent: indexBar } = useParent(indexBarInjectionKey)
const indexAnchorId = ref<string>(`indexBar${uuid()}`)
const { proxy } = getCurrentInstance()!
function getInfo() {
getRect('.wd-index-anchor', false, proxy).then((res) => {
const anchor = indexBar.anchorList.value.find((v) => v.index === props.index)!
anchor.top = res.top!
getRect(`#${indexAnchorId.value}`, false, proxy).then((res) => {
if (isDef(indexBar)) {
const anchor = indexBar.anchorState.anchorList.find((v) => v.index === props.index)!
anchor.top = res.top!
}
})
}
onMounted(() => {
indexBar.anchorList.value.push({ top: 0, index: props.index })
if (isDef(indexBar)) {
indexBar.anchorState.anchorList.push({ top: 0, index: props.index })
}
getInfo()
})
</script>

View File

@ -1,6 +1,7 @@
import type { Ref } from 'vue'
import type { InjectionKey } from 'vue'
import type { ExtractPropTypes } from 'vue'
import { makeBooleanProp } from '../common/props'
export type AnchorIndex = number | string
@ -9,10 +10,21 @@ export interface AnchorItem {
index: AnchorIndex
}
export const indexBarProps = {}
export const indexBarProps = {
/**
* @description
*/
sticky: makeBooleanProp(false)
}
export type IndexBarProps = ExtractPropTypes<typeof indexBarProps>
export const indexBarInjectionKey = 'wdIndexBar' as unknown as InjectionKey<{
anchorList: Ref<AnchorItem[]>
}>
export type InderBarProvide = {
props: { sticky?: boolean }
anchorState: {
anchorList: AnchorItem[] // 锚点列表
activeIndex: AnchorIndex | null // 当前激活的索引
}
}
export const indexBarInjectionKey: InjectionKey<InderBarProvide> = Symbol('wd-index-bar')

View File

@ -1,34 +1,55 @@
<template>
<view class="wd-index-bar">
<scroll-view :scrollTop="scrollTop" :scroll-y="true" class="wd-index-bar__content" @scroll="hanleScroll">
<slot></slot>
</scroll-view>
<view
class="wd-index-bar__sidebar"
@touchmove.stop.prevent="handleTouchMove"
@touchend.stop.prevent="handleTouchEnd"
@touchcancel.stop.prevent="handleTouchEnd"
>
<view class="wd-index-bar__index" :class="{ 'is-active': item.index === currentAnchorIndex }" v-for="item in anchorList" :key="item.index">
{{ item.index }}
<view class="wd-index-bar" :id="indexBarId">
<!-- #ifdef MP-DINGTALK -->
<view class="wd-index-bar" :id="indexBarId">
<!-- #endif -->
<scroll-view :scrollTop="scrollTop" :scroll-y="true" class="wd-index-bar__content" @scroll="hanleScroll">
<slot></slot>
</scroll-view>
<view
class="wd-index-bar__sidebar"
@touchmove.stop.prevent="handleTouchMove"
@touchend.stop.prevent="handleTouchEnd"
@touchcancel.stop.prevent="handleTouchEnd"
>
<view
class="wd-index-bar__index"
:class="{ 'is-active': item.index === state.activeIndex }"
v-for="item in state.anchorList"
:key="item.index"
>
{{ item.index }}
</view>
</view>
<!-- #ifdef MP-DINGTALK -->
</view>
<!-- #endif -->
</view>
</template>
<script setup lang="ts">
import type { AnchorItem, AnchorIndex } from './type'
import { indexBarInjectionKey } from './type'
import { provide, ref, getCurrentInstance, onMounted } from 'vue'
import { getRect } from '../common/util'
import { indexBarInjectionKey, indexBarProps } from './type'
import { ref, getCurrentInstance, onMounted, reactive } from 'vue'
import { getRect, isDef, uuid } from '../common/util'
import { useChildren } from '../composables/useChildren'
const props = defineProps(indexBarProps)
const indexBarId = ref<string>(`indexBar${uuid()}`)
const { proxy } = getCurrentInstance()!
const anchorList = ref<AnchorItem[]>([])
provide(indexBarInjectionKey, { anchorList })
const state = reactive({
activeIndex: null as AnchorIndex | null,
anchorList: [] as AnchorItem[]
})
const { linkChildren } = useChildren(indexBarInjectionKey)
linkChildren({ props, anchorState: state })
const scrollTop = ref(0)
const currentAnchorIndex = ref<AnchorIndex | null>()
//
let offsetTop = 0
@ -41,10 +62,10 @@ let sidebarInfo = {
function init() {
setTimeout(() => {
currentAnchorIndex.value = anchorList.value[0]?.index
state.activeIndex = state.anchorList[0]?.index
Promise.all([
getRect('.wd-index-bar', false, proxy),
getRect(`#${indexBarId.value}`, false, proxy),
getRect('.wd-index-bar__sidebar', false, proxy),
getRect('.wd-index-bar__index', false, proxy)
]).then(([bar, sidebar, index]) => {
@ -61,26 +82,25 @@ onMounted(() => {
function hanleScroll(scrollEvent: any) {
const { detail } = scrollEvent
scrollTop.value = detail.scrollTop
const anchor = anchorList.value.find((item, index) => {
if (anchorList.value[index + 1] == null) return true
if (item.top - offsetTop <= scrollTop.value && anchorList.value[index + 1].top - offsetTop > scrollTop.value) return true
const anchor = state.anchorList.find((item, index) => {
if (!isDef(state.anchorList[index + 1])) return true
if (item.top - offsetTop <= detail.scrollTop && state.anchorList[index + 1].top - offsetTop > detail.scrollTop) return true
return false
})!
currentAnchorIndex.value = anchor.index
state.activeIndex = anchor.index
}
function getAnchorByPageY(pageY: number) {
const y = pageY - sidebarInfo.offsetTop
let idx = Math.floor(y / sidebarInfo.indexHeight)
if (idx < 0) idx = 0
else if (idx > anchorList.value.length - 1) idx = anchorList.value.length - 1
return anchorList.value[idx]
else if (idx > state.anchorList.length - 1) idx = state.anchorList.length - 1
return state.anchorList[idx]
}
function handleTouchMove(e: TouchEvent) {
const clientY = e.touches[0].pageY
currentAnchorIndex.value = getAnchorByPageY(clientY).index
state.activeIndex = getAnchorByPageY(clientY).index
}
function handleTouchEnd(e: TouchEvent) {