更新完善功能

This commit is contained in:
MasonLiu 2026-09-06 19:20:38 +08:00
parent 4e6d6e5307
commit e08a87a4c2
8 changed files with 330 additions and 95 deletions

0
.htaccess Normal file
View File

View File

@ -372,69 +372,6 @@ admin_topbar('content');
</form>
</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="fs-title">底部信息</div>
@ -519,6 +456,69 @@ admin_topbar('content');
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">修改密码</button></div>
</form>
</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>
</main>

View File

@ -168,6 +168,27 @@ admin_topbar('nav');
<div class="err-msg"><?= he($errorMsg) ?></div>
<?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">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save">
@ -280,6 +301,130 @@ admin_topbar('nav');
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) {
var t = document.createElement('template');
t.innerHTML = html.trim();

View File

@ -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 > .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; }
/* 跨整行的设置卡片(内容较多的底部大卡) */
.fieldset-card.col-span-2 { grid-column: 1 / -1; }
/* 后台导航内容管理 */
.m-block { border: 1px solid var(--line); border-radius: 12px; margin-bottom: 16px; overflow: hidden; }

View File

@ -1,9 +1,10 @@
/* ip.js IP
* 提取输入文本中全部 IPv4 /24 段聚合输出文本 / 高亮预览
* IP段 发现数量(段内去重IP数) IP段性质
* 1.2.3.0/24 8 公网
* 1.2.3.4
* IP段 发现数量(段内去重IP数) IP段性质 出现次数
* 1.2.3.0/24 8 公网 56
* 1.2.3.4 出现次数
*
* 段按出现次数段内全部 IP 原文出现次数之和从高到低排列
* 重点匹配 IP填写的 IP 命中后其所在 /24 段优先置顶对应 IP 红色标出
* CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入可在后台维护
*/
@ -106,11 +107,15 @@
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) {
var all = extractIps(text);
if (!all.length) return null;
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 = [];
for (var i = 0; i < all.length; i++) {
@ -141,18 +146,26 @@
}
var segs = segOrder.map(function (k) { return segMap[k]; });
segs.forEach(function (s) { s.label = segLabel(s); });
// 命中重点 IP 的段排前,组内段号升序
var focusSegs = [], otherSegs = [];
segs.forEach(function (s) {
s.label = segLabel(s);
// 段访问次数 = 段内全部具体 IP 在原文出现次数之和
s.count = 0;
s.ips.forEach(function (ip) { s.count += counts[ip] || 0; });
});
// 命中重点 IP 的段置顶;其余段按出现次数降序(同次数按段号升序)
segs.forEach(function (s) {
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) {
totalIps += s.ips.length;
totalCount += s.count;
if (s.hit) hitSeg++;
s.ips.sort(ipLess);
var h = [], o = [];
@ -161,7 +174,7 @@
else o.push(ip);
});
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 };
@ -174,12 +187,14 @@
return {
segs: segs,
totalCount: totalCount,
totalIps: totalIps,
totalSeg: segs.length,
hitSeg: hitSeg,
hitIps: hitIps,
cntSeg: cntSeg,
rangeCount: ranges.length
rangeCount: ranges.length,
counts: counts
};
}
@ -188,22 +203,24 @@
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function plainText(data) {
var lines = ['IP段\t发现数量\tIP段性质'];
var lines = ['IP段 发现数量 IP段性质 出现次数'];
data.segs.forEach(function (s) {
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('');
});
return lines.join('\n');
}
function previewHtml(data) {
var parts = [];
parts.push(esc('IP段\t发现数量\tIP段性质').replace(/\t/g, ' '));
parts.push(esc('IP段 发现数量 IP段性质 出现次数').replace(/\t/g, ' '));
data.segs.forEach(function (s) {
var lineTxt = s.line.replace(/\t/g, ' ');
parts.push(s.hit ? '<span class="ip-seg-hit">★ ' + esc(lineTxt) + '</span>' : esc(lineTxt));
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('');
});
@ -234,10 +251,10 @@
var d = lastData;
$('ipPreview').innerHTML = previewHtml(d);
var st = d.cntSeg;
var note = '共提取去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' +
st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段)。';
var note = 'IP 共出现 ' + d.totalCount + ' 次,去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' +
st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段),按段出现次数降序排列。';
if (Object.keys(parseFocus($('ipFocus').value)).length === 0) {
note += ' 未填写重点 IP,按原顺序输出。';
note += ' 未填写重点 IP。';
} else if (d.hitIps === 0) {
note += ' 填写的重点 IP 均未在输入内容中命中。';
} else {
@ -272,16 +289,75 @@
}
function clearAll() {
$('ipInput').value = '';
$('ipFile').value = '';
$('ipFocus').value = '';
$('ipMsg').innerHTML = '';
$('ipResult').hidden = true;
$('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);
$('ipCopy').addEventListener('click', function () {
if (lastData) copyText(plainText(lastData));
});
$('ipExport').addEventListener('click', exportTxt);
$('ipClear').addEventListener('click', clearAll);
$('ipInput').addEventListener('keydown', function (ev) {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run();

BIN
data/homepage.db Normal file

Binary file not shown.

View File

@ -1,12 +1,16 @@
<?php
/**
* func/ip.php —— IP 地址处理
* 提取输入文本中的全部 IPv4 /24 段聚合输出纯文本结果:
* IP段 发现数量(段内去重IP数) IP段性质
* 1.2.3.0/24 8 公网
* 1.2.3.4
* 提取输入文本 / 上传文本文件中的全部 IPv4 /24 段聚合输出纯文本结果:
* IP段 发现数量(段内去重IP数) IP段性质 出现次数
* 1.2.3.0/24 8 公网 56
* 1.2.3.4 出现次数
*
* 出现次数:具体 IP 为其在原文中的出现总次数;段行出现次数为段内全部 IP 次数之和。
* 段默认按出现次数从高到低排列,命中「重点匹配 IP」的段优先置顶。
* 段性质区分:内网(含保留)/ CDN / 公网IPv6 暂不参与。
* 支持上传常见文本格式文件txt / json / log / md / csv / conf / ini自动载入
* 支持将结果导出为 UTF-8 TXT 文件。
*/
$P = '../';
require_once dirname(__DIR__) . '/includes/layout.php';
@ -19,13 +23,18 @@ func_bar('IP 地址处理');
<div class="wrap page-body">
<div class="panel-card">
<h2>使用说明</h2>
<p class="tip">粘贴任意日志 / 文本,系统提取其中全部 <strong>IPv4</strong>IPv6 暂不参与),
<strong>/24 </strong>聚合为文本结果:每段一行 <code>IP段 / 发现数量 / IP段性质</code>
其中「发现数量」为该段内<strong>去重后的 IP 个数</strong>;段行下方缩进列出该段具体 IP段与段之间空一行分隔。
段性质按 <strong>内网(含保留)/ CDN / 公网</strong> 区分。
若在下方「重点匹配 IP」中填写了需要关注的 IP命中其所在的 /24 段会<strong>优先置顶</strong>,并在预览结果中用<strong>红色</strong>标出该 IP复制得到的纯文本不含颜色</p>
<label class="fl" for="ipInput">待分析文本</label>
<p class="tip">粘贴任意日志 / 文本,或直接<b>上传文本文件</b>txt / json / log / md / csv / conf / ini 20MB
系统提取其中全部 <strong>IPv4</strong>IPv6 暂不参与),按 <strong>/24 </strong>聚合:
每段一行 <code>IP段 / 发现数量 / IP段性质 / 出现次数</code>——发现数量为该段内<strong>去重后的 IP 个数</strong>
出现次数为该段内全部 IP 在原文中的<strong>出现总次数</strong>;段行下方依次列出该段具体 IP并标注<b>每个 IP 的出现次数</b><code>具体IP / 出现次数</code>,段与段之间空一行分隔。
/24 段默认按<b>出现次数从高到低排列</b>;段性质按 <strong>内网(含保留)/ CDN / 公网</strong> 区分。
若填写「重点匹配 IP」命中其所在的 /24 段会<strong>优先置顶</strong>,并在预览结果中用<strong>红色</strong>标出该 IP结果可一键复制或导出为 txt纯文本不含颜色</p>
<label class="fl" for="ipInput">待分析文本(可直接粘贴,也可在上方上传文件载入)</label>
<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>
<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">
@ -38,13 +47,16 @@ func_bar('IP 地址处理');
<div id="ipResult" hidden>
<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>
<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 id="ipPreview" class="ip-pre"></div>
<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>

0
nginx.htaccess Normal file
View File