|
|
@@ -0,0 +1,346 @@
|
|
|
+<!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>
|