Skip to content

Vue集成示例

本页面展示了如何在 Vue 应用中集成和使用 XAI-SDK,包括基础集成、Composables 使用、组件开发和最佳实践。

📋 示例列表

基础 Vue 应用

简单的 Vue AI 助手应用

开发工具面板

Vue 开发者工具扩展

智能代码生成器

AI 驱动的代码生成工具

自定义 Composables

可复用的 XAI-SDK Vue Composables

🚀 快速开始

安装依赖

bash
npm install xai-sdk @xai-sdk/vue-adapter vue@next
# 或者
yarn add xai-sdk @xai-sdk/vue-adapter vue@next

项目结构

src/
├── components/
│   ├── ChatInterface.vue
│   ├── CodeEditor.vue
│   └── AIAssistant.vue
├── composables/
│   ├── useXAI.ts
│   ├── useChat.ts
│   └── useCodeAnalysis.ts
├── plugins/
│   └── xai.ts
├── utils/
│   └── xai-config.ts
├── App.vue
└── main.ts

📚 基础示例

XAI 插件配置

首先创建一个 Vue 插件来管理 XAI-SDK:

typescript
// src/plugins/xai.ts
import { App } from 'vue';
import { XAI_SDK } from 'xai-sdk';
import { VueAdapterPlugin } from '@xai-sdk/vue-adapter';
import { OpenAIProviderPlugin } from '@xai-sdk/openai-provider';

const XAI_SDK_KEY = Symbol('XAI_SDK');

interface XAIPluginOptions {
  apiKey: string;
}

export const createXAIPlugin = (options: XAIPluginOptions) => {
  return {
    install(app: App) {
      const sdk = new XAI_SDK({ environment: 'browser' });
      
      // 初始化 SDK
      const initSDK = async () => {
        try {
          await sdk.registerPlugin(new VueAdapterPlugin());
          await sdk.registerPlugin(new OpenAIProviderPlugin({
            apiKey: options.apiKey
          }));
          await sdk.initialize();
          
          console.log('XAI-SDK 初始化完成');
        } catch (error) {
          console.error('XAI-SDK 初始化失败:', error);
        }
      };
      
      initSDK();
      
      // 提供 SDK 实例
      app.provide(XAI_SDK_KEY, sdk);
      app.config.globalProperties.$xai = sdk;
    }
  };
};

export { XAI_SDK_KEY };

基础 Composable

typescript
// src/composables/useXAI.ts
import { inject, ref, onMounted } from 'vue';
import { XAI_SDK } from 'xai-sdk';
import { XAI_SDK_KEY } from '../plugins/xai';

export const useXAI = () => {
  const sdk = inject<XAI_SDK>(XAI_SDK_KEY);
  const isInitialized = ref(false);
  const error = ref<string | null>(null);

  if (!sdk) {
    throw new Error('XAI SDK not found. Make sure to install the XAI plugin.');
  }

  onMounted(async () => {
    try {
      // 等待 SDK 初始化完成
      await new Promise(resolve => {
        const checkInit = () => {
          if (sdk.isInitialized) {
            resolve(true);
          } else {
            setTimeout(checkInit, 100);
          }
        };
        checkInit();
      });
      
      isInitialized.value = true;
    } catch (err) {
      error.value = err instanceof Error ? err.message : '初始化失败';
    }
  });

  return {
    sdk,
    isInitialized,
    error
  };
};

聊天 Composable

typescript
// src/composables/useChat.ts
import { ref, computed } from 'vue';
import { useXAI } from './useXAI';

interface ChatMessage {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp: Date;
}

interface UseChatOptions {
  maxTokens?: number;
  temperature?: number;
  onError?: (error: Error) => void;
}

