'commit'
This commit is contained in:
@@ -86,7 +86,7 @@ async def get_operators_price_trends(days: int = 7):
|
||||
model = YltAnalyticsModel()
|
||||
rows = await model.get_operators_price_trends(days)
|
||||
|
||||
# 数据结构: { operator: { date_str: [sums_of_24h, counts_of_24h] } }
|
||||
# 数据结构: { operator: { datetime_str: [sums_of_price, counts_of_station] } }
|
||||
trend_data = {}
|
||||
for op in operators:
|
||||
trend_data[op] = {}
|
||||
@@ -95,47 +95,65 @@ async def get_operators_price_trends(days: int = 7):
|
||||
op = row.get("operator")
|
||||
if op not in trend_data:
|
||||
continue
|
||||
|
||||
# 将日期和 schedule_json 展开为 24 小时的数据点
|
||||
d_str = str(row.get("date_str"))
|
||||
# 将 2026-01-21 转换为 01/21
|
||||
try:
|
||||
date_parts = d_str.split('-')
|
||||
display_date = date_parts[1] + '/' + date_parts[2]
|
||||
except:
|
||||
display_date = d_str
|
||||
|
||||
schedule_json = row.get("schedule_json")
|
||||
|
||||
if d_str not in trend_data[op]:
|
||||
trend_data[op][d_str] = {"sums": [0.0] * 24, "counts": [0] * 24}
|
||||
|
||||
series = extract_hourly_prices_from_schedule(schedule_json)
|
||||
for i in range(24):
|
||||
v = series[i]
|
||||
|
||||
for hour in range(24):
|
||||
v = series[hour]
|
||||
if v is not None:
|
||||
trend_data[op][d_str]["sums"][i] += float(v)
|
||||
trend_data[op][d_str]["counts"][i] += 1
|
||||
# 使用原始日期 YYYY-MM-DD 用于排序,显示时由前端或后端格式化
|
||||
# 这里我们构造一个带补全的时间字符串,方便自然排序
|
||||
dt_key = f"{d_str} {hour:02d}:00"
|
||||
if dt_key not in trend_data[op]:
|
||||
trend_data[op][dt_key] = {"sum": 0.0, "count": 0, "display": f"{display_date} {hour:02d}:00"}
|
||||
trend_data[op][dt_key]["sum"] += float(v)
|
||||
trend_data[op][dt_key]["count"] += 1
|
||||
|
||||
# 转换为 ECharts 友好格式
|
||||
# 1. 获取所有日期并排序
|
||||
all_dates = sorted(list(set(str(row.get("date_str")) for row in rows)))
|
||||
# 1. 获取所有时间点并排序
|
||||
all_time_keys = set()
|
||||
for op in operators:
|
||||
all_time_keys.update(trend_data[op].keys())
|
||||
sorted_keys = sorted(list(all_time_keys))
|
||||
|
||||
# 2. 为每个运营商计算每天的平均价格(24小时的平均值)
|
||||
# 2. 提取显示用的标签
|
||||
display_dates = []
|
||||
if sorted_keys:
|
||||
# 从任意一个存在的运营商数据中获取 display 标签
|
||||
first_op = operators[0]
|
||||
for key in sorted_keys:
|
||||
# 找到包含该 key 的 display 标签
|
||||
label = key # fallback
|
||||
for op in operators:
|
||||
if key in trend_data[op]:
|
||||
label = trend_data[op][key]["display"]
|
||||
break
|
||||
display_dates.append(label)
|
||||
|
||||
# 3. 为每个运营商构建完整的时间序列数据
|
||||
series_result = []
|
||||
for op in operators:
|
||||
op_trend = []
|
||||
for d in all_dates:
|
||||
if d in trend_data[op]:
|
||||
day_stats = trend_data[op][d]
|
||||
day_avg_sum = 0.0
|
||||
day_hour_count = 0
|
||||
for i in range(24):
|
||||
if day_stats["counts"][i] > 0:
|
||||
day_avg_sum += (day_stats["sums"][i] / day_stats["counts"][i])
|
||||
day_hour_count += 1
|
||||
|
||||
if day_hour_count > 0:
|
||||
op_trend.append(round(day_avg_sum / day_hour_count, 4))
|
||||
else:
|
||||
op_trend.append(None)
|
||||
op_data = []
|
||||
for key in sorted_keys:
|
||||
if key in trend_data[op]:
|
||||
stats = trend_data[op][key]
|
||||
op_data.append(round(stats["sum"] / stats["count"], 4))
|
||||
else:
|
||||
op_trend.append(None)
|
||||
series_result.append({"name": op, "data": op_trend})
|
||||
op_data.append(None)
|
||||
series_result.append({"name": op, "data": op_data})
|
||||
|
||||
return {
|
||||
"dates": all_dates,
|
||||
"dates": display_dates,
|
||||
"series": series_result
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
|
||||
Binary file not shown.
@@ -10,7 +10,7 @@
|
||||
--success-color: #10b981;
|
||||
--table-header-bg: #0f172a;
|
||||
--table-row-hover: #334155;
|
||||
--scrollbar-thumb: #475569;
|
||||
--scrollbar-thumb: #334155;
|
||||
--scrollbar-track: #0f172a;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ body {
|
||||
|
||||
.ai-box {
|
||||
flex: 1;
|
||||
background-color: rgba(15, 23, 42, 0.3);
|
||||
background-color: #243347;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--card-border);
|
||||
padding: 16px;
|
||||
@@ -240,6 +240,7 @@ body {
|
||||
color: #cbd5e1;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Markdown & LaTeX Rendering */
|
||||
@@ -249,6 +250,7 @@ body {
|
||||
line-height: 1.8;
|
||||
word-wrap: break-word;
|
||||
color: #cbd5e1;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3,
|
||||
@@ -257,37 +259,40 @@ body {
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: var(--text-primary);
|
||||
color: var(--text-primary) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body h1 { font-size: 1.8em; border-bottom: 1px solid var(--card-border); padding-bottom: 0.3em; }
|
||||
.markdown-body h2 { font-size: 1.4em; border-bottom: 1px solid var(--card-border); padding-bottom: 0.3em; }
|
||||
.markdown-body h3 { font-size: 1.2em; }
|
||||
|
||||
.markdown-body p { margin-top: 0; margin-bottom: 16px; }
|
||||
.markdown-body p { margin-top: 0; margin-bottom: 16px; background-color: transparent !important; }
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
padding-left: 2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body li { margin: 0.25em 0; }
|
||||
.markdown-body li { margin: 0.25em 0; background-color: transparent !important; }
|
||||
|
||||
.markdown-body strong { font-weight: 600; color: #fff; }
|
||||
.markdown-body strong { font-weight: 600; color: #fff !important; background-color: transparent !important; }
|
||||
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
color: #94a3b8;
|
||||
border-left: 0.25em solid #3b82f6;
|
||||
margin: 0 0 16px 0;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(148, 163, 184, 0.1);
|
||||
background-color: rgba(59, 130, 246, 0.1) !important;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
@@ -303,11 +308,7 @@ body {
|
||||
border-collapse: collapse;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
background-color: rgba(15, 23, 42, 0.8);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
@@ -316,13 +317,18 @@ body {
|
||||
border: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
background-color: #334155 !important;
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
background-color: transparent;
|
||||
background-color: transparent !important;
|
||||
border-top: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: rgba(30, 41, 59, 0.3);
|
||||
background-color: rgba(51, 65, 85, 0.3) !important;
|
||||
}
|
||||
|
||||
/* LaTeX Styles */
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>分时电价分析 - 驿来特AI智能大脑</title>
|
||||
<link rel="stylesheet" href="css/dashboard.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown-dark.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div v-if="summaryLoading && !summaryText" style="text-align: center; padding: 20px; color: #6b7280;">
|
||||
<p>⚡ AI正在阅读您的知识库并提炼精华,请稍候...</p>
|
||||
</div>
|
||||
<div class="markdown-body" v-html="renderedSummary" style="font-size: 15px; line-height: 1.8; color: #1f2937;"></div>
|
||||
<div class="markdown-body" v-html="renderedSummary"></div>
|
||||
<span v-if="summaryLoading && summaryText" class="cursor-blink">|</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -164,9 +164,41 @@
|
||||
<script src="js/element-plus.zh-cn.min.js"></script>
|
||||
<script src="js/axios.min.js"></script>
|
||||
<!-- Markdown & LaTeX Support -->
|
||||
<link rel="stylesheet" href="https://gcore.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<script src="https://gcore.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<script src="https://gcore.jsdelivr.net/npm/marked@12.0.0/lib/marked.umd.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/github-markdown-css@5.5.1/github-markdown-dark.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/katex@0.16.9/dist/katex.min.css">
|
||||
<script src="https://unpkg.com/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<script src="https://unpkg.com/marked@12.0.0/lib/marked.umd.js"></script>
|
||||
<style>
|
||||
/* 适配对话框中的 Markdown 样式 */
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 15px;
|
||||
background-color: transparent !important;
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
.el-dialog {
|
||||
background-color: #1e293b !important;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
.el-dialog__title {
|
||||
color: #f1f5f9 !important;
|
||||
}
|
||||
.el-dialog__body {
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
.cursor-blink {
|
||||
color: #3b82f6;
|
||||
font-weight: bold;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
from, to { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
<script src="js/douyin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -194,12 +194,37 @@ createApp({
|
||||
textStyle: { color: "#94a3b8" },
|
||||
top: 10
|
||||
},
|
||||
grid: { left: 50, right: 30, top: 60, bottom: 40 },
|
||||
grid: { left: 50, right: 30, top: 60, bottom: 60 },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.dates.map(d => d.split('-').slice(1).join('/')), // Show MM/DD
|
||||
axisLine: { lineStyle: { color: "#475569" } },
|
||||
axisLabel: { color: "#94a3b8" }
|
||||
type: 'category',
|
||||
data: data.dates,
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: '#94a3b8',
|
||||
interval: function(index, value) {
|
||||
// 强制显示 00:00, 06:00, 12:00, 18:00
|
||||
if (value && value.includes(' ')) {
|
||||
const time = value.split(' ')[1];
|
||||
return time === '00:00' || time === '06:00' || time === '12:00' || time === '18:00';
|
||||
}
|
||||
return index % 6 === 0; // 每6个点显示一个(对应 6 小时)
|
||||
},
|
||||
formatter: function(value) {
|
||||
if (!value) return '';
|
||||
const parts = value.split(' ');
|
||||
if (parts.length === 2) {
|
||||
const time = parts[1];
|
||||
// 如果是零点,显示日期和时间
|
||||
if (time === '00:00') {
|
||||
return parts[0] + '\n' + time;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#334155' } },
|
||||
axisTick: { show: true, lineStyle: { color: '#334155' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
|
||||
@@ -62,43 +62,41 @@ createApp({
|
||||
if (!text) return '';
|
||||
|
||||
try {
|
||||
// 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $)
|
||||
let processedText = text;
|
||||
|
||||
// 处理块级 LaTeX: $$ ... $$
|
||||
processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => {
|
||||
try {
|
||||
if (typeof katex !== 'undefined') {
|
||||
// 1. 处理 LaTeX
|
||||
if (typeof katex !== 'undefined') {
|
||||
// 处理块级 LaTeX: $$ ... $$
|
||||
processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => {
|
||||
try {
|
||||
return '<div class="katex-block">' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '</div>';
|
||||
}
|
||||
return match;
|
||||
} catch (e) {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
|
||||
// 处理行内 LaTeX: $ ... $
|
||||
processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => {
|
||||
try {
|
||||
if (typeof katex !== 'undefined') {
|
||||
// 处理行内 LaTeX: $ ... $
|
||||
processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => {
|
||||
try {
|
||||
return katex.renderToString(formula, { displayMode: false, throwOnError: false });
|
||||
}
|
||||
return match;
|
||||
} catch (e) {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
} catch (e) { return match; }
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 使用 marked 解析 Markdown
|
||||
if (typeof marked !== 'undefined') {
|
||||
// marked v12+ 使用 marked.parse
|
||||
return marked.parse(processedText);
|
||||
} else {
|
||||
// 降级使用之前的 simpleMarkdown
|
||||
// 降级使用 simpleMarkdown
|
||||
return simpleMarkdown(processedText);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Markdown/LaTeX rendering error:', e);
|
||||
return text;
|
||||
console.error('Markdown rendering error:', e);
|
||||
// 最后的兜底:如果 marked 报错,尝试用 simpleMarkdown
|
||||
try {
|
||||
return simpleMarkdown(text);
|
||||
} catch (e2) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user