// ==UserScript== // @name 中通快递 - 导出寄出快递记录 // @namespace http://tampermonkey.net/ // @version 4.0 // @description 采集寄出快递列表,导出CSV(运单号文本格式,Excel不丢位数) // @author Codex // @match https://www.zto.com/myExpress // @match https://www.zto.com/myExpress* // @icon https://www.zto.com/favicon.ico // @grant GM_download // @grant GM_addStyle // @license MIT // ==/UserScript== (function () { "use strict"; /* =========================================================== * 配置:文件名、翻页延时等 * =========================================================== */ var CONFIG = { MIN_DELAY: 300, // 翻页最小延时(ms) MAX_DELAY: 800, // 翻页最大延时(ms) RENDER_TIMEOUT: 10000, // 等待列表渲染超时(ms) FILE_NAME: "运单数据.csv", }; /* =========================================================== * 日期工具 * =========================================================== */ function pad2(n) { return n < 10 ? "0" + n : "" + n; } function fmtDate(d) { return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()); } function today() { var d = new Date(); return d; } function yesterday() { var d = new Date(); d.setDate(d.getDate() - 1); return d; } function dayBefore() { var d = new Date(); d.setDate(d.getDate() - 2); return d; } /** 把日期字符串/Date 转为当天 00:00:00 的 Date */ function toStartDate(str) { if (str instanceof Date) return new Date(str.getFullYear(), str.getMonth(), str.getDate()); var p = str.split("-"); return new Date(parseInt(p[0],10), parseInt(p[1],10) - 1, parseInt(p[2],10)); } /** 把日期字符串/Date 转为当天 23:59:59.999 的 Date */ function toEndDate(str) { if (str instanceof Date) return new Date(str.getFullYear(), str.getMonth(), str.getDate(), 23,59,59,999); var p = str.split("-"); return new Date(parseInt(p[0],10), parseInt(p[1],10) - 1, parseInt(p[2],10), 23,59,59,999); } /* 快捷预设 */ var PRESETS = [ { label: "今天", fn: function() { var d = today(); return { start: d, end: d }; } }, { label: "昨天", fn: function() { var d = yesterday(); return { start: d, end: d }; } }, { label: "前天", fn: function() { var d = dayBefore(); return { start: d, end: d }; } }, { label: "近2天", fn: function() { var s = new Date(); s.setDate(s.getDate() - 1); return { start: s, end: today() }; } }, { label: "近7天", fn: function() { var s = new Date(); s.setDate(s.getDate() - 6); return { start: s, end: today() }; } }, { label: "近30天", fn: function() { var s = new Date(); s.setDate(s.getDate() - 29); return { start: s, end: today() }; } }, ]; /* =========================================================== * 注入样式(悬浮按钮、进度浮窗、日期面板) * =========================================================== */ GM_addStyle( "#zto-export-btn{" + "position:fixed;z-index:99999;" + "bottom:120px;right:20px;" + "padding:12px 20px;" + "background:#ff6a00;color:#fff;" + "border:none;border-radius:8px;" + "font-size:15px;font-weight:600;" + "cursor:pointer;" + "box-shadow:0 4px 16px rgba(255,106,0,.35);" + "transition:transform .15s,box-shadow .15s;" + "white-space:nowrap;user-select:none" + "}" + "#zto-export-btn:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(255,106,0,.45)}" + "#zto-export-btn.is-running{background:#999;cursor:not-allowed;box-shadow:0 2px 8px rgba(0,0,0,.2)}" + "#zto-export-progress{" + "position:fixed;z-index:99999;" + "bottom:175px;right:20px;" + "background:rgba(0,0,0,.75);color:#fff;" + "padding:10px 16px;border-radius:6px;" + "font-size:13px;line-height:1.6;" + "min-width:180px;text-align:center;" + "display:none;pointer-events:none" + "}" + "#zto-export-progress.show{display:block}" + "#zto-date-panel{position:fixed;z-index:100000;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);display:none;align-items:center;justify-content:center}" + "#zto-date-panel.show{display:flex}" + "#zto-date-panel .panel-box{background:#fff;border-radius:12px;padding:28px 32px 24px;width:420px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,.25)}" + "#zto-date-panel .panel-title{font-size:17px;font-weight:700;margin-bottom:16px;color:#333;text-align:center}" + "#zto-date-panel .preset-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;justify-content:center}" + "#zto-date-panel .preset-btn{padding:6px 14px;border:1px solid #ddd;border-radius:6px;background:#f7f7f7;cursor:pointer;font-size:13px;color:#555;transition:all .15s}" + "#zto-date-panel .preset-btn:hover{border-color:#ff6a00;color:#ff6a00}" + "#zto-date-panel .preset-btn.active{background:#ff6a00;color:#fff;border-color:#ff6a00}" + "#zto-date-panel .custom-row{display:flex;align-items:center;gap:10px;justify-content:center;margin-bottom:18px}" + "#zto-date-panel .custom-row label{font-size:13px;color:#666}" + "#zto-date-panel .custom-row input[type=date]{padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:14px;color:#333;outline:none}" + "#zto-date-panel .custom-row input[type=date]:focus{border-color:#ff6a00}" + "#zto-date-panel .action-row{display:flex;gap:12px;justify-content:center}" + "#zto-date-panel .action-row button{padding:8px 28px;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;border:none;transition:all .15s}" + "#zto-date-panel .btn-cancel{background:#f0f0f0;color:#666}" + "#zto-date-panel .btn-cancel:hover{background:#e0e0e0}" + "#zto-date-panel .btn-confirm{background:#ff6a00;color:#fff}" + "#zto-date-panel .btn-confirm:hover{background:#e65c00}" ); /* =========================================================== * 通用工具 * =========================================================== */ function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } function randomDelay() { return sleep(CONFIG.MIN_DELAY + Math.random() * (CONFIG.MAX_DELAY - CONFIG.MIN_DELAY)); } /** 获取列表第一条运单号,作为"指纹"判断列表是否已刷新 */ function getListFingerprint() { var items = document.querySelectorAll(".order-list > .order-item-bg"); if (items.length === 0) return null; var code = items[0].querySelector(".item-header .bill-code"); return code ? code.textContent.trim() : null; } /** 等待列表渲染(出现任意 .order-item-bg 即可) */ async function waitForList(timeoutMs) { var deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (document.querySelectorAll(".order-list > .order-item-bg").length > 0) return true; await sleep(200); } return false; } /** 判断翻页按钮「下一页」是否已禁用(末端) */ function isNextDisabled() { var btn = document.querySelector(".pagination-cont .btn-next"); if (!btn) return true; return btn.classList.contains("is-disabled") || btn.getAttribute("aria-disabled") === "true" || btn.disabled; } /** 跳转到指定页码 */ async function goToPage(pageNum) { var allPages = document.querySelectorAll(".pagination-cont .number"); for (var i = 0; i < allPages.length; i++) { if (allPages[i].textContent.trim() === String(pageNum)) { if (allPages[i].classList.contains("is-active")) return true; allPages[i].click(); await sleep(600); return true; } } return false; } /** 获取当前页码 */ function getCurrentPage() { var active = document.querySelector(".pagination-cont .number.is-active"); return active ? parseInt(active.textContent.trim(), 10) : 1; } /** 解析页面上的下单时间字符串 → Date 对象 */ function parseOrderTime(str) { if (!str) return null; var parts = str.split(" "); if (parts.length !== 2) return null; var d = parts[0].split("-"), t = parts[1].split(":"); if (d.length !== 3 || t.length !== 3) return null; return new Date( parseInt(d[0],10), parseInt(d[1],10)-1, parseInt(d[2],10), parseInt(t[0],10), parseInt(t[1],10), parseInt(t[2],10) ); } /* =========================================================== * 数据提取(从当前页 DOM 读取) * =========================================================== */ function extractPageData() { var items = document.querySelectorAll(".order-list > .order-item-bg"); var pageData = []; for (var i = 0; i < items.length; i++) { try { var item = items[i]; var billCodeEl = item.querySelector(".item-header .bill-code"); var headerTimeEl = item.querySelector(".item-header .header-time"); // ---- 运单号 ---- var waybillNo = ""; if (billCodeEl) waybillNo = billCodeEl.textContent.trim().replace(/^运单号[::]\s*/, "").trim(); // ---- 下单时间 ---- var orderTime = ""; if (headerTimeEl) orderTime = headerTimeEl.textContent.trim().replace(/^下单时间[::]\s*/, "").trim(); // ---- 寄件/收件信息 ---- var body = item.querySelector(".item-body"); var senderCity = "", senderName = "", status = ""; var receiverCity = "", receiverName = ""; if (body) { var left = body.querySelector(".left"); var center = body.querySelector(".center"); var right = body.querySelector(".right"); if (left) { var c1 = left.querySelector(".city"), n1 = left.querySelector(".name"); if (c1) senderCity = c1.textContent.trim(); if (n1) senderName = n1.textContent.trim(); } if (center) { var s = center.querySelector(".bill-status"); if (s) status = s.textContent.trim(); } if (right) { var c2 = right.querySelector(".city"), n2 = right.querySelector(".name"); if (c2) receiverCity = c2.textContent.trim(); if (n2) receiverName = n2.textContent.trim(); } } if (!waybillNo) continue; // 跳过无运单号的无效行 pageData.push({ 运单号: waybillNo, 寄件下单时间: orderTime, 收件人姓名: receiverName, 收件手机号: "", 收件详细地址: "", 包裹始发地: senderCity, 目的地: receiverCity, 物流当前状态: status, 签收时间: "", 派送员信息: "", 寄件人: senderName, _orderDate: parseOrderTime(orderTime), // 客户端过滤用 }); } catch(e) { console.warn("[中通导出] 跳过异常条目:", e); } } return pageData; } /* =========================================================== * 翻页采集 + 客户端日期过滤 + 提前截断 * * 列表按时间倒序排列(最新在前), * 一旦某页最旧记录 < 选中日期下限 ⇒ 后续只会更旧 ⇒ 立即中止 * =========================================================== */ async function collectAllPages(progressEl, range) { var allData = []; var seen = {}; // 运单号去重 var page = 1; // 规范化日期范围 var rangeMin = null, rangeMax = null; if (range) { rangeMin = toStartDate(range.start); // 下限 00:00 rangeMax = toEndDate(range.end); // 上限 23:59 } function updateProgress() { progressEl.textContent = "📦 第 " + page + " 页 | 已采集 " + allData.length + " 条"; } // 先确保在第 1 页 if (getCurrentPage() !== 1) { progressEl.textContent = "⏳ 回到第 1 页…"; await goToPage(1); await sleep(500); await waitForList(CONFIG.RENDER_TIMEOUT); } while (true) { // 等待当前页列表渲染 var ready = await waitForList(CONFIG.RENDER_TIMEOUT); if (!ready) { console.warn("[中通导出] 第 " + page + " 页未渲染"); alert("⚠️ 第 " + page + " 页加载失败,请检查页面后继续"); break; } // 校验页码,如果不对就跳转 var actualPage = getCurrentPage(); if (actualPage !== page) { console.warn("[中通导出] 页码不匹配:" + page + " vs " + actualPage); var jumped = await goToPage(page); if (jumped) await sleep(500); ready = await waitForList(CONFIG.RENDER_TIMEOUT); if (!ready) break; } // 稍等让 DOM 稳定 await sleep(400); // 提取本页数据 var pageRecords = extractPageData(); var filteredPage = []; var minDateThisPage = null; // 本页最旧日期,用于截断判断 for (var r = 0; r < pageRecords.length; r++) { var rec = pageRecords[r]; if (seen[rec.运单号]) continue; // 去重 seen[rec.运单号] = true; var d = rec._orderDate; // 追踪本页最旧日期 if (d && (!minDateThisPage || d < minDateThisPage)) { minDateThisPage = d; } // 客户端日期过滤 if (!rangeMin && !rangeMax) { // 无范围限制,全部纳入 filteredPage.push(rec); } else if (rangeMin && rangeMax && d) { if (d >= rangeMin && d <= rangeMax) { filteredPage.push(rec); } } } // 将本页有效数据加入最终结果 for (var f = 0; f < filteredPage.length; f++) { allData.push(filteredPage[f]); } updateProgress(); // ★ 提前截断:如果本页最旧记录已小于范围下限,停止翻页 if (rangeMin && minDateThisPage && minDateThisPage < rangeMin) { console.log("[中通导出] 第 " + page + " 页出现 < " + fmtDate(rangeMin) + " 的旧数据,中断翻页"); break; } // 翻页末端检查 if (isNextDisabled()) break; var nextBtn = document.querySelector(".pagination-cont .btn-next"); if (!nextBtn) { alert("⚠️ 未找到翻页按钮"); break; } await randomDelay(); nextBtn.click(); page++; } // 移除内部过滤字段 for (var i = 0; i < allData.length; i++) delete allData[i]._orderDate; return allData; } /* =========================================================== * CSV 导出 * * 重点: * 1. UTF-8 BOM(\uFEFF),Excel 直接打开不乱码 * 2. 所有字段强制双引号包裹 * 3. 运单号等12位以上纯数字用 Excel 公式 ="" 包裹,彻底避免科学计数法 * 4. 字段内的双引号转义为 "" * =========================================================== */ function exportCSV(data) { if (data.length === 0) { alert("⚠️ 没有采集到任何快递数据"); return; } /* ---- 1. 定义表头(控制输出列的顺序) ---- */ var fields = [ "运单号", "寄件下单时间", "收件人姓名", "收件手机号", "收件详细地址", "包裹始发地", "目的地", "物流当前状态", "签收时间", "派送员信息", "寄件人", ]; /** * CSV 字段转义函数。 * - 双引号 → "" * - 整个字段用双引号包裹 * - 如果字段是 12 位以上纯数字(如运单号),用 Excel 公式 ="" 包裹, * Excel 按文本公式解析,绝不会转科学计数法,也不会显示多余字符 */ function csvField(val) { if (val == null) return '""'; var s = String(val); // 双引号转义 s = s.replace(/"/g, '""'); // ★ 12位以上纯数字 → 用 Excel 公式 ="" 包裹 // ="" 是 Excel 文本公式,显示结果无多余字符,绝不转科学计数法 if (/^\d{12,}$/.test(s)) { return '="' + s + '"'; } // 其他字段:整个用双引号包裹 return '"' + s + '"'; } /* ---- 2. 组装 CSV ---- */ // 表头行 var header = fields.map(function(f) { return csvField(f); }).join(","); // 数据行 var rows = []; for (var i = 0; i < data.length; i++) { var row = fields.map(function(f) { return csvField(data[i][f]); }); rows.push(row.join(",")); } // UTF-8 BOM + CSV 正文 var bom = "\uFEFF"; var csv = bom + header + "\n" + rows.join("\n"); var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); /* ---- 3. 下载 ---- */ if (typeof GM_download !== "undefined") { // Tampermonkey / ScriptCat GM_download({ url: URL.createObjectURL(blob), name: CONFIG.FILE_NAME, saveAs: true, }); } else { // 降级:创建 标签触发下载 var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = CONFIG.FILE_NAME; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } /* =========================================================== * 主流程 * =========================================================== */ function main() { /* ---- 日期选择面板 ---- */ var panel = document.createElement("div"); panel.id = "zto-date-panel"; panel.innerHTML = '
' + '
📅 选择下单时间范围
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(panel); /* ---- 快捷预设按钮 ---- */ var presetRow = document.getElementById("zto-preset-row"); var presetBtns = []; for (var pi = 0; pi < PRESETS.length; pi++) { (function(p, i) { var btn = document.createElement("button"); btn.className = "preset-btn"; btn.textContent = p.label; btn.dataset.index = i; presetRow.appendChild(btn); presetBtns.push(btn); })(PRESETS[pi], pi); } /* ---- 导出按钮(右下角橙色悬浮球) ---- */ var btn = document.createElement("button"); btn.id = "zto-export-btn"; btn.textContent = "📋 导出全部寄出快递"; document.body.appendChild(btn); /* ---- 进度浮窗 ---- */ var progress = document.createElement("div"); progress.id = "zto-export-progress"; document.body.appendChild(progress); /* ---- 状态 ---- */ var selectedRange = null; // { start: Date, end: Date } /* ---- 预设按钮交互 ---- */ for (var bi = 0; bi < presetBtns.length; bi++) { (function(pbtn) { pbtn.addEventListener("click", function() { for (var b = 0; b < presetBtns.length; b++) presetBtns[b].classList.remove("active"); this.classList.add("active"); var idx = parseInt(this.dataset.index, 10); selectedRange = PRESETS[idx].fn(); document.getElementById("zto-date-start").value = fmtDate(selectedRange.start); document.getElementById("zto-date-end").value = fmtDate(selectedRange.end); }); })(presetBtns[bi]); } /* ---- 自定义日期输入 ---- */ var startInput = document.getElementById("zto-date-start"); var endInput = document.getElementById("zto-date-end"); startInput.addEventListener("change", syncCustomRange); endInput.addEventListener("change", syncCustomRange); function syncCustomRange() { for (var b = 0; b < presetBtns.length; b++) presetBtns[b].classList.remove("active"); var sv = startInput.value; var ev = endInput.value; if (sv && ev) { selectedRange = { start: toStartDate(sv), end: toEndDate(ev) }; } else { selectedRange = null; } } /* ---- 面板显隐 ---- */ function showPanel() { panel.classList.add("show"); } function hidePanel() { panel.classList.remove("show"); } document.getElementById("zto-date-cancel").addEventListener("click", hidePanel); panel.addEventListener("click", function(e) { if (e.target === this) hidePanel(); }); /* ---- 点击导出 ---- */ btn.addEventListener("click", function() { if (btn.classList.contains("is-running")) return; // 检查是否在「我寄的」视图 var activeTab = document.querySelector(".subpage-tab-item.active"); if (!activeTab || activeTab.textContent.trim() !== "我寄的") { alert('⚠️ 当前不是"我寄的"视图\n请先点击页面上方的"我寄的"标签。'); return; } // 重置选择状态 selectedRange = null; for (var b = 0; b < presetBtns.length; b++) presetBtns[b].classList.remove("active"); startInput.value = ""; endInput.value = ""; showPanel(); }); /* ---- 确认并导出 ---- */ document.getElementById("zto-date-confirm").addEventListener("click", async function() { if (!selectedRange) { alert("⚠️ 请先选择时间范围"); return; } hidePanel(); btn.classList.add("is-running"); progress.classList.add("show"); try { progress.textContent = "📦 采集数据(不修改页面日期)…"; btn.textContent = "⏳ 采集中…"; var allData = await collectAllPages(progress, selectedRange); progress.textContent = "✅ 共 " + allData.length + " 条"; btn.textContent = "✅ 导出完成"; exportCSV(allData); } catch(err) { console.error("[中通导出] 采集失败:", err); progress.textContent = "❌ 出错,查看控制台(F12)"; alert("❌ 采集失败,请查看浏览器控制台(F12)获取详情。"); } finally { setTimeout(function() { btn.classList.remove("is-running"); btn.textContent = "📋 导出全部寄出快递"; progress.classList.remove("show"); }, 5000); } }); console.log("[中通导出] v4.0 加载完成 — csvField() 处理长数字+全字段引号包裹"); } /* ---- 启动 ---- */ if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", main); } else { main(); } })();