270 lines
8.0 KiB
JavaScript
270 lines
8.0 KiB
JavaScript
// Vue 3 Application
|
|
const { createApp } = Vue;
|
|
|
|
createApp({
|
|
data() {
|
|
return {
|
|
messages: [],
|
|
userInput: '',
|
|
loading: false,
|
|
sessionId: null,
|
|
charts: {}
|
|
};
|
|
},
|
|
mounted() {
|
|
this.sessionId = this.generateSessionId();
|
|
this.autoResizeTextarea();
|
|
},
|
|
methods: {
|
|
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;
|
|
|
|
try {
|
|
const response = await fetch('/api/chat/message', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
message: message,
|
|
session_id: this.sessionId
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('请求失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Add assistant message
|
|
const assistantMessage = {
|
|
role: 'assistant',
|
|
content: data.message,
|
|
timestamp: new Date(),
|
|
metadata: data.metadata
|
|
};
|
|
|
|
this.messages.push(assistantMessage);
|
|
|
|
// Render chart if needed
|
|
if (data.metadata && data.metadata.type === 'chart') {
|
|
this.$nextTick(() => {
|
|
const index = this.messages.length - 1;
|
|
this.renderChart(index, data.metadata.data);
|
|
});
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.scrollToBottom();
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('发送消息失败:', error);
|
|
this.messages.push({
|
|
role: 'assistant',
|
|
content: '抱歉,发送消息失败,请稍后重试。',
|
|
timestamp: new Date()
|
|
});
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
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 => {
|
|
console.error('复制失败:', 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) {
|
|
console.error('复制失败:', 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);
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
userInput() {
|
|
this.$nextTick(() => {
|
|
this.autoResizeTextarea();
|
|
});
|
|
}
|
|
}
|
|
}).mount('#app');
|