@@ -0,0 +1,961 @@
/* 无人机资产管理系统 — 前端逻辑(原生 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 ) ;