// ==UserScript== // @name 中通快递 - 导出寄出快递记录 // @namespace http://tampermonkey.net/ // @version 2.1 // @description 选择下单时间范围后,自动采集全部寄出快递列表并导出CSV // @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, MAX_DELAY: 800, RENDER_TIMEOUT: 10000, FILE_NAME: "中通寄件记录_" + new Date().toISOString().slice(0, 10).replace(/-/g, "") + ".csv", }; // ============================================================ // 日期工具 // ============================================================ function pad2(n) { return n < 10 ? "0" + n : "" + n; } function fmtDate(d) { return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()); } function fmtDateTime(d, endOfDay) { return fmtDate(d) + (endOfDay ? " 23:59:59" : " 00:00:00"); } 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; } var PRESETS = [ { label: "今天", getRange: function() { var d = today(); return { start: fmtDateTime(d, false), end: fmtDateTime(d, true) }; } }, { label: "昨天", getRange: function() { var d = yesterday(); return { start: fmtDateTime(d, false), end: fmtDateTime(d, true) }; } }, { label: "前天", getRange: function() { var d = dayBefore(); return { start: fmtDateTime(d, false), end: fmtDateTime(d, true) }; } }, { label: "近2天", getRange: function() { var s = new Date(); s.setDate(s.getDate() - 1); var e = today(); return { start: fmtDateTime(s, false), end: fmtDateTime(e, true) }; } }, { label: "近7天", getRange: function() { var s = new Date(); s.setDate(s.getDate() - 6); var e = today(); return { start: fmtDateTime(s, false), end: fmtDateTime(e, true) }; } }, { label: "近30天", getRange: function() { var s = new Date(); s.setDate(s.getDate() - 29); var e = today(); return { start: fmtDateTime(s, false), end: fmtDateTime(e, true) }; } }, ]; // ============================================================ // 样式 // ============================================================ 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}" + "#zto-date-panel .btn-confirm:disabled{background:#ccc;cursor:not-allowed}" ); // ============================================================ // 工具函数 // ============================================================ 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)); } 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; } // ============================================================ // 日期设置 — 点击日历交互(可靠) // ============================================================ /** * 通过点击 Element Plus 日期选择器弹出层来设置日期范围 * 这比原生 setter 更可靠,因为 Vue 组件能正确响应 click 事件 */ async function setDateViaCalendar(startDateStr, endDateStr) { // 解析日期: "2026-06-01 00:00:00" → { y, m, d } function parse(s) { var p = s.split(" ")[0].split("-"); return { y: parseInt(p[0], 10), m: parseInt(p[1], 10), d: parseInt(p[2], 10) }; } var start = parse(startDateStr); var end = parse(endDateStr); // ---- 1. 点击日期输入框,打开弹窗 ---- var picker = document.querySelector(".search-date-picker"); if (!picker) return false; picker.click(); await sleep(600); // ---- 2. 点击"清空" ---- var allBtns = document.querySelectorAll(".el-picker-panel button"); var cleared = false; allBtns.forEach(function(b) { if (b.textContent.trim() === "清空") { b.click(); cleared = true; } }); if (!cleared) { // 如果没找到清空按钮(可能已清空),尝试点击"确定"先关闭再来一次 document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); await sleep(300); picker.click(); await sleep(600); } await sleep(300); // ---- 3. 获取当前显示的月份 ---- function getPanelMonth(panelEl) { var labels = panelEl.querySelectorAll(".el-date-range-picker__header-label"); if (labels.length < 2) return null; var year = parseInt(labels[0].textContent.trim(), 10); // 月份可能是英文缩写 Jan/Feb/... 或中文 1月/2月/... var monthText = labels[1].textContent.trim().toLowerCase(); var monthMap = { "jan":1,"january":1,"feb":2,"february":2,"mar":3,"march":3, "apr":4,"april":4,"may":5,"jun":6,"june":6, "jul":7,"july":7,"aug":8,"august":8,"sep":9,"september":9, "oct":10,"october":10,"nov":11,"november":11,"dec":12,"december":12, "1月":1,"2月":2,"3月":3,"4月":4,"5月":5,"6月":6, "7月":7,"8月":8,"9月":9,"10月":10,"11月":11,"12月":12, }; var month = monthMap[monthText]; if (!month) { // 尝试数字提取 var m = parseInt(monthText, 10); if (m >= 1 && m <= 12) month = m; } return month ? { year: year, month: month } : null; } // ---- 4. 导航到目标月份 ---- async function navigateTo(targetYear, targetMonth, panelIndex) { var panels = document.querySelectorAll(".el-date-range-picker__content"); if (panels.length <= panelIndex) return false; var panel = panels[panelIndex]; var current = getPanelMonth(panel); if (!current) return false; var maxIter = 24; while (maxIter-- > 0) { current = getPanelMonth(panel); if (!current) return false; if (current.year === targetYear && current.month === targetMonth) return true; // 判断方向 var diffMonths = (targetYear - current.year) * 12 + (targetMonth - current.month); if (diffMonths > 0) { // 需要往后翻 — 点击"下个月"按钮 var nextBtns = panel.querySelectorAll("button"); var found = false; nextBtns.forEach(function(b) { if (b.classList.contains("arrow-right") || b.getAttribute("aria-label") === "下个月") { b.click(); found = true; } }); if (!found) return false; } else { // 需要往前翻 — 点击"上个月"按钮 var prevBtns = panel.querySelectorAll("button"); var found = false; prevBtns.forEach(function(b) { if (b.classList.contains("arrow-left") || b.getAttribute("aria-label") === "上个月") { b.click(); found = true; } }); if (!found) return false; } await sleep(200); } return false; } // ---- 5. 点击指定日期的单元格 ---- function clickDayCell(panelIndex, day) { var panels = document.querySelectorAll(".el-date-range-picker__content"); if (panels.length <= panelIndex) return false; var panel = panels[panelIndex]; var cells = panel.querySelectorAll(".el-date-table-cell__text"); var found = false; cells.forEach(function(cell) { if (cell.textContent.trim() === String(day)) { cell.click(); found = true; } }); return found; } // ---- 6. 执行:先设开始日期,再设结束日期 ---- // 先导航左侧面板到 start 月份 var ok = await navigateTo(start.y, start.m, 0); if (!ok) return false; await sleep(200); // 点击开始日期 ok = clickDayCell(0, start.d); if (!ok) return false; await sleep(300); // 导航右侧面板到 end 月份(一般在右面板,同月则在左面板) var panels = document.querySelectorAll(".el-date-range-picker__content"); // 检查 end 是否已在右面板可见 var rightPanelMonth = panels.length > 1 ? getPanelMonth(panels[1]) : null; if (rightPanelMonth && rightPanelMonth.year === end.y && rightPanelMonth.month === end.m) { // 已正确显示 } else { // 检查左面板是否显示 end 月份 var leftPanelMonth = getPanelMonth(panels[0]); if (leftPanelMonth && leftPanelMonth.year === end.y && leftPanelMonth.month === end.m) { // 在左面板上点击结束日期 ok = clickDayCell(0, end.d); } else if (panels.length > 1) { // 导航右面板 await navigateTo(end.y, end.m, 1); await sleep(200); ok = clickDayCell(1, end.d); } else { return false; } } if (!ok) return false; await sleep(300); // ---- 7. 点击"确定" ---- var confirmBtns = document.querySelectorAll(".el-picker-panel button"); var confirmed = false; confirmBtns.forEach(function(b) { if (b.textContent.trim() === "确定") { b.click(); confirmed = true; } }); await sleep(400); return confirmed; } // ============================================================ // 设置页面日期 — 主入口 // ============================================================ async function setDateOnPage(startStr, endStr) { // 方法一:尝试用原生 setter(部分 Vue 版本有效) try { var inputs = document.querySelectorAll(".el-range-input"); if (inputs.length < 2) throw new Error("no inputs"); var nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, "value" ).set; nativeSetter.call(inputs[0], startStr); inputs[0].dispatchEvent(new Event("input", { bubbles: true })); nativeSetter.call(inputs[1], endStr); inputs[1].dispatchEvent(new Event("input", { bubbles: true })); inputs[0].dispatchEvent(new Event("change", { bubbles: true })); inputs[1].dispatchEvent(new Event("change", { bubbles: true })); inputs[0].dispatchEvent(new Event("blur", { bubbles: true })); inputs[1].dispatchEvent(new Event("blur", { bubbles: true })); await sleep(500); // 验证是否生效 var v1 = inputs[0].value; var v2 = inputs[1].value; if (v1 === startStr && v2 === endStr) { // 尝试在输入框按 Enter inputs[0].dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); inputs[1].dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); await sleep(300); return true; } } catch(e) { console.warn("[中通导出] native setter 失败,切换到日历导航:", e); } // 方法二:通过日历弹窗交互(更可靠) console.log("[中通导出] 使用日历导航设置日期"); return await setDateViaCalendar(startStr, endStr); } // ============================================================ // 数据提取 // ============================================================ function extractPageData() { var items = document.querySelectorAll(".order-list > .order-item-bg"); var pageData = []; items.forEach(function(item) { try { var billCodeEl = item.querySelector(".item-header .bill-code"); var headerTimeEl = item.querySelector(".item-header .header-time"); var billCodeText = billCodeEl ? billCodeEl.textContent.trim() : ""; var headerTimeText = headerTimeEl ? headerTimeEl.textContent.trim() : ""; var waybillNo = billCodeText.replace(/^运单号[::]\s*/, "").trim(); var orderTime = headerTimeText.replace(/^下单时间[::]\s*/, "").trim(); var body = item.querySelector(".item-body"); var senderCity = "", senderName = "", status = "", 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"); var 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"); var n2 = right.querySelector(".name"); if (c2) receiverCity = c2.textContent.trim(); if (n2) receiverName = n2.textContent.trim(); } } if (!waybillNo) return; pageData.push({ 运单号: waybillNo, 寄件下单时间: orderTime, 收件人姓名: receiverName, 收件手机号: "", 收件详细地址: "", 包裹始发地: senderCity, 目的地: receiverCity, 物流当前状态: status, 签收时间: "", 派送员信息: "", 寄件人: senderName, }); } catch(e) { console.warn("[中通导出] 跳过异常条目:", e); } }); return pageData; } // ============================================================ // 翻页采集 // ============================================================ async function collectAllPages(progressEl) { var allData = []; var seen = {}; var page = 1; function updateProgress() { progressEl.textContent = "📦 采集进度\n当前第 " + page + " 页 | 已采集 " + allData.length + " 条"; } while (true) { var ready = await waitForList(CONFIG.RENDER_TIMEOUT); if (!ready) { console.warn("[中通导出] 第 " + page + " 页列表未渲染"); alert("⚠️ 第 " + page + " 页加载失败,请手动检查页面后点击确定继续"); break; } // 等待额外时间确保 DOM 完全渲染 await sleep(500); var pageRecords = extractPageData(); pageRecords.forEach(function(rec) { if (!seen[rec.运单号]) { seen[rec.运单号] = true; allData.push(rec); } }); updateProgress(); if (isNextDisabled()) break; var nextBtn = document.querySelector(".pagination-cont .btn-next"); if (!nextBtn) { alert("⚠️ 未找到翻页按钮,请手动翻页后点击确定继续"); break; } await randomDelay(); nextBtn.click(); page++; } return allData; } // ============================================================ // CSV 导出(UTF-8 BOM) // ============================================================ function exportCSV(data) { if (data.length === 0) { alert("⚠️ 没有采集到任何快递数据"); return; } var fields = [ "运单号", "寄件下单时间", "收件人姓名", "收件手机号", "收件详细地址", "包裹始发地", "目的地", "物流当前状态", "签收时间", "派送员信息", "寄件人", ]; function esc(v) { if (v == null) return ""; var s = String(v); if (s.indexOf(",") >= 0 || s.indexOf("\"") >= 0 || s.indexOf("\n") >= 0 || s.indexOf("\r") >= 0) return "\"" + s.replace(/\"/g, "\"\"") + "\""; return s; } var header = fields.join(","); var rows = data.map(function(r) { return fields.map(function(f) { return esc(r[f] || ""); }).join(","); }); var csv = "\uFEFF" + header + "\n" + rows.join("\n"); var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); if (typeof GM_download !== "undefined") { 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 = []; PRESETS.forEach(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); }); // ---- 创建导出按钮 ---- 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; // ---- 预设按钮点击 ---- presetBtns.forEach(function(pbtn) { pbtn.addEventListener("click", function() { presetBtns.forEach(function(b) { b.classList.remove("active"); }); this.classList.add("active"); var idx = parseInt(this.dataset.index, 10); selectedRange = PRESETS[idx].getRange(); document.getElementById("zto-date-start").value = selectedRange.start.slice(0, 10); document.getElementById("zto-date-end").value = selectedRange.end.slice(0, 10); }); }); // ---- 自定义日期 ---- document.getElementById("zto-date-start").addEventListener("change", syncCustomRange); document.getElementById("zto-date-end").addEventListener("change", syncCustomRange); function syncCustomRange() { presetBtns.forEach(function(b) { b.classList.remove("active"); }); var s = document.getElementById("zto-date-start").value; var e = document.getElementById("zto-date-end").value; if (s && e) { selectedRange = { start: s + " 00:00:00", end: e + " 23:59:59" }; } 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; presetBtns.forEach(function(b) { b.classList.remove("active"); }); document.getElementById("zto-date-start").value = ""; document.getElementById("zto-date-end").value = ""; showPanel(); }); // ---- 确认并导出 ---- document.getElementById("zto-date-confirm").addEventListener("click", async function() { if (!selectedRange) { alert("⚠️ 请先选择时间范围(点击预设按钮或自定义日期)"); return; } hidePanel(); btn.classList.add("is-running"); btn.textContent = "⏳ 设置日期…"; progress.classList.add("show"); progress.textContent = "⏳ 设置日期范围…"; // 1. 设置日期 var ok = await setDateOnPage(selectedRange.start, selectedRange.end); if (!ok) { alert( "⚠️ 自动设置日期失败。\n\n" + "请在页面顶部的日期选择器中手动选择日期范围:\n" + "1. 点击「下单时间」输入框\n" + "2. 选择开始日期和结束日期\n" + "3. 点击「确定」\n" + "4. 点击「查询」\n" + "5. 再点击「📋 导出全部寄出快递」按钮继续" ); btn.classList.remove("is-running"); btn.textContent = "📋 导出全部寄出快递"; progress.classList.remove("show"); return; } await sleep(400); // 2. 点击"查询" progress.textContent = "⏳ 查询列表中…"; var queryBtn = document.querySelector(".z-search-btn"); if (queryBtn) { queryBtn.click(); // 等待较长时间让列表刷新 await sleep(1500); } // 3. 等待列表刷新 var listReady = await waitForList(CONFIG.RENDER_TIMEOUT); if (!listReady) { alert("⚠️ 筛选后未加载出列表数据,请检查日期范围是否有误。"); btn.classList.remove("is-running"); btn.textContent = "📋 导出全部寄出快递"; progress.classList.remove("show"); return; } // 4. 全量采集 btn.textContent = "⏳ 采集中…"; try { var allData = await collectAllPages(progress); progress.textContent = "✅ 采集完成!共 " + allData.length + " 条记录"; btn.textContent = "✅ 导出完成"; exportCSV(allData); } catch(err) { console.error("[中通导出] 采集失败:", err); progress.textContent = "❌ 采集出错,查看控制台"; alert("❌ 采集过程中出错,请查看浏览器控制台(F12)获取详情。"); } finally { setTimeout(function() { btn.classList.remove("is-running"); btn.textContent = "📋 导出全部寄出快递"; progress.classList.remove("show"); }, 5000); } }); console.log("[中通导出] v2.1 已注入 — 新增近2天预设 + 日历导航设置日期(修复信息不全)"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", main); } else { main(); } })();