export const useChat = (options: UseChatOptions = {}) => {
  const { sdk, isInitialized } = useXAI();
  const messages = ref<ChatMessage[]>([]);
  const isLoading = ref(false);
  const error = ref<string | null>(null);

  const canSendMessage = computed(() => {
    return isInitialized.value && !isLoading.value;
  });

  const addMessage = (role: ChatMessage['role'], content: string): string => {
    const id = Date.now().toString();
    const message: ChatMessage = {
      id,
      role,
      content,
      timestamp: new Date()
    };
    messages.value.push(message);
    return id;
  };

  const updateMessage = (id: string, content: string) => {
    const messageIndex = messages.value.findIndex(msg => msg.id === id);
    if (messageIndex !== -1) {
      messages.value[messageIndex].content = content;
    }
  };

  const sendMessage = async (content: string) => {
    if (!canSendMessage.value || !content.trim()) return;

    error.value = null;
    addMessage('user', content.trim());
    isLoading.value = true;

    const loadingId = addMessage('assistant', '正在思考...');

    try {
      const response = await sdk.chat(content, {
        maxTokens: options.maxTokens || 1000,
        temperature: options.temperature || 0.7,
        stream: false
      });

      updateMessage(loadingId, response.content);
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : '处理请求时出现错误';
      updateMessage(loadingId, `抱歉,${errorMessage}`);
      error.value = errorMessage;
      options.onError?.(err instanceof Error ? err : new Error(errorMessage));
    } finally {
      isLoading.value = false;
    }
  };

  const clearMessages = () => {
    messages.value = [];
    error.value = null;
  };

  return {
    messages,
    isLoading,
    error,
    canSendMessage,
    sendMessage,
    clearMessages
  };
};

聊天界面组件

vue
<!-- src/components/ChatInterface.vue -->
<template>
  <div class="chat-interface">
    <div class="chat-header">
      <h2>AI 助手</h2>
      <div class="status-indicator" :class="{ online: isInitialized }"></div>
    </div>
    
    <div class="chat-messages" ref="messagesContainer">
      <div 
        v-for="message in messages" 
        :key="message.id" 
        :class="['message', message.role]"
      >
        <div class="message-header">
          <span class="role">{{ getRoleName(message.role) }}</span>
          <span class="timestamp">{{ formatTime(message.timestamp) }}</span>
        </div>
        <div class="message-content">
          {{ message.content }}
        </div>
      </div>
    </div>
    
    <div class="chat-input">
      <textarea
        v-model="inputMessage"
        @keydown="handleKeyDown"
        :disabled="!canSendMessage"
        placeholder="输入您的问题... (Shift+Enter 换行)"
        rows="3"
      ></textarea>
      <button 
        @click="handleSendMessage"
        :disabled="!inputMessage.trim() || !canSendMessage"
        class="send-button"
      >
        {{ isLoading ? '发送中...' : '发送' }}
      </button>
    </div>
    
    <div v-if="error" class="error-message">
      错误: {{ error }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, watch, onMounted } from 'vue';
import { useXAI } from '../composables/useXAI';
import { useChat } from '../composables/useChat';

const { isInitialized } = useXAI();
const { messages, isLoading, error, canSendMessage, sendMessage, clearMessages } = useChat({
  onError: (err) => {
    console.error('聊天错误:', err);
  }
});

const inputMessage = ref('');
const messagesContainer = ref<HTMLElement>();

const getRoleName = (role: string): string => {
  const names: Record<string, string> = {
    'user': '用户',
    'assistant': 'AI助手',
    'system': '系统'
  };
  return names[role] || role;
};

const formatTime = (date: Date): string => {
  return date.toLocaleTimeString();
};

const scrollToBottom = async () => {
  await nextTick();
  if (messagesContainer.value) {
    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
  }
};

const handleSendMessage = async () => {
  if (!inputMessage.value.trim()) return;
  
  const message = inputMessage.value;
  inputMessage.value = '';
  
  await sendMessage(message);
  scrollToBottom();
};

const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();
    handleSendMessage();
  }
};

// 监听消息变化,自动滚动到底部
watch(messages, scrollToBottom, { deep: true });

// 初始化时添加欢迎消息
watch(isInitialized, (initialized) => {
  if (initialized) {
    // 添加系统消息
    messages.value.push({
      id: 'welcome',
      role: 'system',
      content: 'AI 助手已就绪,请输入您的问题。',
      timestamp: new Date()
    });
    scrollToBottom();
  }
});
</script>

<style scoped>
.chat-interface {
  display: flex;
  flex-direction: column;
  height: 600px;
  border: 1px solid #e1e5e9;
  border-radius: 8px;
  overflow: hidden;
  background: white;
}

.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  background: #f8f9fa;
  border-bottom: 1px solid #e1e5e9;
}

.chat-header h2 {
  margin: 0;
  font-size: 18px;
  color: #24292e;
}

