Files
2026-10-29 10:08:39 +08:00

58 lines
4.2 KiB
Plaintext

<template>
<view class="trend-chart"><view class="scale-column"><text v-for="scale in scaleLabels" :key="scale" class="scale-label">{{ scale }}</text></view><view class="trend-plot"><view class="trend-lines"><view class="trend-grid-line trend-grid-line-top"></view><view class="trend-grid-line trend-grid-line-upper"></view><view class="trend-grid-line trend-grid-line-lower"></view><view class="trend-grid-line trend-grid-line-bottom"></view><view v-for="segment in averageSegments" :key="segment.key" class="trend-line-segment trend-line-average" :style="segment.style"></view><view v-for="segment in studentSegments" :key="segment.key" class="trend-line-segment trend-line-student" :style="segment.style"></view><view v-for="point in points" :key="point.label + '-average'" class="trend-point trend-point-average" :style="pointStyle(point, false)"></view><view v-for="point in points" :key="point.label + '-student'" class="trend-point trend-point-student" :style="pointStyle(point, true)"></view></view><view class="trend-labels"><text v-for="point in points" :key="point.label" class="trend-day">{{ point.label }}</text></view></view></view>
</template>
<script setup lang="ts">
import type { StudyReportTrendPoint } from '../api/student'
type TrendLineSegment = { key: string, style: string }
const props = defineProps<{ scaleLabels: string[], points: StudyReportTrendPoint[] }>()
const studentSegments = computed((): TrendLineSegment[] => buildSegments(true))
const averageSegments = computed((): TrendLineSegment[] => buildSegments(false))
function buildSegments(isStudent: boolean): TrendLineSegment[] {
const segments: TrendLineSegment[] = []
if (props.points.length < 2) return segments
const chartWidth = 270
const horizontalStep = (chartWidth - 16) / (props.points.length - 1)
for (let index = 0; index < props.points.length - 1; index++) {
const currentHeight = isStudent ? props.points[index].valueHeight : props.points[index].averageHeight
const nextHeight = isStudent ? props.points[index + 1].valueHeight : props.points[index + 1].averageHeight
const verticalStep = nextHeight - currentHeight
const lineWidth = Math.sqrt(horizontalStep * horizontalStep + verticalStep * verticalStep) / chartWidth * 100
const angle = -Math.atan2(verticalStep, horizontalStep) * 180 / Math.PI
const left = 8 + horizontalStep * index
segments.push({ key: `${isStudent ? 'student' : 'average'}-${index}`, style: `left:${left / chartWidth * 100}%;bottom:${currentHeight + 8}px;width:${lineWidth}%;transform:rotate(${angle}deg);` })
}
return segments
}
function pointStyle(point: StudyReportTrendPoint, isStudent: boolean): string {
const index = props.points.indexOf(point)
const left = props.points.length > 1 ? 8 + (270 - 16) / (props.points.length - 1) * index : 135
const pointHeight = isStudent ? point.valueHeight : point.averageHeight
return `left:${left / 270 * 100}%;bottom:${pointHeight + 8}px;`
}
</script>
<style>
.trend-chart { display: flex; flex-direction: row; height: 132px; margin-top: 10px; }
.scale-column { display: flex; flex-direction: column; justify-content: space-between; width: 35px; padding-bottom: 18px; }
.scale-label { font-size: 10px; color: #8992a5; }
.trend-plot { display: flex; flex-direction: column; flex: 1; padding-bottom: 18px; border-bottom: 1px solid #edf0f5; }
.trend-lines { position: relative; height: 90px; }
.trend-grid-line { position: absolute; left: 0; right: 0; height: 1px; background-color: #edf0f5; }
.trend-grid-line-top { top: 8px; }
.trend-grid-line-upper { top: 32px; }
.trend-grid-line-lower { top: 56px; }
.trend-grid-line-bottom { bottom: 8px; }
.trend-line-segment { position: absolute; height: 2px; transform-origin: left center; }
.trend-line-student { background-color: #6558e9; }
.trend-line-average { height: 1px; background-color: #9aa8bc; }
.trend-point { position: absolute; width: 7px; height: 7px; margin-left: -4px; margin-bottom: -4px; border-radius: 4px; background-color: #ffffff; }
.trend-point-student { border: 2px solid #6558e9; }
.trend-point-average { border: 2px solid #9aa8bc; }
.trend-labels { display: flex; flex-direction: row; justify-content: space-between; margin-top: 6px; }
.trend-day { font-size: 10px; color: #8992a5; }
</style>