Files
aiData/static/js/app.js
HuangHai b0b4533f57 'commit'
2026-01-20 07:43:13 +08:00

246 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")