mirror of
https://github.com/jkjoy/Typecho-Theme-Puock.git
synced 2026-08-12 23:09:02 +00:00
Compare commits
18 Commits
e604c6b4d3
..
1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| b416e58596 | |||
| 5d67235be0 | |||
| 134bb0d66c | |||
| 8afb1934dd | |||
| b73e465a79 | |||
| ae990a722b | |||
| 35806ea2fb | |||
| 3bb53afcea | |||
| 9f389fbb34 | |||
| 4c1c795dc2 | |||
| b3b1407647 | |||
| 6b9cc81754 | |||
| c313e29d99 | |||
| 33bc5d53f5 | |||
| 58b2e80883 | |||
| c7efd5abd6 | |||
| 28a72cb279 | |||
| 5abd899791 |
@@ -18,23 +18,42 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var countdown = 5; // 设置倒计时时间(秒)
|
||||
var countdownElement = document.getElementById('time-count-down');
|
||||
|
||||
// 检查是否在 PJAX 环境中
|
||||
var isPjax = typeof window.Pjax !== 'undefined' ||
|
||||
(typeof jQuery !== 'undefined' && jQuery.pjax);
|
||||
// 更新倒计时显示
|
||||
function updateCountdown() {
|
||||
countdownElement.textContent = countdown;
|
||||
countdown--;
|
||||
|
||||
countdown--;
|
||||
if (countdown < 0) {
|
||||
// 倒计时结束,跳转到首页
|
||||
window.location.href = "<?php $this->options->siteUrl(); ?>";
|
||||
var homeUrl = "<?php $this->options->siteUrl(); ?>";
|
||||
|
||||
if (isPjax) {
|
||||
// 使用 PJAX 方式跳转
|
||||
if (typeof window.Pjax !== 'undefined') {
|
||||
// 使用原生 PJAX
|
||||
var pjax = new Pjax();
|
||||
pjax.loadUrl(homeUrl);
|
||||
} else if (typeof jQuery !== 'undefined' && jQuery.pjax) {
|
||||
// 使用 jQuery PJAX
|
||||
$.pjax({url: homeUrl, container: '[data-pjax-container]'});
|
||||
}
|
||||
} else {
|
||||
// 普通跳转
|
||||
window.location.href = homeUrl;
|
||||
}
|
||||
} else {
|
||||
// 继续倒计时
|
||||
setTimeout(updateCountdown, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
updateCountdown();
|
||||
// 如果是 PJAX 加载,需要手动执行一些初始化
|
||||
if (isPjax && typeof window.puockInit !== 'undefined') {
|
||||
window.puockInit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
|
||||
@@ -20,6 +20,49 @@
|
||||
4. 主题部分功能需要安装插件`Puock`。项目地址: [Puock Plugin](https://github.com/jkjoy/typecho-plugin-puock)
|
||||
5. 友情链接功能需要使用插件`Links`。项目地址: [Links Plugin](https://file.imsun.org/upload/2025-06/Links-1.2.7.zip),首页友情链接需要在`友链分类`中添加分类`home`。
|
||||
|
||||
### 主题配置
|
||||
### 更新
|
||||
|
||||
- 全部标签页面需要启用php的 `iconv` 扩展
|
||||
- 2025.07.05
|
||||
- 1.1.3
|
||||
- 兼容typecho 1.3.0版本
|
||||
- 修复pjax黑暗模式下闪烁问题
|
||||
- 修复说说页面的pjax加载问题
|
||||
- 修复上一篇与下一篇文章的链接
|
||||
- 增加归属地显示设置
|
||||
- 增加系统显示设置
|
||||
- 增加浏览器信息显示设置
|
||||
- 修复pjax模式下的404跳转
|
||||
|
||||
- 2025.07.07 表情短代码解析集成
|
||||
- 1.1.5
|
||||
|
||||
- 将表情短代码(如 :smile:)自动解析为图片表情,集成到主题评论内容输出中。
|
||||
|
||||
- 2025.07.08
|
||||
|
||||
- 1.1.6
|
||||
|
||||
- 修复头像区域背景的bug
|
||||
- 优化php8.3兼容性
|
||||
|
||||
- 1.1.8
|
||||
|
||||
- 优化github链接的正则表达式,只解析主仓库链接(如 https://github.com/用户名/仓库名),不再解析子路径(如 /tree/、/blob/ 等)为卡片。
|
||||
- 实现文章内容图片懒加载,自动将常见图片格式(jpg、jpeg、png、webp)的<img>标签替换为带懒加载属性的格式。
|
||||
- 修正getPostCover函数,确保只从原始内容中提取第一张真实图片地址,不受懒加载替换影响,避免首页首图变成load.svg。
|
||||
|
||||
- 新增支持[success]、[primary]、[danger]、[warning]、[info]、[dark]等alert类短代码,自动渲染为对应的Bootstrap风格提示框。
|
||||
- 新增支持[collapse title='xxx']内容[/collapse]折叠面板短代码,自动渲染为带唯一ID的Bootstrap折叠结构。
|
||||
- 新增支持[download file='xxx.zip' size='12MB']文件地址[/download]下载短代码,自动渲染为带文件名、大小、声明和下载地址的下载信息块。
|
||||
- 新增支持[reply]隐藏内容[/reply]回复可见短代码,未满足条件时前端显示提示,已评论且审核通过后显示隐藏内容。
|
||||
|
||||
- 1.2.0
|
||||
|
||||
- 新增支持首页登录
|
||||
|
||||
- 2025.08.02
|
||||
|
||||
- 1.2.2
|
||||
|
||||
- 增加侧边栏显示的全局开关
|
||||
- 修复代码块中的短代码解析问题
|
||||
+15
-5
@@ -3,10 +3,16 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
||||
$this->need('header.php');
|
||||
?>
|
||||
<div class="row row-cols-1">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<div class="animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div class="col-lg-12 col-md-12">
|
||||
<div class="row box-plr15">
|
||||
<?php endif; ?>
|
||||
<div> <!--文章列表-->
|
||||
<div id="posts">
|
||||
<?php if ($this->options->listmodel): ?>
|
||||
<div class=" mr-0 ml-0">
|
||||
<?php while ($this->next()): ?>
|
||||
<?php
|
||||
@@ -61,21 +67,25 @@ $coverImage = getPostCover($this->content, $this->cid);
|
||||
</article>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
<div class="mt20 p-flex-s-right" data-no-instant>
|
||||
<?php $this->pageNav('«', '»', 1, '...', array(
|
||||
<div class="mt20 p-flex-s-right" data-no-instant>
|
||||
<?php $this->pageNav('«', '»', 1, '...', array(
|
||||
'wrapTag' => 'ul',
|
||||
'wrapClass' => 'pagination comment-ajax-load',
|
||||
'itemTag' => 'li',
|
||||
'textTag' => 'span',
|
||||
'currentClass' => 'active',
|
||||
'currentClass' => 'cur',
|
||||
'prevClass' => 'prev',
|
||||
'nextClass' => 'next'
|
||||
)); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php $this->need('card.php'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
File diff suppressed because one or more lines are too long
+80
-43
@@ -74,9 +74,6 @@ class Puock {
|
||||
this.registerModeChangeEvent()
|
||||
this.eventCommentPageChangeEvent()
|
||||
this.eventCommentPreSubmit()
|
||||
this.eventSmiley()
|
||||
this.eventOpenCommentBox()
|
||||
this.eventCloseCommentBox()
|
||||
this.eventSendPostLike()
|
||||
this.eventPostMainBoxResize()
|
||||
this.swiperOnceEvent()
|
||||
@@ -84,6 +81,8 @@ class Puock {
|
||||
this.detectDevice()
|
||||
window.addEventListener('resize', ()=>this.detectDevice());
|
||||
layer.config({shade: 0.5})
|
||||
// 新增:首次加载时初始化评论相关事件
|
||||
this.initCommentEvents();
|
||||
}
|
||||
|
||||
pageInit() {
|
||||
@@ -144,6 +143,10 @@ class Puock {
|
||||
});
|
||||
// form ajax submit
|
||||
$(document).on("submit", ".ajax-form", (e) => {
|
||||
// 如果是登录弹窗表单,允许原生提交
|
||||
if ($(e.target).attr('id') === 'front-login-form') {
|
||||
return true;
|
||||
}
|
||||
e.preventDefault();
|
||||
const form = $(this.ct(e));
|
||||
const formEls = form.find(":input")
|
||||
@@ -209,6 +212,32 @@ class Puock {
|
||||
}
|
||||
return false;
|
||||
})
|
||||
// 登录弹窗表单AJAX提交(集成Puock插件接口)
|
||||
$(document).off('submit', '#front-login-form');
|
||||
$(document).on('submit', '#front-login-form', function(e) {
|
||||
e.preventDefault();
|
||||
var $form = $(this);
|
||||
var data = $form.serialize();
|
||||
$.ajax({
|
||||
url: '/index.php/ajaxlogin/',
|
||||
type: 'POST',
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.success) {
|
||||
window.Puock.toast(res.msg || '登录成功', TYPE_SUCCESS);
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 800);
|
||||
} else {
|
||||
window.Puock.toast(res.msg || '登录失败', TYPE_DANGER);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
window.Puock.toast('请求失败', TYPE_DANGER);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pageLinkBlankOpenInit() {
|
||||
@@ -416,7 +445,7 @@ class Puock {
|
||||
this.pageLinkBlankOpenInit()
|
||||
this.initGithubCard();
|
||||
this.keyUpHandle();
|
||||
this.loadHitokoto();
|
||||
// this.loadHitokoto();
|
||||
// this.asyncCacheViews();
|
||||
this.swiperInit();
|
||||
// this.validateInit();
|
||||
@@ -465,6 +494,8 @@ class Puock {
|
||||
// $('#post-main, #sidebar').theiaStickySidebar({
|
||||
// additionalMarginTop: 20
|
||||
// });
|
||||
// 新增:pjax切换后重新初始化评论相关事件
|
||||
this.initCommentEvents();
|
||||
}
|
||||
|
||||
|
||||
@@ -770,12 +801,14 @@ class Puock {
|
||||
}
|
||||
|
||||
parseFormData(formEl, args = {}) {
|
||||
// 先获取表单所有字段
|
||||
const dataArr = formEl.serializeArray();
|
||||
const data = {...args};
|
||||
const data = {};
|
||||
for (let i = 0; i < dataArr.length; i++) {
|
||||
data[dataArr[i].name] = dataArr[i].value;
|
||||
}
|
||||
return jQuery.param(data);
|
||||
// 合并额外参数
|
||||
return jQuery.param(Object.assign(data, args));
|
||||
}
|
||||
|
||||
eventCommentPreSubmit() {
|
||||
@@ -879,33 +912,49 @@ class Puock {
|
||||
}
|
||||
|
||||
eventOpenCommentBox() {
|
||||
$(document).on("click", "[id^=comment-reply-]", (e) => {
|
||||
this.data.comment.replyId = $(this.ct(e)).attr("data-id");
|
||||
if ($.trim(this.data.comment.replyId) === '') {
|
||||
this.toast('结构有误', TYPE_DANGER);
|
||||
$(document).off("click", ".comment-reply");
|
||||
$(document).on("click", ".comment-reply", function(e) {
|
||||
e.preventDefault();
|
||||
const replyBtn = $(e.currentTarget);
|
||||
const replyId = replyBtn.attr("data-coid");
|
||||
if ($.trim(replyId) === '') {
|
||||
window.Puock.toast('结构有误', TYPE_DANGER);
|
||||
return;
|
||||
}
|
||||
const cf = $("#comment-form"),
|
||||
cb = $("#comment-box-" + this.data.comment.replyId);
|
||||
cf.addClass("box-sw");
|
||||
cb.removeClass("d-none").append(cf);
|
||||
const cf = $("#comment-form");
|
||||
const commentLi = replyBtn.closest('.post-comment');
|
||||
// 只在表单在原位时插入占位符
|
||||
if (!$("#comment-form-place-holder").length && cf.parent().attr("id") === "comment-form-box") {
|
||||
cf.before('<div id="comment-form-place-holder"></div>');
|
||||
}
|
||||
// 每次都append到目标评论下方
|
||||
commentLi.append(cf);
|
||||
$("#comment-cancel").removeClass("d-none");
|
||||
$("#comment").val("");
|
||||
$("#comment_parent").val(this.data.comment.replyId);
|
||||
})
|
||||
|
||||
$("#comment_parent").val(replyId);
|
||||
window.Puock.data.comment.replyId = replyId;
|
||||
// 滚动至表单
|
||||
if (cf.length && cf[0].scrollIntoView) {
|
||||
cf[0].scrollIntoView({behavior: "smooth", block: "center"});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
eventCloseCommentBox() {
|
||||
$(document).off("click", "#comment-cancel");
|
||||
$(document).on("click", "#comment-cancel", () => {
|
||||
const cf = $("#comment-form"),
|
||||
cb = $("#comment-box-" + this.data.comment.replyId);
|
||||
cf.removeClass("box-sw");
|
||||
cb.addClass("d-none");
|
||||
$("#comment-form-box").append(cf);
|
||||
const cf = $("#comment-form");
|
||||
const holder = $("#comment-form-place-holder");
|
||||
if (holder.length) {
|
||||
holder.before(cf);
|
||||
holder.remove();
|
||||
} else {
|
||||
$("#comment-form-box").append(cf);
|
||||
}
|
||||
$("#comment-cancel").addClass("d-none");
|
||||
this.data.comment.replyId = null;
|
||||
})
|
||||
$("#comment_parent").val('');
|
||||
});
|
||||
}
|
||||
|
||||
eventSendPostLike() {
|
||||
@@ -949,11 +998,12 @@ class Puock {
|
||||
}
|
||||
|
||||
eventSmiley() {
|
||||
$(document).off('click', '.smiley-img');
|
||||
$(document).on('click', '.smiley-img', (e) => {
|
||||
const comment = $("#comment");
|
||||
comment.val(comment.val() + ' ' + $(this.ct(e)).attr("data-id") + ' ');
|
||||
layer.closeAll();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
@@ -1135,25 +1185,6 @@ class Puock {
|
||||
});
|
||||
}
|
||||
|
||||
loadHitokoto() {
|
||||
setTimeout(() => {
|
||||
$(".widget-puock-hitokoto").each((_, v) => {
|
||||
const el = $(v);
|
||||
const api = el.attr("data-api") || "https://v1.hitokoto.cn/"
|
||||
$.get(api, (res) => {
|
||||
el.find(".t").text(res.hitokoto ?? res.content ?? "无内容");
|
||||
el.find('.f').text(res.from);
|
||||
el.find('.fb').removeClass("d-none");
|
||||
}, 'json').fail((err) => {
|
||||
console.error(err)
|
||||
el.find(".t").text("加载失败:" + err.responseText || err);
|
||||
el.remove(".fb");
|
||||
})
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
|
||||
|
||||
toast(msg, type = TYPE_PRIMARY, options = {}) {
|
||||
options = Object.assign({
|
||||
duration: 2600,
|
||||
@@ -1171,6 +1202,12 @@ class Puock {
|
||||
return t;
|
||||
}
|
||||
|
||||
// 新增:统一初始化评论相关事件
|
||||
initCommentEvents() {
|
||||
this.eventOpenCommentBox();
|
||||
this.eventCloseCommentBox();
|
||||
this.eventSmiley();
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
|
||||
@@ -63,4 +63,20 @@ $colors = ['bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', 'bg-warning'
|
||||
</div>
|
||||
</article>
|
||||
<?php endwhile; ?>
|
||||
<?php
|
||||
$pageprev = $this->options->pageprev ?? '0';
|
||||
if ($pageprev == '1' && $this->have()):
|
||||
?>
|
||||
<div class="mt20 p-flex-s-right" data-no-instant>
|
||||
<?php $this->pageNav('«', '»', 1, '...', array(
|
||||
'wrapTag' => 'ul',
|
||||
'wrapClass' => 'pagination comment-ajax-load',
|
||||
'itemTag' => 'li',
|
||||
'textTag' => 'span',
|
||||
'currentClass' => 'cur',
|
||||
'prevClass' => 'prev',
|
||||
'nextClass' => 'next'
|
||||
)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
+53
-129
@@ -21,7 +21,6 @@
|
||||
<input type="text" value="1" hidden name="comment-logged" id="comment-logged">
|
||||
<div class="col-12">
|
||||
<p class="t-sm c-sub">登录身份: <a href="<?php $this->options->profileUrl(); ?>"><?php $this->user->screenName(); ?></a> . <a href="<?php $this->options->logoutUrl(); ?>" title="Logout">登出 »</a></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
@@ -44,9 +43,9 @@
|
||||
<div>
|
||||
<?php if(!$this->user->hasLogin()): ?>
|
||||
<div class="d-inline-block">
|
||||
<a class="btn btn-primary btn-ssm" href="<?php $this->options->loginUrl(); ?>" title="登录" target="_blank">
|
||||
<i class="fa fa-right-to-bracket"></i> 登录
|
||||
</a>
|
||||
<button class="btn btn-primary btn-ssm pk-modal-toggle" type="button" data-once-load="true" data-id="front-login" title="快捷登录" data-url="<?php echo get_correct_url('/login/'); ?>">
|
||||
<i class="fa fa-right-to-bracket"></i> 快捷登录
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -60,6 +59,7 @@
|
||||
<i class="fa-regular fa-face-smile t-md"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<input type="hidden" name="parent" id="comment_parent" value="">
|
||||
<button type="submit" id="comment-submit" class="btn btn-primary btn-ssm">
|
||||
<i class="fa-regular fa-paper-plane"></i> 发布评论
|
||||
</button>
|
||||
@@ -69,49 +69,49 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="comment-ajax-load" class="text-center mt20 d-none">
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pk-skeleton _comment">
|
||||
<div class="_h">
|
||||
<div class="_avatar"></div>
|
||||
<div class="_info">
|
||||
<div class="_name"></div>
|
||||
<div class="_date"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mt20 alert alert-warning">
|
||||
<i class="fa fa-exclamation-circle"></i> 评论已关闭
|
||||
@@ -172,21 +172,15 @@
|
||||
</div>
|
||||
<div class="t-sm c-sub">
|
||||
<span><?php $comments->date('Y-m-d H:i:s'); ?></span>
|
||||
<?php $comments->reply(
|
||||
sprintf('<a onclick="return TypechoComment.reply(\'%s\', %d);" class="hide-info animated bounceIn c-sub-a t-sm ml-1 comment-reply" href="%s#comment-%d"><span class="comment-reply-text"><i class="fa fa-share-from-square"></i>回复</span></a>',
|
||||
$comments->theId,
|
||||
$comments->coid,
|
||||
$comments->commentUrl($comments->coid),
|
||||
$comments->coid)
|
||||
); ?>
|
||||
<a rel="nofollow" class="hide-info animated bounceIn c-sub-a t-sm ml-1 comment-reply" href="javascript:void(0);" data-coid="<?php echo $comments->coid; ?>"><span class="comment-reply-text"><i class="fa fa-share-from-square"></i>回复</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content-text t-md mt10 puock-text">
|
||||
<?php if ($comments->parent) {echo getPermalinkFromCoid($comments->parent);} $comments->content();?>
|
||||
<?php if ($comments->parent) {echo getPermalinkFromCoid($comments->parent);} echo parse_smiley_shortcode($comments->content);?>
|
||||
<div class="comment-os c-sub">
|
||||
<?php
|
||||
<?php
|
||||
$deviceInfo = getBrowsersInfo($comments->agent);
|
||||
$icons = getDeviceIcon($deviceInfo);
|
||||
?>
|
||||
@@ -199,7 +193,6 @@
|
||||
: '未知系统';
|
||||
?>
|
||||
</span>
|
||||
|
||||
<!-- 浏览器信息 -->
|
||||
<span class="mt10" title="<?php echo $deviceInfo['browser'] . ' ' . $deviceInfo['version']; ?>">
|
||||
<?php echo $icons['browser']; ?>
|
||||
@@ -209,12 +202,12 @@
|
||||
: '未知浏览器';
|
||||
?>
|
||||
</span>
|
||||
|
||||
<!-- IP 地理位置 -->
|
||||
<?php if($comments->ip): ?>
|
||||
<span class="mt10" title="IP">
|
||||
<i class="fa-solid fa-location-dot"></i> <?php echo get_ip_region($comments->ip); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,73 +217,4 @@
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<script>
|
||||
var TypechoComment = {
|
||||
dom : function (id) {
|
||||
return document.getElementById(id);
|
||||
},
|
||||
|
||||
create : function (tag, attr) {
|
||||
var el = document.createElement(tag);
|
||||
|
||||
for (var key in attr) {
|
||||
el.setAttribute(key, attr[key]);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
reply : function (cid, coid) {
|
||||
var comment = this.dom(cid), parent = comment.parentNode,
|
||||
response = this.dom('comment-form-box'), holder = response.parentNode,
|
||||
form = this.dom('comment-form'), textarea = this.dom('comment'),
|
||||
submit = this.dom('comment-submit'),
|
||||
cancel = this.dom('comment-cancel');
|
||||
|
||||
if (null != this.dom('comment-form-place-holder')) {
|
||||
var holder = this.dom('comment-form-place-holder');
|
||||
}
|
||||
|
||||
// 移动评论表单
|
||||
if (null == this.dom('comment-form-place-holder')) {
|
||||
var holder = this.create('div', {
|
||||
'id': 'comment-form-place-holder'
|
||||
});
|
||||
|
||||
response.parentNode.insertBefore(holder, response);
|
||||
}
|
||||
|
||||
// 修改表单中的内容
|
||||
comment.appendChild(response);
|
||||
|
||||
// 显示取消回复按钮
|
||||
cancel.style.display = '';
|
||||
|
||||
// 记录回复的评论ID
|
||||
this.dom('comment-parent').value = coid;
|
||||
|
||||
// 移动光标到评论框
|
||||
textarea.focus();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
cancelReply : function () {
|
||||
var response = this.dom('comment-form-box'), holder = this.dom('comment-form-place-holder'),
|
||||
comment = this.dom('comment-parent');
|
||||
|
||||
if (null != holder) {
|
||||
holder.parentNode.insertBefore(response, holder);
|
||||
holder.parentNode.removeChild(holder);
|
||||
}
|
||||
|
||||
// 隐藏取消回复按钮
|
||||
this.dom('comment-cancel').style.display = 'none';
|
||||
// 清除回复评论ID
|
||||
comment.value = '';
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<?php } ?>
|
||||
+2
-3
@@ -68,7 +68,7 @@
|
||||
© <?php echo date('Y'); ?> <a href="<?php $this->options->siteUrl(); ?>"><?php $this->options->title(); ?></a>
|
||||
<div class="fs12 mt10 c-sub">
|
||||
<span> Theme by
|
||||
<a target="_blank" class="c-sub" title="Puock v2.8.14" href="https://github.com/jkjoy/typecho-theme-puock">Puock</a>
|
||||
<a target="_blank" class="c-sub" title="Puock v1.2.2" href="https://github.com/jkjoy/typecho-theme-puock">Puock</a>
|
||||
</span>
|
||||
<span> Powered by
|
||||
<a target="_blank" class="c-sub" title="Typecho" href="https://typecho.org">Typecho</a> <p><a target="_blank" class="c-sub" title="老孙博客" href="https://imsun.org">老孙博客</a>制作</p>
|
||||
@@ -85,7 +85,7 @@
|
||||
"home": "<?php $this->options->siteUrl(); ?>",
|
||||
"use_post_menu": true,
|
||||
"is_single": false,
|
||||
"is_pjax": false,
|
||||
"is_pjax": true,
|
||||
"main_lazy_img": true,
|
||||
"link_blank_open": true,
|
||||
//"async_view_id": null,
|
||||
@@ -102,5 +102,4 @@
|
||||
<script type="text/javascript" data-no-instant src="<?php $this->options->themeUrl('assets/js/puock.js'); ?>" id="puock-js"></script>
|
||||
<?php $this->footer(); ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+784
-379
File diff suppressed because it is too large
Load Diff
+56
-14
@@ -4,23 +4,26 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta http-equiv='content-language' content='zh_CN'>
|
||||
<title><?php $this->archiveTitle([
|
||||
'category' => _t('分类 %s 下的文章'),
|
||||
'search' => _t('包含关键字 %s 的文章'),
|
||||
'tag' => _t('标签 %s 下的文章'),
|
||||
'author' => _t('%s 发布的文章')
|
||||
], '', ' - '); ?><?php $this->options->title(); ?></title>
|
||||
], '', ' - '); ?>
|
||||
<?php $this->options->title(); ?>
|
||||
<?php if ($this->is('index')) echo ' - '; ?>
|
||||
<?php if ($this->is('index')) $this->options->description() ?>
|
||||
</title>
|
||||
<link rel="canonical" href="<?php $this->options->siteUrl(); ?>">
|
||||
<meta name='robots' content='max-image-preview:large' />
|
||||
<?php $this->options->addhead(); ?>
|
||||
<style id='puock-inline-css' type='text/css'>
|
||||
body {
|
||||
--pk-c-primary: <?php if ($this->options->primaryColor): ?><?php $this->options->primaryColor() ?><?php else: ?>#A7E6F4<?php endif; ?>;
|
||||
--pk-c-primary: <?php if ($this->options->primaryColor): ?><?php $this->options->primaryColor() ?><?php else: ?>#A7E6F4<?php endif; ?> !important;
|
||||
}
|
||||
:root {
|
||||
--puock-block-not-tran: <?php if ($this->options->blockNotTransparent): ?><?php $this->options->blockNotTransparent() ?><?php else: ?>100<?php endif; ?>%;
|
||||
--puock-block-not-tran: <?php if ($this->options->blockNotTransparent): ?><?php $this->options->blockNotTransparent() ?><?php else: ?>100<?php endif; ?>% !important;
|
||||
}
|
||||
</style>
|
||||
<?php if ($this->options->icoUrl): ?>
|
||||
@@ -32,7 +35,6 @@
|
||||
<!-- 通过自有函数输出HTML头部信息 -->
|
||||
<?php $this->header(); ?>
|
||||
</head>
|
||||
|
||||
<body class="puock-auto custom-background">
|
||||
<div>
|
||||
<div id="header-box" class="animated fadeInDown"></div>
|
||||
@@ -73,7 +75,7 @@
|
||||
</li>
|
||||
<?php \Widget\Contents\Page\Rows::alloc()->to($pages); ?>
|
||||
<?php while ($pages->next()): ?>
|
||||
<li <?php if ($this->is('page', $pages->slug)): ?> class='current-menu-item current_page_item menu-current<?php endif; ?> menu-item menu-item-type-post_type menu-item-object-page '>
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-page<?php if ($this->is('page', $pages->slug)) echo ' current-menu-item current_page_item menu-current'; ?>">
|
||||
<a class='ww'
|
||||
href="<?php $pages->permalink(); ?>"
|
||||
title="<?php $pages->title(); ?>">
|
||||
@@ -81,6 +83,15 @@
|
||||
</a>
|
||||
</li>
|
||||
<?php endwhile; ?>
|
||||
<?php if($this->user->hasLogin()): ?>
|
||||
<li>
|
||||
<a data-bs-toggle="tooltip" title="用户中心" href="/admin" target="_blank">
|
||||
<img alt="用户中心" src="<?php $stats = get_site_statistics();echo $stats['avatar']; ?>" class="min-avatar">
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li><a data-no-instant data-bs-toggle="tooltip" title="登入" data-title="登入" href="javascript:void(0)" class="pk-modal-toggle" data-once-load="true" data-url="<?php echo get_correct_url('/login/'); ?>"><i class="fa fa-right-to-bracket"></i></a></li>
|
||||
<?php endif; ?>
|
||||
<li><a class="colorMode" data-bs-toggle="tooltip" title="模式切换" href="javascript:void(0)"><i class="fa fa-circle-half-stroke"></i></a></li>
|
||||
<li><a class="search-modal-btn" data-bs-toggle="tooltip" title="搜索" href="javascript:void(0)"><i class="fa fa-search"></i></a></li>
|
||||
</ul>
|
||||
@@ -128,8 +139,8 @@
|
||||
<?php endwhile; ?>
|
||||
<li class='menu-item menu-item-type-post_type menu-item-object-page'>
|
||||
<span><a href="#">分类</a>
|
||||
<a href="#menu-sub-689" data-bs-toggle="collapse"><i class="fa fa-chevron-down t-sm ml-1 menu-sub-icon"></i></a></span>
|
||||
<ul id="menu-sub-689" class="sub-menu collapse">
|
||||
<a href="#menu" data-bs-toggle="collapse"><i class="fa fa-chevron-down t-sm ml-1 menu-sub-icon"></i></a></span>
|
||||
<ul id="menu" class="sub-menu collapse">
|
||||
<?php $categories = Typecho_Widget::widget('Widget_Metas_Category_List'); ?>
|
||||
<?php while($categories->next()): ?>
|
||||
<li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-child">
|
||||
@@ -141,6 +152,15 @@
|
||||
</li>
|
||||
<?php endwhile; ?>
|
||||
</ul>
|
||||
<?php if($this->user->hasLogin()): ?>
|
||||
<li>
|
||||
<a data-bs-toggle="tooltip" title="用户中心" href="/admin" target="_blank">
|
||||
<img alt="用户中心" src="<?php $stats = get_site_statistics();echo $stats['avatar']; ?>" class="min-avatar">
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li><a data-no-instant data-bs-toggle="tooltip" title="登入" data-title="登入" href="javascript:void(0)" class="pk-modal-toggle" data-once-load="true" data-url="<?php echo get_correct_url('/login/'); ?>"><i class="fa fa-right-to-bracket"></i></a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -157,12 +177,34 @@
|
||||
<div data-swiper="init" data-swiper-class="global-top-notice-swiper" data-swiper-args='{"direction":"vertical","autoplay":{"delay":3000,"disableOnInteraction":false},"loop":true}'>
|
||||
<div class="swiper global-top-notice-swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide t-line-1">
|
||||
<a class="ta3" data-no-instant href="javascript:void(0)">
|
||||
<span class="notice-icon"><i class="fa-regular fa-bell"></i></span>
|
||||
<span><?php $this->options->gonggao(); ?></span>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
// 获取公告内容
|
||||
$gonggao = $this->options->gonggao;
|
||||
if ($gonggao) {
|
||||
// 按行分割
|
||||
$lines = explode("\n", $gonggao);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode('|', $line);
|
||||
// 只处理格式正确的行
|
||||
if (count($parts) >= 3) {
|
||||
$title = trim($parts[0]);
|
||||
$url = trim($parts[1]);
|
||||
$icon = trim($parts[2]);
|
||||
// 链接为空时使用 javascript:void(0)
|
||||
$href = $url !== '' ? htmlspecialchars($url) : 'javascript:void(0)';
|
||||
// 图标为空时使用默认
|
||||
$icon_class = $icon !== '' ? $icon : 'fa-regular fa-bell';
|
||||
// 输出 HTML
|
||||
echo '<div class="swiper-slide t-line-1">';
|
||||
echo '<a class="ta3" data-no-instant href="' . $href . '">';
|
||||
echo '<span class="notice-icon"><i class="' . $icon_class . '"></i></span>';
|
||||
echo '<span>' . htmlspecialchars($title) . '</span>';
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Pouck theme for Typecho
|
||||
*
|
||||
* 老孙移植
|
||||
*
|
||||
* @package Typecho Pouck Theme
|
||||
* @author 老孙博客
|
||||
* @version 1.1
|
||||
* @version 1.2.2
|
||||
* @link http://www.imsun.org
|
||||
*/
|
||||
|
||||
@@ -13,8 +14,13 @@ $this->need('header.php');
|
||||
$this->need('sticky.php');
|
||||
?>
|
||||
<div class="row row-cols-1">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<div class="animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div class="col-lg-12 col-md-12">
|
||||
<div class="row box-plr15">
|
||||
<?php endif; ?>
|
||||
<div> <!--文章列表-->
|
||||
<div id="posts">
|
||||
<?php if ($this->options->listmodel): ?>
|
||||
@@ -82,7 +88,7 @@ if ($pageprev == '1' && $this->have()):
|
||||
'wrapClass' => 'pagination comment-ajax-load',
|
||||
'itemTag' => 'li',
|
||||
'textTag' => 'span',
|
||||
'currentClass' => 'active',
|
||||
'currentClass' => 'cur',
|
||||
'prevClass' => 'prev',
|
||||
'nextClass' => 'next'
|
||||
)); ?>
|
||||
@@ -96,5 +102,7 @@ if ($pageprev == '1' && $this->have()):
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php endif; ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
@@ -0,0 +1,4 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
/.idea
|
||||
/vendor/
|
||||
/composer.lock
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* class Ip2Region
|
||||
* 为兼容老版本调度而创建
|
||||
* @author Anyon<zoujingli@qq.com>
|
||||
* @datetime 2022/07/18
|
||||
*/
|
||||
class Ip2Region
|
||||
{
|
||||
/**
|
||||
* 查询实例对象
|
||||
* @var XdbSearcher
|
||||
*/
|
||||
private $searcher;
|
||||
|
||||
/**
|
||||
* 初始化构造方法
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
class_exists('XdbSearcher') or include __DIR__ . '/XdbSearcher.php';
|
||||
$this->searcher = XdbSearcher::newWithFileOnly(__DIR__ . '/ip2region.xdb');
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容原 memorySearch 查询
|
||||
* @param string $ip
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function memorySearch($ip)
|
||||
{
|
||||
return ['city_id' => 0, 'region' => $this->searcher->search($ip)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容原 binarySearch 查询
|
||||
* @param string $ip
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function binarySearch($ip)
|
||||
{
|
||||
return $this->memorySearch($ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容原 btreeSearch 查询
|
||||
* @param string $ip
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function btreeSearch($ip)
|
||||
{
|
||||
return $this->memorySearch($ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询并返回名称
|
||||
* @param string $ip
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function simple($ip)
|
||||
{
|
||||
$geo = $this->memorySearch($ip);
|
||||
$arr = explode('|', str_replace(['0|'], '|', isset($geo['region']) ? $geo['region'] : ''));
|
||||
if (($last = array_pop($arr)) === '内网IP') $last = '';
|
||||
return join('', $arr) . (empty($last) ? '' : "【{$last}】");
|
||||
}
|
||||
|
||||
/**
|
||||
* destruct method
|
||||
* resource destroy
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->searcher->close();
|
||||
unset($this->searcher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
==========================================================================
|
||||
The following license applies to the ip2region library
|
||||
--------------------------------------------------------------------------
|
||||
Copyright (c) 2015 Lionsoul<chenxin619315@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,229 @@
|
||||
[](https://packagist.org/packages/zoujingli/ip2region)
|
||||
[](https://packagist.org/packages/zoujingli/ip2region)
|
||||
[](https://packagist.org/packages/zoujingli/ip2region)
|
||||
[](https://packagist.org/packages/zoujingli/ip2region)
|
||||
[](https://packagist.org/packages/ip2region)
|
||||
[](https://packagist.org/packages/zoujingli/ip2region)
|
||||
|
||||
本库基于 [ip2region](https://github.com/lionsoul2014/ip2region) 简单整合,方便使用 `Composer` 管理。
|
||||
|
||||
# Ip2region 是什么
|
||||
|
||||
ip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 `xdb` 数据生成和查询客户端实现。
|
||||
|
||||
# Ip2region 特性
|
||||
|
||||
### 1、标准化的数据格式
|
||||
|
||||
每个 ip 数据段的 region 信息都固定了格式:`国家|区域|省份|城市|ISP`,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,后前的选项全部是0。
|
||||
|
||||
### 2、数据去重和压缩
|
||||
|
||||
`xdb` 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。
|
||||
|
||||
### 3、极速查询响应
|
||||
|
||||
即使是完全基于 `xdb` 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:
|
||||
|
||||
1. `vIndex` 索引缓存 :使用固定的 `512KiB` 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。
|
||||
2. `xdb` 整个文件缓存:将整个 `xdb` 文件全部加载到内存,内存占用等同于 `xdb` 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。
|
||||
|
||||
### 4、IP 数据管理框架
|
||||
|
||||
v2.0 格式的 `xdb` 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。
|
||||
|
||||
# `xdb` 数据查询
|
||||
|
||||
API 介绍,使用文档和测试程序请参考对应 `searcher` 查询客户端下的 ReadMe 介绍,全部查询 binding 实现情况如下:
|
||||
|
||||
| Ok? | 状态 | 编程语言 | 描述 | 贡献者 |
|
||||
|:-------------------|:----|:----------------------------------------------------------------------------------|:---------------------|:------------------------------------------|
|
||||
| :white_check_mark: | 已完成 | [golang](https://github.com/lionsoul2014/ip2region/blob/master/binding/golang) | golang xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [php](https://github.com/lionsoul2014/ip2region/blob/master/binding/php) | php xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [java](https://github.com/lionsoul2014/ip2region/blob/master/binding/java) | java xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [lua](https://github.com/lionsoul2014/ip2region/blob/master/binding/lua) | 纯 lua xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [c](https://github.com/lionsoul2014/ip2region/blob/master/binding/c) | ANSC c xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [lua_c](https://github.com/lionsoul2014/ip2region/blob/master/binding/lua_c) | lua c 扩展 xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| | 待开始 | [rust](https://github.com/lionsoul2014/ip2region/blob/master/binding/rust) | rust xdb 查询客户端实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [python](https://github.com/lionsoul2014/ip2region/blob/master/binding/python) | python xdb 查询客户端实现 | [厉害的花花](https://github.com/luckydog6132) |
|
||||
| :white_check_mark: | 已完成 | [nodejs](https://github.com/lionsoul2014/ip2region/blob/master/binding/nodejs) | nodejs xdb 查询客户端实现 | [Wu Jian Ping](https://github.com/wujjpp) |
|
||||
| :white_check_mark: | 已完成 | [csharp](https://github.com/lionsoul2014/ip2region/blob/master/binding/csharp) | csharp xdb 查询客户端实现 | [Alen Lee](https://github.com/malus2077) |
|
||||
| | 待开始 | [php_ext](https://github.com/lionsoul2014/ip2region/blob/master/binding/php7_ext) | php c 扩展 xdb 查询客户端实现 | 待确定 |
|
||||
| | 待开始 | [nginx](https://github.com/lionsoul2014/ip2region/blob/master/binding/nginx) | nginx 扩展 xdb 查询客户端实现 | 待确定 |
|
||||
|
||||
# `xdb` 数据生成
|
||||
|
||||
API 介绍,使用文档和测试程序请参考对应 `maker` 生成程序下的 ReadMe 介绍,全部生成 maker 实现情况如下:
|
||||
|
||||
| Ok? | 状态 | 编程语言 | 描述 | 贡献者 |
|
||||
|:-------------------|:----|:-----------------------------------------------------------------------------|:------------------|:-----------------------------------------|
|
||||
| :white_check_mark: | 已完成 | [golang](https://github.com/lionsoul2014/ip2region/blob/master/maker/golang) | golang xdb 生成程序实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [java](https://github.com/lionsoul2014/ip2region/blob/master/maker/java) | java xdb 生成程序实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| | 待开始 | [c](https://github.com/lionsoul2014/ip2region/blob/master/maker/c) | ANSC c xdb 生成程序实现 | [Lion](https://github.com/lionsoul2014) |
|
||||
| :white_check_mark: | 已完成 | [python](https://github.com/lionsoul2014/ip2region/blob/master/maker/python) | python xdb 生成程序实现 | [leolin49](https://github.com/leolin49) |
|
||||
| :white_check_mark: | 已完成 | [csharp](https://github.com/lionsoul2014/ip2region/blob/master/maker/csharp) | csharp xdb 生成程序实现 | [Alan Lee](https://github.com/malus2077) |
|
||||
|
||||
# 并发查询必读
|
||||
|
||||
全部查询客户端的 search 接口都 <b>不是</b> 并发安全的实现,不同进程/线程/协程需要通过创建不同的查询对象来安全使用,并发量很大的情况下,基于文件查询的方式可能会导致打开文件数过多的错误,请修改内核的最大允许打开文件数(fs.file-max=一个更高的值),或者将整个xdb加载到内存进行安全并发使用。
|
||||
|
||||
# 相关备注
|
||||
|
||||
### 1、使用声明
|
||||
|
||||
ip2region 重点在于<b>研究 IP 定位数据的存储设计和各种语言的查询实现</b>,并没有原始 IP 数据的支撑,本项目不保证及时的数据更新,没有也不会有商用版本,你可以使用自定义的数据导入 ip2region 进行管理。
|
||||
|
||||
### 2、技术交流
|
||||
|
||||
ip2region 微信交流群,请先加微信:lionsoul2014 (请备注 ip2region)
|
||||
|
||||
### 3、数据更新
|
||||
|
||||
基于检测算法的数据更新方式视频分享:[数据更新实现视频分享 - part1](https://www.bilibili.com/video/BV1934y1E7Q5/),[数据更新实现视频分享 - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)
|
||||
|
||||
### 4、数据结构
|
||||
|
||||
1. xdb 数据结构分析:[“ip2region xdb 数据结构和查询过程详解“](https://mp.weixin.qq.com/s?__biz=MzU4MDc2MzQ5OA==&mid=2247483696&idx=1&sn=6e9e138e86cf18245656c54ff4be3129&chksm=fd50ab35ca2722239ae7c0bb08efa44f499110c810227cbad3a16f36ebc1c2afc58eb464a57c#rd)
|
||||
2. xdb 查询过程分析:[“ip2region xdb 数据结构和查询过程详解”](https://mp.weixin.qq.com/s?__biz=MzU4MDc2MzQ5OA==&mid=2247483696&idx=1&sn=6e9e138e86cf18245656c54ff4be3129&chksm=fd50ab35ca2722239ae7c0bb08efa44f499110c810227cbad3a16f36ebc1c2afc58eb464a57c#rd)
|
||||
3. xdb 生成过程分析:[“ip2region xdb 二进制数据生成过程详解”](https://mp.weixin.qq.com/s?__biz=MzU4MDc2MzQ5OA==&mid=2247483718&idx=1&sn=92e552f3bba44a97ca661da244f35574&chksm=fd50ab43ca2722559733ed4e1082f239f381aaa881f9dbeb479174936145522696d9d200531e#rd)
|
||||
|
||||
# 关于 ip2region v2.0 的 PHP 用法
|
||||
|
||||
### 完全基于文件的查询
|
||||
|
||||
```php
|
||||
$dbFile = "ip2region.xdb file path";
|
||||
try {
|
||||
$searcher = XdbSearcher::newWithFileOnly($dbFile);
|
||||
} catch (Exception $e) {
|
||||
printf("failed to create searcher with '%s': %s\n", $dbFile, $e);
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = '1.2.3.4';
|
||||
$sTime = XdbSearcher::now();
|
||||
$region = $searcher->search($ip);
|
||||
if ($region === null) {
|
||||
// something is wrong
|
||||
printf("failed search(%s)\n", $ip);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("{region: %s, took: %.5f ms}\n", $region, XdbSearcher::now() - $sTime);
|
||||
|
||||
// 备注:并发使用,每个线程或者协程需要创建一个独立的 searcher 对象。
|
||||
```
|
||||
|
||||
### 缓存 `VectorIndex` 索引
|
||||
|
||||
如果你的 php 母环境支持,可以预先加载 vectorIndex 缓存,然后做成全局变量,每次创建 Searcher 的时候使用全局的 vectorIndex,可以减少一次固定的 IO 操作从而加速查询,减少 io 压力。
|
||||
|
||||
```php
|
||||
// 1、从 dbPath 加载 VectorIndex 缓存,把下述的 vIndex 变量缓存到内存里面。
|
||||
$vIndex = XdbSearcher::loadVectorIndexFromFile($dbPath);
|
||||
if ($vIndex === null) {
|
||||
printf("failed to load vector index from '%s'\n", $dbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
|
||||
try {
|
||||
$searcher = XdbSearcher::newWithVectorIndex($dbFile, $vIndex);
|
||||
} catch (Exception $e) {
|
||||
printf("failed to create vectorIndex cached searcher with '%s': %s\n", $dbFile, $e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3、查询
|
||||
$sTime = XdbSearcher::now();
|
||||
$region = $searcher->search('1.2.3.4');
|
||||
if ($region === null) {
|
||||
printf("failed search(1.2.3.4)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("{region: %s, took: %.5f ms}\n", $region, XdbSearcher::now() - $sTime);
|
||||
|
||||
// 备注:并发使用,每个线程或者协程需要创建一个独立的 searcher 对象,但是都共享统一的只读 vectorIndex。
|
||||
```
|
||||
|
||||
### 缓存整个 `xdb` 数据
|
||||
|
||||
如果你的 PHP 母环境支持,可以预先加载整个 `xdb` 的数据到内存,这样可以实现完全基于内存的查询,类似之前的 memory search 查询。
|
||||
|
||||
```php
|
||||
// 1、从 dbPath 加载整个 xdb 到内存。
|
||||
$cBuff = XdbSearcher::loadContentFromFile($dbPath);
|
||||
if ($cBuff === null) {
|
||||
printf("failed to load content buffer from '%s'\n", $dbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2、使用全局的 cBuff 创建带完全基于内存的查询对象。
|
||||
try {
|
||||
$searcher = XdbSearcher::newWithBuffer($cBuff);
|
||||
} catch (Exception $e) {
|
||||
printf("failed to create buffer cached searcher: %s\n", $dbFile, $e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3、查询
|
||||
$sTime = XdbSearcher::now();
|
||||
$region = $searcher->search('1.2.3.4');
|
||||
if ($region === null) {
|
||||
printf("failed search(1.2.3.4)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("{region: %s, took: %.5f ms}\n", $region, XdbSearcher::now() - $sTime);
|
||||
|
||||
// 备注:并发使用,用整个 xdb 缓存创建的 searcher 对象可以安全用于并发。
|
||||
```
|
||||
|
||||
# 查询测试
|
||||
|
||||
通过 `search_test.php` 脚本来进行查询测试:
|
||||
|
||||
```bash
|
||||
➜ php git:(v2.0_xdb) ✗ php ./search_test.php
|
||||
php ./search_test.php [command options]
|
||||
options:
|
||||
--db string ip2region binary xdb file path
|
||||
--cache-policy string cache policy: file/vectorIndex/content
|
||||
```
|
||||
|
||||
例如:使用默认的 data/ip2region.xdb 进行查询测试:
|
||||
|
||||
```bash
|
||||
➜ php git:(v2.0_xdb) ✗ php ./search_test.php --db=../../data/ip2region.xdb --cache-policy=vectorIndex
|
||||
ip2region xdb searcher test program, cachePolicy: vectorIndex
|
||||
type 'quit' to exit
|
||||
ip2region>> 1.2.3.4
|
||||
{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 0.04492 ms}
|
||||
ip2region>>
|
||||
```
|
||||
|
||||
输入 ip 即可进行查询测试。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效率。
|
||||
|
||||
# bench 测试
|
||||
|
||||
通过 `bench_test.php` 脚本来进行自动 bench 测试,一方面确保 `xdb` 文件没有错误,另一方面通过大量的查询测试平均查询性能:
|
||||
|
||||
```bash
|
||||
➜ php git:(v2.0_xdb) ✗ php ./bench_test.php
|
||||
php ./bench_test.php [command options]
|
||||
options:
|
||||
--db string ip2region binary xdb file path
|
||||
--src string source ip text file path
|
||||
--cache-policy string cache policy: file/vectorIndex/content
|
||||
```
|
||||
|
||||
例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 来进行 bench 测试:
|
||||
|
||||
```bash
|
||||
➜ php git:(v2.0_xdb) ✗ php ./bench_test.php --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt --cache-policy=vectorIndex
|
||||
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 15s, cost: 0.005 ms/op}
|
||||
```
|
||||
|
||||
可以通过设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 三种不同的缓存实现的的性能。
|
||||
@Note:请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件的相同的源文件。
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||
// Use of this source code is governed by a Apache2.0-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// @Author Lion <chenxin619315@gmail.com>
|
||||
// @Date 2022/06/21
|
||||
|
||||
class XdbSearcher
|
||||
{
|
||||
const HeaderInfoLength = 256;
|
||||
const VectorIndexRows = 256;
|
||||
const VectorIndexCols = 256;
|
||||
const VectorIndexSize = 8;
|
||||
const SegmentIndexSize = 14;
|
||||
|
||||
// xdb file handle
|
||||
private $handle = null;
|
||||
|
||||
// header info
|
||||
private $header = null;
|
||||
private $ioCount = 0;
|
||||
|
||||
// vector index in binary string.
|
||||
// string decode will be faster than the map based Array.
|
||||
private $vectorIndex = null;
|
||||
|
||||
// xdb content buffer
|
||||
private $contentBuff = null;
|
||||
|
||||
// ---
|
||||
// static function to create searcher
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function newWithFileOnly($dbFile)
|
||||
{
|
||||
return new XdbSearcher($dbFile, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function newWithVectorIndex($dbFile, $vIndex)
|
||||
{
|
||||
return new XdbSearcher($dbFile, $vIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function newWithBuffer($cBuff)
|
||||
{
|
||||
return new XdbSearcher(null, null, $cBuff);
|
||||
}
|
||||
|
||||
// --- End of static creator
|
||||
|
||||
/**
|
||||
* initialize the xdb searcher
|
||||
* @throws Exception
|
||||
*/
|
||||
function __construct($dbFile = null, $vectorIndex = null, $cBuff = null)
|
||||
{
|
||||
// check the content buffer first
|
||||
if ($cBuff != null) {
|
||||
$this->vectorIndex = null;
|
||||
$this->contentBuff = $cBuff;
|
||||
} else {
|
||||
// 加载默认数据文件 by Anyon
|
||||
if (is_null($dbFile)) {
|
||||
$dbFile = __DIR__ . DIRECTORY_SEPARATOR . 'ip2region.xdb';
|
||||
}
|
||||
// open the xdb binary file
|
||||
$this->handle = fopen($dbFile, "r");
|
||||
if ($this->handle === false) {
|
||||
throw new Exception("failed to open xdb file '%s'", $dbFile);
|
||||
}
|
||||
|
||||
$this->vectorIndex = $vectorIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function close()
|
||||
{
|
||||
if ($this->handle != null) {
|
||||
fclose($this->handle);
|
||||
}
|
||||
}
|
||||
|
||||
function getIOCount()
|
||||
{
|
||||
return $this->ioCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* find the region info for the specified ip address
|
||||
* @throws Exception
|
||||
*/
|
||||
function search($ip)
|
||||
{
|
||||
// check and convert the sting ip to a 4-bytes long
|
||||
if (is_string($ip)) {
|
||||
$t = self::ip2long($ip);
|
||||
if ($t === null) {
|
||||
throw new Exception("invalid ip address `$ip`");
|
||||
}
|
||||
$ip = $t;
|
||||
}
|
||||
|
||||
// reset the global counter
|
||||
$this->ioCount = 0;
|
||||
|
||||
// locate the segment index block based on the vector index
|
||||
$il0 = ($ip >> 24) & 0xFF;
|
||||
$il1 = ($ip >> 16) & 0xFF;
|
||||
$idx = $il0 * self::VectorIndexCols * self::VectorIndexSize + $il1 * self::VectorIndexSize;
|
||||
if ($this->vectorIndex != null) {
|
||||
$sPtr = self::getLong($this->vectorIndex, $idx);
|
||||
$ePtr = self::getLong($this->vectorIndex, $idx + 4);
|
||||
} elseif ($this->contentBuff != null) {
|
||||
$sPtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx);
|
||||
$ePtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx + 4);
|
||||
} else {
|
||||
// read the vector index block
|
||||
$buff = $this->read(self::HeaderInfoLength + $idx, 8);
|
||||
if ($buff === null) {
|
||||
throw new Exception("failed to read vector index at {$idx}");
|
||||
}
|
||||
|
||||
$sPtr = self::getLong($buff, 0);
|
||||
$ePtr = self::getLong($buff, 4);
|
||||
}
|
||||
|
||||
// printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr);
|
||||
|
||||
// binary search the segment index to get the region info
|
||||
$dataLen = 0;
|
||||
$dataPtr = null;
|
||||
$l = 0;
|
||||
$h = ($ePtr - $sPtr) / self::SegmentIndexSize;
|
||||
while ($l <= $h) {
|
||||
$m = ($l + $h) >> 1;
|
||||
$p = $sPtr + $m * self::SegmentIndexSize;
|
||||
|
||||
// read the segment index
|
||||
$buff = $this->read($p, self::SegmentIndexSize);
|
||||
if ($buff == null) {
|
||||
throw new Exception("failed to read segment index at {$p}");
|
||||
}
|
||||
|
||||
$sip = self::getLong($buff, 0);
|
||||
if ($ip < $sip) {
|
||||
$h = $m - 1;
|
||||
} else {
|
||||
$eip = self::getLong($buff, 4);
|
||||
if ($ip > $eip) {
|
||||
$l = $m + 1;
|
||||
} else {
|
||||
$dataLen = self::getShort($buff, 8);
|
||||
$dataPtr = self::getLong($buff, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match nothing interception.
|
||||
// @TODO: could this even be a case ?
|
||||
// printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr);
|
||||
if ($dataPtr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// load and return the region data
|
||||
$buff = $this->read($dataPtr, $dataLen);
|
||||
if ($buff == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $buff;
|
||||
}
|
||||
|
||||
// read specified bytes from the specified index
|
||||
private function read($offset, $len)
|
||||
{
|
||||
// check the in-memory buffer first
|
||||
if ($this->contentBuff != null) {
|
||||
return substr($this->contentBuff, $offset, $len);
|
||||
}
|
||||
|
||||
// read from the file
|
||||
$r = fseek($this->handle, $offset);
|
||||
if ($r == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->ioCount++;
|
||||
$buff = fread($this->handle, $len);
|
||||
if ($buff === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($buff) != $len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $buff;
|
||||
}
|
||||
|
||||
// --- static util functions ----
|
||||
|
||||
// convert a string ip to long
|
||||
public static function ip2long($ip)
|
||||
{
|
||||
$ip = ip2long($ip);
|
||||
if ($ip === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// convert signed int to unsigned int if on 32 bit operating system
|
||||
if ($ip < 0 && PHP_INT_SIZE == 4) {
|
||||
$ip = sprintf("%u", $ip);
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
// read a 4bytes long from a byte buffer
|
||||
public static function getLong($b, $idx)
|
||||
{
|
||||
$val = (ord($b[$idx])) | (ord($b[$idx + 1]) << 8)
|
||||
| (ord($b[$idx + 2]) << 16) | (ord($b[$idx + 3]) << 24);
|
||||
|
||||
// convert signed int to unsigned int if on 32 bit operating system
|
||||
if ($val < 0 && PHP_INT_SIZE == 4) {
|
||||
$val = sprintf("%u", $val);
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
// read a 2bytes short from a byte buffer
|
||||
public static function getShort($b, $idx)
|
||||
{
|
||||
return ((ord($b[$idx])) | (ord($b[$idx + 1]) << 8));
|
||||
}
|
||||
|
||||
// load header info from a specified file handle
|
||||
public static function loadHeader($handle)
|
||||
{
|
||||
if (fseek($handle, 0) == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$buff = fread($handle, self::HeaderInfoLength);
|
||||
if ($buff === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// read bytes length checking
|
||||
if (strlen($buff) != self::HeaderInfoLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// return the decoded header info
|
||||
return [
|
||||
'version' => self::getShort($buff, 0),
|
||||
'indexPolicy' => self::getShort($buff, 2),
|
||||
'createdAt' => self::getLong($buff, 4),
|
||||
'startIndexPtr' => self::getLong($buff, 8),
|
||||
'endIndexPtr' => self::getLong($buff, 12)
|
||||
];
|
||||
}
|
||||
|
||||
// load header info from the specified xdb file path
|
||||
public static function loadHeaderFromFile($dbFile)
|
||||
{
|
||||
$handle = fopen($dbFile, 'r');
|
||||
if ($handle === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$header = self::loadHeader($handle);
|
||||
fclose($handle);
|
||||
return $header;
|
||||
}
|
||||
|
||||
// load vector index from a file handle
|
||||
public static function loadVectorIndex($handle)
|
||||
{
|
||||
if (fseek($handle, self::HeaderInfoLength) == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rLen = self::VectorIndexRows * self::VectorIndexCols * self::SegmentIndexSize;
|
||||
$buff = fread($handle, $rLen);
|
||||
if ($buff === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($buff) != $rLen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $buff;
|
||||
}
|
||||
|
||||
// load vector index from a specified xdb file path
|
||||
public static function loadVectorIndexFromFile($dbFile)
|
||||
{
|
||||
$handle = fopen($dbFile, 'r');
|
||||
if ($handle === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vIndex = self::loadVectorIndex($handle);
|
||||
fclose($handle);
|
||||
return $vIndex;
|
||||
}
|
||||
|
||||
// load the xdb content from a file handle
|
||||
public static function loadContent($handle)
|
||||
{
|
||||
if (fseek($handle, 0, SEEK_END) == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$size = ftell($handle);
|
||||
if ($size === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// seek to the head for reading
|
||||
if (fseek($handle, 0) == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$buff = fread($handle, $size);
|
||||
if ($buff === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// read length checking
|
||||
if (strlen($buff) != $size) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $buff;
|
||||
}
|
||||
|
||||
// load the xdb content from a file path
|
||||
public static function loadContentFromFile($dbFile)
|
||||
{
|
||||
$str = file_get_contents($dbFile, false);
|
||||
if ($str === false) {
|
||||
return null;
|
||||
} else {
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
||||
public static function now()
|
||||
{
|
||||
return (microtime(true) * 1000);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
// 建议使用 php _test.php 命令行运行测试文件
|
||||
|
||||
require 'Ip2Region.php';
|
||||
|
||||
$ip2region = new Ip2Region();
|
||||
|
||||
|
||||
// array (
|
||||
// 'city_id' => 1713,
|
||||
// 'region' => '中国|0|广东省|广州市|电信',
|
||||
// )
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
test();
|
||||
}
|
||||
|
||||
function getIp()
|
||||
{
|
||||
$ip_long = array(
|
||||
array('607649792', '608174079'), // 36.56.0.0-36.63.255.255
|
||||
array('1038614528', '1039007743'), // 61.232.0.0-61.237.255.255
|
||||
array('1783627776', '1784676351'), // 106.80.0.0-106.95.255.255
|
||||
array('2035023872', '2035154943'), // 121.76.0.0-121.77.255.255
|
||||
array('2078801920', '2079064063'), // 123.232.0.0-123.235.255.255
|
||||
array('-1950089216', '-1948778497'), // 139.196.0.0-139.215.255.255
|
||||
array('-1425539072', '-1425014785'), // 171.8.0.0-171.15.255.255
|
||||
array('-1236271104', '-1235419137'), // 182.80.0.0-182.92.255.255
|
||||
array('-770113536', '-768606209'), // 210.25.0.0-210.47.255.255
|
||||
array('-569376768', '-564133889'), // 222.16.0.0-222.95.255.255
|
||||
);
|
||||
$rkey = mt_rand(0, 9);
|
||||
return long2ip(mt_rand($ip_long[$rkey][0], $ip_long[$rkey][1]));
|
||||
}
|
||||
|
||||
function test()
|
||||
{
|
||||
$ip = getIp();
|
||||
global $ip2region;
|
||||
|
||||
echo PHP_EOL . "===============================";
|
||||
echo PHP_EOL . "测试 IP 地址: {$ip}";
|
||||
echo PHP_EOL . "--------【完整结果】------------" . PHP_EOL;
|
||||
$info = $ip2region->memorySearch($ip);
|
||||
var_export($info);
|
||||
|
||||
echo PHP_EOL . "---------【简易结果】----------" . PHP_EOL;
|
||||
var_export($ip2region->simple($ip));
|
||||
echo PHP_EOL . "===============================" . PHP_EOL . PHP_EOL;
|
||||
sleep(2);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "library",
|
||||
"name": "zoujingli/ip2region",
|
||||
"homepage": "https://github.com/zoujingli/Ip2Region",
|
||||
"description": "Ip2Region for PHP",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Anyon",
|
||||
"email": "zoujingli@qq.com",
|
||||
"homepage": "https://thinkadmin.top"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.4"
|
||||
},
|
||||
"keywords": [
|
||||
"Ip2Region"
|
||||
],
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"Ip2Region.php",
|
||||
"XdbSearcher.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+9
-17
@@ -15,32 +15,28 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="page-archives">
|
||||
<div id="page" class="w-100">
|
||||
<div id="posts" class="animated fadeInLeft ">
|
||||
<div class="p-block puock-text">
|
||||
<div class="timeline no-style">
|
||||
<?php
|
||||
<div id="page" class="w-100">
|
||||
<div id="posts" class="animated fadeInLeft ">
|
||||
<div class="p-block puock-text">
|
||||
<div class="timeline no-style">
|
||||
<?php
|
||||
$stat = Typecho_Widget::widget('Widget_Stat');
|
||||
Typecho_Widget::widget('Widget_Contents_Post_Recent', 'pageSize=' . $stat->publishedPostsNum)->to($archives);
|
||||
$year = 0;
|
||||
$mon = 0;
|
||||
$output = '';
|
||||
|
||||
while ($archives->next()) {
|
||||
$year_tmp = date('Y', $archives->created);
|
||||
$mon_tmp = date('m', $archives->created);
|
||||
$day_tmp = date('d', $archives->created);
|
||||
|
||||
// 检查是否需要新的时间线项目
|
||||
if ($year != $year_tmp || $mon != $mon_tmp) {
|
||||
// 如果不是第一个项目,先关闭之前的ul
|
||||
if ($year > 0 && $mon > 0) {
|
||||
$output .= '</ul></div></div>';
|
||||
}
|
||||
|
||||
}
|
||||
$year = $year_tmp;
|
||||
$mon = $mon_tmp;
|
||||
|
||||
// 开始新的时间线项目
|
||||
$output .= '<div class="timeline-item">';
|
||||
$output .= '<div class="timeline-location"></div>';
|
||||
@@ -48,19 +44,15 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
$output .= '<h4>' . $year . '-' . $mon . '</h4>';
|
||||
$output .= '<ul class="pd-links pl-0">';
|
||||
}
|
||||
|
||||
// 输出文章项
|
||||
$output .= '<li><a title="' . $archives->title . '" href="' . $archives->permalink . '">';
|
||||
$output .= $archives->title . ' ( ' . $day_tmp . '日 )</a></li>';
|
||||
}
|
||||
|
||||
// 循环结束后关闭最后的标签
|
||||
if ($year > 0 && $mon > 0) {
|
||||
$output .= '</ul></div></div>';
|
||||
}
|
||||
|
||||
echo $output;
|
||||
?>
|
||||
</div> </div> </div> </div> </div>
|
||||
|
||||
<?php $this->need('footer.php'); ?>
|
||||
?>
|
||||
</div></div></div></div></div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
+4
-1
@@ -14,7 +14,7 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="page-links">
|
||||
<div id="page">
|
||||
<div class="row row-cols-1">
|
||||
<div id="posts" class="col-12 animated fadeInLeft ">
|
||||
<div class="puock-text no-style">
|
||||
@@ -22,5 +22,8 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($this->allow('comment')): ?>
|
||||
<?php $this->need('comments.php'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
+21
-37
@@ -7,44 +7,28 @@
|
||||
if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
<?php $this->need('header.php'); ?>
|
||||
<div id="breadcrumb" class="animated fadeInUp">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a class="a-link" href="<?php $this->options->siteUrl(); ?>">首页</a></li>
|
||||
<li class="breadcrumb-item active " aria-current="page"><?php $this->title() ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a class="a-link" href="<?php $this->options->siteUrl(); ?>">首页</a></li>
|
||||
<li class="breadcrumb-item active " aria-current="page"><?php $this->title() ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="page-links">
|
||||
<div id="page" class="row row-cols-1">
|
||||
<div id="posts" class="col-12 animated fadeInLeft ">
|
||||
<div class="puock-text no-style">
|
||||
<div class="p-block links-main" id="page-links-217">
|
||||
<h6><?php $this->title() ?></h6>
|
||||
<div class="links-main-box row t-sm">
|
||||
<?php
|
||||
Links_Plugin::output('
|
||||
<a class="link-item a-link col-lg-3 col-md-4 col-sm-6 col-6"
|
||||
href="{url}"
|
||||
target="_blank" rel="me" title="{name}" data-bs-toggle="tooltip">
|
||||
<div class="clearfix puock-bg">
|
||||
<img alt="{name}"
|
||||
src="{image}"
|
||||
class="lazy md-avatar"
|
||||
data-src="{image}"
|
||||
alt="{name}">
|
||||
<div class="info">
|
||||
<p class="ml-1 text-nowrap text-truncate">{name}</p>
|
||||
<p class="c-sub ml-1 text-nowrap text-truncate">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
');
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="page-links">
|
||||
<div id="page" class="row row-cols-1">
|
||||
<div id="posts" class="col-12 animated fadeInLeft ">
|
||||
<div class="puock-text no-style">
|
||||
<div class="p-block links-main" id="page-links-217">
|
||||
<h6><?php $this->title() ?></h6>
|
||||
<div class="links-main-box row t-sm">
|
||||
<?php Links_Plugin::output('<a class="link-item a-link col-lg-3 col-md-4 col-sm-6 col-6" href="{url}" target="_blank" rel="me" title="{name}" data-bs-toggle="tooltip"><div class="clearfix puock-bg"><img alt="{name}" src="{image}" class="lazy md-avatar" data-src="{image}" alt="{name}"><div class="info"><p class="ml-1 text-nowrap text-truncate">{name}</p><p class="c-sub ml-1 text-nowrap text-truncate">{title}</p></div></div></a>');?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($this->allow('comment')): ?>
|
||||
<?php $this->need('comments.php'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
+8
-2
@@ -17,7 +17,11 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
<?php $commenters = getAllCommenters(); ?>
|
||||
<div id="page-reads">
|
||||
<div id="page" class="row row-cols-1">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div id="posts" class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div id="posts" class="col-lg-12 col-md-12">
|
||||
<?php endif; ?>
|
||||
<div class="p-block puock-text">
|
||||
<h2 class="t-lg"><?php $this->title() ?></h2>
|
||||
<div class="mt20 row pd-links">
|
||||
@@ -44,5 +48,7 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php endif; ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
+62
-54
@@ -13,74 +13,71 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
<li class="breadcrumb-item active " aria-current="page"><?php $this->title() ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div id="page-moments">
|
||||
<div id="page-moments">
|
||||
<div class="row">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div id="posts" class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div id="posts" class="col-lg-12 col-md-12">
|
||||
<?php endif; ?>
|
||||
<?php $tooot = $this->fields->tooot ? $this->fields->tooot : 'https://www.imsun.org/toot.json'; ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.12/marked.min.js" integrity="sha512-rCQgmUulW6f6QegOvTntKKb5IAoxTpGVCdWqYjkXEpzAns6XUFs8NKVqWe+KQpctp/EoRSFSuykVputqknLYMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.5/css/lightbox.min.css" integrity="sha512-xtV3HfYNbQXS/1R1jP53KbFcU9WXiSA1RFKzl5hRlJgdOJm4OxHCWYpskm6lN0xp0XtKGpAfVShpbvlFH3MDAA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.5/js/lightbox.min.js" integrity="sha512-KbRFbjA5bwNan6DvPl1ODUolvTTZ/vckssnFhka5cG80JVa5zSlRPCr055xSgU/q6oMIGhZWLhcbgIC0fyw3RQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<div id="tooot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
let offset = 0; // 初始偏移量
|
||||
const limit = 20; // 每次加载的数量
|
||||
|
||||
function fetchAndDisplayToots() {
|
||||
let offset = 0;
|
||||
const limit = 20;
|
||||
function formatHTML(toots) {
|
||||
let htmlString = '';
|
||||
let htmlString = '';
|
||||
toots.forEach(toot => {
|
||||
// 判断是否存在 reblog
|
||||
const isReblog = toot.reblog && toot.reblog.content;
|
||||
const content = isReblog ? toot.reblog.content : toot.content;
|
||||
const url = isReblog ? toot.reblog.url : toot.url;
|
||||
const account = isReblog ? toot.reblog.account : toot.account;
|
||||
const created_at = isReblog ? toot.reblog.created_at : toot.created_at;
|
||||
const media_attachments = isReblog ? toot.reblog.media_attachments : toot.media_attachments;
|
||||
|
||||
let mediaHTML = ''; // 初始化资源相关HTML为空字符串
|
||||
// 处理媒体附件
|
||||
if (media_attachments.length > 0) {
|
||||
let mediaHTML = '';
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
media_attachments.forEach(attachment => {
|
||||
if (attachment.type === 'image') {
|
||||
mediaHTML += `<a href="${attachment.url}" target="_blank" data-lightbox="image-set"><img src="${attachment.preview_url}" class="thumbnail-image img" ></a>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 使用 marked 转换 markdown 内容为 HTML
|
||||
const htmlContent = marked.parse(content);
|
||||
// 创建 HTML 字符串
|
||||
const htmlContent = marked.parse(content || '');
|
||||
htmlString += `
|
||||
<div class="mb20 puock-text moments-item">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-1">
|
||||
<div class="col-12 col-md-1">
|
||||
<a class="meta ta3" href="${account.url}" target="_blank" rel="nofollow">
|
||||
<div class="avatar mb10">
|
||||
<img src='${account.avatar}'
|
||||
class='lazy md-avatar mt-1'
|
||||
data-src='${account.avatar}'
|
||||
alt="${account.display_name}" title="${account.display_name}">
|
||||
</div>
|
||||
<div class="t-line-1 info fs12">${account.display_name}</div>
|
||||
</a>
|
||||
<div class="avatar mb10">
|
||||
<img src='${account.avatar}'
|
||||
class='lazy md-avatar mt-1'
|
||||
data-src='${account.avatar}'
|
||||
alt="${account.display_name}" title="${account.display_name}">
|
||||
</div>
|
||||
<div class="col-12 col-md-11">
|
||||
<div class="p-block moment-content-box"> <span class="al"></span>
|
||||
<div class="mt10 moment-content entry-content show-link-icon">
|
||||
<p>${htmlContent}
|
||||
</p>
|
||||
<div class="resimg">${mediaHTML}</div>
|
||||
</div>
|
||||
<div class="mt10 moment-footer p-flex-s-right"> <span class="t-sm c-sub">
|
||||
<a class="c-sub-a" href="${url}">
|
||||
<span class="mr-2"><i class="fa-regular fa-clock mr-1"></i>${new Date(created_at).toLocaleString()}</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-line-1 info fs12">${account.display_name}</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-11">
|
||||
<div class="p-block moment-content-box"> <span class="al"></span>
|
||||
<div class="mt10 moment-content entry-content show-link-icon">
|
||||
<p>${htmlContent}</p>
|
||||
<div class="resimg">${mediaHTML}</div>
|
||||
</div>
|
||||
<div class="mt10 moment-footer p-flex-s-right"> <span class="t-sm c-sub">
|
||||
<a class="c-sub-a" href="${url}">
|
||||
<span class="mr-2"><i class="fa-regular fa-clock mr-1"></i>${new Date(created_at).toLocaleString()}</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return htmlString;
|
||||
@@ -93,17 +90,26 @@ window.onload = function() {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
function fetchAndDisplayToots() {
|
||||
fetchToots().then(data => {
|
||||
const memosContainer = document.getElementById('tooot');
|
||||
const tootsToShow = data.slice(offset, offset + limit); // 选择要显示的toots
|
||||
memosContainer.innerHTML += formatHTML(tootsToShow);
|
||||
});
|
||||
}
|
||||
// 在页面加载完成后获取并展示 toots
|
||||
fetchAndDisplayToots();
|
||||
};
|
||||
</script>
|
||||
const memosContainer = document.getElementById('tooot');
|
||||
if (memosContainer) memosContainer.innerHTML = '';
|
||||
fetchToots().then(data => {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('toot.json is not an array:', data);
|
||||
return;
|
||||
}
|
||||
const tootsToShow = data.slice(offset, offset + limit);
|
||||
if (memosContainer) memosContainer.innerHTML += formatHTML(tootsToShow);
|
||||
});
|
||||
}
|
||||
|
||||
// 保证首次和 pjax 都能调用
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", fetchAndDisplayToots);
|
||||
} else {
|
||||
fetchAndDisplayToots();
|
||||
}
|
||||
document.addEventListener('pjax:end', fetchAndDisplayToots);
|
||||
</script>
|
||||
<style>
|
||||
div pre code {
|
||||
white-space: pre-wrap;
|
||||
@@ -159,8 +165,10 @@ img {
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
@@ -10,7 +10,11 @@
|
||||
</div>
|
||||
<div id="page-empty">
|
||||
<div id="page" class="row row-cols-1">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div id="post-main" class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div id="post-main" class="col-lg-12 col-md-12">
|
||||
<?php endif; ?>
|
||||
<div class="p-block"><div>
|
||||
<h1 id="post-title" class="mb-0 puock-text t-xxl"><?php $this->title() ?></h1>
|
||||
</div>
|
||||
@@ -60,5 +64,7 @@ if($days > 180){
|
||||
<?php $this->need('comments.php'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
<?php endif; ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
@@ -15,7 +15,11 @@
|
||||
<div class="puock-text p-block t-md ad-page-top"><?php $this->options->articletop(); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="row row-cols-1 post-row">
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<div id="post-main" class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<?php else: ?>
|
||||
<div id="post-main" class="col-lg-12 col-md-12">
|
||||
<?php endif; ?>
|
||||
<div class="p-block"><div>
|
||||
<h1 id="post-title" class="mb-0 puock-text t-xxl"><?php $this->title() ?></h1>
|
||||
</div>
|
||||
@@ -90,6 +94,7 @@ if($days > 180){
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分享海报点赞打赏 -->
|
||||
<div class="mt15 post-action-panel">
|
||||
<div class="post-action-content">
|
||||
<div class="d-flex justify-content-center w-100 c-sub">
|
||||
@@ -120,71 +125,102 @@ if($days > 180){
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!--内页中-->
|
||||
</div>
|
||||
<!--内页中-->
|
||||
</div>
|
||||
<?php if ($this->options->articlemid): ?>
|
||||
<div class="puock-text p-block t-md ad-page-content-bottom"><?php $this->options->articlemid(); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php $this->related(4)->to($relatedPosts); if ($relatedPosts->have()):?>
|
||||
<div class="p-block pb-0">
|
||||
<div class="row puock-text post-relevant">
|
||||
<?php while ($relatedPosts->next()): ?>
|
||||
<a href="<?php $relatedPosts->permalink(); ?>"
|
||||
class="col-6 col-md-3 post-relevant-item ww">
|
||||
<div
|
||||
style="background:url('<?php echo getPostCover($relatedPosts->content, $relatedPosts->cid); ?>')">
|
||||
<div class="title"> <?php $relatedPosts->title(); ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
<?php if ($this->options->articlemid): ?>
|
||||
<!--文章中广告-->
|
||||
<div class="puock-text p-block t-md ad-page-content-bottom"><?php $this->options->articlemid(); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php $this->related(4)->to($relatedPosts); if ($relatedPosts->have()):?>
|
||||
<!--相关文章-->
|
||||
<div class="p-block pb-0">
|
||||
<div class="row puock-text post-relevant">
|
||||
<?php while ($relatedPosts->next()): ?>
|
||||
<a href="<?php $relatedPosts->permalink(); ?>" class="col-6 col-md-3 post-relevant-item ww">
|
||||
<div style="background:url('<?php echo getPostCover($relatedPosts->content, $relatedPosts->cid); ?>')">
|
||||
<div class="title"> <?php $relatedPosts->title(); ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!--上一篇与下一篇-->
|
||||
<div class="p-block p-lf-15">
|
||||
<div class="row text-center pd-links single-next-or-pre t-md">
|
||||
<div class="col-6 p-border-r-1 p-0">
|
||||
<?php
|
||||
// 查询上一篇(比当前文章更早的文章)
|
||||
$db = Typecho_Db::get();
|
||||
$prev = $db->fetchRow($db->select('cid', 'title', 'slug', 'created')
|
||||
->from('table.contents')
|
||||
->where('created < ?', $this->created) // 比当前文章更早
|
||||
->where('type = ?', 'post') // 只查询文章,排除页面
|
||||
->where('status = ?', 'publish') // 只查询已发布的
|
||||
->order('created', Typecho_Db::SORT_DESC) // 按时间降序(最近的上一篇)
|
||||
->limit(1));
|
||||
|
||||
if ($prev):
|
||||
// 生成正确链接(兼容伪静态和自定义固定链接)
|
||||
$prevUrl = Typecho_Router::url('post', $prev, $this->options->index);
|
||||
?>
|
||||
<a href="<?php echo $prevUrl; ?>" rel="prev" title="<?php echo $prev['title']; ?>">
|
||||
<div class="abhl puock-text">
|
||||
<p class="t-line-1"><?php echo $prev['title']; ?></p>
|
||||
<span>上一篇</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="p-block p-lf-15">
|
||||
<div class="row text-center pd-links single-next-or-pre t-md ">
|
||||
<?php $prevPost = get_previous_post($this); ?>
|
||||
<div class="col-6 p-border-r-1 p-0">
|
||||
<?php if ($prevPost) { ?>
|
||||
<a href="<?php echo $prevPost->permalink; ?>"
|
||||
rel="prev">
|
||||
<div class='abhl puock-text'>
|
||||
<p class='t-line-1'><?php echo $prevPost->title; ?></p>
|
||||
<span>上一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a href="javascript:void(0);" rel="prev">
|
||||
<div class='abhl puock-text'>
|
||||
<p class='t-line-1'>没有上一篇</p>
|
||||
<span>上一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php $nextPost = get_next_post($this); ?>
|
||||
<div class="col-6 p-0">
|
||||
<?php if ($nextPost) { ?>
|
||||
<a href="<?php echo $nextPost->permalink; ?>" rel="next">
|
||||
<div class="abhl">
|
||||
<p class="t-line-1"><?php echo $nextPost->title; ?></p>
|
||||
<span>下一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a href="javascript:void(0);" rel="next">
|
||||
<div class='abhl puock-text'>
|
||||
<p class='t-line-1'>已是最新的文章</p>
|
||||
<span>下一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!--评论上方-->
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="javascript:void(0);" rel="prev">
|
||||
<div class="abhl puock-text">
|
||||
<p class="t-line-1">没有上一篇</p>
|
||||
<span>上一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-6 p-0">
|
||||
<?php
|
||||
// 查询下一篇(比当前文章更新的文章)
|
||||
$next = $db->fetchRow($db->select('cid', 'title', 'slug', 'created')
|
||||
->from('table.contents')
|
||||
->where('created > ?', $this->created) // 比当前文章更新
|
||||
->where('type = ?', 'post') // 只查询文章,排除页面
|
||||
->where('status = ?', 'publish') // 只查询已发布的
|
||||
->order('created', Typecho_Db::SORT_ASC) // 按时间升序(最早的下一条)
|
||||
->limit(1));
|
||||
|
||||
if ($next):
|
||||
// 生成正确链接(兼容伪静态和自定义固定链接)
|
||||
$nextUrl = Typecho_Router::url('post', $next, $this->options->index);
|
||||
?>
|
||||
<a href="<?php echo $nextUrl; ?>" rel="next" title="<?php echo $next['title']; ?>">
|
||||
<div class="abhl puock-text">
|
||||
<p class="t-line-1"><?php echo $next['title']; ?></p>
|
||||
<span>下一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="javascript:void(0);" rel="next">
|
||||
<div class="abhl puock-text">
|
||||
<p class="t-line-1">已是最新文章</p>
|
||||
<span>下一篇</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--评论上方-->
|
||||
<?php $this->need('comments.php'); ?>
|
||||
<?php if ($this->options->articlefoot): ?>
|
||||
<!--文章底部广告-->
|
||||
<div class="puock-text p-block t-md ad-comment-top"><?php $this->options->articlefoot(); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($this->options->showsidebar): ?>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php endif; ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
+22
-48
@@ -18,57 +18,34 @@
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 个人信息 -->
|
||||
<?php
|
||||
// 获取数据库连接
|
||||
$db = Typecho_Db::get();
|
||||
$prefix = $db->getPrefix();
|
||||
// 1. 获取uid=1的用户信息
|
||||
$user = $db->fetchRow($db->select()->from('table.users')->where('uid = ?', 1));
|
||||
$email = $user['mail'];
|
||||
$nickname = $user['screenName'];
|
||||
// 获取用户设置的 Gravatar 镜像
|
||||
$cnavatar = Helper::options()->cnavatar ? Helper::options()->cnavatar : 'https://cravatar.cn/avatar/';
|
||||
$hash = md5($email);
|
||||
$avatar = rtrim($cnavatar, '/') . '/' . $hash . '?s=80&d=identicon';
|
||||
// 2. 获取用户总数
|
||||
$userCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.users'))->num;
|
||||
// 3. 获取文章总数(只统计 type='post' 且 status='publish')
|
||||
$postCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.contents')->where('type = ?', 'post')->where('status = ?', 'publish'))->num;
|
||||
// 4. 获取评论总数
|
||||
$commentCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.comments'))->num;
|
||||
// 5. 获取文章浏览总量(累加所有文章的 views 字段)
|
||||
$totalViews = $db->fetchObject(
|
||||
$db->select(array('SUM(views)' => 'viewsum'))->from('table.contents')->where('type = ?', 'post')
|
||||
)->viewsum;
|
||||
if ($totalViews === null) $totalViews = 0;
|
||||
?>
|
||||
<!-- 个人信息 -->
|
||||
<?php $stats = get_site_statistics(); ?>
|
||||
<?php if (!empty($this->options->sidebarBlock) && in_array('ShowAdmin', $this->options->sidebarBlock)): ?>
|
||||
<div class="widget-puock-author widget">
|
||||
<div class="header" style="background-image: url('<?php echo $this->options->bgUrl() ?: $this->options->themeUrl('assets/img/cover.png'); ?>')">
|
||||
<img src='<?php $this->options->themeUrl('assets/img/load.svg'); ?>' class='lazy avatar' data-src='<?php echo $avatar; ?>' >
|
||||
<div class="header" style="background-image: url('<?php echo !empty($this->options->bgUrl) ? $this->options->bgUrl : $this->options->themeUrl('assets/img/cover.png'); ?>')">
|
||||
<img src='<?php $this->options->themeUrl('assets/img/load.svg'); ?>' class='lazy avatar' data-src='<?php echo $stats['avatar']; ?>' >
|
||||
</div>
|
||||
<div class="content t-md puock-text">
|
||||
<div class="text-center p-2">
|
||||
<div class="t-lg"><?php echo $nickname; ?></div>
|
||||
<div class="t-lg"><?php echo $stats['nickname']; ?></div>
|
||||
<div class="mt10 t-sm"><?php $this->options->description(); ?></div>
|
||||
</div>
|
||||
<div class="row mt10">
|
||||
<div class="col-3 text-center">
|
||||
<div class="c-sub t-sm">用户数</div>
|
||||
<div><?php echo $userCount; ?></div>
|
||||
<div><?php echo $stats['userCount']; ?></div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="c-sub t-sm">文章数</div>
|
||||
<div><?php echo $postCount; ?></div>
|
||||
<div><?php echo $stats['postCount']; ?></div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="c-sub t-sm">评论数</div>
|
||||
<div><?php echo $commentCount; ?></div>
|
||||
<div><?php echo $stats['commentCount']; ?></div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="c-sub t-sm">阅读量</div>
|
||||
<div><?php echo $totalViews; ?></div>
|
||||
<div><?php echo $stats['totalViews']; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,27 +109,24 @@ if ($totalViews === null) $totalViews = 0;
|
||||
<div class="pk-widget p-block">
|
||||
<div>
|
||||
<span class="t-lg border-bottom border-primary puock-text pb-2">
|
||||
<i class="fa fa-chart-simple mr-1"></i>热门文章
|
||||
<i class="fa-solid fa-fire"></i> 热门文章
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt20">
|
||||
<?php foreach ($hotPosts as $post): ?>
|
||||
<?php
|
||||
// Typecho 1.3.0 兼容处理
|
||||
$widget = $this->widget('Widget_Archive@post_' . $post['cid'], 'type=post');
|
||||
// 更可靠的获取文章链接方式
|
||||
$widget = Typecho_Widget::widget('Widget_Contents_Post_Recent');
|
||||
$permalink = '';
|
||||
try {
|
||||
$widget->setArchiveProperty('cid', $post['cid']);
|
||||
$widget->setArchiveProperty('title', $post['title']);
|
||||
$widget->setArchiveProperty('slug', $post['slug']);
|
||||
$widget->setArchiveProperty('created', $post['created']);
|
||||
$widget->setArchiveProperty('authorId', $post['authorId']);
|
||||
$widget->setArchiveProperty('type', $post['type']);
|
||||
$widget->setArchiveProperty('status', $post['status']);
|
||||
$widget->setArchiveProperty('commentsNum', $post['commentsNum']);
|
||||
|
||||
// 生成正确链接
|
||||
$permalink = $widget->archiveUrl;
|
||||
|
||||
// 方法1:使用Typecho的Router类
|
||||
$permalink = Typecho_Router::url('post', $post, $this->options->index);
|
||||
// 方法2:或者使用辅助函数(如果方法1不行)
|
||||
if (empty($permalink)) {
|
||||
$widget->push($post);
|
||||
$permalink = $widget->permalink;
|
||||
$widget->pop();
|
||||
}
|
||||
if (empty($post['title']) || empty($permalink)) {
|
||||
continue;
|
||||
}
|
||||
@@ -177,7 +151,7 @@ if ($totalViews === null) $totalViews = 0;
|
||||
<?php endif; ?>
|
||||
<!-- 最近评论 -->
|
||||
<?php if (!empty($this->options->sidebarBlock) && in_array('ShowRecentComments', $this->options->sidebarBlock)): ?>
|
||||
<?php
|
||||
<?php
|
||||
// 设置参数来排除管理员评论
|
||||
$comments = \Widget\Comments\Recent::alloc(array(
|
||||
'ignoreAuthor' => true // 这里添加参数来排除作者/管理员评论
|
||||
|
||||
Reference in New Issue
Block a user