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