AI数字人可视化图表设计文档
📋 目录
🎯 项目概述
项目背景
AI数字人可视化图表系统是一个集成了人工智能、语音识别、自然语言处理和数据可视化的智能交互平台。用户可以通过语音或文字与AI数字人进行对话,AI理解用户意图后生成相应的图表数据,前端实时渲染展示。
核心功能
- 🎤 多模态输入:支持语音和文字输入
- 🤖 AI智能解析:理解用户意图,生成图表需求
- 📊 多样化图表:支持饼图、折线图、柱状图、表格
- 🔄 实时交互:支持单图表和多图表展示
- 👤 数字人界面:Live2D数字人交互界面
技术栈
- 前端:Vue 3 + TypeScript + ECharts + Element Plus
- 语音处理:Web Speech API + VAD (Voice Activity Detection)
- AI集成:OpenAI API / 自定义AI模型
- 实时通信:WebSocket / Server-Sent Events
- 数字人:Live2D / VRM模型
🏗️ 技术架构
整体架构图
graph TB
A[用户输入层] --> B[前端处理层]
B --> C[通信层]
C --> D[后端服务层]
D --> E[AI处理层]
E --> F[数据生成层]
F --> C
C --> B
B --> G[渲染展示层]
A1[语音输入] --> A
A2[文字输入] --> A
G1[图表渲染] --> G
G2[数字人展示] --> G
G3[交互反馈] --> G
数据流程
sequenceDiagram
participant U as 用户
participant F as 前端
participant B as 后端
participant AI as AI服务
U->>F: 语音/文字输入
F->>F: 语音转文字(如需要)
F->>B: 发送用户请求
B->>AI: 解析用户意图
AI->>B: 返回图表配置
B->>B: 生成图表数据
B->>F: 返回图表数据
F->>F: 渲染图表
F->>U: 展示结果
🎨 前端设计方案
组件架构
bash
src/
├── components/
│ ├── AIAssistant/ # AI助手组件
│ │ ├── DigitalHuman.vue # 数字人展示
│ │ ├── VoiceInput.vue # 语音输入
│ │ └── ChatInterface.vue # 对话界面
│ ├── Charts/ # 图表组件
│ │ ├── BaseChart.vue # 基础图表组件
│ │ ├── LineChart.vue # 折线图
│ │ ├── BarChart.vue # 柱状图
│ │ ├── PieChart.vue # 饼图
│ │ └── DataTable.vue # 数据表格
│ └── Layout/ # 布局组件
│ ├── ChartContainer.vue # 图表容器
│ └── MultiChart.vue # 多图表布局
核心组件设计
1. AI助手主组件 (AIAssistant.vue)
vue
<template>
<div class="ai-assistant">
<!-- 数字人展示区 -->
<DigitalHuman
:emotion="currentEmotion"
:speaking="isSpeaking"
@ready="onDigitalHumanReady"
/>
<!-- 交互界面 -->
<div class="interaction-panel">
<!-- 语音输入 -->
<VoiceInput
v-model:recording="isRecording"
@voice-result="handleVoiceInput"
@error="handleVoiceError"
/>
<!-- 文字输入 -->
<div class="text-input">
<el-input
v-model="textInput"
placeholder="请输入您的需求..."
@keyup.enter="handleTextInput"
/>
<el-button @click="handleTextInput" type="primary">
发送
</el-button>
</div>
<!-- 对话历史 -->
<ChatInterface
:messages="chatHistory"
@message-click="handleMessageClick"
/>
</div>
<!-- 图表展示区 -->
<ChartContainer
:charts="chartData"
:layout="chartLayout"
@chart-interaction="handleChartInteraction"
/>
</div>
</template>
2. 语音输入组件 (VoiceInput.vue)
vue
<template>
<div class="voice-input">
<el-button
:class="{ recording: recording }"
@mousedown="startRecording"
@mouseup="stopRecording"
@mouseleave="stopRecording"
circle
size="large"
>
<el-icon><Microphone /></el-icon>
</el-button>
<!-- 语音波形显示 -->
<div class="voice-wave" v-if="recording">
<canvas ref="waveCanvas"></canvas>
</div>
<!-- 识别结果预览 -->
<div class="recognition-preview" v-if="recognitionText">
{{ recognitionText }}
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
recording: Boolean
})
const emit = defineEmits(['update:recording', 'voice-result', 'error'])
let recognition = null
let audioContext = null
let analyser = null
// 初始化语音识别
function initSpeechRecognition() {
if ('webkitSpeechRecognition' in window) {
recognition = new webkitSpeechRecognition()
recognition.continuous = true
recognition.interimResults = true
recognition.lang = 'zh-CN'
recognition.onresult = handleSpeechResult
recognition.onerror = handleSpeechError
}
}
// 处理语音识别结果
function handleSpeechResult(event) {
let finalTranscript = ''
let interimTranscript = ''
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript
if (event.results[i].isFinal) {
finalTranscript += transcript
} else {
interimTranscript += transcript
}
}
if (finalTranscript) {
emit('voice-result', finalTranscript)
}
}
</script>
3. 基础图表组件 (BaseChart.vue)
vue
<template>
<div class="base-chart" ref="chartContainer">
<div class="chart-header" v-if="title">
<h3>{{ title }}</h3>
<div class="chart-actions">
<el-button @click="exportChart" size="small">导出</el-button>
<el-button @click="refreshChart" size="small">刷新</el-button>
</div>
</div>
<div ref="chartDom" class="chart-content"></div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
type: {
type: String,
required: true,
validator: (value) => ['line', 'bar', 'pie', 'table'].includes(value)
},
data: {
type: Object,
required: true
},
title: String,
options: Object
})
const emit = defineEmits(['chart-ready', 'chart-click', 'data-change'])
const chartContainer = ref(null)
const chartDom = ref(null)
let chartInstance = null
// 初始化图表
function initChart() {
if (!chartDom.value) return
chartInstance = echarts.init(chartDom.value)
chartInstance.on('click', handleChartClick)
updateChart()
emit('chart-ready', chartInstance)
}
// 更新图表
function updateChart() {
if (!chartInstance) return
const option = generateChartOption()
chartInstance.setOption(option, true)
}
// 生成图表配置
function generateChartOption() {
const baseOption = {
title: { text: props.title },
tooltip: { trigger: 'axis' },
legend: {},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
}
switch (props.type) {
case 'line':
return generateLineOption(baseOption)
case 'bar':
return generateBarOption(baseOption)
case 'pie':
return generatePieOption(baseOption)
default:
return baseOption
}
}
</script>
🔌 API接口设计
接口规范
1. 用户输入处理接口
typescript
// POST /api/ai/process
interface ProcessRequest {
input: string; // 用户输入内容
inputType: 'voice' | 'text'; // 输入类型
sessionId: string; // 会话ID
context?: any; // 上下文信息
}
interface ProcessResponse {
success: boolean;
data: {
intent: string; // 用户意图
chartType: 'line' | 'bar' | 'pie' | 'table' | 'multi';
chartConfig: ChartConfig;
response: string; // AI回复文本
suggestions?: string[]; // 建议操作
};
error?: string;
}
2. 图表数据接口
typescript
// POST /api/charts/generate
interface ChartGenerateRequest {
intent: string; // 解析后的意图
parameters: any; // 参数
chartType: string; // 图表类型
}
interface ChartGenerateResponse {
success: boolean;
data: {
chartId: string;
chartType: string;
title: string;
data: ChartData;
metadata: ChartMetadata;
};
}
// 图表数据结构
interface ChartData {
// 折线图/柱状图数据
categories?: string[]; // X轴分类
series?: SeriesData[]; // 数据系列
// 饼图数据
pieData?: PieDataItem[];
// 表格数据
tableData?: TableRow[];
columns?: TableColumn[];
}
interface SeriesData {
name: string;
data: number[];
type?: string;
}
interface PieDataItem {
name: string;
value: number;
}
3. 实时数据更新接口
typescript
// WebSocket 消息格式
interface WSMessage {
type: 'chart_update' | 'ai_response' | 'error';
sessionId: string;
data: any;
}
// 图表更新消息
interface ChartUpdateMessage extends WSMessage {
type: 'chart_update';
data: {
chartId: string;
updateType: 'append' | 'replace' | 'modify';
newData: Partial<ChartData>;
};
}
📊 图表组件设计
图表类型支持
1. 折线图组件
vue
<template>
<BaseChart
type="line"
:data="chartData"
:title="title"
:options="lineOptions"
@chart-click="handleLineClick"
/>
</template>
<script setup>
const lineOptions = {
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2
},
areaStyle: {
opacity: 0.1
}
}
// 处理折线图点击
function handleLineClick(params) {
emit('drill-down', {
category: params.name,
series: params.seriesName,
value: params.value
})
}
</script>
2. 多图表布局组件
vue
<template>
<div class="multi-chart-container">
<div
v-for="chart in charts"
:key="chart.id"
:class="getChartClass(chart)"
class="chart-item"
>
<component
:is="getChartComponent(chart.type)"
v-bind="chart.props"
@chart-interaction="handleChartInteraction"
/>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
charts: Array,
layout: {
type: String,
default: 'grid', // grid, row, column
validator: (value) => ['grid', 'row', 'column'].includes(value)
}
})
// 计算图表样式类
const getChartClass = (chart) => {
const baseClass = 'chart-item'
const layoutClass = `layout-${props.layout}`
const sizeClass = `size-${chart.size || 'medium'}`
return [baseClass, layoutClass, sizeClass]
}
// 获取图表组件
const getChartComponent = (type) => {
const components = {
line: 'LineChart',
bar: 'BarChart',
pie: 'PieChart',
table: 'DataTable'
}
return components[type] || 'BaseChart'
}
</script>
<style scoped>
.multi-chart-container {
display: grid;
gap: 20px;
padding: 20px;
}
.layout-grid {
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
}
.layout-row .chart-item {
display: inline-block;
width: calc(50% - 10px);
margin-right: 20px;
}
.layout-column .chart-item {
width: 100%;
margin-bottom: 20px;
}
.size-small { min-height: 200px; }
.size-medium { min-height: 300px; }
.size-large { min-height: 400px; }
</style>
🎤 语音交互设计
语音处理流程
1. 语音输入管理
typescript
// 语音输入管理器
class VoiceInputManager {
private recognition: SpeechRecognition | null = null
private audioContext: AudioContext | null = null
private vadProcessor: VADProcessor | null = null
constructor() {
this.initSpeechRecognition()
this.initVAD()
}
// 初始化语音识别
private initSpeechRecognition() {
if ('webkitSpeechRecognition' in window) {
this.recognition = new webkitSpeechRecognition()
this.recognition.continuous = true
this.recognition.interimResults = true
this.recognition.lang = 'zh-CN'
}
}
// 初始化VAD (Voice Activity Detection)
private async initVAD() {
try {
const { VAD } = await import('@ricky0123/vad-web')
this.vadProcessor = new VAD({
onSpeechStart: () => this.handleSpeechStart(),
onSpeechEnd: () => this.handleSpeechEnd(),
onVADMisfire: () => this.handleVADMisfire()
})
} catch (error) {
console.warn('VAD initialization failed:', error)
}
}
// 开始录音
async startRecording(): Promise<void> {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
if (this.vadProcessor) {
this.vadProcessor.start(stream)
}
if (this.recognition) {
this.recognition.start()
}
this.setupAudioVisualization(stream)
} catch (error) {
throw new Error(`录音启动失败: ${error.message}`)
}
}
// 停止录音
stopRecording(): void {
if (this.vadProcessor) {
this.vadProcessor.pause()
}
if (this.recognition) {
this.recognition.stop()
}
}
// 设置音频可视化
private setupAudioVisualization(stream: MediaStream) {
this.audioContext = new AudioContext()
const analyser = this.audioContext.createAnalyser()
const source = this.audioContext.createMediaStreamSource(stream)
source.connect(analyser)
analyser.fftSize = 256
this.drawWaveform(analyser)
}
// 绘制波形
private drawWaveform(analyser: AnalyserNode) {
const canvas = document.querySelector('.voice-wave canvas') as HTMLCanvasElement
if (!canvas) return
const ctx = canvas.getContext('2d')!
const bufferLength = analyser.frequencyBinCount
const dataArray = new Uint8Array(bufferLength)
const draw = () => {
requestAnimationFrame(draw)
analyser.getByteFrequencyData(dataArray)
ctx.fillStyle = 'rgb(240, 240, 240)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
const barWidth = (canvas.width / bufferLength) * 2.5
let barHeight
let x = 0
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2
ctx.fillStyle = `rgb(50, ${barHeight + 100}, 50)`
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight)
x += barWidth + 1
}
}
draw()
}
}
2. 语音命令解析
typescript
// 语音命令解析器
class VoiceCommandParser {
private commandPatterns = {
chart: {
line: /折线图|线图|趋势图/,
bar: /柱状图|柱图|条形图/,
pie: /饼图|圆饼图|扇形图/,
table: /表格|数据表|列表/
},
action: {
create: /创建|生成|制作|画一个/,
update: /更新|刷新|修改/,
export: /导出|下载|保存/,
clear: /清除|删除|移除/
},
data: {
sales: /销售|营收|收入/,
user: /用户|客户|人员/,
time: /时间|日期|月份|年份/
}
}
// 解析语音命令
parseCommand(text: string): VoiceCommand {
const command: VoiceCommand = {
action: this.extractAction(text),
chartType: this.extractChartType(text),
dataType: this.extractDataType(text),
parameters: this.extractParameters(text),
confidence: this.calculateConfidence(text)
}
return command
}
// 提取操作类型
private extractAction(text: string): string {
for (const [action, pattern] of Object.entries(this.commandPatterns.action)) {
if (pattern.test(text)) {
return action
}
}
return 'create' // 默认为创建
}
// 提取图表类型
private extractChartType(text: string): string {
for (const [type, pattern] of Object.entries(this.commandPatterns.chart)) {
if (pattern.test(text)) {
return type
}
}
return 'line' // 默认为折线图
}
// 提取数据类型
private extractDataType(text: string): string {
for (const [type, pattern] of Object.entries(this.commandPatterns.data)) {
if (pattern.test(text)) {
return type
}
}
return 'general'
}
// 提取参数
private extractParameters(text: string): Record<string, any> {
const params: Record<string, any> = {}
// 提取时间范围
const timeMatch = text.match(/(\d+)(天|周|月|年)/)
if (timeMatch) {
params.timeRange = {
value: parseInt(timeMatch[1]),
unit: timeMatch[2]
}
}
// 提取数量
const countMatch = text.match(/(\d+)个/)
if (countMatch) {
params.count = parseInt(countMatch[1])
}
return params
}
// 计算置信度
private calculateConfidence(text: string): number {
let score = 0
const totalPatterns = Object.values(this.commandPatterns).reduce(
(sum, category) => sum + Object.keys(category).length, 0
)
// 检查匹配的模式数量
for (const category of Object.values(this.commandPatterns)) {
for (const pattern of Object.values(category)) {
if (pattern.test(text)) {
score += 1
}
}
}
return Math.min(score / totalPatterns * 100, 100)
}
}
interface VoiceCommand {
action: string
chartType: string
dataType: string
parameters: Record<string, any>
confidence: number
}
🚀 实现指南
项目初始化
1. 安装依赖
bash
# 核心依赖
npm install vue@next vue-router@4 vuex@4
npm install element-plus @element-plus/icons-vue
npm install echarts vue-echarts
npm install axios
# 语音处理
npm install @ricky0123/vad-web
npm install wavesurfer.js
# Live2D支持
npm install pixi.js pixi-live2d-display
# 开发依赖
npm install -D @vitejs/plugin-vue
npm install -D typescript @vue/tsconfig
npm install -D sass
2. 项目配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
host: '0.0.0.0',
port: 3000,
https: true // HTTPS required for microphone access
},
build: {
rollupOptions: {
external: ['echarts/core']
}
}
})
3. 主应用入口
typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// ECharts
import ECharts from 'vue-echarts'
import { use } from 'echarts/core'
import {
CanvasRenderer,
SVGRenderer
} from 'echarts/renderers'
import {
LineChart,
BarChart,
PieChart
} from 'echarts/charts'
import {
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
} from 'echarts/components'
// 注册ECharts组件
use([
CanvasRenderer,
SVGRenderer,
LineChart,
BarChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
])
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(store)
app.use(router)
app.use(ElementPlus)
app.component('VChart', ECharts)
app.mount('#app')
核心功能实现
1. AI助手状态管理
typescript
// store/modules/aiAssistant.ts
import { Module } from 'vuex'
interface AIAssistantState {
isListening: boolean
isProcessing: boolean
currentEmotion: string
chatHistory: ChatMessage[]
charts: ChartInstance[]
sessionId: string
}
interface ChatMessage {
id: string
type: 'user' | 'ai'
content: string
timestamp: number
metadata?: any
}
interface ChartInstance {
id: string
type: string
title: string
data: any
position: { x: number; y: number }
size: { width: number; height: number }
}
const aiAssistantModule: Module<AIAssistantState, any> = {
namespaced: true,
state: {
isListening: false,
isProcessing: false,
currentEmotion: 'neutral',
chatHistory: [],
charts: [],
sessionId: ''
},
mutations: {
SET_LISTENING(state, isListening: boolean) {
state.isListening = isListening
},
SET_PROCESSING(state, isProcessing: boolean) {
state.isProcessing = isProcessing
},
SET_EMOTION(state, emotion: string) {
state.currentEmotion = emotion
},
ADD_MESSAGE(state, message: ChatMessage) {
state.chatHistory.push(message)
},
ADD_CHART(state, chart: ChartInstance) {
state.charts.push(chart)
},
UPDATE_CHART(state, { chartId, data }: { chartId: string; data: any }) {
const chart = state.charts.find(c => c.id === chartId)
if (chart) {
chart.data = { ...chart.data, ...data }
}
},
REMOVE_CHART(state, chartId: string) {
const index = state.charts.findIndex(c => c.id === chartId)
if (index > -1) {
state.charts.splice(index, 1)
}
}
},
actions: {
async processUserInput({ commit, state }, { input, type }: { input: string; type: 'voice' | 'text' }) {
commit('SET_PROCESSING', true)
try {
// 添加用户消息
const userMessage: ChatMessage = {
id: Date.now().toString(),
type: 'user',
content: input,
timestamp: Date.now()
}
commit('ADD_MESSAGE', userMessage)
// 发送到后端处理
const response = await fetch('/api/ai/process', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
input,
inputType: type,
sessionId: state.sessionId
})
})
const result = await response.json()
if (result.success) {
// 添加AI回复
const aiMessage: ChatMessage = {
id: Date.now().toString(),
type: 'ai',
content: result.data.response,
timestamp: Date.now(),
metadata: result.data
}
commit('ADD_MESSAGE', aiMessage)
// 如果需要生成图表
if (result.data.chartConfig) {
await this.dispatch('aiAssistant/generateChart', result.data.chartConfig)
}
}
} catch (error) {
console.error('处理用户输入失败:', error)
} finally {
commit('SET_PROCESSING', false)
}
},
async generateChart({ commit }, chartConfig: any) {
try {
const response = await fetch('/api/charts/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(chartConfig)
})
const result = await response.json()
if (result.success) {
const chart: ChartInstance = {
id: result.data.chartId,
type: result.data.chartType,
title: result.data.title,
data: result.data.data,
position: { x: 0, y: 0 },
size: { width: 400, height: 300 }
}
commit('ADD_CHART', chart)
}
} catch (error) {
console.error('生成图表失败:', error)
}
}
}
}
export default aiAssistantModule
2. 实时数据更新
typescript
// composables/useWebSocket.ts
import { ref, onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex'
export function useWebSocket(url: string) {
const store = useStore()
const isConnected = ref(false)
const error = ref<string | null>(null)
let ws: WebSocket | null = null
let reconnectTimer: number | null = null
let reconnectAttempts = 0
const maxReconnectAttempts = 5
const connect = () => {
try {
ws = new WebSocket(url)
ws.onopen = () => {
isConnected.value = true
error.value = null
reconnectAttempts = 0
console.log('WebSocket连接已建立')
}
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data)
handleMessage(message)
} catch (err) {
console.error('解析WebSocket消息失败:', err)
}
}
ws.onclose = () => {
isConnected.value = false
console.log('WebSocket连接已关闭')
// 自动重连
if (reconnectAttempts < maxReconnectAttempts) {
reconnectTimer = window.setTimeout(() => {
reconnectAttempts++
console.log(`尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})`)
connect()
}, 3000 * reconnectAttempts)
}
}
ws.onerror = (err) => {
error.value = 'WebSocket连接错误'
console.error('WebSocket错误:', err)
}
} catch (err) {
error.value = '无法建立WebSocket连接'
console.error('WebSocket连接失败:', err)
}
}
const handleMessage = (message: any) => {
switch (message.type) {
case 'chart_update':
store.commit('aiAssistant/UPDATE_CHART', {
chartId: message.data.chartId,
data: message.data.newData
})
break
case 'ai_response':
store.commit('aiAssistant/ADD_MESSAGE', {
id: Date.now().toString(),
type: 'ai',
content: message.data.content,
timestamp: Date.now()
})
break
case 'emotion_change':
store.commit('aiAssistant/SET_EMOTION', message.data.emotion)
break
default:
console.log('未知消息类型:', message.type)
}
}
const send = (data: any) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data))
} else {
console.warn('WebSocket未连接,无法发送消息')
}
}
const disconnect = () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (ws) {
ws.close()
ws = null
}
}
onMounted(() => {
connect()
})
onUnmounted(() => {
disconnect()
})
return {
isConnected,
error,
send,
disconnect,
reconnect: connect
}
}
⚡ 性能优化
1. 图表渲染优化
typescript
// 图表渲染优化策略
class ChartOptimizer {
private static readonly LARGE_DATA_THRESHOLD = 1000
private static readonly UPDATE_THROTTLE_MS = 100
// 数据采样
static sampleData(data: number[], maxPoints: number = 500): number[] {
if (data.length <= maxPoints) return data
const step = Math.ceil(data.length / maxPoints)
return data.filter((_, index) => index % step === 0)
}
// 渐进式渲染
static async progressiveRender(chart: any, data: any[], batchSize: number = 100) {
const batches = Math.ceil(data.length / batchSize)
for (let i = 0; i < batches; i++) {
const start = i * batchSize
const end = Math.min(start + batchSize, data.length)
const batch = data.slice(start, end)
chart.appendData({
seriesIndex: 0,
data: batch
})
// 让出控制权,避免阻塞UI
await new Promise(resolve => setTimeout(resolve, 0))
}
}
// 虚拟滚动
static createVirtualScroll(container: HTMLElement, itemHeight: number) {
return {
visibleStart: 0,
visibleEnd: 0,
updateVisibleRange(scrollTop: number, containerHeight: number, totalItems: number) {
this.visibleStart = Math.floor(scrollTop / itemHeight)
this.visibleEnd = Math.min(
this.visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
totalItems
)
},
getVisibleItems<T>(items: T[]): T[] {
return items.slice(this.visibleStart, this.visibleEnd)
}
}
}
}
2. 内存管理
typescript
// 内存管理工具
class MemoryManager {
private static chartInstances = new Map<string, any>()
private static maxCharts = 10
// 注册图表实例
static registerChart(id: string, instance: any) {
// 如果超过最大数量,移除最旧的图表
if (this.chartInstances.size >= this.maxCharts) {
const oldestId = this.chartInstances.keys().next().value
this.disposeChart(oldestId)
}
this.chartInstances.set(id, instance)
}
// 销毁图表实例
static disposeChart(id: string) {
const instance = this.chartInstances.get(id)
if (instance) {
instance.dispose()
this.chartInstances.delete(id)
}
}
// 清理所有图表
static disposeAllCharts() {
for (const [id, instance] of this.chartInstances) {
instance.dispose()
}
this.chartInstances.clear()
}
// 监控内存使用
static monitorMemory() {
if ('memory' in performance) {
const memory = (performance as any).memory
console.log('内存使用情况:', {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
})
}
}
}
🚀 部署方案
1. Docker部署
dockerfile
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
}
2. 环境配置
yaml
# docker-compose.yml
version: '3.8'
services:
frontend:
build: .
ports:
- "80:80"
depends_on:
- backend
environment:
- NODE_ENV=production
backend:
image: ai-chart-backend:latest
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/aiChart
- REDIS_URL=redis://redis:6379
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
- db
- redis
db:
image: postgres:15
environment:
- POSTGRES_DB=aiChart
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
📝 总结
本文档详细设计了AI数字人可视化图表系统的前端架构和实现方案。主要特点包括:
🎯 核心优势
- 智能交互:支持语音和文字多模态输入
- 实时响应:WebSocket实时数据更新
- 丰富图表:支持多种图表类型和布局
- 性能优化:渐进式渲染和内存管理
- 可扩展性:模块化设计,易于扩展
🔧 技术亮点
- Vue 3 + TypeScript 现代化开发
- ECharts 专业图表渲染
- Web Speech API 语音识别
- Live2D 数字人交互
- WebSocket 实时通信
📈 应用场景
- 商业智能分析
- 数据可视化展示
- 智能客服系统
- 教育培训平台
- 企业数字化转型
这套方案为构建现代化的AI数字人可视化图表系统提供了完整的技术指导,可以根据具体需求进行定制和扩展。