const {createApp,onMounted,ref} = Vue createApp({ setup(){ const apiBase = ref(window.location.origin || "http://localhost:8000") const operators = ref([ {label:"新电途",value:"新电途"}, {label:"特来电",value:"特来电"}, {label:"驿来特",value:"驿来特"}, {label:"艾特吉易充",value:"艾特吉易充"} ]) const selectedOperator = ref("驿来特") const aiText = ref("") const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价,再使用右侧AI综合分析功能对各司定价策略进行对比。") 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 initChart = () => { const dom = document.getElementById("chart") if (dom && !chartInstance) { chartInstance = echarts.init(dom) } } 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 if (chartInstance) { 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) } return {name: op.label, type:"line", smooth:true, data:seriesData} }) const hours = rows.map(r => r.hour) const option = { backgroundColor: 'transparent', tooltip: { trigger: "axis", backgroundColor: 'rgba(30, 41, 59, 0.9)', borderColor: '#334155', textStyle: { color: '#f1f5f9' } }, 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) } } 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 renderedAiText = Vue.computed(() => { if (!aiText.value) return "" try { // 配置 marked marked.setOptions({ breaks: true, gfm: true }); let html = marked.parse(aiText.value); // 简单处理LaTeX (如果有需要的话,可以引入更复杂的处理,这里假设Katex auto-render会处理或者手动处理) // 由于是computed,我们返回html,然后通过v-html渲染。 // v-html更新后,Vue不保证脚本执行。 // 我们可以在updated hook里调用renderMathInElement,或者这里仅仅返回html。 // 为了支持Latex,我们可以在watch aiText时手动更新DOM,或者使用nextTick。 // 简易方案:返回HTML,由Vue渲染。如果需要Latex渲染,可以在watch里做。 return html; } catch (e) { return aiText.value; } }) // 监听 aiText 变化以渲染 Latex Vue.watch(aiText, () => { Vue.nextTick(() => { const el = document.querySelector('.ai-result'); if (el && window.renderMathInElement) { renderMathInElement(el, { delimiters: [ {left: '$$', right: '$$', display: true}, {left: '$', right: '$', display: false}, {left: '\\(', right: '\\)', display: false}, {left: '\\[', right: '\\]', display: true} ], throwOnError: false }); } }); }); onMounted(()=>{ initChart() loadAllOperatorsPrices() }) 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 } return {apiBase,operators,aiText,placeholder,loading,exporting,exportingReport,aiLoading,priceTableRows,loadAllOperatorsPrices,exportAllPrices,exportAiReport,startAiAnalysis,formatCell,getPriceColor,renderedAiText,aiBoxRef} } }).mount("#app")