工作台整体布局 数据智能体工作台的前端目标:低门槛提问、透明可审计、结果可复制 。推荐三区布局:
1 2 3 4 5 6 7 8 flowchart LR subgraph Layout["Workbench 页面"] NAV[顶栏:专题 / 知识库 / 历史] MAIN[主区:快捷指标卡片] DRAWER[右侧抽屉:DataAgent] end NAV --> MAIN MAIN -->|点击问数| DRAWER
区域
职责
顶栏
导航、用户信息、新对话
主区
核心指标快捷入口(销售收入、客户留存率、新增客户数)
抽屉
多轮对话、SQL 展示、图表、反馈
技术栈
依赖
版本建议
用途
Vue 3
3.4+
Composition API
Vite
5+
构建
Element Plus
2.6+
UI 组件
ECharts
5+
图表
Pinia
2+
会话状态
axios
1+
API 调用
目录结构(虚构) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 src/ ├── views/ │ └── Workbench.vue ├── components/ │ └── data-agent/ │ ├── DataAgentDrawer.vue │ ├── ChatMessageList.vue │ ├── ChatInput.vue │ ├── ClarificationPanel.vue │ ├── SqlPreview.vue │ ├── ChartPanel.vue │ └── AgentFlowTimeline.vue ├── stores/ │ └── session.ts └── api/ └── ask.ts
Workbench 主页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <template> <div class="workbench"> <el-row :gutter="16"> <el-col v-for="card in metricCards" :key="card.code" :span="8"> <el-card shadow="hover" @click="openAsk(card.prompt)"> <h3>{{ card.name }}</h3> <p>{{ card.desc }}</p> </el-card> </el-col> </el-row> <DataAgentDrawer v-model="drawerVisible" :seed-question="seedQuestion" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import DataAgentDrawer from '@/components/data-agent/DataAgentDrawer.vue' const drawerVisible = ref(false) const seedQuestion = ref('') const metricCards = [ { code: 'M_SALES_REVENUE', name: '销售收入', desc: '华东区 / 华北区营收', prompt: '华东区 2026 年一季度销售收入' }, { code: 'M_RETENTION', name: '客户留存率', desc: '活跃客户留存', prompt: '华北区 2026 年客户留存率' }, { code: 'M_NEW_CUST', name: '新增客户数', desc: '当期新客', prompt: '2026 年 5 月新增客户数' }, ] function openAsk(q: string) { seedQuestion.value = q drawerVisible.value = true } </script>
DataAgentDrawer 抽屉 1 2 3 4 5 6 7 8 9 flowchart TB DR[DataAgentDrawer] DR --> HDR[抽屉头:标题 + 新对话] DR --> LIST[ChatMessageList] DR --> CLAR[ClarificationPanel 条件渲染] DR --> INPUT[ChatInput] DR --> SIDE[侧边:SqlPreview + AgentFlow] LIST --> MSG1[用户气泡] LIST --> MSG2[助手气泡 + ChartPanel]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <el-drawer v-model="visible" size="480px" title="数据智能体"> <div class="drawer-body"> <ChatMessageList :messages="messages" /> <ClarificationPanel v-if="pendingClarification" :data="pendingClarification" @select="onClarificationSelect" /> <ChatInput :loading="loading" @submit="onSubmit" /> </div> <template #footer> <SqlPreview v-if="lastSql" :sql="lastSql" /> <AgentFlowTimeline v-if="agentFlow" :steps="agentFlow" /> </template> </el-drawer> </template>
会话状态 Pinia 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { defineStore } from 'pinia' export const useSessionStore = defineStore ('session' , { state : () => ({ sessionId : '' as string , messages : [] as ChatMessage [], pendingClarification : null as Clarification | null , lastSql : '' as string , agentFlow : [] as AgentStep [], }), actions : { reset ( ) { this .sessionId = crypto.randomUUID () this .messages = [] this .pendingClarification = null this .lastSql = '' this .agentFlow = [] }, }, })
API 封装 1 2 3 4 5 6 7 8 9 10 11 12 13 import axios from 'axios' export interface AskPayload { question : string session_id : string clarification_reply ?: { field : string ; value : string } }export async function postAsk (payload : AskPayload ) { const { data } = await axios.post ('/api/ask' , payload) return data }
消息列表与澄清 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!-- ClarificationPanel.vue --> <template> <div class="clarification"> <p>{{ data.questions[0] }}</p> <div class="chips"> <el-tag v-for="q in data.suggested_questions" :key="q" class="chip" @click="$emit('select', q)" > {{ q }} </el-tag> </div> <el-radio-group v-if="data.options" @change="onOptionPick"> <el-radio v-for="opt in data.options" :key="opt.code" :value="opt.code"> {{ opt.label }} — {{ opt.desc }} </el-radio> </el-radio-group> </div> </template>
澄清区与正式答案视觉区分 :浅黄背景 + 虚线左边框。
助手消息与图表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!-- ChartPanel.vue --> <template> <div v-if="chartOption" ref="chartRef" class="chart" /> </template> <script setup lang="ts"> import { ref, onMounted, watch } from 'vue' import * as echarts from 'echarts' const props = defineProps<{ chartOption: object | null }>() const chartRef = ref<HTMLElement>() onMounted(() => { if (props.chartOption && chartRef.value) { const chart = echarts.init(chartRef.value) chart.setOption(props.chartOption) } }) </script>
后端 explain_result 返回 chart_type: bar | line | pie | gauge 与 echarts_option,前端原样渲染。
虚构图表示例(销售收入柱状) 1 2 3 4 5 6 7 8 9 { "chart_type" : "bar" , "echarts_option" : { "title" : { "text" : "华东区 2026Q1 销售收入" } , "xAxis" : { "type" : "category" , "data" : [ "华东区" ] } , "yAxis" : { "type" : "value" , "name" : "万元" } , "series" : [ { "type" : "bar" , "data" : [ 12850.6 ] } ] } }
SqlPreview 可审计 1 2 3 4 5 6 7 8 <template> <el-collapse> <el-collapse-item title="查看 SQL"> <pre class="sql-block"><code>{{ sql }}</code></pre> <el-button size="small" @click="copySql">复制</el-button> </el-collapse-item> </el-collapse> </template>
默认折叠,满足审计与排障,不干扰业务阅读。
AgentFlowTimeline 将 /ask 返回的 agent_flow 步骤可视化为时间线:
步骤
状态
耗时
classify_domain
ok
12ms
retrieve_all
ok
45ms
build_sql
ok
8ms
validate_sql
ok
3ms
execute_sql
ok
120ms
explain_result
ok
15ms
1 2 3 4 5 6 7 8 9 <el-timeline> <el-timeline-item v-for="step in steps" :key="step.name" :type="step.status === 'ok' ? 'success' : 'danger'" > {{ step.name }} — {{ step.duration_ms }}ms </el-timeline-item> </el-timeline>
提交与加载态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 async function onSubmit (question : string ) { loading.value = true store.messages .push ({ role : 'user' , content : question }) try { const res = await postAsk ({ question, session_id : store.sessionId , }) if (res.needs_clarification ) { store.pendingClarification = res.clarification store.messages .push ({ role : 'assistant' , content : res.clarification .questions [0 ], type : 'clarify' }) } else { store.pendingClarification = null store.lastSql = res.sql store.agentFlow = res.agent_flow store.messages .push ({ role : 'assistant' , content : res.answer , chart : res.echarts_option , }) } } finally { loading.value = false } }
样式要点 1 2 3 4 .workbench .el-card { cursor : pointer; transition : transform 0.2s ; }.workbench .el-card :hover { transform : translateY (-2px ); }.clarification { background : #fffbe6 ; border-left : 3px dashed #e6a23c ; padding : 12px ; }.sql-block { font-size : 12px ; background : #f5f7fa ; padding : 8px ; overflow-x : auto; }
本章小结
工作台 = 快捷指标入口 + 右侧问数抽屉,降低首次使用成本
澄清、答案、SQL、图表、agent_flow 分区展示,职责清晰
Pinia 管理 session_id 与消息流,支持多轮与重置
SQL 默认折叠可复制,满足企业审计习惯
思考题
抽屉宽度 480px 是否足够展示宽表结果?何时改用全屏模态?
图表为空但文本有数时,前端应如何降级展示?
如何在 UI 上提示用户「当前继承上一轮上下文」?
系列导航
← 返回专题首页