cl-read-more.uvue 4.8 KB

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