cl-loading.uvue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <template>
  2. <view
  3. ref="loadingRef"
  4. class="cl-loading"
  5. :class="[
  6. {
  7. 'cl-loading--dark': isDark && color == '',
  8. 'cl-loading--spin': loading,
  9. '!border-r-transparent': true
  10. },
  11. pt.className
  12. ]"
  13. :style="{
  14. height: getPx(size!),
  15. width: getPx(size!),
  16. // #ifndef APP
  17. borderWidth: '1px',
  18. borderTopColor: color,
  19. borderRightColor: 'transparent',
  20. borderBottomColor: color,
  21. borderLeftColor: color
  22. // #endif
  23. }"
  24. v-if="loading"
  25. >
  26. </view>
  27. </template>
  28. <script setup lang="ts">
  29. import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
  30. import { ctx, isDark, parsePt } from "@/cool";
  31. import type { ClIconProps } from "../cl-icon/props";
  32. import { useSize } from "../../hooks";
  33. defineOptions({
  34. name: "cl-loading"
  35. });
  36. // 定义组件属性
  37. const props = defineProps({
  38. // 透传样式
  39. pt: {
  40. type: Object,
  41. default: () => ({})
  42. },
  43. // 是否加载中
  44. loading: {
  45. type: Boolean,
  46. default: true
  47. },
  48. // 图标大小
  49. size: {
  50. type: [Number, String],
  51. default: 24
  52. },
  53. // 图标颜色
  54. color: {
  55. type: String,
  56. default: ""
  57. }
  58. });
  59. const { getPxValue, getPx } = useSize();
  60. // 透传样式类型定义
  61. type PassThrough = {
  62. className?: string;
  63. icon?: ClIconProps;
  64. };
  65. // 解析透传样式
  66. const pt = computed(() => parsePt<PassThrough>(props.pt));
  67. // 组件引用
  68. const loadingRef = shallowRef<UniElement | null>(null);
  69. const color = computed<string>(() => {
  70. if (props.color == "") {
  71. return isDark.value ? "#ffffff" : (ctx.color["surface-700"] as string);
  72. }
  73. switch (props.color) {
  74. case "primary":
  75. return ctx.color["primary-500"] as string;
  76. case "success":
  77. return "#22c55e";
  78. case "warn":
  79. return "#eab308";
  80. case "error":
  81. return "#ef4444";
  82. case "info":
  83. return "#71717a";
  84. case "dark":
  85. return "#3f3f46";
  86. case "light":
  87. return "#ffffff";
  88. case "disabled":
  89. return "#d4d4d8";
  90. default:
  91. return props.color;
  92. }
  93. });
  94. async function drawLoading() {
  95. // #ifdef APP
  96. await nextTick();
  97. if (loadingRef.value == null) {
  98. return;
  99. }
  100. const drawContext = loadingRef.value!.getDrawableContext();
  101. // 重置画布,准备绘制新的loading图形
  102. drawContext!.reset();
  103. drawContext!.beginPath();
  104. // 获取loading图标的尺寸和半径
  105. const size = getPxValue(props.size!);
  106. const radius = size / 2;
  107. const centerX = radius;
  108. const centerY = radius;
  109. // 设置线宽
  110. const lineWidth = 1;
  111. // 缺口角度为60度(Math.PI / 3),用于形成loading的缺口效果
  112. const gapAngle = Math.PI / 3; // 缺口60度
  113. // 起始角度为顶部(-90度)
  114. const startAngle = -Math.PI / 2; // 从顶部开始
  115. // 结束角度为起始角度加上300度(360-60),形成环形缺口
  116. const endAngle = startAngle + (2 * Math.PI - gapAngle); // 画300度
  117. // 绘制圆弧,形成loading环
  118. drawContext!.arc(centerX, centerY, radius - lineWidth, startAngle, endAngle, false);
  119. // 设置描边颜色和线宽
  120. drawContext!.strokeStyle = color.value;
  121. drawContext!.lineWidth = lineWidth;
  122. // 执行描边操作
  123. drawContext!.stroke();
  124. // 更新画布显示
  125. drawContext!.update();
  126. // #endif
  127. }
  128. // 开始旋转动画
  129. async function start() {
  130. // #ifdef APP
  131. await drawLoading();
  132. if (loadingRef.value == null) {
  133. return;
  134. }
  135. loadingRef.value!.animate(
  136. [
  137. {
  138. transform: "rotate(0deg)"
  139. },
  140. {
  141. transform: "rotate(360deg)"
  142. }
  143. ],
  144. {
  145. duration: 2500,
  146. easing: "linear",
  147. iterations: 999999
  148. }
  149. );
  150. // #endif
  151. }
  152. // 组件挂载后监听loading状态
  153. onMounted(() => {
  154. // #ifdef APP
  155. watch(
  156. computed(() => props.loading),
  157. (val: boolean) => {
  158. // 当loading为true时开始旋转
  159. if (val) {
  160. start();
  161. }
  162. },
  163. {
  164. immediate: true
  165. }
  166. );
  167. watch(
  168. computed(() => [props.color, props.size, isDark.value]),
  169. () => {
  170. drawLoading();
  171. }
  172. );
  173. // #endif
  174. });
  175. </script>
  176. <style lang="scss" scoped>
  177. .cl-loading {
  178. @apply flex flex-row items-center justify-center rounded-full;
  179. // #ifndef APP
  180. @apply border-surface-700 border-solid;
  181. // #endif
  182. &--dark {
  183. border-color: white !important;
  184. border-right-color: transparent !important;
  185. }
  186. // #ifdef H5 || MP
  187. &--spin {
  188. animation: spin 2.5s linear infinite;
  189. }
  190. @keyframes spin {
  191. from {
  192. transform: rotate(0deg);
  193. }
  194. to {
  195. transform: rotate(360deg);
  196. }
  197. }
  198. // #endif
  199. }
  200. </style>