Files
shanxia2.0/案例html/index.html
T
2026-10-02 09:28:00 +08:00

904 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>山峡代工一体化平台 · 危化品车辆</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--green: #148a3c;
--green-dark: #0f7331;
--green-soft: #e8f6ec;
--green-line: #b7e0c3;
--ink: #1c2430;
--muted: #6a7380;
--line: #e4e8ee;
--paper: #f3f5f7;
--warn: #e24b4a;
--amber: #c2410c;
--blue: #1d4f91;
--shadow: 0 1px 2px rgba(28, 36, 48, 0.06), 0 8px 24px rgba(28, 36, 48, 0.04);
--sidebar: 200px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", sans-serif;
color: var(--ink); background: var(--paper); font-size: 13px; line-height: 1.5;
}
button, input, select, textarea { font-family: inherit; font-size: 13px; }
button { cursor: pointer; border: 0; background: none; }
.app { min-height: 100%; display: flex; flex-direction: column; }
.topbar {
height: 48px; background: linear-gradient(90deg, #117a36, #17a045 60%, #1bb34d);
color: #fff; display: flex; align-items: center; padding: 0 16px 0 12px; gap: 16px;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; min-width: 210px; }
.logo { width: 28px; height: 28px; border-radius: 7px; background: rgba(255,255,255,.18); display: grid; place-items: center; }
.crumb { opacity: .92; font-size: 12px; }
.crumb b { font-weight: 500; }
.top-search {
margin-left: auto; width: 240px; height: 30px; border-radius: 15px; color: #fff; outline: none; padding: 0 12px 0 32px;
border: 1px solid rgba(255,255,255,.22); background: rgba(255,255,255,.16) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%23fff' stroke-width='2'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath d='M9.5 9.5 L12 12'/%3E%3C/svg%3E") 10px center no-repeat;
}
.top-search::placeholder { color: rgba(255,255,255,.75); }
.avatar { width: 28px; height: 28px; border-radius: 50%; background: #fff; color: var(--green); font-weight: 700; display: grid; place-items: center; }
.shell { display: flex; flex: 1; min-height: 0; }
.sidebar { width: var(--sidebar); background: #fff; border-right: 1px solid var(--line); padding: 12px 8px; flex-shrink: 0; }
.side-item { display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 10px; border-radius: 8px; color: #3d4654; width: 100%; text-align: left; }
.side-item:hover { background: #f4f7f5; }
.side-item.active { background: var(--green-soft); color: var(--green-dark); font-weight: 600; }
.side-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .4; }
.side-item.active .side-dot { opacity: 1; background: var(--green); }
.badge-n { margin-left: auto; min-width: 18px; height: 16px; padding: 0 5px; border-radius: 8px; background: var(--warn); color: #fff; font-size: 10px; display: grid; place-items: center; font-weight: 600; }
.main { flex: 1; padding: 16px 20px 28px; overflow: auto; }
.subnav { display: flex; gap: 4px; background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 4px; margin-bottom: 14px; box-shadow: var(--shadow); flex-wrap: wrap; }
.subnav button { height: 32px; padding: 0 12px; border-radius: 8px; color: #4b5563; font-weight: 500; white-space: nowrap; }
.subnav button.on { background: var(--green-soft); color: var(--green-dark); font-weight: 650; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.page-head h1 { font-size: 18px; font-weight: 650; }
.page-head p { color: var(--muted); margin-top: 4px; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { height: 32px; padding: 0 12px; border-radius: 8px; font-weight: 500; border: 1px solid var(--line); background: #fff; }
.btn:hover { border-color: #c9d2cc; }
.btn-primary { background: var(--green); color: #fff; border-color: var(--green); }
.btn-primary:hover { background: var(--green-dark); }
.alerts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.alert { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 8px; background: #fff7ed; color: var(--amber); border: 1px solid #fed7aa; font-size: 12px; }
.alert.red { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
.alert.ok { background: var(--green-soft); color: var(--green-dark); border-color: var(--green-line); }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 14px; }
.stat { background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; box-shadow: var(--shadow); }
.stat .k { color: var(--muted); font-size: 12px; }
.stat .v { font-size: 22px; font-weight: 700; margin-top: 4px; letter-spacing: -.03em; }
.stat .s { font-size: 11px; color: var(--muted); margin-top: 2px; }
.grid { display: grid; grid-template-columns: 1.35fr .9fr; gap: 12px; align-items: start; }
.stack { display: flex; flex-direction: column; gap: 12px; }
.card { background: #fff; border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }
.card-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
.card-h h2 { font-size: 14px; font-weight: 650; }
.card-b { padding: 12px 14px; }
.quick { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px; }
.qbtn { text-align: left; border: 1px solid var(--line); border-radius: 10px; padding: 12px; background: #fbfcfd; min-height: 92px; }
.qbtn:hover { border-color: var(--green-line); background: #fff; }
.qbtn .ico { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; margin-bottom: 8px; font-weight: 700; font-size: 12px; background: var(--green-soft); color: var(--green-dark); }
.qbtn h3 { font-size: 13px; }
.qbtn p { color: var(--muted); font-size: 12px; margin-top: 4px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid #eef1f4; font-size: 12px; }
th { color: var(--muted); font-weight: 500; }
tr:hover td { background: #f8faf9; }
.st { display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; }
.st-run { background: var(--green-soft); color: var(--green-dark); }
.st-fix { background: #fff7ed; color: var(--amber); }
.st-stop { background: #f3f4f6; color: #4b5563; }
.st-due { background: #fef2f2; color: #b91c1c; }
.timeline { display: flex; flex-direction: column; gap: 0; }
.tl { display: grid; grid-template-columns: 72px 1fr; gap: 8px; padding: 8px 0; border-bottom: 1px solid #eef1f4; }
.tl:last-child { border: 0; }
.tl .t { color: var(--muted); font-size: 11px; }
.tl b { font-weight: 600; }
.tl .d { color: var(--muted); }
.bars { display: flex; flex-direction: column; gap: 8px; }
.bar-row { display: grid; grid-template-columns: 88px 1fr 64px; gap: 8px; align-items: center; }
.bar-track { height: 8px; background: #eef1f4; border-radius: 99px; overflow: visible; position: relative; }
.bar-fill { height: 100%; background: linear-gradient(90deg, #148a3c, #3cb464); border-radius: 99px; }
.bar-fill.warn { background: linear-gradient(90deg, #e24b4a, #f97316); }
.bar-mark { position: absolute; top: -3px; height: 14px; width: 2px; background: var(--warn); border-radius: 1px; }
.bar-row .n { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.rate-over { color: #b91c1c; font-weight: 650; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.filters input, .filters select, .form input, .form select, .form textarea {
height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
}
.form textarea { height: 64px; padding: 8px 10px; resize: vertical; }
.form { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.form label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 12px; }
.form .full { grid-column: 1 / -1; }
.form .span2 { grid-column: span 2; }
.file-box {
border: 1px dashed var(--green-line); border-radius: 8px; background: #f7fbf8;
padding: 8px 10px; min-height: 64px; display: flex; flex-direction: column; gap: 6px;
}
.file-box input[type=file] { height: auto; padding: 0; border: 0; background: transparent; }
.file-hint { color: var(--amber); font-size: 11px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 99px;
background: var(--green-soft); color: var(--green-dark); font-size: 11px; font-weight: 600; border: 0;
}
.chip.ghost { background: #f3f4f6; color: #4b5563; cursor: default; }
.chip:disabled { opacity: .65; cursor: default; }
dialog {
border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow);
max-width: min(720px, 92vw); width: 720px;
}
dialog.apply-dlg { max-width: min(760px, 94vw); width: 760px; }
dialog::backdrop { background: rgba(28, 36, 48, .35); }
.dlg-h { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); }
.dlg-h.green { background: #eef8f1; }
.dlg-h h3 { font-size: 14px; }
.dlg-x { width: 28px; height: 28px; border-radius: 6px; color: #5b6572; font-size: 18px; line-height: 1; }
.dlg-x:hover { background: #e4eee7; }
.dlg-b { padding: 14px; max-height: 70vh; overflow: auto; }
.dlg-b img { max-width: 100%; border-radius: 8px; display: block; }
.dlg-f { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--line); }
.apply { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 28px; }
.apply .row { display: grid; grid-template-columns: 88px 1fr; gap: 10px; align-items: start; }
.apply .full { grid-column: 1 / -1; }
.apply .lab { color: var(--ink); padding-top: 7px; text-align: right; }
.apply input, .apply select, .apply textarea {
width: 100%; height: 32px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; background: #fff; outline: none;
}
.apply textarea { height: 88px; padding: 8px 10px; resize: vertical; }
.star { color: #e24b4a; margin-right: 2px; }
.stepper { display: flex; height: 32px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: #fff; }
.stepper button { width: 36px; background: #f7f8fa; color: #6a7380; }
.stepper button:hover { background: #eef1f4; }
.stepper input { flex: 1; border: 0; border-radius: 0; border-left: 1px solid var(--line); border-right: 1px solid var(--line); text-align: center; height: 32px; width: auto; }
.upload {
width: 132px; height: 108px; border: 1px dashed var(--green-line); border-radius: 10px;
background: #f4faf6; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: var(--green); gap: 4px; cursor: pointer; font-weight: 500;
}
.upload:hover { background: #ebf7ef; }
.upload input { display: none; }
.upload .plus { font-size: 22px; line-height: 1; font-weight: 400; }
.hint { color: var(--muted); font-size: 11px; margin-top: 8px; }
.hidden { display: none !important; }
.toast {
position: fixed; right: 20px; bottom: 20px; background: #1c2430; color: #fff;
padding: 10px 14px; border-radius: 8px; display: none; z-index: 50;
}
.toast.show { display: block; }
.pie-wrap { display: flex; align-items: center; gap: 16px; }
.legend { display: flex; flex-direction: column; gap: 6px; }
.legend span { display: flex; align-items: center; gap: 6px; }
.sw { width: 8px; height: 8px; border-radius: 2px; }
@media (max-width: 1100px) {
.stats, .grid, .quick, .form, .apply { grid-template-columns: 1fr; }
.sidebar { display: none; }
.form .span2 { grid-column: auto; }
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M4 16c4-9 12-9 16 0" stroke="#fff" stroke-width="2"/><circle cx="12" cy="9" r="2.2" fill="#fff"/></svg>
</div>
山峡代工一体化平台
</div>
<div class="crumb">首页 / <b>车辆管理</b> / <span id="crumb">总览</span></div>
<input class="top-search" id="globalSearch" placeholder="搜索车牌、单号" />
<div class="avatar">王</div>
</header>
<div class="shell">
<aside class="sidebar">
<button class="side-item" type="button">收藏</button>
<button class="side-item" type="button">财务发票</button>
<button class="side-item" type="button">CRM 管理</button>
<button class="side-item" type="button">采购管理</button>
<button class="side-item" type="button">仓储管理</button>
<button class="side-item" type="button"><span class="side-dot"></span>运营管理</button>
<button class="side-item active" type="button"><span class="side-dot"></span>车辆管理 <span class="badge-n" id="sideBadge">4</span></button>
<button class="side-item" type="button">质检管理</button>
<button class="side-item" type="button">规划管理</button>
</aside>
<main class="main">
<nav class="subnav" id="subnav">
<button class="on" type="button" data-view="dash">总览</button>
<button type="button" data-view="fleet">车辆台账</button>
<button type="button" data-view="fuel">加油登记</button>
<button type="button" data-view="fuelwarn">油耗预警</button>
<button type="button" data-view="etc">ETC 申请登记</button>
<button type="button" data-view="maint">维修保养</button>
<button type="button" data-view="stats">统计</button>
</nav>
<section id="view-dash">
<div class="page-head">
<div>
<h1>危化品车辆作业</h1>
<p>公司自有危货车辆的加油、ETC 申请登记、维修保养,以及油耗与费用统计。</p>
</div>
<div class="actions">
<button class="btn" type="button" onclick="showView('fuel')">登记加油</button>
<button class="btn" type="button" onclick="showView('etc')">ETC 申请</button>
<button class="btn btn-primary" type="button" onclick="showView('maint')">登记维保</button>
</div>
</div>
<div class="alerts" id="dashAlerts"></div>
<div class="stats" id="dashStats"></div>
<div class="grid">
<div class="stack">
<article class="card">
<div class="card-h"><h2>日常登记</h2><span style="color:var(--muted);font-size:12px">同一车辆贯穿三类业务</span></div>
<div class="card-b">
<div class="quick">
<button class="qbtn" type="button" onclick="showView('fuel')"><div class="ico">油</div><h3>加油</h3><p>油品、升数、金额、里程、附件</p></button>
<button class="qbtn" type="button" onclick="showView('fuelwarn')"><div class="ico">警</div><h3>油耗预警</h3><p>超过 0.33 L/km 即预警</p></button>
<button class="qbtn" type="button" onclick="showView('etc')"><div class="ico">E</div><h3>ETC 申请</h3><p>自有车牌、期初公里、附件</p></button>
<button class="qbtn" type="button" onclick="showView('maint')"><div class="ico">保</div><h3>维修保养</h3><p>进厂、项目、费用、下次保养</p></button>
</div>
</div>
</article>
<article class="card">
<div class="card-h"><h2>车辆运行状态</h2><button class="btn" type="button" onclick="showView('fleet')">台账</button></div>
<div class="card-b" style="padding-top:4px"><div id="dashFleet"></div></div>
</article>
</div>
<div class="stack">
<article class="card">
<div class="card-h"><h2>最近登记</h2></div>
<div class="card-b"><div class="timeline" id="dashTl"></div></div>
</article>
<article class="card">
<div class="card-h"><h2>本月单车油耗(升)</h2><button class="btn" type="button" onclick="showView('fuelwarn')">油耗预警</button></div>
<div class="card-b"><div class="bars" id="dashBars"></div></div>
</article>
</div>
</div>
</section>
<section id="view-fleet" class="hidden">
<div class="page-head">
<div><h1>车辆台账</h1><p>危货牵引车 / 罐车基础信息与资质到期。</p></div>
</div>
<div class="filters">
<input id="fleetQ" placeholder="车牌 / 驾驶员" oninput="renderFleet()" />
<select id="fleetSt" onchange="renderFleet()">
<option value="">全部状态</option>
<option>运行</option>
<option>维保中</option>
<option>停运</option>
</select>
</div>
<article class="card"><div class="card-b" style="overflow:auto" id="fleetTable"></div></article>
</section>
<section id="view-fuel" class="hidden">
<div class="page-head">
<div><h1>加油登记</h1><p>按车记录加油,须上传加油小票或发票,并登记当前里程以便核对油耗。</p></div>
</div>
<article class="card" style="margin-bottom:12px">
<div class="card-h"><h2>新增加油</h2></div>
<div class="card-b">
<form class="form" id="fuelForm" onsubmit="return addFuel(event)">
<label>车辆<select name="plate" required></select></label>
<label>日期<input name="date" type="date" required /></label>
<label>加油站<input name="station" required placeholder="如 中石化猇亭站" /></label>
<label>油品<select name="oil"><option>0# 柴油</option><option>-10# 柴油</option><option>LNG</option></select></label>
<label>升数<input name="liters" type="number" step="0.01" required /></label>
<label>金额(元)<input name="amount" type="number" step="0.01" required /></label>
<label>当前里程(km)<input name="km" type="number" required /></label>
<label class="span2">备注<input name="note" placeholder="可选" /></label>
<label class="full">附件(必传)
<div class="file-box">
<input name="attach" type="file" required multiple accept="image/*,.pdf,application/pdf" onchange="previewFuelFiles(this)" />
<span class="file-hint">须上传加油小票、发票或加油凭证(图片或 PDF,可多选)</span>
<div class="chips" id="fuelFileChips"></div>
</div>
</label>
<label style="justify-content:flex-end"><span>&nbsp;</span><button class="btn btn-primary" type="submit">保存登记</button></label>
</form>
</div>
</article>
<article class="card"><div class="card-h"><h2>加油记录</h2></div><div class="card-b" id="fuelTable"></div></article>
</section>
<section id="view-fuelwarn" class="hidden">
<div class="page-head">
<div>
<h1>油耗预警</h1>
<p>按相邻两次加油核对每公里油耗。标准:33 L / 100 km = <b>0.33 L/km</b>,超过即预警。</p>
</div>
</div>
<div class="stats" id="fuelwarnKpis"></div>
<div class="filters">
<select id="fuelwarnSt" onchange="renderFuelWarn()">
<option value="">全部区间</option>
<option value="预警">仅超标预警</option>
<option value="正常">仅正常</option>
<option value="不足">数据不足 / 异常</option>
</select>
<input id="fuelwarnQ" placeholder="车牌" oninput="renderFuelWarn()" />
</div>
<article class="card" style="margin-bottom:12px">
<div class="card-h"><h2>单车油耗对照(L/km)</h2><span style="color:var(--muted);font-size:12px">红线为 0.33</span></div>
<div class="card-b"><div class="bars" id="fuelwarnBars"></div></div>
</article>
<article class="card">
<div class="card-h"><h2>区间明细</h2></div>
<div class="card-b" id="fuelwarnTable"></div>
</article>
</section>
<section id="view-etc" class="hidden">
<div class="page-head">
<div><h1>ETC 申请登记</h1><p>自有车辆开通 / 充值 ETC,须选择车牌、填写期初公里并上传附件。</p></div>
<div class="actions">
<button class="btn btn-primary" type="button" onclick="openEtcDlg()">新增申请</button>
</div>
</div>
<article class="card"><div class="card-h"><h2>申请记录</h2></div><div class="card-b" id="etcTable"></div></article>
</section>
<section id="view-maint" class="hidden">
<div class="page-head">
<div><h1>维修保养登记</h1><p>维修、定期保养、年审、罐检进厂出厂一并登记。</p></div>
</div>
<article class="card" style="margin-bottom:12px">
<div class="card-h"><h2>新增维保</h2></div>
<div class="card-b">
<form class="form" id="maintForm" onsubmit="return addMaint(event)">
<label>车辆<select name="plate" required></select></label>
<label>类型<select name="type"><option>定期保养</option><option>故障维修</option><option>年审</option><option>罐体检验</option></select></label>
<label>进厂日期<input name="inDate" type="date" required /></label>
<label>出厂日期<input name="outDate" type="date" /></label>
<label>承修单位<input name="shop" required placeholder="如 宜昌安达汽修" /></label>
<label>费用(元)<input name="amount" type="number" step="0.01" required /></label>
<label>下次保养里程<input name="nextKm" type="number" placeholder="km" /></label>
<label class="span2">项目说明<input name="item" required placeholder="更换机油、刹车片等" /></label>
<label><span>&nbsp;</span><button class="btn btn-primary" type="submit">保存登记</button></label>
</form>
</div>
</article>
<article class="card"><div class="card-h"><h2>维保记录</h2></div><div class="card-b" id="maintTable"></div></article>
</section>
<section id="view-stats" class="hidden">
<div class="page-head">
<div><h1>统计</h1><p>本月油耗、维保费用与 ETC 申请笔数,辅助单车成本对照。</p></div>
</div>
<div class="stats" id="statKpis"></div>
<div class="grid">
<article class="card">
<div class="card-h"><h2>单车加油量</h2></div>
<div class="card-b"><div class="bars" id="statBars"></div></div>
</article>
<article class="card">
<div class="card-h"><h2>本月费用构成</h2></div>
<div class="card-b">
<div class="pie-wrap">
<svg width="140" height="140" viewBox="0 0 32 32" id="pie"></svg>
<div class="legend" id="pieLegend"></div>
</div>
</div>
</article>
</div>
<article class="card" style="margin-top:12px">
<div class="card-h"><h2>资质到期一览</h2></div>
<div class="card-b" id="certTable"></div>
</article>
</section>
</main>
</div>
</div>
<div class="toast" id="toast">已保存</div>
<dialog id="attachDialog">
<div class="dlg-h">
<h3 id="attachTitle">附件</h3>
<button class="btn" type="button" onclick="closeAttach()">关闭</button>
</div>
<div class="dlg-b" id="attachBody"></div>
</dialog>
<dialog class="apply-dlg" id="etcDialog">
<div class="dlg-h green">
<h3>新增申请</h3>
<button class="dlg-x" type="button" aria-label="关闭" onclick="closeEtcDlg()">×</button>
</div>
<form id="etcForm" onsubmit="return false">
<div class="dlg-b">
<div class="apply">
<div class="row">
<div class="lab"><span class="star">*</span>申请人</div>
<select name="applicant" required></select>
</div>
<div class="row">
<div class="lab"><span class="star">*</span>车牌号</div>
<select name="plate" required onchange="syncEtcKm()"></select>
</div>
<div class="row">
<div class="lab"><span class="star">*</span>充值类型</div>
<select name="type" required>
<option>ETC</option>
<option>油卡</option>
</select>
</div>
<div class="row">
<div class="lab"><span class="star">*</span>申请日期</div>
<input name="date" type="date" required />
</div>
<div class="row full">
<div class="lab"><span class="star">*</span>期初公里数</div>
<div class="stepper">
<button type="button" onclick="stepKm(-1)">−</button>
<input name="km" type="number" min="0" required placeholder="请输入期初公里数" />
<button type="button" onclick="stepKm(1)">+</button>
</div>
</div>
<div class="row full">
<div class="lab"><span class="star">*</span>附件</div>
<div>
<label class="upload">
<input name="attach" type="file" required multiple accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,application/pdf" onchange="previewEtcFiles(this)" />
<span class="plus">+</span>
<span>上传附件</span>
</label>
<div class="hint">支持图片、视频、PDF、Word、Excel 等文件;可拖拽或在此区域粘贴上传</div>
<div class="chips" id="etcFileChips" style="margin-top:8px"></div>
</div>
</div>
<div class="row full">
<div class="lab">备注</div>
<textarea name="note" placeholder="备注"></textarea>
</div>
</div>
</div>
<div class="dlg-f">
<button class="btn" type="button" onclick="closeEtcDlg()">× 取消</button>
<button class="btn" type="button" onclick="saveEtc(false)">✓ 保存</button>
<button class="btn btn-primary" type="button" onclick="saveEtc(true)">↑ 保存并提交</button>
</div>
</form>
</dialog>
<script>
const today = "2026-09-29";
const vehicles = [
{ plate: "鄂E·危A123", type: "柴油罐车", media: "柴油", driver: "周建军", escort: "李强", km: 186420, status: "运行", tankDue: "2026-10-12", insDue: "2026-12-01", nextMaint: 188000 },
{ plate: "鄂E·危B618", type: "甲醇罐车", media: "甲醇", driver: "陈伟", escort: "王磊", km: 142880, status: "维保中", tankDue: "2027-03-08", insDue: "2026-10-20", nextMaint: 143000 },
{ plate: "鄂E·危C902", type: "液氨罐车", media: "液氨", driver: "胡明", escort: "赵鹏", km: 97850, status: "运行", tankDue: "2026-11-02", insDue: "2027-01-15", nextMaint: 102000 },
{ plate: "鄂E·危D077", type: "柴油罐车", media: "柴油", driver: "吴涛", escort: "孙斌", km: 210330, status: "运行", tankDue: "2027-06-18", insDue: "2026-09-30", nextMaint: 212000 },
{ plate: "鄂E·危E441", type: "硫酸罐车", media: "硫酸", driver: "郑浩", escort: "刘洋", km: 65420, status: "停运", tankDue: "2026-10-05", insDue: "2026-11-11", nextMaint: 68000 }
];
let fuels = [
{ plate: "鄂E·危A123", date: "2026-09-28", station: "中石化猇亭站", oil: "0# 柴油", liters: 320, amount: 2368, km: 186420, note: "", attachments: [{ name: "猇亭站小票_0928.jpg" }] },
{ plate: "鄂E·危C902", date: "2026-09-27", station: "中石油伍家岗", oil: "0# 柴油", liters: 280, amount: 2072, km: 97850, note: "", attachments: [{ name: "伍家岗发票_0927.pdf" }] },
{ plate: "鄂E·危D077", date: "2026-09-26", station: "公司内部油库", oil: "0# 柴油", liters: 410, amount: 2952, km: 210110, note: "夜班补油", attachments: [{ name: "内库出库单_0926.jpg" }] },
{ plate: "鄂E·危B618", date: "2026-09-22", station: "中石化夷陵站", oil: "0# 柴油", liters: 340, amount: 2516, km: 142200, note: "", attachments: [{ name: "夷陵站小票_0922.jpg" }] },
{ plate: "鄂E·危A123", date: "2026-09-21", station: "中石化猇亭站", oil: "0# 柴油", liters: 300, amount: 2220, km: 185210, note: "", attachments: [{ name: "猇亭站小票_0921.jpg" }] },
{ plate: "鄂E·危C902", date: "2026-09-20", station: "中石油伍家岗", oil: "0# 柴油", liters: 260, amount: 1924, km: 96800, note: "", attachments: [{ name: "伍家岗小票_0920.jpg" }] },
{ plate: "鄂E·危D077", date: "2026-09-18", station: "中石化夷陵站", oil: "0# 柴油", liters: 380, amount: 2736, km: 208900, note: "", attachments: [{ name: "夷陵站小票_0918.jpg" }] },
{ plate: "鄂E·危B618", date: "2026-09-10", station: "公司内部油库", oil: "0# 柴油", liters: 350, amount: 2520, km: 141500, note: "", attachments: [{ name: "内库出库单_0910.jpg" }] }
];
let etcs = [
{ applicant: "吴九华", plate: "鄂E·危B618", type: "ETC", date: "2026-09-25", km: 142880, note: "", status: "已提交", attachments: [{ name: "ETC开通凭证.jpg" }] },
{ applicant: "周建军", plate: "鄂E·危A123", type: "ETC", date: "2026-09-12", km: 185210, note: "新车上牌后开通", status: "草稿", attachments: [{ name: "行驶证.pdf" }] }
];
let maints = [
{ plate: "鄂E·危B618", type: "故障维修", inDate: "2026-09-28", outDate: "", shop: "宜昌安达汽修", amount: 1860, nextKm: 143000, item: "更换刹车片、排查气路" },
{ plate: "鄂E·危A123", type: "定期保养", inDate: "2026-09-08", outDate: "2026-09-08", shop: "公司维修班", amount: 620, nextKm: 188000, item: "机油机滤、检查罐阀" },
{ plate: "鄂E·危E441", type: "罐体检验", inDate: "2026-09-20", outDate: "", shop: "省特检院宜昌分院", amount: 4800, nextKm: 68000, item: "罐体定期检验" }
];
const labels = { dash: "总览", fleet: "车辆台账", fuel: "加油登记", fuelwarn: "油耗预警", etc: "ETC 申请登记", maint: "维修保养", stats: "统计" };
const FUEL_NORM = 33 / 100;
const money = (n) => Number(n).toLocaleString("zh-CN", { maximumFractionDigits: 0 });
const stClass = (s) => s === "运行" ? "st-run" : s === "维保中" ? "st-fix" : "st-stop";
const applicants = ["吴九华", ...vehicles.map((v) => v.driver)].filter((n, i, a) => a.indexOf(n) === i);
function fillPlates() {
["fuelForm", "maintForm"].forEach((id) => {
const sel = document.querySelector(`#${id} [name=plate]`);
sel.innerHTML = vehicles.map((v) => `<option>${v.plate}</option>`).join("");
});
const etcPlate = document.querySelector("#etcForm [name=plate]");
etcPlate.innerHTML = `<option value="">请选择自有车牌</option>` + vehicles.map((v) => `<option>${v.plate}</option>`).join("");
const appSel = document.querySelector("#etcForm [name=applicant]");
appSel.innerHTML = applicants.map((n) => `<option value="${n}">${n}(${n})</option>`).join("");
document.querySelector("#fuelForm [name=date]").value = today;
document.querySelector("#etcForm [name=date]").value = today;
document.querySelector("#maintForm [name=inDate]").value = today;
}
function daysTo(d) {
return Math.ceil((new Date(d) - new Date(today)) / 86400000);
}
function showView(name) {
["dash", "fleet", "fuel", "fuelwarn", "etc", "maint", "stats"].forEach((v) => {
document.getElementById("view-" + v).classList.toggle("hidden", v !== name);
});
document.querySelectorAll("#subnav button").forEach((b) => b.classList.toggle("on", b.dataset.view === name));
document.getElementById("crumb").textContent = labels[name];
if (name === "dash") renderDash();
if (name === "fleet") renderFleet();
if (name === "fuel") renderFuel();
if (name === "fuelwarn") renderFuelWarn();
if (name === "etc") renderEtc();
if (name === "maint") renderMaint();
if (name === "stats") renderStats();
}
function toast(msg) {
const el = document.getElementById("toast");
el.textContent = msg;
el.classList.add("show");
setTimeout(() => el.classList.remove("show"), 1800);
}
function fuelSegments() {
const byPlate = {};
fuels.forEach((f) => {
(byPlate[f.plate] ||= []).push(f);
});
const rows = [];
vehicles.forEach((v) => {
const list = (byPlate[v.plate] || []).slice().sort((a, b) => a.km - b.km || a.date.localeCompare(b.date));
if (list.length < 2) {
rows.push({
plate: v.plate,
driver: v.driver,
fromDate: list[0]?.date || "—",
toDate: list[0]?.date || "—",
fromKm: list[0]?.km || v.km,
toKm: list[0]?.km || v.km,
km: 0,
liters: list[0]?.liters || 0,
rate: null,
per100: null,
status: list.length ? "不足" : "无记录"
});
return;
}
for (let i = 1; i < list.length; i++) {
const prev = list[i - 1];
const cur = list[i];
const km = cur.km - prev.km;
const liters = Number(cur.liters);
const rate = km > 0 ? liters / km : null;
let status = "正常";
if (km <= 0) status = "异常";
else if (rate > FUEL_NORM) status = "预警";
rows.push({
plate: v.plate,
driver: v.driver,
fromDate: prev.date,
toDate: cur.date,
fromKm: prev.km,
toKm: cur.km,
km,
liters,
rate,
per100: rate == null ? null : rate * 100,
status
});
}
});
const order = { 预警: 0, 异常: 1, 不足: 2, 无记录: 3, 正常: 4 };
return rows.sort((a, b) => order[a.status] - order[b.status] || a.plate.localeCompare(b.plate));
}
function fuelWarnCount() {
return fuelSegments().filter((r) => r.status === "预警").length;
}
function renderDash() {
const due = vehicles.filter((v) => daysTo(v.tankDue) <= 20 || daysTo(v.insDue) <= 20);
const fixing = vehicles.filter((v) => v.status === "维保中").length;
const warnN = fuelWarnCount();
document.getElementById("dashAlerts").innerHTML = `
<div class="alert red" style="cursor:pointer" onclick="showView('fuelwarn')">油耗超标预警 ${warnN} 条(标准 0.33 L/km)</div>
<div class="alert red">罐检 / 保险 20 日内到期 ${due.length} 辆</div>
<div class="alert">维保中 ${fixing} 辆,注意出厂登记</div>
<div class="alert ok">本月已登加油 ${fuels.length} 笔</div>`;
const fuelSum = fuels.reduce((s, x) => s + x.amount, 0);
const maintOpen = maints.filter((m) => !m.outDate).length;
document.getElementById("dashStats").innerHTML = [
["在册车辆", vehicles.length, `运行 ${vehicles.filter(v=>v.status==="运行").length} · 维保 ${fixing}`],
["本月加油(元)", money(fuelSum), `${fuels.reduce((s,x)=>s+x.liters,0)} 升`],
["未出厂维保", maintOpen, `记录 ${maints.length} 条`],
["本月 ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} · 草稿 ${etcs.filter(x=>x.status==="草稿").length}`]
].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
document.getElementById("dashFleet").innerHTML = `<table><thead><tr><th>车牌</th><th>介质</th><th>驾驶员</th><th>状态</th><th>罐检到期</th></tr></thead><tbody>${
vehicles.map((v) => `<tr><td>${v.plate}</td><td>${v.media}</td><td>${v.driver}</td><td><span class="st ${stClass(v.status)}">${v.status}</span></td><td>${daysTo(v.tankDue) <= 20 ? `<span class="st st-due">${v.tankDue}</span>` : v.tankDue}</td></tr>`).join("")
}</tbody></table>`;
const events = [
...fuels.map((x) => ({ date: x.date, title: "加油 " + x.plate, d: x.liters + " 升 · " + x.station })),
...etcs.map((x) => ({ date: x.date, title: "ETC申请 " + x.plate, d: x.applicant + " · 期初 " + x.km + " km" })),
...maints.map((x) => ({ date: x.inDate, title: x.type + " " + x.plate, d: x.item }))
].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 6);
document.getElementById("dashTl").innerHTML = events.map((e) => `<div class="tl"><div class="t">${e.date.slice(5)}</div><div><b>${e.title}</b><div class="d">${e.d}</div></div></div>`).join("");
const byPlate = {};
fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
const max = Math.max(...Object.values(byPlate), 1);
document.getElementById("dashBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
`<div class="bar-row"><span>${p.replace("鄂E·","")}</span><div class="bar-track"><div class="bar-fill" style="width:${(n/max)*100}%"></div></div><span class="n">${n}</span></div>`
).join("");
document.getElementById("sideBadge").textContent = due.length + maintOpen + warnN;
}
function renderFleet() {
const q = (document.getElementById("fleetQ").value || "").trim();
const st = document.getElementById("fleetSt").value;
const rows = vehicles.filter((v) => (!q || v.plate.includes(q) || v.driver.includes(q)) && (!st || v.status === st));
document.getElementById("fleetTable").innerHTML = `<table><thead><tr><th>车牌</th><th>车型</th><th>介质</th><th>驾驶员</th><th>押运员</th><th>里程</th><th>状态</th><th>罐检</th><th>保险</th><th>下次保养</th></tr></thead><tbody>${
rows.map((v) => `<tr><td>${v.plate}</td><td>${v.type}</td><td>${v.media}</td><td>${v.driver}</td><td>${v.escort}</td><td>${v.km.toLocaleString()}</td><td><span class="st ${stClass(v.status)}">${v.status}</span></td><td>${v.tankDue}</td><td>${v.insDue}</td><td>${v.nextMaint.toLocaleString()} km</td></tr>`).join("")
}</tbody></table>`;
}
function attachChips(source, rowIndex) {
const items = (source === "etc" ? etcs : fuels)[rowIndex].attachments || [];
if (!items.length) return `<span class="st st-due">缺附件</span>`;
return `<div class="chips">${items.map((a, i) =>
a.url
? `<button class="chip" type="button" onclick="openAttach('${source}',${rowIndex},${i})">${a.name}</button>`
: `<span class="chip ghost" title="历史存档">${a.name}</span>`
).join("")}</div>`;
}
function renderFuel() {
document.getElementById("fuelTable").innerHTML = `<table><thead><tr><th>日期</th><th>车辆</th><th>加油站</th><th>油品</th><th>升数</th><th>金额</th><th>里程</th><th>附件</th><th>备注</th></tr></thead><tbody>${
fuels.map((x, i) => `<tr><td>${x.date}</td><td>${x.plate}</td><td>${x.station}</td><td>${x.oil}</td><td>${x.liters}</td><td>${x.amount}</td><td>${x.km}</td><td>${attachChips("fuel", i)}</td><td>${x.note || "—"}</td></tr>`).join("")
}</tbody></table>`;
}
function fmtRate(n, digits) {
return n == null ? "—" : n.toFixed(digits);
}
function renderFuelWarn() {
const q = (document.getElementById("fuelwarnQ").value || "").trim();
const st = document.getElementById("fuelwarnSt").value;
const all = fuelSegments();
const warnN = all.filter((r) => r.status === "预警").length;
const okN = all.filter((r) => r.status === "正常").length;
const lackN = all.filter((r) => r.status === "不足" || r.status === "无记录" || r.status === "异常").length;
const rates = all.filter((r) => r.rate != null).map((r) => r.rate);
const maxRate = rates.length ? Math.max(...rates) : 0;
document.getElementById("fuelwarnKpis").innerHTML = [
["标准油耗", "0.33 L/km", "33 ÷ 100"],
["超标预警", warnN, "相邻加油区间"],
["正常区间", okN, "未超过标准"],
["数据不足 / 异常", lackN, "少于两次加油或里程未增"]
].map(([k, v, s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
const latestByPlate = {};
all.filter((r) => r.rate != null).forEach((r) => {
const prev = latestByPlate[r.plate];
if (!prev || r.toDate > prev.toDate) latestByPlate[r.plate] = r;
});
const barMax = Math.max(maxRate, FUEL_NORM, 0.01);
document.getElementById("fuelwarnBars").innerHTML = Object.values(latestByPlate).map((r) => {
const over = r.rate > FUEL_NORM;
return `<div class="bar-row"><span>${r.plate.replace("鄂E·","")}</span><div class="bar-track"><div class="bar-fill${over ? " warn" : ""}" style="width:${(r.rate / barMax) * 100}%"></div><i class="bar-mark" style="left:${(FUEL_NORM / barMax) * 100}%" title="标准 0.33"></i></div><span class="n${over ? " rate-over" : ""}">${fmtRate(r.rate, 3)}</span></div>`;
}).join("") || `<div style="color:var(--muted)">暂无可对照的加油区间</div>`;
const rows = all.filter((r) => (!q || r.plate.includes(q)) && (!st || (st === "不足" ? ["不足", "无记录", "异常"].includes(r.status) : r.status === st)));
const stHtml = (s) => s === "预警" || s === "异常" ? `<span class="st st-due">${s}</span>` : s === "正常" ? `<span class="st st-run">${s}</span>` : `<span class="st st-stop">${s}</span>`;
document.getElementById("fuelwarnTable").innerHTML = `<table><thead><tr><th>车辆</th><th>驾驶员</th><th>区间</th><th>起止里程</th><th>行驶(km)</th><th>加油(L)</th><th>L/km</th><th>L/100km</th><th>对照</th><th>状态</th></tr></thead><tbody>${
rows.map((r) => {
const over = r.status === "预警";
const delta = r.rate == null ? "—" : (r.rate - FUEL_NORM >= 0 ? "+" : "") + (r.rate - FUEL_NORM).toFixed(3);
return `<tr><td>${r.plate}</td><td>${r.driver}</td><td>${r.fromDate} → ${r.toDate}</td><td>${Number(r.fromKm).toLocaleString()} → ${Number(r.toKm).toLocaleString()}</td><td>${r.km || "—"}</td><td>${r.liters || "—"}</td><td class="${over ? "rate-over" : ""}">${fmtRate(r.rate, 3)}</td><td class="${over ? "rate-over" : ""}">${fmtRate(r.per100, 1)}</td><td>${delta}</td><td>${stHtml(r.status)}</td></tr>`;
}).join("") || `<tr><td colspan="10" style="color:var(--muted)">没有符合条件的记录</td></tr>`
}</tbody></table>`;
}
function previewFuelFiles(input) {
const box = document.getElementById("fuelFileChips");
const files = [...input.files];
box.innerHTML = files.length
? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
: "";
}
function readFiles(fileList) {
return Promise.all([...fileList].map((file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve({ name: file.name, type: file.type, url: reader.result });
reader.onerror = reject;
reader.readAsDataURL(file);
})));
}
function openAttach(source, rowIndex, fileIndex) {
const file = (source === "etc" ? etcs : fuels)[rowIndex].attachments[fileIndex];
document.getElementById("attachTitle").textContent = file.name;
const body = document.getElementById("attachBody");
if (file.type && file.type.startsWith("image/") && file.url) {
body.innerHTML = `<img alt="${file.name}" src="${file.url}" />`;
} else if (file.url) {
body.innerHTML = `<p style="margin-bottom:8px;color:var(--muted)">文件可新窗口打开查看。</p><a class="btn btn-primary" href="${file.url}" target="_blank" rel="noopener">打开 ${file.name}</a>`;
} else {
body.innerHTML = `<p>该附件已随历史记录存档,当前演示环境未保留原文件内容。</p>`;
}
document.getElementById("attachDialog").showModal();
}
function closeAttach() {
document.getElementById("attachDialog").close();
}
function renderEtc() {
document.getElementById("etcTable").innerHTML = `<table><thead><tr><th>申请日期</th><th>申请人</th><th>车牌</th><th>充值类型</th><th>期初公里</th><th>状态</th><th>附件</th><th>备注</th></tr></thead><tbody>${
etcs.map((x, i) => `<tr><td>${x.date}</td><td>${x.applicant}</td><td>${x.plate}</td><td>${x.type}</td><td>${Number(x.km).toLocaleString()}</td><td><span class="st ${x.status==="已提交"?"st-run":"st-fix"}">${x.status}</span></td><td>${attachChips("etc", i)}</td><td>${x.note || "—"}</td></tr>`).join("")
}</tbody></table>`;
}
function renderMaint() {
document.getElementById("maintTable").innerHTML = `<table><thead><tr><th>进厂</th><th>出厂</th><th>车辆</th><th>类型</th><th>项目</th><th>承修单位</th><th>费用</th><th>下次保养</th></tr></thead><tbody>${
maints.map((x) => `<tr><td>${x.inDate}</td><td>${x.outDate || "未出厂"}</td><td>${x.plate}</td><td>${x.type}</td><td>${x.item}</td><td>${x.shop}</td><td>${x.amount}</td><td>${x.nextKm || "—"}</td></tr>`).join("")
}</tbody></table>`;
}
function renderStats() {
const fuelAmt = fuels.reduce((s, x) => s + Number(x.amount), 0);
const maintAmt = maints.reduce((s, x) => s + Number(x.amount), 0);
const liters = fuels.reduce((s, x) => s + Number(x.liters), 0);
document.getElementById("statKpis").innerHTML = [
["加油升数", liters, "本月已登记"],
["加油费用", money(fuelAmt) + " 元", ""],
["维保费用", money(maintAmt) + " 元", "含未出厂"],
["ETC 申请", etcs.length, `已提交 ${etcs.filter(x=>x.status==="已提交").length} 笔`]
].map(([k,v,s]) => `<div class="stat"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s || "&nbsp;"}</div></div>`).join("");
const byPlate = {};
fuels.forEach((f) => { byPlate[f.plate] = (byPlate[f.plate] || 0) + f.liters; });
const max = Math.max(...Object.values(byPlate), 1);
document.getElementById("statBars").innerHTML = Object.entries(byPlate).map(([p, n]) =>
`<div class="bar-row"><span>${p.replace("鄂E·","")}</span><div class="bar-track"><div class="bar-fill" style="width:${(n/max)*100}%"></div></div><span class="n">${n} L</span></div>`
).join("");
const total = fuelAmt + maintAmt || 1;
const segs = [
{ name: "加油", n: fuelAmt, c: "#148a3c" },
{ name: "维保", n: maintAmt, c: "#c2410c" }
];
let acc = 0;
const circ = 2 * Math.PI * 10;
document.getElementById("pie").innerHTML = segs.map((s) => {
const len = (s.n / total) * circ;
const dash = `${len} ${circ}`;
const rot = -90 + acc * 360;
acc += s.n / total;
return `<circle r="10" cx="16" cy="16" fill="none" stroke="${s.c}" stroke-width="4" stroke-dasharray="${dash}" transform="rotate(${rot} 16 16)"/>`;
}).join("");
document.getElementById("pieLegend").innerHTML = segs.map((s) =>
`<span><i class="sw" style="background:${s.c}"></i>${s.name} ${money(s.n)} 元</span>`
).join("");
document.getElementById("certTable").innerHTML = `<table><thead><tr><th>车牌</th><th>罐检到期</th><th>剩余</th><th>保险到期</th><th>剩余</th></tr></thead><tbody>${
vehicles.map((v) => `<tr><td>${v.plate}</td><td>${v.tankDue}</td><td>${daysTo(v.tankDue)} 天</td><td>${v.insDue}</td><td>${daysTo(v.insDue)} 天</td></tr>`).join("")
}</tbody></table>`;
}
function addFuel(e) {
e.preventDefault();
const input = e.target.querySelector("[name=attach]");
if (!input.files || !input.files.length) {
toast("请上传加油小票或发票");
return false;
}
const fd = new FormData(e.target);
const f = Object.fromEntries(fd);
delete f.attach;
readFiles(input.files).then((attachments) => {
fuels.unshift({ ...f, liters: +f.liters, amount: +f.amount, km: +f.km, attachments });
const v = vehicles.find((x) => x.plate === f.plate);
if (v) v.km = +f.km;
toast("加油已登记,附件已保存");
e.target.reset();
document.getElementById("fuelFileChips").innerHTML = "";
fillPlates();
renderFuel();
renderDash();
if (!document.getElementById("view-fuelwarn").classList.contains("hidden")) renderFuelWarn();
});
return false;
}
function previewEtcFiles(input) {
const box = document.getElementById("etcFileChips");
const files = [...input.files];
box.innerHTML = files.length
? files.map((f) => `<span class="chip ghost">${f.name}</span>`).join("")
: "";
}
function stepKm(delta) {
const el = document.querySelector("#etcForm [name=km]");
el.value = Math.max(0, (+el.value || 0) + delta);
}
function syncEtcKm() {
const plate = document.querySelector("#etcForm [name=plate]").value;
const v = vehicles.find((x) => x.plate === plate);
if (v) document.querySelector("#etcForm [name=km]").value = v.km;
}
function openEtcDlg() {
const form = document.getElementById("etcForm");
form.reset();
document.getElementById("etcFileChips").innerHTML = "";
fillPlates();
document.getElementById("etcDialog").showModal();
}
function closeEtcDlg() {
document.getElementById("etcDialog").close();
}
function saveEtc(submit) {
const form = document.getElementById("etcForm");
if (!form.reportValidity()) return;
const input = form.querySelector("[name=attach]");
if (!input.files || !input.files.length) {
toast("请上传附件");
return;
}
const f = Object.fromEntries(new FormData(form));
delete f.attach;
readFiles(input.files).then((attachments) => {
etcs.unshift({ ...f, km: +f.km, attachments, status: submit ? "已提交" : "草稿" });
toast(submit ? "ETC 申请已提交" : "ETC 申请已保存");
closeEtcDlg();
renderEtc();
renderDash();
});
}
function addMaint(e) {
e.preventDefault();
const f = Object.fromEntries(new FormData(e.target));
maints.unshift({ ...f, amount: +f.amount, nextKm: f.nextKm ? +f.nextKm : "" });
const v = vehicles.find((x) => x.plate === f.plate);
if (v && !f.outDate) v.status = "维保中";
toast("维保已登记");
e.target.reset();
fillPlates();
renderMaint();
renderDash();
}
document.getElementById("subnav").addEventListener("click", (e) => {
const b = e.target.closest("button");
if (b) showView(b.dataset.view);
});
document.getElementById("globalSearch").addEventListener("keydown", (e) => {
if (e.key !== "Enter") return;
document.getElementById("fleetQ").value = e.target.value;
showView("fleet");
});
fillPlates();
renderDash();
</script>
</body>
</html>