工作台整体布局

数据智能体工作台的前端目标:低门槛提问、透明可审计、结果可复制。推荐三区布局:

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
// stores/session.ts
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
// api/ask.ts
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 | gaugeecharts_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 默认折叠可复制,满足企业审计习惯

思考题

  1. 抽屉宽度 480px 是否足够展示宽表结果?何时改用全屏模态?
  2. 图表为空但文本有数时,前端应如何降级展示?
  3. 如何在 UI 上提示用户「当前继承上一轮上下文」?

系列导航

主题
01 架构全景
02 Text-to-SQL
03 RAG 指标库
04 多轮澄清
05 安全校验
06 本篇
07 演示验证
08 反馈闭环

← 返回专题首页