cl-read-more.uvue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <template>
  2. <view class="cl-read-more" :class="[pt.className]">
  3. <!-- 内容区域 -->
  4. <view class="cl-read-more__wrapper" :class="[pt.wrapper?.className]" :style="wrapperStyle">
  5. <view class="cl-read-more__content" :class="[pt.content?.className]">
  6. <slot></slot>
  7. </view>
  8. <view
  9. class="cl-read-more__mask"
  10. :class="[
  11. {
  12. 'is-show': !isExpanded && showToggle,
  13. 'is-dark': isDark
  14. },
  15. pt.mask?.className
  16. ]"
  17. ></view>
  18. </view>
  19. <!-- 展开/收起按钮 -->
  20. <slot name="toggle" :isExpanded="isExpanded">
  21. <view
  22. class="cl-read-more__toggle"
  23. :class="[
  24. {
  25. 'is-disabled': disabled
  26. },
  27. pt.toggle?.className
  28. ]"
  29. @tap="toggle"
  30. v-if="showToggle"
  31. >
  32. <cl-text
  33. color="primary"
  34. :pt="{
  35. className: 'text-sm mr-1'
  36. }"
  37. >
  38. {{ isExpanded ? collapseText : expandText }}
  39. </cl-text>
  40. <cl-icon :name="isExpanded ? collapseIcon : expandIcon" color="primary"></cl-icon>
  41. </view>
  42. </slot>
  43. </view>
  44. </template>
  45. <script setup lang="ts">
  46. import { computed, getCurrentInstance, ref, onMounted, watch } from "vue";
  47. import { getPx, isDark, parsePt } from "@/cool";
  48. import type { PassThroughProps } from "../../types";
  49. import { t } from "@/locale";
  50. defineOptions({
  51. name: "cl-read-more"
  52. });
  53. // 组件属性定义
  54. const props = defineProps({
  55. // 透传样式
  56. pt: {
  57. type: Object,
  58. default: () => ({})
  59. },
  60. // 是否展开
  61. modelValue: {
  62. type: Boolean,
  63. default: false
  64. },
  65. // 收起状态下的最大高度
  66. height: {
  67. type: [Number, String],
  68. default: 80
  69. },
  70. // 展开文本
  71. expandText: {
  72. type: String,
  73. default: () => t("展开")
  74. },
  75. // 收起文本
  76. collapseText: {
  77. type: String,
  78. default: () => t("收起")
  79. },
  80. // 展开图标
  81. expandIcon: {
  82. type: String,
  83. default: "arrow-down-s-line"
  84. },
  85. // 收起图标
  86. collapseIcon: {
  87. type: String,
  88. default: "arrow-up-s-line"
  89. },
  90. // 是否禁用
  91. disabled: {
  92. type: Boolean,
  93. default: false
  94. }
  95. });
  96. // 事件定义
  97. const emit = defineEmits(["update:modelValue", "change", "toggle"]);
  98. // 插槽定义
  99. defineSlots<{
  100. toggle(props: { isExpanded: boolean }): any;
  101. }>();
  102. const { proxy } = getCurrentInstance()!;
  103. // 透传样式类型
  104. type PassThrough = {
  105. className?: string;
  106. wrapper?: PassThroughProps;
  107. content?: PassThroughProps;
  108. mask?: PassThroughProps;
  109. toggle?: PassThroughProps;
  110. };
  111. // 解析透传样式
  112. const pt = computed(() => parsePt<PassThrough>(props.pt));
  113. // 展开状态
  114. const isExpanded = ref(props.modelValue);
  115. // 内容实际高度
  116. const contentHeight = ref(0);
  117. // 是否显示切换按钮
  118. const showToggle = ref(true);
  119. // 包裹器样式
  120. const wrapperStyle = computed(() => {
  121. const style = {};
  122. if (showToggle.value) {
  123. style["height"] = isExpanded.value ? `${contentHeight.value}px` : `${props.height}rpx`;
  124. }
  125. return style;
  126. });
  127. /**
  128. * 切换展开/收起状态
  129. */
  130. function toggle() {
  131. if (props.disabled) {
  132. emit("toggle", isExpanded.value);
  133. return;
  134. }
  135. isExpanded.value = !isExpanded.value;
  136. emit("update:modelValue", isExpanded.value);
  137. emit("change", isExpanded.value);
  138. }
  139. /**
  140. * 获取内容实际高度
  141. */
  142. function getContentHeight() {
  143. uni.createSelectorQuery()
  144. .in(proxy)
  145. .select(".cl-read-more__content")
  146. .boundingClientRect((node) => {
  147. // 获取内容高度
  148. contentHeight.value = (node as NodeInfo).height ?? 0;
  149. // 实际高度是否大于折叠高度
  150. showToggle.value = contentHeight.value > getPx(props.height);
  151. })
  152. .exec();
  153. }
  154. // 监听modelValue变化
  155. watch(
  156. computed(() => props.modelValue),
  157. (val: boolean) => {
  158. isExpanded.value = val;
  159. }
  160. );
  161. // 组件挂载后获取内容高度
  162. onMounted(() => {
  163. getContentHeight();
  164. });
  165. // 暴露方法
  166. defineExpose({
  167. toggle
  168. });
  169. </script>
  170. <style lang="scss" scoped>
  171. .cl-read-more {
  172. @apply relative;
  173. &__wrapper {
  174. @apply relative duration-300;
  175. transition-property: height;
  176. }
  177. &__mask {
  178. @apply absolute bottom-0 left-0 w-full h-14 opacity-0 pointer-events-none;
  179. // #ifndef APP-IOS
  180. transition-duration: 200ms;
  181. transition-property: opacity;
  182. // #endif
  183. background-image: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
  184. &.is-dark {
  185. background-image: linear-gradient(to top, rgba(30, 30, 30, 1), rgba(30, 30, 30, 0));
  186. }
  187. &.is-show {
  188. @apply opacity-100;
  189. }
  190. }
  191. &__toggle {
  192. @apply flex flex-row items-center justify-center mt-2;
  193. &.is-disabled {
  194. @apply opacity-50;
  195. }
  196. }
  197. }
  198. </style>