cl-read-more.uvue 4.7 KB

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