AI数字人可视化图表设计文档

AI数字人可视化图表设计文档

📋 目录

  1. 项目概述
  2. 技术架构
  3. 前端设计方案
  4. API接口设计
  5. 图表组件设计
  6. 语音交互设计
  7. 实现指南
  8. 性能优化
  9. 部署方案

🎯 项目概述

项目背景

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数字人可视化图表系统提供了完整的技术指导,可以根据具体需求进行定制和扩展。

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax