版本发布

This commit is contained in:
icssoa
2025-07-21 16:47:04 +08:00
parent 1abed7a2e1
commit 6d8193880a
307 changed files with 41718 additions and 0 deletions

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

View 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;
};