713 行
22 KiB
Plaintext
713 行
22 KiB
Plaintext
<template>
|
||
<view class="container" id="container">
|
||
<list-view ref="listRef" class="list" @scroll="onListScroll" @scrollend="onScrollEnd" :scroll-with-animation="true">
|
||
<list-item v-for="(item, index) in items" :key="item.id" class="item-wrapper"
|
||
:style="{ backgroundColor: item.bgColor, transform: 'translateY(' + itemMetas[index].translateY + 'px)', transition: itemMetas[index].wrapperTransition, zIndex: itemMetas[index].zIndex }"
|
||
@touchstart="(e: TouchEvent) => onTouchStart(e, index)" @touchmove="onTouchMove" @touchend="onTouchEnd"
|
||
@touchcancel="onTouchEnd" @longpress="onLongPress">
|
||
<!-- 多选模式下显示 checkbox -->
|
||
<checkbox v-if="isMultiSelectMode" style="margin-left: 10px;" :checked="selectedIds.includes(item.id)"
|
||
@click="toggleSelect(item.id)" />
|
||
<view class="item-content"
|
||
:style="{ height: item.height + 'px', transform: 'translateX(' + itemMetas[index].translateX + 'px)', transition: itemMetas[index].contentTransition }"
|
||
@transitionend="() => onDeleteItemTransitionEnd(index)">
|
||
<text class="item-text">试试左滑和长按 {{ item.id + 1 }}</text>
|
||
<text class="item-info">高度: {{ item.height }}px</text>
|
||
<view class="item-sub-items">
|
||
<view v-for="subItem in item.subItems" :key="subItem.index" :style="{ backgroundColor: subItem.bgColor }">
|
||
<text class="item-info">{{ subItem.index + 1 }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</list-item>
|
||
</list-view>
|
||
<!-- 全局侧滑菜单(始终渲染,通过 visibility 控制显示) -->
|
||
<view class="swipe-menu"
|
||
:style="{ transform: menuTransform, transition: menuTransition, height: menuHeight + 'px' }">
|
||
<view class="menu-item delete-btn" @click="onDeleteClick">
|
||
<text class="menu-text">删除</text>
|
||
</view>
|
||
</view>
|
||
<!-- 多选模式底部工具栏 -->
|
||
<view v-if="isMultiSelectMode" class="bottom-toolbar">
|
||
<view class="toolbar">
|
||
<checkbox :checked="isAllSelected" @click="toggleSelectAll">
|
||
<text>{{ isAllSelected ? '取消全选' : '全选' }}</text>
|
||
</checkbox>
|
||
<text style="margin: 0px 10px;">已选 {{ selectedIds.length }} 项</text>
|
||
<button style="margin: 0px 10px;" @click="exitMultiSelectMode">取消</button>
|
||
<button type="warn" :disabled="selectedIds.length == 0" @click="onBatchDelete">删除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
/**
|
||
* @description: 支持“左滑删除”与“多选批量删除”的列表。侧滑和删除都支持动画
|
||
* @logic: 用不等高、背景色不同的列表项,可以看到侧滑菜单和删除动画覆盖在不同列表项上的效果。长按进入多选模式,支持全选和批量删除。
|
||
* @optimization: 全局只有一个侧滑菜单,在拖动列表项时动态改变侧滑菜单的translateY和Height以覆盖在列表项上。这样避免每个列表项一个侧滑菜单,减少大量的view创建
|
||
*/
|
||
// 定义列表项数据类型
|
||
type SubItemData = {
|
||
index : number,
|
||
bgColor : string
|
||
}
|
||
type ListItemData = {
|
||
id : number,
|
||
height : number,
|
||
bgColor : string,
|
||
subItems : SubItemData[]
|
||
}
|
||
|
||
type ListItemMeta = {
|
||
translateX : number,
|
||
translateY : number,
|
||
zIndex : number,
|
||
wrapperTransition : string,
|
||
contentTransition : string
|
||
}
|
||
|
||
const createListItemMeta = () : ListItemMeta => {
|
||
const meta : ListItemMeta = {
|
||
translateX: 0,
|
||
translateY: 0,
|
||
zIndex: 0,
|
||
wrapperTransition: '',
|
||
contentTransition: ''
|
||
}
|
||
return meta
|
||
}
|
||
|
||
// 10个主色的基础RGB值(深色版本)
|
||
// 红、橙、黄、绿、青、蓝、紫、粉、棕、灰
|
||
const baseColors : number[][] = [
|
||
[180, 40, 40], // 红色
|
||
[200, 120, 40], // 橙色
|
||
[180, 160, 40], // 黄色
|
||
[40, 140, 60], // 绿色
|
||
[40, 140, 160], // 青色
|
||
[40, 80, 160], // 蓝色
|
||
[120, 60, 160], // 紫色
|
||
[180, 80, 120], // 粉色
|
||
[140, 90, 60], // 棕色
|
||
[100, 100, 100] // 灰色
|
||
]
|
||
|
||
// 生成颜色:根据主色组和组内索引生成从深到浅的颜色
|
||
// 通过与白色混合实现深浅变化
|
||
const generateColor = (groupIndex : number, itemInGroup : number) : string => {
|
||
const base = baseColors[groupIndex]
|
||
// 混合因子从 0(最深)到 0.6(较浅),每个增加 0.067
|
||
const factor = itemInGroup * 0.067
|
||
// 与白色混合:result = base + (255 - base) * factor
|
||
const r = Math.floor(base[0] + (255 - base[0]) * factor)
|
||
const g = Math.floor(base[1] + (255 - base[1]) * factor)
|
||
const b = Math.floor(base[2] + (255 - base[2]) * factor)
|
||
return `rgba(${r}, ${g}, ${b}, 1)`
|
||
}
|
||
|
||
// 生成 100 个列表项数据
|
||
const generateItems = () : ListItemData[] => {
|
||
const result : ListItemData[] = []
|
||
for (let i = 0; i < 100; i++) {
|
||
const groupIndex = Math.floor(i / 10) // 0-9,共10组
|
||
const itemInGroup = i % 10 // 0-9,组内索引
|
||
const height = 60 + i * 2 // 第一个 60px,依次增加 2px
|
||
const bgColor = generateColor(groupIndex, itemInGroup)
|
||
const subItems : SubItemData[] = []
|
||
for (let j = 0; j < itemInGroup; j++) {
|
||
subItems.push({
|
||
index: j,
|
||
bgColor: generateColor(groupIndex, j)
|
||
})
|
||
}
|
||
const item : ListItemData = {
|
||
id: i,
|
||
height: height,
|
||
bgColor: bgColor,
|
||
subItems: subItems
|
||
}
|
||
result.push(item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
const items = ref<ListItemData[]>(generateItems())
|
||
const itemMetas = ref<ListItemMeta[]>(items.value.map((item) : ListItemMeta => createListItemMeta()))
|
||
const listRef = ref<UniElement | null>(null)
|
||
|
||
// 侧滑相关状态
|
||
const MENU_WIDTH = 80 // 菜单宽度
|
||
const MIN_ITEM_HEIGHT = 60 // 最小列表项高度,用于估算可见区域
|
||
|
||
// 删除动画相关变量
|
||
let deleteAnimationTargetBelow = 0
|
||
let deleteAnimationTargetAbove = 0
|
||
let deleteAnimationTargetHorizontal = 0
|
||
let deleteAnimationIndexToDelete = -1
|
||
const DELETE_ANIMATION_DURATION = 280
|
||
const DELETE_ANIMATION_TRANSITION = 'transform ' + DELETE_ANIMATION_DURATION + 'ms ease-out'
|
||
|
||
// 滚动状态
|
||
let isScrolling = false
|
||
let scrollEndTimer : number | null = null
|
||
const SCROLL_END_DELAY = 150 // 滚动结束判定延迟(ms)
|
||
|
||
// 触摸状态
|
||
let startX = 0
|
||
let startY = 0
|
||
let currentIndex = -1
|
||
let isSwiping = false
|
||
let swipeStarted = false
|
||
let currentListItemElement : UniElement | null = null
|
||
|
||
// 菜单状态
|
||
const menuTop = ref(0)
|
||
const menuHeight = ref(0)
|
||
const menuVisible = ref(false)
|
||
const menuShowWidth = ref(0) // 菜单显示宽度,用于同步滑出效果
|
||
const menuTransition = ref('')
|
||
const activeIndex = ref(-1)
|
||
|
||
// 删除动画状态
|
||
let isDeleting = false
|
||
let deletingIndex = -1 // 正在删除的索引
|
||
let deletedItemHeight = 0 // 被删除项的高度
|
||
|
||
// 多选模式状态
|
||
const isMultiSelectMode = ref(false)
|
||
const selectedIds = ref<number[]>([])
|
||
|
||
// 计算是否全选
|
||
const isAllSelected = computed(() : boolean => {
|
||
return items.value.length > 0 && selectedIds.value.length == items.value.length
|
||
})
|
||
|
||
let visibleStartIndex = 0
|
||
let visibleEndIndex = -1
|
||
const VISIBLE_RANGE_BUFFER = 2
|
||
|
||
function resetItemMeta(index : number) {
|
||
if (index < 0 || index >= itemMetas.value.length) return
|
||
const meta = itemMetas.value[index]
|
||
meta.translateX = 0
|
||
meta.translateY = 0
|
||
meta.zIndex = 0
|
||
meta.wrapperTransition = 'none'
|
||
meta.contentTransition = 'none'
|
||
}
|
||
|
||
function resetAllItemMetas() {
|
||
for (let i = 0; i < itemMetas.value.length; i++) {
|
||
resetItemMeta(i)
|
||
}
|
||
}
|
||
|
||
const applyDeleteAnimationMetas = () => {
|
||
if (deletingIndex == -1) return
|
||
const start = Math.max(0, visibleStartIndex - VISIBLE_RANGE_BUFFER)
|
||
const end = Math.min(itemMetas.value.length - 1, visibleEndIndex + VISIBLE_RANGE_BUFFER)
|
||
for (let i = start; i <= end; i++) {
|
||
const meta = itemMetas.value[i]
|
||
if (i > deletingIndex) {
|
||
// #ifndef APP-ANDROID && VUE3-VAPOR
|
||
meta.wrapperTransition = DELETE_ANIMATION_TRANSITION
|
||
meta.translateY = -deleteAnimationTargetBelow
|
||
// #endif
|
||
meta.zIndex = 1
|
||
} else if (i < deletingIndex) {
|
||
// #ifndef APP-ANDROID && VUE3-VAPOR
|
||
meta.wrapperTransition = DELETE_ANIMATION_TRANSITION
|
||
meta.translateY = deleteAnimationTargetAbove
|
||
// #endif
|
||
meta.zIndex = 1
|
||
} else {
|
||
meta.contentTransition = DELETE_ANIMATION_TRANSITION
|
||
meta.translateX = deleteAnimationTargetHorizontal
|
||
meta.zIndex = 0
|
||
}
|
||
}
|
||
// #ifdef APP-ANDROID && VUE3-VAPOR
|
||
const sortedListItems = Array.from(listRef.value!.querySelectorAll('.item-wrapper'))
|
||
sortedListItems.sort((a, b) => {
|
||
return a.getBoundingClientRect().top - b.getBoundingClientRect().top
|
||
})
|
||
const currentListItemRenderIndex = sortedListItems.findIndex(el => el === currentListItemElement)
|
||
if (currentListItemRenderIndex === -1) {
|
||
return
|
||
}
|
||
for (let i = 0; i < sortedListItems.length; i++) {
|
||
if (i > currentListItemRenderIndex) {
|
||
sortedListItems[i].style.setProperty('transition', DELETE_ANIMATION_TRANSITION)
|
||
sortedListItems[i].style.setProperty('transform', `translateY(${-deleteAnimationTargetBelow}px)`)
|
||
} else if (i < currentListItemRenderIndex) {
|
||
sortedListItems[i].style.setProperty('transition', DELETE_ANIMATION_TRANSITION)
|
||
sortedListItems[i].style.setProperty('transform', `translateY(${deleteAnimationTargetAbove}px)`)
|
||
}
|
||
}
|
||
// #endif
|
||
}
|
||
|
||
let containerTop = 0
|
||
// #ifdef WEB
|
||
// swipe-menu未使用fixed定位,而是相对于父元素(container)定位。按照现有布局仅web平台需要额外计算。如果后续调整布局,也需要同步的调整此处逻辑
|
||
onMounted(() => {
|
||
// 获取 安全区高度
|
||
const containerEle = uni.getElementById('container')
|
||
const containerRect = containerEle?.getBoundingClientRect()
|
||
if (containerRect != null) {
|
||
containerTop = containerRect.top
|
||
}
|
||
})
|
||
// #endif
|
||
|
||
// 计算菜单的 transform
|
||
const menuTransform = computed(() : string => {
|
||
return `translateY(${menuTop.value}px) translateX(${-menuShowWidth.value}px)`
|
||
})
|
||
|
||
// 关闭菜单(需要在调用之前定义)
|
||
const closeMenu = () => {
|
||
if (activeIndex.value != -1) {
|
||
resetItemMeta(activeIndex.value)
|
||
}
|
||
menuVisible.value = false
|
||
menuShowWidth.value = 0
|
||
menuTransition.value = 'none'
|
||
activeIndex.value = -1
|
||
currentListItemElement = null
|
||
}
|
||
|
||
// 更新菜单位置(需要在调用之前定义)
|
||
const updateMenuPosition = () => {
|
||
if (currentListItemElement == null) return
|
||
if (currentIndex == -1 || currentIndex >= items.value.length) return
|
||
|
||
if (!menuVisible.value) {
|
||
// 获取列表项的位置,如下值在一次menu显示期间仅需更新一次
|
||
menuVisible.value = true
|
||
const rect = currentListItemElement!.getBoundingClientRect()
|
||
const item = items.value[currentIndex]
|
||
menuTop.value = rect.top - containerTop
|
||
menuHeight.value = item.height
|
||
}
|
||
}
|
||
|
||
// 长按事件 - 进入多选模式
|
||
const onLongPress = () => {
|
||
if (isDeleting || isMultiSelectMode.value) return
|
||
|
||
// 关闭可能打开的侧滑菜单
|
||
closeMenu()
|
||
|
||
// 进入多选模式
|
||
isMultiSelectMode.value = true
|
||
selectedIds.value = []
|
||
}
|
||
|
||
// 退出多选模式
|
||
const exitMultiSelectMode = () => {
|
||
isMultiSelectMode.value = false
|
||
selectedIds.value = []
|
||
}
|
||
|
||
// 切换选中状态
|
||
const toggleSelect = (id : number) => {
|
||
const index = selectedIds.value.indexOf(id)
|
||
if (index == -1) {
|
||
selectedIds.value.push(id)
|
||
} else {
|
||
selectedIds.value.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
// 切换全选状态
|
||
const toggleSelectAll = () => {
|
||
if (isAllSelected.value) {
|
||
// 取消全选
|
||
selectedIds.value = []
|
||
} else {
|
||
// 全选
|
||
selectedIds.value = items.value.map((item) : number => item.id)
|
||
}
|
||
}
|
||
|
||
// 批量删除
|
||
const onBatchDelete = () => {
|
||
if (selectedIds.value.length == 0) return
|
||
|
||
// 从后往前删除,避免索引错位
|
||
const idsToDelete = [...selectedIds.value]
|
||
items.value = items.value.filter((item) : boolean => !idsToDelete.includes(item.id))
|
||
itemMetas.value = items.value.map((item) : ListItemMeta => createListItemMeta())
|
||
|
||
// 清空选中列表
|
||
selectedIds.value = []
|
||
|
||
// 如果删除完了所有项,退出多选模式
|
||
if (items.value.length == 0) {
|
||
exitMultiSelectMode()
|
||
}
|
||
}
|
||
|
||
// 触摸开始
|
||
const onTouchStart = (e : TouchEvent, index : number) => {
|
||
// 如果正在滚动或正在删除,不响应触摸
|
||
if (isScrolling || isDeleting) return
|
||
|
||
// 多选模式下不响应侧滑
|
||
if (isMultiSelectMode.value) return
|
||
|
||
const touch = e.touches[0]
|
||
startX = touch.screenX
|
||
startY = touch.screenY
|
||
currentIndex = index
|
||
isSwiping = false
|
||
swipeStarted = false
|
||
|
||
|
||
// 如果点击的不是当前展开的项,先关闭菜单
|
||
if (activeIndex.value != -1 && activeIndex.value != index) {
|
||
closeMenu()
|
||
}
|
||
|
||
// 保存当前列表项元素引用
|
||
const target = e.currentTarget as UniElement | null
|
||
currentListItemElement = target!
|
||
|
||
// 如果当前项已展开,从展开位置开始
|
||
if (activeIndex.value == index) {
|
||
const meta = itemMetas.value[index]
|
||
meta.translateX = -MENU_WIDTH
|
||
menuShowWidth.value = MENU_WIDTH
|
||
}
|
||
}
|
||
|
||
// 触摸移动
|
||
const onTouchMove = (e : TouchEvent) => {
|
||
// 如果正在滚动或正在删除,不响应触摸
|
||
if (isScrolling || isDeleting) return
|
||
if (currentIndex == -1) return
|
||
|
||
// 多选模式下不响应侧滑
|
||
if (isMultiSelectMode.value) return
|
||
|
||
const touch = e.touches[0]
|
||
const deltaX = touch.screenX - startX
|
||
const deltaY = touch.screenY - startY
|
||
|
||
// 判断是否开始侧滑(水平滑动距离大于垂直滑动距离)
|
||
if (!swipeStarted) {
|
||
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
||
swipeStarted = true
|
||
isSwiping = Math.abs(deltaX) > Math.abs(deltaY)
|
||
}
|
||
}
|
||
|
||
if (!isSwiping) {
|
||
return
|
||
} else {
|
||
// #ifdef WEB
|
||
// 滚动已经开始等情况时,web端不可阻止默认行为。去除此if会在控制台告警
|
||
if (e.cancelable) {
|
||
e.preventDefault()
|
||
}
|
||
// #endif
|
||
// #ifndef WEB
|
||
e.preventDefault()
|
||
// #endif
|
||
}
|
||
|
||
// 计算偏移量
|
||
let offsetX : number
|
||
if (activeIndex.value == currentIndex) {
|
||
// 当前项已展开,从展开位置开始计算
|
||
offsetX = -MENU_WIDTH + deltaX
|
||
} else {
|
||
offsetX = deltaX
|
||
}
|
||
|
||
// 限制范围:最多左滑菜单宽度,不能右滑超过0
|
||
offsetX = Math.max(-MENU_WIDTH, Math.min(0, offsetX))
|
||
const meta = itemMetas.value[currentIndex]
|
||
meta.translateX = offsetX
|
||
|
||
// 更新菜单位置和宽度(同步滑出效果)
|
||
if (offsetX < 0) {
|
||
updateMenuPosition()
|
||
// 菜单宽度与列表项偏移量同步
|
||
menuShowWidth.value = Math.abs(offsetX)
|
||
} else {
|
||
menuVisible.value = false
|
||
menuShowWidth.value = 0
|
||
}
|
||
}
|
||
|
||
// 触摸结束
|
||
const onTouchEnd = (_ : TouchEvent) => {
|
||
// 如果正在滚动或正在删除,不响应触摸
|
||
if (isScrolling || isDeleting) return
|
||
if (currentIndex == -1) return
|
||
|
||
// 多选模式下不响应侧滑
|
||
if (isMultiSelectMode.value) return
|
||
|
||
if (isSwiping) {
|
||
// 判断是否展开菜单
|
||
if (itemMetas.value[currentIndex].translateX < -MENU_WIDTH / 2) {
|
||
// 展开菜单
|
||
itemMetas.value[currentIndex].translateX = -MENU_WIDTH
|
||
menuShowWidth.value = MENU_WIDTH
|
||
activeIndex.value = currentIndex
|
||
menuVisible.value = true
|
||
} else {
|
||
// 关闭菜单
|
||
closeMenu()
|
||
}
|
||
}
|
||
|
||
currentIndex = -1
|
||
isSwiping = false
|
||
swipeStarted = false
|
||
}
|
||
|
||
// 列表滚动事件
|
||
const onListScroll = (e : UniScrollEvent) => {
|
||
// 标记正在滚动
|
||
isScrolling = true
|
||
// 清除之前的定时器
|
||
if (scrollEndTimer != null) {
|
||
clearTimeout(scrollEndTimer!)
|
||
scrollEndTimer = null
|
||
}
|
||
|
||
// 设置滚动结束检测定时器(作为备用方案)
|
||
scrollEndTimer = setTimeout(() => {
|
||
isScrolling = false
|
||
scrollEndTimer = null
|
||
}, SCROLL_END_DELAY) as number
|
||
|
||
// 滚动时关闭菜单
|
||
if (menuVisible.value) {
|
||
closeMenu()
|
||
}
|
||
}
|
||
|
||
// 滚动结束事件
|
||
const onScrollEnd = (_ : ScrollEvent) => {
|
||
// 清除定时器
|
||
if (scrollEndTimer != null) {
|
||
clearTimeout(scrollEndTimer!)
|
||
scrollEndTimer = null
|
||
}
|
||
// 标记滚动结束
|
||
isScrolling = false
|
||
}
|
||
|
||
// 删除动画完成处理
|
||
const finishDeleteAnimation = () => {
|
||
const indexToDelete = deleteAnimationIndexToDelete
|
||
// 重置动画状态
|
||
deletingIndex = -1
|
||
deletedItemHeight = 0
|
||
isDeleting = false
|
||
menuTransition.value = 'none'
|
||
deleteAnimationIndexToDelete = -1
|
||
const tempCurrentListItemElement = currentListItemElement
|
||
currentListItemElement = null
|
||
|
||
items.value.splice(indexToDelete, 1)
|
||
itemMetas.value.splice(indexToDelete, 1)
|
||
resetAllItemMetas()
|
||
// #ifdef APP-ANDROID && VUE3-VAPOR
|
||
const sortedListItems = Array.from(listRef.value!.querySelectorAll('.item-wrapper'))
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
for (let i = 0; i < sortedListItems.length; i++) {
|
||
const element = sortedListItems[i]
|
||
if (tempCurrentListItemElement === element) {
|
||
continue
|
||
}
|
||
element.style.setProperty('transition', 'none')
|
||
element.style.setProperty('transform', `translateY(0px)`)
|
||
}
|
||
})
|
||
})
|
||
// #endif
|
||
}
|
||
|
||
const onDeleteItemTransitionEnd = (index : number) => {
|
||
if (!isDeleting) return
|
||
if (index != deleteAnimationIndexToDelete) return
|
||
finishDeleteAnimation()
|
||
}
|
||
|
||
// 删除按钮点击
|
||
const onDeleteClick = () => {
|
||
if (activeIndex.value == -1 || isDeleting) return
|
||
|
||
const indexToDelete = activeIndex.value
|
||
|
||
// 保存删除项信息用于动画
|
||
deletingIndex = indexToDelete
|
||
const currentListItemRect = currentListItemElement!.getBoundingClientRect()
|
||
deletedItemHeight = currentListItemRect.height
|
||
isDeleting = true
|
||
menuVisible.value = false
|
||
activeIndex.value = -1
|
||
|
||
const listElement = listRef.value!
|
||
const listRect = listElement.getBoundingClientRect()
|
||
const scrollTop = listElement.scrollTop
|
||
const scrollOffsetBottom = Math.max(0, listElement.scrollHeight - scrollTop - listRect.height)
|
||
const scrollOffsetTop = scrollTop
|
||
if (scrollOffsetBottom < deletedItemHeight) {
|
||
deleteAnimationTargetBelow = scrollOffsetBottom
|
||
deleteAnimationTargetAbove = Math.min(scrollOffsetTop, deletedItemHeight - scrollOffsetBottom)
|
||
} else {
|
||
deleteAnimationTargetBelow = deletedItemHeight
|
||
deleteAnimationTargetAbove = 0
|
||
}
|
||
deleteAnimationTargetHorizontal = -(MENU_WIDTH + 400)
|
||
deleteAnimationIndexToDelete = indexToDelete
|
||
|
||
const visibleHeightTop = Math.max(0, currentListItemRect.top - listRect.top)
|
||
const visibleHeightBottom = Math.max(0, listRect.bottom - currentListItemRect.bottom)
|
||
const visibleItemCountTop = Math.ceil(visibleHeightTop / MIN_ITEM_HEIGHT)
|
||
const visibleItemCountBottom = Math.ceil(visibleHeightBottom / MIN_ITEM_HEIGHT)
|
||
|
||
// 减少计算量,直接推测可见区域
|
||
visibleStartIndex = Math.max(0, indexToDelete - visibleItemCountTop)
|
||
visibleEndIndex = Math.min(items.value.length - 1, indexToDelete + visibleItemCountBottom)
|
||
menuTransition.value = DELETE_ANIMATION_TRANSITION
|
||
menuShowWidth.value = 0
|
||
applyDeleteAnimationMetas()
|
||
}
|
||
|
||
type DataType = {
|
||
listTop : number
|
||
listWidth : number
|
||
}
|
||
const data = reactive({
|
||
listTop: 0,
|
||
listWidth: 0
|
||
})
|
||
function jest_getListRect() {
|
||
const listRect = listRef.value!.getBoundingClientRect()
|
||
data.listTop = listRect.top
|
||
data.listWidth = listRect.width
|
||
}
|
||
async function jest_batchDeleteItem() {
|
||
listRef.value!.scrollTop = 1000000
|
||
await new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
resolve(true)
|
||
}, 1000)
|
||
})
|
||
const itemsToDelete = items.value.slice(0, items.value.length - 1)
|
||
const idsToDelete = itemsToDelete.map((item) : number => item.id)
|
||
items.value = items.value.filter((item) : boolean => {
|
||
return !idsToDelete.includes(item.id)
|
||
})
|
||
itemMetas.value = items.value.map((item) : ListItemMeta => createListItemMeta())
|
||
}
|
||
defineExpose({
|
||
jest_getListRect,
|
||
jest_batchDeleteItem,
|
||
data
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.container {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.list {
|
||
flex: 1;
|
||
}
|
||
|
||
.item-wrapper {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.item-content {
|
||
flex: 1;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 10px;
|
||
}
|
||
|
||
.item-text {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.item-info {
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.item-sub-items {
|
||
width: 100%;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 侧滑菜单样式 */
|
||
.swipe-menu {
|
||
position: absolute;
|
||
right: -80px;
|
||
top: 0;
|
||
width: 80px;
|
||
flex-direction: row;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.menu-item {
|
||
width: 80px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.delete-btn {
|
||
background-color: #ff4d4f;
|
||
}
|
||
|
||
.menu-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* 底部工具栏样式 */
|
||
.bottom-toolbar {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #ffffff;
|
||
align-items: center;
|
||
padding-left: 15px;
|
||
padding-right: 15px;
|
||
padding-bottom: var(--uni-safe-area-inset-bottom);
|
||
border-top-width: 1px;
|
||
border-top-color: #e5e5e5;
|
||
border-top-style: solid;
|
||
}
|
||
|
||
.toolbar {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin: 2px 0px;
|
||
}
|
||
</style>
|