.status-indicator {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #dc3545;
  transition: background-color 0.3s;
}

.status-indicator.online {
  background: #28a745;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.message {
  max-width: 80%;
  padding: 12px;
  border-radius: 8px;
  word-wrap: break-word;
}

.message.user {
  align-self: flex-end;
  background: #007bff;
  color: white;
}

.message.assistant {
  align-self: flex-start;
  background: #f1f3f4;
  border: 1px solid #d0d7de;
}

.message.system {
  align-self: center;
  background: #fff3cd;
  border: 1px solid #ffeaa7;
  color: #856404;
  font-style: italic;
}

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 4px;
  font-size: 12px;
  opacity: 0.7;
}

.message-content {
  line-height: 1.4;
  white-space: pre-wrap;
}

.chat-input {
  display: flex;
  padding: 16px;
  border-top: 1px solid #e1e5e9;
  background: #f8f9fa;
  gap: 12px;
}

.chat-input textarea {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  resize: none;
  font-family: inherit;
  font-size: 14px;
}

.chat-input textarea:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}

.send-button {
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s;
}

.send-button:hover:not(:disabled) {
  background: #0056b3;
}

.send-button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.error-message {
  padding: 8px 16px;
  background: #f8d7da;
  color: #721c24;
  border-top: 1px solid #f5c6cb;
  font-size: 14px;
}
</style>

🔧 高级示例

代码分析 Composable

typescript
// src/composables/useCodeAnalysis.ts
import { ref, computed } from 'vue';
import { useXAI } from './useXAI';

interface AnalysisResult {
  score: number;
  suggestions: string[];
  metrics: {
    lines: number;
    complexity: number;
    maintainability: number;
  };
  issues: Array<{
    type: 'error' | 'warning' | 'info';
    message: string;
    line?: number;
  }>;
}

export const useCodeAnalysis = () => {
  const { sdk, isInitialized } = useXAI();
  const isAnalyzing = ref(false);
  const result = ref<AnalysisResult | null>(null);
  const error = ref<string | null>(null);

  const canAnalyze = computed(() => {
    return isInitialized.value && !isAnalyzing.value;
  });

  const analyzeCode = async (code: string, language: string = 'javascript') => {
    if (!canAnalyze.value || !code.trim()) return;

    isAnalyzing.value = true;
    error.value = null;
    result.value = null;

    try {
      const analysis = await sdk.analyze(code, {
        type: 'code-quality',
        language,
        includeMetrics: true,
        includeSuggestions: true
      });

      result.value = analysis as AnalysisResult;
    } catch (err) {
      error.value = err instanceof Error ? err.message : '分析失败';
    } finally {
      isAnalyzing.value = false;
    }
  };

  const clearResult = () => {
    result.value = null;
    error.value = null;
  };

  return {
    analyzeCode,
    isAnalyzing,
    result,
    error,
    canAnalyze,
    clearResult
  };
};

代码编辑器组件

