'commit'
This commit is contained in:
@@ -97,27 +97,30 @@ async def export_prices_zip():
|
||||
|
||||
@router.get("/api/ai/pricing/strategy-summary")
|
||||
async def ai_pricing_strategy_summary():
|
||||
resp = await get_operators_hourly_prices()
|
||||
data = resp.get("operators", [])
|
||||
text_data = []
|
||||
for item in data:
|
||||
text_data.append({"operator": item.get("operator"), "series": item.get("series")})
|
||||
text = (
|
||||
"下面是四家供应商(新电途、特来电、驿来特、艾特吉易充)基于最新爬取数据计算出的平均24小时分时电价:\n"
|
||||
f"{text_data}\n"
|
||||
"请根据这些数据,综合分析各司的定价策略差异,重点对比我司(驿来特)与其他供应商的分时电价水平,"
|
||||
"指出我司在不同时段可能存在的潜在问题和风险(例如明显偏贵、价格结构不合理等),并给出2-3条可执行的优化建议。"
|
||||
"回答控制在600字以内。"
|
||||
)
|
||||
chunks: List[str] = []
|
||||
async for chunk in get_llm_response(
|
||||
text,
|
||||
stream=False,
|
||||
system_prompt="你是熟悉中国充电桩行业的电价策略分析顾问。",
|
||||
):
|
||||
chunks.append(chunk)
|
||||
summary_text = "".join(chunks)
|
||||
return JSONResponse({"summary": summary_text})
|
||||
async def generate_stream():
|
||||
resp = await get_operators_hourly_prices()
|
||||
data = resp.get("operators", [])
|
||||
text_data = []
|
||||
for item in data:
|
||||
text_data.append({"operator": item.get("operator"), "series": item.get("series")})
|
||||
|
||||
prompt = (
|
||||
"下面是四家供应商(新电途、特来电、驿来特、艾特吉易充)基于最新爬取数据计算出的平均24小时分时电价:\n"
|
||||
f"{text_data}\n"
|
||||
"请根据这些数据,综合分析各司的定价策略差异,重点对比我司(驿来特)与其他供应商的分时电价水平,"
|
||||
"指出我司在不同时段可能存在的潜在问题和风险(例如明显偏贵、价格结构不合理等),并给出2-3条可执行的优化建议。"
|
||||
"请使用Markdown格式输出,重点可以使用加粗,如有公式可使用LaTeX格式。"
|
||||
"回答控制在800字以内。"
|
||||
)
|
||||
|
||||
async for chunk in get_llm_response(
|
||||
prompt,
|
||||
stream=True,
|
||||
system_prompt="你是熟悉中国充电桩行业的电价策略分析顾问。",
|
||||
):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(generate_stream(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@router.get("/api/ylt/stations", response_model=List[StationBase])
|
||||
|
||||
Binary file not shown.
@@ -1,28 +1,296 @@
|
||||
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;}
|
||||
/* Modern Dark Theme - Slate/Blue Palette */
|
||||
:root {
|
||||
--bg-color: #0f172a; /* Slate 900 */
|
||||
--card-bg: #1e293b; /* Slate 800 */
|
||||
--card-border: #334155; /* Slate 700 */
|
||||
--text-primary: #f1f5f9; /* Slate 100 */
|
||||
--text-secondary: #94a3b8; /* Slate 400 */
|
||||
--accent-color: #3b82f6; /* Blue 500 */
|
||||
--accent-hover: #2563eb; /* Blue 600 */
|
||||
--success-color: #10b981; /* Emerald 500 */
|
||||
--table-header-bg: #0f172a;
|
||||
--table-row-hover: #334155;
|
||||
--scrollbar-thumb: #475569;
|
||||
--scrollbar-track: #0f172a;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-image: radial-gradient(circle at 50% 0%, #1e293b 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column; /* Changed to column to include header */
|
||||
height: 100vh;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Header Styling */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, #60a5fa, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* Main Layout */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
min-height: 0; /* Important for nested scrolling */
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
padding: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
/* Station List / Table */
|
||||
.station-list {
|
||||
flex: 1; /* Give it space */
|
||||
min-height: 0;
|
||||
@apply card; /* Conceptually using card styles */
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.station-list-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background-color: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
.station-table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
.station-table {
|
||||
width: 100%;
|
||||
border-collapse: separate; /* Changed for border-radius support if needed, but sticky needs collapse usually or specific handling. Keeping collapse for alignment. */
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.station-table th {
|
||||
background-color: var(--table-header-bg);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 12px 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.station-table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.station-table tbody tr:hover td {
|
||||
background-color: var(--table-row-hover);
|
||||
}
|
||||
|
||||
.station-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Chart */
|
||||
#chart {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* AI Analysis Section */
|
||||
.ai-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-title::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background-color: var(--accent-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ai-box {
|
||||
flex: 1;
|
||||
background-color: rgba(15, 23, 42, 0.3); /* Slightly darker inner bg */
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--card-border);
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #cbd5e1; /* Slate 300 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.ai-question {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border-left: 3px solid var(--accent-color);
|
||||
padding: 12px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ai-question .label {
|
||||
font-size: 12px;
|
||||
color: var(--accent-color);
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ai-question .question-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ai-result {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Markdown Styling in AI Result */
|
||||
.ai-result h1, .ai-result h2, .ai-result h3 {
|
||||
color: var(--text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ai-result strong {
|
||||
color: #60a5fa; /* Blue 400 */
|
||||
}
|
||||
.ai-result ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.ai-result li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ai-result p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar Webkit */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,55 +4,75 @@
|
||||
<meta charset="utf-8">
|
||||
<title>多供应商分时电价分析大屏</title>
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="left-panel">
|
||||
<div class="controls">
|
||||
<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>
|
||||
<button @click="exportAllPrices" :disabled="exporting">一键导出各供应商电价</button>
|
||||
</div>
|
||||
<div class="station-list">
|
||||
<div class="station-list-header">四家供应商24小时平均电价对比</div>
|
||||
<table class="station-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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="(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="priceTableRows.length===0">
|
||||
<td :colspan="operators.length + 1">暂无数据,请先查询</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<div class="ai-title">AI 辅助分析</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>
|
||||
<header class="dashboard-header">
|
||||
<div class="dashboard-title">⚡ 充电站电价分析驾驶舱</div>
|
||||
<div class="controls">
|
||||
<button class="btn-primary" @click="exportAllPrices" :disabled="exporting">
|
||||
<span v-if="!exporting">📊 一键导出分析报告</span>
|
||||
<span v-else>⏳ 导出中...</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="left-panel">
|
||||
<div class="station-list">
|
||||
<div class="station-list-header">全网供应商24小时电价监控</div>
|
||||
<div class="station-table-container">
|
||||
<table class="station-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:15%;">时段</th>
|
||||
<th v-for="op in operators" :key="op.value" :style="{width: (85/operators.length) + '%'}">{{ op.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in priceTableRows" :key="idx">
|
||||
<td style="font-family: monospace; color: var(--text-secondary);">{{ row.hour }}</td>
|
||||
<td v-for="cell in row.values" :key="cell.operator" :style="{color: getPriceColor(cell.price)}">
|
||||
{{ formatCell(cell.price) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="priceTableRows.length===0">
|
||||
<td :colspan="operators.length + 1" style="text-align:center; padding: 32px; color: var(--text-secondary);">
|
||||
数据加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="ai-title">智能决策分析助手</div>
|
||||
<div class="ai-box" ref="aiBoxRef">
|
||||
<div class="ai-question">
|
||||
<div class="label">当前分析任务</div>
|
||||
<div class="question-text">
|
||||
请根据爬取的各供应商分时电价等信息,对各司的定价策略,
|
||||
与我司(驿来特)的定价策略进行综合对比,分析我司可能存在的潜在问题。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 10px 0;">
|
||||
<button class="btn-primary" @click="startAiAnalysis" :disabled="aiLoading" style="width: 100%;">
|
||||
{{ aiLoading ? '✨ AI 正在深入分析...' : '🚀 发起 AI 综合诊断' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ai-result" v-html="renderedAiText || placeholder" style="opacity: 0.9;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/vue.global.js"></script>
|
||||
<script src="js/axios.min.js"></script>
|
||||
|
||||
117
static/js/app.js
117
static/js/app.js
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user