Compare commits

...

3 Commits

9 changed files with 104 additions and 53 deletions
+5
View File
@@ -35,3 +35,8 @@ htmlcov/
# ===== 操作系统 =====
.DS_Store
Thumbs.db
# ===== 工具状态 / 临时文件 =====
.reasonix/
cmd_out.txt
uav.db.zip
@@ -1,8 +0,0 @@
{
"topic_20260807-122045_ad70a5c6e85a19b4": {
"stage": 3,
"userTurns": 3,
"basisHash": "67a0aadd52e2dfe4",
"updatedAt": 1786107445489
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"topic_20260807-122045_ad70a5c6e85a19b4": 1786105245321
}
@@ -1,3 +0,0 @@
{
"topic_20260807-122045_ad70a5c6e85a19b4": "auto"
}
-3
View File
@@ -1,3 +0,0 @@
{
"topic_20260807-122045_ad70a5c6e85a19b4": "我想建立一个无人机资产管理库,采用B…"
}
+1 -1
View File
@@ -14,7 +14,7 @@ BS 架构的无人机资产管理库:管理无人机基本信息(机型、
- **飞行透视表**:行=日期、列=编号,单元格=当日飞行次数,支持机型与日期范围筛选
- **按机型透视表**:行 = 日期、列 = 飞机编号、单元格 = 当日状态;无变更日期自动沿用前一状态;支持自定义日期范围(默认近 30 天)
- **状态颜色区分**:待组装=灰、待调试=橙、待飞=绿、损坏=红
- **手动导出 Excel**:点击「导出 Excel」按钮,下载 `无人机台账.xlsx`(含"无人机台账"与"状态历史"两个工作表),数据以点击时刻为准
- **手动导出 Excel**:点击「导出 Excel」按钮,下载 `无人机台账.xlsx`;不含日期时导出「无人机台账(含每架飞行次数)+ 状态历史 + 飞行历史」,选择日期时导出「每日状态矩阵 + 状态变更明细 + 无人机台账 + 飞行历史」;飞行历史 sheet 行=飞机编号、首列=飞行次数总计、后列=每日飞行记录;数据以点击时刻为准
## 技术栈
-18
View File
@@ -1,18 +0,0 @@
============================================
无人机资产管理系统 - 一键启动
============================================
[1/2] 检查依赖 ...
[2/2] 启动服务: http://127.0.0.1:8000
错误: 不支持输入重新定向,立即退出此进程。
服务已启动,浏览器将自动打开页面。
局域网内其他电脑可访问: http://本机IP:8000
关闭本窗口即停止服务。
INFO: Started server process [26368]
INFO: Waiting for application startup.
INFO: Application startup complete.
ERROR: [Errno 10048] error while attempting to bind on address ('0.0.0.0', 8000): 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
+90 -8
View File
@@ -251,6 +251,39 @@ def _current_status(conn: sqlite3.Connection, drone_id: int) -> Optional[dict]:
# ---------------------------------------------------------------- Excel 台账
def _flights_day_map(flight_rows) -> dict:
"""将飞行记录行按 (drone_id -> {date: [摘要]}) 分组,同日多条保留。"""
day_map: dict = {}
for f in flight_rows:
parts = []
if f["duration_min"]:
parts.append(f"{f['duration_min']}分钟")
if f["takeoff"] or f["landing"]:
parts.append(f"{f['takeoff']}→{f['landing']}")
if f["remark"]:
parts.append(f["remark"])
summary = " ".join(parts) if parts else "飞行"
day_map.setdefault(f["drone_id"], {}).setdefault(f["flight_date"], []).append(summary)
return day_map
def _write_flight_history_sheet(ws, rows, dates, day_map) -> None:
"""写入「飞行历史」sheet:行=飞机编号,首列=次数总计,后列=每日飞行记录。"""
ws.append(["飞机编号", "飞行次数总计"] + list(dates))
for r in rows:
dm = day_map.get(r["id"], {})
total = sum(len(v) for v in dm.values())
row_vals = [r["serial_no"], total]
for ds in dates:
row_vals.append("\n".join(dm.get(ds, [])))
ws.append(row_vals)
# 每日单元格换行显示
for row in ws.iter_rows(min_row=2, min_col=3):
for cell in row:
if cell.value:
cell.alignment = Alignment(vertical="center", wrap_text=True)
def export_to_excel(
models: Optional[List[str]] = None,
start: Optional[str] = None,
@@ -361,16 +394,31 @@ def export_to_excel(
# ---- Sheet3: 无人机台账 ----
ws3 = wb.create_sheet("无人机台账")
ws3.append(["机型", "编号", "链路ID", "飞控ID", "飞控版本",
"导航版本", "当前状态", "备注", "创建时间"])
"导航版本", "当前状态", "飞行次数", "备注", "创建时间"])
for r in rows:
cnt = conn.execute(
"SELECT COUNT(*) FROM flight_records "
"WHERE drone_id = ? AND flight_date BETWEEN ? AND ?",
(r["id"], dates[0], dates[-1]),
).fetchone()[0]
ws3.append([
r["model"], r["serial_no"], r["link_id"], r["fc_id"],
r["fc_version"], r["nav_version"],
r["cur_status"] or "", r["remark"], r["created_at"],
r["cur_status"] or "", cnt, r["remark"], r["created_at"],
])
sheets = [ws, ws2, ws3]
# ---- Sheet4: 飞行历史(范围内) ----
flight_rows = conn.execute(
"SELECT f.drone_id, f.flight_date, f.duration_min, f.takeoff, f.landing, f.remark "
"FROM flight_records f JOIN drones d ON d.id = f.drone_id "
f"{where} AND f.flight_date BETWEEN ? AND ? "
"ORDER BY f.drone_id, f.flight_date, f.id",
params + (dates[0], dates[-1]),
).fetchall()
ws4 = wb.create_sheet("飞行历史")
_write_flight_history_sheet(ws4, rows, dates, _flights_day_map(flight_rows))
sheets = [ws, ws2, ws3, ws4]
else:
# ---- 无日期:台账 + 状态历史 ----
# ---- 无日期:台账 + 状态历史 + 飞行历史 ----
statuses = conn.execute(
"SELECT d.serial_no, d.model, s.status, s.remark, s.changed_at "
"FROM drone_status s JOIN drones d ON d.id = s.drone_id "
@@ -378,22 +426,47 @@ def export_to_excel(
"ORDER BY s.changed_at, s.id",
params,
).fetchall()
# 全部飞行记录(用于次数统计与飞行历史 sheet)
flight_rows = conn.execute(
"SELECT f.drone_id, f.flight_date, f.duration_min, f.takeoff, f.landing, f.remark "
"FROM flight_records f JOIN drones d ON d.id = f.drone_id "
f"{where} ORDER BY f.drone_id, f.flight_date, f.id",
params,
).fetchall()
if flight_rows:
d0 = date.fromisoformat(min(f["flight_date"] for f in flight_rows))
d1 = date.fromisoformat(max(f["flight_date"] for f in flight_rows))
dates = []
d = d0
while d <= d1:
dates.append(d.isoformat())
d += timedelta(days=1)
else:
dates = []
# 每机每日飞行摘要
day_map = _flights_day_map(flight_rows)
ws = wb.active
ws.title = "无人机台账"
ws.append(["机型", "编号", "链路ID", "飞控ID", "飞控版本",
"导航版本", "当前状态", "备注", "创建时间"])
"导航版本", "当前状态", "飞行次数", "备注", "创建时间"])
for r in rows:
cnt = conn.execute(
"SELECT COUNT(*) FROM flight_records WHERE drone_id = ?",
(r["id"],),
).fetchone()[0]
ws.append([
r["model"], r["serial_no"], r["link_id"], r["fc_id"],
r["fc_version"], r["nav_version"],
r["cur_status"] or "", r["remark"], r["created_at"],
r["cur_status"] or "", cnt, r["remark"], r["created_at"],
])
ws2 = wb.create_sheet("状态历史")
ws2.append(["编号", "机型", "状态", "备注", "变更时间"])
for s in statuses:
ws2.append([s["serial_no"], s["model"], s["status"],
s["remark"], s["changed_at"]])
sheets = [ws, ws2]
ws4 = wb.create_sheet("飞行历史")
_write_flight_history_sheet(ws4, rows, dates, day_map)
sheets = [ws, ws2, ws4]
# ---- 样式 ----
for sheet in sheets:
@@ -1141,4 +1214,13 @@ def export_excel_api(
# ---------------------------------------------------------------- 静态页面
app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
class NoCacheStaticFiles(StaticFiles):
"""静态文件禁用缓存:每次请求都让浏览器重新校验(避免改版后看不到更新)。"""
def file_response(self, *args, **kwargs):
resp = super().file_response(*args, **kwargs)
resp.headers["Cache-Control"] = "no-cache"
return resp
app.mount("/", NoCacheStaticFiles(directory=STATIC_DIR, html=True), name="static")
+8 -9
View File
@@ -158,9 +158,8 @@ function renderDrones() {
table.innerHTML = `
<thead><tr>
<th style="width:34px"><input type="checkbox" id="check-all" title="全选/取消全选"></th>
<th>机型</th><th>编号</th><th>链路ID</th><th>飞控ID</th>
<th>飞控版本</th><th>导航版本</th>
<th>当前状态</th><th>备注</th><th>操作</th>
<th>机型</th><th>编号</th><th>当前状态</th><th>操作</th>
<th>飞控ID</th><th>链路ID</th><th>飞控版本</th><th>导航版本</th><th>备注</th>
</tr></thead><tbody></tbody>`;
const tbodyEl = table.querySelector("tbody");
for (const d of dronesCache) {
@@ -171,19 +170,19 @@ function renderDrones() {
<td><input type="checkbox" class="drone-check" value="${d.id}"></td>
<td>${esc(d.model)}</td>
<td><strong>${esc(d.serial_no)}</strong></td>
<td>${esc(d.link_id)}</td>
<td>${esc(d.fc_id)}</td>
<td>${esc(d.fc_version)}</td>
<td>${esc(d.nav_version)}</td>
<td>${badge(st)}${lastRemark ? `<div style="font-size:12px;color:#7a8699;margin-top:2px">${esc(lastRemark)}</div>` : ""}</td>
<td class="remark-cell">${esc(d.remark)}</td>
<td><div class="actions">
<button class="small" onclick="showHistory(${d.id})">历史</button>
<button class="small" onclick="openFlightModal(${d.id})">飞行</button>
<button class="small primary" onclick="openStatusModal(${d.id})">修改状态</button>
<button class="small" onclick="openEditModal(${d.id})">编辑</button>
<button class="small danger" onclick="confirmDelete(${d.id})">删除</button>
</div></td>`;
</div></td>
<td>${esc(d.fc_id)}</td>
<td>${esc(d.link_id)}</td>
<td>${esc(d.fc_version)}</td>
<td>${esc(d.nav_version)}</td>
<td class="remark-cell">${esc(d.remark)}</td>`;
tbodyEl.appendChild(tr);
}
wrap.innerHTML = "";