vue
<!-- src/components/CodeEditor.vue -->
<template>
  <div class="code-editor">
    <div class="editor-toolbar">
      <div class="toolbar-left">
        <select v-model="selectedLanguage" class="language-select">
          <option value="javascript">JavaScript</option>
          <option value="typescript">TypeScript</option>
          <option value="python">Python</option>
          <option value="java">Java</option>
        </select>
        <span class="lines-count">{{ lineCount }} 行</span>
      </div>
      <div class="toolbar-right">
        <button 
          @click="handleAnalyze"
          :disabled="!canAnalyze || !code.trim()"
          class="analyze-button"
        >
          {{ isAnalyzing ? '分析中...' : '🔍 分析代码' }}
        </button>
        <button 
          @click="generateCode"
          :disabled="!canAnalyze"
          class="generate-button"
        >
          ✨ 生成代码
        </button>
        <button 
          @click="showAnalysis = !showAnalysis"
          :disabled="!result"
          class="toggle-analysis"
        >
          {{ showAnalysis ? '隐藏分析' : '显示分析' }}
        </button>
      </div>
    </div>
    
    <div class="editor-container">
      <div class="editor-main">
        <textarea
          v-model="code"
          @keydown="handleKeyDown"
          class="code-textarea"
          placeholder="在这里输入代码..."
          spellcheck="false"
        ></textarea>
      </div>
      
      <div v-if="showAnalysis && (result || error)" class="analysis-panel">
        <div class="analysis-header">
          <h3>分析结果</h3>
          <button @click="showAnalysis = false" class="close-button">×</button>
        </div>
        
        <div v-if="error" class="analysis-error">
          <p>分析失败: {{ error }}</p>
        </div>
        
        <div v-else-if="result" class="analysis-content">
          <div class="metrics">
            <div class="metric">
              <span class="metric-label">质量评分</span>
              <span :class="['metric-value', `score-${getScoreClass(result.score)}`]">
                {{ result.score }}/100
              </span>
            </div>
            <div class="metric">
              <span class="metric-label">代码行数</span>
              <span class="metric-value">{{ result.metrics.lines }}</span>
            </div>
            <div class="metric">
              <span class="metric-label">复杂度</span>
              <span class="metric-value">{{ result.metrics.complexity }}</span>
            </div>
          </div>
          
          <div v-if="result.suggestions.length > 0" class="suggestions">
            <h4>改进建议</h4>
            <ul>
              <li v-for="(suggestion, index) in result.suggestions" :key="index">
                {{ suggestion }}
              </li>
            </ul>
          </div>
          
          <div v-if="result.issues.length > 0" class="issues">
            <h4>发现的问题</h4>
            <ul>
              <li 
                v-for="(issue, index) in result.issues" 
                :key="index" 
                :class="`issue-${issue.type}`"
              >
                <span class="issue-type">{{ issue.type.toUpperCase() }}</span>
                <span class="issue-message">{{ issue.message }}</span>
                <span v-if="issue.line" class="issue-line">第 {{ issue.line }} 行</span>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 代码生成对话框 -->
    <div v-if="showGenerateDialog" class="generate-dialog-overlay" @click="closeGenerateDialog">
      <div class="generate-dialog" @click.stop>
        <div class="dialog-header">
          <h3>AI 代码生成</h3>
          <button @click="closeGenerateDialog" class="close-button">×</button>
        </div>
        <div class="dialog-content">
          <textarea
            v-model="generatePrompt"
            placeholder="描述您想要生成的代码功能..."
            rows="4"
            class="prompt-input"
          ></textarea>
          <div class="dialog-actions">
            <button @click="closeGenerateDialog" class="cancel-button">取消</button>
            <button 
              @click="handleGenerateCode"
              :disabled="!generatePrompt.trim() || isGenerating"
              class="confirm-button"
            >
              {{ isGenerating ? '生成中...' : '生成代码' }}
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useCodeAnalysis } from '../composables/useCodeAnalysis';
import { useXAI } from '../composables/useXAI';

interface Props {
  initialCode?: string;
  language?: string;
}

interface Emits {
  (e: 'update:code', code: string): void;
}

const props = withDefaults(defineProps<Props>(), {
  initialCode: '',
  language: 'javascript'
});

const emit = defineEmits<Emits>();

const { sdk } = useXAI();
const { analyzeCode, isAnalyzing, result, error, canAnalyze, clearResult } = useCodeAnalysis();

const code = ref(props.initialCode);
const selectedLanguage = ref(props.language);
const showAnalysis = ref(false);
const showGenerateDialog = ref(false);
const generatePrompt = ref('');
const isGenerating = ref(false);

const lineCount = computed(() => {
  return code.value.split('\n').length;
});

const getScoreClass = (score: number): string => {
  if (score >= 80) return 'good';
  if (score >= 60) return 'medium';
  return 'poor';
};

const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === 'Tab') {
    event.preventDefault();
    const textarea = event.target as HTMLTextAreaElement;
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const newCode = code.value.substring(0, start) + '  ' + code.value.substring(end);
    code.value = newCode;
    
    // 设置光标位置
    setTimeout(() => {
      textarea.selectionStart = textarea.selectionEnd = start + 2;
    }, 0);
  }
};

const handleAnalyze = async () => {
  await analyzeCode(code.value, selectedLanguage.value);
  if (result.value) {
    showAnalysis.value = true;
  }
};

const generateCode = () => {
  showGenerateDialog.value = true;
  generatePrompt.value = '';
};

const closeGenerateDialog = () => {
  showGenerateDialog.value = false;
  generatePrompt.value = '';
};

