Files
aiData/static/index.html
HuangHai 5c0a1a67ac 'commit'
2026-01-18 16:02:40 +08:00

236 lines
8.0 KiB
HTML
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.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>驿来特价格分析大屏</title>
<style>
body{margin:0;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#050816;color:#e5e7eb;}
#app{display:flex;flex-direction:row;height:100vh;}
.left-panel{flex:3;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;}
.right-panel{flex:2;padding:16px;box-sizing:border-box;border-left:1px solid #111827;background:#020617;}
.controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:4px;align-items:center;}
.controls input{padding:6px 8px;border-radius:4px;border:1px solid #374151;background:#020617;color:#e5e7eb;}
.controls button{padding:6px 12px;border-radius:4px;border:none;background:#2563eb;color:#f9fafb;cursor:pointer;}
.controls button:disabled{opacity:.5;cursor:not-allowed;}
.controls-spacer{flex:1;}
.station-list{flex:0 0 220px;border:1px solid #111827;border-radius:8px;background:#020617;overflow:hidden;display:flex;flex-direction:column;}
.station-list-header{padding:8px 12px;border-bottom:1px solid #111827;font-size:14px;background:#020617;}
.station-table{width:100%;border-collapse:collapse;font-size:13px;}
.station-table th,.station-table td{padding:6px 8px;border-bottom:1px solid #111827;}
.station-table th{background:#020617;color:#9ca3af;text-align:left;}
.station-row{cursor:pointer;}
.station-row:hover{background:#111827;}
.station-row.active{background:#1d283a;}
#chart{flex:1;border:1px solid #111827;border-radius:8px;background:#0b1120;}
.price-table{flex:0 0 260px;border:1px solid #111827;border-radius:8px;background:#020617;padding:8px;box-sizing:border-box;overflow:auto;}
.price-table-title{font-size:14px;margin-bottom:4px;}
.price-table-inner{width:100%;border-collapse:collapse;font-size:12px;}
.price-table-inner th,.price-table-inner td{padding:4px 6px;border-bottom:1px solid #111827;}
.price-table-inner th{background:#020617;color:#9ca3af;text-align:right;}
.price-table-inner th:first-child,.price-table-inner td:first-child{text-align:left;}
.ai-title{font-size:16px;margin-bottom:8px;}
.ai-box{height:calc(100% - 40px);border-radius:8px;border:1px solid #111827;background:#020617;padding:12px;box-sizing:border-box;white-space:pre-wrap;overflow:auto;font-size:14px;line-height:1.6;}
.label{font-size:14px;margin-right:8px;}
</style>
</head>
<body>
<div id="app">
<div class="left-panel">
<div class="controls">
<span class="label">API</span>
<input v-model="apiBase" style="min-width:220px;">
<span class="label">搜索驿来特场站</span>
<input v-model="searchKeyword" placeholder="输入名称或地址关键字" style="min-width:220px;">
<button @click="loadStations">查询场站</button>
<div class="controls-spacer"></div>
<span class="label">当前场站:{{ selectedStation ? selectedStation.station_name : "未选择" }}</span>
<button @click="startAiStream" :disabled="!selectedStation || aiLoading">AI价差分析</button>
</div>
<div class="station-list">
<div class="station-list-header">驿来特场站列表</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>
</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>
<tr v-if="stations.length===0">
<td colspan="4">暂无数据,请先查询场站</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>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
const {createApp,onMounted,ref} = Vue
createApp({
setup(){
const apiBase = ref(window.location.origin || "http://localhost:8000")
const stationHash = ref("")
const aiText = ref("")
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([])
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 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])
})
rows.push(row)
}
priceTableRows.value = rows
}
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
})
data.competitors.forEach(c=>{
series.push({
name:c.operator,
type:"line",
smooth:true,
data:c.series
})
})
buildPriceTable(data)
const option = {
tooltip:{trigger:"axis"},
legend:{data:series.map(s=>s.name),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)
}finally{
loading.value = false
}
}
const selectStation = s => {
selectedStation.value = s
stationHash.value = s.station_hash
loadComparison()
}
const startAiStream = () => {
if (!selectedStation.value || !stationHash.value) return
if (es) {
es.close()
es = null
}
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 = () => {
aiLoading.value = false
if (es) {
es.close()
es = null
}
}
}
onMounted(()=>{
initChart()
loadStations()
})
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
}
return {apiBase,stationHash,aiText,placeholder,loading,aiLoading,stations,searchKeyword,selectedStation,priceTableColumns,priceTableRows,loadStations,loadComparison,selectStation,startAiStream,formatCell}
}
}).mount("#app")
</script>
</body>
</html>