| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>三方车辆 · 证书附件批量 ZIP 下载 Demo</title>
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --border: #d9e8e2;
- --text: #1e293b;
- --muted: #64748b;
- --page: #f1f5f9;
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
- background: var(--page);
- color: var(--text);
- line-height: 1.5;
- padding: 24px;
- }
- h1 { font-size: 22px; margin: 0 0 6px; }
- .sub { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
- .panel {
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 10px;
- overflow: hidden;
- margin-bottom: 20px;
- }
- .toolbar {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
- padding: 12px 14px;
- border-bottom: 1px solid #e2e8f0;
- }
- .btn {
- height: 32px;
- padding: 0 12px;
- border-radius: 8px;
- border: 1px solid var(--border);
- background: #fff;
- color: var(--text);
- font-size: 13px;
- cursor: pointer;
- }
- .btn:disabled { color: #94a3b8; cursor: not-allowed; }
- .btn-main { border: none; background: var(--primary); color: #fff; font-weight: 600; }
- .btn-zip {
- border-color: #a7f3d0;
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 600;
- }
- .btn-zip:disabled { opacity: 0.45; }
- .sel { margin-left: auto; font-size: 12px; color: var(--muted); }
- .sel b { color: var(--primary); }
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
- th, td { padding: 12px; text-align: left; border-top: 1px solid #e2e8f0; vertical-align: middle; }
- th { background: #f8fafc; color: var(--muted); font-weight: 600; border-top: none; }
- tr.selected { background: var(--primary-bg); }
- a.plate { color: var(--primary); text-decoration: none; font-weight: 600; }
- .thumb {
- position: relative;
- width: 72px;
- height: 42px;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: var(--primary-bg);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--primary);
- font-size: 11px;
- }
- .thumb.empty { border-style: dashed; background: #f8fafc; color: #94a3b8; font-size: 10px; }
- .thumb .dl {
- position: absolute; right: 2px; top: 2px; width: 14px; height: 14px;
- border-radius: 50%; background: #fff; border: 1px solid #e2e8f0;
- font-size: 9px; line-height: 12px; text-align: center;
- }
- .thumb .cap {
- position: absolute; left: 0; right: 0; bottom: 0;
- background: rgba(15,23,42,.62); color: #fff; font-size: 9px; text-align: center; padding: 1px 0;
- }
- .certs { display: flex; gap: 8px; flex-wrap: wrap; }
- .tag {
- display: inline-block; padding: 2px 8px; border-radius: 4px;
- background: var(--primary-dark); color: #fff; font-size: 12px;
- }
- .tag.warn { background: #d97706; }
- .toast {
- display: none;
- margin: 0 14px 14px;
- padding: 12px 14px;
- border-radius: 10px;
- border: 1px solid #a7f3d0;
- background: var(--primary-bg);
- color: #065f46;
- font-size: 13px;
- }
- .toast.show { display: block; }
- .toast.warn { border-color: #fcd34d; background: #fffbeb; color: #92400e; }
- .toast pre {
- margin: 6px 0 0;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- line-height: 1.55;
- white-space: pre-wrap;
- }
- .options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
- @media (max-width: 900px) { .options { grid-template-columns: 1fr; } }
- .opt {
- border: 2px solid var(--border);
- border-radius: 10px;
- padding: 14px;
- background: #fff;
- cursor: pointer;
- }
- .opt.active { border-color: var(--primary); background: var(--primary-bg); }
- .opt h3 { margin: 0 0 6px; font-size: 15px; }
- .opt p { margin: 0; font-size: 13px; color: var(--muted); }
- .letter {
- display: inline-flex; width: 24px; height: 24px; border-radius: 50%;
- align-items: center; justify-content: center;
- background: var(--primary); color: #fff; font-size: 12px; font-weight: 700;
- margin-bottom: 8px;
- }
- .split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
- @media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
- .card {
- background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 14px;
- }
- .card h3 { margin: 0 0 8px; font-size: 14px; color: var(--muted); font-weight: 600; }
- .card pre { margin: 0; font-size: 12px; line-height: 1.6; color: #334155; }
- .hint { font-size: 13px; color: var(--muted); margin-bottom: 10px; }
- </style>
- </head>
- <body>
- <h1>三方车辆 · 证书附件批量 ZIP 下载 Demo</h1>
- <p class="sub">勾选车辆 → 点「批量下载证书 ZIP」→ 下方展示模拟包结构。单个附件仍可在缩略图右上角单独下载。</p>
- <div class="panel">
- <div class="toolbar" id="toolbar">
- <span style="font-size:12px;color:var(--muted)">承运商 ▾</span>
- <span class="btn" style="color:#94a3b8;cursor:default">车牌号</span>
- <button class="btn" type="button">快速识别创建</button>
- <button class="btn btn-main" type="button">+ 新增车辆</button>
- <button class="btn" type="button" disabled>编辑</button>
- <button class="btn" type="button" disabled style="border-color:#fecaca;color:#fca5a5">删除</button>
- <button class="btn" type="button">导出 ▾</button>
- <button class="btn btn-zip" id="btn-batch-zip" type="button" onclick="demoDownload()">⬇ 批量下载证书 ZIP</button>
- <span class="sel">已选 <b id="sel-count">2</b> 项</span>
- </div>
- <table>
- <thead>
- <tr>
- <th style="width:40px"><input type="checkbox" id="check-all" onchange="toggleAll(this)" /></th>
- <th style="width:48px">序号</th>
- <th>车牌号</th>
- <th>
- 证书附件
- <button class="btn btn-zip" id="btn-col-zip" type="button" onclick="demoDownload()" style="display:none;height:24px;padding:0 8px;font-size:11px;margin-left:8px">批量 ZIP</button>
- </th>
- <th>车辆状态</th>
- </tr>
- </thead>
- <tbody>
- <tr class="selected">
- <td><input type="checkbox" checked data-plate="皖A·D12345" data-files="主·行驶证.jpg,挂·行驶证.pdf" onchange="syncSel()" /></td>
- <td>1</td>
- <td><a class="plate" href="#">皖A·D12345 / 挂A8888</a></td>
- <td>
- <div class="certs">
- <div class="thumb">📷<span class="dl">↓</span><span class="cap">主·行驶证</span></div>
- <div class="thumb">📷<span class="dl">↓</span><span class="cap">挂·行驶证</span></div>
- <div class="thumb empty">未上传<span class="cap">主·道路运输</span></div>
- </div>
- </td>
- <td><span class="tag">正常</span></td>
- </tr>
- <tr class="selected">
- <td><input type="checkbox" checked data-plate="皖B·F67890" data-files="主·行驶证.jpg,主·营运证.pdf" onchange="syncSel()" /></td>
- <td>2</td>
- <td><a class="plate" href="#">皖B·F67890</a></td>
- <td>
- <div class="certs">
- <div class="thumb">📷<span class="dl">↓</span><span class="cap">主·行驶证</span></div>
- <div class="thumb">📷<span class="dl">↓</span><span class="cap">主·营运证</span></div>
- </div>
- </td>
- <td><span class="tag">正常</span></td>
- </tr>
- <tr>
- <td><input type="checkbox" data-plate="皖C·H11111" data-files="" onchange="syncSel()" /></td>
- <td>3</td>
- <td><a class="plate" href="#">皖C·H11111</a></td>
- <td style="color:#94a3b8">—</td>
- <td><span class="tag warn">停用</span></td>
- </tr>
- </tbody>
- </table>
- <div class="toast" id="toast">
- <div id="toast-title" style="font-weight:600"></div>
- <pre id="toast-tree"></pre>
- </div>
- </div>
- <p class="hint">入口位置偏好(点击切换上方 Demo 按钮位置):</p>
- <div class="options" style="margin-bottom:20px">
- <div class="opt active" data-mode="a" onclick="pickEntry(this)">
- <div class="letter">A</div>
- <h3>工具栏按钮(推荐)</h3>
- <p>勾选后点工具栏「批量下载证书 ZIP」,与导出同级。</p>
- </div>
- <div class="opt" data-mode="b" onclick="pickEntry(this)">
- <div class="letter">B</div>
- <h3>证书附件列头小按钮</h3>
- <p>入口紧贴附件列,语义更近。</p>
- </div>
- <div class="opt" data-mode="c" onclick="pickEntry(this)">
- <div class="letter">C</div>
- <h3>两处都有</h3>
- <p>发现成本最低,略冗余。</p>
- </div>
- </div>
- <div class="split">
- <div class="card">
- <h3>勾选 1 辆</h3>
- <pre>车辆证书_皖A·D12345.zip
- ├── 主·行驶证.jpg
- ├── 挂·行驶证.pdf
- └── (无附件证书跳过)</pre>
- </div>
- <div class="card">
- <h3>勾选多辆</h3>
- <pre>车辆证书批量_2辆_20260908.zip
- ├── 皖A·D12345/
- │ ├── 主·行驶证.jpg
- │ └── 挂·行驶证.pdf
- └── 皖B·F67890/
- ├── 主·行驶证.jpg
- └── 主·营运证.pdf</pre>
- </div>
- </div>
- <script>
- function syncSel() {
- const boxes = Array.from(document.querySelectorAll('tbody input[type=checkbox]'));
- boxes.forEach((b) => {
- b.closest('tr').classList.toggle('selected', b.checked);
- });
- const checked = boxes.filter((b) => b.checked);
- document.getElementById('sel-count').textContent = String(checked.length);
- const btn = document.getElementById('btn-batch-zip');
- const col = document.getElementById('btn-col-zip');
- const disabled = checked.length === 0;
- btn.disabled = disabled;
- col.disabled = disabled;
- document.getElementById('check-all').checked = checked.length === boxes.length;
- document.getElementById('check-all').indeterminate = checked.length > 0 && checked.length < boxes.length;
- }
- function toggleAll(el) {
- document.querySelectorAll('tbody input[type=checkbox]').forEach((b) => {
- b.checked = el.checked;
- });
- syncSel();
- }
- function demoDownload() {
- const boxes = Array.from(document.querySelectorAll('tbody input[type=checkbox]:checked'));
- const toast = document.getElementById('toast');
- const title = document.getElementById('toast-title');
- const tree = document.getElementById('toast-tree');
- toast.classList.add('show');
- if (!boxes.length) {
- toast.classList.add('warn');
- title.textContent = '请先勾选至少一辆车';
- tree.textContent = '';
- return;
- }
- toast.classList.remove('warn');
- const rows = boxes.map((b) => ({
- plate: b.getAttribute('data-plate'),
- files: String(b.getAttribute('data-files') || '').split(',').filter(Boolean)
- }));
- const withFiles = rows.filter((r) => r.files.length);
- if (!withFiles.length) {
- toast.classList.add('warn');
- title.textContent = '所选车辆均无证书附件,无法打包';
- tree.textContent = '';
- return;
- }
- if (withFiles.length === 1) {
- const r = withFiles[0];
- title.textContent = '模拟下载成功 · 单车 ZIP';
- tree.textContent =
- '车辆证书_' + r.plate + '.zip\n' +
- r.files.map((f, i) => (i === r.files.length - 1 ? '└── ' : '├── ') + f).join('\n');
- } else {
- title.textContent = '模拟下载成功 · 多车 ZIP(按车牌分子目录)';
- let out = '车辆证书批量_' + withFiles.length + '辆_20260908.zip\n';
- withFiles.forEach((r, i) => {
- const last = i === withFiles.length - 1;
- out += (last ? '└── ' : '├── ') + r.plate + '/\n';
- r.files.forEach((f, j) => {
- const lastF = j === r.files.length - 1;
- out += (last ? ' ' : '│ ') + (lastF ? '└── ' : '├── ') + f + '\n';
- });
- });
- tree.textContent = out.trimEnd();
- }
- }
- function pickEntry(el) {
- document.querySelectorAll('.opt').forEach((o) => o.classList.remove('active'));
- el.classList.add('active');
- const mode = el.getAttribute('data-mode');
- const toolbarBtn = document.getElementById('btn-batch-zip');
- const colBtn = document.getElementById('btn-col-zip');
- if (mode === 'a') {
- toolbarBtn.style.display = 'inline-block';
- colBtn.style.display = 'none';
- } else if (mode === 'b') {
- toolbarBtn.style.display = 'none';
- colBtn.style.display = 'inline-block';
- } else {
- toolbarBtn.style.display = 'inline-block';
- colBtn.style.display = 'inline-block';
- }
- }
- syncSel();
- </script>
- </body>
- </html>
|