更新完善功能
This commit is contained in:
parent
4e6d6e5307
commit
e08a87a4c2
@ -372,69 +372,6 @@ admin_topbar('content');
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 四大板块:名称 / 说明 / 顺序 -->
|
|
||||||
<div class="fieldset-card">
|
|
||||||
<div class="fs-title">四大板块(名称 / 悬停说明文案 / 顺序)</div>
|
|
||||||
<form method="post">
|
|
||||||
<?= csrf_field() ?><input type="hidden" name="act" value="boards">
|
|
||||||
<?php foreach ($cats as $c): $id = (int)$c['id']; ?>
|
|
||||||
<div class="m-block" style="margin-bottom:10px">
|
|
||||||
<div class="m-block-head">
|
|
||||||
<strong><?= he((string)$c['key']) ?></strong>
|
|
||||||
<span class="ops"><input type="number" name="b_sort[<?= $id ?>]" value="<?= (int)$c['sort'] ?>" min="1" max="99" style="width:70px" title="顺序数值,越小越靠前"></span>
|
|
||||||
</div>
|
|
||||||
<div class="m-items">
|
|
||||||
<div class="icon-ctl" style="margin-top:8px">
|
|
||||||
<span class="icon-preview"></span>
|
|
||||||
<input type="text" class="icon-input" name="b_icon[<?= $id ?>]" value="<?= he((string)($c['icon'] ?? '')) ?>" placeholder="字符 / emoji,或粘贴/上传图片">
|
|
||||||
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
|
||||||
<input type="file" class="icon-file" accept="image/*" hidden>
|
|
||||||
</div>
|
|
||||||
<input type="text" name="b_name[<?= $id ?>]" value="<?= he((string)$c['name']) ?>" placeholder="板块名称" style="margin-top:8px">
|
|
||||||
<textarea name="b_desc[<?= $id ?>]" rows="2" placeholder="悬停说明文案"><?= he(trim((string)$c['description']) !== '' ? (string)$c['description'] : ($boardDefaults[(string)$c['key']] ?? '')) ?></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<button type="submit" class="btn btn-primary">保存板块设置</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 首页快捷站点(本域名其它网站) -->
|
|
||||||
<div class="fieldset-card">
|
|
||||||
<div class="fs-title">首页快捷站点(四大板块上方的胶囊按钮,新标签页打开)</div>
|
|
||||||
<form method="post" id="qlForm">
|
|
||||||
<?= csrf_field() ?><input type="hidden" name="act" value="quicklinks">
|
|
||||||
<textarea name="data" id="qlData" hidden></textarea>
|
|
||||||
<p class="tip">用于放置本域名下的其它网站入口;点击后新开标签页。地址需为完整 http(s) 链接。列表顺序即展示顺序。</p>
|
|
||||||
<div id="qlRows">
|
|
||||||
<?php $quickLinks = $pdo->query('SELECT * FROM quick_links ORDER BY sort ASC, id ASC')->fetchAll(); ?>
|
|
||||||
<?php foreach ($quickLinks as $ql): ?>
|
|
||||||
<div class="l-row" data-id="<?= (int)$ql['id'] ?>">
|
|
||||||
<span class="icon-ctl">
|
|
||||||
<span class="icon-preview"></span>
|
|
||||||
<input type="text" class="icon-input l-icon" value="<?= he((string)$ql['icon']) ?>" placeholder="图标">
|
|
||||||
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
|
||||||
<input type="file" class="icon-file" accept="image/*" hidden>
|
|
||||||
</span>
|
|
||||||
<input type="text" class="l-name" value="<?= he((string)$ql['name']) ?>" placeholder="站点名称(必填)">
|
|
||||||
<input type="text" class="l-url" value="<?= he((string)$ql['url']) ?>" placeholder="完整网址,如 https://子站.域名.com">
|
|
||||||
<input type="text" class="l-note" value="<?= he((string)($ql['note'] ?? '')) ?>" placeholder="备注 / 网站介绍(悬停首页按钮显示)">
|
|
||||||
<label class="t-on"><input type="checkbox" class="l-enabled"<?= (int)$ql['enabled'] === 1 ? ' checked' : '' ?>> 启用</label>
|
|
||||||
<span class="ops">
|
|
||||||
<button type="button" class="btn btn-sm l-up" title="上移">↑</button>
|
|
||||||
<button type="button" class="btn btn-sm l-down" title="下移">↓</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-danger l-del" title="删除">✕</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<div class="field-row" style="margin:12px 0">
|
|
||||||
<button type="button" class="btn btn-sm" id="qlAdd">+ 添加快捷站点</button>
|
|
||||||
<button type="submit" class="btn btn-primary">保存快捷站点</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
<!-- 底部信息 -->
|
||||||
<div class="fieldset-card">
|
<div class="fieldset-card">
|
||||||
<div class="fs-title">底部信息</div>
|
<div class="fs-title">底部信息</div>
|
||||||
@ -519,6 +456,69 @@ admin_topbar('content');
|
|||||||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">修改密码</button></div>
|
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">修改密码</button></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 四大板块:名称 / 说明 / 顺序 -->
|
||||||
|
<div class="fieldset-card col-span-2">
|
||||||
|
<div class="fs-title">四大板块(名称 / 悬停说明文案 / 顺序)</div>
|
||||||
|
<form method="post">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="boards">
|
||||||
|
<?php foreach ($cats as $c): $id = (int)$c['id']; ?>
|
||||||
|
<div class="m-block" style="margin-bottom:10px">
|
||||||
|
<div class="m-block-head">
|
||||||
|
<strong><?= he((string)$c['key']) ?></strong>
|
||||||
|
<span class="ops"><input type="number" name="b_sort[<?= $id ?>]" value="<?= (int)$c['sort'] ?>" min="1" max="99" style="width:70px" title="顺序数值,越小越靠前"></span>
|
||||||
|
</div>
|
||||||
|
<div class="m-items">
|
||||||
|
<div class="icon-ctl" style="margin-top:8px">
|
||||||
|
<span class="icon-preview"></span>
|
||||||
|
<input type="text" class="icon-input" name="b_icon[<?= $id ?>]" value="<?= he((string)($c['icon'] ?? '')) ?>" placeholder="字符 / emoji,或粘贴/上传图片">
|
||||||
|
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
||||||
|
<input type="file" class="icon-file" accept="image/*" hidden>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="b_name[<?= $id ?>]" value="<?= he((string)$c['name']) ?>" placeholder="板块名称" style="margin-top:8px">
|
||||||
|
<textarea name="b_desc[<?= $id ?>]" rows="2" placeholder="悬停说明文案"><?= he(trim((string)$c['description']) !== '' ? (string)$c['description'] : ($boardDefaults[(string)$c['key']] ?? '')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<button type="submit" class="btn btn-primary">保存板块设置</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 首页快捷站点(本域名其它网站) -->
|
||||||
|
<div class="fieldset-card col-span-2">
|
||||||
|
<div class="fs-title">首页快捷站点(四大板块上方的胶囊按钮,新标签页打开)</div>
|
||||||
|
<form method="post" id="qlForm">
|
||||||
|
<?= csrf_field() ?><input type="hidden" name="act" value="quicklinks">
|
||||||
|
<textarea name="data" id="qlData" hidden></textarea>
|
||||||
|
<p class="tip">用于放置本域名下的其它网站入口;点击后新开标签页。地址需为完整 http(s) 链接。列表顺序即展示顺序。</p>
|
||||||
|
<div id="qlRows">
|
||||||
|
<?php $quickLinks = $pdo->query('SELECT * FROM quick_links ORDER BY sort ASC, id ASC')->fetchAll(); ?>
|
||||||
|
<?php foreach ($quickLinks as $ql): ?>
|
||||||
|
<div class="l-row" data-id="<?= (int)$ql['id'] ?>">
|
||||||
|
<span class="icon-ctl">
|
||||||
|
<span class="icon-preview"></span>
|
||||||
|
<input type="text" class="icon-input l-icon" value="<?= he((string)$ql['icon']) ?>" placeholder="图标">
|
||||||
|
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
||||||
|
<input type="file" class="icon-file" accept="image/*" hidden>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="l-name" value="<?= he((string)$ql['name']) ?>" placeholder="站点名称(必填)">
|
||||||
|
<input type="text" class="l-url" value="<?= he((string)$ql['url']) ?>" placeholder="完整网址,如 https://子站.域名.com">
|
||||||
|
<input type="text" class="l-note" value="<?= he((string)($ql['note'] ?? '')) ?>" placeholder="备注 / 网站介绍(悬停首页按钮显示)">
|
||||||
|
<label class="t-on"><input type="checkbox" class="l-enabled"<?= (int)$ql['enabled'] === 1 ? ' checked' : '' ?>> 启用</label>
|
||||||
|
<span class="ops">
|
||||||
|
<button type="button" class="btn btn-sm l-up" title="上移">↑</button>
|
||||||
|
<button type="button" class="btn btn-sm l-down" title="下移">↓</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger l-del" title="删除">✕</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div class="field-row" style="margin:12px 0">
|
||||||
|
<button type="button" class="btn btn-sm" id="qlAdd">+ 添加快捷站点</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存快捷站点</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
145
admin/nav.php
145
admin/nav.php
@ -168,6 +168,27 @@ admin_topbar('nav');
|
|||||||
<div class="err-msg"><?= he($errorMsg) ?></div>
|
<div class="err-msg"><?= he($errorMsg) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="fieldset-card" style="margin-bottom:16px">
|
||||||
|
<div class="fs-title">⚡ 批量导入导航链接(<?= he($catNames[$cat]) ?>)</div>
|
||||||
|
<p class="tip">在下面一次粘贴多行内容,点击「解析导入」后会按格式自动生成导航块,并<b>追加到下方编辑器末尾</b>,可再编辑确认后统一点「保存全部修改」。<br>
|
||||||
|
每行一条,支持几种写法:
|
||||||
|
<code>名称 | 网址 [ | 备注]</code>(也兼容 <code>网址 | 名称</code>)或 <code>[名称](网址)</code> 或 <code>名称 https://网址</code>;以 <code>## 块名</code> 开头的行开启一个新导航块;未写块名则归入“批量导入”。
|
||||||
|
单个块最多 15 个链接,超出会自动拆分为 <code>块名 2</code>、<code>块名 3</code> …</p>
|
||||||
|
<textarea id="importText" rows="7" spellcheck="false" placeholder="例如:
|
||||||
|
## 安全研究
|
||||||
|
看雪 | https://www.kanxue.com | 逆向/安全社区
|
||||||
|
先知社区 https://xz.aliyun.com
|
||||||
|
[FreeBuf](https://www.freebuf.com)
|
||||||
|
## 资讯
|
||||||
|
https://www.ithome.com | IT之家"></textarea>
|
||||||
|
<div class="field-row" style="margin-top:8px">
|
||||||
|
<button type="button" class="btn btn-primary" id="btnParseImport">解析并导入</button>
|
||||||
|
<button type="button" class="btn" id="btnClearImport">清空输入框</button>
|
||||||
|
<span class="tip" style="align-self:center">导入仅是“填入编辑区”,不会立即生效,请务必点击下方的「保存全部修改」。</span>
|
||||||
|
</div>
|
||||||
|
<div id="importMsg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" id="navForm" autocomplete="off">
|
<form method="post" id="navForm" autocomplete="off">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="action" value="save">
|
<input type="hidden" name="action" value="save">
|
||||||
@ -280,6 +301,130 @@ admin_topbar('nav');
|
|||||||
wrap.insertAdjacentHTML('beforeend', blockHTML(0, '', []));
|
wrap.insertAdjacentHTML('beforeend', blockHTML(0, '', []));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ---------- 批量导入解析 ---------- */
|
||||||
|
var MAX_ITEMS = 15;
|
||||||
|
var importTextEl = document.getElementById('importText');
|
||||||
|
var importMsg = document.getElementById('importMsg');
|
||||||
|
|
||||||
|
// 单行 -> item;无法识别返回 null
|
||||||
|
function parseImportLine(raw) {
|
||||||
|
var s = String(raw).trim();
|
||||||
|
if (s === '') return null;
|
||||||
|
// 1) | 分隔:名称 | 网址 [ | 备注](也兼容 网址 | 名称)
|
||||||
|
if (s.indexOf('|') !== -1) {
|
||||||
|
var p = s.split('|').map(function (x) { return x.trim(); });
|
||||||
|
var isUrl = function (x) { return /^https?:\/\//i.test(x); };
|
||||||
|
var label1 = p[0], url1 = p[1], note1 = p[2] || '';
|
||||||
|
if (isUrl(p[0]) && p[1] !== '' && !isUrl(p[1])) {
|
||||||
|
// 网址在前、名称在后
|
||||||
|
label1 = p[1]; url1 = p[0]; note1 = p[2] || '';
|
||||||
|
}
|
||||||
|
if (url1 !== '') return { label: label1, url: url1, note: note1 };
|
||||||
|
}
|
||||||
|
// 2) Markdown:[名称](网址 ["备注"])
|
||||||
|
var m = s.match(/^\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+["“]([^"”]*)["”])?\s*\)$/);
|
||||||
|
if (m) return { label: m[1].trim(), url: m[2].trim(), note: (m[3] || '').trim() };
|
||||||
|
// 3) 名称 https://… 或 名称 https://… 备注
|
||||||
|
var m2 = s.match(/^(.*?)(https?:\/\/\S+)(?:\s+(.*))?$/i);
|
||||||
|
if (m2 && m2[2]) {
|
||||||
|
return { label: (m2[1] || '').trim(), url: m2[2].replace(/[),,。]+$/, ''), note: (m2[3] || '').trim() };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// host 作为缺省名称
|
||||||
|
function hostOf(url) {
|
||||||
|
try {
|
||||||
|
var u = new URL(url);
|
||||||
|
return u.hostname.replace(/^www\./, '');
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全文 -> [{title, items}]
|
||||||
|
function importTextToBlocks(text) {
|
||||||
|
var lines = String(text).split(/\r?\n/);
|
||||||
|
var blocks = [];
|
||||||
|
var cur = null; // 当前块
|
||||||
|
var used = {}; // 已使用的标题(同名自动加序号)
|
||||||
|
var parsed = 0, skipped = 0;
|
||||||
|
|
||||||
|
function makeTitle(base) {
|
||||||
|
var t = base;
|
||||||
|
var n = 1;
|
||||||
|
while (used[t]) {
|
||||||
|
n++;
|
||||||
|
t = base + ' ' + n;
|
||||||
|
}
|
||||||
|
used[t] = true;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBlock() {
|
||||||
|
if (cur && cur.items.length < MAX_ITEMS) return;
|
||||||
|
var title = makeTitle(cur ? cur.title : '批量导入');
|
||||||
|
cur = { title: title, items: [] };
|
||||||
|
blocks.push(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.forEach(function (ln) {
|
||||||
|
var s = String(ln).trim();
|
||||||
|
if (s === '') return;
|
||||||
|
// 块标题行
|
||||||
|
var hm = s.match(/^#{1,6}\s+(.+)$/);
|
||||||
|
if (hm) {
|
||||||
|
var t = hm[1].trim();
|
||||||
|
if (t !== '') {
|
||||||
|
var t2 = makeTitle(t);
|
||||||
|
cur = { title: t2, items: [] };
|
||||||
|
blocks.push(cur);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var it = parseImportLine(s);
|
||||||
|
if (!it) { skipped++; return; }
|
||||||
|
if (it.url === '') { skipped++; return; }
|
||||||
|
if (it.label === '') it.label = hostOf(it.url);
|
||||||
|
ensureBlock();
|
||||||
|
cur.items.push(it);
|
||||||
|
parsed++;
|
||||||
|
});
|
||||||
|
return { blocks: blocks, parsed: parsed, skipped: skipped };
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindImport() {
|
||||||
|
var btn = document.getElementById('btnParseImport');
|
||||||
|
var clearBtn = document.getElementById('btnClearImport');
|
||||||
|
if (!btn || !importTextEl || !importMsg) return;
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var r = importTextToBlocks(importTextEl.value);
|
||||||
|
if (!r.parsed) {
|
||||||
|
importMsg.innerHTML = '<div class="err-msg">未解析到任何链接。请检查格式,每行至少包含“名称 网址”或“名称 | 网址”。</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var frag = document.createDocumentFragment();
|
||||||
|
r.blocks.forEach(function (b) {
|
||||||
|
var div = createElFromHTML(blockHTML(0, b.title, b.items));
|
||||||
|
frag.appendChild(div);
|
||||||
|
});
|
||||||
|
wrap.appendChild(frag);
|
||||||
|
var parts = [];
|
||||||
|
parts.push('<div class="ok-msg">已解析 ' + r.parsed + ' 条链接,生成 ' + r.blocks.length + ' 个导航块并追加到下方。</div>');
|
||||||
|
if (r.skipped) parts.push('<p class="tip" style="margin-top:6px">另有 ' + r.skipped + ' 行未能识别,已跳过。</p>');
|
||||||
|
parts.push('<p class="tip">请在下方向下核对;如需调整可修改标题/链接后,统一点击「保存全部修改」生效。</p>');
|
||||||
|
importMsg.innerHTML = parts.join('');
|
||||||
|
importTextEl.value = '';
|
||||||
|
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
});
|
||||||
|
clearBtn.addEventListener('click', function () {
|
||||||
|
importTextEl.value = '';
|
||||||
|
importMsg.innerHTML = '';
|
||||||
|
importTextEl.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bindImport();
|
||||||
|
|
||||||
function createElFromHTML(html) {
|
function createElFromHTML(html) {
|
||||||
var t = document.createElement('template');
|
var t = document.createElement('template');
|
||||||
t.innerHTML = html.trim();
|
t.innerHTML = html.trim();
|
||||||
|
|||||||
@ -409,6 +409,8 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran
|
|||||||
.fieldset-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 18px; margin-bottom: 20px; box-shadow: var(--shadow-sm); }
|
.fieldset-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 18px; margin-bottom: 20px; box-shadow: var(--shadow-sm); }
|
||||||
.fieldset-card > .fs-title { font-weight: 700; font-size: 16px; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
|
.fieldset-card > .fs-title { font-weight: 700; font-size: 16px; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
|
||||||
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 18px; }
|
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 18px; }
|
||||||
|
/* 跨整行的设置卡片(内容较多的底部大卡) */
|
||||||
|
.fieldset-card.col-span-2 { grid-column: 1 / -1; }
|
||||||
|
|
||||||
/* 后台导航内容管理 */
|
/* 后台导航内容管理 */
|
||||||
.m-block { border: 1px solid var(--line); border-radius: 12px; margin-bottom: 16px; overflow: hidden; }
|
.m-block { border: 1px solid var(--line); border-radius: 12px; margin-bottom: 16px; overflow: hidden; }
|
||||||
|
|||||||
114
assets/js/ip.js
114
assets/js/ip.js
@ -1,9 +1,10 @@
|
|||||||
/* ip.js —— IP 地址处理
|
/* ip.js —— IP 地址处理
|
||||||
* 提取输入文本中全部 IPv4 → 按 /24 段聚合输出(文本 / 高亮预览):
|
* 提取输入文本中全部 IPv4 → 按 /24 段聚合输出(文本 / 高亮预览):
|
||||||
* IP段 发现数量(段内去重IP数) IP段性质
|
* IP段 发现数量(段内去重IP数) IP段性质 出现次数
|
||||||
* 1.2.3.0/24 8 公网
|
* 1.2.3.0/24 8 公网 56
|
||||||
* 1.2.3.4
|
* 1.2.3.4 出现次数
|
||||||
*
|
*
|
||||||
|
* 段按出现次数(段内全部 IP 原文出现次数之和)从高到低排列;
|
||||||
* 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。
|
* 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。
|
||||||
* CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。
|
* CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。
|
||||||
*/
|
*/
|
||||||
@ -106,11 +107,15 @@
|
|||||||
|
|
||||||
var TYPE_ZH = { priv: '内网', cdn: 'CDN', pub: '公网' };
|
var TYPE_ZH = { priv: '内网', cdn: 'CDN', pub: '公网' };
|
||||||
|
|
||||||
/* 返回 { segs:[{line, hit, ips:[{ip,hit}]}], segTotals, totalIps, totalSeg, hitSeg, hitIps, rangeCount } */
|
/* 返回 { segs:[{line, hit, count, ips:[...], details:[ip]}], totalCount, totalIps, totalSeg, hitSeg, hitIps, rangeCount, counts } */
|
||||||
function analyze(text, cdnRaw, focusSet) {
|
function analyze(text, cdnRaw, focusSet) {
|
||||||
var all = extractIps(text);
|
var all = extractIps(text);
|
||||||
if (!all.length) return null;
|
if (!all.length) return null;
|
||||||
var ranges = parseCdnText(cdnRaw);
|
var ranges = parseCdnText(cdnRaw);
|
||||||
|
var counts = {};
|
||||||
|
for (var ci = 0; ci < all.length; ci++) {
|
||||||
|
counts[all[ci]] = (counts[all[ci]] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
var segMap = {}, segOrder = [];
|
var segMap = {}, segOrder = [];
|
||||||
for (var i = 0; i < all.length; i++) {
|
for (var i = 0; i < all.length; i++) {
|
||||||
@ -141,18 +146,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var segs = segOrder.map(function (k) { return segMap[k]; });
|
var segs = segOrder.map(function (k) { return segMap[k]; });
|
||||||
segs.forEach(function (s) { s.label = segLabel(s); });
|
segs.forEach(function (s) {
|
||||||
// 命中重点 IP 的段排前,组内段号升序
|
s.label = segLabel(s);
|
||||||
var focusSegs = [], otherSegs = [];
|
// 段访问次数 = 段内全部具体 IP 在原文出现次数之和
|
||||||
|
s.count = 0;
|
||||||
|
s.ips.forEach(function (ip) { s.count += counts[ip] || 0; });
|
||||||
|
});
|
||||||
|
// 命中重点 IP 的段置顶;其余段按出现次数降序(同次数按段号升序)
|
||||||
segs.forEach(function (s) {
|
segs.forEach(function (s) {
|
||||||
s.hit = s.ips.some(function (ip) { return focusSet[ip]; });
|
s.hit = s.ips.some(function (ip) { return focusSet[ip]; });
|
||||||
(s.hit ? focusSegs : otherSegs).push(s);
|
|
||||||
});
|
});
|
||||||
segs = focusSegs.concat(otherSegs);
|
segs.sort(function (a, b) {
|
||||||
|
if (a.hit !== b.hit) return a.hit ? -1 : 1;
|
||||||
|
if (a.count !== b.count) return b.count - a.count;
|
||||||
|
return keyLess(a.key, b.key);
|
||||||
|
});
|
||||||
|
|
||||||
var totalIps = 0, hitIps = 0, hitSeg = 0;
|
var totalIps = 0, hitIps = 0, hitSeg = 0, totalCount = 0;
|
||||||
segs.forEach(function (s) {
|
segs.forEach(function (s) {
|
||||||
totalIps += s.ips.length;
|
totalIps += s.ips.length;
|
||||||
|
totalCount += s.count;
|
||||||
if (s.hit) hitSeg++;
|
if (s.hit) hitSeg++;
|
||||||
s.ips.sort(ipLess);
|
s.ips.sort(ipLess);
|
||||||
var h = [], o = [];
|
var h = [], o = [];
|
||||||
@ -161,7 +174,7 @@
|
|||||||
else o.push(ip);
|
else o.push(ip);
|
||||||
});
|
});
|
||||||
s.details = h.concat(o);
|
s.details = h.concat(o);
|
||||||
s.line = s.key + '.0/24\t' + s.ips.length + '\t' + s.label;
|
s.line = s.key + '.0/24\t' + s.ips.length + '\t' + s.label + '\t' + s.count;
|
||||||
});
|
});
|
||||||
|
|
||||||
var cntSeg = { priv: 0, cdn: 0, pub: 0 };
|
var cntSeg = { priv: 0, cdn: 0, pub: 0 };
|
||||||
@ -174,12 +187,14 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
segs: segs,
|
segs: segs,
|
||||||
|
totalCount: totalCount,
|
||||||
totalIps: totalIps,
|
totalIps: totalIps,
|
||||||
totalSeg: segs.length,
|
totalSeg: segs.length,
|
||||||
hitSeg: hitSeg,
|
hitSeg: hitSeg,
|
||||||
hitIps: hitIps,
|
hitIps: hitIps,
|
||||||
cntSeg: cntSeg,
|
cntSeg: cntSeg,
|
||||||
rangeCount: ranges.length
|
rangeCount: ranges.length,
|
||||||
|
counts: counts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,22 +203,24 @@
|
|||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
function plainText(data) {
|
function plainText(data) {
|
||||||
var lines = ['IP段\t发现数量\tIP段性质'];
|
var lines = ['IP段 发现数量 IP段性质 出现次数'];
|
||||||
data.segs.forEach(function (s) {
|
data.segs.forEach(function (s) {
|
||||||
lines.push(s.line);
|
lines.push(s.line);
|
||||||
s.details.forEach(function (ip) { lines.push('\t' + ip); });
|
s.details.forEach(function (ip) { lines.push(ip + ' ' + (data.counts[ip] || 1)); });
|
||||||
lines.push('');
|
lines.push('');
|
||||||
});
|
});
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
function previewHtml(data) {
|
function previewHtml(data) {
|
||||||
var parts = [];
|
var parts = [];
|
||||||
parts.push(esc('IP段\t发现数量\tIP段性质').replace(/\t/g, ' '));
|
parts.push(esc('IP段 发现数量 IP段性质 出现次数').replace(/\t/g, ' '));
|
||||||
data.segs.forEach(function (s) {
|
data.segs.forEach(function (s) {
|
||||||
var lineTxt = s.line.replace(/\t/g, ' ');
|
var lineTxt = s.line.replace(/\t/g, ' ');
|
||||||
parts.push(s.hit ? '<span class="ip-seg-hit">★ ' + esc(lineTxt) + '</span>' : esc(lineTxt));
|
parts.push(s.hit ? '<span class="ip-seg-hit">★ ' + esc(lineTxt) + '</span>' : esc(lineTxt));
|
||||||
s.details.forEach(function (ip) {
|
s.details.forEach(function (ip) {
|
||||||
parts.push(focusCur[ip] ? '<span class="ip-hit">' + esc(ip) + '</span>' : esc(ip));
|
var cnt = data.counts[ip] || 1;
|
||||||
|
var row = esc(ip) + ' ' + cnt;
|
||||||
|
parts.push(focusCur[ip] ? '<span class="ip-hit">' + row + '</span>' : row);
|
||||||
});
|
});
|
||||||
parts.push('');
|
parts.push('');
|
||||||
});
|
});
|
||||||
@ -234,10 +251,10 @@
|
|||||||
var d = lastData;
|
var d = lastData;
|
||||||
$('ipPreview').innerHTML = previewHtml(d);
|
$('ipPreview').innerHTML = previewHtml(d);
|
||||||
var st = d.cntSeg;
|
var st = d.cntSeg;
|
||||||
var note = '共提取去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' +
|
var note = 'IP 共出现 ' + d.totalCount + ' 次,去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' +
|
||||||
st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段)。';
|
st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段),按段出现次数降序排列。';
|
||||||
if (Object.keys(parseFocus($('ipFocus').value)).length === 0) {
|
if (Object.keys(parseFocus($('ipFocus').value)).length === 0) {
|
||||||
note += ' 未填写重点 IP,按原顺序输出。';
|
note += ' 未填写重点 IP。';
|
||||||
} else if (d.hitIps === 0) {
|
} else if (d.hitIps === 0) {
|
||||||
note += ' 填写的重点 IP 均未在输入内容中命中。';
|
note += ' 填写的重点 IP 均未在输入内容中命中。';
|
||||||
} else {
|
} else {
|
||||||
@ -272,16 +289,75 @@
|
|||||||
}
|
}
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
$('ipInput').value = '';
|
$('ipInput').value = '';
|
||||||
|
$('ipFile').value = '';
|
||||||
$('ipFocus').value = '';
|
$('ipFocus').value = '';
|
||||||
$('ipMsg').innerHTML = '';
|
$('ipMsg').innerHTML = '';
|
||||||
$('ipResult').hidden = true;
|
$('ipResult').hidden = true;
|
||||||
$('ipInput').focus();
|
$('ipInput').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 上传文本文件识别 ---------- */
|
||||||
|
var FILE_MAX = 20 * 1024 * 1024; // ≤ 20MB
|
||||||
|
var FILE_EXTS = ['.txt', '.json', '.log', '.md', '.csv', '.conf', '.ini'];
|
||||||
|
var fileInput = $('ipFile');
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.addEventListener('change', function () {
|
||||||
|
var f = fileInput.files && fileInput.files[0];
|
||||||
|
var msg = $('ipMsg');
|
||||||
|
msg.innerHTML = '';
|
||||||
|
if (!f) return;
|
||||||
|
var name = (f.name || '').toLowerCase();
|
||||||
|
var okExt = false;
|
||||||
|
for (var ei = 0; ei < FILE_EXTS.length; ei++) {
|
||||||
|
if (name.slice(-FILE_EXTS[ei].length) === FILE_EXTS[ei]) { okExt = true; break; }
|
||||||
|
}
|
||||||
|
if (!okExt) {
|
||||||
|
msg.innerHTML = '<div class="err-msg">不支持的文件格式。请上传 txt / json / log / md / csv / conf / ini 文本文件。</div>';
|
||||||
|
fileInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (f.size > FILE_MAX) {
|
||||||
|
msg.innerHTML = '<div class="err-msg">文件超过 20MB 限制,请拆分后再分析。</div>';
|
||||||
|
fileInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rd = new FileReader();
|
||||||
|
rd.onload = function () {
|
||||||
|
$('ipInput').value = String(rd.result || '');
|
||||||
|
msg.innerHTML = '<div class="ok-msg">已读取「' + esc(f.name) + '」(' + (f.size / 1024).toFixed(1) + ' KB),正在分析…</div>';
|
||||||
|
run();
|
||||||
|
};
|
||||||
|
rd.onerror = function () {
|
||||||
|
msg.innerHTML = '<div class="err-msg">文件读取失败,请重试或改用文本粘贴。</div>';
|
||||||
|
};
|
||||||
|
rd.readAsText(f, 'UTF-8');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 导出 TXT ---------- */
|
||||||
|
function stamp() {
|
||||||
|
var d = new Date(), p = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||||
|
return '' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
||||||
|
}
|
||||||
|
function exportTxt() {
|
||||||
|
if (!lastData) return;
|
||||||
|
var text = plainText(lastData);
|
||||||
|
var blob = new Blob(['\ufeff' + text], { type: 'text/plain;charset=utf-8' }); // BOM 便于记事本识别 UTF-8
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'ip_result_' + stamp() + '.txt';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
$('ipBtn').addEventListener('click', run);
|
$('ipBtn').addEventListener('click', run);
|
||||||
$('ipCopy').addEventListener('click', function () {
|
$('ipCopy').addEventListener('click', function () {
|
||||||
if (lastData) copyText(plainText(lastData));
|
if (lastData) copyText(plainText(lastData));
|
||||||
});
|
});
|
||||||
|
$('ipExport').addEventListener('click', exportTxt);
|
||||||
$('ipClear').addEventListener('click', clearAll);
|
$('ipClear').addEventListener('click', clearAll);
|
||||||
$('ipInput').addEventListener('keydown', function (ev) {
|
$('ipInput').addEventListener('keydown', function (ev) {
|
||||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run();
|
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run();
|
||||||
|
|||||||
BIN
data/homepage.db
Normal file
BIN
data/homepage.db
Normal file
Binary file not shown.
38
func/ip.php
38
func/ip.php
@ -1,12 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* func/ip.php —— IP 地址处理
|
* func/ip.php —— IP 地址处理
|
||||||
* 提取输入文本中的全部 IPv4,按 /24 段聚合输出纯文本结果:
|
* 提取输入文本 / 上传文本文件中的全部 IPv4,按 /24 段聚合输出纯文本结果:
|
||||||
* IP段 发现数量(段内去重IP数) IP段性质
|
* IP段 发现数量(段内去重IP数) IP段性质 出现次数
|
||||||
* 1.2.3.0/24 8 公网
|
* 1.2.3.0/24 8 公网 56
|
||||||
* 1.2.3.4
|
* 1.2.3.4 出现次数
|
||||||
*
|
*
|
||||||
|
* 出现次数:具体 IP 为其在原文中的出现总次数;段行出现次数为段内全部 IP 次数之和。
|
||||||
|
* 段默认按出现次数从高到低排列,命中「重点匹配 IP」的段优先置顶。
|
||||||
* 段性质区分:内网(含保留)/ CDN / 公网;IPv6 暂不参与。
|
* 段性质区分:内网(含保留)/ CDN / 公网;IPv6 暂不参与。
|
||||||
|
* 支持上传常见文本格式文件(txt / json / log / md / csv / conf / ini)自动载入,
|
||||||
|
* 支持将结果导出为 UTF-8 TXT 文件。
|
||||||
*/
|
*/
|
||||||
$P = '../';
|
$P = '../';
|
||||||
require_once dirname(__DIR__) . '/includes/layout.php';
|
require_once dirname(__DIR__) . '/includes/layout.php';
|
||||||
@ -19,13 +23,18 @@ func_bar('IP 地址处理');
|
|||||||
<div class="wrap page-body">
|
<div class="wrap page-body">
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<h2>使用说明</h2>
|
<h2>使用说明</h2>
|
||||||
<p class="tip">粘贴任意日志 / 文本,系统提取其中全部 <strong>IPv4</strong>(IPv6 暂不参与),
|
<p class="tip">粘贴任意日志 / 文本,或直接<b>上传文本文件</b>(txt / json / log / md / csv / conf / ini,≤ 20MB),
|
||||||
按 <strong>/24 段</strong>聚合为文本结果:每段一行 <code>IP段 / 发现数量 / IP段性质</code>,
|
系统提取其中全部 <strong>IPv4</strong>(IPv6 暂不参与),按 <strong>/24 段</strong>聚合:
|
||||||
其中「发现数量」为该段内<strong>去重后的 IP 个数</strong>;段行下方缩进列出该段具体 IP,段与段之间空一行分隔。
|
每段一行 <code>IP段 / 发现数量 / IP段性质 / 出现次数</code>——发现数量为该段内<strong>去重后的 IP 个数</strong>,
|
||||||
段性质按 <strong>内网(含保留)/ CDN / 公网</strong> 区分。
|
出现次数为该段内全部 IP 在原文中的<strong>出现总次数</strong>;段行下方依次列出该段具体 IP,并标注<b>每个 IP 的出现次数</b>:<code>具体IP / 出现次数</code>,段与段之间空一行分隔。
|
||||||
若在下方「重点匹配 IP」中填写了需要关注的 IP,命中其所在的 /24 段会<strong>优先置顶</strong>,并在预览结果中用<strong>红色</strong>标出该 IP(复制得到的纯文本不含颜色)。</p>
|
各 /24 段默认按<b>出现次数从高到低排列</b>;段性质按 <strong>内网(含保留)/ CDN / 公网</strong> 区分。
|
||||||
<label class="fl" for="ipInput">待分析文本</label>
|
若填写「重点匹配 IP」,命中其所在的 /24 段会<strong>优先置顶</strong>,并在预览结果中用<strong>红色</strong>标出该 IP;结果可一键复制或导出为 txt(纯文本不含颜色)。</p>
|
||||||
|
<label class="fl" for="ipInput">待分析文本(可直接粘贴,也可在上方上传文件载入)</label>
|
||||||
<textarea id="ipInput" rows="12" placeholder="粘贴防火墙 / nginx / 安全设备日志等包含 IP 的内容…"></textarea>
|
<textarea id="ipInput" rows="12" placeholder="粘贴防火墙 / nginx / 安全设备日志等包含 IP 的内容…"></textarea>
|
||||||
|
<div class="field-row" style="margin-top:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<input type="file" id="ipFile" accept=".txt,.json,.log,.md,.csv,.conf,.ini" style="flex:1 1 280px;min-width:220px">
|
||||||
|
<span class="tip" style="align-self:center">选择 txt / json / log 等文本文件,读取后自动填入并分析</span>
|
||||||
|
</div>
|
||||||
<label class="fl" for="ipFocus" style="margin-top:12px">重点匹配 IP(可选,多个用空格 / 逗号 / 换行分隔)</label>
|
<label class="fl" for="ipFocus" style="margin-top:12px">重点匹配 IP(可选,多个用空格 / 逗号 / 换行分隔)</label>
|
||||||
<input type="text" id="ipFocus" placeholder="如 8.8.8.8 104.16.1.18;命中该 IP 的 /24 段将优先置顶展示,并将对应 IP 标红">
|
<input type="text" id="ipFocus" placeholder="如 8.8.8.8 104.16.1.18;命中该 IP 的 /24 段将优先置顶展示,并将对应 IP 标红">
|
||||||
<div class="field-row" style="margin-top:10px">
|
<div class="field-row" style="margin-top:10px">
|
||||||
@ -38,13 +47,16 @@ func_bar('IP 地址处理');
|
|||||||
|
|
||||||
<div id="ipResult" hidden>
|
<div id="ipResult" hidden>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="field-row" style="align-items:center;justify-content:space-between">
|
<div class="field-row" style="align-items:center;justify-content:space-between;flex-wrap:wrap">
|
||||||
<h2 style="margin:0">识别结果</h2>
|
<h2 style="margin:0">识别结果</h2>
|
||||||
<button type="button" class="btn btn-sm" id="ipCopy">复制结果</button>
|
<span class="ops" style="display:flex;gap:8px">
|
||||||
|
<button type="button" class="btn btn-sm" id="ipExport">⬇ 导出 TXT</button>
|
||||||
|
<button type="button" class="btn btn-sm" id="ipCopy">复制结果</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="ipPreview" class="ip-pre"></div>
|
<div id="ipPreview" class="ip-pre"></div>
|
||||||
<p class="tip" id="ipStat" style="margin:8px 0"></p>
|
<p class="tip" id="ipStat" style="margin:8px 0"></p>
|
||||||
<p class="tip">复制结果为纯文本(不含红色标记);含重点 IP 的段已优先置顶展示。若 /24 段内混有不同性质,按数量多者标注“xx为主(混合)”。</p>
|
<p class="tip">段行为 <code>IP段 / 发现数量 / 性质 / 出现次数</code>(出现次数为段内全部 IP 原文出现次数之和);具体 IP 行为 <code>IP / 出现次数</code>。段按出现次数降序排列,含重点 IP 的段置顶;复制或导出的纯文本不含颜色。若 /24 段内混有不同性质,按数量多者标注“xx为主(混合)”。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
0
nginx.htaccess
Normal file
0
nginx.htaccess
Normal file
Loading…
Reference in New Issue
Block a user