添加查看更多组件

This commit is contained in:
icssoa
2025-09-13 20:48:11 +08:00
parent 742e67bd5f
commit b64b5c5b62
7 changed files with 353 additions and 1 deletions

View File

@@ -316,6 +316,12 @@
"navigationBarTitleText": "Avatar 头像"
}
},
{
"path": "data/read-more",
"style": {
"navigationBarTitleText": "ReadMore 查看更多"
}
},
{
"path": "data/draggable",
"style": {

View File

@@ -0,0 +1,85 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-read-more>
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
<demo-item :label="t('禁用切换按钮')">
<cl-read-more
v-model="visible"
:disabled="disabled"
:expand-text="disabled ? '付费解锁' : '展开'"
:expand-icon="disabled ? 'lock-line' : 'arrow-down-s-line'"
@toggle="toggle"
>
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
<demo-item :label="t('自定义高度')">
<cl-read-more :height="300">
<cl-text>
云想衣裳花想容,春风拂槛露华浓。若非群玉山头见,会向瑶台月下逢。
一枝红艳露凝香,云雨巫山枉断肠。借问汉宫谁得似?可怜飞燕倚新妆。
</cl-text>
<cl-image
:height="300"
width="100%"
:pt="{
className: 'my-3'
}"
src="https://uni-docs.cool-js.com/demo/pages/demo/static/bg1.png"
></cl-image>
<cl-text>
名花倾国两相欢,常得君王带笑看。解释春风无限恨,沉香亭北倚阑干。
</cl-text>
</cl-read-more>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { router } from "@/cool";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ref } from "vue";
import { useUi } from "@/uni_modules/cool-ui";
const ui = useUi();
const visible = ref(false);
const disabled = ref(true);
function toggle(isExpanded: boolean) {
ui.showConfirm({
title: "提示",
message: "需支付100元才能解锁全部内容是否继续",
callback(action) {
if (action == "confirm") {
ui.showToast({
message: "支付成功"
});
disabled.value = false;
visible.value = true;
}
}
});
}
</script>
<style lang="scss" scoped></style>

View File

@@ -48,7 +48,7 @@
<view class="group" v-for="item in data" :key="item.label">
<cl-text :pt="{ className: '!text-sm !text-surface-400 mb-2 ml-2' }">{{
item.label
}}</cl-text>
}}{{ item.children?.length ?? 0 }}</cl-text>
<view class="list">
<cl-row :gutter="10">
@@ -275,6 +275,11 @@ const data = computed<Item[]>(() => {
icon: "account-circle-line",
path: "/pages/demo/data/avatar"
},
{
label: t("查看更多"),
icon: "menu-add-line",
path: "/pages/demo/data/read-more"
},
{
label: t("列表"),
icon: "list-check",

View File

@@ -0,0 +1,230 @@
<template>
<view class="cl-read-more" :class="[pt.className]">
<!-- 内容区域 -->
<view class="cl-read-more__wrapper" :class="[pt.wrapper?.className]" :style="wrapperStyle">
<view class="cl-read-more__content" :class="[pt.content?.className]">
<slot></slot>
</view>
<view
class="cl-read-more__mask"
:class="[
{
'is-show': !isExpanded && showToggle,
'is-dark': isDark
},
pt.mask?.className
]"
></view>
</view>
<!-- 展开/收起按钮 -->
<slot name="toggle" :isExpanded="isExpanded">
<view
class="cl-read-more__toggle"
:class="[
{
'is-disabled': disabled
},
pt.toggle?.className
]"
@tap="toggle"
v-if="showToggle"
>
<cl-text
color="primary"
:pt="{
className: 'text-sm mr-1'
}"
>
{{ isExpanded ? collapseText : expandText }}
</cl-text>
<cl-icon :name="isExpanded ? collapseIcon : expandIcon" color="primary"></cl-icon>
</view>
</slot>
</view>
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, ref, onMounted, watch } from "vue";
import { getPx, isDark, parsePt } from "@/cool";
import type { PassThroughProps } from "../../types";
import { t } from "@/locale";
defineOptions({
name: "cl-read-more"
});
// 组件属性定义
const props = defineProps({
// 透传样式
pt: {
type: Object,
default: () => ({})
},
// 是否展开
modelValue: {
type: Boolean,
default: false
},
// 收起状态下的最大高度
height: {
type: [Number, String],
default: 80
},
// 展开文本
expandText: {
type: String,
default: () => t("展开")
},
// 收起文本
collapseText: {
type: String,
default: () => t("收起")
},
// 展开图标
expandIcon: {
type: String,
default: "arrow-down-s-line"
},
// 收起图标
collapseIcon: {
type: String,
default: "arrow-up-s-line"
},
// 是否禁用
disabled: {
type: Boolean,
default: false
}
});
// 事件定义
const emit = defineEmits(["update:modelValue", "change", "toggle"]);
// 插槽定义
defineSlots<{
toggle(props: { isExpanded: boolean }): any;
}>();
const { proxy } = getCurrentInstance()!;
// 透传样式类型
type PassThrough = {
className?: string;
wrapper?: PassThroughProps;
content?: PassThroughProps;
mask?: PassThroughProps;
toggle?: PassThroughProps;
};
// 解析透传样式
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 展开状态
const isExpanded = ref(props.modelValue);
// 内容实际高度
const contentHeight = ref(0);
// 是否显示切换按钮
const showToggle = ref(true);
// 包裹器样式
const wrapperStyle = computed(() => {
const style = {};
if (showToggle.value) {
style["height"] = isExpanded.value ? `${contentHeight.value}px` : `${props.height}rpx`;
}
return style;
});
/**
* 切换展开/收起状态
*/
function toggle() {
if (props.disabled) {
emit("toggle", isExpanded.value);
return;
}
isExpanded.value = !isExpanded.value;
emit("update:modelValue", isExpanded.value);
emit("change", isExpanded.value);
}
/**
* 获取内容实际高度
*/
function getContentHeight() {
uni.createSelectorQuery()
.in(proxy)
.select(".cl-read-more__content")
.boundingClientRect((node) => {
// 获取内容高度
contentHeight.value = (node as NodeInfo).height ?? 0;
// 实际高度是否大于折叠高度
showToggle.value = contentHeight.value > getPx(props.height);
})
.exec();
}
// 监听modelValue变化
watch(
computed(() => props.modelValue),
(val: boolean) => {
isExpanded.value = val;
}
);
// 组件挂载后获取内容高度
onMounted(() => {
getContentHeight();
});
// 暴露方法
defineExpose({
toggle
});
</script>
<style lang="scss" scoped>
.cl-read-more {
@apply relative;
&__wrapper {
@apply relative duration-300;
transition-property: height;
}
&__mask {
@apply absolute bottom-0 left-0 w-full h-14 opacity-0 pointer-events-none;
// #ifndef APP-IOS
transition-duration: 200ms;
transition-property: opacity;
// #endif
background-image: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
&.is-dark {
background-image: linear-gradient(to top, rgba(30, 30, 30, 1), rgba(30, 30, 30, 0));
}
&.is-show {
@apply opacity-100;
}
}
&__toggle {
@apply flex flex-row items-center justify-center mt-2;
&.is-disabled {
@apply opacity-50;
}
}
}
</style>

