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。