cl-progress-circle.uvue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <template>
  2. <view class="cl-progress-circle" :class="[pt.className]">
  3. <canvas
  4. class="cl-progress-circle__canvas"
  5. :id="canvasId"
  6. :style="{
  7. height: `${props.size}px`,
  8. width: `${props.size}px`
  9. }"
  10. ></canvas>
  11. <slot name="text">
  12. <cl-text
  13. :value="`${value}${unit}`"
  14. :pt="{
  15. className: parseClass(['absolute', pt.text?.className])
  16. }"
  17. v-if="showText"
  18. ></cl-text>
  19. </slot>
  20. </view>
  21. </template>
  22. <script lang="ts" setup>
  23. import { getColor, getDevicePixelRatio, isDark, parseClass, parsePt, uuid } from "@/cool";
  24. import { computed, getCurrentInstance, onMounted, ref, watch, type PropType } from "vue";
  25. import type { PassThroughProps } from "../../types";
  26. defineOptions({
  27. name: "cl-progress-circle"
  28. });
  29. const props = defineProps({
  30. pt: {
  31. type: Object,
  32. default: () => ({})
  33. },
  34. // 数值 (0-100)
  35. value: {
  36. type: Number,
  37. default: 0
  38. },
  39. // 圆形大小
  40. size: {
  41. type: Number,
  42. default: 120
  43. },
  44. // 线条宽度
  45. strokeWidth: {
  46. type: Number,
  47. default: 8
  48. },
  49. // 进度条颜色
  50. color: {
  51. type: String as PropType<string | null>,
  52. default: null
  53. },
  54. // 底色
  55. unColor: {
  56. type: String as PropType<string | null>,
  57. default: null
  58. },
  59. // 是否显示文本
  60. showText: {
  61. type: Boolean,
  62. default: true
  63. },
  64. // 单位
  65. unit: {
  66. type: String,
  67. default: "%"
  68. },
  69. // 起始角度 (弧度)
  70. startAngle: {
  71. type: Number,
  72. default: -Math.PI / 2
  73. },
  74. // 是否顺时针
  75. clockwise: {
  76. type: Boolean,
  77. default: true
  78. },
  79. // 动画时长
  80. duration: {
  81. type: Number,
  82. default: 500
  83. }
  84. });
  85. const { proxy } = getCurrentInstance()!;
  86. // 获取设备像素比
  87. const dpr = getDevicePixelRatio();
  88. // 透传样式类型定义
  89. type PassThrough = {
  90. className?: string;
  91. text?: PassThroughProps;
  92. };
  93. // 解析透传样式配置
  94. const pt = computed(() => parsePt<PassThrough>(props.pt));
  95. // canvas组件上下文
  96. let canvasCtx: CanvasContext | null = null;
  97. // 绘图上下文
  98. let drawCtx: CanvasRenderingContext2D | null = null;
  99. // 生成唯一的canvas ID
  100. const canvasId = `cl-progress-circle__${uuid()}`;
  101. // 当前显示值
  102. const value = ref(0);
  103. // 绘制圆形进度条
  104. function drawProgress() {
  105. if (drawCtx == null) return;
  106. const centerX = (props.size / 2) * dpr;
  107. const centerY = (props.size / 2) * dpr;
  108. const radius = ((props.size - props.strokeWidth) / 2) * dpr;
  109. // 清除画布
  110. // #ifdef APP
  111. drawCtx!.reset();
  112. // #endif
  113. // #ifndef APP
  114. drawCtx!.clearRect(0, 0, props.size * dpr, props.size * dpr);
  115. // #endif
  116. // 优化渲染质量
  117. drawCtx!.textBaseline = "middle";
  118. drawCtx!.textAlign = "center";
  119. drawCtx!.miterLimit = 10;
  120. // 保存当前状态
  121. drawCtx!.save();
  122. // 优化的圆环绘制
  123. const drawCircle = (startAngle: number, endAngle: number, color: string) => {
  124. if (drawCtx == null) return;
  125. drawCtx!.beginPath();
  126. drawCtx!.arc(centerX, centerY, radius, startAngle, endAngle, false);
  127. drawCtx!.strokeStyle = color;
  128. drawCtx!.lineWidth = props.strokeWidth * dpr;
  129. drawCtx!.lineCap = "round";
  130. drawCtx!.lineJoin = "round";
  131. drawCtx!.stroke();
  132. };
  133. // 绘制底色圆环
  134. drawCircle(
  135. 0,
  136. 2 * Math.PI,
  137. props.unColor ?? (isDark.value ? getColor("surface-700") : getColor("surface-200"))
  138. );
  139. // 绘制进度圆弧
  140. if (value.value > 0) {
  141. const progress = Math.max(0, Math.min(100, value.value)) / 100;
  142. const endAngle = props.startAngle + (props.clockwise ? 1 : -1) * 2 * Math.PI * progress;
  143. drawCircle(props.startAngle, endAngle, props.color ?? getColor("primary-500"));
  144. }
  145. }
  146. // 动画更新数值
  147. function animate(targetValue: number) {
  148. const startValue = value.value;
  149. const startTime = Date.now();
  150. function update() {
  151. // 获取当前时间
  152. const currentTime = Date.now();
  153. // 计算动画经过的时间
  154. const elapsed = currentTime - startTime;
  155. // 计算动画进度
  156. const progress = Math.min(elapsed / props.duration, 1);
  157. // 缓动函数
  158. const easedProgress = 1 - Math.pow(1 - progress, 3);
  159. // 计算当前值
  160. value.value = Math.round(startValue + (targetValue - startValue) * easedProgress);
  161. // 绘制进度条
  162. drawProgress();
  163. if (progress < 1) {
  164. if (canvasCtx != null) {
  165. // @ts-ignore
  166. canvasCtx!.requestAnimationFrame(() => {
  167. update();
  168. });
  169. }
  170. }
  171. }
  172. update();
  173. }
  174. // 初始化画布
  175. function initCanvas() {
  176. uni.createCanvasContextAsync({
  177. id: canvasId,
  178. component: proxy,
  179. success: (context: CanvasContext) => {
  180. // 设置canvas上下文
  181. canvasCtx = context;
  182. // 获取绘图上下文
  183. drawCtx = context.getContext("2d")!;
  184. // 设置宽高
  185. drawCtx!.canvas.width = props.size * dpr;
  186. drawCtx!.canvas.height = props.size * dpr;
  187. // 开始动画
  188. animate(props.value);
  189. }
  190. });
  191. }
  192. onMounted(() => {
  193. initCanvas();
  194. // 监听value变化
  195. watch(
  196. computed(() => props.value),
  197. (val: number) => {
  198. animate(Math.max(0, Math.min(100, val)));
  199. },
  200. {
  201. immediate: true
  202. }
  203. );
  204. watch(
  205. computed(() => [props.color, props.unColor, isDark.value]),
  206. () => {
  207. drawProgress();
  208. }
  209. );
  210. });
  211. defineExpose({
  212. animate
  213. });
  214. </script>
  215. <style lang="scss" scoped>
  216. .cl-progress-circle {
  217. @apply flex flex-col items-center justify-center relative;
  218. }
  219. </style>