962 lines
43 KiB
JavaScript
962 lines
43 KiB
JavaScript
/* 无人机资产管理系统 — 前端逻辑(原生 JS,无依赖) */
|
||
"use strict";
|
||
|
||
const API = "";
|
||
|
||
// 动态状态定义(由 /api/status-types 加载)
|
||
let statusTypes = []; // [{name, color, is_builtin}]
|
||
let statusMap = {}; // name -> color
|
||
|
||
async function loadStatusTypes() {
|
||
statusTypes = await api("/api/status-types");
|
||
statusMap = {};
|
||
for (const s of statusTypes) statusMap[s.name] = s.color;
|
||
fillStatusFilter();
|
||
}
|
||
|
||
function fillStatusFilter() {
|
||
const opts = `<option value="">全部状态</option>` +
|
||
statusTypes.map((s) => `<option value="${esc(s.name)}">${esc(s.name)}</option>`).join("") +
|
||
`<option value="__none__">无状态</option>`;
|
||
const sel = document.getElementById("status-filter");
|
||
if (sel) sel.innerHTML = opts;
|
||
const selM = document.getElementById("matrix-status");
|
||
if (selM) selM.innerHTML = opts;
|
||
}
|
||
|
||
function badgeColor(status) {
|
||
return statusMap[status] || "#9e9e9e";
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 工具
|
||
async function api(path, options = {}) {
|
||
const resp = await fetch(API + path, {
|
||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||
...options,
|
||
});
|
||
if (resp.status === 204) return null;
|
||
const data = await resp.json().catch(() => null);
|
||
if (!resp.ok) {
|
||
const msg = (data && data.detail) || `请求失败 (${resp.status})`;
|
||
throw new Error(msg);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||
}[c]));
|
||
}
|
||
|
||
function badge(status) {
|
||
if (!status) return `<span class="badge empty">无状态</span>`;
|
||
return `<span class="badge" style="background:${esc(badgeColor(status))}">${esc(status)}</span>`;
|
||
}
|
||
|
||
function toast(msg, isError = false) {
|
||
let el = document.querySelector(".toast");
|
||
if (!el) {
|
||
el = document.createElement("div");
|
||
el.className = "toast";
|
||
document.body.appendChild(el);
|
||
}
|
||
el.textContent = msg;
|
||
el.classList.toggle("error", isError);
|
||
el.classList.add("show");
|
||
clearTimeout(el._t);
|
||
el._t = setTimeout(() => el.classList.remove("show"), 2600);
|
||
}
|
||
|
||
// 弹窗管理:一个容器反复复用
|
||
function openModal(title, bodyHTML, footerHTML = "") {
|
||
const mask = document.getElementById("modal");
|
||
mask.innerHTML = `
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2>${esc(title)}</h2>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">${bodyHTML}</div>
|
||
${footerHTML ? `<div class="modal-footer">${footerHTML}</div>` : ""}
|
||
</div>`;
|
||
mask.classList.add("open");
|
||
}
|
||
function closeModal() {
|
||
document.getElementById("modal").classList.remove("open");
|
||
}
|
||
document.addEventListener("click", (e) => {
|
||
if (e.target.id === "modal") closeModal();
|
||
});
|
||
|
||
// ---------------------------------------------------------------- Tab
|
||
function switchTab(name) {
|
||
document.querySelectorAll(".tab").forEach((b) =>
|
||
b.classList.toggle("active", b.dataset.tab === name)
|
||
);
|
||
document.querySelectorAll("main.view").forEach((v) =>
|
||
v.classList.toggle("active", v.id === "view-" + name)
|
||
);
|
||
if (name === "matrix") renderMatrix();
|
||
if (name === "flight") renderFlightMatrix();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 无人机列表
|
||
let currentModelFilter = "";
|
||
|
||
function buildDronesQuery() {
|
||
const p = [];
|
||
const m = document.getElementById("model-filter").value;
|
||
const s = document.getElementById("serial-filter").value.trim();
|
||
const f = document.getElementById("fc-filter").value.trim();
|
||
const st = document.getElementById("status-filter").value;
|
||
if (m) p.push("model=" + encodeURIComponent(m));
|
||
if (s) p.push("serial_no=" + encodeURIComponent(s));
|
||
if (f) p.push("fc_id=" + encodeURIComponent(f));
|
||
if (st) p.push("status=" + encodeURIComponent(st));
|
||
return p.length ? "?" + p.join("&") : "";
|
||
}
|
||
|
||
function resetDronesFilter() {
|
||
document.getElementById("model-filter").value = "";
|
||
document.getElementById("serial-filter").value = "";
|
||
document.getElementById("fc-filter").value = "";
|
||
document.getElementById("status-filter").value = "";
|
||
currentModelFilter = "";
|
||
loadDrones();
|
||
}
|
||
let dronesCache = [];
|
||
|
||
let globalModels = [];
|
||
|
||
async function loadModels() {
|
||
const models = await api("/api/drones/models");
|
||
globalModels = models;
|
||
const sel = document.getElementById("model-filter");
|
||
sel.innerHTML = `<option value="">全部机型</option>` +
|
||
models.map((m) => `<option value="${esc(m)}">${esc(m)}</option>`).join("");
|
||
const selM = document.getElementById("matrix-model");
|
||
selM.innerHTML = models.map((m) => `<option value="${esc(m)}">${esc(m)}</option>`).join("");
|
||
const selF = document.getElementById("flight-model");
|
||
selF.innerHTML = models.map((m) => `<option value="${esc(m)}">${esc(m)}</option>`).join("");
|
||
}
|
||
|
||
async function loadDrones() {
|
||
currentModelFilter = document.getElementById("model-filter").value;
|
||
dronesCache = await api("/api/drones" + buildDronesQuery());
|
||
renderDrones();
|
||
}
|
||
|
||
function renderDrones() {
|
||
const wrap = document.getElementById("drones-body");
|
||
if (!dronesCache.length) {
|
||
wrap.innerHTML = `<div class="empty-tip">暂无无人机,点击右上角「新增无人机」添加</div>`;
|
||
return;
|
||
}
|
||
wrap.innerHTML = "";
|
||
const table = document.createElement("table");
|
||
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>
|
||
</tr></thead><tbody></tbody>`;
|
||
const tbodyEl = table.querySelector("tbody");
|
||
for (const d of dronesCache) {
|
||
const tr = document.createElement("tr");
|
||
const st = d.current_status ? d.current_status.status : "";
|
||
const lastRemark = d.current_status ? d.current_status.remark : "";
|
||
tr.innerHTML = `
|
||
<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>`;
|
||
tbodyEl.appendChild(tr);
|
||
}
|
||
wrap.innerHTML = "";
|
||
wrap.appendChild(table);
|
||
const ca = document.getElementById("check-all");
|
||
if (ca) {
|
||
ca.onchange = () => {
|
||
document.querySelectorAll(".drone-check").forEach((c) => (c.checked = ca.checked));
|
||
};
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 新增 / 编辑
|
||
function openCreateModal() {
|
||
openModal(
|
||
"新增无人机",
|
||
`
|
||
<div class="form-row"><label>机型 *</label><input id="f-model" type="text" placeholder="如 DJI Mavic 3"></div>
|
||
<div class="form-row"><label>编号 *</label><input id="f-serial" type="text" placeholder="唯一编号,如 UAV-001"></div>
|
||
<div class="form-row"><label>链路ID</label><input id="f-link" type="text"></div>
|
||
<div class="form-row"><label>飞控ID</label><input id="f-fc" type="text"></div>
|
||
<div class="form-row"><label>飞控版本号</label><input id="f-fcv" type="text"></div>
|
||
<div class="form-row"><label>导航版本号</label><input id="f-nav" type="text"></div>
|
||
<div class="form-row"><label>备注</label><textarea id="f-remark"></textarea></div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitCreate()">保存</button>`
|
||
);
|
||
}
|
||
|
||
async function submitCreate() {
|
||
const body = {
|
||
model: document.getElementById("f-model").value,
|
||
serial_no: document.getElementById("f-serial").value,
|
||
link_id: document.getElementById("f-link").value,
|
||
fc_id: document.getElementById("f-fc").value,
|
||
fc_version: document.getElementById("f-fcv").value,
|
||
nav_version: document.getElementById("f-nav").value,
|
||
remark: document.getElementById("f-remark").value,
|
||
};
|
||
if (!body.model.trim() || !body.serial_no.trim()) return toast("机型与编号不能为空", true);
|
||
try {
|
||
await api("/api/drones", { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast("新增成功");
|
||
await Promise.all([loadModels(), loadDrones()]);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
function openEditModal(id) {
|
||
const d = dronesCache.find((x) => x.id === id);
|
||
if (!d) return;
|
||
openModal(
|
||
`编辑无人机 ${d.serial_no}`,
|
||
`
|
||
<div class="form-row"><label>机型</label><input id="f-model" type="text" value="${esc(d.model)}"></div>
|
||
<div class="form-row"><label>链路ID</label><input id="f-link" type="text" value="${esc(d.link_id)}"></div>
|
||
<div class="form-row"><label>飞控ID</label><input id="f-fc" type="text" value="${esc(d.fc_id)}"></div>
|
||
<div class="form-row"><label>飞控版本号</label><input id="f-fcv" type="text" value="${esc(d.fc_version)}"></div>
|
||
<div class="form-row"><label>导航版本号</label><input id="f-nav" type="text" value="${esc(d.nav_version)}"></div>
|
||
<div class="form-row"><label>备注</label><textarea id="f-remark">${esc(d.remark)}</textarea></div>
|
||
<div style="font-size:12px;color:#7a8699">编号为唯一标识,新增时设定,此处不可修改。</div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitEdit(${id})">保存</button>`
|
||
);
|
||
}
|
||
|
||
async function submitEdit(id) {
|
||
const body = {
|
||
model: document.getElementById("f-model").value,
|
||
link_id: document.getElementById("f-link").value,
|
||
fc_id: document.getElementById("f-fc").value,
|
||
fc_version: document.getElementById("f-fcv").value,
|
||
nav_version: document.getElementById("f-nav").value,
|
||
remark: document.getElementById("f-remark").value,
|
||
};
|
||
try {
|
||
await api(`/api/drones/${id}`, { method: "PUT", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast("已保存");
|
||
await Promise.all([loadModels(), loadDrones()]);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
function confirmDelete(id) {
|
||
const d = dronesCache.find((x) => x.id === id);
|
||
if (!d) return;
|
||
openModal(
|
||
"删除确认",
|
||
`<p style="font-size:14px">确定删除编号 <strong>${esc(d.serial_no)}</strong> 的无人机吗?<br>
|
||
其全部状态变更记录将一并删除,此操作不可恢复。</p>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary danger" onclick="doDelete(${id})">删除</button>`
|
||
);
|
||
}
|
||
|
||
async function doDelete(id) {
|
||
try {
|
||
await api(`/api/drones/${id}`, { method: "DELETE" });
|
||
closeModal();
|
||
toast("已删除");
|
||
await Promise.all([loadModels(), loadDrones()]);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 修改状态
|
||
function openStatusModal(id) {
|
||
const d = dronesCache.find((x) => x.id === id);
|
||
if (!d) return;
|
||
const cur = d.current_status ? d.current_status.status : "";
|
||
openModal(
|
||
`修改状态 — ${d.serial_no}`,
|
||
`
|
||
<div class="form-row"><label>当前状态</label><div>${badge(cur)}</div></div>
|
||
<div class="form-row"><label>新状态 *</label>
|
||
<select id="s-status">
|
||
${statusTypes.map((s) => `<option value="${esc(s.name)}" ${s.name === cur ? "selected" : ""}>${esc(s.name)}</option>`).join("")}
|
||
</select>
|
||
</div>
|
||
<div class="form-row"><label>变更日期时间(可改,用于补充历史记录)</label>
|
||
<input type="datetime-local" id="s-time" value="${nowLocal()}">
|
||
<div style="font-size:12px;color:#7a8699;margin-top:4px">默认当前时间;如需补充补录某天的状态,改成对应日期时间即可。</div>
|
||
</div>
|
||
<div class="form-row"><label>本次备注(可选)</label><textarea id="s-remark" placeholder="如:检修完成、入库等"></textarea></div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitStatus(${id})">提交</button>`
|
||
);
|
||
}
|
||
|
||
async function submitStatus(id) {
|
||
const body = {
|
||
status: document.getElementById("s-status").value,
|
||
remark: document.getElementById("s-remark").value,
|
||
};
|
||
const t = document.getElementById("s-time").value;
|
||
if (t) body.changed_at = t.replace("T", " ") + ":00";
|
||
try {
|
||
await api(`/api/drones/${id}/status`, { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast("状态已更新");
|
||
await loadDrones();
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
function nowLocal() {
|
||
const d = new Date();
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 批量修改状态
|
||
function getSelectedDroneIds() {
|
||
return Array.from(document.querySelectorAll(".drone-check:checked")).map((c) => parseInt(c.value, 10));
|
||
}
|
||
|
||
function openBatchStatusModal() {
|
||
const ids = getSelectedDroneIds();
|
||
if (!ids.length) return toast("请先勾选需要修改状态的无人机", true);
|
||
const opts = statusTypes.map((s) => `<option value="${esc(s.name)}">${esc(s.name)}</option>`).join("");
|
||
openModal(
|
||
`批量修改状态(${ids.length} 架)`,
|
||
`
|
||
<div class="form-row"><label>新状态 *</label><select id="bs-status">${opts}</select></div>
|
||
<div class="form-row"><label>变更日期时间(可改,用于补充历史记录)</label>
|
||
<input type="datetime-local" id="bs-time" value="${nowLocal()}">
|
||
<div style="font-size:12px;color:#7a8699;margin-top:4px">默认当前时间;如需批量补录历史,改为对应日期时间即可。</div>
|
||
</div>
|
||
<div class="form-row"><label>本次备注(可选,写入每一架的记录)</label>
|
||
<textarea id="bs-remark" placeholder="统一备注,如:批量入库"></textarea>
|
||
</div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitBatchStatus()">提交</button>`
|
||
);
|
||
}
|
||
|
||
async function submitBatchStatus() {
|
||
const ids = getSelectedDroneIds();
|
||
const body = {
|
||
ids,
|
||
status: document.getElementById("bs-status").value,
|
||
remark: document.getElementById("bs-remark").value,
|
||
};
|
||
const t = document.getElementById("bs-time").value;
|
||
if (t) body.changed_at = t.replace("T", " ") + ":00";
|
||
try {
|
||
const r = await api("/api/drones/status/batch", { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast(`已更新 ${r.updated.length} 架` + (r.missing.length ? `,${r.missing.length} 架未找到` : ""));
|
||
await loadDrones();
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 批量修改飞行
|
||
function openBatchEditFlightModal() {
|
||
const ids = getSelectedDroneIds();
|
||
if (!ids.length) return toast("请先勾选需要修改飞行记录的无人机", true);
|
||
openModal(
|
||
`批量修改飞行(${ids.length} 架)`,
|
||
`
|
||
<div style="font-size:13px;color:#7a8699;margin-bottom:10px">
|
||
按日期统一修改这些飞机「该日期」的飞行记录;留空的字段不修改。
|
||
</div>
|
||
<div class="form-row"><label>飞行日期 *</label><input id="bef-date" type="date"></div>
|
||
<div class="form-row"><label>飞行时长(分钟,留空不改)</label><input id="bef-dur" type="number" min="0" placeholder="如:60"></div>
|
||
<div class="form-row"><label>起飞地点(留空不改)</label><input id="bef-takeoff" type="text" placeholder="如:场A"></div>
|
||
<div class="form-row"><label>降落地点(留空不改)</label><input id="bef-landing" type="text" placeholder="如:场B"></div>
|
||
<div class="form-row"><label>备注(留空不改)</label><input id="bef-remark" type="text" placeholder="如:批量调整"></div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitBatchEditFlight()">提交</button>`
|
||
);
|
||
}
|
||
|
||
async function submitBatchEditFlight() {
|
||
const ids = getSelectedDroneIds();
|
||
const date = document.getElementById("bef-date").value;
|
||
if (!date) return toast("请选择飞行日期", true);
|
||
const dur = document.getElementById("bef-dur").value;
|
||
const takeoff = document.getElementById("bef-takeoff").value;
|
||
const landing = document.getElementById("bef-landing").value;
|
||
const remark = document.getElementById("bef-remark").value;
|
||
const body = { ids, flight_date: date };
|
||
if (dur !== "") body.duration_min = parseInt(dur, 10);
|
||
if (takeoff !== "") body.takeoff = takeoff;
|
||
if (landing !== "") body.landing = landing;
|
||
if (remark !== "") body.remark = remark;
|
||
if (!("duration_min" in body) && !("takeoff" in body) && !("landing" in body) && !("remark" in body)) {
|
||
return toast("请至少填写一个要修改的字段", true);
|
||
}
|
||
try {
|
||
const r = await api("/api/flights/batch-update", { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast(`已修改 ${r.updated_records} 条飞行记录`);
|
||
await loadDrones();
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 批量删除
|
||
function openBatchDeleteModal() {
|
||
const ids = getSelectedDroneIds();
|
||
if (!ids.length) return toast("请先勾选需要操作的无人机", true);
|
||
openModal(
|
||
`批量删除(${ids.length} 架)`,
|
||
`
|
||
<div class="form-row"><label>删除方式</label>
|
||
<select id="bd-mode" onchange="toggleBdDate()">
|
||
<option value="flights-all">删除这些飞机的全部飞行记录</option>
|
||
<option value="flights-date">删除这些飞机指定日期的飞行记录</option>
|
||
<option value="drones">删除整架无人机(含状态与飞行记录)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row" id="bd-date-row" style="display:none">
|
||
<label>飞行日期 *</label><input id="bd-date" type="date">
|
||
</div>
|
||
<div style="font-size:13px;color:#dc2626;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:10px">
|
||
警告:删除操作不可恢复!整机删除会连同其状态历史与飞行记录一并删除。
|
||
</div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary danger" onclick="submitBatchDelete()">确认删除</button>`
|
||
);
|
||
}
|
||
|
||
function toggleBdDate() {
|
||
const mode = document.getElementById("bd-mode").value;
|
||
document.getElementById("bd-date-row").style.display = mode === "flights-date" ? "block" : "none";
|
||
}
|
||
|
||
async function submitBatchDelete() {
|
||
const ids = getSelectedDroneIds();
|
||
const mode = document.getElementById("bd-mode").value;
|
||
if (!confirm("确认执行批量删除?此操作不可恢复!")) return;
|
||
try {
|
||
if (mode === "drones") {
|
||
const r = await api("/api/drones/batch-delete", { method: "POST", body: JSON.stringify({ ids }) });
|
||
closeModal();
|
||
toast(`已删除 ${r.deleted.length} 架` + (r.not_found.length ? `,${r.not_found.length} 架未找到` : ""));
|
||
await Promise.all([loadModels(), loadDrones(), loadStatusTypes()]);
|
||
} else {
|
||
const body = { ids, mode: mode === "flights-date" ? "date" : "all" };
|
||
if (mode === "flights-date") {
|
||
const date = document.getElementById("bd-date").value;
|
||
if (!date) return toast("请选择飞行日期", true);
|
||
body.flight_date = date;
|
||
}
|
||
const r = await api("/api/flights/batch-delete", { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast(`已删除 ${r.deleted_records} 条飞行记录`);
|
||
await loadDrones();
|
||
}
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 状态管理
|
||
async function openStatusManager() {
|
||
await loadStatusTypes();
|
||
const rows = statusTypes.map((s) => `
|
||
<div class="sm-row" style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #eef1f5">
|
||
<input type="color" data-action="color" data-name="${esc(s.name)}" value="${esc(s.color)}"
|
||
style="width:34px;height:26px;border:none;padding:0;cursor:pointer" title="点击修改颜色">
|
||
<span class="badge" style="background:${esc(s.color)}">${esc(s.name)}</span>
|
||
${s.is_builtin
|
||
? '<span style="font-size:12px;color:#7a8699">内置</span>'
|
||
: `<button class="small" data-action="rename" data-name="${esc(s.name)}">改名</button>
|
||
<button class="small danger" data-action="delete" data-name="${esc(s.name)}">删除</button>`}
|
||
</div>`).join("");
|
||
openModal(
|
||
"状态管理",
|
||
`
|
||
<div style="margin-bottom:14px;font-size:13px;color:#7a8699">
|
||
状态用于「修改状态」下拉及透视表/历史展示;点击颜色块可修改颜色,内置状态不可删除,已被记录使用的状态不可删除。
|
||
</div>
|
||
<div class="sm-list" style="max-height:300px;overflow-y:auto">${rows || '<div class="empty-tip">暂无状态</div>'}</div>
|
||
<div style="margin-top:16px;border-top:1px solid #eef1f5;padding-top:14px">
|
||
<div class="form-row" style="margin-bottom:0">
|
||
<label>新增自定义状态</label>
|
||
<div style="display:flex;gap:10px;align-items:center">
|
||
<input id="sm-name" type="text" placeholder="状态名称,如:巡检中" style="flex:1">
|
||
<input id="sm-color" type="color" value="#3b82f6" style="width:44px;height:32px;border:none;padding:0;cursor:pointer">
|
||
<button class="primary" onclick="createStatus()">添加</button>
|
||
</div>
|
||
</div>
|
||
</div>`,
|
||
`<button class="primary" onclick="closeModal()">关闭</button>`
|
||
);
|
||
// 颜色修改
|
||
document.querySelectorAll("#modal input[data-action=color]").forEach((inp) => {
|
||
inp.addEventListener("change", async () => {
|
||
try {
|
||
await api(`/api/status-types/${encodeURIComponent(inp.dataset.name)}`, {
|
||
method: "PUT", body: JSON.stringify({ color: inp.value }),
|
||
});
|
||
toast("颜色已更新");
|
||
await loadStatusTypes(); await loadDrones(); openStatusManager();
|
||
} catch (e) { toast(e.message, true); }
|
||
});
|
||
});
|
||
// 改名 / 删除
|
||
document.querySelectorAll("#modal [data-action=rename], #modal [data-action=delete]").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
const name = btn.dataset.name;
|
||
try {
|
||
if (btn.dataset.action === "rename") {
|
||
const newName = prompt(`将状态「${name}」改名为:`, name);
|
||
if (newName === null) return;
|
||
if (!newName.trim()) return toast("名称不能为空", true);
|
||
await api(`/api/status-types/${encodeURIComponent(name)}`, {
|
||
method: "PUT", body: JSON.stringify({ name: newName.trim() }),
|
||
});
|
||
toast("已改名");
|
||
} else {
|
||
if (!confirm(`确定删除状态「${name}」吗?`)) return;
|
||
await api(`/api/status-types/${encodeURIComponent(name)}`, { method: "DELETE" });
|
||
toast("已删除");
|
||
}
|
||
await loadStatusTypes(); await loadDrones(); openStatusManager();
|
||
} catch (e) { toast(e.message, true); }
|
||
});
|
||
});
|
||
}
|
||
|
||
async function createStatus() {
|
||
const name = document.getElementById("sm-name").value;
|
||
const color = document.getElementById("sm-color").value;
|
||
if (!name.trim()) return toast("请输入状态名称", true);
|
||
try {
|
||
await api("/api/status-types", { method: "POST", body: JSON.stringify({ name, color }) });
|
||
toast("状态已添加");
|
||
await loadStatusTypes(); await loadDrones(); openStatusManager();
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 历史
|
||
async function showHistory(id) {
|
||
const data = await api(`/api/drones/${id}/history`);
|
||
const d = data.drone;
|
||
const items = data.history.length
|
||
? data.history.map((h) => `
|
||
<div class="tl-item">
|
||
<span class="dot" style="background:${badgeColor(h.status)}"></span>
|
||
<div class="tl-time">${esc(h.changed_at)}</div>
|
||
<div style="flex:1">
|
||
<span class="badge" style="background:${esc(badgeColor(h.status))}">${esc(h.status)}</span>
|
||
${h.remark ? `<div class="tl-remark">${esc(h.remark)}</div>` : ""}
|
||
</div>
|
||
<button class="small" data-action="edit-status" data-id="${h.id}">编辑</button>
|
||
<button class="small danger" data-action="del-status" data-id="${h.id}">删除</button>
|
||
</div>`).join("")
|
||
: `<div class="empty-tip">暂无状态变更记录</div>`;
|
||
openModal(
|
||
`状态历史 — ${d.serial_no}(${esc(d.model)})`,
|
||
`<div class="timeline">${items}</div>`,
|
||
`<button class="primary" onclick="closeModal()">关闭</button>`
|
||
);
|
||
document.querySelectorAll("#modal [data-action=del-status]").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
if (!confirm("确定删除这条状态记录吗?删除后当前状态取剩余最新一条。")) return;
|
||
try {
|
||
await api(`/api/drones/${id}/history/${btn.dataset.id}`, { method: "DELETE" });
|
||
toast("已删除");
|
||
await loadDrones();
|
||
showHistory(id);
|
||
} catch (e) { toast(e.message, true); }
|
||
});
|
||
});
|
||
document.querySelectorAll("#modal [data-action=edit-status]").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const rec = data.history.find((x) => x.id === parseInt(btn.dataset.id, 10));
|
||
if (rec) openEditStatusModal(id, rec);
|
||
});
|
||
});
|
||
}
|
||
|
||
function toLocalInput(iso) {
|
||
return iso ? iso.slice(0, 16) : "";
|
||
}
|
||
|
||
function openEditStatusModal(id, rec) {
|
||
const opts = statusTypes.map((s) =>
|
||
`<option value="${esc(s.name)}" ${s.name === rec.status ? "selected" : ""}>${esc(s.name)}</option>`
|
||
).join("");
|
||
const d = dronesCache.find((x) => x.id === id);
|
||
openModal(
|
||
`编辑状态记录 — ${d ? d.serial_no : id}`,
|
||
`
|
||
<div class="form-row"><label>状态 *</label><select id="es-status">${opts}</select></div>
|
||
<div class="form-row"><label>变更日期时间</label>
|
||
<input type="datetime-local" id="es-time" value="${toLocalInput(rec.changed_at)}">
|
||
</div>
|
||
<div class="form-row"><label>备注</label><input id="es-remark" type="text" value="${esc(rec.remark)}"></div>`,
|
||
`<button onclick="showHistory(${id})">返回</button><button class="primary" onclick="saveStatusRecord(${id}, ${rec.id})">保存</button>`
|
||
);
|
||
}
|
||
|
||
async function saveStatusRecord(id, recordId) {
|
||
const body = {
|
||
status: document.getElementById("es-status").value,
|
||
remark: document.getElementById("es-remark").value,
|
||
};
|
||
const t = document.getElementById("es-time").value;
|
||
if (t) body.changed_at = t.replace("T", " ") + ":00";
|
||
try {
|
||
await api(`/api/drones/${id}/history/${recordId}`, { method: "PUT", body: JSON.stringify(body) });
|
||
toast("已保存");
|
||
await loadDrones();
|
||
showHistory(id);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Excel 导出
|
||
function openExportModal() {
|
||
const items = globalModels.length
|
||
? globalModels.map((m) => `
|
||
<label style="display:flex;align-items:center;gap:8px;padding:6px 4px;cursor:pointer">
|
||
<input type="checkbox" class="ex-model" value="${esc(m)}"> <span>${esc(m)}</span>
|
||
</label>`).join("")
|
||
: `<div class="empty-tip">暂无机型,请先添加无人机</div>`;
|
||
openModal(
|
||
"导出 Excel",
|
||
`
|
||
<div style="margin-bottom:10px;font-size:13px;color:#7a8699">
|
||
<div style="margin-bottom:6px">
|
||
选择机型(不勾选则导出全部):
|
||
<a href="javascript:void(0)" onclick="toggleExModels(true)" style="margin-left:10px">全选</a>
|
||
<a href="javascript:void(0)" onclick="toggleExModels(false)" style="margin-left:8px">清空</a>
|
||
</div>
|
||
<div style="max-height:200px;overflow-y:auto;border:1px solid #eef1f5;border-radius:8px;padding:6px 10px">
|
||
${items}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<label style="font-size:13px;color:#7a8699">日期范围(可选):</label>
|
||
<input type="date" id="ex-start">
|
||
<span style="color:#7a8699">至</span>
|
||
<input type="date" id="ex-end">
|
||
</div>
|
||
<div style="margin-top:10px;font-size:12px;color:#7a8699">
|
||
未选日期:导出「无人机台账 + 状态历史」;选择日期:导出「每日状态矩阵(行=日期、列=编号)+ 变更明细 + 台账」。
|
||
</div>`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="doExportExcel()">导出</button>`
|
||
);
|
||
}
|
||
|
||
function toggleExModels(checked) {
|
||
document.querySelectorAll(".ex-model").forEach((c) => (c.checked = checked));
|
||
}
|
||
|
||
function doExportExcel() {
|
||
const selected = Array.from(document.querySelectorAll(".ex-model:checked")).map((c) => c.value);
|
||
const start = document.getElementById("ex-start").value;
|
||
const end = document.getElementById("ex-end").value;
|
||
if ((start && !end) || (end && !start)) return toast("开始与结束日期需同时选择", true);
|
||
if (start && end && start > end) return toast("开始日期不能晚于结束日期", true);
|
||
const parts = [];
|
||
for (const m of selected) parts.push("models=" + encodeURIComponent(m));
|
||
if (start && end) { parts.push("start=" + start); parts.push("end=" + end); }
|
||
window.location.href = API + "/api/export-excel" + (parts.length ? "?" + parts.join("&") : "");
|
||
closeModal();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 飞行历史
|
||
async function openFlightModal(id) {
|
||
const data = await api(`/api/drones/${id}/flights`);
|
||
const d = data.drone;
|
||
const items = data.flights.length
|
||
? data.flights.map((f) => `
|
||
<div class="tl-item" style="border-left-color:#3b82f6">
|
||
<span class="dot" style="background:#3b82f6"></span>
|
||
<div class="tl-time">${esc(f.flight_date)}</div>
|
||
<div style="flex:1">
|
||
<strong>${f.duration_min} 分钟</strong>
|
||
<div class="tl-remark">${esc(f.takeoff)} → ${esc(f.landing)}${f.remark ? " | " + esc(f.remark) : ""}</div>
|
||
</div>
|
||
<button class="small" onclick="editFlight(${id}, ${f.id})">编辑</button>
|
||
<button class="small danger" data-action="del-flight" data-id="${f.id}">删除</button>
|
||
</div>`).join("")
|
||
: `<div class="empty-tip">暂无飞行记录</div>`;
|
||
openModal(
|
||
`飞行历史 — ${d.serial_no}(${esc(d.model)})`,
|
||
`
|
||
<div style="max-height:280px;overflow-y:auto">${items}</div>
|
||
<div style="margin-top:14px;border-top:1px solid #eef1f5;padding-top:14px">
|
||
<div style="font-size:13px;color:#7a8699;margin-bottom:8px">新增飞行记录</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||
<input id="fl-date" type="date">
|
||
<input id="fl-dur" type="number" min="0" placeholder="时长(分钟)">
|
||
<input id="fl-takeoff" type="text" placeholder="起飞地点">
|
||
<input id="fl-landing" type="text" placeholder="降落地点">
|
||
</div>
|
||
<input id="fl-remark" type="text" placeholder="备注(可选)" style="margin-top:8px;width:100%">
|
||
</div>`,
|
||
`<button onclick="closeModal()">关闭</button><button class="primary" onclick="createFlight(${id})">添加飞行记录</button>`
|
||
);
|
||
document.querySelectorAll("#modal [data-action=del-flight]").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
if (!confirm("确定删除这条飞行记录吗?")) return;
|
||
try {
|
||
await api(`/api/drones/${id}/flights/${btn.dataset.id}`, { method: "DELETE" });
|
||
toast("已删除");
|
||
openFlightModal(id);
|
||
} catch (e) { toast(e.message, true); }
|
||
});
|
||
});
|
||
}
|
||
|
||
async function createFlight(id) {
|
||
const body = {
|
||
flight_date: document.getElementById("fl-date").value,
|
||
duration_min: parseInt(document.getElementById("fl-dur").value || "0", 10),
|
||
takeoff: document.getElementById("fl-takeoff").value,
|
||
landing: document.getElementById("fl-landing").value,
|
||
remark: document.getElementById("fl-remark").value,
|
||
};
|
||
if (!body.flight_date) return toast("请选择飞行日期", true);
|
||
try {
|
||
await api(`/api/drones/${id}/flights`, { method: "POST", body: JSON.stringify(body) });
|
||
toast("已添加");
|
||
openFlightModal(id);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
async function editFlight(id, flightId) {
|
||
const data = await api(`/api/drones/${id}/flights`);
|
||
const f = data.flights.find((x) => x.id === flightId);
|
||
if (!f) return;
|
||
openModal(
|
||
`编辑飞行记录 — ${data.drone.serial_no}`,
|
||
`
|
||
<div class="form-row"><label>飞行日期</label><input id="ef-date" type="date" value="${esc(f.flight_date)}"></div>
|
||
<div class="form-row"><label>飞行时长(分钟)</label><input id="ef-dur" type="number" min="0" value="${f.duration_min}"></div>
|
||
<div class="form-row"><label>起飞地点</label><input id="ef-takeoff" type="text" value="${esc(f.takeoff)}"></div>
|
||
<div class="form-row"><label>降落地点</label><input id="ef-landing" type="text" value="${esc(f.landing)}"></div>
|
||
<div class="form-row"><label>备注</label><input id="ef-remark" type="text" value="${esc(f.remark)}"></div>`,
|
||
`<button onclick="openFlightModal(${id})">返回</button><button class="primary" onclick="saveFlight(${id}, ${flightId})">保存</button>`
|
||
);
|
||
}
|
||
|
||
async function saveFlight(id, flightId) {
|
||
const body = {
|
||
flight_date: document.getElementById("ef-date").value,
|
||
duration_min: parseInt(document.getElementById("ef-dur").value || "0", 10),
|
||
takeoff: document.getElementById("ef-takeoff").value,
|
||
landing: document.getElementById("ef-landing").value,
|
||
remark: document.getElementById("ef-remark").value,
|
||
};
|
||
if (!body.flight_date) return toast("请选择飞行日期", true);
|
||
try {
|
||
await api(`/api/drones/${id}/flights/${flightId}`, { method: "PUT", body: JSON.stringify(body) });
|
||
toast("已保存");
|
||
openFlightModal(id);
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
function openBatchFlightModal() {
|
||
const items = dronesCache.length
|
||
? dronesCache.map((d) => `
|
||
<label style="display:flex;align-items:center;gap:8px;padding:5px 4px;cursor:pointer">
|
||
<input type="checkbox" class="bf-serial" value="${esc(d.serial_no)}">
|
||
<span>${esc(d.serial_no)}</span>
|
||
<span style="color:#7a8699;font-size:12px">(${esc(d.model)})</span>
|
||
</label>`).join("")
|
||
: `<div class="empty-tip">暂无无人机</div>`;
|
||
openModal(
|
||
"统一添加飞行历史",
|
||
`
|
||
<div style="margin-bottom:8px;font-size:13px;color:#7a8699">
|
||
勾选飞机编号(列表为当前机型筛选结果),填写飞行信息后统一添加,对应飞机自动同步:
|
||
<a href="javascript:void(0)" onclick="toggleBatch(true)" style="margin-left:10px">全选</a>
|
||
<a href="javascript:void(0)" onclick="toggleBatch(false)" style="margin-left:8px">清空</a>
|
||
</div>
|
||
<div style="max-height:180px;overflow-y:auto;border:1px solid #eef1f5;border-radius:8px;padding:6px 10px">${items}</div>
|
||
<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||
<input id="bf-date" type="date">
|
||
<input id="bf-dur" type="number" min="0" placeholder="时长(分钟)">
|
||
<input id="bf-takeoff" type="text" placeholder="起飞地点">
|
||
<input id="bf-landing" type="text" placeholder="降落地点">
|
||
</div>
|
||
<input id="bf-remark" type="text" placeholder="备注(可选)" style="margin-top:8px;width:100%">`,
|
||
`<button onclick="closeModal()">取消</button><button class="primary" onclick="submitBatchFlight()">统一添加</button>`
|
||
);
|
||
}
|
||
|
||
function toggleBatch(checked) {
|
||
document.querySelectorAll(".bf-serial").forEach((c) => (c.checked = checked));
|
||
}
|
||
|
||
async function submitBatchFlight() {
|
||
const serials = Array.from(document.querySelectorAll(".bf-serial:checked")).map((c) => c.value);
|
||
const body = {
|
||
serial_nos: serials,
|
||
flight_date: document.getElementById("bf-date").value,
|
||
duration_min: parseInt(document.getElementById("bf-dur").value || "0", 10),
|
||
takeoff: document.getElementById("bf-takeoff").value,
|
||
landing: document.getElementById("bf-landing").value,
|
||
remark: document.getElementById("bf-remark").value,
|
||
};
|
||
if (!serials.length) return toast("请至少勾选一架飞机", true);
|
||
if (!body.flight_date) return toast("请选择飞行日期", true);
|
||
try {
|
||
const r = await api("/api/flights/batch", { method: "POST", body: JSON.stringify(body) });
|
||
closeModal();
|
||
toast(`已为 ${r.added.length} 架添加飞行记录` + (r.missing.length ? `,${r.missing.length} 个编号未找到` : ""));
|
||
} catch (e) { toast(e.message, true); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 透视表
|
||
async function renderMatrix() {
|
||
const model = document.getElementById("matrix-model").value;
|
||
const start = document.getElementById("matrix-start").value;
|
||
const end = document.getElementById("matrix-end").value;
|
||
const box = document.getElementById("matrix-box");
|
||
if (!model) {
|
||
box.innerHTML = `<div class="empty-tip">请先在「无人机管理」中添加无人机,然后选择机型查看透视表</div>`;
|
||
return;
|
||
}
|
||
let q = `?model=${encodeURIComponent(model)}`;
|
||
const ms = document.getElementById("matrix-serial").value.trim();
|
||
const mf = document.getElementById("matrix-fc").value.trim();
|
||
const mst = document.getElementById("matrix-status").value;
|
||
if (ms) q += `&serial_no=${encodeURIComponent(ms)}`;
|
||
if (mf) q += `&fc_id=${encodeURIComponent(mf)}`;
|
||
if (mst) q += `&status=${encodeURIComponent(mst)}`;
|
||
if (start) q += `&start=${start}`;
|
||
if (end) q += `&end=${end}`;
|
||
box.innerHTML = `<div class="empty-tip">加载中…</div>`;
|
||
const m = await api("/api/status-matrix" + q);
|
||
if (!m.drones.length) {
|
||
box.innerHTML = `<div class="empty-tip">该机型下暂无无人机</div>`;
|
||
return;
|
||
}
|
||
const thead = `<tr><th class="corner">日期 \ 编号</th>` +
|
||
m.drones.map((d) => `<th>${esc(d.serial_no)}</th>`).join("") + `</tr>`;
|
||
const tbody = m.dates.map((day, i) => {
|
||
const cells = m.drones.map((d) => {
|
||
const st = d.statuses[i];
|
||
const rm = d.remarks[i];
|
||
return st
|
||
? `<td><span class="cell" style="background:${badgeColor(st)}">${esc(st)}${rm ? `<span class="cell-remark">${esc(rm)}</span>` : ""}</span></td>`
|
||
: `<td><span class="cell empty-cell">—</span></td>`;
|
||
}).join("");
|
||
return `<tr><td>${esc(day)}</td>${cells}</tr>`;
|
||
}).join("");
|
||
box.innerHTML = `
|
||
<div style="margin-bottom:10px;font-size:13px;color:#7a8699">
|
||
机型:<strong>${esc(m.model)}</strong> | 日期范围:${esc(m.start)} ~ ${esc(m.end)}
|
||
(共 ${m.dates.length} 天,${m.drones.length} 架)
|
||
</div>
|
||
<div class="table-wrap matrix-wrap">
|
||
<table id="matrix-table"><thead>${thead}</thead><tbody>${tbody}</tbody></table>
|
||
</div>`;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 飞行透视表
|
||
async function renderFlightMatrix() {
|
||
const model = document.getElementById("flight-model").value;
|
||
const start = document.getElementById("flight-start").value;
|
||
const end = document.getElementById("flight-end").value;
|
||
const box = document.getElementById("flight-box");
|
||
if (!model) {
|
||
box.innerHTML = `<div class="empty-tip">请先在「无人机管理」中添加无人机,然后选择机型查看飞行透视表</div>`;
|
||
return;
|
||
}
|
||
let q = `?model=${encodeURIComponent(model)}`;
|
||
const fs2 = document.getElementById("flight-serial").value.trim();
|
||
const ff = document.getElementById("flight-fc").value.trim();
|
||
if (fs2) q += `&serial_no=${encodeURIComponent(fs2)}`;
|
||
if (ff) q += `&fc_id=${encodeURIComponent(ff)}`;
|
||
if (start) q += `&start=${start}`;
|
||
if (end) q += `&end=${end}`;
|
||
box.innerHTML = `<div class="empty-tip">加载中…</div>`;
|
||
const m = await api("/api/flight-matrix" + q);
|
||
if (!m.drones.length) {
|
||
box.innerHTML = `<div class="empty-tip">该机型下暂无无人机</div>`;
|
||
return;
|
||
}
|
||
const thead = `<tr><th class="corner">日期 \ 编号</th>` +
|
||
m.drones.map((d) => `<th>${esc(d.serial_no)}</th>`).join("") + `</tr>`;
|
||
const tbody = m.dates.map((day, i) => {
|
||
const cells = m.drones.map((d) => {
|
||
const c = d.counts[i];
|
||
const rm = d.remarks[i];
|
||
return c
|
||
? `<td><span class="cell" style="background:#3b82f6">${c} 次${rm ? `<span class="cell-remark">${esc(rm)}</span>` : ""}</span></td>`
|
||
: `<td><span class="cell empty-cell">—</span></td>`;
|
||
}).join("");
|
||
return `<tr><td>${esc(day)}</td>${cells}</tr>`;
|
||
}).join("");
|
||
// 总架次统计行(按筛选日期范围汇总)
|
||
const totals = m.drones.map((d) => d.counts.reduce((a, b) => a + b, 0));
|
||
const totalRow = `<tr style="border-top:2px solid #c3cbd6">
|
||
<td style="font-weight:700;background:#eef1f5">总架次</td>` +
|
||
totals.map((t) =>
|
||
t
|
||
? `<td style="background:#eef1f5"><span class="cell" style="background:#64748b">${t} 次</span></td>`
|
||
: `<td style="background:#eef1f5"><span class="cell empty-cell">—</span></td>`
|
||
).join("") + `</tr>`;
|
||
box.innerHTML = `
|
||
<div style="margin-bottom:10px;font-size:13px;color:#7a8699">
|
||
机型:<strong>${esc(m.model)}</strong> | 日期范围:${esc(m.start)} ~ ${esc(m.end)}
|
||
(共 ${m.dates.length} 天,${m.drones.length} 架)
|
||
</div>
|
||
<div class="table-wrap matrix-wrap">
|
||
<table id="matrix-table"><thead>${thead}</thead><tbody>${totalRow}${tbody}</tbody></table>
|
||
</div>`;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 初始化
|
||
function initMatrixDefaults() {
|
||
const now = new Date();
|
||
document.getElementById("matrix-end").value = now.toISOString().slice(0, 10);
|
||
const start = new Date(now);
|
||
start.setDate(start.getDate() - 29);
|
||
document.getElementById("matrix-start").value = start.toISOString().slice(0, 10);
|
||
}
|
||
|
||
async function init() {
|
||
document.getElementById("tab-drones").onclick = () => switchTab("drones");
|
||
document.getElementById("tab-matrix").onclick = () => switchTab("matrix");
|
||
document.getElementById("tab-flight").onclick = () => switchTab("flight");
|
||
document.getElementById("add-drone").onclick = openCreateModal;
|
||
document.getElementById("status-manager").onclick = openStatusManager;
|
||
document.getElementById("export-excel").onclick = openExportModal;
|
||
document.getElementById("batch-flight").onclick = openBatchFlightModal;
|
||
document.getElementById("batch-status").onclick = openBatchStatusModal;
|
||
document.getElementById("batch-edit-flight").onclick = openBatchEditFlightModal;
|
||
document.getElementById("batch-delete").onclick = openBatchDeleteModal;
|
||
document.getElementById("model-filter").onchange = () => loadDrones();
|
||
document.getElementById("apply-filter").onclick = loadDrones;
|
||
document.getElementById("reset-filter").onclick = resetDronesFilter;
|
||
document.getElementById("matrix-query").onclick = renderMatrix;
|
||
document.getElementById("flight-query").onclick = renderFlightMatrix;
|
||
initMatrixDefaults();
|
||
document.getElementById("flight-end").value = new Date().toISOString().slice(0, 10);
|
||
const fs = new Date(); fs.setDate(fs.getDate() - 29);
|
||
document.getElementById("flight-start").value = fs.toISOString().slice(0, 10);
|
||
try {
|
||
await Promise.all([loadModels(), loadDrones(), loadStatusTypes()]);
|
||
const hashTab = location.hash.replace("#", "");
|
||
if (["matrix", "drones", "flight"].includes(hashTab)) switchTab(hashTab);
|
||
} catch (e) {
|
||
toast("加载数据失败:" + e.message, true);
|
||
}
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|