const { createApp, ref, computed, watch, nextTick, onMounted } = Vue; createApp({ setup() { const apiBase = ref(window.location.origin || "http://localhost:8000"); // Query State const userQuery = ref(''); const queryLoading = ref(false); const queryResult = ref(''); const eventSource = ref(null); const rowsAcc = ref([]); const bufferRow = ref(null); const examples = [ "查询12月份充电量TOP 10场站的充电情况", "查询净月商贸城站12月的充电量", "查询12月企业充电量排名的TOP 10", "查询所有场站的近3个月的充电情况,找出变化最大的前10名" ]; // Helpers const isPlainObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'; const colSynonyms = { name: ['name', 'station', 'station_name', 'site', '场站', '场站名称', '站点', '站名'], total: ['total', 'total_kwh', 'sum_kwh', 'energy_total', '总电量', '总充电量', '总度', '总度数'], peak: ['peak', 'peak_kwh', 'energy_peak', '峰', '峰时电量'], flat: ['flat', 'flat_kwh', 'energy_flat', '平', '平时电量'], valley: ['valley', 'valley_kwh', 'energy_valley', '谷', '谷时电量'], }; const pickKey = (obj, keys) => keys.find(k => Object.prototype.hasOwnProperty.call(obj, k)); const pickVal = (obj, keys) => { const k = pickKey(obj, keys); return k ? obj[k] : undefined; }; const formatNumber = (n) => { if (n === null || n === undefined || n === '') return ''; const v = parseFloat(String(n).replace(/,/g, '')); if (isNaN(v)) return String(n); return v.toLocaleString('zh-CN', { maximumFractionDigits: 2 }); }; const pushRow = (row) => { if (!row || !row.name) return; rowsAcc.value.push(row); const r = row; const line = `| ${r.name ?? ''} | ${formatNumber(r.total)} | ${formatNumber(r.peak)} | ${formatNumber(r.flat)} | ${formatNumber(r.valley)} |`; if (rowsAcc.value.length === 1) { const header = `| 场站名称 | 总电量 | 峰 | 平 | 谷 |\n| --- | ---: | ---: | ---: | ---: |`; if (queryResult.value && !queryResult.value.endsWith('\n')) { queryResult.value += '\n\n'; } else if (queryResult.value) { queryResult.value += '\n'; } queryResult.value += header + '\n'; } queryResult.value += line + '\n'; }; const handleDegreeSearch = () => { if (!userQuery.value.trim()) { if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.warning('请输入您的问题'); else alert('请输入您的问题'); return; } if (queryLoading.value) { if (eventSource.value) { eventSource.value.close(); } } queryLoading.value = true; queryResult.value = ''; rowsAcc.value = []; bufferRow.value = null; const url = `${apiBase.value}/degree/chat?q=${encodeURIComponent(userQuery.value)}`; try { const es = new EventSource(url); eventSource.value = es; es.onmessage = (event) => { const eventData = event.data.trim(); if (eventData === '[DONE]') { queryLoading.value = false; es.close(); renderCharts(); return; } try { const data = JSON.parse(event.data); let chunk = ''; if (data.content) chunk = data.content; else if (data.markdown) chunk = data.markdown; else if (data.chunk) chunk = data.chunk; else if (typeof data === 'string') chunk = data; if (chunk) { queryResult.value += chunk; } } catch (e) { if (eventData && eventData !== '[DONE]') { queryResult.value += event.data; } } }; es.onerror = (err) => { console.error('SSE Error:', err); queryLoading.value = false; es.close(); if (!queryResult.value) { queryResult.value = '查询过程出现异常,未能获取到数据'; } }; } catch (err) { console.error('Failed to create EventSource:', err); queryLoading.value = false; if (typeof ElementPlus !== 'undefined') ElementPlus.ElMessage.error('无法建立连接'); else alert('无法建立连接'); } }; const stopDegreeGeneration = () => { if (eventSource.value) { eventSource.value.close(); eventSource.value = null; } queryLoading.value = false; }; const setExample = (text) => { userQuery.value = text; handleDegreeSearch(); }; const renderCharts = () => { if (typeof echarts === 'undefined') return; nextTick(() => { const containers = document.querySelectorAll('.echarts-container'); containers.forEach(container => { if (container.getAttribute('data-rendered') === 'true') return; try { const configStr = decodeURIComponent(container.getAttribute('data-config')).trim(); if (!configStr.startsWith('{') || !configStr.endsWith('}')) return; const config = JSON.parse(configStr); let chart = echarts.getInstanceByDom(container); if (!chart) { chart = echarts.init(container, 'dark'); window.addEventListener('resize', () => chart.resize()); } chart.setOption(config); container.setAttribute('data-rendered', 'true'); container.style.opacity = '1'; } catch (e) { if (queryLoading.value) return; console.error('ECharts rendering error:', e); container.innerHTML = `
图表渲染失败: ${e.message}
`; } }); }); }; // Configure Marked if (typeof marked !== 'undefined') { const renderer = new marked.Renderer(); const oldCode = renderer.code.bind(renderer); renderer.code = function(code, language) { if (language === 'echarts') { const id = 'chart-' + Math.random().toString(36).substr(2, 9); return ``; } return oldCode(code, language); }; marked.setOptions({ renderer: renderer, gfm: true, breaks: true }); } // 降级用的简易 Markdown 解析器 const simpleMarkdown = (text) => { if (!text) return ''; let lines = text.split('\n'); let html = ''; let inList = false; const parseInline = (str) => { return str .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/`(.*?)`/g, '$1');
};
for (let line of lines) {
let trimmed = line.trim();
if (!trimmed) {
if (inList) { html += ''; inList = false; }
html += '${parseInline(trimmed)}
`; } } if (inList) html += ''; return html; }; // 增强的 Markdown & LaTeX 解析器 const renderMarkdownAndLatex = (text) => { if (!text) return ''; try { // 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $) let processedText = text; // 处理块级 LaTeX: $$ ... $$ processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { try { if (typeof katex !== 'undefined') { return '