feat: 请求间隔散点图按模型区分颜色

- 后端 get_interval_timeline 接口返回数据添加 model 字段
- 前端散点图按模型分组显示不同颜色的数据点
- 横线统计信息支持按模型分别显示统计数据
- 管理员视图保持按用户分组,用户视图按模型分组
- 更新 mock 数据支持模型字段
This commit is contained in:
fawney19
2025-12-11 21:33:39 +08:00
parent 2dce4102b0
commit 0e8bf0a23b
6 changed files with 199 additions and 46 deletions

View File

@@ -6,9 +6,32 @@
class="absolute top-2 right-2 bg-gray-800/90 text-gray-100 px-3 py-2 rounded-lg text-sm shadow-lg border border-gray-600"
>
<div class="font-medium text-yellow-400">Y = {{ crosshairStats.yValue.toFixed(1) }} 分钟</div>
<div class="mt-1">
<span class="text-green-400">{{ crosshairStats.belowCount }}</span> / {{ crosshairStats.totalCount }} 点在横线以下
<span class="ml-2 text-blue-400">({{ crosshairStats.belowPercent.toFixed(1) }}%)</span>
<!-- 单个 dataset 时显示简单统计 -->
<div v-if="crosshairStats.datasets.length === 1" class="mt-1">
<span class="text-green-400">{{ crosshairStats.datasets[0].belowCount }}</span> / {{ crosshairStats.datasets[0].totalCount }} 点在横线以下
<span class="ml-2 text-blue-400">({{ crosshairStats.datasets[0].belowPercent.toFixed(1) }}%)</span>
</div>
<!-- 多个 dataset 时按模型分别显示 -->
<div v-else class="mt-1 space-y-0.5">
<div
v-for="ds in crosshairStats.datasets"
:key="ds.label"
class="flex items-center gap-2"
>
<div
class="w-2 h-2 rounded-full flex-shrink-0"
:style="{ backgroundColor: ds.color }"
/>
<span class="text-gray-300 truncate max-w-[80px]">{{ ds.label }}:</span>
<span class="text-green-400">{{ ds.belowCount }}</span>/<span class="text-gray-400">{{ ds.totalCount }}</span>
<span class="text-blue-400">({{ ds.belowPercent.toFixed(0) }}%)</span>
</div>
<!-- 总计 -->
<div class="flex items-center gap-2 pt-1 border-t border-gray-600 mt-1">
<span class="text-gray-300">总计:</span>
<span class="text-green-400">{{ crosshairStats.totalBelowCount }}</span>/<span class="text-gray-400">{{ crosshairStats.totalCount }}</span>
<span class="text-blue-400">({{ crosshairStats.totalBelowPercent.toFixed(1) }}%)</span>
</div>
</div>
</div>
</div>
@@ -48,13 +71,22 @@ interface Props {
height?: number
}
interface CrosshairStats {
yValue: number
interface DatasetStats {
label: string
color: string
belowCount: number
totalCount: number
belowPercent: number
}
interface CrosshairStats {
yValue: number
datasets: DatasetStats[]
totalBelowCount: number
totalCount: number
totalBelowPercent: number
}
const props = withDefaults(defineProps<Props>(), {
height: 300
})
@@ -67,29 +99,47 @@ const crosshairY = ref<number | null>(null)
const crosshairStats = computed<CrosshairStats | null>(() => {
if (crosshairY.value === null || !props.data.datasets) return null
const datasetStats: DatasetStats[] = []
let totalBelowCount = 0
let totalCount = 0
let belowCount = 0
for (const dataset of props.data.datasets) {
if (!dataset.data) continue
let belowCount = 0
let dsTotal = 0
for (const point of dataset.data) {
const p = point as { x: string; y: number }
if (typeof p.y === 'number') {
dsTotal++
totalCount++
if (p.y <= crosshairY.value) {
belowCount++
totalBelowCount++
}
}
}
if (dsTotal > 0) {
datasetStats.push({
label: dataset.label || 'Unknown',
color: (dataset.backgroundColor as string) || 'rgba(59, 130, 246, 0.7)',
belowCount,
totalCount: dsTotal,
belowPercent: (belowCount / dsTotal) * 100
})
}
}
if (totalCount === 0) return null
return {
yValue: crosshairY.value,
belowCount,
datasets: datasetStats,
totalBelowCount,
totalCount,
belowPercent: (belowCount / totalCount) * 100
totalBelowPercent: (totalBelowCount / totalCount) * 100
}
})