const handleGenerateCode = async () => {
  if (!generatePrompt.value.trim()) return;
  
  isGenerating.value = true;
  
  try {
    const response = await sdk.generate(generatePrompt.value, {
      language: selectedLanguage.value,
      style: 'clean',
      includeComments: true
    });
    
    code.value = response.code;
    closeGenerateDialog();
  } catch (err) {
    console.error('代码生成失败:', err);
    alert('代码生成失败: ' + (err instanceof Error ? err.message : '未知错误'));
  } finally {
    isGenerating.value = false;
  }
};

// 监听代码变化
watch(code, (newCode) => {
  emit('update:code', newCode);
  // 清除之前的分析结果
  if (result.value) {
    clearResult();
    showAnalysis.value = false;
  }
});
</script>

<style scoped>
.code-editor {
  display: flex;
  flex-direction: column;
  height: 600px;
  border: 1px solid #e1e5e9;
  border-radius: 8px;
  overflow: hidden;
  background: white;
}

.editor-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  background: #f8f9fa;
  border-bottom: 1px solid #e1e5e9;
}

.toolbar-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.language-select {
  padding: 4px 8px;
  border: 1px solid #d0d7de;
  border-radius: 4px;
  font-size: 12px;
}

.lines-count {
  font-size: 12px;
  color: #656d76;
}

.toolbar-right {
  display: flex;
  gap: 8px;
}

.toolbar-right button {
  padding: 6px 12px;
  border: 1px solid #d0d7de;
  border-radius: 4px;
  background: white;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
}

.toolbar-right button:hover:not(:disabled) {
  background: #f3f4f6;
}

.toolbar-right button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.analyze-button {
  background: #0366d6 !important;
  color: white !important;
  border-color: #0366d6 !important;
}

.generate-button {
  background: #28a745 !important;
  color: white !important;
  border-color: #28a745 !important;
}

.editor-container {
  display: flex;
  flex: 1;
  overflow: hidden;
}

.editor-main {
  flex: 1;
  display: flex;
}

.code-textarea {
  flex: 1;
  padding: 16px;
  border: none;
  outline: none;
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  font-size: 14px;
  line-height: 1.5;
  resize: none;
  background: #fafbfc;
}

.analysis-panel {
  width: 300px;
  border-left: 1px solid #e1e5e9;
  background: white;
  display: flex;
  flex-direction: column;
}

.analysis-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid #e1e5e9;
  background: #f8f9fa;
}

.analysis-header h3 {
  margin: 0;
  font-size: 14px;
}

