
Granite TimeSeries FlowState R1前端可视化实战使用Vue构建预测结果仪表盘如果你正在使用Granite TimeSeries FlowState R1这类强大的时序预测模型可能会遇到一个普遍的问题模型预测的结果通常是一堆冷冰冰的数字或JSON数据。业务同事或者非技术背景的决策者很难从这些原始数据中快速洞察趋势、发现问题或做出判断。这时候一个直观、交互式的可视化仪表盘就显得至关重要。它能将复杂的预测数据转化为一目了然的图表让数据自己“说话”。今天我们就来聊聊如何用大家熟悉的Vue.js框架快速为你的预测服务搭建一个既专业又好看的前端仪表盘。整个过程不复杂核心就是调用API、画图、再加点交互让业务人员也能玩转预测数据。1. 项目目标与核心思路在开始写代码之前我们先明确一下要做一个什么样的东西以及大概怎么实现。我们的目标是构建一个单页应用SPA它主要做三件事获取数据从部署好的Granite TimeSeries FlowState R1预测服务的后端API获取历史数据和预测结果。展示数据用图表清晰展示历史趋势与未来预测的对比。交互控制提供一些简单的控件比如滑块、下拉菜单让用户能动态调整查看数据的维度或模拟不同预测参数的效果。技术选型上Vue 3的组合式API开发体验非常流畅是我们构建这类交互密集型应用的好帮手。可视化方面ECharts功能强大、图表类型丰富而且有成熟的Vue版本集成方案。网络请求则用Axios简单可靠。整个应用的架构思路很直接Vue组件负责渲染界面和响应用户交互当用户操作比如点击查询、调整滤波器时组件通过Axios向预测服务发送请求拿到数据后驱动ECharts图表更新。下面我们就一步步把它实现出来。2. 环境搭建与项目初始化首先确保你的开发环境已经准备好。你需要安装Node.js建议16.x或以上版本和npm或yarn、pnpm。打开终端我们用Vite来快速创建一个Vue项目它比传统的Vue CLI更轻更快。# 使用 npm 创建项目 npm create vuelatest granite-timeseries-dashboard # 按照提示进行选择 # ✔ Project name: granite-timeseries-dashboard # ✔ Add TypeScript? No 根据喜好本文用JavaScript # ✔ Add JSX Support? No # ✔ Add Vue Router for Single Page Application development? No 我们先不引入路由 # ✔ Add Pinia for state management? No 初期简单应用可不用 # ✔ Add Vitest for Unit Testing? No # ✔ Add an End-to-End Testing Solution? No # ✔ Add ESLint for code quality? Yes 建议添加保持代码规范 # 进入项目目录并安装依赖 cd granite-timeseries-dashboard npm install项目创建好后我们安装必要的依赖库# 安装核心依赖图表库、HTTP客户端、UI组件库可选用于快速构建表单和控件 npm install echarts vue-echarts axios # 如果你喜欢Element Plus的UI风格可以安装 npm install element-plus element-plus/icons-vue安装完成后可以运行npm run dev启动开发服务器在浏览器打开http://localhost:5173就能看到默认的Vue页面了。接下来我们需要对项目结构做一点调整并初始化核心的图表组件。在src/components目录下我们新建两个文件TimeSeriesChart.vue和FilterPanel.vue。3. 核心组件开发可视化图表图表是我们仪表盘的心脏。我们使用vue-echarts来封装ECharts这样可以在Vue中声明式地使用图表。首先在src/components/TimeSeriesChart.vue中创建图表组件template div classchart-container v-chart classchart :optionchartOption :autoresizetrue v-loadingisLoading / /div /template script setup import { computed, ref, watch } from vue; import { use } from echarts/core; import { CanvasRenderer } from echarts/renderers; import { LineChart } from echarts/charts; import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent, } from echarts/components; import VChart from vue-echarts; // 按需引入ECharts模块以减小打包体积 use([ CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent, ]); // 定义组件接收的props const props defineProps({ // 历史数据格式为 [{timestamp: 2024-01-01, value: 100}, ...] historicalData: { type: Array, default: () [], }, // 预测数据格式同上 forecastData: { type: Array, default: () [], }, // 图表标题 title: { type: String, default: 时间序列预测分析, }, // 是否显示加载状态 isLoading: { type: Boolean, default: false, }, }); // 计算属性根据props数据生成ECharts配置项 const chartOption computed(() { // 处理历史数据 const historySeries props.historicalData.map(item [item.timestamp, item.value]); // 处理预测数据预测数据的时间戳通常是连续的 const forecastSeries props.forecastData.map(item [item.timestamp, item.value]); // 找到历史数据的最后一个时间点用于在图表上区分历史与预测区域 const lastHistoryPoint historySeries[historySeries.length - 1]; const splitTime lastHistoryPoint ? lastHistoryPoint[0] : null; return { title: { text: props.title, left: center, textStyle: { fontSize: 16, }, }, tooltip: { trigger: axis, axisPointer: { type: cross, label: { backgroundColor: #6a7985, }, }, // 自定义tooltip格式 formatter: function (params) { let result ${params[0].axisValue}br/; params.forEach(param { const marker param.marker; const seriesName param.seriesName; const value param.value[1].toFixed(2); result ${marker} ${seriesName}: ${value}br/; }); return result; }, }, legend: { data: [历史数据, 预测数据], top: 30, }, grid: { left: 3%, right: 4%, bottom: 12%, top: 15%, containLabel: true, }, xAxis: { type: time, boundaryGap: false, axisLine: { onZero: false }, splitLine: { show: true }, // 时间轴格式化 axisLabel: { formatter: function (value) { // 简单格式化日期显示 const date new Date(value); return ${date.getMonth()1}/${date.getDate()}; }, }, }, yAxis: { type: value, splitLine: { show: true }, axisLabel: { formatter: {value}, }, }, // 数据区域缩放方便查看细节 dataZoom: [ { type: inside, start: 0, end: 100, }, { show: true, start: 0, end: 100, bottom: 10, height: 20, }, ], series: [ { name: 历史数据, type: line, smooth: true, symbol: circle, symbolSize: 6, itemStyle: { color: #5470c6, // 历史数据用蓝色 }, lineStyle: { width: 3, }, data: historySeries, markLine: splitTime ? { silent: true, lineStyle: { type: dashed, color: #999, width: 1, }, data: [ { xAxis: splitTime, // 在历史与预测分界处画一条虚线 }, ], label: { formatter: 预测起点, position: insideEndTop, }, } : null, }, { name: 预测数据, type: line, smooth: true, symbol: circle, symbolSize: 6, itemStyle: { color: #91cc75, // 预测数据用绿色 }, lineStyle: { width: 3, type: dashed, // 预测线用虚线表示 }, data: forecastSeries, }, ], }; }); /script style scoped .chart-container { width: 100%; height: 500px; position: relative; } .chart { width: 100%; height: 100%; } /style这个组件接收历史数据和预测数据并将它们绘制成一条连续的曲线用一条垂直虚线清晰地区分历史部分和预测部分。图表还内置了缩放、提示框等交互功能。4. 核心组件开发交互滤波器面板光有图表还不够一个好的仪表盘需要让用户能够“控制”数据。接下来我们构建一个滤波器面板组件用于调整预测参数或数据视图。在src/components/FilterPanel.vue中template div classfilter-panel h3数据筛选与控制/h3 el-form :modelfilterForm label-width100px submit.preventhandleSubmit !-- 预测步长选择 -- el-form-item label预测步长 el-select v-modelfilterForm.forecastSteps placeholder请选择 changeemitFilterChange el-option label未来7天 :value7 / el-option label未来30天 :value30 / el-option label未来90天 :value90 / /el-select /el-form-item !-- 置信区间滑块 -- el-form-item label置信区间 div classslider-with-input el-slider v-modelfilterForm.confidenceLevel :min50 :max99 :step1 show-stops :show-tooltipfalse inputemitFilterChange / span classslider-value{{ filterForm.confidenceLevel }}%/span /div div classform-tip值越高预测的不确定性范围越宽。/div /el-form-item !-- 数据平滑滤波器 -- el-form-item label数据平滑 el-radio-group v-modelfilterForm.smoothing changeemitFilterChange el-radio :labelnone无/el-radio el-radio :labelmoving_avg移动平均/el-radio el-radio :labelexponential指数平滑/el-radio /el-radio-group div classform-tip v-iffilterForm.smoothing ! none 应用{{ filterForm.smoothing moving_avg ? 移动平均 : 指数平滑 }}滤波器使趋势更明显。 /div /el-form-item !-- 异常值处理 -- el-form-item label异常值处理 el-switch v-modelfilterForm.removeOutliers active-text开启 inactive-text关闭 changeemitFilterChange / div classform-tip开启后将尝试自动识别并处理极端数据点。/div /el-form-item !-- 操作按钮 -- el-form-item el-button typeprimary clickhandleSubmit :loadingisLoading应用筛选/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form /div /template script setup import { ref, reactive } from vue; // 定义组件发射的事件 const emit defineEmits([filter-change, filter-reset]); // 加载状态用于按钮 const isLoading ref(false); // 滤波器表单数据 const filterForm reactive({ forecastSteps: 30, // 默认预测未来30步 confidenceLevel: 95, // 默认95%置信区间 smoothing: none, // 平滑方法 removeOutliers: false, // 是否移除异常值 }); // 提交筛选这里触发父组件重新获取数据 const handleSubmit async () { isLoading.value true; // 触发事件将当前滤波器参数传递给父组件 emit(filter-change, { ...filterForm }); // 模拟一个短暂的网络延迟让用户体验更好 setTimeout(() { isLoading.value false; }, 300); }; // 单个参数变化时实时触发可选根据需求决定是实时更新还是点击按钮更新 const emitFilterChange () { // 如果是滑块等频繁操作可以防抖这里简单触发 // emit(filter-change, { ...filterForm }); }; // 重置滤波器 const handleReset () { Object.assign(filterForm, { forecastSteps: 30, confidenceLevel: 95, smoothing: none, removeOutliers: false, }); emit(filter-reset); emit(filter-change, { ...filterForm }); // 重置后也应用一次 }; /script style scoped .filter-panel { padding: 20px; background-color: #f9fafc; border-radius: 8px; border: 1px solid #ebeef5; } .filter-panel h3 { margin-top: 0; margin-bottom: 20px; color: #303133; } .slider-with-input { display: flex; align-items: center; gap: 20px; } .slider-value { min-width: 50px; text-align: center; font-weight: bold; color: #409eff; } .form-tip { font-size: 12px; color: #909399; margin-top: 5px; } /style这个面板提供了几个常见的时序数据控制选项选择预测未来的时间长度、调整预测的置信区间、选择是否对数据进行平滑处理、以及是否过滤异常值。当用户调整这些参数并点击“应用筛选”时组件会将这些参数打包发送出去。5. 页面集成与数据获取现在我们把图表和滤波器组合到主页面中并实现最关键的一步从Granite TimeSeries FlowState R1的后端API获取真实数据。修改src/App.vue文件template div idapp div classdashboard-header h1Granite时序预测分析仪表盘/h1 p基于FlowState R1模型的交互式预测结果可视化/p /div div classdashboard-container !-- 左侧滤波器面板 -- aside classdashboard-sidebar FilterPanel filter-changehandleFilterChange filter-resethandleFilterReset / !-- 可以在这里添加其他面板比如数据概览 -- div classinfo-panel h4数据状态/h4 p历史数据点{{ historicalData.length }}/p p预测数据点{{ forecastData.length }}/p p最后更新{{ lastUpdateTime }}/p /div /aside !-- 右侧主内容区 -- main classdashboard-main !-- 图表展示区 -- div classchart-section TimeSeriesChart :historical-datahistoricalData :forecast-dataforecastData :is-loadingchartLoading title业务指标预测趋势 / /div !-- 数据摘要或额外图表可以放在这里 -- div classmetrics-section el-row :gutter20 el-col :span8 div classmetric-card h4历史均值/h4 p classmetric-value{{ historicalMean.toFixed(2) }}/p /div /el-col el-col :span8 div classmetric-card h4预测均值/h4 p classmetric-value{{ forecastMean.toFixed(2) }}/p /div /el-col el-col :span8 div classmetric-card h4趋势变化/h4 p classmetric-value :classtrendClass{{ trendPercentage }}%/p /div /el-col /el-row /div /main /div /div /template script setup import { ref, reactive, computed, onMounted } from vue; import axios from axios; import TimeSeriesChart from ./components/TimeSeriesChart.vue; import FilterPanel from ./components/FilterPanel.vue; import { ElMessage } from element-plus; // 引入Element Plus样式 import element-plus/dist/index.css; // 状态定义 const historicalData ref([]); // 历史数据 const forecastData ref([]); // 预测数据 const chartLoading ref(false); // 图表加载状态 const lastUpdateTime ref(--); // 最后更新时间 const currentFilters reactive({}); // 当前应用的滤波器参数 // 计算属性一些简单的数据摘要 const historicalMean computed(() { if (historicalData.value.length 0) return 0; const sum historicalData.value.reduce((acc, cur) acc cur.value, 0); return sum / historicalData.value.length; }); const forecastMean computed(() { if (forecastData.value.length 0) return 0; const sum forecastData.value.reduce((acc, cur) acc cur.value, 0); return sum / forecastData.value.length; }); const trendPercentage computed(() { if (historicalMean.value 0) return 0; return (((forecastMean.value - historicalMean.value) / historicalMean.value) * 100).toFixed(1); }); const trendClass computed(() { return trendPercentage.value 0 ? trend-up : trend-down; }); // 模拟或真实API的URL请替换为你的Granite服务地址 const API_BASE_URL http://your-granite-api-server:port; // TODO: 替换为实际地址 const FORECAST_ENDPOINT /api/v1/predict; // TODO: 替换为实际端点 // 获取预测数据 const fetchForecastData async (filters {}) { chartLoading.value true; try { // 构建请求参数这些参数需要与你的Granite服务API匹配 const params { steps: filters.forecastSteps || 30, confidence: filters.confidenceLevel || 95, smoothing: filters.smoothing || none, remove_outliers: filters.removeOutliers || false, // 这里通常还需要传递历史数据或数据标识根据你的API设计调整 // series_id: your_series_id, }; // 使用axios发送GET或POST请求 const response await axios.get(API_BASE_URL FORECAST_ENDPOINT, { params }); // 或者如果是POST // const response await axios.post(API_BASE_URL FORECAST_ENDPOINT, params); // 假设API返回格式为 { historical: [...], forecast: [...], metadata: {...} } const result response.data; if (result result.historical result.forecast) { historicalData.value result.historical; forecastData.value result.forecast; lastUpdateTime.value new Date().toLocaleTimeString(); ElMessage.success(数据更新成功); } else { throw new Error(API返回数据格式异常); } } catch (error) { console.error(获取预测数据失败:, error); ElMessage.error(数据加载失败请检查网络或API配置); // 这里可以加载模拟数据作为后备 loadMockData(); } finally { chartLoading.value false; } }; // 加载模拟数据用于演示或API不可用时 const loadMockData () { const now new Date(); const mockHistory []; const mockForecast []; // 生成过去30天的历史数据模拟 for (let i 30; i 0; i--) { const date new Date(now); date.setDate(date.getDate() - i); mockHistory.push({ timestamp: date.toISOString().split(T)[0], value: 100 Math.random() * 50 Math.sin(i / 5) * 20, // 模拟一些趋势和噪声 }); } // 生成未来30天的预测数据 for (let i 1; i 30; i) { const date new Date(now); date.setDate(date.getDate() i); // 基于最后一点历史数据加上一点趋势和随机性 const lastValue mockHistory[mockHistory.length - 1].value; mockForecast.push({ timestamp: date.toISOString().split(T)[0], value: lastValue 0.5 * i (Math.random() - 0.5) * 10, }); } historicalData.value mockHistory; forecastData.value mockForecast; lastUpdateTime.value new Date().toLocaleTimeString() (模拟数据); ElMessage.warning(正在使用模拟数据演示); }; // 处理滤波器变化 const handleFilterChange (filters) { Object.assign(currentFilters, filters); // 根据新的滤波器参数重新获取数据 fetchForecastData(currentFilters); }; // 处理滤波器重置 const handleFilterReset () { // currentFilters会被FilterPanel组件内部重置这里只需重新获取数据 fetchForecastData(currentFilters); }; // 组件挂载时加载初始数据 onMounted(() { fetchForecastData(); }); /script style #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; min-height: 100vh; background-color: #f5f7fa; } .dashboard-header { text-align: center; padding: 30px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; margin-bottom: 30px; } .dashboard-header h1 { margin: 0 0 10px 0; font-size: 2.2rem; } .dashboard-header p { margin: 0; opacity: 0.9; } .dashboard-container { display: flex; max-width: 1400px; margin: 0 auto; padding: 0 20px; gap: 30px; } .dashboard-sidebar { flex: 0 0 320px; } .dashboard-main { flex: 1; min-width: 0; /* 防止flex item溢出 */ } .chart-section { background: white; border-radius: 12px; padding: 25px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); margin-bottom: 30px; } .metrics-section { margin-top: 20px; } .metric-card { background: white; border-radius: 8px; padding: 20px; text-align: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); height: 100%; } .metric-card h4 { margin-top: 0; margin-bottom: 10px; color: #606266; font-size: 14px; } .metric-value { font-size: 28px; font-weight: bold; margin: 0; color: #303133; } .trend-up { color: #67c23a; } .trend-down { color: #f56c6c; } .info-panel { background: white; border-radius: 8px; padding: 20px; margin-top: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .info-panel h4 { margin-top: 0; color: #303133; } .info-panel p { margin: 8px 0; color: #606266; font-size: 14px; } /* 响应式调整 */ media (max-width: 992px) { .dashboard-container { flex-direction: column; } .dashboard-sidebar { flex: none; width: 100%; } } /style在这个主页面中我们完成了最后的拼图布局采用了经典的侧边栏主内容区布局侧边栏放置滤波器主区域展示图表和关键指标。数据流页面加载时自动调用fetchForecastData获取初始数据。当用户在FilterPanel中调整参数并点击应用时handleFilterChange会被触发带着新的参数再次调用API。状态管理使用Vue的响应式系统ref,reactive管理数据、加载状态和滤波器参数。错误处理与降级在fetchForecastData中如果真实API调用失败会捕获错误并加载模拟数据确保页面始终有内容可展示。用户体验添加了加载状态、成功/错误提示以及一些简单的数据摘要均值、趋势来丰富仪表盘的信息维度。6. 总结与展望到这里一个具备基本功能的Granite时序预测可视化仪表盘就搭建完成了。我们利用Vue的响应式特性和组件化开发将数据获取、图表渲染、用户交互清晰地分离开来。ECharts提供了强大的图表表现力而Element Plus则帮助我们快速构建了美观的滤波器界面。实际使用时你需要将代码中的API_BASE_URL和FORECAST_ENDPOINT替换成你部署的Granite TimeSeries FlowState R1服务的真实地址和端点。根据后端API的具体要求可能还需要调整请求参数和数据处理逻辑。这个仪表盘只是一个起点你可以根据实际业务需求进行大量扩展例如多图表对比同时展示多个关键指标的预测情况。下钻分析点击图表上的某个点可以查看该时间点的详细数据或相关维度。预测区间展示在图表上用阴影区域展示置信区间让预测的不确定性一目了然。数据导出添加将图表或数据导出为图片、PDF或CSV的功能。定时刷新实现数据的自动定时更新。将复杂的AI模型预测结果通过这样一个直观的界面呈现出来能够极大地提升数据在团队中的沟通效率和决策支持能力。希望这个实战示例能为你构建自己的数据可视化应用提供一个扎实的起点。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。