cl-skeleton.uvue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <template>
  2. <view
  3. class="cl-skeleton"
  4. :class="[
  5. {
  6. 'is-loading': loading,
  7. 'is-dark': isDark
  8. },
  9. pt.className,
  10. `cl-skeleton--${props.type}`,
  11. `${loading ? `${pt.loading?.className}` : ''}`
  12. ]"
  13. :style="{
  14. opacity: loading ? opacity : 1
  15. }"
  16. >
  17. <template v-if="loading">
  18. <cl-icon
  19. name="image-line"
  20. :pt="{
  21. className: '!text-surface-400'
  22. }"
  23. :size="40"
  24. v-if="type == 'image'"
  25. ></cl-icon>
  26. </template>
  27. <template v-else>
  28. <slot></slot>
  29. </template>
  30. </view>
  31. </template>
  32. <script setup lang="ts">
  33. import { computed, onMounted, ref, watch, type PropType } from "vue";
  34. import type { PassThroughProps } from "../../types";
  35. import { isDark, parsePt } from "@/cool";
  36. defineOptions({
  37. name: "cl-skeleton"
  38. });
  39. const props = defineProps({
  40. pt: {
  41. type: Object,
  42. default: () => ({})
  43. },
  44. loading: {
  45. type: Boolean,
  46. default: true
  47. },
  48. type: {
  49. type: String as PropType<"text" | "image" | "circle" | "button">,
  50. default: "text"
  51. }
  52. });
  53. type PassThrough = {
  54. className?: string;
  55. loading?: PassThroughProps;
  56. };
  57. const pt = computed(() => parsePt<PassThrough>(props.pt));
  58. const opacity = ref(0.3);
  59. let animationId: number = 0;
  60. let startTime: number;
  61. function start() {
  62. if (!props.loading) return;
  63. startTime = 0;
  64. function animate(currentTime: number) {
  65. if (startTime == 0) {
  66. startTime = currentTime;
  67. }
  68. // 计算动画进度 (0-1)
  69. const elapsed = currentTime - startTime;
  70. const progress = (elapsed % 2000) / 2000;
  71. // 使用正弦波形创建平滑的闪动效果
  72. // 从0.3到1.0之间变化
  73. const minOpacity = 0.3;
  74. const maxOpacity = 1.0;
  75. opacity.value =
  76. minOpacity + (maxOpacity - minOpacity) * (Math.sin(progress * Math.PI * 2) * 0.5 + 0.5);
  77. // 继续动画
  78. if (props.loading) {
  79. animationId = requestAnimationFrame((time) => {
  80. animate(time);
  81. });
  82. }
  83. }
  84. animationId = requestAnimationFrame((time) => {
  85. animate(time);
  86. });
  87. }
  88. /**
  89. * 停止闪动动画
  90. */
  91. function stop() {
  92. if (animationId != 0) {
  93. cancelAnimationFrame(animationId);
  94. animationId = 0;
  95. startTime = 0;
  96. }
  97. }
  98. onMounted(() => {
  99. // #ifdef APP
  100. watch(
  101. computed(() => props.loading),
  102. (val: boolean) => {
  103. if (val) {
  104. start();
  105. } else {
  106. stop();
  107. }
  108. },
  109. {
  110. immediate: true
  111. }
  112. );
  113. // #endif
  114. });
  115. </script>
  116. <style lang="scss" scoped>
  117. .cl-skeleton {
  118. &.is-loading {
  119. @apply bg-surface-100 rounded-md;
  120. &.is-dark {
  121. @apply bg-surface-600;
  122. }
  123. // #ifdef MP | H5
  124. animation: shimmer-opacity 2s infinite;
  125. // #endif
  126. &.cl-skeleton--text {
  127. height: 40rpx;
  128. width: 300rpx;
  129. }
  130. &.cl-skeleton--image {
  131. @apply flex flex-row items-center justify-center;
  132. width: 120rpx;
  133. height: 120rpx;
  134. border-radius: 16rpx;
  135. }
  136. &.cl-skeleton--circle {
  137. @apply rounded-full;
  138. width: 120rpx;
  139. height: 120rpx;
  140. }
  141. &.cl-skeleton--button {
  142. @apply rounded-lg;
  143. height: 66rpx;
  144. width: 150rpx;
  145. }
  146. }
  147. // #ifdef MP | H5
  148. @keyframes shimmer-opacity {
  149. 0% {
  150. opacity: 1;
  151. }
  152. 50% {
  153. opacity: 0.3;
  154. }
  155. 100% {
  156. opacity: 1;
  157. }
  158. }
  159. // #endif
  160. }
  161. </style>