/* 无人机资产管理系统 — 前端逻辑(原生 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 = ` `; 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)} ${badge(st)}${lastRemark ? `
${esc(lastRemark)}
` : ""}
${esc(d.fc_id)} ${esc(d.link_id)} ${esc(d.fc_version)} ${esc(d.nav_version)} ${esc(d.remark)}`; 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}`, `
${badge(cur)}
默认当前时间;如需补充补录某天的状态,改成对应日期时间即可。
`, `` ); } 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( "状态管理", `
状态用于「修改状态」下拉及透视表/历史展示;点击颜色块可修改颜色,内置状态不可删除,已被记录使用的状态不可删除。
${rows || '
暂无状态
'}
`, `` ); // 颜色修改 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 ? `
${esc(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", `
选择机型(不勾选则导出全部): 全选 清空
${items}
至
未选日期:导出「无人机台账 + 状态历史」;选择日期:导出「每日状态矩阵(行=日期、列=编号)+ 变更明细 + 台账」。
`, `` ); } 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} 分钟
${esc(f.takeoff)} → ${esc(f.landing)}${f.remark ? " | " + esc(f.remark) : ""}
`).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 ? `${esc(rm)}` : ""}` : `—`; }).join(""); return `${esc(day)}${cells}`; }).join(""); box.innerHTML = `
机型:${esc(m.model)} | 日期范围:${esc(m.start)} ~ ${esc(m.end)} (共 ${m.dates.length} 天,${m.drones.length} 架)
${thead}${tbody}
`; } // ---------------------------------------------------------------- 飞行透视表 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 ? `${esc(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);