This commit is contained in:
HuangHai
2026-01-20 07:31:23 +08:00
parent b20b778f5d
commit 6263487425
5 changed files with 483 additions and 113 deletions

View File

@@ -13,7 +13,9 @@ const selectedOperator = ref("驿来特")
const aiText = ref("")
const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价,再使用右侧AI综合分析功能对各司定价策略进行对比。")
const loading = ref(false)
const exporting = ref(false)
const aiLoading = ref(false)
const aiBoxRef = ref(null)
const priceTableRows = ref([])
const hourlyPricesByOperator = ref({})
let chartInstance = null
@@ -47,15 +49,37 @@ 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 = {
tooltip:{trigger:"axis"},
legend:{data:operators.value.map(o=>o.label),textStyle:{color:"#e5e7eb"}},
xAxis:{type:"category",data:hours,axisLine:{lineStyle:{color:"#4b5563"}},axisLabel:{color:"#9ca3af"}},
yAxis:{type:"value",name:"元/度",axisLine:{lineStyle:{color:"#4b5563"}},axisLabel:{color:"#9ca3af"},splitLine:{lineStyle:{color:"#111827"}}},
grid:{left:40,right:20,top:30,bottom:30},
series:series
}
chartInstance.setOption(option)
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)
}
}
@@ -78,14 +102,9 @@ loading.value = false
}
}
const loadOperatorPrices = async () => {
if (!selectedOperator.value) return
await loadAllOperatorsPrices()
}
const exportAllPrices = async () => {
try{
loading.value = true
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)
@@ -99,7 +118,7 @@ window.URL.revokeObjectURL(url)
}catch(e){
console.error(e)
}finally{
loading.value = false
exporting.value = false
}
}
@@ -108,15 +127,68 @@ if (aiLoading.value) return
aiText.value = ""
aiLoading.value = true
try{
const res = await axios.get(apiBase.value + "/api/ai/pricing/strategy-summary")
aiText.value = res.data && res.data.summary ? res.data.summary : ""
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()
@@ -131,6 +203,13 @@ return v.toFixed(2)
return v
}
return {apiBase,operators,selectedOperator,aiText,placeholder,loading,aiLoading,priceTableRows,loadOperatorPrices,loadAllOperatorsPrices,exportAllPrices,startAiAnalysis,formatCell}
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,aiLoading,priceTableRows,loadAllOperatorsPrices,exportAllPrices,startAiAnalysis,formatCell,getPriceColor,renderedAiText,aiBoxRef}
}
}).mount("#app")