/* 无人机资产管理系统 — 前端逻辑(原生 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 = `` +
statusTypes.map((s) => ``).join("") +
``;
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 `无状态`;
return `${esc(status)}`;
}
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 = `
${bodyHTML}
${footerHTML ? `` : ""}
`;
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 = `` +
models.map((m) => ``).join("");
const selM = document.getElementById("matrix-model");
selM.innerHTML = models.map((m) => ``).join("");
const selF = document.getElementById("flight-model");
selF.innerHTML = models.map((m) => ``).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 = `暂无无人机,点击右上角「新增无人机」添加
`;
return;
}
wrap.innerHTML = "";
const table = document.createElement("table");
table.innerHTML = `
|
机型 | 编号 | 链路ID | 飞控ID |
飞控版本 | 导航版本 |
当前状态 | 备注 | 操作 |
`;
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 = `
|
${esc(d.model)} |
${esc(d.serial_no)} |
${esc(d.link_id)} |
${esc(d.fc_id)} |
${esc(d.fc_version)} |
${esc(d.nav_version)} |
${badge(st)}${lastRemark ? ` ${esc(lastRemark)} ` : ""} |
| `;
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(
"新增无人机",
`
`,
``
);
}
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}`,
`
编号为唯一标识,新增时设定,此处不可修改。
`,
``
);
}
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(
"删除确认",
`确定删除编号 ${esc(d.serial_no)} 的无人机吗?
其全部状态变更记录将一并删除,此操作不可恢复。
`,
``
);
}
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}`,
`
`,
``
);
}
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) => ``).join("");
openModal(
`批量修改状态(${ids.length} 架)`,
`
`,
``
);
}
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} 架)`,
`
按日期统一修改这些飞机「该日期」的飞行记录;留空的字段不修改。
`,
``
);
}
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} 架)`,
`
警告:删除操作不可恢复!整机删除会连同其状态历史与飞行记录一并删除。
`,
``
);
}
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) => `
${esc(s.name)}
${s.is_builtin
? '内置'
: `
`}
`).join("");
openModal(
"状态管理",
`
状态用于「修改状态」下拉及透视表/历史展示;点击颜色块可修改颜色,内置状态不可删除,已被记录使用的状态不可删除。
`,
``
);
// 颜色修改
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) => `
${esc(h.changed_at)}
${esc(h.status)}
${h.remark ? `` : ""}
`).join("")
: `暂无状态变更记录
`;
openModal(
`状态历史 — ${d.serial_no}(${esc(d.model)})`,
`${items}
`,
``
);
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) =>
``
).join("");
const d = dronesCache.find((x) => x.id === id);
openModal(
`编辑状态记录 — ${d ? d.serial_no : id}`,
`
`,
``
);
}
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) => `
`).join("")
: `暂无机型,请先添加无人机
`;
openModal(
"导出 Excel",
`
至
未选日期:导出「无人机台账 + 状态历史」;选择日期:导出「每日状态矩阵(行=日期、列=编号)+ 变更明细 + 台账」。
`,
``
);
}
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) => `
${esc(f.flight_date)}
${f.duration_min} 分钟
`).join("")
: `暂无飞行记录
`;
openModal(
`飞行历史 — ${d.serial_no}(${esc(d.model)})`,
`
${items}
`,
``
);
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}`,
`
`,
``
);
}
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) => `
`).join("")
: `暂无无人机
`;
openModal(
"统一添加飞行历史",
`
勾选飞机编号(列表为当前机型筛选结果),填写飞行信息后统一添加,对应飞机自动同步:
全选
清空
${items}
`,
``
);
}
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 = `请先在「无人机管理」中添加无人机,然后选择机型查看透视表
`;
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 = `加载中…
`;
const m = await api("/api/status-matrix" + q);
if (!m.drones.length) {
box.innerHTML = `该机型下暂无无人机
`;
return;
}
const thead = `| 日期 \ 编号 | ` +
m.drones.map((d) => `${esc(d.serial_no)} | `).join("") + `
`;
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
? `${esc(st)}${rm ? `` : ""} | `
: `— | `;
}).join("");
return `| ${esc(day)} | ${cells}
`;
}).join("");
box.innerHTML = `
机型:${esc(m.model)} | 日期范围:${esc(m.start)} ~ ${esc(m.end)}
(共 ${m.dates.length} 天,${m.drones.length} 架)
`;
}
// ---------------------------------------------------------------- 飞行透视表
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 = `请先在「无人机管理」中添加无人机,然后选择机型查看飞行透视表
`;
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 = `加载中…
`;
const m = await api("/api/flight-matrix" + q);
if (!m.drones.length) {
box.innerHTML = `该机型下暂无无人机
`;
return;
}
const thead = `| 日期 \ 编号 | ` +
m.drones.map((d) => `${esc(d.serial_no)} | `).join("") + `
`;
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
? `${c} 次${rm ? `` : ""} | `
: `— | `;
}).join("");
return `| ${esc(day)} | ${cells}
`;
}).join("");
// 总架次统计行(按筛选日期范围汇总)
const totals = m.drones.map((d) => d.counts.reduce((a, b) => a + b, 0));
const totalRow = `
| 总架次 | ` +
totals.map((t) =>
t
? `${t} 次 | `
: `— | `
).join("") + `
`;
box.innerHTML = `
机型:${esc(m.model)} | 日期范围:${esc(m.start)} ~ ${esc(m.end)}
(共 ${m.dates.length} 天,${m.drones.length} 架)
${thead}${totalRow}${tbody}
`;
}
// ---------------------------------------------------------------- 初始化
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);