【AI测试全栈:Vue核心】19、Vue3+ECharts实战:构建AI测试可视化仪表盘全攻略
本文介绍了使用Vue3和ECharts构建AI测试可视化仪表盘的技术方案。针对AI测试场景中海量多维度数据(如准确率、召回率、混淆矩阵等)的可视化需求,文章详细分析了Vue3 Composition API相比传统Options API的优势,特别是其响应式系统和组合式函数设计模式。通过useTestData组合函数的示例代码,展示了如何高效管理测试数据状态、实现派生计算和异步加载。该技术栈能够有
Vue3+ECharts实战:构建AI测试可视化仪表盘全攻略
1. 引言:AI测试可视化的核心价值
为什么AI测试需要专业可视化?
在人工智能测试领域,我们面对的不仅仅是传统软件的功能验证,更涉及复杂的模型性能评估、数据分布分析和实时指标监控。一个AI模型的测试过程会产生海量的多维度数据:
- 准确率、精确率、召回率等多维度评估指标
- 混淆矩阵和ROC曲线等分类性能数据
- 推理延迟、吞吐量等性能指标
- 特征重要性和误差分布等深度分析数据
面对如此复杂的数据体系,传统的表格展示方式已无法满足高效分析的需求。专业的数据可视化能够:
- 直观呈现模型表现:通过图表快速识别性能瓶颈
- 实时监控测试进度:动态跟踪测试执行状态
- 多维数据分析:从不同角度深入理解模型行为
- 团队协作共享:统一的视觉语言促进跨团队沟通
Vue3+ECharts技术栈的选型优势
Vue3的核心优势
// Vue3的组合式API示例
import { ref, computed, watchEffect } from 'vue'
// 清晰的逻辑组织
export function useModelMetrics() {
const accuracy = ref(0)
const latency = ref(0)
// 计算属性自动更新
const performanceScore = computed(() => {
return accuracy.value * 0.7 + (1 / latency.value) * 0.3
})
return { accuracy, latency, performanceScore }
}
ECharts的强大能力
- 丰富的图表类型:支持50+种可视化类型
- 强大的交互能力:数据筛选、缩放、联动
- 大数据量处理:千万级数据点的流畅渲染
- 高度可定制化:完全开放的配置选项
技术栈协同效应
┌─────────────────────────────────────────┐
│ Vue3响应式系统 │
│ ┌──────────────────────────┐ │
│ │ 实时数据更新 │ │
│ └───────────┬──────────────┘ │
└────────────────┼────────────────────────┘
│
┌────────────────┼────────────────────────┐
│ ECharts渲染引擎 │
│ ┌──────────────────────────┐ │
│ │ 高效图表绘制 │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────┘
本文目标读者与前置知识要求
目标读者
- 前端开发工程师:希望掌握Vue3高级特性的开发者
- AI测试工程师:需要构建测试可视化工具的技术人员
- 全栈工程师:负责AI产品前后端一体化的开发者
- 技术负责人:评估技术选型和架构设计的决策者
前置知识
-
必要基础:
- Vue.js基础概念(组件、指令、生命周期)
- JavaScript ES6+语法
- 基本的HTML/CSS知识
-
推荐了解:
- TypeScript基础类型
- 响应式编程概念
- 数据可视化基本原则
2. Vue3 Composition API在AI测试场景的应用
2.1 传统Options API的痛点分析
在AI测试可视化场景中,传统的Options API面临诸多挑战:
// Options API的典型问题示例
export default {
data() {
return {
// 测试数据分散在不同属性中
accuracyData: [],
latencyData: [],
confusionMatrix: [],
// 相关逻辑被割裂
loading: false,
error: null
}
},
computed: {
// 计算属性难以复用
formattedAccuracy() {
return this.accuracyData.map(item => ({
...item,
percentage: (item.value * 100).toFixed(2)
}))
}
},
methods: {
// 相关方法分散各处
async fetchTestData() {
this.loading = true
try {
const [accuracy, latency] = await Promise.all([
fetchAccuracy(),
fetchLatency()
])
// 数据更新逻辑分散
this.accuracyData = accuracy
this.latencyData = latency
} catch (error) {
this.error = error
} finally {
this.loading = false
}
}
},
mounted() {
// 生命周期中初始化逻辑
this.fetchTestData()
}
}
主要痛点总结:
- 逻辑关注点分散:相关代码被分割到不同选项中
- 复用性差:相似逻辑无法在不同组件间共享
- TypeScript支持有限:类型推断不够友好
- 代码组织困难:大型组件变得难以维护
2.2 useTestData组合函数设计模式
响应式数据定义(ref/reactive)
// useTestData.js 完整实现
import { ref, reactive, computed, watch } from 'vue'
import { fetchTestResults, subscribeToUpdates } from '@/api/testService'
/**
* AI测试数据组合函数
* @param {string} testId - 测试ID
* @returns {Object} 测试数据相关状态和方法
*/
export function useTestData(testId) {
// 使用ref定义独立的基本类型响应式数据
const isLoading = ref(false)
const error = ref(null)
const lastUpdated = ref(new Date())
// 使用reactive定义复杂的对象类型响应式数据
const testState = reactive({
id: testId,
metrics: {
accuracy: 0,
precision: 0,
recall: 0,
f1Score: 0
},
performance: {
latency: 0,
throughput: 0,
memoryUsage: 0
},
confusionMatrix: {
truePositives: 0,
falsePositives: 0,
trueNegatives: 0,
falseNegatives: 0
},
rawData: [],
predictions: []
})
// 使用computed定义派生状态
const modelPerformance = computed(() => {
const { accuracy, precision, recall, f1Score } = testState.metrics
return {
overallScore: (accuracy + precision + recall + f1Score) / 4,
isExcellent: f1Score > 0.9,
needsImprovement: accuracy < 0.7 || recall < 0.6
}
})
const formattedConfusionMatrix = computed(() => {
const { truePositives, falsePositives, trueNegatives, falseNegatives } =
testState.confusionMatrix
const total = truePositives + falsePositives + trueNegatives + falseNegatives
return {
matrix: [
[truePositives, falsePositives],
[falseNegatives, trueNegatives]
],
percentages: [
[
(truePositives / total * 100).toFixed(1),
(falsePositives / total * 100).toFixed(1)
],
[
(falseNegatives / total * 100).toFixed(1),
(trueNegatives / total * 100).toFixed(1)
]
]
}
})
// 异步数据加载逻辑
const loadTestData = async (options = {}) => {
try {
isLoading.value = true
error.value = null
const { forceRefresh = false, timeRange } = options
const params = { testId, forceRefresh }
if (timeRange) {
params.startTime = timeRange.start
params.endTime = timeRange.end
}
const data = await fetchTestResults(params)
// 批量更新响应式状态
Object.assign(testState, {
metrics: data.metrics,
performance: data.performance,
confusionMatrix: data.confusionMatrix,
rawData: data.rawData || [],
predictions: data.predictions || []
})
lastUpdated.value = new Date()
return data
} catch (err) {
error.value = err.message || 'Failed to load test data'
console.error('Error loading test data:', err)
throw err
} finally {
isLoading.value = false
}
}
// 实时数据订阅
const setupRealtimeUpdates = () => {
const unsubscribe = subscribeToUpdates(testId, (update) => {
// 增量更新测试状态
if (update.type === 'metrics') {
Object.assign(testState.metrics, update.data)
} else if (update.type === 'performance') {
Object.assign(testState.performance, update.data)
}
lastUpdated.value = new Date()
})
return unsubscribe
}
// 数据导出功能
const exportData = (format = 'json') => {
const data = {
testId: testState.id,
timestamp: lastUpdated.value.toISOString(),
...testState
}
if (format === 'csv') {
return convertToCSV(data)
}
return JSON.stringify(data, null, 2)
}
// 数据质量检查
const dataQuality = computed(() => {
const checks = {
hasMetrics: Object.values(testState.metrics).every(v => v !== null),
hasPerformanceData: testState.performance.latency > 0,
hasConfusionMatrix: Object.values(testState.confusionMatrix).every(v => v >= 0),
isRecent: (new Date() - lastUpdated.value) < 5 * 60 * 1000 // 5分钟内更新
}
const passedChecks = Object.values(checks).filter(Boolean).length
const totalChecks = Object.keys(checks).length
return {
score: (passedChecks / totalChecks) * 100,
checks,
status: passedChecks === totalChecks ? 'excellent' :
passedChecks >= totalChecks / 2 ? 'good' : 'poor'
}
})
// 监视数据变化,触发自动保存
watch(
() => ({ ...testState.metrics, ...testState.performance }),
(newData, oldData) => {
if (JSON.stringify(newData) !== JSON.stringify(oldData)) {
// 触发自动保存或分析
console.log('Test data changed, triggering analysis...')
}
},
{ deep: true }
)
return {
// 状态
testState,
isLoading,
error,
lastUpdated,
// 计算属性
modelPerformance,
formattedConfusionMatrix,
dataQuality,
// 方法
loadTestData,
setupRealtimeUpdates,
exportData,
// 工具函数
reset: () => {
Object.assign(testState, {
metrics: { accuracy: 0, precision: 0, recall: 0, f1Score: 0 },
performance: { latency: 0, throughput: 0, memoryUsage: 0 },
confusionMatrix: { truePositives: 0, falsePositives: 0, trueNegatives: 0, falseNegatives: 0 }
})
}
}
}
// 辅助函数:转换为CSV格式
function convertToCSV(data) {
const rows = []
// 添加标题行
rows.push('Metric,Value')
// 添加指标数据
Object.entries(data.metrics).forEach(([key, value]) => {
rows.push(`${key},${value}`)
})
// 添加性能数据
Object.entries(data.performance).forEach(([key, value]) => {
rows.push(`${key},${value}`)
})
return rows.join('\n')
}
组合函数使用示例
// TestDashboard.vue
<script setup>
import { useTestData } from '@/composables/useTestData'
import { onMounted, onUnmounted } from 'vue'
const props = defineProps({
testId: {
type: String,
required: true
}
})
// 使用组合函数
const {
testState,
isLoading,
error,
modelPerformance,
formattedConfusionMatrix,
loadTestData,
setupRealtimeUpdates
} = useTestData(props.testId)
// 组件生命周期
onMounted(async () => {
await loadTestData()
const unsubscribe = setupRealtimeUpdates()
onUnmounted(() => {
unsubscribe()
})
})
// 手动刷新数据
const handleRefresh = async () => {
await loadTestData({ forceRefresh: true })
}
</script>
<template>
<div class="test-dashboard">
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-indicator">
加载测试数据中...
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-alert">
{{ error }}
<button @click="loadTestData">重试</button>
</div>
<!-- 正常状态 -->
<div v-else class="metrics-display">
<h3>模型性能概览</h3>
<div class="metric-grid">
<div class="metric-card">
<span class="metric-label">准确率</span>
<span class="metric-value">
{{ (testState.metrics.accuracy * 100).toFixed(2) }}%
</span>
</div>
<div class="metric-card">
<span class="metric-label">F1分数</span>
<span class="metric-value">
{{ testState.metrics.f1Score.toFixed(3) }}
</span>
</div>
<div class="metric-card">
<span class="metric-label">推理延迟</span>
<span class="metric-value">
{{ testState.performance.latency.toFixed(2) }}ms
</span>
</div>
</div>
<!-- 混淆矩阵展示 -->
<div class="confusion-matrix">
<h4>混淆矩阵</h4>
<table>
<tr>
<td v-for="(cell, index) in formattedConfusionMatrix.matrix[0]"
:key="index"
class="matrix-cell">
{{ cell }}
<div class="percentage">
{{ formattedConfusionMatrix.percentages[0][index] }}%
</div>
</td>
</tr>
<tr>
<td v-for="(cell, index) in formattedConfusionMatrix.matrix[1]"
:key="index"
class="matrix-cell">
{{ cell }}
<div class="percentage">
{{ formattedConfusionMatrix.percentages[1][index] }}%
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
组合函数数据流图示
3. Vue3响应式系统深度解析与性能优化
3.1 Proxy vs Object.defineProperty的革新
Vue2响应式系统的局限性
// Vue2基于Object.defineProperty的实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`get ${key}: ${val}`)
return val
},
set(newVal) {
if (newVal === val) return
console.log(`set ${key}: ${newVal}`)
val = newVal
// 触发更新
dep.notify()
}
})
}
// 存在的问题:
// 1. 无法检测对象属性的添加/删除
// 2. 数组变异方法需要重写
// 3. 需要递归遍历所有属性
Vue3 Proxy的优势
// Vue3基于Proxy的实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
// 递归代理嵌套对象
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (result && oldValue !== value) {
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, key)
}
return result
}
})
}
// Proxy的优势:
// 1. 支持数组索引修改和length变化
// 2. 支持Map、Set等ES6集合类型
// 3. 更好的性能表现
3.2 实时数据更新场景下的响应式陷阱
常见陷阱及解决方案
// 陷阱1:不必要的深度响应式
import { reactive, markRaw } from 'vue'
const testStore = reactive({
config: {
modelName: 'resnet50',
batchSize: 32
},
// 大型静态数据,不需要响应式
largeStaticData: markRaw({
// 数十MB的参考数据集
referenceSamples: [...],
groundTruth: [...]
})
})
// 陷阱2:频繁更新导致的性能问题
const performanceMetrics = reactive({
fps: 0,
memory: 0,
cpu: 0
})
// 不推荐的写法:频繁独立更新
setInterval(() => {
performanceMetrics.fps = getFPS()
performanceMetrics.memory = getMemoryUsage()
performanceMetrics.cpu = getCPUUsage()
}, 100)
// 推荐的写法:批量更新
setInterval(() => {
const updates = {
fps: getFPS(),
memory: getMemoryUsage(),
cpu: getCPUUsage()
}
Object.assign(performanceMetrics, updates)
}, 100)
// 陷阱3:循环引用导致的无限更新
const circularData = reactive({
value: 0,
get doubled() {
return this.value * 2
}
})
// 不安全的用法:在getter中修改自身
watch(() => circularData.doubled, () => {
// 可能导致无限循环
circularData.value += 1
})
// 解决方案:使用watchEffect的清除机制
watchEffect((onCleanup) => {
if (circularData.value < 10) {
const timer = setTimeout(() => {
circularData.value += 1
}, 1000)
onCleanup(() => clearTimeout(timer))
}
})
3.3 watch vs watchEffect在数据监控中的选择
import { ref, watch, watchEffect, onScopeDispose } from 'vue'
// 场景1:精确监控特定数据变化 - 使用watch
const accuracy = ref(0)
const precision = ref(0)
const recall = ref(0)
// watch适合:
// 1. 需要知道变化前后的值
// 2. 需要惰性执行(lazy)
// 3. 需要控制触发时机
watch(
[accuracy, precision, recall],
([newAccuracy, newPrecision, newRecall], [oldAccuracy, oldPrecision, oldRecall]) => {
console.log(`准确率变化: ${oldAccuracy} -> ${newAccuracy}`)
console.log(`精确率变化: ${oldPrecision} -> ${newPrecision}`)
console.log(`召回率变化: ${oldRecall} -> ${newRecall}`)
// 只有当所有指标都达到阈值时才触发
if (newAccuracy > 0.9 && newPrecision > 0.85 && newRecall > 0.85) {
console.log('模型达到优秀标准')
}
},
{
deep: false, // 不需要深度监听
immediate: false, // 不需要立即执行
flush: 'post' // DOM更新后执行
}
)
// 场景2:响应式依赖自动收集 - 使用watchEffect
const testConfig = reactive({
modelType: 'classification',
threshold: 0.5,
enableAugmentation: true
})
const testResults = ref([])
// watchEffect适合:
// 1. 依赖自动收集,代码更简洁
// 2. 立即执行副作用
// 3. 清理副作用更方便
const stopWatch = watchEffect((onCleanup) => {
console.log('配置变化,重新运行测试:', testConfig)
// 清理之前的测试
onCleanup(() => {
console.log('清理之前的测试任务')
// 取消之前的请求等
})
// 根据当前配置运行测试
runTest(testConfig).then(results => {
testResults.value = results
})
// 自动收集依赖:testConfig的所有属性
})
// 手动停止监听
onScopeDispose(() => {
stopWatch()
})
// 场景3:结合使用的最佳实践
const realtimeData = reactive({
predictions: [],
confidenceScores: [],
timestamps: []
})
// 使用watch精确控制
watch(
() => realtimeData.predictions.length,
(newLength, oldLength) => {
if (newLength > oldLength) {
console.log(`新增 ${newLength - oldLength} 个预测结果`)
}
}
)
// 使用watchEffect处理副作用
watchEffect(() => {
// 当有新数据时更新图表
if (realtimeData.predictions.length > 0) {
updateChart(realtimeData)
// 自动清理旧数据,保持内存稳定
if (realtimeData.predictions.length > 1000) {
realtimeData.predictions.splice(0, 500)
realtimeData.confidenceScores.splice(0, 500)
realtimeData.timestamps.splice(0, 500)
}
}
})
3.4 代码实战:WebSocket实时性能指标推送
// realtimeMetrics.js
import { reactive, ref, onUnmounted, watch } from 'vue'
/**
* WebSocket实时性能监控
* @param {string} url - WebSocket服务器地址
* @param {Object} options - 配置选项
*/
export function useRealtimeMetrics(url, options = {}) {
const {
autoConnect = true,
reconnectAttempts = 3,
reconnectDelay = 3000,
bufferSize = 1000
} = options
// 响应式状态
const connectionState = ref('disconnected') // disconnected, connecting, connected, error
const errorMessage = ref('')
const metrics = reactive({
// 性能指标
performance: {
fps: [],
memory: [],
latency: []
},
// 模型指标
model: {
accuracy: [],
loss: [],
confidence: []
},
// 系统指标
system: {
cpu: [],
gpu: [],
network: []
}
})
// 原始数据缓冲区
const rawDataBuffer = reactive([])
// WebSocket实例
let ws = null
let reconnectCount = 0
let reconnectTimer = null
// 连接WebSocket
const connect = () => {
if (connectionState.value === 'connected') {
console.warn('WebSocket already connected')
return
}
connectionState.value = 'connecting'
errorMessage.value = ''
try {
ws = new WebSocket(url)
ws.onopen = () => {
connectionState.value = 'connected'
reconnectCount = 0
console.log('WebSocket connected successfully')
// 发送初始配置
ws.send(JSON.stringify({
type: 'config',
bufferSize,
metrics: ['performance', 'model', 'system']
}))
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
processIncomingData(data)
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
}
}
ws.onerror = (error) => {
connectionState.value = 'error'
errorMessage.value = `WebSocket error: ${error.message || 'Unknown error'}`
console.error('WebSocket error:', error)
}
ws.onclose = (event) => {
connectionState.value = 'disconnected'
console.log(`WebSocket closed: ${event.code} ${event.reason}`)
// 自动重连逻辑
if (reconnectCount < reconnectAttempts) {
reconnectCount++
console.log(`Attempting to reconnect (${reconnectCount}/${reconnectAttempts})...`)
reconnectTimer = setTimeout(() => {
connect()
}, reconnectDelay)
}
}
} catch (error) {
connectionState.value = 'error'
errorMessage.value = `Failed to create WebSocket: ${error.message}`
console.error('Failed to create WebSocket:', error)
}
}
// 处理接收到的数据
const processIncomingData = (data) => {
const { type, timestamp, ...metricData } = data
// 添加到原始数据缓冲区
rawDataBuffer.push({
type,
timestamp: timestamp || Date.now(),
...metricData
})
// 保持缓冲区大小
if (rawDataBuffer.length > bufferSize) {
rawDataBuffer.splice(0, rawDataBuffer.length - bufferSize)
}
// 根据数据类型分类存储
switch (type) {
case 'performance':
updateMetricsArray(metrics.performance, metricData)
break
case 'model':
updateMetricsArray(metrics.model, metricData)
break
case 'system':
updateMetricsArray(metrics.system, metricData)
break
default:
console.warn(`Unknown metric type: ${type}`)
}
}
// 更新指标数组
const updateMetricsArray = (target, data) => {
Object.entries(data).forEach(([key, value]) => {
if (!target[key]) {
target[key] = []
}
target[key].push({
value,
timestamp: Date.now()
})
// 保持数组长度
if (target[key].length > 100) {
target[key].shift()
}
})
}
// 发送命令到服务器
const sendCommand = (command, data = {}) => {
if (connectionState.value !== 'connected' || !ws) {
console.warn('WebSocket not connected')
return false
}
try {
ws.send(JSON.stringify({
type: 'command',
command,
...data,
timestamp: Date.now()
}))
return true
} catch (error) {
console.error('Failed to send command:', error)
return false
}
}
// 断开连接
const disconnect = () => {
if (ws) {
// 清除重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
// 发送关闭消息
sendCommand('disconnect')
// 关闭连接
ws.close(1000, 'Client disconnected')
ws = null
}
connectionState.value = 'disconnected'
}
// 数据导出
const exportMetrics = (type = 'json') => {
const data = {
timestamp: Date.now(),
connectionState: connectionState.value,
metrics: JSON.parse(JSON.stringify(metrics)),
rawData: [...rawDataBuffer]
}
if (type === 'csv') {
return convertMetricsToCSV(data)
}
return JSON.stringify(data, null, 2)
}
// 计算统计信息
const statistics = reactive({
performance: computed(() => calculateStats(metrics.performance)),
model: computed(() => calculateStats(metrics.model)),
system: computed(() => calculateStats(metrics.system))
})
const calculateStats = (metricGroup) => {
const stats = {}
Object.entries(metricGroup).forEach(([key, values]) => {
if (values.length === 0) {
stats[key] = { avg: 0, min: 0, max: 0, count: 0 }
return
}
const numericValues = values.map(v => v.value).filter(v => !isNaN(v))
if (numericValues.length === 0) {
stats[key] = { avg: 0, min: 0, max: 0, count: 0 }
return
}
const sum = numericValues.reduce((a, b) => a + b, 0)
const avg = sum / numericValues.length
const min = Math.min(...numericValues)
const max = Math.max(...numericValues)
stats[key] = {
avg: parseFloat(avg.toFixed(4)),
min: parseFloat(min.toFixed(4)),
max: parseFloat(max.toFixed(4)),
count: numericValues.length,
latest: numericValues[numericValues.length - 1]
}
})
return stats
}
// 监视连接状态变化
watch(connectionState, (newState, oldState) => {
console.log(`Connection state changed: ${oldState} -> ${newState}`)
if (newState === 'connected') {
// 连接成功后的初始化逻辑
console.log('WebSocket connected, starting data collection...')
} else if (newState === 'error') {
// 错误处理
console.error('WebSocket connection error')
}
})
// 自动连接
if (autoConnect) {
connect()
}
// 清理函数
onUnmounted(() => {
disconnect()
})
return {
// 状态
connectionState,
errorMessage,
metrics,
statistics,
rawDataBuffer,
// 方法
connect,
disconnect,
sendCommand,
exportMetrics,
// 计算属性
isConnected: computed(() => connectionState.value === 'connected'),
isConnecting: computed(() => connectionState.value === 'connecting'),
hasError: computed(() => connectionState.value === 'error')
}
}
// 使用示例
// RealtimeMetricsDashboard.vue
<script setup>
import { useRealtimeMetrics } from '@/composables/realtimeMetrics'
import { onMounted, onUnmounted } from 'vue'
const {
connectionState,
metrics,
statistics,
isConnected,
connect,
disconnect,
sendCommand
} = useRealtimeMetrics('ws://localhost:8080/metrics', {
autoConnect: true,
bufferSize: 5000
})
// 组件挂载时自定义逻辑
onMounted(() => {
// 可以添加额外的初始化逻辑
console.log('Realtime metrics dashboard mounted')
})
onUnmounted(() => {
// 确保清理
disconnect()
})
// 自定义命令
const startProfiling = () => {
sendCommand('start_profiling', {
duration: 60000, // 60秒
interval: 100 // 100ms采样间隔
})
}
const stopProfiling = () => {
sendCommand('stop_profiling')
}
// 获取最新指标
const getLatestMetric = (category, metric) => {
const values = metrics[category]?.[metric]
return values && values.length > 0
? values[values.length - 1].value
: null
}
</script>
<template>
<div class="realtime-metrics">
<!-- 连接状态 -->
<div class="connection-status" :class="connectionState">
Status: {{ connectionState }}
<button v-if="!isConnected" @click="connect">连接</button>
<button v-else @click="disconnect">断开</button>
</div>
<!-- 实时指标展示 -->
<div class="metrics-grid">
<div class="metric-category">
<h4>性能指标</h4>
<div v-for="(stat, key) in statistics.performance"
:key="key"
class="metric-item">
<span class="metric-name">{{ key }}</span>
<span class="metric-value">{{ stat.latest?.toFixed(2) || 'N/A' }}</span>
<div class="metric-trend">
<span>Avg: {{ stat.avg.toFixed(2) }}</span>
<span>Min: {{ stat.min.toFixed(2) }}</span>
<span>Max: {{ stat.max.toFixed(2) }}</span>
</div>
</div>
</div>
<div class="metric-category">
<h4>模型指标</h4>
<div v-for="(stat, key) in statistics.model"
:key="key"
class="metric-item">
<span class="metric-name">{{ key }}</span>
<span class="metric-value">{{ stat.latest?.toFixed(4) || 'N/A' }}</span>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="startProfiling" :disabled="!isConnected">
开始性能分析
</button>
<button @click="stopProfiling" :disabled="!isConnected">
停止性能分析
</button>
</div>
</div>
</template>
<style scoped>
.realtime-metrics {
padding: 20px;
}
.connection-status {
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
}
.connection-status.connected {
background-color: #d4edda;
color: #155724;
}
.connection-status.disconnected {
background-color: #f8d7da;
color: #721c24;
}
.connection-status.connecting {
background-color: #fff3cd;
color: #856404;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.metric-category {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
}
.metric-category h4 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.metric-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.metric-item:last-child {
border-bottom: none;
}
.metric-name {
font-weight: 500;
color: #555;
}
.metric-value {
font-weight: bold;
color: #1890ff;
}
.metric-trend {
font-size: 12px;
color: #888;
}
.metric-trend span {
margin-left: 10px;
}
.controls {
display: flex;
gap: 10px;
margin-top: 20px;
}
.controls button {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.controls button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>
4. Pinia状态管理在AI测试仪表盘中的实践
4.1 为什么需要集中式状态管理?
在复杂的AI测试仪表盘中,数据流的管理面临多重挑战:
- 多组件状态共享:多个图表组件需要访问相同的测试数据
- 复杂的业务逻辑:测试配置、结果筛选、统计计算等
- 数据持久化需求:保存用户偏好、测试历史等
- 类型安全:需要完善的TypeScript支持
4.2 TestStore设计:测试配置、结果、筛选条件
// stores/testStore.ts
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'
import type { TestConfig, TestResult, FilterCriteria } from '@/types/test'
export const useTestStore = defineStore('test', () => {
// 状态定义
const testConfigs = ref<TestConfig[]>([])
const currentTestId = ref<string>('')
const testResults = ref<Map<string, TestResult>>(new Map())
const filterCriteria = reactive<FilterCriteria>({
dateRange: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 最近7天
end: new Date()
},
metricThresholds: {
accuracy: 0.7,
precision: 0.6,
recall: 0.6,
f1Score: 0.65
},
modelTypes: ['classification', 'regression', 'object_detection'],
status: ['passed', 'failed', 'running']
})
// 当前测试配置
const currentConfig = computed<TestConfig | undefined>(() => {
return testConfigs.value.find(config => config.id === currentTestId.value)
})
// 当前测试结果
const currentResults = computed(() => {
return testResults.value.get(currentTestId.value)
})
// 根据筛选条件过滤的结果
const filteredResults = computed(() => {
const results = Array.from(testResults.value.values())
return results.filter(result => {
// 日期筛选
const testDate = new Date(result.timestamp)
if (testDate < filterCriteria.dateRange.start ||
testDate > filterCriteria.dateRange.end) {
return false
}
// 指标阈值筛选
for (const [metric, threshold] of Object.entries(filterCriteria.metricThresholds)) {
if (result.metrics[metric] < threshold) {
return false
}
}
// 模型类型筛选
if (!filterCriteria.modelTypes.includes(result.modelType)) {
return false
}
// 状态筛选
if (!filterCriteria.status.includes(result.status)) {
return false
}
return true
})
})
// 统计数据
const statistics = computed(() => {
const results = filteredResults.value
if (results.length === 0) {
return {
total: 0,
passed: 0,
failed: 0,
averageAccuracy: 0,
averageLatency: 0
}
}
const passed = results.filter(r => r.status === 'passed').length
const accuracies = results.map(r => r.metrics.accuracy)
const latencies = results.map(r => r.performance.latency)
return {
total: results.length,
passed,
failed: results.length - passed,
passRate: (passed / results.length * 100).toFixed(1),
averageAccuracy: accuracies.reduce((a, b) => a + b, 0) / accuracies.length,
averageLatency: latencies.reduce((a, b) => a + b, 0) / latencies.length,
minLatency: Math.min(...latencies),
maxLatency: Math.max(...latencies)
}
})
// Actions
const loadTestConfigs = async () => {
try {
const response = await fetch('/api/test/configs')
const data = await response.json()
testConfigs.value = data
// 如果当前没有选中的测试,选择第一个
if (!currentTestId.value && data.length > 0) {
currentTestId.value = data[0].id
}
} catch (error) {
console.error('Failed to load test configs:', error)
throw error
}
}
const loadTestResults = async (testId: string) => {
try {
const response = await fetch(`/api/test/results/${testId}`)
const data = await response.json()
testResults.value.set(testId, data)
} catch (error) {
console.error(`Failed to load results for test ${testId}:`, error)
throw error
}
}
const runTest = async (config: TestConfig) => {
try {
const response = await fetch('/api/test/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
})
const result = await response.json()
testResults.value.set(config.id, result)
// 更新测试配置列表
if (!testConfigs.value.some(c => c.id === config.id)) {
testConfigs.value.push(config)
}
return result
} catch (error) {
console.error('Failed to run test:', error)
throw error
}
}
const updateFilter = (criteria: Partial<FilterCriteria>) => {
Object.assign(filterCriteria, criteria)
// 保存到localStorage
saveToLocalStorage()
}
const resetFilter = () => {
Object.assign(filterCriteria, {
dateRange: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
end: new Date()
},
metricThresholds: {
accuracy: 0.7,
precision: 0.6,
recall: 0.6,
f1Score: 0.65
},
modelTypes: ['classification', 'regression', 'object_detection'],
status: ['passed', 'failed', 'running']
})
}
// 本地存储相关
const saveToLocalStorage = () => {
try {
const data = {
filterCriteria,
lastTestId: currentTestId.value
}
localStorage.setItem('testDashboardState', JSON.stringify(data))
} catch (error) {
console.error('Failed to save to localStorage:', error)
}
}
const loadFromLocalStorage = () => {
try {
const data = localStorage.getItem('testDashboardState')
if (data) {
const parsed = JSON.parse(data)
// 恢复筛选条件
if (parsed.filterCriteria) {
Object.assign(filterCriteria, parsed.filterCriteria)
// 转换日期字符串为Date对象
if (parsed.filterCriteria.dateRange) {
filterCriteria.dateRange.start = new Date(parsed.filterCriteria.dateRange.start)
filterCriteria.dateRange.end = new Date(parsed.filterCriteria.dateRange.end)
}
}
// 恢复最后选择的测试
if (parsed.lastTestId) {
currentTestId.value = parsed.lastTestId
}
}
} catch (error) {
console.error('Failed to load from localStorage:', error)
}
}
// 初始化
const initialize = async () => {
// 从localStorage加载状态
loadFromLocalStorage()
// 加载测试配置
await loadTestConfigs()
// 加载当前测试的结果
if (currentTestId.value) {
await loadTestResults(currentTestId.value)
}
}
return {
// State
testConfigs,
currentTestId,
testResults,
filterCriteria,
// Getters
currentConfig,
currentResults,
filteredResults,
statistics,
// Actions
loadTestConfigs,
loadTestResults,
runTest,
updateFilter,
resetFilter,
initialize,
saveToLocalStorage
}
})
4.3 类型定义支持
// types/test.ts
export interface TestConfig {
id: string
name: string
modelType: 'classification' | 'regression' | 'object_detection' | 'segmentation'
modelPath: string
datasetPath: string
batchSize: number
epochs: number
learningRate: number
parameters: Record<string, any>
createdAt: Date
updatedAt: Date
}
export interface TestMetrics {
accuracy: number
precision: number
recall: number
f1Score: number
auc?: number
mse?: number
mae?: number
}
export interface PerformanceMetrics {
latency: number
throughput: number
memoryUsage: number
cpuUsage: number
gpuUsage?: number
}
export interface TestResult {
id: string
testId: string
status: 'pending' | 'running' | 'passed' | 'failed'
metrics: TestMetrics
performance: PerformanceMetrics
confusionMatrix?: number[][]
predictions: Array<{
input: any
prediction: any
groundTruth?: any
confidence: number
}>
errors?: Array<{
type: string
message: string
timestamp: Date
}>
timestamp: Date
duration: number
}
export interface FilterCriteria {
dateRange: {
start: Date
end: Date
}
metricThresholds: {
accuracy: number
precision: number
recall: number
f1Score: number
}
modelTypes: string[]
status: string[]
}
4.4 多组件状态共享架构示例
<!-- TestDashboard.vue -->
<script setup lang="ts">
import { useTestStore } from '@/stores/testStore'
import { storeToRefs } from 'pinia'
import { onMounted, watch } from 'vue'
const testStore = useTestStore()
// 使用storeToRefs保持响应性
const {
currentConfig,
currentResults,
filteredResults,
statistics,
filterCriteria
} = storeToRefs(testStore)
// 初始化
onMounted(async () => {
await testStore.initialize()
})
// 监视当前测试ID变化,加载对应结果
watch(
() => testStore.currentTestId,
async (newTestId) => {
if (newTestId) {
await testStore.loadTestResults(newTestId)
testStore.saveToLocalStorage()
}
}
)
// 运行新测试
const handleRunTest = async () => {
const newConfig: TestConfig = {
id: `test_${Date.now()}`,
name: 'New Classification Test',
modelType: 'classification',
modelPath: '/models/resnet50.onnx',
datasetPath: '/datasets/imagenet-sample',
batchSize: 32,
epochs: 10,
learningRate: 0.001,
parameters: {
optimizer: 'adam',
lossFunction: 'categorical_crossentropy'
},
createdAt: new Date(),
updatedAt: new Date()
}
await testStore.runTest(newConfig)
}
</script>
<template>
<div class="test-dashboard">
<!-- 侧边栏:测试配置和筛选 -->
<div class="sidebar">
<TestConfigPanel
:configs="testStore.testConfigs"
v-model="testStore.currentTestId"
@run-test="handleRunTest"
/>
<FilterPanel
v-model="filterCriteria"
@update="testStore.updateFilter"
@reset="testStore.resetFilter"
/>
</div>
<!-- 主内容区:结果展示 -->
<div class="main-content">
<!-- 统计概览 -->
<div class="stats-overview">
<StatCard
title="总测试数"
:value="statistics.total"
icon="📊"
/>
<StatCard
title="通过率"
:value="`${statistics.passRate}%`"
icon="✅"
/>
<StatCard
title="平均准确率"
:value="`${(statistics.averageAccuracy * 100).toFixed(1)}%`"
icon="🎯"
/>
<StatCard
title="平均延迟"
:value="`${statistics.averageLatency.toFixed(1)}ms`"
icon="⚡"
/>
</div>
<!-- 当前测试详情 -->
<div v-if="currentConfig" class="current-test">
<h3>{{ currentConfig.name }}</h3>
<div v-if="currentResults">
<MetricsChart
:metrics="currentResults.metrics"
:performance="currentResults.performance"
/>
<ConfusionMatrix
v-if="currentResults.confusionMatrix"
:matrix="currentResults.confusionMatrix"
/>
</div>
<div v-else class="no-results">
<p>暂无测试结果,点击运行测试开始</p>
<button @click="handleRunTest">运行测试</button>
</div>
</div>
<!-- 历史测试结果 -->
<div class="history-results">
<h3>历史测试结果</h3>
<TestResultsTable
:results="filteredResults"
@select-test="testStore.currentTestId = $event"
/>
</div>
</div>
</div>
</template>
<style scoped>
.test-dashboard {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 300px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
padding: 20px;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.current-test {
background-color: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.history-results {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.no-results {
text-align: center;
padding: 40px;
color: #6c757d;
}
.no-results button {
margin-top: 20px;
padding: 10px 20px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
5. 架构设计图解析
组件通信流程图
状态管理时序图
6. 总结与下篇预告
本文技术要点回顾
通过本文的深入探讨,我们完整构建了一个基于Vue3+Pinia的AI测试可视化仪表盘基础架构:
-
Composition API的深度应用
- 通过
useTestData组合函数实现了测试数据的逻辑封装 - 利用响应式API(ref/reactive)和计算属性(computed)构建数据层
- 实现异步数据加载和实时更新的完整解决方案
- 通过
-
响应式系统的高级特性
- 深入理解Proxy-based响应式系统的优势
- 掌握watch和watchEffect在不同场景下的正确用法
- 实现WebSocket实时数据推送的高性能方案
-
Pinia状态管理的最佳实践
- 设计完整的TestStore管理测试配置、结果和筛选条件
- 实现类型安全的Store定义和组件间状态共享
- 集成本地存储实现状态持久化
-
架构设计的核心思想
- 清晰的层次分离:UI层、状态层、服务层
- 响应式数据流的单向控制
- 可扩展、可维护的代码组织方式
性能优化关键点总结
-
避免不必要的响应式
- 对大型静态数据使用
markRaw - 合理使用
shallowRef和shallowReactive
- 对大型静态数据使用
-
批量更新策略
- 使用
Object.assign批量更新响应式对象 - 利用
nextTick控制DOM更新时机
- 使用
-
内存管理
- 及时清理WebSocket连接和定时器
- 实现数据缓冲区,防止内存泄漏
-
计算属性缓存
- 合理使用computed缓存复杂计算
- 避免在模板中进行复杂运算
下篇预告:ECharts图表深度开发
在下一篇中,我们将深入探讨ECharts在AI测试可视化中的高级应用:
《ECharts深度开发:AI测试数据的可视化艺术》
核心内容包括:
-
ECharts与Vue3的深度集成
- 响应式图表组件的封装策略
- 动态数据更新的性能优化
- 自定义主题和样式系统
-
AI测试专用图表开发
- 混淆矩阵可视化方案
- ROC曲线与PR曲线的交互实现
- 模型性能对比雷达图
- 训练损失曲线的实时绘制
-
大数据量可视化优化
- 百万级数据点的渲染策略
- 虚拟滚动和数据分片加载
- WebGL加速的3D可视化
-
交互与联动设计
- 多图表联动分析
- 数据钻取和下钻实现
- 自定义交互控件开发
-
移动端适配与性能
- 响应式图表布局
- 移动端手势交互
- 离线数据可视化
技术深度:
- 自定义ECharts扩展开发
- WebAssembly在数据处理中的应用
- 深度学习模型的可视化解释
通过下篇的学习,您将掌握如何将复杂的AI测试数据转化为直观、交互丰富的可视化洞察,真正发挥数据可视化的决策支持价值。
立即开始实践:
本文的完整代码示例已准备就绪,建议您按照以下步骤开始实践:
- 环境准备:确保Node.js版本≥16,创建Vue3项目
- 核心实现:逐步实现useTestData组合函数
- 状态管理:搭建Pinia Store并集成本地存储
- 实时功能:添加WebSocket实时数据支持
- 优化迭代:根据性能分析结果持续优化
期待在下篇ECharts深度开发中与您再次相见,共同探索数据可视化的艺术与科学!
更多推荐



所有评论(0)