|
|
@@ -1,62 +1,75 @@
|
|
|
<template>
|
|
|
- <div :style="{
|
|
|
- ...styleMap?.mainStyle
|
|
|
- }" class="relative w-full h-full box-border overflow-hidden relative">
|
|
|
+ <div
|
|
|
+ :style="{
|
|
|
+ ...styleMap?.mainStyle
|
|
|
+ }"
|
|
|
+ class="relative w-full h-full box-border relative"
|
|
|
+ >
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.mainStyle?.imageSrc"
|
|
|
+ :src="styleMap?.mainStyle?.imageSrc"
|
|
|
+ :imageStyle="styleMap?.mainStyle?.imageStyle"
|
|
|
+ />
|
|
|
|
|
|
- <ImageBg v-if="styleMap?.mainStyle?.imageSrc" :src="styleMap?.mainStyle?.imageSrc"
|
|
|
- :imageStyle="styleMap?.mainStyle?.imageStyle" />
|
|
|
-
|
|
|
- <div class="absolute inset-0 w-full h-full" :style="{ transform: `rotate(${props.rotate}deg)` }">
|
|
|
- <svg :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="xMidYMid meet" class="w-full h-full block"
|
|
|
- xmlns="http://www.w3.org/2000/svg">
|
|
|
- <defs>
|
|
|
- <!-- 背景条图片 pattern:保持图像原始尺寸,居中显示 -->
|
|
|
- <pattern v-if="styleMap?.mainStyle?.curve?.imageSrc" :id="`${arcId}-bg-pattern`" patternUnits="userSpaceOnUse"
|
|
|
- :width="width" :height="height" x="0" y="0">
|
|
|
- <image :href="styleMap.mainStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
|
|
|
- </pattern>
|
|
|
- <!-- 进度值条图片 pattern:保持图像原始尺寸,居中显示 -->
|
|
|
- <pattern v-if="styleMap?.indicatorStyle?.curve?.imageSrc" :id="`${arcId}-indicator-pattern`"
|
|
|
- patternUnits="userSpaceOnUse" :width="width" :height="height" x="0" y="0">
|
|
|
- <image :href="styleMap.indicatorStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
|
|
|
- </pattern>
|
|
|
- </defs>
|
|
|
- <!-- 背景条:绘制完整的起始到结束角度 -->
|
|
|
- <path :d="bgPath" fill="none"
|
|
|
- :stroke="styleMap?.mainStyle?.curve?.imageSrc ? `url(#${arcId}-bg-pattern)` : (styleMap?.mainStyle?.curve?.color || '#eeeeee')"
|
|
|
- :stroke-width="styleMap?.mainStyle?.curve?.width ?? 1"
|
|
|
- :stroke-linecap="styleMap?.mainStyle?.curve?.radius ? 'round' : 'butt'"
|
|
|
- :opacity="styleMap?.mainStyle?.curve?.opacity" />
|
|
|
-
|
|
|
- <!-- 进度值条 -->
|
|
|
- <path :d="valuePath" fill="none"
|
|
|
- :stroke="styleMap?.indicatorStyle?.curve?.imageSrc ? `url(#${arcId}-indicator-pattern)` : (styleMap?.indicatorStyle?.curve?.color || '#2092f5')"
|
|
|
- :stroke-width="styleMap?.indicatorStyle?.curve?.width ?? 1"
|
|
|
- :stroke-linecap="styleMap?.indicatorStyle?.curve?.radius ? 'round' : 'butt'"
|
|
|
- :opacity="styleMap?.indicatorStyle?.curve?.opacity" />
|
|
|
-
|
|
|
- <!-- 进度圆点:无背景图时用实心圆 -->
|
|
|
- <circle v-if="dotPos && !styleMap?.knobStyle?.imageSrc" :cx="dotPos.x" :cy="dotPos.y" :r="knobRadius.radius"
|
|
|
- :fill="styleMap?.knobStyle?.backgroundColor || '#2092f5'" />
|
|
|
+ <div class="absolute inset-0 w-full h-full">
|
|
|
+ <svg
|
|
|
+ :viewBox="`0 0 ${width} ${height}`"
|
|
|
+ preserveAspectRatio="xMidYMid meet"
|
|
|
+ class="w-full h-full block"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ >
|
|
|
+ <defs></defs>
|
|
|
+ <!-- 主轴直线:按 area 分段绘制,每段使用对应范围的直线颜色 -->
|
|
|
+ <line
|
|
|
+ v-for="(seg, segIdx) in axisSegments"
|
|
|
+ :key="'axis-' + segIdx"
|
|
|
+ :x1="seg.x1"
|
|
|
+ :y1="seg.y1"
|
|
|
+ :x2="seg.x2"
|
|
|
+ :y2="seg.y2"
|
|
|
+ :stroke="seg.stroke"
|
|
|
+ :stroke-width="mainLineWidth"
|
|
|
+ stroke-linecap="butt"
|
|
|
+ />
|
|
|
+ <!-- 刻度线:所有刻度,按 area 使用对应直线颜色 -->
|
|
|
+ <line
|
|
|
+ v-for="(pos, idx) in tickPositions"
|
|
|
+ :key="'tick-' + idx"
|
|
|
+ :x1="pos.x1"
|
|
|
+ :y1="pos.y1"
|
|
|
+ :x2="pos.x2"
|
|
|
+ :y2="pos.y2"
|
|
|
+ :stroke="pos.stroke"
|
|
|
+ :stroke-width="itemsLineWidth"
|
|
|
+ stroke-linecap="butt"
|
|
|
+ />
|
|
|
+ <!-- 标签(仅主刻度位置显示),按 area 使用对应文本颜色 -->
|
|
|
+ <template v-if="enableLabels">
|
|
|
+ <text
|
|
|
+ v-for="(lb, idx) in labelList"
|
|
|
+ :key="'label-' + idx"
|
|
|
+ :x="lb.x"
|
|
|
+ :y="lb.y"
|
|
|
+ :text-anchor="lb.textAnchor"
|
|
|
+ :dominant-baseline="lb.dominantBaseline"
|
|
|
+ :fill="lb.fill"
|
|
|
+ :font-size="mainTextSize"
|
|
|
+ :font-family="mainFontFamily"
|
|
|
+ >
|
|
|
+ {{ lb.text }}
|
|
|
+ </text>
|
|
|
+ </template>
|
|
|
</svg>
|
|
|
-
|
|
|
- <!-- 进度圆点背景图:与 SVG 同坐标系,用 ImageBg 统一实现 -->
|
|
|
- <div v-if="dotPos && styleMap?.knobStyle?.imageSrc" class="absolute overflow-hidden pointer-events-none"
|
|
|
- :style="knobOverlayStyle">
|
|
|
- <ImageBg :src="styleMap.knobStyle.imageSrc" :imageStyle="styleMap.knobStyle.imageStyle" />
|
|
|
- </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed } from 'vue'
|
|
|
import { useWidgetStyle } from '../hooks/useWidgetStyle'
|
|
|
-import { useProjectStore } from '@/store/modules/project';
|
|
|
-
|
|
|
-import ImageBg from '../ImageBg.vue';
|
|
|
|
|
|
-const arcId = ref('arc-' + Math.random().toString(36).slice(2))
|
|
|
+import ImageBg from '../ImageBg.vue'
|
|
|
+import type { AreaSection } from './Config.vue'
|
|
|
|
|
|
const props = defineProps<{
|
|
|
width: number
|
|
|
@@ -64,196 +77,296 @@ const props = defineProps<{
|
|
|
styles: any
|
|
|
state?: string
|
|
|
part?: string
|
|
|
- mode: 'normal' | 'symmetrical' | 'reverse'
|
|
|
+ mode: 'horizontal_top' | 'horizontal_bottom' | 'vertical_left' | 'vertical_right'
|
|
|
+ tick: number
|
|
|
+ mainTick: number
|
|
|
rangeStart: number
|
|
|
rangeEnd: number
|
|
|
- angleStart: number
|
|
|
- angleEnd: number
|
|
|
- value: number
|
|
|
- rotate: number
|
|
|
- // 偏移开关
|
|
|
- rotateOffset?: boolean
|
|
|
- // 偏移量(度数)
|
|
|
- rotateOffsetValue?: number
|
|
|
+ enableLabels: boolean
|
|
|
+ labels: string
|
|
|
+ area: AreaSection[]
|
|
|
}>()
|
|
|
|
|
|
-const projectStore = useProjectStore()
|
|
|
-
|
|
|
const styleMap = useWidgetStyle({
|
|
|
- widget: 'lv_arc',
|
|
|
+ widget: 'lv_scale',
|
|
|
props
|
|
|
})
|
|
|
|
|
|
-const cx = computed(() => {
|
|
|
- const { width, height } = props
|
|
|
- const min = Math.min(width, height)
|
|
|
- return min / 2
|
|
|
-})
|
|
|
-
|
|
|
-/** 弧线半径:与背景/进度条一致,留出边距避免贴边 */
|
|
|
-const trackRadius = computed(() => {
|
|
|
- const { width, height } = props
|
|
|
- const min = Math.min(width, height)
|
|
|
- const curveWidth = styleMap.value?.mainStyle?.curve?.width ?? 12
|
|
|
- return Math.max(4, min / 2 - curveWidth / 2 - knobRadius.value.padding)
|
|
|
+// 归一化后的 area 列表(start/end 为数值范围,颜色为 #hex 字符串)
|
|
|
+const normalizedAreas = computed(() => {
|
|
|
+ const list = props.area || []
|
|
|
+ return list.map(
|
|
|
+ (s: AreaSection & { textColor?: string | number; lineColor?: string | number }) => ({
|
|
|
+ start: Number(s.start),
|
|
|
+ end: Number(s.end),
|
|
|
+ textColor: s.textColor,
|
|
|
+ lineColor: s.lineColor
|
|
|
+ })
|
|
|
+ )
|
|
|
})
|
|
|
|
|
|
-/** 从 schema 的 styles 中解析 knob 的 padding.left 作为圆点半径 */
|
|
|
-const knobRadius = computed(() => {
|
|
|
- const defaultIndicatorStyle = projectStore.globalStyle
|
|
|
- ?.find((item) => item.widget === 'lv_arc')
|
|
|
- ?.part?.find((item) => item.partName === 'indicator')?.defaultStyle
|
|
|
+// 根据归一化位置 t∈[0,1] 得到数值
|
|
|
+const valueAt = (t: number) => props.rangeStart + t * (props.rangeEnd - props.rangeStart)
|
|
|
|
|
|
- const defaultKnobStyle = projectStore.globalStyle
|
|
|
- ?.find((item) => item.widget === 'lv_arc')
|
|
|
- ?.part?.find((item) => item.partName === 'knob')?.defaultStyle
|
|
|
-
|
|
|
- const indicatorStyle = props.styles?.find((s: any) => s.part?.name === 'indicator' && s.part.state === props.state)
|
|
|
- const knobStyle = props.styles?.find((s: any) => s.part?.name === 'knob' && s.part.state === props.state)
|
|
|
- const padding = knobStyle?.padding?.left ?? defaultKnobStyle?.padding?.left
|
|
|
- const r = indicatorStyle?.curve?.width ?? defaultIndicatorStyle?.curve?.width ?? 12
|
|
|
-
|
|
|
- return {
|
|
|
- radius: r / 2 + padding,
|
|
|
- padding: padding,
|
|
|
- width: r
|
|
|
+// 根据数值取所在 area 的直线颜色,否则用默认
|
|
|
+const getLineColorForValue = (value: number): string => {
|
|
|
+ for (const s of normalizedAreas.value) {
|
|
|
+ if (value >= s.start && value <= s.end) return s.lineColor
|
|
|
}
|
|
|
-})
|
|
|
+ return itemsLineColor.value
|
|
|
+}
|
|
|
|
|
|
-/**
|
|
|
- * 极坐标转直角坐标
|
|
|
- * 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)
|
|
|
+// 根据数值取所在 area 的文本颜色,否则用默认
|
|
|
+const getTextColorForValue = (value: number): string => {
|
|
|
+ for (const s of normalizedAreas.value) {
|
|
|
+ if (value >= s.start && value <= s.end) return s.textColor
|
|
|
}
|
|
|
+ return mainTextColor.value
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 生成 SVG 弧线路径指令
|
|
|
- * 始终从 startAngle 顺时针画到 endAngle,得到“近整圆、缺口在底部”的环(图2效果)
|
|
|
- */
|
|
|
-function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
|
|
|
- const start = polarToCartesian(x, y, radius, startAngle)
|
|
|
- const end = polarToCartesian(x, y, radius, endAngle)
|
|
|
- // 顺时针扫过的角度(0~360)
|
|
|
- let clockwiseSpan = (endAngle - startAngle) % 360
|
|
|
- if (clockwiseSpan <= 0) clockwiseSpan += 360
|
|
|
- if (clockwiseSpan === 0) clockwiseSpan = 360
|
|
|
- const largeArcFlag = clockwiseSpan > 180 ? '1' : '0'
|
|
|
- const sweepFlag = '1' // 顺时针
|
|
|
+// 从 styles 中读取 items 的 other.length(刻度线长度)
|
|
|
+const itemsStyleConfig = computed(() =>
|
|
|
+ props.styles?.find(
|
|
|
+ (s: any) =>
|
|
|
+ s.part?.name === 'items' && (s.part?.state === props.state || s.part?.state === 'default')
|
|
|
+ )
|
|
|
+)
|
|
|
+const tickLength = computed(() => itemsStyleConfig.value?.other?.length ?? 5)
|
|
|
+const mainTickLength = computed(() => Math.max(tickLength.value * 1.5, tickLength.value + 4))
|
|
|
|
|
|
- return [
|
|
|
- 'M',
|
|
|
- start.x,
|
|
|
- start.y,
|
|
|
- 'A',
|
|
|
- radius,
|
|
|
- radius,
|
|
|
- 0,
|
|
|
- largeArcFlag,
|
|
|
- sweepFlag,
|
|
|
- end.x,
|
|
|
- end.y
|
|
|
- ].join(' ')
|
|
|
-}
|
|
|
+const mainLineColor = computed(() => styleMap.value?.mainStyle?.line?.color ?? '#212121')
|
|
|
+const mainLineWidth = computed(() => styleMap.value?.mainStyle?.line?.width ?? 2)
|
|
|
+const itemsLineColor = computed(() => styleMap.value?.itemsStyle?.line?.color ?? '#212121')
|
|
|
+const itemsLineWidth = computed(() => styleMap.value?.itemsStyle?.line?.width ?? 2)
|
|
|
|
|
|
-// 1. 背景路径
|
|
|
-const bgPath = computed(() => {
|
|
|
- const r = trackRadius.value
|
|
|
- return describeArc(cx.value, cx.value, r, props.angleStart, props.angleEnd)
|
|
|
-})
|
|
|
+const mainTextColor = computed(() => styleMap.value?.mainStyle?.color ?? '#000000')
|
|
|
+const mainTextSize = computed(() => styleMap.value?.mainStyle?.fontSize ?? '16px')
|
|
|
+const mainFontFamily = computed(() => styleMap.value?.mainStyle?.fontFamily ?? 'sans-serif')
|
|
|
+
|
|
|
+const isHorizontal = computed(
|
|
|
+ () => props.mode === 'horizontal_top' || props.mode === 'horizontal_bottom'
|
|
|
+)
|
|
|
+const isTopOrLeft = computed(
|
|
|
+ () => props.mode === 'horizontal_top' || props.mode === 'vertical_left'
|
|
|
+)
|
|
|
|
|
|
-// 顺时针从 angleStart 到 angleEnd 的弧度跨度(0~360)
|
|
|
-const clockwiseSpan = computed(() => {
|
|
|
- const { angleStart, angleEnd } = props
|
|
|
- let s = (angleEnd - angleStart) % 360
|
|
|
- if (s <= 0) s += 360
|
|
|
- return s === 0 ? 360 : s
|
|
|
+// 刻度数量、主刻度数量
|
|
|
+const tickCount = computed(() => Math.max(2, props.tick))
|
|
|
+const mainTickCount = computed(() => Math.max(1, Math.min(props.mainTick, tickCount.value)))
|
|
|
+
|
|
|
+// 主刻度在刻度数组中的索引集合
|
|
|
+const majorIndices = computed(() => {
|
|
|
+ const set = new Set<number>()
|
|
|
+ const n = tickCount.value
|
|
|
+ const m = mainTickCount.value
|
|
|
+ if (m <= 1) {
|
|
|
+ set.add(0)
|
|
|
+ return set
|
|
|
+ }
|
|
|
+ for (let i = 0; i < m; i++) {
|
|
|
+ const idx = Math.round((i * (n - 1)) / (m - 1))
|
|
|
+ set.add(Math.min(idx, n - 1))
|
|
|
+ }
|
|
|
+ return set
|
|
|
})
|
|
|
|
|
|
-// 2. 进度计算逻辑(与 describeArc 的顺时针方向一致)
|
|
|
-const progressData = computed(() => {
|
|
|
- const { value, rangeStart, rangeEnd, angleStart, angleEnd, mode } = props
|
|
|
+// 归一化位置 [0..1] 的刻度列表
|
|
|
+const tickNormPositions = computed(() => {
|
|
|
+ const n = tickCount.value
|
|
|
+ if (n <= 1) return [0.5]
|
|
|
+ const list: number[] = []
|
|
|
+ for (let i = 0; i < n; i++) list.push(i / (n - 1))
|
|
|
+ return list
|
|
|
+})
|
|
|
|
|
|
- const rangeDiff = rangeEnd - rangeStart
|
|
|
- const ratio =
|
|
|
- Math.abs(rangeDiff) < 1e-9
|
|
|
- ? 0
|
|
|
- : Math.max(0, Math.min(1, (value - rangeStart) / rangeDiff))
|
|
|
+// 根据 mode 计算轴线坐标(主轴直线)
|
|
|
+// horizontal_top: 主轴贴在最底部;horizontal_bottom: 主轴贴在最顶部
|
|
|
+const axisLine = computed(() => {
|
|
|
+ const { width, height } = props
|
|
|
+ const pad = mainTickLength.value + 8
|
|
|
+ const halfLine = mainLineWidth.value / 2
|
|
|
+ if (isHorizontal.value) {
|
|
|
+ const y = isTopOrLeft.value ? height - halfLine : halfLine
|
|
|
+ return { x1: 0, y1: y, x2: width, y2: y }
|
|
|
+ }
|
|
|
+ const x = isTopOrLeft.value ? pad : width - pad
|
|
|
+ return { x1: x, y1: 0, x2: x, y2: height }
|
|
|
+})
|
|
|
|
|
|
- const span = clockwiseSpan.value
|
|
|
- let startA = angleStart
|
|
|
- let endA = angleEnd
|
|
|
+// 主轴按 area 分段:根据范围边界拆成多段,每段使用对应 area 的直线颜色
|
|
|
+const axisSegments = computed(() => {
|
|
|
+ const { width, height } = props
|
|
|
+ const start = props.rangeStart
|
|
|
+ const end = props.rangeEnd
|
|
|
+ const span = end - start
|
|
|
+ const axis = axisLine.value
|
|
|
|
|
|
- if (mode === 'normal') {
|
|
|
- // 从 angleStart 顺时针填充 span * ratio
|
|
|
- endA = angleStart + span * ratio
|
|
|
- } else if (mode === 'reverse') {
|
|
|
- startA = angleEnd - span * ratio
|
|
|
- endA = angleEnd
|
|
|
- } else if (mode === 'symmetrical') {
|
|
|
- const midAngle = angleStart + span / 2
|
|
|
- const midValue = (rangeStart + rangeEnd) / 2
|
|
|
- const rangeToEnd = rangeEnd - midValue
|
|
|
- const rangeToStart = midValue - rangeStart
|
|
|
+ const boundaries = new Set<number>([0, 1])
|
|
|
+ normalizedAreas.value.forEach((s) => {
|
|
|
+ if (span !== 0) {
|
|
|
+ const t0 = Math.max(0, Math.min(1, (s.start - start) / span))
|
|
|
+ const t1 = Math.max(0, Math.min(1, (s.end - start) / span))
|
|
|
+ boundaries.add(t0)
|
|
|
+ boundaries.add(t1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const sorted = Array.from(boundaries).sort((a, b) => a - b)
|
|
|
|
|
|
- if (value >= midValue && Math.abs(rangeToEnd) >= 1e-9) {
|
|
|
- startA = midAngle
|
|
|
- const halfRatio = Math.max(0, Math.min(1, (value - midValue) / rangeToEnd))
|
|
|
- endA = midAngle + (span / 2) * halfRatio
|
|
|
- } else if (value < midValue && Math.abs(rangeToStart) >= 1e-9) {
|
|
|
- endA = midAngle
|
|
|
- const halfRatio = Math.max(0, Math.min(1, (midValue - value) / rangeToStart))
|
|
|
- startA = midAngle - (span / 2) * halfRatio
|
|
|
+ const segments: { x1: number; y1: number; x2: number; y2: number; stroke: string }[] = []
|
|
|
+ for (let i = 0; i < sorted.length - 1; i++) {
|
|
|
+ const t0 = sorted[i]
|
|
|
+ const t1 = sorted[i + 1]
|
|
|
+ const midValue = valueAt((t0 + t1) / 2)
|
|
|
+ const stroke = getLineColorForValue(midValue)
|
|
|
+ if (isHorizontal.value) {
|
|
|
+ segments.push({
|
|
|
+ x1: t0 * width,
|
|
|
+ y1: axis.y1,
|
|
|
+ x2: t1 * width,
|
|
|
+ y2: axis.y2,
|
|
|
+ stroke
|
|
|
+ })
|
|
|
} else {
|
|
|
- startA = midAngle
|
|
|
- endA = midAngle
|
|
|
+ segments.push({
|
|
|
+ x1: axis.x1,
|
|
|
+ y1: t0 * height,
|
|
|
+ x2: axis.x2,
|
|
|
+ y2: t1 * height,
|
|
|
+ stroke
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- return { startA, endA }
|
|
|
+ if (segments.length === 0) {
|
|
|
+ segments.push({
|
|
|
+ x1: axis.x1,
|
|
|
+ y1: axis.y1,
|
|
|
+ x2: axis.x2,
|
|
|
+ y2: axis.y2,
|
|
|
+ stroke: mainLineColor.value
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return segments
|
|
|
})
|
|
|
|
|
|
-// 3. 值路径(与背景弧同半径)
|
|
|
-const valuePath = computed(() => {
|
|
|
- const { startA, endA } = progressData.value
|
|
|
- if (startA === endA) return ''
|
|
|
- return describeArc(cx.value, cx.value, trackRadius.value, startA, endA)
|
|
|
+// 刻度线端点:与主轴共线;每根刻度按数值取 area 直线颜色
|
|
|
+const tickPositions = computed(() => {
|
|
|
+ const { width, height } = props
|
|
|
+ const pad = mainTickLength.value + 8
|
|
|
+ const axis = axisLine.value
|
|
|
+ const norm = tickNormPositions.value
|
|
|
+ const major = majorIndices.value
|
|
|
+ const list: { x1: number; y1: number; x2: number; y2: number; stroke: string }[] = []
|
|
|
+
|
|
|
+ if (isHorizontal.value) {
|
|
|
+ const yAxis = axis.y1
|
|
|
+ // horizontal_top: 刻度朝上;horizontal_bottom: 刻度朝下
|
|
|
+ const sign = isTopOrLeft.value ? -1 : 1
|
|
|
+ norm.forEach((t, idx) => {
|
|
|
+ const x = t * width
|
|
|
+ const len = major.has(idx) ? mainTickLength.value : tickLength.value
|
|
|
+ const value = valueAt(t)
|
|
|
+ list.push({
|
|
|
+ x1: x,
|
|
|
+ y1: yAxis,
|
|
|
+ x2: x,
|
|
|
+ y2: yAxis + sign * len,
|
|
|
+ stroke: getLineColorForValue(value)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ const xAxis = isTopOrLeft.value ? pad : width - pad
|
|
|
+ const sign = isTopOrLeft.value ? -1 : 1
|
|
|
+ norm.forEach((t, idx) => {
|
|
|
+ const y = t * height
|
|
|
+ const len = major.has(idx) ? mainTickLength.value : tickLength.value
|
|
|
+ const value = valueAt(t)
|
|
|
+ list.push({
|
|
|
+ x1: xAxis,
|
|
|
+ y1: y,
|
|
|
+ x2: xAxis + sign * len,
|
|
|
+ y2: y,
|
|
|
+ stroke: getLineColorForValue(value)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return list
|
|
|
})
|
|
|
|
|
|
-// 4. 圆点位置(与弧同半径)
|
|
|
-const dotPos = computed(() => {
|
|
|
- // 原始结束角:normal / symmetrical 用 endA,reverse 用 startA
|
|
|
- const baseAngle =
|
|
|
- props.mode === 'reverse' ? progressData.value.startA : progressData.value.endA
|
|
|
- // 仅对“结束值原点”做偏移,不整体偏移整条进度弧
|
|
|
- const offset = props.rotateOffset ? props.rotateOffsetValue ?? 0 : 0
|
|
|
- const angle = baseAngle + offset
|
|
|
- return polarToCartesian(cx.value, cx.value, trackRadius.value, angle)
|
|
|
+// 标签文案:有设置内容用设置内容,否则用主刻度数值(根据范围变化),主刻度数为整数
|
|
|
+const labelTexts = computed(() => {
|
|
|
+ const raw = (props.labels || '').trim()
|
|
|
+ if (raw) {
|
|
|
+ return raw
|
|
|
+ .split(',')
|
|
|
+ .map((s) => s.trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ }
|
|
|
+ const m = mainTickCount.value
|
|
|
+ const start = props.rangeStart
|
|
|
+ const end = props.rangeEnd
|
|
|
+ const list: string[] = []
|
|
|
+ for (let i = 0; i < m; i++) {
|
|
|
+ const v = m <= 1 ? start : start + ((end - start) * i) / (m - 1)
|
|
|
+ list.push(String(Math.round(v)))
|
|
|
+ }
|
|
|
+ return list
|
|
|
})
|
|
|
|
|
|
-/** 进度圆点 overlay 的定位样式(与 viewBox 比例一致,用 ImageBg 时使用) */
|
|
|
-const knobOverlayStyle = computed(() => {
|
|
|
- if (!dotPos.value || !props.width || !props.height) return {}
|
|
|
- const { x, y } = dotPos.value
|
|
|
- const r = knobRadius.value.radius
|
|
|
- return {
|
|
|
- left: `${((x - r) / props.width) * 100}%`,
|
|
|
- top: `${((y - r) / props.height) * 100}%`,
|
|
|
- width: `${(r * 2 / props.width) * 100}%`,
|
|
|
- height: `${(r * 2 / props.height) * 100}%`,
|
|
|
- borderRadius: '50%'
|
|
|
+// 标签位置与对齐(仅主刻度位置),按 area 使用对应文本颜色
|
|
|
+const labelList = computed(() => {
|
|
|
+ if (!props.enableLabels || labelTexts.value.length === 0) return []
|
|
|
+ const { width, height } = props
|
|
|
+ const pad = mainTickLength.value + 8
|
|
|
+ const labelGap = 4
|
|
|
+ const m = mainTickCount.value
|
|
|
+ const texts = labelTexts.value
|
|
|
+ const list: {
|
|
|
+ x: number
|
|
|
+ y: number
|
|
|
+ text: string
|
|
|
+ textAnchor: string
|
|
|
+ dominantBaseline: string
|
|
|
+ fill: string
|
|
|
+ }[] = []
|
|
|
+
|
|
|
+ if (isHorizontal.value) {
|
|
|
+ const yAxis = axisLine.value.y1
|
|
|
+ // horizontal_top: 文字在刻度上(刻度朝上,标签在刻度线顶端上方);horizontal_bottom: 文字在刻度下
|
|
|
+ const labelY = isTopOrLeft.value
|
|
|
+ ? yAxis - mainTickLength.value - labelGap
|
|
|
+ : yAxis + mainTickLength.value + labelGap
|
|
|
+ for (let i = 0; i < m; i++) {
|
|
|
+ const t = m <= 1 ? 0.5 : i / (m - 1)
|
|
|
+ const x = t * width
|
|
|
+ const value = valueAt(t)
|
|
|
+ list.push({
|
|
|
+ x,
|
|
|
+ y: labelY,
|
|
|
+ text: texts[i] ?? '',
|
|
|
+ textAnchor: 'middle',
|
|
|
+ dominantBaseline: isTopOrLeft.value ? 'auto' : 'hanging',
|
|
|
+ fill: getTextColorForValue(value)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const xAxis = isTopOrLeft.value ? pad : width - pad
|
|
|
+ const labelX = isTopOrLeft.value
|
|
|
+ ? xAxis - mainTickLength.value - labelGap
|
|
|
+ : xAxis + mainTickLength.value + labelGap
|
|
|
+ for (let i = 0; i < m; i++) {
|
|
|
+ const t = m <= 1 ? 0.5 : i / (m - 1)
|
|
|
+ const y = t * height
|
|
|
+ const value = valueAt(t)
|
|
|
+ list.push({
|
|
|
+ x: labelX,
|
|
|
+ y,
|
|
|
+ text: texts[i] ?? '',
|
|
|
+ textAnchor: isTopOrLeft.value ? 'end' : 'start',
|
|
|
+ dominantBaseline: 'middle',
|
|
|
+ fill: getTextColorForValue(value)
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
+ return list
|
|
|
})
|
|
|
</script>
|