feat: 添加网页进行访问查询
This commit is contained in:
32
app.py
Normal file
32
app.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from models.ammeter_usage import AmmeterUsageModel
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
# 添加模板过滤器:时间戳转日期显示
|
||||||
|
def format_datetime(value):
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(value / 1000).strftime("%Y-%m-%d %H:%M")
|
||||||
|
except:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# 🔥关键:为 Jinja2 添加过滤器
|
||||||
|
templates.env.filters["datetime"] = format_datetime
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request):
|
||||||
|
data = await AmmeterUsageModel.get_recent(6462, days=30)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{"request": request, "room_id": 6462, "data": data},
|
||||||
|
)
|
||||||
7
main.py
7
main.py
@@ -8,6 +8,7 @@ from models.ammeter_usage import AmmeterUsageModel
|
|||||||
from scheduler import start_scheduler
|
from scheduler import start_scheduler
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", message="Glyph .* missing from font")
|
warnings.filterwarnings("ignore", message="Glyph .* missing from font")
|
||||||
|
|
||||||
@@ -32,8 +33,10 @@ async def main():
|
|||||||
|
|
||||||
# await SD().push()
|
# await SD().push()
|
||||||
|
|
||||||
while True:
|
# 🔥启动FastAPI Web服务
|
||||||
await asyncio.sleep(3600)
|
config = uvicorn.Config("app:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class AmmeterUsageModel:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def insert(cls, data: dict):
|
async def insert(cls, data: dict):
|
||||||
print(data)
|
|
||||||
async with get_conn() as conn:
|
async with get_conn() as conn:
|
||||||
conn.row_factory = aiosqlite.Row
|
conn.row_factory = aiosqlite.Row
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
@@ -53,3 +52,68 @@ class AmmeterUsageModel:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def exec(cls, sql: str, *args, **kwargs):
|
||||||
|
async with get_conn() as conn:
|
||||||
|
conn.row_factory = aiosqlite.Row
|
||||||
|
await conn.execute(sql, *args, **kwargs)
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_recent(cls, room_id: int, days: int = 30):
|
||||||
|
"""查询最近N天数据"""
|
||||||
|
now = int(time.time() * 1000)
|
||||||
|
start = now - days * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
async with get_conn() as conn:
|
||||||
|
conn.row_factory = aiosqlite.Row
|
||||||
|
cur = await conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT created_at, left_ele, left_free_ele, left_money
|
||||||
|
FROM ammeter_usage
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND created_at >= ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""",
|
||||||
|
(room_id, start),
|
||||||
|
)
|
||||||
|
rows = await cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_range(cls, room_id: int, start: int, end: int):
|
||||||
|
"""按时间区间查询"""
|
||||||
|
async with get_conn() as conn:
|
||||||
|
conn.row_factory = aiosqlite.Row
|
||||||
|
cur = await conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM ammeter_usage
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND created_at BETWEEN ? AND ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""",
|
||||||
|
(room_id, start, end),
|
||||||
|
)
|
||||||
|
rows = await cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def list(cls, room_id: int, page: int = 1, limit: int = 20):
|
||||||
|
"""分页查询,用于表格展示"""
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
async with get_conn() as conn:
|
||||||
|
conn.row_factory = aiosqlite.Row
|
||||||
|
cur = await conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM ammeter_usage
|
||||||
|
WHERE room_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""",
|
||||||
|
(room_id, limit, offset),
|
||||||
|
)
|
||||||
|
rows = await cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ def plot_line(
|
|||||||
has_free = free_values is not None and len(free_values) > 0
|
has_free = free_values is not None and len(free_values) > 0
|
||||||
|
|
||||||
if has_free:
|
if has_free:
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 6), sharex=True)
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), sharex=True)
|
||||||
else:
|
else:
|
||||||
fig, ax1 = plt.subplots(1, 1, figsize=(16, 4))
|
fig, ax1 = plt.subplots(1, 1, figsize=(14, 6))
|
||||||
ax2 = None
|
ax2 = None
|
||||||
|
|
||||||
# ===== 普通额度 =====
|
# ===== 普通额度 =====
|
||||||
|
|||||||
45
static/js/echarts.min.js
vendored
Normal file
45
static/js/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
96
templates/index.html
Normal file
96
templates/index.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>房间 {{ room_id }} - 用电详情</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- ECharts -->
|
||||||
|
<script src="/static/js/echarts.min.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-100 text-gray-800">
|
||||||
|
|
||||||
|
<div class="max-w-5xl mx-auto p-6 space-y-8">
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold text-center">
|
||||||
|
房间 {{ room_id }} 最近用电情况
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div id="eleChart" class="w-full h-80 bg-white rounded-xl shadow"></div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<div class="bg-white rounded-xl shadow p-6 overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm text-left">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3">记录时间</th>
|
||||||
|
<th class="px-4 py-3">剩余电量 (度)</th>
|
||||||
|
<th class="px-4 py-3">剩余金额 (元)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in data %}
|
||||||
|
<tr class="border-t hover:bg-gray-50 transition">
|
||||||
|
<td class="px-4 py-3">{{ i.created_at | datetime }}</td>
|
||||||
|
<td class="px-4 py-3">{{ i.left_ele }}</td>
|
||||||
|
<td class="px-4 py-3">{{ i.left_money }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/" class="text-blue-600 hover:underline">返回首页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const raw = {{ data | tojson }};
|
||||||
|
const labels = raw.map(i => new Date(i.created_at).toLocaleString());
|
||||||
|
const values = raw.map(i => i.left_ele);
|
||||||
|
|
||||||
|
const chartDom = document.getElementById('eleChart');
|
||||||
|
const myChart = echarts.init(chartDom);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
|
||||||
|
// 🔥支持区域缩放:鼠标拖动 & 下方滑块
|
||||||
|
dataZoom: [
|
||||||
|
{ type: 'inside' }, // 鼠标滚轮缩放 + 拖动
|
||||||
|
{
|
||||||
|
type: 'slider', // 底部滑块
|
||||||
|
height: 20,
|
||||||
|
bottom: 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: labels,
|
||||||
|
boundaryGap: false
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
|
||||||
|
series: [{
|
||||||
|
data: values,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 0.3
|
||||||
|
},
|
||||||
|
lineStyle: { width: 2 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
myChart.setOption(option);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user