CollapsePanel.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <script setup lang="ts">
  2. interface Props {
  3. // 默认展示的行数
  4. inline?: number
  5. // 是否默认展开
  6. defaultExpanded?: boolean
  7. // 展开按钮文字
  8. expandText?: string
  9. // 收起按钮文字
  10. collapseText?: string
  11. // 行高(px),用于计算行数
  12. lineHeight?: number
  13. }
  14. const props = withDefaults(defineProps<Props>(), {
  15. inline: 3,
  16. defaultExpanded: false,
  17. expandText: '展开',
  18. collapseText: '收起',
  19. lineHeight: 100,
  20. })
  21. const emit = defineEmits<{
  22. (e: 'toggle', isExpanded: boolean): void
  23. }>()
  24. const _this = getCurrentInstance()
  25. const isExpanded = ref(props.defaultExpanded)
  26. const contentHeight = ref(0)
  27. const showToggle = ref(false)
  28. const toggleText = computed(() =>
  29. isExpanded.value ? props.collapseText : props.expandText,
  30. )
  31. const contentStyle = computed(() => {
  32. const style: Record<string, string> = {}
  33. if (isExpanded.value) {
  34. style.overflow = 'visible'
  35. }
  36. else {
  37. // 收起状态,限制行数
  38. style.overflow = 'hidden'
  39. style.maxHeight = `${props.inline * props.lineHeight}rpx`
  40. }
  41. return style
  42. })
  43. function handleToggle() {
  44. isExpanded.value = !isExpanded.value
  45. emit('toggle', isExpanded.value)
  46. }
  47. function getContentHeight() {
  48. return new Promise<number>((resolve) => {
  49. const query = uni.createSelectorQuery().in(_this)
  50. query.select('.collapse-panel__content').boundingClientRect()
  51. query.exec((res) => {
  52. if (res && res[0]) {
  53. resolve(res[0].height)
  54. }
  55. else {
  56. resolve(0)
  57. }
  58. })
  59. })
  60. }
  61. async function checkNeedToggle() {
  62. await nextTick()
  63. try {
  64. const height = await getContentHeight()
  65. contentHeight.value = height
  66. const maxHeightInPx = (props.inline * props.lineHeight) / 2
  67. showToggle.value = height > maxHeightInPx
  68. }
  69. catch (error) {
  70. console.error('获取内容高度失败:', error)
  71. }
  72. }
  73. // 暴露方法给父组件
  74. defineExpose({
  75. expand: () => {
  76. isExpanded.value = true
  77. emit('toggle', true)
  78. },
  79. collapse: () => {
  80. isExpanded.value = false
  81. emit('toggle', false)
  82. },
  83. toggle: handleToggle,
  84. refresh: checkNeedToggle,
  85. isExpanded: computed(() => isExpanded.value),
  86. })
  87. // 监听inline变化,重新检查
  88. watch(() => props.inline, () => {
  89. if (!isExpanded.value) {
  90. checkNeedToggle()
  91. }
  92. })
  93. onMounted(() => {
  94. setTimeout(() => {
  95. checkNeedToggle()
  96. }, 100)
  97. })
  98. </script>
  99. <template>
  100. <view class="collapse-panel">
  101. <view
  102. class="collapse-panel__content pb20rpx"
  103. :class="{
  104. 'collapse-panel__content--collapsed': !isExpanded,
  105. 'collapse-panel__content--expanded': isExpanded,
  106. }"
  107. :style="contentStyle"
  108. >
  109. <slot />
  110. </view>
  111. <view
  112. v-if="showToggle"
  113. class="collapse-panel__toggle"
  114. @tap="handleToggle"
  115. >
  116. <text class="collapse-panel__toggle-text">
  117. {{ toggleText }}
  118. </text>
  119. <view class="collapse-panel__toggle-icon" :class="{ 'collapse-panel__toggle-icon--expanded': isExpanded }">
  120. <wd-icon name="arrow-down" size="22px" />
  121. </view>
  122. </view>
  123. </view>
  124. </template>
  125. <style lang="scss" scoped>
  126. .collapse-panel {
  127. width: 100%;
  128. &__content {
  129. overflow: hidden;
  130. transition: max-height 0.3s ease;
  131. line-height: 1.5;
  132. &--collapsed {
  133. position: relative;
  134. // 添加渐变遮罩效果(可选)
  135. &::after {
  136. content: '';
  137. position: absolute;
  138. bottom: 0;
  139. left: 0;
  140. right: 0;
  141. height: 60rpx;
  142. background: linear-gradient(to bottom, transparent, #ffffff);
  143. pointer-events: none;
  144. }
  145. }
  146. }
  147. &__toggle {
  148. display: flex;
  149. align-items: center;
  150. justify-content: center;
  151. padding: 16rpx 0;
  152. color: #222222;
  153. font-size: 28rpx;
  154. &-text {
  155. margin-right: 8rpx;
  156. }
  157. &-icon {
  158. transition: transform 0.3s ease;
  159. font-size: 24rpx;
  160. display: flex;
  161. align-items: center;
  162. justify-content: center;
  163. width: 32rpx;
  164. height: 32rpx;
  165. &--expanded {
  166. transform: rotate(180deg);
  167. }
  168. }
  169. }
  170. }
  171. // 如果在小程序中需要更精确的文本行数控制,可以使用以下样式类
  172. .text-lines {
  173. display: -webkit-box;
  174. -webkit-box-orient: vertical;
  175. overflow: hidden;
  176. text-overflow: ellipsis;
  177. }
  178. </style>