// Vue 3 Application const { createApp } = Vue; createApp({ data() { return { messages: [], userInput: '', loading: false, sessionId: null, charts: {}, showImageModal: false, modalImageUrl: '', showContactModal: false, currentModel: null }; }, mounted() { // 检查登录状态 if (!this.checkAuth()) { window.location.href = '/static/login.html'; return; } this.sessionId = this.generateSessionId(); this.autoResizeTextarea(); this.loadModels(); }, methods: { checkAuth() { const token = localStorage.getItem('token'); if (!token) return false; // 验证token是否过期(简单检查) try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 > Date.now(); } catch { return false; } }, logout() { localStorage.removeItem('token'); window.location.href = '/static/login.html'; }, async sendMessage() { if (!this.userInput.trim() || this.loading) return; const message = this.userInput.trim(); this.userInput = ''; // Add user message this.messages.push({ role: 'user', content: message, timestamp: new Date() }); this.$nextTick(() => { this.scrollToBottom(); this.autoResizeTextarea(); }); this.loading = true; // 创建一个空的助手消息用于流式更新 const assistantMessage = { role: 'assistant', content: '', timestamp: new Date(), metadata: null, streaming: true // 标记为流式输出中 }; this.messages.push(assistantMessage); const messageIndex = this.messages.length - 1; try { const token = localStorage.getItem('token'); // 使用流式API const response = await fetch('/api/chat/message/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ message: message, session_id: this.sessionId }) }); if (response.status === 401) { // Token过期或无效,跳转登录页 localStorage.removeItem('token'); window.location.href = '/static/login.html'; return; } if (!response.ok) { throw new Error('请求失败'); } // 读取流式响应 const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let chunkCount = 0; while (true) { const { done, value } = await reader.read(); if (done) { break; } chunkCount++; // 解码数据 buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); // 保留最后一个不完整的行 buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); if (data.type === 'session_id') { this.sessionId = data.session_id; } else if (data.type === 'content') { // 追加内容 - 使用 Vue.set 确保响应式更新 const currentContent = this.messages[messageIndex].content; this.messages[messageIndex].content = currentContent + data.content; this.$nextTick(() => { this.scrollToBottom(); }); } else if (data.type === 'done') { // 完成 - 标记流式输出结束 this.messages[messageIndex].streaming = false; } else if (data.type === 'error') { throw new Error(data.error); } } catch (e) { } } } } // 确保流式标志被清除 this.messages[messageIndex].streaming = false; } catch (error) { this.messages[messageIndex].content = '抱歉,发送消息失败,请稍后重试。'; this.messages[messageIndex].streaming = false; } finally { this.loading = false; } }, sendExample(exampleText) { // Set the example text to input and send this.userInput = exampleText; this.sendMessage(); }, renderMarkdown(content) { if (!content) return ''; // Configure marked options marked.setOptions({ breaks: true, gfm: true, headerIds: false, mangle: false }); return marked.parse(content); }, renderChart(index, data) { const chartId = `chart-${index}`; const container = document.getElementById(chartId); if (!container || !data.kline_data) return; const chart = LightweightCharts.createChart(container, { width: container.clientWidth, height: 400, layout: { background: { color: '#000000' }, textColor: '#a0a0a0' }, grid: { vertLines: { color: '#1a1a1a' }, horzLines: { color: '#1a1a1a' } }, timeScale: { borderColor: '#333333', timeVisible: true }, rightPriceScale: { borderColor: '#333333' } }); const candlestickSeries = chart.addCandlestickSeries({ upColor: '#00ff41', downColor: '#ff0040', borderVisible: false, wickUpColor: '#00ff41', wickDownColor: '#ff0040' }); const klineData = data.kline_data.map(item => ({ time: item.trade_date, open: item.open, high: item.high, low: item.low, close: item.close })); candlestickSeries.setData(klineData); if (data.volume_data) { const volumeSeries = chart.addHistogramSeries({ color: '#00ff4140', priceFormat: { type: 'volume' }, priceScaleId: '' }); const volumeData = data.volume_data.map(item => ({ time: item.trade_date, value: item.vol, color: item.close >= item.open ? '#00ff4140' : '#ff004040' })); volumeSeries.setData(volumeData); } chart.timeScale().fitContent(); this.charts[chartId] = chart; // Handle resize window.addEventListener('resize', () => { if (this.charts[chartId]) { chart.applyOptions({ width: container.clientWidth }); } }); }, scrollToBottom() { const container = this.$refs.chatContainer; if (container) { setTimeout(() => { container.scrollTop = container.scrollHeight; }, 100); } }, autoResizeTextarea() { const textarea = this.$refs.textarea; if (textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } }, generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); }, copyWechat() { const wechatId = 'aaronlzhou'; // 使用现代的 Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(wechatId).then(() => { this.showCopyNotification(); }).catch(err => { this.fallbackCopy(wechatId); }); } else { this.fallbackCopy(wechatId); } }, fallbackCopy(text) { // 降级方案:使用传统方法 const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); this.showCopyNotification(); } catch (err) { } document.body.removeChild(textarea); }, showCopyNotification() { // 创建临时提示 const notification = document.createElement('div'); notification.textContent = '已复制微信号'; notification.style.cssText = ` position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: #00ff41; color: #000000; padding: 8px 16px; border-radius: 2px; font-size: 13px; font-weight: 500; z-index: 10000; animation: fadeInOut 2s ease; `; document.body.appendChild(notification); setTimeout(() => { document.body.removeChild(notification); }, 2000); }, copyMessage(content) { // 移除HTML标签,只保留纯文本 const tempDiv = document.createElement('div'); tempDiv.innerHTML = marked.parse(content); const plainText = tempDiv.textContent || tempDiv.innerText; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(plainText).then(() => { this.showNotification('已复制内容'); }).catch(err => { this.fallbackCopy(plainText); }); } else { this.fallbackCopy(plainText); } }, async generateShareImage(content, index) { try { this.showNotification('正在生成分享图...'); // 获取用户提问(前一条消息) let userQuestion = ''; if (index > 0 && this.messages[index - 1].role === 'user') { userQuestion = this.messages[index - 1].content; } // 创建临时容器 const container = document.createElement('div'); container.className = 'share-image-container'; container.style.left = '-9999px'; // 构建分享图内容(包含用户提问和AI回答) container.innerHTML = `
${userQuestion ? ` ` : ''} `; document.body.appendChild(container); // 等待渲染 await new Promise(resolve => setTimeout(resolve, 100)); // 生成图片 const canvas = await html2canvas(container, { backgroundColor: '#0a0a0a', scale: 2, logging: false, useCORS: true }); // 移除临时容器 document.body.removeChild(container); // 显示图片在模态框中 this.modalImageUrl = canvas.toDataURL('image/png'); this.showImageModal = true; this.showNotification('长按图片可保存'); } catch (error) { this.showNotification('生成失败,请重试'); } }, closeImageModal() { this.showImageModal = false; this.modalImageUrl = ''; }, showNotification(text) { const notification = document.createElement('div'); notification.textContent = text; notification.style.cssText = ` position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: #00ff41; color: #000000; padding: 8px 16px; border-radius: 2px; font-size: 13px; font-weight: 500; z-index: 10000; animation: fadeInOut 2s ease; `; document.body.appendChild(notification); setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 2000); }, async loadModels() { try { const response = await fetch('/api/llm/models'); const data = await response.json(); if (data.success) { this.currentModel = data.current; } } catch (error) { // 忽略错误 } } }, watch: { userInput() { this.$nextTick(() => { this.autoResizeTextarea(); }); } } }).mount('#app');