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

相关推荐
我是天龙_绍2 小时前
仿一下微信的截图标注功能
前端
_AaronWong2 小时前
前端工程化:基于Node.js的自动化版本管理与发布说明生成工具
前端·javascript·node.js
Healer9183 小时前
纯css实现高度0-auto动画过度interpolate-size 和 height: calc-size(auto,size)
前端
智慧源点3 小时前
解决 Vite + React 项目部署 GitHub Pages 的完整指南:从 404 到成功部署
前端·react.js·github
葡萄城技术团队3 小时前
浏览器端音视频处理新选择:Mediabunny 让 Web 媒体开发飞起来
前端·音视频·媒体
FogLetter3 小时前
深入浅出 JavaScript 闭包:从背包理论到实战应用
前端·javascript
前端大卫3 小时前
表单与上传组件校验
前端·javascript·vue.js
伊织code3 小时前
Cap‘n Web - JavaScript原生RPC系统
前端·javascript·rpc
周尛先森4 小时前
匠心管控 package.json:让前端依赖告别臃肿与混乱
前端