第一步,放入记录脚本,(可以跟你的统计代码放在一起)
在你的网页插入这段代码
<script src="https://api.afmax.cn/so/history/index.php?writer.js"></script>
第2步:(可选)首页插入 Cookie 提示
<script src="https://api.afmax.cn/use/cookie-consent.js"></script>第3步:使用插入浏览查看代码
<!-- ID1经常访问/访问频次排序 num12输出12条-->
<script src="https://api.afmax.cn/so/history/index.php?id=1&num=12.js"></script>
<!-- ID2最近访问 num12输出12条-->
<script src="https://api.afmax.cn/so/history/index.php?id=2&num=12.js"></script>
如果num0是输出500条
推荐方法1,实时更新
或者:下载浏览历史记录查看页HTML到你的服务器
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>浏览记录</title>
<script>
(function(){
var __API_DOMAIN__ = 'api.afmax.cn';
var __WRITER_URL__ = 'https://' + __API_DOMAIN__ + '/so/history/index.php?writer.js';
var __TDK_API__ = 'https://' + __API_DOMAIN__ + '/so/tdk/index.php';
var __ICO_API__ = 'https://' + __API_DOMAIN__ + '/so/ico/index.php';
Object.defineProperty(window, 'API_DOMAIN', {value: __API_DOMAIN__, writable: false, configurable: false});
Object.defineProperty(window, 'WRITER_URL', {value: __WRITER_URL__, writable: false, configurable: false});
Object.defineProperty(window, 'TDK_API', {value: __TDK_API__, writable: false, configurable: false});
Object.defineProperty(window, 'ICO_API', {value: __ICO_API__, writable: false, configurable: false});
})();
// =================================
</script>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
:root{--bg:#f5f5f7;--card:#fff;--text:#1d1d1f;--text2:#86868b;--accent:#0071e3;--red:#ff3b30;--green:#34c759;--border:#e5e5ea;--radius:12px;--shadow:0 1px 3px rgba(0,0,0,.08);}
@media(prefers-color-scheme:dark){:root{--bg:#000;--card:#1c1c1e;--text:#f5f5f7;--text2:#98989d;--accent:#0a84ff;--red:#ff453a;--green:#30d158;--border:#38383a;--shadow:0 1px 3px rgba(0,0,0,.3);}}
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);-webkit-tap-highlight-color:transparent;overflow-x:hidden;}
.header{position:sticky;top:0;z-index:100;background:var(--bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);padding:12px 16px;border-bottom:1px solid var(--border);}
.header h1{font-size:20px;font-weight:700;letter-spacing:-.3px;}
.toolbar{display:flex;gap:8px;padding:8px 16px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;}
.toolbar::-webkit-scrollbar{display:none;}
.toolbar .search{flex:1;min-width:0;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-size:14px;outline:none;}
.toolbar .search:focus{border-color:var(--accent);}
.toolbar .search::placeholder{color:var(--text2);}
.btn{padding:8px 14px;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;transition:opacity .15s;}
.btn:active{opacity:.6;}
.btn-primary{background:var(--accent);color:#fff;}
.btn-danger{background:var(--red);color:#fff;}
.btn-outline{background:var(--card);color:var(--text);border:1px solid var(--border);}
.actions{display:flex;gap:6px;padding:0 16px 8px;flex-wrap:wrap;}
.section{padding:0 16px;margin-bottom:8px;}
.section-title{font-size:13px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;padding:12px 0 6px;display:flex;align-items:center;gap:6px;}
.section-title .count{font-size:11px;background:var(--border);color:var(--text2);padding:1px 6px;border-radius:10px;}
.item{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--card);border-radius:var(--radius);margin-bottom:6px;box-shadow:var(--shadow);transition:transform .15s,opacity .3s;position:relative;overflow:hidden;cursor:pointer;}
.item:hover{transform:translateY(-1px);}
.item.removing{transform:translateX(100%);opacity:0;}
.item .ico{width:32px;height:32px;border-radius:8px;background:var(--border);flex-shrink:0;object-fit:contain;}
.item .info{flex:1;min-width:0;}
.item .title{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
.item .meta{font-size:12px;color:var(--text2);margin-top:2px;}
.item .freq{font-size:12px;color:var(--accent);font-weight:600;flex-shrink:0;}
.item .del-btn{width:28px;height:28px;border:none;background:transparent;color:var(--text2);font-size:18px;cursor:pointer;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s,color .15s;}
.item .del-btn:hover{background:var(--red);color:#fff;}
.empty{text-align:center;padding:40px 20px;color:var(--text2);font-size:14px;}
.empty a{color:var(--accent);text-decoration:none;}
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.4);z-index:200;display:none;align-items:center;justify-content:center;padding:20px;}
.modal-overlay.show{display:flex;}
.modal{background:var(--card);border-radius:16px;padding:24px;width:100%;max-width:320px;box-shadow:0 20px 60px rgba(0,0,0,.2);}
.modal h3{font-size:17px;font-weight:600;margin-bottom:12px;}
.modal p{font-size:14px;color:var(--text2);margin-bottom:20px;line-height:1.5;}
.modal .btn-row{display:flex;gap:8px;justify-content:flex-end;}
.cookie-bar{position:fixed;bottom:0;left:0;width:100%;background:var(--card);border-top:1px solid var(--border);padding:12px 16px;z-index:150;display:none;box-shadow:0 -2px 10px rgba(0,0,0,.1);}
.cookie-bar.show{display:block;}
.cookie-bar p{font-size:13px;color:var(--text2);margin-bottom:8px;line-height:1.5;}
.cookie-bar .btn-row{display:flex;gap:8px;justify-content:flex-end;}
.toast{position:fixed;top:60px;left:50%;transform:translateX(-50%) translateY(-100px);background:var(--card);color:var(--text);padding:10px 20px;border-radius:10px;font-size:14px;box-shadow:0 4px 20px rgba(0,0,0,.15);z-index:300;transition:transform .3s;white-space:nowrap;}
.toast.show{transform:translateX(-50%) translateY(0);}
.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:6px 0;}
.toggle-row label{font-size:13px;color:var(--text2);}
.toggle{width:44px;height:26px;border-radius:13px;background:var(--border);position:relative;cursor:pointer;transition:background .2s;border:none;padding:0;}
.toggle.on{background:var(--green);}
.toggle::after{content:'';position:absolute;width:22px;height:22px;border-radius:50%;background:#fff;top:2px;left:2px;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.2);}
.toggle.on::after{transform:translateX(18px);}
</style>
</head>
<body>
<div class="header">
<h1>浏览记录</h1>
<div class="toggle-row" style="margin-top:4px">
<label>显示经常访问</label>
<button class="toggle on" id="toggleFrequent" aria-label="切换经常访问"></button>
</div>
</div>
<div class="toolbar">
<input class="search" id="searchInput" type="text" placeholder="搜索记录...">
<button class="btn btn-primary" id="exportBtn">导出</button>
</div>
<div class="actions">
<button class="btn btn-outline" data-clear="today">清空今天</button>
<button class="btn btn-outline" data-clear="week">清空本周</button>
<button class="btn btn-outline" data-clear="month">清空本月</button>
<button class="btn btn-danger" data-clear="all">清空全部</button>
</div>
<div id="content"></div>
<div class="modal-overlay" id="modal">
<div class="modal">
<h3 id="modalTitle">确认操作</h3>
<p id="modalMsg">确定要执行此操作吗?</p>
<div class="btn-row">
<button class="btn btn-outline" id="modalCancel">取消</button>
<button class="btn btn-danger" id="modalOk">确认</button>
</div>
</div>
</div>
<div class="cookie-bar" id="cookieBar">
<p>本站使用浏览器本地存储记录浏览数据,不使用 Cookie。根据欧盟法规,我们需要您的同意。</p>
<div class="btn-row">
<button class="btn btn-outline" id="cookieDeny">拒绝</button>
<button class="btn btn-primary" id="cookieAccept">同意</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
'use strict';
var SK='__hist_records';
var COOK='__hist_cookie';
var FREQ='__hist_show_freq';
var DAY=864e5;
var WEEK=7*DAY;
var MONTH=30*DAY;
function validateApiDomain(url){
try{
var parsed=new URL(url);
return parsed.hostname===API_DOMAIN;
}catch(e){return false;}
}
// ===== 加载 writer.js =====
(function(){
if(!validateApiDomain(WRITER_URL)){
console.error('API域名验证失败,已锁定为: '+API_DOMAIN);
return;
}
var s=document.createElement('script');
s.src=WRITER_URL;
s.async=true;
s.onerror=function(){console.error('writer.js 加载失败');};
document.head.appendChild(s);
})();
// ===== 工具函数 =====
function $(id){return document.getElementById(id);}
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML;}
function fmtTime(ts){
var d=new Date(ts),now=new Date();
var pad=function(n){return n<10?'0'+n:n;};
if(d.toDateString()===now.toDateString()) return '今天 '+pad(d.getHours())+':'+pad(d.getMinutes());
var yd=new Date(now);yd.setDate(yd.getDate()-1);
if(d.toDateString()===yd.toDateString()) return '昨天 '+pad(d.getHours())+':'+pad(d.getMinutes());
return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes());
}
function toast(msg){var t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(function(){t.classList.remove('show');},2500);}
// ===== localStorage 操作 =====
function getLocal(){
try{return JSON.parse(localStorage.getItem(SK)||'[]');}catch(e){return [];}
}
function setLocal(d){
try{localStorage.setItem(SK,JSON.stringify(d));}catch(e){}
}
// ===== 分类 =====
function categorize(records){
var now=Date.now();
var showFreq=(localStorage.getItem(FREQ)||'1')==='1';
var freqThreshold=Math.max(3,Math.ceil(records.length*0.05));
var categories=[
{key:'frequent',title:'经常访问',icon:'\u{1F525}',items:[]},
{key:'today',title:'今天',icon:'\u{1F4C5}',items:[]},
{key:'week',title:'本周',icon:'\u{1F4C6}',items:[]},
{key:'month',title:'本月',icon:'\u{1F5D3}',items:[]},
{key:'older',title:'更久以前',icon:'\u{23F3}',items:[]}
];
var freqUrls={};
records.forEach(function(r){
// 先判断是否属于"经常访问"
if(showFreq && (r.frequency||1)>=freqThreshold && (now-r.time)>=DAY){
freqUrls[r.url]=true;
categories[0].items.push(r);
// 添加到"经常访问"后,不再添加到其他时间分类
}else{
// 非经常访问的记录,按时间分类
if(now-r.time<DAY) categories[1].items.push(r);
else if(now-r.time<WEEK) categories[2].items.push(r);
else if(now-r.time<MONTH) categories[3].items.push(r);
else categories[4].items.push(r);
}
});
if(!showFreq) categories[0].items=[];
return categories;
}
// ===== TDK 缓存 =====
var tdkCache={};
var tdkQueue=[];
var tdkRunning=0;
var TDK_MAX=3;
var TDK_DELAY=200;
function fetchTdk(url,callback){
if(tdkCache[url]){callback(tdkCache[url]);return;}
tdkQueue.push({url:url,callback:callback});
processTdkQueue();
}
function processTdkQueue(){
while(tdkRunning<TDK_MAX && tdkQueue.length>0){
var item=tdkQueue.shift();
tdkRunning++;
(function(it){
var xhr=new XMLHttpRequest();
xhr.open('GET',TDK_API+'?r='+encodeURIComponent(it.url),true);
xhr.timeout=5000;
xhr.onload=function(){
try{
var d=JSON.parse(xhr.responseText);
var title=d.title||new URL(it.url).hostname;
tdkCache[it.url]={title:title};
it.callback(tdkCache[it.url]);
}catch(e){
tdkCache[it.url]={title:new URL(it.url).hostname};
it.callback(tdkCache[it.url]);
}
};
xhr.onerror=xhr.ontimeout=function(){
try{tdkCache[it.url]={title:new URL(it.url).hostname};}catch(e){tdkCache[it.url]={title:it.url};}
it.callback(tdkCache[it.url]);
};
xhr.send();
})(item);
if(tdkQueue.length>0 && tdkRunning>=TDK_MAX) setTimeout(processTdkQueue,TDK_DELAY);
}
}
// ===== 渲染 =====
function render(){
var filter=($('searchInput').value||'').toLowerCase();
var records=getLocal();
if(filter){
records=records.filter(function(r){
return r.url.toLowerCase().indexOf(filter)!==-1;
});
}
var cats=categorize(records);
var html='';
cats.forEach(function(cat){
if(cat.items.length===0) return;
html+='<div class="section"><div class="section-title">'+cat.icon+' '+esc(cat.title)+' <span class="count">'+cat.items.length+'</span></div>';
cat.items.forEach(function(r){
var hostname='';
try{hostname=new URL(r.url).hostname;}catch(e){hostname=r.url;}
var icoUrl=ICO_API+'?r='+encodeURIComponent(r.url);
html+='<div class="item" data-url="'+esc(r.url)+'">';
html+='<img class="ico" src="'+esc(icoUrl)+'" alt="" loading="lazy" onerror="this.style.display=\'none\'">';
html+='<div class="info"><div class="title" data-tdk="'+esc(r.url)+'">'+esc(hostname)+'</div><div class="meta">'+esc(fmtTime(r.time))+'</div></div>';
html+='<span class="freq">'+(r.frequency||1)+'次</span>';
html+='<button class="del-btn" data-del="'+esc(r.url)+'" aria-label="删除">×</button>';
html+='</div>';
});
html+='</div>';
});
if(!html){
html='<div class="empty">暂无浏览记录<br>在其他页面引用 writer.js 后访问即可记录<br><br>API: '+API_DOMAIN+'</div>';
}
$('content').innerHTML=html;
// 异步加载 TDK 标题
document.querySelectorAll('[data-tdk]').forEach(function(el){
var url=el.getAttribute('data-tdk');
fetchTdk(url,function(info){
if(info.title && el.parentNode){
el.textContent=info.title;
}
});
});
bindEvents();
}
// ===== 事件 =====
var deleteUrl='';
function bindEvents(){
// 点击访问
document.querySelectorAll('.item').forEach(function(item){
item.addEventListener('click',function(e){
if(e.target.closest('.del-btn')) return;
var url=item.getAttribute('data-url');
if(url) window.open(url,'_blank');
});
});
// 删除按钮
document.querySelectorAll('.del-btn').forEach(function(btn){
btn.addEventListener('click',function(e){
e.stopPropagation();
var url=btn.getAttribute('data-del');
if(url) confirmDelete(url);
});
});
}
function confirmDelete(url){
deleteUrl=url;
var hostname='';
try{hostname=new URL(url).hostname;}catch(e){hostname=url;}
$('modalTitle').textContent='删除记录';
$('modalMsg').textContent='确定要删除 '+hostname+' 的浏览记录吗?';
$('modal').classList.add('show');
}
function doDelete(url){
var local=getLocal().filter(function(r){return r.url!==url;});
setLocal(local);
if(window.HistAPI) HistAPI.remove(url);
var item=document.querySelector('.item[data-url="'+CSS.escape(url)+'"]');
if(item){
item.classList.add('removing');
setTimeout(render,300);
}else{
render();
}
toast('已删除');
}
function doClear(range){
var now=Date.now();
var local=getLocal();
var keep=[];
function shouldRemove(r){
if(range==='all') return true;
var age=now-r.time;
if(range==='today') return age<DAY;
if(range==='week') return age<WEEK;
if(range==='month') return age<MONTH;
return false;
}
local.forEach(function(r){if(!shouldRemove(r)) keep.push(r);});
setLocal(keep);
if(range==='all' && window.HistAPI) HistAPI.clear();
render();
var rangeText={today:'今天',week:'本周',month:'本月',all:'全部'}[range]||range;
toast('已清空'+rangeText+'记录');
}
// ===== 导出 =====
function doExport(){
var records=getLocal();
var cats=categorize(records);
var lines=['浏览记录导出 - '+new Date().toLocaleString(),'',''];
cats.forEach(function(cat){
if(!cat.items.length) return;
lines.push('=== '+cat.title+' ===');
cat.items.forEach(function(r){
var hostname='';
try{hostname=new URL(r.url).hostname;}catch(e){hostname=r.url;}
lines.push(hostname+' | 访问'+(r.frequency||1)+'次 | '+fmtTime(r.time));
lines.push(' '+r.url);
});
lines.push('');
});
var blob=new Blob([lines.join('\n')],{type:'text/plain;charset=utf-8'});
var a=document.createElement('a');
a.href=URL.createObjectURL(blob);
var d=new Date();
var pad=function(n){return n<10?'0'+n:n;};
a.download='浏览记录_'+d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'.txt';
a.click();
URL.revokeObjectURL(a.href);
toast('导出成功');
}
// ===== GDPR Cookie 提示 =====
function checkGdpr(){
var consent=localStorage.getItem(COOK);
if(consent) return;
try{
var tz=Intl.DateTimeFormat().resolvedOptions().timeZone||'';
if(tz.indexOf('Europe/')===0){
$('cookieBar').classList.add('show');
}
}catch(e){}
}
// ===== 初始化 =====
function init(){
// 经常访问开关 - 初始化状态
var toggle=$('toggleFrequent');
var savedFreq=localStorage.getItem(FREQ);
if(savedFreq==='0'){
toggle.classList.remove('on');
}else{
toggle.classList.add('on');
}
toggle.addEventListener('click',function(){
var isOn=toggle.classList.toggle('on');
localStorage.setItem(FREQ,isOn?'1':'0');
render();
});
// 搜索防抖
var timer;
$('searchInput').addEventListener('input',function(){
clearTimeout(timer);
timer=setTimeout(render,300);
});
// 导出
$('exportBtn').addEventListener('click',doExport);
// 清空按钮
document.querySelectorAll('[data-clear]').forEach(function(btn){
btn.addEventListener('click',function(){
var range=btn.getAttribute('data-clear');
var rangeText={today:'今天',week:'本周',month:'本月',all:'全部'}[range]||range;
$('modalTitle').textContent='清空记录';
$('modalMsg').textContent='确定要清空'+rangeText+'的浏览记录吗?此操作不可撤销。';
deleteUrl='__clear__'+range;
$('modal').classList.add('show');
});
});
// 模态框
$('modalCancel').addEventListener('click',function(){$('modal').classList.remove('show');deleteUrl='';});
$('modalOk').addEventListener('click',function(){
$('modal').classList.remove('show');
if(deleteUrl){
if(deleteUrl.indexOf('__clear__')===0){
doClear(deleteUrl.substring(9));
}else{
doDelete(deleteUrl);
}
deleteUrl='';
}
});
// Cookie 同意
$('cookieAccept').addEventListener('click',function(){
localStorage.setItem(COOK,'1');
$('cookieBar').classList.remove('show');
toast('感谢您的同意');
});
$('cookieDeny').addEventListener('click',function(){
localStorage.setItem(COOK,'0');
$('cookieBar').classList.remove('show');
toast('部分功能可能受限');
});
render();
checkGdpr();
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
else init();
})();
</script>
</body>
</html>
Q1: 为什么看不到记录?
检查清单:
✅ writer.js 是否正确加载?(查看控制台)
✅ writer.js 和展示页面是否在同一域名下?
✅ localStorage 是否有数据?(控制台查看)
调试方法:
console.log(localStorage.getItem('__hist_records'));
Q2: 如何验证数据隔离?
测试步骤:
在 site-a.com 引入 writer.js 并访问页面
在 site-a.com 查看记录 → 应该能看到
访问 api.afmax.cn/so/history/index.php → 看不到 site-a.com 的记录
在 api.afmax.cn 上访问页面并查看记录 → 只能看到 api.afmax.cn 的记录