wd-circle.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <template>
  2. <view :class="`wd-circle ${customClass}`" :style="customStyle">
  3. <!-- #ifdef MP-WEIXIN -->
  4. <canvas :style="canvasStyle" :id="canvasId" :canvas-id="canvasId" type="2d"></canvas>
  5. <!-- #endif -->
  6. <!-- #ifndef MP-WEIXIN -->
  7. <canvas :width="canvasSize" :height="canvasSize" :style="canvasStyle" :id="canvasId" :canvas-id="canvasId"></canvas>
  8. <!-- #endif -->
  9. <view v-if="!text" class="wd-circle__text">
  10. <!-- 自定义提示内容 -->
  11. <slot></slot>
  12. </view>
  13. <text v-else class="wd-circle__text">
  14. {{ text }}
  15. </text>
  16. </view>
  17. </template>
  18. <script lang="ts">
  19. export default {
  20. name: 'wd-circle',
  21. options: {
  22. addGlobalClass: true,
  23. virtualHost: true,
  24. styleIsolation: 'shared'
  25. }
  26. }
  27. </script>
  28. <script lang="ts" setup>
  29. import { computed, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
  30. import { addUnit, isObj, objToStyle, uuid } from '../common/util'
  31. import { circleProps } from './types'
  32. // #ifdef MP-WEIXIN
  33. import { canvas2dAdapter } from '../common/canvasHelper'
  34. // #endif
  35. // 大于等于0且小于等于100
  36. function format(rate: number) {
  37. return Math.min(Math.max(rate, 0), 100)
  38. }
  39. // 结束角度
  40. const PERIMETER = 2 * Math.PI
  41. // 开始角度
  42. const BEGIN_ANGLE = -Math.PI / 2
  43. const STEP = 1
  44. const props = defineProps(circleProps)
  45. const { proxy } = getCurrentInstance() as any
  46. const progressColor = ref<string | CanvasGradient>('') // 进度条颜色
  47. const currentValue = ref<number>(0) // 当前值
  48. const interval = ref<any>(null) // 定时器
  49. const pixelRatio = ref<number>(1) // 像素比
  50. const canvasId = ref<string>(`wd-circle${uuid()}`) // canvasId
  51. let ctx: UniApp.CanvasContext | null = null
  52. // canvas渲染大小
  53. const canvasSize = computed(() => {
  54. let size = props.size
  55. // #ifdef MP-ALIPAY
  56. size = size * pixelRatio.value
  57. // #endif
  58. return size
  59. })
  60. // 进度条宽度
  61. const sWidth = computed(() => {
  62. let sWidth = props.strokeWidth
  63. // #ifdef MP-ALIPAY
  64. sWidth = sWidth * pixelRatio.value
  65. // #endif
  66. return sWidth
  67. })
  68. // Circle 样式
  69. const canvasStyle = computed(() => {
  70. const style = {
  71. width: addUnit(props.size),
  72. height: addUnit(props.size)
  73. }
  74. return `${objToStyle(style)}`
  75. })
  76. // 监听目标数值变化
  77. watch(
  78. () => props.modelValue,
  79. () => {
  80. reRender()
  81. },
  82. { immediate: true }
  83. )
  84. // 监听Circle大小变化
  85. watch(
  86. () => props.size,
  87. () => {
  88. let timer = setTimeout(() => {
  89. drawCircle(currentValue.value)
  90. clearTimeout(timer)
  91. }, 50)
  92. },
  93. { immediate: false }
  94. )
  95. // 监听进度条颜色变化
  96. watch(
  97. () => props.color,
  98. () => {
  99. drawCircle(currentValue.value)
  100. },
  101. { immediate: false, deep: true }
  102. )
  103. onBeforeMount(() => {
  104. pixelRatio.value = uni.getSystemInfoSync().pixelRatio
  105. })
  106. onMounted(() => {
  107. currentValue.value = props.modelValue
  108. drawCircle(currentValue.value)
  109. })
  110. onUnmounted(() => {
  111. clearTimeInterval()
  112. })
  113. /**
  114. * 获取canvas上下文
  115. */
  116. function getContext() {
  117. return new Promise<UniApp.CanvasContext>((resolve) => {
  118. if (ctx) {
  119. return resolve(ctx)
  120. }
  121. // #ifndef MP-WEIXIN
  122. ctx = uni.createCanvasContext(canvasId.value, proxy)
  123. resolve(ctx)
  124. // #endif
  125. // #ifdef MP-WEIXIN
  126. uni
  127. .createSelectorQuery()
  128. .in(proxy)
  129. .select(`#${canvasId.value}`)
  130. .node((res) => {
  131. if (res && res.node) {
  132. const canvas = res.node
  133. ctx = canvas2dAdapter(canvas.getContext('2d') as CanvasRenderingContext2D)
  134. canvas.width = props.size * pixelRatio.value
  135. canvas.height = props.size * pixelRatio.value
  136. ctx.scale(pixelRatio.value, pixelRatio.value)
  137. resolve(ctx)
  138. }
  139. })
  140. .exec()
  141. // #endif
  142. })
  143. }
  144. /**
  145. * 设置canvas
  146. */
  147. function presetCanvas(context: any, strokeStyle: string | CanvasGradient, beginAngle: number, endAngle: number, fill?: string) {
  148. let width = sWidth.value
  149. const position = canvasSize.value / 2
  150. if (!fill) {
  151. width = width / 2
  152. }
  153. const radius = position - width / 2
  154. context.strokeStyle = strokeStyle
  155. context.setStrokeStyle(strokeStyle)
  156. context.setLineWidth(width)
  157. context.setLineCap(props.strokeLinecap)
  158. context.beginPath()
  159. context.arc(position, position, radius, beginAngle, endAngle, !props.clockwise)
  160. context.stroke()
  161. if (fill) {
  162. context.setLineWidth(width)
  163. context.setFillStyle(fill)
  164. context.fill()
  165. }
  166. }
  167. /**
  168. * 渲染管道
  169. */
  170. function renderLayerCircle(context: UniApp.CanvasContext) {
  171. presetCanvas(context, props.layerColor, 0, PERIMETER, props.fill)
  172. }
  173. /**
  174. * 渲染进度条
  175. */
  176. function renderHoverCircle(context: UniApp.CanvasContext, formatValue: number) {
  177. // 结束角度
  178. const progress = PERIMETER * (formatValue / 100)
  179. const endAngle = props.clockwise ? BEGIN_ANGLE + progress : 3 * Math.PI - (BEGIN_ANGLE + progress)
  180. // 设置进度条颜色
  181. if (isObj(props.color)) {
  182. const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
  183. Object.keys(props.color)
  184. .sort((a, b) => parseFloat(a) - parseFloat(b))
  185. .map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
  186. progressColor.value = LinearColor
  187. } else {
  188. progressColor.value = props.color
  189. }
  190. presetCanvas(context, progressColor.value, BEGIN_ANGLE, endAngle)
  191. }
  192. /**
  193. * 渲染圆点
  194. * 进度值为0时渲染一个圆点
  195. */
  196. function renderDot(context: UniApp.CanvasContext) {
  197. const strokeWidth = sWidth.value // 管道宽度=小圆点直径
  198. const position = canvasSize.value / 2 // 坐标
  199. // 设置进度条颜色
  200. if (isObj(props.color)) {
  201. const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
  202. Object.keys(props.color)
  203. .sort((a, b) => parseFloat(a) - parseFloat(b))
  204. .map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
  205. progressColor.value = LinearColor
  206. } else {
  207. progressColor.value = props.color
  208. }
  209. context.beginPath()
  210. context.arc(position, strokeWidth / 4, strokeWidth / 4, 0, PERIMETER)
  211. context.setFillStyle(progressColor.value)
  212. context.fill()
  213. }
  214. /**
  215. * 画圆
  216. */
  217. function drawCircle(currentValue: number) {
  218. getContext().then((context) => {
  219. context.clearRect(0, 0, canvasSize.value, canvasSize.value)
  220. renderLayerCircle(context)
  221. const formatValue = format(currentValue)
  222. if (formatValue !== 0) {
  223. renderHoverCircle(context, formatValue)
  224. } else {
  225. renderDot(context)
  226. }
  227. context.draw()
  228. })
  229. }
  230. /**
  231. * Circle组件渲染
  232. * 当前进度值变化时重新渲染Circle组件
  233. */
  234. function reRender() {
  235. // 动画通过定时器渲染
  236. if (props.speed <= 0 || props.speed > 1000) {
  237. drawCircle(props.modelValue)
  238. return
  239. }
  240. clearTimeInterval()
  241. currentValue.value = currentValue.value || 0
  242. const run = () => {
  243. interval.value = setTimeout(() => {
  244. if (currentValue.value !== props.modelValue) {
  245. if (Math.abs(currentValue.value - props.modelValue) < STEP) {
  246. currentValue.value = props.modelValue
  247. } else if (currentValue.value < props.modelValue) {
  248. currentValue.value += STEP
  249. } else {
  250. currentValue.value -= STEP
  251. }
  252. drawCircle(currentValue.value)
  253. run()
  254. } else {
  255. clearTimeInterval()
  256. }
  257. }, 1000 / props.speed)
  258. }
  259. run()
  260. }
  261. /**
  262. * 清除定时器
  263. */
  264. function clearTimeInterval() {
  265. interval.value && clearTimeout(interval.value)
  266. }
  267. </script>
  268. <style lang="scss" scoped>
  269. @import './index.scss';
  270. </style>