CollapsePanel.vue 4.4 KB

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