版本发布
This commit is contained in:
442
uni_modules/cool-ui/components/cl-list-item/cl-list-item.uvue
Normal file
442
uni_modules/cool-ui/components/cl-list-item/cl-list-item.uvue
Normal file
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<view
|
||||
class="cl-list-item"
|
||||
:class="[
|
||||
{
|
||||
'cl-list-item--disabled': disabled
|
||||
},
|
||||
pt.className
|
||||
]"
|
||||
@touchstart="onTouchStart"
|
||||
@touchend="onTouchEnd"
|
||||
@touchmove="onTouchMove"
|
||||
@touchcancel="onTouchCancel"
|
||||
>
|
||||
<view
|
||||
class="cl-list-item__wrapper"
|
||||
:class="[
|
||||
{
|
||||
'is-transition': !isHover,
|
||||
[isDark ? 'bg-surface-800' : 'bg-white']: true,
|
||||
[isDark ? '!bg-surface-700' : '!bg-surface-50']: hoverable && isHover
|
||||
}
|
||||
]"
|
||||
:style="{
|
||||
transform: `translateX(${swipe.offsetX}px)`
|
||||
}"
|
||||
@tap="onTap"
|
||||
>
|
||||
<view class="cl-list-item__inner w-" :class="[pt.inner?.className]">
|
||||
<cl-icon
|
||||
:name="icon"
|
||||
:size="pt.icon?.size ?? 36"
|
||||
:color="pt.icon?.color"
|
||||
:pt="{
|
||||
className: `mr-3 ${pt.icon?.className}`
|
||||
}"
|
||||
v-if="icon"
|
||||
></cl-icon>
|
||||
|
||||
<cl-text
|
||||
v-if="label"
|
||||
:pt="{
|
||||
className: parseClass([
|
||||
'cl-list-item__label w-24 whitespace-nowrap overflow-visible',
|
||||
pt.label?.className
|
||||
])
|
||||
}"
|
||||
>
|
||||
{{ label }}
|
||||
</cl-text>
|
||||
|
||||
<view
|
||||
class="cl-list-item__content"
|
||||
:class="[
|
||||
{
|
||||
'justify-start': justify == 'start',
|
||||
'justify-center': justify == 'center',
|
||||
'justify-end': justify == 'end'
|
||||
},
|
||||
pt.content?.className
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</view>
|
||||
|
||||
<cl-icon
|
||||
name="arrow-right-s-line"
|
||||
:size="36"
|
||||
:pt="{
|
||||
className: parseClass([
|
||||
'!text-surface-400 ml-1 duration-200',
|
||||
{
|
||||
'rotate-90': isCollapse
|
||||
}
|
||||
])
|
||||
}"
|
||||
v-if="arrow"
|
||||
></cl-icon>
|
||||
</view>
|
||||
|
||||
<view
|
||||
:class="['cl-list-item__swipe', `cl-list-item__swipe-${swipe.direction}`]"
|
||||
v-if="swipeable"
|
||||
>
|
||||
<slot name="swipe-left"></slot>
|
||||
<slot name="swipe-right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-collapse
|
||||
v-model="isCollapse"
|
||||
:pt="{
|
||||
className: parseClass(['p-[24rpx]', pt.collapse?.className])
|
||||
}"
|
||||
>
|
||||
<slot name="collapse"></slot>
|
||||
</cl-collapse>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
useSlots,
|
||||
type PropType
|
||||
} from "vue";
|
||||
import { isDark, isHarmony, parseClass, parsePt } from "@/cool";
|
||||
import type { Justify, PassThroughProps } from "../../types";
|
||||
import type { ClIconProps } from "../cl-icon/props";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-list-item"
|
||||
});
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 透传样式配置
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 图标名称
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
// 标签文本
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
// 内容对齐方式
|
||||
justify: {
|
||||
type: String as PropType<Justify>,
|
||||
default: "end"
|
||||
},
|
||||
// 是否显示箭头
|
||||
arrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否可滑动
|
||||
swipeable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示点击态
|
||||
hoverable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示折叠
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
const slots = useSlots();
|
||||
|
||||
// 透传样式类型定义 - 用于定义可以传入的样式属性类型
|
||||
type PassThrough = {
|
||||
className?: string; // 根元素类名
|
||||
inner?: PassThroughProps; // 内部容器样式
|
||||
label?: PassThroughProps; // 标签文本样式
|
||||
content?: PassThroughProps; // 内容区域样式
|
||||
icon?: ClIconProps; // 图标样式
|
||||
collapse?: PassThroughProps; // 折叠内容样式
|
||||
};
|
||||
|
||||
// 计算透传样式 - 解析传入的样式配置
|
||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
|
||||
// 滑动状态类型定义 - 用于管理滑动相关的状态数据
|
||||
type Swipe = {
|
||||
width: number; // 滑动区域宽度
|
||||
maxX: number; // 最大滑动距离
|
||||
startX: number; // 开始触摸位置
|
||||
endX: number; // 结束触摸位置
|
||||
offsetX: number; // 当前偏移量
|
||||
direction: "right" | "left"; // 滑动方向 - right表示向右滑动显示左侧内容,left表示向左滑动显示右侧内容
|
||||
moveDirection: "right" | "left"; // 移动方向 - 实时记录手指滑动方向
|
||||
};
|
||||
|
||||
// 滑动状态数据 - 初始化滑动状态
|
||||
const swipe = reactive<Swipe>({
|
||||
width: 0, // 滑动区域宽度,通过查询获取
|
||||
maxX: 0, // 最大可滑动距离
|
||||
startX: 0, // 开始触摸的X坐标
|
||||
endX: 0, // 结束触摸的X坐标
|
||||
offsetX: 0, // X轴偏移量
|
||||
direction: "left", // 默认向左滑动
|
||||
moveDirection: "left" // 默认向左移动
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化滑动状态
|
||||
* 根据插槽判断滑动方向并获取滑动区域宽度
|
||||
*/
|
||||
function initSwipe() {
|
||||
if (!props.swipeable) return;
|
||||
|
||||
// 根据是否有左侧插槽判断滑动方向
|
||||
swipe.direction = slots["swipe-left"] != null ? "right" : "left";
|
||||
|
||||
// 获取滑动区域宽度
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".cl-list-item__swipe")
|
||||
.boundingClientRect((node) => {
|
||||
// 获取滑动区域的宽度,如果未获取到则默认为0
|
||||
swipe.width = (node as NodeInfo).width ?? 0;
|
||||
// 根据滑动方向(left/right)设置最大可滑动距离,左滑为负,右滑为正
|
||||
swipe.maxX = swipe.width * (swipe.direction == "left" ? -1 : 1);
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置滑动状态
|
||||
* 将开始和结束位置重置为0
|
||||
*/
|
||||
function resetSwipe() {
|
||||
swipe.startX = 0;
|
||||
swipe.endX = 0;
|
||||
swipe.offsetX = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑动到指定位置
|
||||
* @param num 目标位置
|
||||
* 使用requestAnimationFrame实现平滑滑动动画
|
||||
*/
|
||||
function swipeTo(num: number) {
|
||||
// #ifdef APP
|
||||
function next() {
|
||||
requestAnimationFrame(() => {
|
||||
if (swipe.offsetX != num) {
|
||||
// 计算每次移动的距离
|
||||
const step = 2;
|
||||
const direction = swipe.offsetX < num ? 1 : -1;
|
||||
|
||||
// 更新偏移量
|
||||
swipe.offsetX += step * direction;
|
||||
|
||||
// 防止过度滑动
|
||||
if (direction > 0 ? swipe.offsetX > num : swipe.offsetX < num) {
|
||||
swipe.offsetX = num;
|
||||
}
|
||||
|
||||
next();
|
||||
} else {
|
||||
// 动画结束,更新结束位置
|
||||
swipe.endX = swipe.offsetX;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP
|
||||
swipe.offsetX = num;
|
||||
swipe.endX = num;
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 点击态状态 - 用于显示点击效果
|
||||
const isHover = ref(false);
|
||||
|
||||
/**
|
||||
* 触摸开始事件处理
|
||||
* @param e 触摸事件对象
|
||||
*/
|
||||
function onTouchStart(e: UniTouchEvent) {
|
||||
isHover.value = true;
|
||||
|
||||
// 记录开始触摸位置
|
||||
if (props.swipeable) {
|
||||
swipe.startX = (e.touches[0] as UniTouch).pageX;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸结束事件处理
|
||||
* 根据滑动距离判断是否触发完整滑动
|
||||
*/
|
||||
function onTouchEnd() {
|
||||
if (isHover.value) {
|
||||
// 计算滑动阈值 - 取滑动区域一半和50px中的较小值
|
||||
const threshold = swipe.width / 2 > 50 ? 50 : swipe.width / 2;
|
||||
// 计算实际滑动距离
|
||||
const offset = Math.abs(swipe.offsetX - swipe.endX);
|
||||
|
||||
// 移除点击效果
|
||||
isHover.value = false;
|
||||
|
||||
// 根据滑动距离判断是否触发滑动
|
||||
if (offset > threshold) {
|
||||
// 如果滑动方向与预设方向一致,滑动到最大位置
|
||||
if (swipe.direction == swipe.moveDirection) {
|
||||
swipeTo(swipe.maxX);
|
||||
} else {
|
||||
// 否则回到起始位置
|
||||
swipeTo(0);
|
||||
}
|
||||
} else {
|
||||
// 滑动距离不够,回到最近的位置
|
||||
swipeTo(swipe.endX == 0 ? 0 : swipe.maxX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸取消事件处理
|
||||
*/
|
||||
function onTouchCancel() {
|
||||
isHover.value = false; // 移除点击效果
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动事件处理
|
||||
* @param e 触摸事件对象
|
||||
*/
|
||||
function onTouchMove(e: UniTouchEvent) {
|
||||
if (isHover.value) {
|
||||
// 计算滑动偏移量
|
||||
const offsetX = (e.touches[0] as UniTouch).pageX - swipe.startX;
|
||||
|
||||
// 根据偏移量判断滑动方向
|
||||
swipe.moveDirection = offsetX > 0 ? "right" : "left";
|
||||
|
||||
// 计算目标位置
|
||||
let x = offsetX + swipe.endX;
|
||||
|
||||
// 限制滑动范围
|
||||
if (swipe.direction == "right") {
|
||||
// 向右滑动时的边界处理
|
||||
if (x > swipe.maxX) {
|
||||
x = swipe.maxX;
|
||||
}
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (swipe.direction == "left") {
|
||||
// 向左滑动时的边界处理
|
||||
if (x < swipe.maxX) {
|
||||
x = swipe.maxX;
|
||||
}
|
||||
if (x > 0) {
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新偏移量
|
||||
swipe.offsetX = x;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态
|
||||
const isCollapse = ref(false);
|
||||
|
||||
/**
|
||||
* 点击事件处理
|
||||
*/
|
||||
function onTap() {
|
||||
if (props.collapse) {
|
||||
isCollapse.value = !isCollapse.value;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(
|
||||
() => {
|
||||
initSwipe();
|
||||
},
|
||||
isHarmony() ? 50 : 0
|
||||
);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
initSwipe,
|
||||
resetSwipe
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-list-item {
|
||||
@apply flex flex-col w-full relative;
|
||||
|
||||
&__wrapper {
|
||||
@apply w-full;
|
||||
overflow: visible;
|
||||
|
||||
&.is-transition {
|
||||
// #ifdef H5 || MP
|
||||
transition-property: transform;
|
||||
transition-duration: 0.2s;
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
@apply flex flex-row items-center;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@apply flex flex-row items-center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__swipe {
|
||||
@apply absolute h-full;
|
||||
|
||||
&-left {
|
||||
@apply left-full;
|
||||
transform: translateX(1rpx);
|
||||
}
|
||||
|
||||
&-right {
|
||||
@apply right-full;
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
@apply opacity-50;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
uni_modules/cool-ui/components/cl-list-item/props.ts
Normal file
24
uni_modules/cool-ui/components/cl-list-item/props.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Justify, PassThroughProps } from "../../types";
|
||||
import type { ClIconProps } from "../cl-icon/props";
|
||||
|
||||
export type ClListItemPassThrough = {
|
||||
className?: string;
|
||||
inner?: PassThroughProps;
|
||||
label?: PassThroughProps;
|
||||
content?: PassThroughProps;
|
||||
icon?: ClIconProps;
|
||||
collapse?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClListItemProps = {
|
||||
className?: string;
|
||||
pt?: ClListItemPassThrough;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
justify?: Justify;
|
||||
arrow?: boolean;
|
||||
swipeable?: boolean;
|
||||
hoverable?: boolean;
|
||||
disabled?: boolean;
|
||||
collapse?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user