.close-button {
  background: none;
  border: none;
  font-size: 18px;
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.analysis-content {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.metrics {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}

.metric {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px;
  background: #f8f9fa;
  border-radius: 4px;
}

.metric-label {
  font-size: 12px;
  color: #656d76;
}

.metric-value {
  font-weight: bold;
  font-size: 14px;
}

.score-good { color: #28a745; }
.score-medium { color: #ffc107; }
.score-poor { color: #dc3545; }

.suggestions, .issues {
  margin-bottom: 16px;
}

.suggestions h4, .issues h4 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #24292e;
}

.suggestions ul, .issues ul {
  margin: 0;
  padding-left: 16px;
  font-size: 12px;
}

.suggestions li, .issues li {
  margin-bottom: 4px;
  line-height: 1.4;
}

.issue-error { color: #dc3545; }
.issue-warning { color: #ffc107; }
.issue-info { color: #17a2b8; }

.issue-type {
  font-weight: bold;
  margin-right: 4px;
}

.issue-line {
  font-style: italic;
  margin-left: 4px;
  opacity: 0.7;
}

.analysis-error {
  padding: 16px;
  color: #dc3545;
  text-align: center;
}

/* 生成对话框样式 */
.generate-dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.generate-dialog {
  background: white;
  border-radius: 8px;
  width: 500px;
  max-width: 90vw;
  max-height: 80vh;
  overflow: hidden;
}

.dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid #e1e5e9;
  background: #f8f9fa;
}

.dialog-header h3 {
  margin: 0;
  font-size: 16px;
}

.dialog-content {
  padding: 16px;
}

.prompt-input {
  width: 100%;
  padding: 12px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  font-family: inherit;
  font-size: 14px;
  resize: vertical;
  margin-bottom: 16px;
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.cancel-button, .confirm-button {
  padding: 8px 16px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.cancel-button {
  background: white;
  color: #24292e;
}

.confirm-button {
  background: #28a745;
  color: white;
  border-color: #28a745;
}

.confirm-button:disabled {
  background: #6c757d;
  border-color: #6c757d;
  cursor: not-allowed;
}
</style>

主应用组件

vue
<!-- src/App.vue -->
<template>
  <div id="app">
    <header class="app-header">
      <h1>XAI-SDK Vue 示例</h1>
      <nav class="app-nav">
        <button 
          v-for="tab in tabs" 
          :key="tab.key"
          :class="{ active: activeTab === tab.key }"
          @click="activeTab = tab.key"
        >
          {{ tab.icon }} {{ tab.label }}
        </button>
        <button @click="showSettings = true" class="settings-button">
          ⚙️ 设置
        </button>
      </nav>
    </header>
    
    <main class="app-main">
      <component :is="currentComponent" />
    </main>
    
    <!-- 设置对话框 -->
    <div v-if="showSettings" class="settings-overlay" @click="closeSettings">
      <div class="settings-dialog" @click.stop>
        <div class="dialog-header">
          <h3>设置</h3>
          <button @click="closeSettings" class="close-button">×</button>
        </div>
        <div class="dialog-content">
          <div class="setting-item">
            <label for="api-key">OpenAI API Key:</label>
            <input
              id="api-key"
              v-model="tempApiKey"
              type="password"
              placeholder="sk-..."
              class="api-key-input"
            />
          </div>
          <div class="dialog-actions">
            <button @click="closeSettings" class="cancel-button">取消</button>
            <button @click="saveSettings" class="confirm-button">保存</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import ChatInterface from './components/ChatInterface.vue';
import CodeEditor from './components/CodeEditor.vue';

const activeTab = ref<'chat' | 'editor'>('chat');
const showSettings = ref(false);
const apiKey = ref('');
const tempApiKey = ref('');

const tabs = [
  { key: 'chat', label: '聊天助手', icon: '💬' },
  { key: 'editor', label: '代码编辑器', icon: '📝' }
] as const;

const currentComponent = computed(() => {
  switch (activeTab.value) {
    case 'chat':
      return ChatInterface;
    case 'editor':
      return CodeEditor;
    default:
      return ChatInterface;
  }
});

const closeSettings = () => {
  showSettings.value = false;
  tempApiKey.value = apiKey.value;
};

const saveSettings = () => {
  apiKey.value = tempApiKey.value;
  localStorage.setItem('openai-api-key', apiKey.value);
  showSettings.value = false;
  
  // 重新加载页面以应用新的 API Key
  if (apiKey.value) {
    window.location.reload();
  }
};

onMounted(() => {
  // 从 localStorage 加载 API Key
  const savedApiKey = localStorage.getItem('openai-api-key');
  if (savedApiKey) {
    apiKey.value = savedApiKey;
    tempApiKey.value = savedApiKey;
  } else {
    showSettings.value = true;
  }
});
</script>

<style>
#app {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  margin: 0;
  padding: 0;
  min-height: 100vh;
  background: #f6f8fa;
}

.app-header {
  background: white;
  border-bottom: 1px solid #e1e5e9;
  padding: 16px 24px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.app-header h1 {
  margin: 0;
  font-size: 24px;
  color: #24292e;
}

.app-nav {
  display: flex;
  gap: 8px;
}

.app-nav button {
  padding: 8px 16px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  background: white;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s;
}

.app-nav button:hover {
  background: #f3f4f6;
}

.app-nav button.active {
  background: #0366d6;
  color: white;
  border-color: #0366d6;
}

.settings-button {
  background: #6f42c1 !important;
  color: white !important;
  border-color: #6f42c1 !important;
}

.app-main {
  padding: 24px;
  max-width: 1200px;
  margin: 0 auto;
}

/* 设置对话框样式 */
.settings-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.settings-dialog {
  background: white;
  border-radius: 8px;
  width: 400px;
  max-width: 90vw;
}

.dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid #e1e5e9;
  background: #f8f9fa;
}

.dialog-header h3 {
  margin: 0;
  font-size: 16px;
}

.close-button {
  background: none;
  border: none;
  font-size: 18px;
  cursor: pointer;
  padding: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-content {
  padding: 16px;
}

.setting-item {
  margin-bottom: 16px;
}

.setting-item label {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  font-weight: 500;
}

.api-key-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  font-size: 14px;
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.cancel-button, .confirm-button {
  padding: 8px 16px;
  border: 1px solid #d0d7de;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
}

.cancel-button {
  background: white;
  color: #24292e;
}

.confirm-button {
  background: #28a745;
  color: white;
  border-color: #28a745;
}
</style>

主入口文件

typescript
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createXAIPlugin } from './plugins/xai';

const app = createApp(App);

// 获取 API Key
const apiKey = localStorage.getItem('openai-api-key') || '';

if (apiKey) {
  // 安装 XAI 插件
  app.use(createXAIPlugin({ apiKey }));
}

app.mount('#app');

🧪 测试示例

typescript
// src/composables/__tests__/useChat.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useChat } from '../useChat';
import { useXAI } from '../useXAI';

// Mock useXAI
vi.mock('../useXAI', () => ({
  useXAI: vi.fn()
}));

const mockSDK = {
  chat: vi.fn()
};

const mockUseXAI = {
  sdk: mockSDK,
  isInitialized: { value: true },
  error: { value: null }
};

describe('useChat', () => {
  beforeEach(() => {
    vi.mocked(useXAI).mockReturnValue(mockUseXAI as any);
    mockSDK.chat.mockClear();
  });

  it('should send message successfully', async () => {
    mockSDK.chat.mockResolvedValue({ content: 'Test response' });
    
    const { sendMessage, messages } = useChat();
    
    await sendMessage('Hello');
    
    expect(mockSDK.chat).toHaveBeenCalledWith('Hello', {
      maxTokens: 1000,
      temperature: 0.7,
      stream: false
    });
    
    expect(messages.value).toHaveLength(2);
    expect(messages.value[0].content).toBe('Hello');
    expect(messages.value[1].content).toBe('Test response');
  });

  it('should handle errors', async () => {
    const error = new Error('API Error');
    mockSDK.chat.mockRejectedValue(error);
    
    const onError = vi.fn();
    const { sendMessage } = useChat({ onError });
    
    await sendMessage('Hello');
    
    expect(onError).toHaveBeenCalledWith(error);
  });
});

📖 最佳实践

1. 响应式数据管理

typescript
// 使用 reactive 管理复杂状态
import { reactive, readonly } from 'vue';

const state = reactive({
  messages: [],
  isLoading: false,
  error: null
});

// 只暴露只读版本
export const useReadonlyState = () => readonly(state);

2. 错误处理

typescript
// src/composables/useErrorHandler.ts
import { ref } from 'vue';

export const useErrorHandler = () => {
  const error = ref<string | null>(null);
  
  const handleError = (err: unknown) => {
    if (err instanceof Error) {
      error.value = err.message;
    } else {
      error.value = '未知错误';
    }
    
    // 可以添加错误上报逻辑
    console.error('Error occurred:', err);
  };
  
  const clearError = () => {
    error.value = null;
  };
  
  return {
    error,
    handleError,
    clearError
  };
};

3. 性能优化

vue
<template>
  <!-- 使用 v-memo 优化列表渲染 -->
  <div 
    v-for="message in messages" 
    :key="message.id"
    v-memo="[message.content, message.timestamp]"
  >
    {{ message.content }}
  </div>
</template>

<script setup lang="ts">
import { shallowRef, markRaw } from 'vue';

// 对于不需要深度响应的对象使用 shallowRef
const sdk = shallowRef(null);

// 对于不需要响应式的对象使用 markRaw
const config = markRaw({
  apiKey: 'xxx',
  baseURL: 'https://api.openai.com'
});
</script>

🚀 构建和部署

Vite 配置

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')
    }
  },
  define: {
    'process.env': {}
  },
  build: {
    rollupOptions: {
      external: ['xai-sdk'],
      output: {
        globals: {
          'xai-sdk': 'XAI_SDK'
        }
      }
    }
  }
});

TypeScript 配置

json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

📝 相关资源


💡 提示: 这些示例展示了 XAI-SDK 在 Vue 3 应用中的完整集成方案。您可以根据项目需求选择合适的组件和 Composables。

Released under the MIT License.