|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="zh-CN">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<meta name="robots" content="noindex,nofollow">
|
|
|
|
<title>邮件导出</title>
|
|
|
|
<style>
|
|
|
|
*{box-sizing:border-box;margin:0;padding:0;}
|
|
|
|
body{background:#f6f8fb;color:#22303f;font:14px/1.7 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;}
|
|
|
|
.hide{display:none !important;}
|
|
|
|
|
|
|
|
/* 顶栏 */
|
|
|
|
.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:16px;
|
|
|
|
height:56px;padding:0 22px;background:#fff;border-bottom:1px solid #e4eaf2;}
|
|
|
|
.top .logo{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:600;white-space:nowrap;}
|
|
|
|
.top .logo i{display:inline-block;width:8px;height:18px;border-radius:3px;background:#2f6bd8;}
|
|
|
|
.top .meta{color:#93a1b3;font-size:12px;white-space:nowrap;}
|
|
|
|
.top .meta b{color:#5b6b7e;font-weight:500;}
|
|
|
|
.top .grow{flex:1;}
|
|
|
|
.top .quit{color:#8a97a8;font-size:12px;text-decoration:none;border:1px solid #e4eaf2;
|
|
|
|
border-radius:6px;padding:4px 10px;transition:.15s;}
|
|
|
|
.top .quit:hover{color:#d0342c;border-color:#f0c9c6;background:#fdf5f4;}
|
|
|
|
|
|
|
|
main{max-width:940px;margin:0 auto;padding:24px 22px 60px;}
|
|
|
|
|
|
|
|
/* 卡片 */
|
|
|
|
.card{background:#fff;border:1px solid #e4eaf2;border-radius:12px;padding:20px 22px;margin-bottom:16px;}
|
|
|
|
.hd{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap;}
|
|
|
|
.hd .num{width:20px;height:20px;border-radius:50%;background:#eaf1fd;color:#2f6bd8;
|
|
|
|
font-size:12px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;}
|
|
|
|
.hd h2{font-size:15px;font-weight:600;}
|
|
|
|
.hd .tip{color:#93a1b3;font-size:12px;}
|
|
|
|
|
|
|
|
/* 公司项目搜索 */
|
|
|
|
.search{position:relative;}
|
|
|
|
.search input{width:100%;height:42px;padding:0 14px;border:1px solid #dde5ef;border-radius:9px;
|
|
|
|
font-size:14px;outline:none;transition:.15s;background:#fcfdff;color:#22303f;}
|
|
|
|
.search input:focus{border-color:#7ba4e8;box-shadow:0 0 0 3px rgba(47,107,216,.1);background:#fff;}
|
|
|
|
.dropdown{position:absolute;left:0;right:0;top:46px;z-index:30;background:#fff;border:1px solid #e4eaf2;
|
|
|
|
border-radius:10px;box-shadow:0 12px 30px rgba(31,63,107,.12);max-height:320px;overflow:auto;}
|
|
|
|
.dd-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;border-bottom:1px solid #f2f6fb;}
|
|
|
|
.dd-item:last-child{border-bottom:0;}
|
|
|
|
.dd-item:hover,.dd-item.on{background:#f4f8ff;}
|
|
|
|
.dd-main{flex:1;min-width:0;}
|
|
|
|
.dd-company{font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
|
|
.dd-sub{color:#93a1b3;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
|
|
.dd-sub span+span:before{content:"·";margin:0 6px;color:#c9d4e2;}
|
|
|
|
.dd-bind{color:#8a97a8;font-size:12px;white-space:nowrap;}
|
|
|
|
.dd-bind b{color:#2f6bd8;font-weight:600;}
|
|
|
|
.dd-empty{padding:14px;color:#93a1b3;font-size:13px;text-align:center;}
|
|
|
|
|
|
|
|
.picked{display:flex;align-items:center;gap:14px;padding:12px 14px;margin-top:12px;
|
|
|
|
background:#f4f8ff;border:1px solid #dbe7fb;border-radius:10px;}
|
|
|
|
.picked .picked-main{flex:1;min-width:0;}
|
|
|
|
.picked .company{font-size:14px;font-weight:600;color:#1b3a66;}
|
|
|
|
.picked .sub{color:#6b7c90;font-size:12px;margin-top:2px;}
|
|
|
|
.picked .sub span+span:before{content:"·";margin:0 6px;color:#c9d4e2;}
|
|
|
|
.picked .link{border:0;background:none;color:#2f6bd8;font-size:12.5px;cursor:pointer;padding:4px 2px;}
|
|
|
|
.picked .link:hover{text-decoration:underline;}
|
|
|
|
|
|
|
|
/* 导出条件 */
|
|
|
|
.scopes{display:flex;gap:12px;flex-wrap:wrap;}
|
|
|
|
.scope{flex:1;min-width:240px;display:flex;gap:10px;align-items:flex-start;padding:14px 16px;
|
|
|
|
border:1px solid #e2e9f3;border-radius:10px;cursor:pointer;transition:.15s;background:#fcfdff;}
|
|
|
|
.scope:hover{border-color:#c8d9f3;background:#fafcff;}
|
|
|
|
.scope input{margin-top:4px;accent-color:#2f6bd8;}
|
|
|
|
.scope b{display:block;font-size:13.5px;font-weight:600;}
|
|
|
|
.scope span{display:block;color:#8a97a8;font-size:12px;margin-top:2px;}
|
|
|
|
.scope.on{border-color:#7ba4e8;background:#f4f8ff;box-shadow:0 0 0 3px rgba(47,107,216,.08);}
|
|
|
|
|
|
|
|
.row{display:flex;align-items:center;gap:18px;margin-top:16px;flex-wrap:wrap;}
|
|
|
|
.field{display:flex;align-items:center;gap:8px;font-size:13px;color:#5b6b7e;}
|
|
|
|
.field input{width:96px;height:34px;padding:0 10px;border:1px solid #dde5ef;border-radius:8px;
|
|
|
|
font-size:13px;outline:none;color:#22303f;}
|
|
|
|
.field input:focus{border-color:#7ba4e8;box-shadow:0 0 0 3px rgba(47,107,216,.1);}
|
|
|
|
.field em{font-style:normal;color:#93a1b3;}
|
|
|
|
.hit{font-size:13px;color:#5b6b7e;}
|
|
|
|
.hit b{color:#2f6bd8;font-weight:600;}
|
|
|
|
.hit .zero{color:#c07a00;}
|
|
|
|
|
|
|
|
/* 操作 */
|
|
|
|
.action{display:flex;align-items:center;gap:14px;margin:4px 0 20px;flex-wrap:wrap;}
|
|
|
|
.primary{height:42px;padding:0 30px;border:0;border-radius:9px;background:#2f6bd8;color:#fff;
|
|
|
|
font-size:14px;font-weight:500;cursor:pointer;transition:.15s;}
|
|
|
|
.primary:hover{background:#255cc0;}
|
|
|
|
.primary:disabled{background:#c3d2e6;cursor:not-allowed;}
|
|
|
|
.hint{color:#93a1b3;font-size:12px;}
|
|
|
|
|
|
|
|
/* 结果 */
|
|
|
|
.result .ok{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:600;color:#1a9c62;}
|
|
|
|
.result .ok i{width:20px;height:20px;border-radius:50%;background:#e6f6ee;color:#1a9c62;
|
|
|
|
display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-style:normal;}
|
|
|
|
.stats{display:flex;gap:26px;margin:16px 0 4px;flex-wrap:wrap;}
|
|
|
|
.stat{font-size:12px;color:#8a97a8;}
|
|
|
|
.stat b{display:block;font-size:19px;color:#22303f;font-weight:600;line-height:1.4;}
|
|
|
|
.stat b.warn{color:#d0342c;}
|
|
|
|
.dl{display:inline-flex;align-items:center;gap:8px;height:42px;padding:0 24px;margin-top:16px;
|
|
|
|
border-radius:9px;background:#2f6bd8;color:#fff;font-size:14px;font-weight:500;text-decoration:none;
|
|
|
|
transition:.15s;}
|
|
|
|
.dl:hover{background:#255cc0;}
|
|
|
|
.path{margin-top:12px;color:#93a1b3;font-size:12px;word-break:break-all;}
|
|
|
|
.path code{color:#5b6b7e;background:#f4f7fb;border-radius:4px;padding:2px 6px;}
|
|
|
|
.errbox{margin-top:16px;border-top:1px dashed #e4eaf2;padding-top:14px;}
|
|
|
|
.errbox h3{font-size:13px;font-weight:600;color:#d0342c;margin-bottom:8px;}
|
|
|
|
.errlist{max-height:220px;overflow:auto;border:1px solid #f0e2e1;border-radius:8px;background:#fffbfb;}
|
|
|
|
table{width:100%;border-collapse:collapse;font-size:12.5px;}
|
|
|
|
th,td{text-align:left;padding:7px 10px;border-bottom:1px solid #f4ebea;vertical-align:top;}
|
|
|
|
th{color:#a08a89;font-weight:500;background:#fdf6f5;position:sticky;top:0;}
|
|
|
|
td.sub{color:#5b6b7e;}
|
|
|
|
|
|
|
|
/* 遮罩 */
|
|
|
|
.mask{position:fixed;inset:0;z-index:50;background:rgba(229,237,247,.72);backdrop-filter:blur(2px);
|
|
|
|
display:flex;align-items:center;justify-content:center;}
|
|
|
|
.mask-box{background:#fff;border:1px solid #e4eaf2;border-radius:14px;padding:26px 34px;text-align:center;
|
|
|
|
box-shadow:0 18px 50px rgba(31,63,107,.16);min-width:260px;}
|
|
|
|
.spinner{width:30px;height:30px;margin:0 auto 14px;border:3px solid #e3ecfa;border-top-color:#2f6bd8;
|
|
|
|
border-radius:50%;animation:spin .8s linear infinite;}
|
|
|
|
@keyframes spin{to{transform:rotate(360deg);}}
|
|
|
|
.mask-box p{font-size:13.5px;color:#22303f;}
|
|
|
|
.mask-box .mask-sub{margin-top:6px;color:#93a1b3;font-size:12px;}
|
|
|
|
|
|
|
|
@media (max-width:640px){
|
|
|
|
.top .meta{display:none;}
|
|
|
|
main{padding:16px 12px 40px;}
|
|
|
|
.card{padding:16px;}
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
|
|
|
|
<header class="top">
|
|
|
|
<div class="logo"><i></i>邮件导出</div>
|
|
|
|
<div class="meta">导出为 eml · 打包 zip 下载</div>
|
|
|
|
<div class="grow"></div>
|
|
|
|
<div class="meta"><b><!--{#host#}--></b> · <!--{#time#}--></div>
|
|
|
|
<a class="quit" href="/mail-export/logout">退出</a>
|
|
|
|
</header>
|
|
|
|
|
|
|
|
<main>
|
|
|
|
<section class="card">
|
|
|
|
<div class="hd">
|
|
|
|
<span class="num">1</span>
|
|
|
|
<h2>选择公司项目</h2>
|
|
|
|
<span class="tip">输入公司名称模糊搜索,或直接输入 postid</span>
|
|
|
|
</div>
|
|
|
|
<div class="search" id="searchWrap">
|
|
|
|
<input id="keyword" type="text" placeholder="公司项目名称 / postid" autocomplete="off" spellcheck="false">
|
|
|
|
<div class="dropdown hide" id="dropdown"></div>
|
|
|
|
</div>
|
|
|
|
<div class="picked hide" id="picked">
|
|
|
|
<div class="picked-main">
|
|
|
|
<div class="company" id="pickedCompany"></div>
|
|
|
|
<div class="sub">
|
|
|
|
<span id="pickedPostid"></span>
|
|
|
|
<span id="pickedPlan"></span>
|
|
|
|
<span id="pickedBind"></span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<button type="button" class="link" id="clearProject">重新选择</button>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<section class="card">
|
|
|
|
<div class="hd">
|
|
|
|
<span class="num">2</span>
|
|
|
|
<h2>导出条件</h2>
|
|
|
|
<span class="tip">数据源与命令行脚本一致:仅 AI 邮箱,已排除删除的邮件</span>
|
|
|
|
</div>
|
|
|
|
<div class="scopes" id="scopes">
|
|
|
|
<label class="scope" data-scope="starred">
|
|
|
|
<input type="radio" name="scope" value="starred">
|
|
|
|
<div>
|
|
|
|
<b>导出星标</b>
|
|
|
|
<span>所有文件夹里带星标的邮件</span>
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
<label class="scope on" data-scope="inbox">
|
|
|
|
<input type="radio" name="scope" value="inbox" checked>
|
|
|
|
<div>
|
|
|
|
<b>收件箱所有</b>
|
|
|
|
<span>收件箱里全部邮件,含星标、未读</span>
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
<div class="row">
|
|
|
|
<label class="field">
|
|
|
|
<span>本次最多导出</span>
|
|
|
|
<input id="limit" type="number" min="1" max="2000" value="300">
|
|
|
|
<em>封</em>
|
|
|
|
</label>
|
|
|
|
<div class="hit" id="hit">选择公司项目后可查看命中数量</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<div class="action">
|
|
|
|
<button class="primary" id="start" disabled>开始导出</button>
|
|
|
|
<span class="hint">同步执行,数量越多越久,期间请不要关闭页面</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<section class="card result hide" id="result">
|
|
|
|
<div class="hd" style="margin-bottom:10px;">
|
|
|
|
<span class="num" style="background:#e6f6ee;color:#1a9c62;">✓</span>
|
|
|
|
<h2>导出完成</h2>
|
|
|
|
<span class="tip" id="resCompany"></span>
|
|
|
|
</div>
|
|
|
|
<div class="stats">
|
|
|
|
<div class="stat">命中邮件<b id="resTotal">0</b></div>
|
|
|
|
<div class="stat">成功导出<b id="resSuccess">0</b></div>
|
|
|
|
<div class="stat">失败<b id="resFail" class="warn">0</b></div>
|
|
|
|
<div class="stat">耗时<b id="resCost">0s</b></div>
|
|
|
|
<div class="stat">压缩包<b id="resSize">0</b></div>
|
|
|
|
</div>
|
|
|
|
<a class="dl" id="resDownload" href="#" download>下载 zip 压缩包</a>
|
|
|
|
<div class="path">导出目录:<code id="resDir"></code></div>
|
|
|
|
<div class="errbox hide" id="errBox">
|
|
|
|
<h3>部分邮件导出失败</h3>
|
|
|
|
<div class="errlist">
|
|
|
|
<table>
|
|
|
|
<thead><tr><th style="width:52%">主题</th><th>UUID</th><th>原因</th></tr></thead>
|
|
|
|
<tbody id="errBody"></tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
|
|
|
|
|
|
<div class="mask hide" id="mask">
|
|
|
|
<div class="mask-box">
|
|
|
|
<div class="spinner"></div>
|
|
|
|
<p id="maskText">正在导出…</p>
|
|
|
|
<p class="mask-sub" id="maskSub">已用时 0s</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
(function () {
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var API = {
|
|
|
|
projects: '/mail-export/projects',
|
|
|
|
count: '/mail-export/count',
|
|
|
|
start: '/mail-export/start'
|
|
|
|
};
|
|
|
|
|
|
|
|
var el = {
|
|
|
|
keyword: document.getElementById('keyword'),
|
|
|
|
dropdown: document.getElementById('dropdown'),
|
|
|
|
picked: document.getElementById('picked'),
|
|
|
|
pickedCompany: document.getElementById('pickedCompany'),
|
|
|
|
pickedPostid: document.getElementById('pickedPostid'),
|
|
|
|
pickedPlan: document.getElementById('pickedPlan'),
|
|
|
|
pickedBind: document.getElementById('pickedBind'),
|
|
|
|
clearProject: document.getElementById('clearProject'),
|
|
|
|
scopes: document.getElementById('scopes'),
|
|
|
|
limit: document.getElementById('limit'),
|
|
|
|
hit: document.getElementById('hit'),
|
|
|
|
start: document.getElementById('start'),
|
|
|
|
result: document.getElementById('result'),
|
|
|
|
mask: document.getElementById('mask'),
|
|
|
|
maskText: document.getElementById('maskText'),
|
|
|
|
maskSub: document.getElementById('maskSub')
|
|
|
|
};
|
|
|
|
|
|
|
|
var state = {
|
|
|
|
project: null,
|
|
|
|
scope: 'inbox',
|
|
|
|
items: [],
|
|
|
|
active: -1,
|
|
|
|
seq: 0,
|
|
|
|
timer: null,
|
|
|
|
running: false
|
|
|
|
};
|
|
|
|
|
|
|
|
/* ---------- 请求 ---------- */
|
|
|
|
function api(url, params, method) {
|
|
|
|
method = method || 'GET';
|
|
|
|
var opt = {credentials: 'same-origin', headers: {}};
|
|
|
|
var target = url;
|
|
|
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
var qs = new URLSearchParams(params || {}).toString();
|
|
|
|
if (qs) target += '?' + qs;
|
|
|
|
} else {
|
|
|
|
opt.method = 'POST';
|
|
|
|
opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
|
|
opt.body = new URLSearchParams(params || {}).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return fetch(target, opt).then(function (res) {
|
|
|
|
return res.text().then(function (text) {
|
|
|
|
var data;
|
|
|
|
try {
|
|
|
|
data = JSON.parse(text);
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('服务返回异常(HTTP ' + res.status + '),请稍后重试');
|
|
|
|
}
|
|
|
|
if (data && data.status === 401) {
|
|
|
|
location.reload();
|
|
|
|
throw new Error('登录已失效');
|
|
|
|
}
|
|
|
|
if (data && data.error_message) {
|
|
|
|
throw new Error(data.error_message);
|
|
|
|
}
|
|
|
|
return data || {};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function esc(str) {
|
|
|
|
return String(str == null ? '' : str).replace(/[&<>"']/g, function (c) {
|
|
|
|
return {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function sizeText(n) {
|
|
|
|
n = Number(n) || 0;
|
|
|
|
if (n < 1024) return n + ' B';
|
|
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
|
|
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
|
|
|
|
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ---------- 公司项目搜索 ---------- */
|
|
|
|
var searchTimer = null;
|
|
|
|
|
|
|
|
el.keyword.addEventListener('input', function () {
|
|
|
|
var v = el.keyword.value.trim();
|
|
|
|
clearTimeout(searchTimer);
|
|
|
|
if (!v) {
|
|
|
|
closeDropdown();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
searchTimer = setTimeout(function () { doSearch(v); }, 280);
|
|
|
|
});
|
|
|
|
|
|
|
|
el.keyword.addEventListener('keydown', function (e) {
|
|
|
|
if (el.dropdown.classList.contains('hide')) return;
|
|
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
|
|
e.preventDefault();
|
|
|
|
var total = state.items.length;
|
|
|
|
if (!total) return;
|
|
|
|
state.active = e.key === 'ArrowDown'
|
|
|
|
? (state.active + 1) % total
|
|
|
|
: (state.active - 1 + total) % total;
|
|
|
|
highlight();
|
|
|
|
} else if (e.key === 'Enter') {
|
|
|
|
e.preventDefault();
|
|
|
|
if (state.active >= 0 && state.items[state.active]) {
|
|
|
|
pick(state.items[state.active]);
|
|
|
|
}
|
|
|
|
} else if (e.key === 'Escape') {
|
|
|
|
closeDropdown();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
el.keyword.addEventListener('focus', function () {
|
|
|
|
if (el.keyword.value.trim() && state.items.length) {
|
|
|
|
el.dropdown.classList.remove('hide');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
if (!document.getElementById('searchWrap').contains(e.target)) {
|
|
|
|
closeDropdown();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function doSearch(keyword) {
|
|
|
|
var seq = ++state.seq;
|
|
|
|
el.dropdown.classList.remove('hide');
|
|
|
|
el.dropdown.innerHTML = '<div class="dd-empty">搜索中…</div>';
|
|
|
|
|
|
|
|
api(API.projects, {keyword: keyword}).then(function (data) {
|
|
|
|
if (seq !== state.seq) return;
|
|
|
|
state.items = data.item || [];
|
|
|
|
state.active = state.items.length ? 0 : -1;
|
|
|
|
renderDropdown();
|
|
|
|
}).catch(function (err) {
|
|
|
|
if (seq !== state.seq) return;
|
|
|
|
state.items = [];
|
|
|
|
el.dropdown.innerHTML = '<div class="dd-empty">' + esc(err.message) + '</div>';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderDropdown() {
|
|
|
|
if (!state.items.length) {
|
|
|
|
el.dropdown.innerHTML = '<div class="dd-empty">没有匹配的公司项目</div>';
|
|
|
|
el.dropdown.classList.remove('hide');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var html = '';
|
|
|
|
state.items.forEach(function (item, i) {
|
|
|
|
html += '<div class="dd-item' + (i === state.active ? ' on' : '') + '" data-i="' + i + '">'
|
|
|
|
+ '<div class="dd-main">'
|
|
|
|
+ '<div class="dd-company">' + esc(item.company) + '</div>'
|
|
|
|
+ '<div class="dd-sub"><span>postid ' + item.postid + '</span>'
|
|
|
|
+ (item.plan ? '<span>' + esc(item.plan) + '</span>' : '')
|
|
|
|
+ '</div></div>'
|
|
|
|
+ '<div class="dd-bind">绑定邮箱 <b>' + item.bind + '</b></div>'
|
|
|
|
+ '</div>';
|
|
|
|
});
|
|
|
|
|
|
|
|
el.dropdown.innerHTML = html;
|
|
|
|
el.dropdown.classList.remove('hide');
|
|
|
|
|
|
|
|
[].forEach.call(el.dropdown.querySelectorAll('.dd-item'), function (node) {
|
|
|
|
node.addEventListener('click', function () {
|
|
|
|
pick(state.items[parseInt(node.getAttribute('data-i'), 10)]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function highlight() {
|
|
|
|
[].forEach.call(el.dropdown.querySelectorAll('.dd-item'), function (node, i) {
|
|
|
|
node.classList.toggle('on', i === state.active);
|
|
|
|
});
|
|
|
|
var on = el.dropdown.querySelector('.dd-item.on');
|
|
|
|
if (on && on.scrollIntoView) on.scrollIntoView({block: 'nearest'});
|
|
|
|
}
|
|
|
|
|
|
|
|
function closeDropdown() {
|
|
|
|
el.dropdown.classList.add('hide');
|
|
|
|
}
|
|
|
|
|
|
|
|
function pick(item) {
|
|
|
|
if (!item) return;
|
|
|
|
state.project = item;
|
|
|
|
closeDropdown();
|
|
|
|
el.keyword.value = '';
|
|
|
|
el.keyword.blur();
|
|
|
|
|
|
|
|
el.picked.classList.remove('hide');
|
|
|
|
el.pickedCompany.textContent = item.company;
|
|
|
|
el.pickedPostid.textContent = 'postid ' + item.postid;
|
|
|
|
el.pickedPlan.textContent = item.plan || '';
|
|
|
|
el.pickedPlan.style.display = item.plan ? '' : 'none';
|
|
|
|
el.pickedBind.textContent = '绑定邮箱 ' + item.bind;
|
|
|
|
|
|
|
|
el.start.disabled = false;
|
|
|
|
el.result.classList.add('hide');
|
|
|
|
refreshCount();
|
|
|
|
}
|
|
|
|
|
|
|
|
el.clearProject.addEventListener('click', function () {
|
|
|
|
state.project = null;
|
|
|
|
el.picked.classList.add('hide');
|
|
|
|
el.start.disabled = true;
|
|
|
|
el.hit.textContent = '选择公司项目后可查看命中数量';
|
|
|
|
el.keyword.focus();
|
|
|
|
});
|
|
|
|
|
|
|
|
/* ---------- 条件 / 数量 ---------- */
|
|
|
|
[].forEach.call(el.scopes.querySelectorAll('.scope'), function (node) {
|
|
|
|
node.addEventListener('click', function () {
|
|
|
|
[].forEach.call(el.scopes.querySelectorAll('.scope'), function (n) {
|
|
|
|
n.classList.remove('on');
|
|
|
|
});
|
|
|
|
node.classList.add('on');
|
|
|
|
state.scope = node.getAttribute('data-scope');
|
|
|
|
refreshCount();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
el.limit.addEventListener('input', function () {
|
|
|
|
if (state.project && state.count) refreshCount(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
function refreshCount(silent) {
|
|
|
|
if (!state.project) return;
|
|
|
|
if (!silent) el.hit.textContent = '正在统计命中数量…';
|
|
|
|
|
|
|
|
api(API.count, {
|
|
|
|
postid: state.project.postid,
|
|
|
|
scope: state.scope
|
|
|
|
}).then(function (data) {
|
|
|
|
state.count = Number(data.count);
|
|
|
|
if (isNaN(state.count)) state.count = 0;
|
|
|
|
var limit = parseInt(el.limit.value, 10) || 0;
|
|
|
|
|
|
|
|
if (state.count < 0) {
|
|
|
|
el.hit.innerHTML = '<b class="zero">统计失败</b>,es 查询异常,可直接尝试导出';
|
|
|
|
el.start.disabled = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!state.count) {
|
|
|
|
el.hit.innerHTML = '<b class="zero">0</b> 封命中,当前条件没有可导出的邮件';
|
|
|
|
} else if (limit && state.count > limit) {
|
|
|
|
el.hit.innerHTML = '命中 <b>' + state.count + '</b> 封,本次导出最新 <b>' + limit + '</b> 封';
|
|
|
|
} else {
|
|
|
|
el.hit.innerHTML = '命中 <b>' + state.count + '</b> 封,全部导出';
|
|
|
|
}
|
|
|
|
|
|
|
|
el.start.disabled = !state.count || state.running;
|
|
|
|
}).catch(function (err) {
|
|
|
|
el.hit.textContent = '统计失败:' + err.message;
|
|
|
|
el.start.disabled = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ---------- 开始导出 ---------- */
|
|
|
|
el.start.addEventListener('click', function () {
|
|
|
|
if (!state.project || state.running) return;
|
|
|
|
|
|
|
|
var limit = parseInt(el.limit.value, 10);
|
|
|
|
if (!limit || limit < 1) {
|
|
|
|
alert('请填写本次最多导出的数量');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (limit > 2000) {
|
|
|
|
alert('单次最多导出 2000 封,请调小数量或分批导出');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
runExport(state.project, state.scope, limit);
|
|
|
|
});
|
|
|
|
|
|
|
|
function runExport(project, scope, limit) {
|
|
|
|
state.running = true;
|
|
|
|
el.start.disabled = true;
|
|
|
|
el.result.classList.add('hide');
|
|
|
|
|
|
|
|
el.maskText.textContent = '正在导出「' + project.company + '」的邮件…';
|
|
|
|
el.mask.classList.remove('hide');
|
|
|
|
|
|
|
|
var begin = Date.now();
|
|
|
|
state.timer = setInterval(function () {
|
|
|
|
el.maskSub.textContent = '已用时 ' + Math.floor((Date.now() - begin) / 1000) + 's';
|
|
|
|
}, 1000);
|
|
|
|
el.maskSub.textContent = '已用时 0s';
|
|
|
|
|
|
|
|
api(API.start, {
|
|
|
|
postid: project.postid,
|
|
|
|
scope: scope,
|
|
|
|
limit: limit
|
|
|
|
}, 'POST').then(function (data) {
|
|
|
|
showResult(data);
|
|
|
|
refreshCount(true);
|
|
|
|
}).catch(function (err) {
|
|
|
|
alert(err.message);
|
|
|
|
}).then(function () {
|
|
|
|
stopTimer();
|
|
|
|
state.running = false;
|
|
|
|
el.start.disabled = !state.count;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function stopTimer() {
|
|
|
|
if (state.timer) clearInterval(state.timer);
|
|
|
|
state.timer = null;
|
|
|
|
el.mask.classList.add('hide');
|
|
|
|
}
|
|
|
|
|
|
|
|
function showResult(data) {
|
|
|
|
document.getElementById('resCompany').textContent = (data.company || '') + ' · ' + (data.scope_label || '');
|
|
|
|
document.getElementById('resTotal').textContent = data.total || 0;
|
|
|
|
document.getElementById('resSuccess').textContent = data.success || 0;
|
|
|
|
document.getElementById('resFail').textContent = data.fail || 0;
|
|
|
|
document.getElementById('resCost').textContent = (data.cost || 0) + 's';
|
|
|
|
document.getElementById('resSize').textContent = sizeText(data.zip_size);
|
|
|
|
document.getElementById('resDir').textContent = data.dir || '';
|
|
|
|
|
|
|
|
var dl = document.getElementById('resDownload');
|
|
|
|
dl.setAttribute('href', data.download || '#');
|
|
|
|
dl.setAttribute('download', data.zip_name || '');
|
|
|
|
|
|
|
|
var errors = data.errors || [];
|
|
|
|
document.getElementById('errBox').classList.toggle('hide', !errors.length);
|
|
|
|
|
|
|
|
var body = document.getElementById('errBody');
|
|
|
|
body.innerHTML = '';
|
|
|
|
errors.forEach(function (item) {
|
|
|
|
var tr = document.createElement('tr');
|
|
|
|
tr.innerHTML = '<td>' + esc(item.subject || '(无主题)') + '</td>'
|
|
|
|
+ '<td class="sub">' + esc(item.uuid || '') + '</td>'
|
|
|
|
+ '<td class="sub">' + esc(item.message || '') + '</td>';
|
|
|
|
body.appendChild(tr);
|
|
|
|
});
|
|
|
|
|
|
|
|
el.result.classList.remove('hide');
|
|
|
|
el.result.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
|
|
}
|
|
|
|
|
|
|
|
el.keyword.focus();
|
|
|
|
})();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html> |
...
|
...
|
|