View File

@@ -0,0 +1,20 @@
import type { PassThroughProps } from "../../types";
export type ClReadMorePassThrough = {
className?: string;
content?: PassThroughProps;
mask?: PassThroughProps;
toggle?: PassThroughProps;
};
export type ClReadMoreProps = {
className?: string;
pt?: ClReadMorePassThrough;
modelValue?: boolean;
height?: any;
expandText?: string;
collapseText?: string;
expandIcon?: string;
collapseIcon?: string;
disabled?: boolean;
};

View File

@@ -51,6 +51,7 @@ import type { ClProgressCircleProps, ClProgressCirclePassThrough } from "./compo
import type { ClQrcodeProps } from "./components/cl-qrcode/props";
import type { ClRadioProps, ClRadioPassThrough } from "./components/cl-radio/props";
import type { ClRateProps, ClRatePassThrough } from "./components/cl-rate/props";
import type { ClReadMoreProps, ClReadMorePassThrough } from "./components/cl-read-more/props";
import type { ClRowProps, ClRowPassThrough } from "./components/cl-row/props";
import type { ClSafeAreaProps, ClSafeAreaPassThrough } from "./components/cl-safe-area/props";
import type { ClSelectProps, ClSelectPassThrough } from "./components/cl-select/props";
@@ -130,6 +131,7 @@ declare module "vue" {
"cl-qrcode": (typeof import('./components/cl-qrcode/cl-qrcode.uvue')['default']) & import('vue').DefineComponent<ClQrcodeProps>;
"cl-radio": (typeof import('./components/cl-radio/cl-radio.uvue')['default']) & import('vue').DefineComponent<ClRadioProps>;
"cl-rate": (typeof import('./components/cl-rate/cl-rate.uvue')['default']) & import('vue').DefineComponent<ClRateProps>;
"cl-read-more": (typeof import('./components/cl-read-more/cl-read-more.uvue')['default']) & import('vue').DefineComponent<ClReadMoreProps>;
"cl-row": (typeof import('./components/cl-row/cl-row.uvue')['default']) & import('vue').DefineComponent<ClRowProps>;
"cl-safe-area": (typeof import('./components/cl-safe-area/cl-safe-area.uvue')['default']) & import('vue').DefineComponent<ClSafeAreaProps>;
"cl-select": (typeof import('./components/cl-select/cl-select.uvue')['default']) & import('vue').DefineComponent<ClSelectProps>;

View File

@@ -238,3 +238,7 @@ declare type ClMarqueeComponentPublicInstance = {
stop(): void;
reset(): void;
};
declare type ClReadMoreComponentPublicInstance = {
toggle(): void;
};