const { createApp, ref, computed, watch, nextTick, onMounted } = Vue; createApp({ setup() { // ========================================== // Common State // ========================================== const apiBase = ref(window.location.origin || "http://localhost:8000"); const isMobile = ref(window.innerWidth <= 768); // Handle window resize window.addEventListener('resize', () => { isMobile.value = window.innerWidth <= 768; if (chartInstance) chartInstance.resize(); }); // ========================================== // Dashboard State & Logic // ========================================== const operators = ref([ {label:"新电途",value:"新电途"}, {label:"特来电",value:"特来电"}, {label:"驿来特",value:"驿来特"}, {label:"艾特吉易充",value:"艾特吉易充"} ]); // Default select all or specific one? App.js had "驿来特" const selectedOperator = ref("驿来特"); const aiText = ref(""); const loading = ref(false); const exporting = ref(false); const exportingReport = ref(false); const aiLoading = ref(false); const aiBoxRef = ref(null); const priceTableRows = ref([]); const hourlyPricesByOperator = ref({}); let chartInstance = null; const chartType = ref('line'); // ECharts Initialization const initChart = () => { const dom = document.getElementById("chart"); if (dom && !chartInstance) { if (typeof echarts === 'undefined') { console.error("ECharts not loaded"); return; } chartInstance = echarts.init(dom); } }; const renderChart = () => { if (!chartInstance) return; const series = operators.value.map(op => { const seriesData = []; for (let h = 0; h < 24; h++) { const list = hourlyPricesByOperator.value[op.value] || []; seriesData.push(list[h] !== undefined ? list[h] : null); } if (chartType.value === 'bar') { return { name: op.label, type: "bar", barGap: 0, emphasis: { focus: 'series' }, data: seriesData }; } else { return { name: op.label, type: "line", smooth: true, emphasis: { focus: 'series' }, data: seriesData }; } }); const hours = []; for (let h = 0; h < 24; h++) { hours.push(h.toString().padStart(2,"0") + ":00"); } const option = { backgroundColor: 'transparent', tooltip: { trigger: "axis", backgroundColor: 'rgba(30, 41, 59, 0.9)', borderColor: '#334155', textStyle: { color: '#f1f5f9' }, axisPointer: { type: chartType.value === 'bar' ? 'shadow' : 'line' } }, legend: { data: operators.value.map(o => o.label), textStyle: { color: "#94a3b8" }, bottom: 0 }, xAxis: { type: "category", data: hours, axisLine: { lineStyle: { color: "#475569" } }, axisLabel: { color: "#94a3b8" } }, yAxis: { type: "value", name: "元/度", nameTextStyle: { color: "#94a3b8" }, axisLine: { lineStyle: { color: "#475569" } }, axisLabel: { color: "#94a3b8" }, splitLine: { lineStyle: { color: "#334155", type: 'dashed' } } }, grid: { left: 50, right: 30, top: 40, bottom: 40 }, series: series }; chartInstance.setOption(option, true); }; const buildPriceTable = () => { const rows = []; for (let h = 0; h < 24; h++) { const row = {hour: (h.toString().padStart(2,"0") + ":00"), values: []}; operators.value.forEach(op => { const series = hourlyPricesByOperator.value[op.value] || []; const price = series[h] !== undefined ? series[h] : null; row.values.push({operator: op.value, price}); }); rows.push(row); } priceTableRows.value = rows; renderChart(); }; const loadAllOperatorsPrices = async () => { loading.value = true; try{ const res = await axios.get(apiBase.value + "/api/operators/hourly-prices"); if (res && res.data && Array.isArray(res.data.operators)) { const dict = {}; res.data.operators.forEach(item => { dict[item.operator] = item.series || []; }); hourlyPricesByOperator.value = dict; buildPriceTable(); } }catch(e){ console.error(e); }finally{ loading.value = false; } }; const exportAllPrices = async () => { try{ exporting.value = true; const res = await axios.get(apiBase.value + "/api/export/prices-zip",{responseType:"blob"}); const blob = new Blob([res.data],{type:"application/zip"}); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "多供应商电价导出.zip"; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); }catch(e){ console.error(e); }finally{ exporting.value = false; } }; const exportAiReport = async () => { if (!aiText.value) { alert("请先生成AI分析报告"); return; } try { exportingReport.value = true; const res = await axios.post(apiBase.value + "/api/export/ai-report-docx", { content: aiText.value }, {responseType: "blob"}); const blob = new Blob([res.data], {type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "AI分析报告.docx"; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (e) { console.error(e); alert("导出失败,请检查是否安装Pandoc"); } finally { exportingReport.value = false; } }; const startAiAnalysis = async () => { if (aiLoading.value) return; aiText.value = ""; aiLoading.value = true; try{ const response = await fetch(apiBase.value + "/api/ai/pricing/strategy-summary"); if (!response.body) return; const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const {done, value} = await reader.read(); if (done) break; const chunk = decoder.decode(value, {stream: true}); aiText.value += chunk; await Vue.nextTick(); if (aiBoxRef.value) { aiBoxRef.value.scrollTop = aiBoxRef.value.scrollHeight; } } }catch(e){ console.error(e); aiText.value += "\n(分析过程出错: " + e.message + ")"; }finally{ aiLoading.value = false; } }; const formatCell = v => { if (v === null || v === undefined || v === "") return "-"; if (typeof v === "number") { if (Number.isNaN(v)) return "-"; return v.toFixed(2); } return v; }; const getPriceColor = (price) => { if (price === null || price === undefined) return 'inherit'; if (price > 1.2) return '#ef4444'; // Red-500 if (price < 0.8) return '#10b981'; // Emerald-500 return '#f1f5f9'; // Slate-100 }; // 内置简易 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) continue; if (trimmed.startsWith('### ')) { if (inList) { html += ''; inList = false; } html += `

${parseInline(trimmed.substring(4))}

`; } else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { if (!inList) { html += ''; inList = false; } html += `

${parseInline(trimmed)}

`; } } if (inList) html += ''; return html; }; const renderedAiText = computed(() => { return simpleMarkdown(aiText.value); }); onMounted(() => { initChart(); loadAllOperatorsPrices(); }); return { operators, loading, exporting, exportingReport, aiLoading, aiText, aiBoxRef, priceTableRows, hourlyPricesByOperator, chartType, // Actions loadAllOperatorsPrices, exportAllPrices, exportAiReport, startAiAnalysis, formatCell, getPriceColor, renderedAiText }; } }).mount('#app');