合同小程序前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

329 lines
7.6 KiB

1 week ago
  1. // type UseLoadingOtions = {
  2. // type: string,
  3. // color: string,
  4. // el: UniElement
  5. // }
  6. import {tinyColor} from '@/uni_modules/lime-color'
  7. function easeInOutCubic(t : number) : number {
  8. return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
  9. }
  10. type useLoadingReturnType = {
  11. state : Ref<boolean>
  12. color : Ref<string>
  13. play: () => void
  14. failed: () => void
  15. clear : () => void
  16. destroy : () => void
  17. }
  18. type Point = {
  19. x1: number
  20. y1: number
  21. x2: number
  22. y2: number
  23. }
  24. export function useLoading(
  25. element : Ref<UniElement | null>,
  26. type : 'circular' | 'spinner',
  27. strokeColor : string,
  28. ratio : number,
  29. immediate: boolean = false,
  30. ) : useLoadingReturnType {
  31. const state = ref(false)
  32. const color = ref(strokeColor)
  33. let tick = 0 // 0 不绘制 | 1 旋转 | 2 错误
  34. let init = false
  35. let isDestroy = ref(false)
  36. let width = 0
  37. let height = 0
  38. let size = 0
  39. let x = 0
  40. let y = 0
  41. let ctx : DrawableContext | null = null
  42. let timer = -1
  43. let isClear = false;
  44. let drawing = false;
  45. const updateSize = () => {
  46. if (element.value == null) return
  47. const rect = element.value!.getBoundingClientRect();
  48. ctx = element.value!.getDrawableContext()! as DrawableContext
  49. width = rect.width
  50. height = rect.height
  51. size = ratio > 1 ? ratio : Math.floor(Math.min(width, height) * ratio)
  52. x = width / 2
  53. y = height / 2
  54. }
  55. const circular = () => {
  56. if (ctx == null) return
  57. let _ctx = ctx!
  58. let startAngle = 0;
  59. let endAngle = 0;
  60. let startSpeed = 0;
  61. let endSpeed = 0;
  62. let rotate = 0;
  63. // 不使用360的原因是加上rotate后,会导致闪烁
  64. const ARC_LENGTH = 359.5
  65. const PI = Math.PI / 180
  66. const SPEED = 0.018
  67. const ROTATE_INTERVAL = 0.09
  68. const center = size / 2
  69. const lineWidth = size / 10;
  70. function draw() {
  71. if(isClear) return
  72. _ctx.reset();
  73. _ctx.beginPath();
  74. _ctx.arc(
  75. x,
  76. y,
  77. center - lineWidth,
  78. startAngle * PI + rotate,
  79. endAngle * PI + rotate);
  80. _ctx.lineWidth = lineWidth;
  81. _ctx.strokeStyle = color.value;
  82. _ctx.stroke();
  83. if (endAngle < ARC_LENGTH && startAngle == 0) {
  84. endSpeed += SPEED
  85. endAngle = Math.min(ARC_LENGTH, easeInOutCubic(endSpeed) * ARC_LENGTH)
  86. } else if (endAngle == ARC_LENGTH && startAngle < ARC_LENGTH) {
  87. startSpeed += SPEED
  88. startAngle = Math.min(ARC_LENGTH, easeInOutCubic(startSpeed) * ARC_LENGTH);
  89. } else if (endAngle >= ARC_LENGTH && startAngle >= ARC_LENGTH) {
  90. endSpeed = 0
  91. startSpeed = 0
  92. startAngle = 0;
  93. endAngle = 0;
  94. }
  95. rotate += ROTATE_INTERVAL;
  96. _ctx.update()
  97. // clearTimeout(timer)
  98. timer = setTimeout(() => draw(), 24)
  99. }
  100. draw()
  101. }
  102. const spinner = () => {
  103. if (ctx == null) return
  104. let _ctx = ctx!
  105. const steps = 12;
  106. let step = 0;
  107. const lineWidth = size / 10;
  108. // 线长度和距离圆心距离
  109. const length = size / 4 - lineWidth;
  110. const offset = size / 4;
  111. function generateColorGradient(hex: string, steps: number):string[]{
  112. const colors:string[] = []
  113. const _color = tinyColor(hex)
  114. for (let i = 1; i <= steps; i++) {
  115. _color.setAlpha(i/steps);
  116. colors.push(_color.toRgbString());
  117. }
  118. return colors
  119. }
  120. let colors = computed(():string[]=> generateColorGradient(color.value, steps))
  121. function draw() {
  122. if(tick == 0) return
  123. _ctx.reset();
  124. for (let i = 0; i < steps; i++) {
  125. const stepAngle = 360 / steps
  126. const angle = stepAngle * i;
  127. const index =(steps + i - (step % steps)) % steps
  128. // 正余弦
  129. const sin = Math.sin(angle / 180 * Math.PI);
  130. const cos = Math.cos(angle / 180 * Math.PI);
  131. // 开始绘制
  132. _ctx.lineWidth = lineWidth;
  133. _ctx.lineCap = 'round';
  134. _ctx.beginPath();
  135. _ctx.moveTo(size / 2 + offset * cos, size / 2 + offset * sin);
  136. _ctx.lineTo(size / 2 + (offset + length) * cos, size / 2 + (offset + length) * sin);
  137. _ctx.strokeStyle = colors.value[index]
  138. _ctx.stroke();
  139. }
  140. step += 1
  141. _ctx.update()
  142. timer = setTimeout(() => draw(), 1000/10)
  143. }
  144. draw()
  145. }
  146. const clear = () => {
  147. clearTimeout(timer)
  148. drawing = false
  149. tick = 0
  150. if(ctx == null) return
  151. // ctx?.reset()
  152. // ctx?.update()
  153. setTimeout(()=>{
  154. ctx!.reset()
  155. ctx!.update()
  156. },1000)
  157. }
  158. const failed = () => {
  159. if(tick == 1) {
  160. drawing = false
  161. }
  162. clearTimeout(timer)
  163. tick = 2
  164. if (ctx == null || drawing) return
  165. let _ctx = ctx!
  166. const _size = size * 0.61
  167. const _sizeX = _size * 0.65
  168. const lineWidth = _size / 6;
  169. const lineLength = Math.ceil(Math.sqrt(Math.pow(_sizeX, 2) * 2))
  170. const startX1 = (width - _sizeX) * 0.5
  171. const startY = (height - _sizeX) * 0.5
  172. const startX2 = startX1 + _sizeX
  173. // 添加圆的参数
  174. const centerX = width / 2;
  175. const centerY = height / 2;
  176. const radius = (_size * Math.sqrt(2)) / 2 + lineWidth / 2;
  177. const totalSteps = 36;
  178. function generateSteps(stepsCount: number):Point[][] {
  179. const halfStepsCount = stepsCount / 2;
  180. const step = lineLength / halfStepsCount //Math.floor(lineLength / 18);
  181. const steps:Point[][] = []
  182. for (let i = 0; i < stepsCount; i++) {
  183. const sub:Point[] = []
  184. const index = i % 18 + 1
  185. if(i < halfStepsCount) {
  186. const x2 = Math.sin(45 * Math.PI / 180) * step * index + startX1
  187. const y2 = Math.cos(45 * Math.PI / 180) * step * index + startY
  188. const start1 = {
  189. x1: startX1,
  190. y1: startY,
  191. x2,
  192. y2,
  193. } as Point
  194. sub.push(start1)
  195. } else {
  196. sub.push(steps[halfStepsCount-1][0])
  197. const x2 = Math.sin((45 - 90) * Math.PI / 180) * step * index + startX2
  198. const y2 = Math.cos((45 - 90) * Math.PI / 180) * step * index + startY
  199. const start2 = {
  200. x1: startX2,
  201. y1: startY,
  202. x2,
  203. y2,
  204. } as Point
  205. sub.push(start2)
  206. }
  207. steps.push(sub)
  208. }
  209. return steps
  210. }
  211. const steps = generateSteps(36);
  212. function draw(){
  213. if(steps.length == 0 || tick == 0) {
  214. clearTimeout(timer)
  215. return
  216. }
  217. const drawStep = steps.shift()!
  218. _ctx.reset()
  219. _ctx.lineWidth = lineWidth;
  220. _ctx.strokeStyle = color.value;
  221. // 绘制逐渐显示的圆
  222. _ctx.beginPath();
  223. _ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
  224. _ctx.lineWidth = lineWidth;
  225. _ctx.strokeStyle = color.value;
  226. _ctx.stroke();
  227. // 绘制X
  228. _ctx.beginPath();
  229. drawStep.forEach(item => {
  230. _ctx.beginPath();
  231. _ctx.moveTo(item.x1, item.y1)
  232. _ctx.lineTo(item.x2, item.y2)
  233. _ctx.stroke();
  234. })
  235. _ctx.update()
  236. timer = setTimeout(() => draw(), 1000/30)
  237. }
  238. draw()
  239. }
  240. const destroy = () => {
  241. isDestroy.value = true;
  242. clear()
  243. }
  244. const play = () => {
  245. if(tick == 2) {
  246. drawing = false
  247. }
  248. if(drawing) return
  249. tick = 1
  250. if(width == 0 || height == 0) return
  251. if (type == 'circular') {
  252. circular()
  253. } else if (type == 'spinner') {
  254. spinner()
  255. }
  256. drawing = true
  257. }
  258. const _watch = (v:boolean) => {
  259. if(isDestroy.value) return
  260. if (v) {
  261. play()
  262. } else {
  263. failed()
  264. }
  265. }
  266. const stopWatchState = watch(state, _watch)
  267. const ob = new UniResizeObserver((entries: UniResizeObserverEntry[])=>{
  268. if(isDestroy.value) return
  269. entries.forEach(entry => {
  270. if(isDestroy.value) return
  271. const rect = entry.target.getBoundingClientRect();
  272. if(rect.width > 0 && rect.height > 0) {
  273. updateSize();
  274. if(tick == 1) {
  275. play()
  276. state.value = true
  277. } else if(tick == 2) {
  278. failed()
  279. state.value = false
  280. } else if(immediate && !init) {
  281. _watch(state.value)
  282. init = true
  283. }
  284. }
  285. })
  286. })
  287. const stopWatchElement = watch(element, (el:UniElement|null) => {
  288. if(el == null || isDestroy.value) return
  289. ob.observe(el)
  290. })
  291. onUnmounted(()=>{
  292. stopWatchState()
  293. stopWatchElement()
  294. clear()
  295. ob.disconnect()
  296. })
  297. return {
  298. state,
  299. play,
  300. failed,
  301. clear,
  302. color,
  303. destroy
  304. } as useLoadingReturnType
  305. }