This commit is contained in:
HuangHai
2026-01-19 13:56:47 +08:00
parent 219cd5c220
commit b20b778f5d
10 changed files with 242 additions and 132 deletions

View File

@@ -2,64 +2,56 @@
<html>
<head>
<meta charset="utf-8">
<title>驿来特价格分析大屏</title>
<title>多供应商分时电价分析大屏</title>
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="app">
<div class="left-panel">
<div class="controls">
<span class="label">搜索驿来特场站</span>
<input v-model="searchKeyword" placeholder="输入名称或地址关键字" style="min-width:220px;">
<button @click="loadStations">查询场站</button>
<span class="label">选择供应商</span>
<select v-model="selectedOperator" @change="loadOperatorPrices">
<option v-for="op in operators" :key="op.value" :value="op.value">{{ op.label }}</option>
</select>
<button @click="loadAllOperatorsPrices" :disabled="loading">查询四家最新24小时电价</button>
<div class="controls-spacer"></div>
<span class="label">当前场站:{{ selectedStation ? selectedStation.station_name : "未选择" }}</span>
<button @click="startAiStream" :disabled="!selectedStation || aiLoading">AI价差分析</button>
<button @click="exportAllPrices" :disabled="exporting">一键导出各供应商电价</button>
</div>
<div class="station-list">
<div class="station-list-header">驿来特场站列表</div>
<div class="station-list-header">四家供应商24小时平均电价对比</div>
<table class="station-table">
<thead>
<tr>
<th style="width:32%;">场站名称</th>
<th style="width:48%;">地址</th>
<th style="width:10%;">当前价</th>
<th style="width:10%;">操作</th>
<th style="width:20%;">小时</th>
<th v-for="op in operators" :key="op.value" style="width:20%;">{{ op.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="s in stations" :key="s.station_hash" class="station-row" :class="{active:selectedStation && selectedStation.station_hash===s.station_hash}" @click="selectStation(s)">
<td>{{ s.station_name }}</td>
<td>{{ s.address }}</td>
<td>{{ formatCell(s.current_price) }}</td>
<td><button @click.stop="selectStation(s)" :disabled="loading">查看价差</button></td>
<tr v-for="(row, idx) in priceTableRows" :key="idx">
<td>{{ row.hour }}</td>
<td v-for="cell in row.values" :key="cell.operator">{{ formatCell(cell.price) }}</td>
</tr>
<tr v-if="stations.length===0">
<td colspan="4">暂无数据,请先查询场站</td>
<tr v-if="priceTableRows.length===0">
<td :colspan="operators.length + 1">暂无数据,请先查询</td>
</tr>
</tbody>
</table>
</div>
<div id="chart"></div>
<div class="price-table" v-if="priceTableColumns.length">
<div class="price-table-title">24 小时分时电价表(元/度)</div>
<table class="price-table-inner">
<thead>
<tr>
<th v-for="col in priceTableColumns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,idx) in priceTableRows" :key="idx">
<td v-for="(cell,j) in row" :key="j">{{ formatCell(cell) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="right-panel">
<div class="ai-title">AI 辅助分析</div>
<div class="ai-box">{{ aiText || placeholder }}</div>
<div class="ai-box">
<div class="ai-question">
<div class="label">默认问题</div>
<div class="question-text">
请根据爬取的各供应商分时电价等信息,对各司的定价策略,
与我司(驿来特)的定价策略进行综合对比,分析我司可能存在的潜在问题。
</div>
</div>
<button @click="startAiAnalysis" :disabled="aiLoading">发起AI综合分析</button>
<div class="ai-result">{{ aiText || placeholder }}</div>
</div>
</div>
</div>
<script src="js/vue.global.js"></script>

View File

@@ -1,128 +1,127 @@
const {createApp,onMounted,ref} = Vue
createApp({
setup(){
const apiBase = ref(window.location.origin || "http://localhost:8000")
const stationHash = ref("")
const operators = ref([
{label:"新电途",value:"新电途"},
{label:"特来电",value:"特来电"},
{label:"驿来特",value:"驿来特"},
{label:"艾特吉易充",value:"艾特吉易充"}
])
const selectedOperator = ref("驿来特")
const aiText = ref("")
const placeholder = ref("在左侧列表中选择驿来特场站点击“查看价差”加载24小时分时电价与竞品对比再点击“AI价差分析”获取模型给出的策略建议。")
const placeholder = ref("点击左侧按钮查询四家供应商最新24小时分时电价再使用右侧AI综合分析功能对各司定价策略进行对比。")
const loading = ref(false)
const aiLoading = ref(false)
const stations = ref([])
const searchKeyword = ref("")
const selectedStation = ref(null)
const priceTableColumns = ref([])
const priceTableRows = ref([])
const hourlyPricesByOperator = ref({})
let chartInstance = null
let es = null
const initChart = () => {
const dom = document.getElementById("chart")
if (dom && !chartInstance) {
chartInstance = echarts.init(dom)
}
}
const loadStations = async () => {
try{
const url = apiBase.value + "/api/ylt/stations"
const params = searchKeyword.value ? {params:{q:searchKeyword.value}} : {}
const res = await axios.get(url,params)
stations.value = Array.isArray(res.data) ? res.data : []
if (!selectedStation.value && stations.value.length > 0) {
selectStation(stations.value[0])
}
}catch(e){
console.error(e)
}
}
const buildPriceTable = data => {
const cols = []
cols.push("时段")
cols.push("驿来特")
data.competitors.forEach(c => {
cols.push(c.operator)
})
priceTableColumns.value = cols
const buildPriceTable = () => {
const rows = []
for (let i = 0; i < data.hours.length; i++) {
const row = []
row.push(data.hours[i] + ":00")
row.push(data.ylt.series[i])
data.competitors.forEach(c => {
row.push(c.series[i])
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)
}
const loadComparison = async () => {
if (!stationHash.value) return
loading.value = true
try{
const url = apiBase.value + "/api/ylt/pricing/comparison/" + stationHash.value
const res = await axios.get(url)
const data = res.data
const hours = data.hours.map(h => h + ":00")
const series = []
series.push({
name:"驿来特",
type:"line",
smooth:true,
data:data.ylt.series
return {name: op.label, type:"line", smooth:true, data:seriesData}
})
data.competitors.forEach(c=>{
series.push({
name:c.operator,
type:"line",
smooth:true,
data:c.series
})
})
buildPriceTable(data)
const hours = rows.map(r => r.hour)
const option = {
tooltip:{trigger:"axis"},
legend:{data:series.map(s=>s.name),textStyle:{color:"#e5e7eb"}},
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
}
initChart()
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 selectStation = s => {
selectedStation.value = s
stationHash.value = s.station_hash
loadComparison()
const loadOperatorPrices = async () => {
if (!selectedOperator.value) return
await loadAllOperatorsPrices()
}
const startAiStream = () => {
if (!selectedStation.value || !stationHash.value) return
if (es) {
es.close()
es = null
const exportAllPrices = async () => {
try{
loading.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{
loading.value = false
}
}
const startAiAnalysis = async () => {
if (aiLoading.value) return
aiText.value = ""
aiLoading.value = true
const url = apiBase.value + "/api/ylt/pricing/comparison/" + stationHash.value + "/sse"
es = new EventSource(url)
es.onmessage = ev => {
aiText.value += ev.data
}
es.onerror = () => {
try{
const res = await axios.get(apiBase.value + "/api/ai/pricing/strategy-summary")
aiText.value = res.data && res.data.summary ? res.data.summary : ""
}catch(e){
console.error(e)
}finally{
aiLoading.value = false
if (es) {
es.close()
es = null
}
}
}
onMounted(()=>{
initChart()
loadStations()
loadAllOperatorsPrices()
})
const formatCell = v => {
if (v === null || v === undefined || v === "") return "-"
if (typeof v === "number") {
@@ -131,6 +130,7 @@ return v.toFixed(2)
}
return v
}
return {stationHash,aiText,placeholder,loading,aiLoading,stations,searchKeyword,selectedStation,priceTableColumns,priceTableRows,loadStations,loadComparison,selectStation,startAiStream,formatCell}
return {apiBase,operators,selectedOperator,aiText,placeholder,loading,aiLoading,priceTableRows,loadOperatorPrices,loadAllOperatorsPrices,exportAllPrices,startAiAnalysis,formatCell}
}
}).mount("#app")