|
@@ -0,0 +1,182 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ ...styleMap?.mainStyle
|
|
|
|
|
+ }"
|
|
|
|
|
+ class="box-border overflow-hidden relative"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg
|
|
|
|
|
+ :viewBox="`0 0 ${width} ${height}`"
|
|
|
|
|
+ preserveAspectRatio="xMinYMin meet"
|
|
|
|
|
+ class="w-full h-full"
|
|
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
+ :style="{ transform: `rotate(${props.rotate}deg)` }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 背景条:绘制完整的起始到结束角度 -->
|
|
|
|
|
+ <path
|
|
|
|
|
+ :d="bgPath"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ :stroke="styles?.mainStyle?.curve?.color || '#eeeeee'"
|
|
|
|
|
+ :stroke-width="styles?.mainStyle?.curve?.width || 1"
|
|
|
|
|
+ :stroke-linecap="styles?.mainStyle?.curve?.radius ? 'round' : 'butt'"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 进度值条 -->
|
|
|
|
|
+ <path
|
|
|
|
|
+ :d="valuePath"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ :stroke="styles?.indicatorStyle?.curve?.color || '#eeeeee'"
|
|
|
|
|
+ :stroke-width="styles?.indicatorStyle?.curve?.width || 1"
|
|
|
|
|
+ :stroke-linecap="styles?.indicatorStyle?.curve?.radius ? 'round' : 'butt'"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 进度圆点 -->
|
|
|
|
|
+ <circle
|
|
|
|
|
+ v-if="dotPos"
|
|
|
|
|
+ :cx="dotPos.x"
|
|
|
|
|
+ :cy="dotPos.y"
|
|
|
|
|
+ :r="styles?.knobStyle?.padding?.left || 5"
|
|
|
|
|
+ :fill="styles?.knobStyle?.backgroundColor || '#2092f5'"
|
|
|
|
|
+ />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed } from 'vue'
|
|
|
|
|
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps<{
|
|
|
|
|
+ width: number
|
|
|
|
|
+ height: number
|
|
|
|
|
+ styles: any
|
|
|
|
|
+ state?: string
|
|
|
|
|
+ part?: string
|
|
|
|
|
+ mode: 'normal' | 'symmetrical' | 'reverse'
|
|
|
|
|
+ rangeStart: number
|
|
|
|
|
+ rangeEnd: number
|
|
|
|
|
+ angleStart: number
|
|
|
|
|
+ angleEnd: number
|
|
|
|
|
+ value: number
|
|
|
|
|
+ rotate: number
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const styleMap = useWidgetStyle({
|
|
|
|
|
+ widget: 'lv_arc',
|
|
|
|
|
+ props
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const cx = computed(() => {
|
|
|
|
|
+ const { width, height } = props
|
|
|
|
|
+ const min = Math.min(width, height)
|
|
|
|
|
+ return min / 2
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 极坐标转直角坐标
|
|
|
|
|
+ * SVG 角度习惯:0度在右侧(3点钟),需调整使0度在上方(12点钟)
|
|
|
|
|
+ */
|
|
|
|
|
+function polarToCartesian(
|
|
|
|
|
+ centerX: number,
|
|
|
|
|
+ centerY: number,
|
|
|
|
|
+ radius: number,
|
|
|
|
|
+ angleInDegrees: number
|
|
|
|
|
+) {
|
|
|
|
|
+ // LVGL 习惯通常 0 度在右侧,如果需要 0 度在上方,这里减去 90
|
|
|
|
|
+ // 这里我们遵循标准:angleStart 为输入值
|
|
|
|
|
+ const radians = ((angleInDegrees - 0) * Math.PI) / 180.0
|
|
|
|
|
+ return {
|
|
|
|
|
+ x: centerX + radius * Math.cos(radians),
|
|
|
|
|
+ y: centerY + radius * Math.sin(radians)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 生成 SVG 弧线路径指令
|
|
|
|
|
+ */
|
|
|
|
|
+function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
|
|
|
|
|
+ // 如果起始角度大于结束角度,交换位置(取决于业务逻辑,这里默认顺时针绘制)
|
|
|
|
|
+ const isReversed = startAngle > endAngle
|
|
|
|
|
+ const start = polarToCartesian(x, y, radius, isReversed ? endAngle : startAngle)
|
|
|
|
|
+ const end = polarToCartesian(x, y, radius, isReversed ? startAngle : endAngle)
|
|
|
|
|
+
|
|
|
|
|
+ const diff = Math.abs(endAngle - startAngle)
|
|
|
|
|
+ const largeArcFlag = diff <= 180 ? '0' : '1'
|
|
|
|
|
+ // sweep-flag: 1 为顺时针
|
|
|
|
|
+ const sweepFlag = isReversed ? '0' : '1'
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'M',
|
|
|
|
|
+ start.x,
|
|
|
|
|
+ start.y,
|
|
|
|
|
+ 'A',
|
|
|
|
|
+ radius,
|
|
|
|
|
+ radius,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ largeArcFlag,
|
|
|
|
|
+ sweepFlag,
|
|
|
|
|
+ end.x,
|
|
|
|
|
+ end.y
|
|
|
|
|
+ ].join(' ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 1. 背景路径
|
|
|
|
|
+const bgPath = computed(() => {
|
|
|
|
|
+ const { width, height } = props
|
|
|
|
|
+ const min = Math.min(width, height)
|
|
|
|
|
+ const r = min / 2
|
|
|
|
|
+ return describeArc(r, r, r - 10, props.angleStart, props.angleEnd)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 2. 进度计算逻辑
|
|
|
|
|
+const progressData = computed(() => {
|
|
|
|
|
+ const { value, rangeStart, rangeEnd, angleStart, angleEnd, mode } = props
|
|
|
|
|
+
|
|
|
|
|
+ // 基础百分比计算 (0-1)
|
|
|
|
|
+ let ratio = (value - rangeStart) / (rangeEnd - rangeStart)
|
|
|
|
|
+ ratio = Math.max(0, Math.min(1, ratio)) // 限制在 0-1
|
|
|
|
|
+
|
|
|
|
|
+ let startA = angleStart
|
|
|
|
|
+ let endA = angleEnd
|
|
|
|
|
+
|
|
|
|
|
+ if (mode === 'normal') {
|
|
|
|
|
+ endA = angleStart + (angleEnd - angleStart) * ratio
|
|
|
|
|
+ } else if (mode === 'reverse') {
|
|
|
|
|
+ // 从终点往回走
|
|
|
|
|
+ startA = angleEnd - (angleEnd - angleStart) * ratio
|
|
|
|
|
+ endA = angleEnd
|
|
|
|
|
+ } else if (mode === 'symmetrical') {
|
|
|
|
|
+ // 对称模式:通常 rangeStart 到 rangeEnd 的中间值是起点
|
|
|
|
|
+ const midAngle = (angleStart + angleEnd) / 2
|
|
|
|
|
+ const midValue = (rangeStart + rangeEnd) / 2
|
|
|
|
|
+
|
|
|
|
|
+ if (value >= midValue) {
|
|
|
|
|
+ startA = midAngle
|
|
|
|
|
+ const halfRatio = (value - midValue) / (rangeEnd - midValue)
|
|
|
|
|
+ endA = midAngle + (angleEnd - midAngle) * halfRatio
|
|
|
|
|
+ } else {
|
|
|
|
|
+ endA = midAngle
|
|
|
|
|
+ const halfRatio = (midValue - value) / (midValue - rangeStart)
|
|
|
|
|
+ startA = midAngle - (midAngle - angleStart) * halfRatio
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { startA, endA }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 3. 值路径
|
|
|
|
|
+const valuePath = computed(() => {
|
|
|
|
|
+ const { startA, endA } = progressData.value
|
|
|
|
|
+ if (startA === endA) return ''
|
|
|
|
|
+ return describeArc(cx.value, cx.value, cx.value, startA, endA)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 4. 圆点位置
|
|
|
|
|
+const dotPos = computed(() => {
|
|
|
|
|
+ // const { endA } = progressData.value
|
|
|
|
|
+ // 如果是 reverse 模式,圆点可能应该在 startA(取决于视觉习惯)
|
|
|
|
|
+ // 这里默认跟随进度的“活动端”
|
|
|
|
|
+ const angle = props.mode === 'reverse' ? progressData.value.startA : progressData.value.endA
|
|
|
|
|
+ return polarToCartesian(cx.value, cx.value, cx.value, angle)
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|