2026-01-18 16:08:46 +08:00
|
|
|
|
const {createApp,onMounted,ref} = Vue
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
2026-01-18 16:08:46 +08:00
|
|
|
|
createApp({
|
|
|
|
|
|
setup(){
|
|
|
|
|
|
const apiBase = ref(window.location.origin || "http://localhost:8000")
|
2026-01-19 13:56:47 +08:00
|
|
|
|
const operators = ref([
|
|
|
|
|
|
{label:"新电途",value:"新电途"},
|
|
|
|
|
|
{label:"特来电",value:"特来电"},
|
|
|
|
|
|
{label:"驿来特",value:"驿来特"},
|
|
|
|
|
|
{label:"艾特吉易充",value:"艾特吉易充"}
|
|
|
|
|
|
])
|
|
|
|
|
|
const selectedOperator = ref("驿来特")
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const aiText = ref("")
|
2026-01-19 13:56:47 +08:00
|
|
|
|
const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价,再使用右侧AI综合分析功能对各司定价策略进行对比。")
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const loading = ref(false)
|
2026-01-20 07:31:23 +08:00
|
|
|
|
const exporting = ref(false)
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const aiLoading = ref(false)
|
2026-01-20 07:31:23 +08:00
|
|
|
|
const aiBoxRef = ref(null)
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const priceTableRows = ref([])
|
2026-01-19 13:56:47 +08:00
|
|
|
|
const hourlyPricesByOperator = ref({})
|
2026-01-18 16:08:46 +08:00
|
|
|
|
let chartInstance = null
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const initChart = () => {
|
|
|
|
|
|
const dom = document.getElementById("chart")
|
|
|
|
|
|
if (dom && !chartInstance) {
|
|
|
|
|
|
chartInstance = echarts.init(dom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
const buildPriceTable = () => {
|
2026-01-18 16:08:46 +08:00
|
|
|
|
const rows = []
|
2026-01-19 13:56:47 +08:00
|
|
|
|
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})
|
2026-01-18 16:08:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
rows.push(row)
|
|
|
|
|
|
}
|
|
|
|
|
|
priceTableRows.value = rows
|
2026-01-19 13:56:47 +08:00
|
|
|
|
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}
|
2026-01-18 16:08:46 +08:00
|
|
|
|
})
|
2026-01-19 13:56:47 +08:00
|
|
|
|
const hours = rows.map(r => r.hour)
|
2026-01-20 07:31:23 +08:00
|
|
|
|
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)
|
2026-01-19 13:56:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
2026-01-18 16:17:09 +08:00
|
|
|
|
}catch(e){
|
|
|
|
|
|
console.error(e)
|
2026-01-18 16:08:46 +08:00
|
|
|
|
}finally{
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
const exportAllPrices = async () => {
|
|
|
|
|
|
try{
|
2026-01-20 07:31:23 +08:00
|
|
|
|
exporting.value = true
|
2026-01-19 13:56:47 +08:00
|
|
|
|
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{
|
2026-01-20 07:31:23 +08:00
|
|
|
|
exporting.value = false
|
2026-01-18 16:08:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
const startAiAnalysis = async () => {
|
|
|
|
|
|
if (aiLoading.value) return
|
2026-01-18 16:08:46 +08:00
|
|
|
|
aiText.value = ""
|
|
|
|
|
|
aiLoading.value = true
|
2026-01-19 13:56:47 +08:00
|
|
|
|
try{
|
2026-01-20 07:31:23 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
}catch(e){
|
|
|
|
|
|
console.error(e)
|
2026-01-20 07:31:23 +08:00
|
|
|
|
aiText.value += "\n(分析过程出错: " + e.message + ")"
|
2026-01-19 13:56:47 +08:00
|
|
|
|
}finally{
|
2026-01-18 16:08:46 +08:00
|
|
|
|
aiLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
2026-01-20 07:31:23 +08:00
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-18 16:08:46 +08:00
|
|
|
|
onMounted(()=>{
|
|
|
|
|
|
initChart()
|
2026-01-19 13:56:47 +08:00
|
|
|
|
loadAllOperatorsPrices()
|
2026-01-18 16:08:46 +08:00
|
|
|
|
})
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
2026-01-18 16:08:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-01-19 13:56:47 +08:00
|
|
|
|
|
2026-01-20 07:31:23 +08:00
|
|
|
|
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}
|
2026-01-18 16:08:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}).mount("#app")
|