mirror of
https://github.com/jkjoy/Typecho-Theme-Puock.git
synced 2026-08-12 23:09:02 +00:00
Compare commits
15 Commits
e604c6b4d3
..
1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,40 @@
|
||||
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
|
||||
|
||||
- 新增支持首页登录
|
||||
+1
-1
@@ -67,7 +67,7 @@ $coverImage = getPostCover($this->content, $this->cid);
|
||||
'wrapClass' => 'pagination comment-ajax-load',
|
||||
'itemTag' => 'li',
|
||||
'textTag' => 'span',
|
||||
'currentClass' => 'active',
|
||||
'currentClass' => 'cur',
|
||||
'prevClass' => 'prev',
|
||||
'nextClass' => 'next'
|
||||
)); ?>
|
||||
|
||||
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 } ?>
|
||||
+1
-2
@@ -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>
|
||||
+561
-220
@@ -166,161 +166,50 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['likeup']) && isset($_
|
||||
* 随机封面
|
||||
*/
|
||||
function getPostCover($content, $cid, $fields = null) {
|
||||
// 首先检查是否有自定义封面字段
|
||||
// 优先使用自定义封面字段
|
||||
if ($fields && !empty($fields->cover)) {
|
||||
return $fields->cover;
|
||||
}
|
||||
|
||||
// 尝试从内容中提取第一张图片
|
||||
preg_match_all('/<img.+src=[\'"]([^\'"]+)[\'"].*>/i', $content, $matches);
|
||||
|
||||
if (!empty($matches[1][0])) {
|
||||
// 如果找到图片,返回第一张图片URL
|
||||
return $matches[1][0];
|
||||
} else {
|
||||
// 如果没有图片,使用随机封面(基于文章ID的伪随机)
|
||||
$coverNumber = ($cid % 8) + 1; // 得到1-8的值
|
||||
return Helper::options()->themeUrl . '/assets/img/random/' . $coverNumber . '.jpg';
|
||||
// 从原始内容中提取第一张图片(不管src是什么)
|
||||
if (preg_match('/<img[^>]+src=["\']([^"\']+\.(?:jpg|jpeg|png|webp))["\'][^>]*>/i', $content, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
// 没有图片则用随机封面
|
||||
$coverNumber = ($cid % 8) + 1;
|
||||
return Helper::options()->themeUrl . '/assets/img/random/' . $coverNumber . '.jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一篇文章
|
||||
*
|
||||
* @param Widget_Archive $archive 当前文章归档对象
|
||||
* @return object|null 上一篇文章对象,如果没有则返回null
|
||||
*/
|
||||
function get_previous_post($archive) {
|
||||
if (!$archive->is('single')) {
|
||||
return null;
|
||||
}
|
||||
$db = Typecho_Db::get();
|
||||
$prefix = $db->getPrefix();
|
||||
// 获取上一篇文章(按创建时间排序)
|
||||
$post = $db->fetchRow($db->select()
|
||||
->from('table.contents')
|
||||
->where('table.contents.status = ?', 'publish')
|
||||
->where('table.contents.created < ?', $archive->created)
|
||||
->where('table.contents.type = ?', 'post')
|
||||
->order('table.contents.created', Typecho_Db::SORT_DESC)
|
||||
->limit(1));
|
||||
|
||||
if (!$post) {
|
||||
return null;
|
||||
}
|
||||
// 构建标准化的文章对象
|
||||
$result = new stdClass();
|
||||
$result->cid = $post['cid'];
|
||||
$result->title = $post['title'];
|
||||
$result->slug = $post['slug'];
|
||||
$result->created = $post['created'];
|
||||
$result->content = isset($post['text']) ? $post['text'] : '';
|
||||
$result->text = isset($post['text']) ? $post['text'] : '';
|
||||
$result->permalink = get_permalink($post['cid']);
|
||||
// 获取文章自定义字段
|
||||
$fields = $db->fetchAll($db->select()->from('table.fields')
|
||||
->where('cid = ?', $post['cid']));
|
||||
// 添加自定义字段到文章对象
|
||||
if ($fields) {
|
||||
$result->fields = new stdClass();
|
||||
foreach ($fields as $field) {
|
||||
$result->fields->{$field['name']} = $field['str_value'] ? $field['str_value'] : $field['int_value'];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一篇文章
|
||||
*
|
||||
* @param Widget_Archive $archive 当前文章归档对象
|
||||
* @return object|null 下一篇文章对象,如果没有则返回null
|
||||
*/
|
||||
function get_next_post($archive) {
|
||||
if (!$archive->is('single')) {
|
||||
return null;
|
||||
}
|
||||
$db = Typecho_Db::get();
|
||||
$prefix = $db->getPrefix();
|
||||
// 获取下一篇文章(按创建时间排序)
|
||||
$post = $db->fetchRow($db->select()
|
||||
->from('table.contents')
|
||||
->where('table.contents.status = ?', 'publish')
|
||||
->where('table.contents.created > ?', $archive->created)
|
||||
->where('table.contents.type = ?', 'post')
|
||||
->order('table.contents.created', Typecho_Db::SORT_ASC)
|
||||
->limit(1));
|
||||
|
||||
if (!$post) {
|
||||
return null;
|
||||
}
|
||||
// 构建标准化的文章对象
|
||||
$result = new stdClass();
|
||||
$result->cid = $post['cid'];
|
||||
$result->title = $post['title'];
|
||||
$result->slug = $post['slug'];
|
||||
$result->created = $post['created'];
|
||||
$result->content = isset($post['text']) ? $post['text'] : '';
|
||||
$result->text = isset($post['text']) ? $post['text'] : '';
|
||||
$result->permalink = get_permalink($post['cid']);
|
||||
// 获取文章自定义字段
|
||||
$fields = $db->fetchAll($db->select()->from('table.fields')
|
||||
->where('cid = ?', $post['cid']));
|
||||
// 添加自定义字段到文章对象
|
||||
if ($fields) {
|
||||
$result->fields = new stdClass();
|
||||
foreach ($fields as $field) {
|
||||
$result->fields->{$field['name']} = $field['str_value'] ? $field['str_value'] : $field['int_value'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章永久链接
|
||||
*
|
||||
* @param int $cid 文章ID
|
||||
* @return string 文章链接
|
||||
*/
|
||||
function get_permalink($cid) {
|
||||
try {
|
||||
// 获取文章对象
|
||||
$db = Typecho_Db::get();
|
||||
$post = $db->fetchRow($db->select()
|
||||
->from('table.contents')
|
||||
->where('cid = ?', $cid)
|
||||
->where('status = ?', 'publish'));
|
||||
if (!$post) {
|
||||
return '';
|
||||
}
|
||||
// 构造文章对象
|
||||
$post['type'] = 'post'; // 确保类型为文章
|
||||
$post = Typecho_Widget::widget('Widget_Abstract_Contents')->filter($post);
|
||||
// 使用文章对象的 permalink 方法生成链接
|
||||
return $post['permalink'] ?? '';
|
||||
} catch (Exception $e) {
|
||||
// 出现异常时使用最简单的方式
|
||||
$options = Helper::options();
|
||||
return $options->siteUrl . '?cid=' . $cid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 获取所有评论者信息的函数
|
||||
*/
|
||||
function getAllCommenters() {
|
||||
$db = Typecho_Db::get();
|
||||
$commenters = array();
|
||||
|
||||
// 查询所有评论者信息并按邮箱分组统计
|
||||
$query = $db->select('author, mail, COUNT(*) as comment_count, url')
|
||||
$isSqlite = strpos(strtolower(get_class($db->getAdapter())), 'sqlite') !== false;
|
||||
if ($isSqlite) {
|
||||
// SQLite 不支持 ANY_VALUE
|
||||
$query = $db->select('author', 'mail', 'COUNT(*) as comment_count', 'url')
|
||||
->from('table.comments')
|
||||
->where('status = ?', 'approved')
|
||||
->where('authorId != ?', 1)
|
||||
->group('mail')
|
||||
->order('comment_count', Typecho_Db::SORT_DESC);
|
||||
} else {
|
||||
// MySQL 用 ANY_VALUE 兼容 ONLY_FULL_GROUP_BY
|
||||
$query = $db->select(
|
||||
'ANY_VALUE(author) as author',
|
||||
'mail',
|
||||
'COUNT(*) as comment_count',
|
||||
'ANY_VALUE(url) as url'
|
||||
)
|
||||
->from('table.comments')
|
||||
->where('status = ?', 'approved') // 只统计已通过审核的评论
|
||||
->where('authorId != ?', 1) // 排除ID为1的管理员
|
||||
->where('authorId != ?', 1)
|
||||
->where('status = ?', 'approved')
|
||||
->group('mail')
|
||||
->order('comment_count', Typecho_Db::SORT_DESC);
|
||||
|
||||
}
|
||||
|
||||
$rows = $db->fetchAll($query);
|
||||
|
||||
// 获取 Gravatar 镜像设置
|
||||
@@ -343,85 +232,71 @@ function getAllCommenters() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP归属地 (使用 ip.asbid.cn API)
|
||||
* 获取IP归属地
|
||||
*/
|
||||
// 加载 XdbSearcher 类
|
||||
require_once __DIR__ . '/ip2region/XdbSearcher.php';
|
||||
|
||||
// 单例方式加载 ip2region.xdb 到内存
|
||||
function getIp2regionSearcher() {
|
||||
static $searcher = null;
|
||||
if ($searcher === null) {
|
||||
$dbPath = __DIR__ . '/ip2region/ip2region.xdb';
|
||||
$cBuff = XdbSearcher::loadContentFromFile($dbPath);
|
||||
if ($cBuff === null) {
|
||||
error_log("无法加载 ip2region.xdb");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$searcher = XdbSearcher::newWithBuffer($cBuff);
|
||||
} catch (Exception $e) {
|
||||
error_log("创建 ip2region searcher 失败: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $searcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 IP 归属地
|
||||
*
|
||||
* @param string $region 归属地字符串
|
||||
* @return string 格式化后的归属地
|
||||
*/
|
||||
function format_ip_region($region) {
|
||||
// 分割字符串
|
||||
$parts = explode('|', $region);
|
||||
|
||||
// 去除为 0 或 空字符串的部分
|
||||
$parts = array_filter($parts, function($item) {
|
||||
return $item !== '0' && $item !== '';
|
||||
});
|
||||
|
||||
// 如果第一个元素是"中国",则移除
|
||||
if (isset($parts[0]) && $parts[0] === '中国') {
|
||||
array_shift($parts);
|
||||
}
|
||||
|
||||
// 重新拼接
|
||||
return implode('', $parts);
|
||||
}
|
||||
|
||||
// 通过 IP 获取归属地
|
||||
function get_ip_region($ip) {
|
||||
// 检查是否是内网IP
|
||||
// 检查是否是 IPv6 地址
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return 'IPv6';
|
||||
}
|
||||
// 检查是否是内网IP
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
return '内网IP';
|
||||
}
|
||||
|
||||
// 缓存目录路径
|
||||
$cacheDir = __TYPECHO_ROOT_DIR__ . '/usr/cache/ip_cache/';
|
||||
if (!file_exists($cacheDir)) {
|
||||
mkdir($cacheDir, 0755, true);
|
||||
}
|
||||
|
||||
// 缓存文件名 (使用ip作为文件名)
|
||||
$cacheFile = $cacheDir . md5($ip) . '.json';
|
||||
|
||||
// 检查是否有缓存
|
||||
if (file_exists($cacheFile)) {
|
||||
$cacheData = json_decode(file_get_contents($cacheFile), true);
|
||||
if ($cacheData && isset($cacheData['expire']) && $cacheData['expire'] > time()) {
|
||||
return format_api_ip_region($cacheData['data']);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API获取IP信息
|
||||
$apiUrl = 'https://ip.asbid.cn/' . $ip;
|
||||
$response = @file_get_contents($apiUrl);
|
||||
|
||||
if ($response === false) {
|
||||
// API调用失败,返回未知
|
||||
return '未知';
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data || !isset($data['country'])) {
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 缓存数据 (设置1个月有效期)
|
||||
$cacheData = [
|
||||
'data' => $data,
|
||||
'expire' => time() + 30 * 24 * 3600
|
||||
];
|
||||
file_put_contents($cacheFile, json_encode($cacheData));
|
||||
|
||||
return format_api_ip_region($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化API返回的IP归属地信息
|
||||
*
|
||||
* @param array $data API返回的数据
|
||||
* @return string 格式化后的归属地
|
||||
*/
|
||||
function format_api_ip_region($data) {
|
||||
$regionParts = [];
|
||||
|
||||
if (isset($data['country']) && $data['country']) {
|
||||
$regionParts[] = $data['country'];
|
||||
}
|
||||
if (isset($data['province']) && $data['province']) {
|
||||
$regionParts[] = $data['province'];
|
||||
}
|
||||
if (isset($data['city']) && $data['city']) {
|
||||
// 处理"中国-江苏-南京"这样的格式
|
||||
$cityParts = explode('–', $data['city']);
|
||||
foreach ($cityParts as $part) {
|
||||
if (trim($part) && !in_array(trim($part), $regionParts)) {
|
||||
$regionParts[] = trim($part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['isp']) && $data['isp']) {
|
||||
$regionParts[] = $data['isp'];
|
||||
}
|
||||
|
||||
return implode(' ', $regionParts);
|
||||
$searcher = getIp2regionSearcher();
|
||||
if (!$searcher) return '未知';
|
||||
$region = $searcher->search($ip);
|
||||
if ($region === null) return '未知';
|
||||
return format_ip_region($region);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,9 +444,11 @@ function getBrowsersInfo($userAgent) {
|
||||
"WebOS" => pregMatch("/^.*hpwOS\/([\d.]+);.*$/", $userAgent)
|
||||
];
|
||||
|
||||
if ($systemVersion[$deviceInfo['system']]) {
|
||||
$deviceInfo['systemVersion'] = $systemVersion[$deviceInfo['system']];
|
||||
if ($deviceInfo['systemVersion'] == $userAgent) $deviceInfo['systemVersion'] = '';
|
||||
if (isset($deviceInfo['system']) && $deviceInfo['system'] !== "" && isset($systemVersion[$deviceInfo['system']])) {
|
||||
$deviceInfo['systemVersion'] = $systemVersion[$deviceInfo['system']];
|
||||
if ($deviceInfo['systemVersion'] == $userAgent) {
|
||||
$deviceInfo['systemVersion'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览器版本信息
|
||||
@@ -672,9 +549,11 @@ function getBrowsersInfo($userAgent) {
|
||||
];
|
||||
|
||||
|
||||
if ($browsersVersion[$deviceInfo['browser']]) {
|
||||
$deviceInfo["version"] = $browsersVersion[$deviceInfo['browser']];
|
||||
if ($deviceInfo["version"] == $userAgent) $deviceInfo['version'] = '';
|
||||
if (isset($deviceInfo['browser'], $browsersVersion[$deviceInfo['browser']])) {
|
||||
$deviceInfo['version'] = $browsersVersion[$deviceInfo['browser']];
|
||||
if ($deviceInfo['version'] == $userAgent) {
|
||||
$deviceInfo['version'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 修正浏览器版本信息
|
||||
@@ -1054,4 +933,466 @@ class AttachmentHelper {
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
/**
|
||||
* 解析表情短代码为图片
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
function parse_smiley_shortcode($content) {
|
||||
$smileys = [
|
||||
':?:' => 'doubt.png',
|
||||
':razz:' => 'razz.png',
|
||||
':sad:' => 'sad.png',
|
||||
':evil:' => 'evil.png',
|
||||
':naughty:' => 'naughty.png',
|
||||
':!:' => 'scare.png',
|
||||
':smile:' => 'smile.png',
|
||||
':oops:' => 'oops.png',
|
||||
':neutral:' => 'neutral.png',
|
||||
':cry:' => 'cry.png',
|
||||
':mrgreen:' => 'mrgreen.png',
|
||||
':grin:' => 'grin.png',
|
||||
':eek:' => 'eek.png',
|
||||
':shock:' => 'shock.png',
|
||||
':???:' => 'bz.png',
|
||||
':cool:' => 'cool.png',
|
||||
':lol:' => 'lol.png',
|
||||
':mad:' => 'mad.png',
|
||||
':twisted:' => 'twisted.png',
|
||||
':roll:' => 'roll.png',
|
||||
':wink:' => 'wink.png',
|
||||
':idea:' => 'idea.png',
|
||||
':despise:' => 'despise.png',
|
||||
':celebrate:' => 'celebrate.png',
|
||||
':watermelon:' => 'watermelon.png',
|
||||
':xmas:' => 'xmas.png',
|
||||
':warn:' => 'warn.png',
|
||||
':rainbow:' => 'rainbow.png',
|
||||
':loveyou:' => 'loveyou.png',
|
||||
':love:' => 'love.png',
|
||||
':beer:' => 'beer.png',
|
||||
];
|
||||
$themeUrl = Helper::options()->themeUrl . '/assets/img/smiley/';
|
||||
foreach ($smileys as $code => $img) {
|
||||
$imgTag = '<img class="smiley-img" src="' . $themeUrl . $img . '" alt="' . $code . '" title="表情" style="width:16px;height:16px;vertical-align:middle;" />';
|
||||
$content = str_replace($code, $imgTag, $content);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短代码实现
|
||||
*/
|
||||
function get_article_info($atts) {
|
||||
$default_atts = array('id' => '');
|
||||
$atts = array_merge($default_atts, $atts);
|
||||
$db = Typecho_Db::get();
|
||||
if (!empty($atts['id'])) {
|
||||
$post = $db->fetchRow($db->select()->from('table.contents')->where('cid = ?', $atts['id'])->limit(1));
|
||||
} else {
|
||||
return '请提供文章ID';
|
||||
}
|
||||
if (!$post) {
|
||||
return '未找到文章';
|
||||
}
|
||||
// 获取文章对象,避免permalink警告
|
||||
$post = Typecho_Widget::widget('Widget_Abstract_Contents')->push($post);
|
||||
$permalink = $post['permalink'];
|
||||
$title = htmlspecialchars($post['title']);
|
||||
$summary = get_article_summary($post);
|
||||
|
||||
$output = '<blockquote class="article-quote">';
|
||||
$output .= '<div class="t-lg t-line-1">';
|
||||
$output .= '<a class="a-link" title="' . $title . '" href="' . $permalink . '" target="_blank">' . $title . '</a>';
|
||||
$output .= '</div>';
|
||||
$output .= '<div class="t-md c-sub text-2line">' . htmlspecialchars($summary) . '</div>';
|
||||
$output .= '</blockquote>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
// 创建一个新的类来处理内容过滤
|
||||
class ContentFilter
|
||||
{
|
||||
public static function filterContent($content, $widget, $lastResult)
|
||||
{
|
||||
// 先做github短代码和链接替换
|
||||
$content = preg_replace_callback('/\[github=([\w\-\.]+\/[\w\-\.]+)\]/i', function($matches) {
|
||||
$repo = htmlspecialchars($matches[1]);
|
||||
return '<div class="github-card text-center" data-repo="' . $repo . '"><div class="spinner-grow text-primary"></div></div>';
|
||||
}, $content);
|
||||
// 只匹配主仓库链接,后面只能是空格、标点、换行或结尾
|
||||
$content = preg_replace_callback(
|
||||
'#https://github\.com/([\w\-\.]+/[\w\-\.]+)(?=[\s\.,;:!\?\)\]\}\"\'\n]|$)#i',
|
||||
function($matches) {
|
||||
$repo = htmlspecialchars($matches[1]);
|
||||
return '<div class="github-card text-center" data-repo="' . $repo . '"><div class="spinner-grow text-primary"></div></div>';
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
// 再进行 Markdown 解析
|
||||
$content = empty($lastResult) ? $widget->markdown($content) : $lastResult;
|
||||
|
||||
// alert类短代码批量替换
|
||||
$alertShortcodes = [
|
||||
'success' => 'success',
|
||||
'primary' => 'primary',
|
||||
'danger' => 'danger',
|
||||
'warning' => 'warning',
|
||||
'info' => 'info',
|
||||
'dark' => 'dark',
|
||||
];
|
||||
foreach ($alertShortcodes as $shortcode => $class) {
|
||||
$content = preg_replace(
|
||||
'/\[' . $shortcode . '\](.*?)\[\/' . $shortcode . '\]/is',
|
||||
'<div class="alert alert-' . $class . '">$1</div>',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
// 其他短代码处理
|
||||
$content = preg_replace_callback('/\[article\s+([^\]]+)\]/', function($matches) {
|
||||
$atts = self::parse_atts($matches[1]);
|
||||
return get_article_info($atts);
|
||||
}, $content);
|
||||
|
||||
// 懒加载图片替换
|
||||
$themeUrl = Helper::options()->themeUrl;
|
||||
$loadSvg = $themeUrl . '/assets/img/load.svg';
|
||||
$title = htmlspecialchars($widget->title);
|
||||
$content = preg_replace_callback(
|
||||
'/<img\s+[^>]*src=["\"]([^"\"]+\.(?:jpg|jpeg|png|webp))["\"][^>]*>/i',
|
||||
function($matches) use ($title, $loadSvg) {
|
||||
$imgUrl = $matches[1];
|
||||
return '<img title="' . $title . '" alt="' . $title . '" decoding="async" data-src="' . $imgUrl . '" data-lazy="true" src="' . $loadSvg . '" />';
|
||||
},
|
||||
$content
|
||||
);
|
||||
// collapse折叠面板短代码
|
||||
static $collapseIndex = 0;
|
||||
$content = preg_replace_callback(
|
||||
'/\[collapse\s+title=(?:\'([^\']*)\'|\"([^\"]*)\")\](.*?)\[\/collapse\]/is',
|
||||
function($matches) use (&$collapseIndex) {
|
||||
$title = $matches[1] !== '' ? $matches[1] : $matches[2];
|
||||
$body = $matches[3];
|
||||
$collapseIndex++;
|
||||
$uniqid = 'collapse-' . mt_rand(100,999) . '-' . $collapseIndex;
|
||||
return '<div class="pk-sc-collapse"><a class="btn btn-primary btn-sm" data-bs-toggle="collapse" href="#' . $uniqid . '" role="button" aria-expanded="false" aria-controls="' . $uniqid . '"><i class="fa fa-angle-up"></i> ' . htmlspecialchars($title) . '</a></div><div class="collapse" id="' . $uniqid . '">' . $body . '</div>';
|
||||
},
|
||||
$content
|
||||
);
|
||||
// download下载短代码
|
||||
$content = preg_replace_callback(
|
||||
'/\[download\s+file=(?:\'([^\']*)\'|\"([^\"]*)\")\s+size=(?:\'([^\']*)\'|\"([^\"]*)\")\](.*?)\[\/download\]/is',
|
||||
function($matches) {
|
||||
$file = $matches[1] !== '' ? $matches[1] : $matches[2];
|
||||
$size = $matches[3] !== '' ? $matches[3] : $matches[4];
|
||||
$url = $matches[5];
|
||||
return '<div class="p-block p-down-box">'
|
||||
. "<div class='mb15'><i class='fa fa-file-zipper'></i> <span> 文件名称:" . htmlspecialchars($file) . "</span></div>"
|
||||
. "<div class='mb15'><i class='fa fa-download'></i> <span> 文件大小:" . htmlspecialchars($size) . "</span></div>"
|
||||
. "<div class='mb15'><i class='fa-regular fa-bell'></i> <span> 下载声明:本站部分资源来自于网络收集,若侵犯了你的隐私或版权,请及时联系我们删除有关信息。</span></div>"
|
||||
. "<div><i class='fa fa-link'></i><span> 下载地址:<a href='" . htmlspecialchars($url) . "' target='_blank'>点此下载</a> </span></div></p></div>";
|
||||
},
|
||||
$content
|
||||
);
|
||||
// 回复可见短代码
|
||||
$content = preg_replace_callback(
|
||||
'/\[reply\](.*?)\[\/reply\]/is',
|
||||
function($matches) use ($widget) {
|
||||
$show = false;
|
||||
// 仅在文章页生效
|
||||
if ($widget instanceof Widget_Archive && $widget->is('single')) {
|
||||
$user = Typecho_Widget::widget('Widget_User');
|
||||
$db = Typecho_Db::get();
|
||||
if ($user->hasLogin) {
|
||||
// 登录用户,判断是否有通过审核的评论
|
||||
$hasComment = $db->fetchRow($db->select()->from('table.comments')
|
||||
->where('cid = ?', $widget->cid)
|
||||
->where('mail = ?', $user->mail)
|
||||
->where('status = ?', 'approved')
|
||||
);
|
||||
if ($hasComment) $show = true;
|
||||
} else {
|
||||
// 未登录,判断IP
|
||||
$hasComment = $db->fetchRow($db->select()->from('table.comments')
|
||||
->where('cid = ?', $widget->cid)
|
||||
->where('status = ?', 'approved')
|
||||
->where('ip = ?', $widget->request->getIp())
|
||||
);
|
||||
if ($hasComment) $show = true;
|
||||
}
|
||||
}
|
||||
if ($show) {
|
||||
return '<div class="reply-visible">' . $matches[1] . '</div>';
|
||||
} else {
|
||||
return "<div class='alert alert-primary alert-outline'><span class='c-sub fs14'><i class='fa-regular fa-eye'></i> 此处含有隐藏内容,请提交评论并审核通过刷新后即可查看!</span></div>";
|
||||
}
|
||||
},
|
||||
$content
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
// 解析短代码属性
|
||||
private static function parse_atts($text) {
|
||||
$atts = array();
|
||||
$pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
|
||||
$text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
|
||||
if (preg_match_all($pattern, $text, $match, PREG_SET_ORDER)) {
|
||||
foreach ($match as $m) {
|
||||
if (!empty($m[1]))
|
||||
$atts[strtolower($m[1])] = stripcslashes($m[2]);
|
||||
elseif (!empty($m[3]))
|
||||
$atts[strtolower($m[3])] = stripcslashes($m[4]);
|
||||
elseif (!empty($m[5]))
|
||||
$atts[strtolower($m[5])] = stripcslashes($m[6]);
|
||||
elseif (isset($m[7]) && strlen($m[7]))
|
||||
$atts[] = stripcslashes($m[7]);
|
||||
elseif (isset($m[8]))
|
||||
$atts[] = stripcslashes($m[8]);
|
||||
}
|
||||
}
|
||||
return $atts;
|
||||
}
|
||||
}
|
||||
// 注册钩子,自动处理所有内容输出
|
||||
Typecho_Plugin::factory('Widget_Abstract_Contents')->content = array('ContentFilter', 'filterContent');
|
||||
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('ContentFilter', 'filterContent');
|
||||
|
||||
// 编辑器按钮类
|
||||
class EditorButton {
|
||||
public static function render()
|
||||
{
|
||||
echo <<<EOF
|
||||
<style>
|
||||
#wmd-button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 40px;
|
||||
height: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
#text, .wmd-input {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-article-button" title="插入文章引用"><span style="background: none;"><svg t="1687164718203" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4158" width="20" height="20"><path d="M810.666667 213.333333H213.333333c-46.933333 0-85.333333 38.4-85.333333 85.333334v426.666666c0 46.933333 38.4 85.333333 85.333333 85.333334h597.333334c46.933333 0 85.333333-38.4 85.333333-85.333334V298.666667c0-46.933333-38.4-85.333333-85.333333-85.333334z m0 512H213.333333V298.666667h597.333334v426.666666z" p-id="4159"></path><path d="M298.666667 384h426.666666v85.333333H298.666667zM298.666667 554.666667h426.666666v85.333333H298.666667z" p-id="4160"></path></svg></span></li>');
|
||||
// 新增github按钮
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-github-button" title="插入GitHub仓库"><span style="background: none;"><svg t="1714380000000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8888" width="20" height="20"><path d="M512 76C276.6 76 80 272.6 80 508c0 190.2 123.2 351.4 294 408.6 21.4 4 29.2-9.2 29.2-20.4 0-10-0.4-43-0.6-78.2-119.6 26-144.8-57.6-144.8-57.6-19.4-49.2-47.4-62.2-47.4-62.2-38.8-26.6 2.8-26 2.8-26 42.8 3 65.4 44 65.4 44 38.2 65.4 100.2 46.5 124.6 35.6 3.8-27.7 15-46.5 27.2-57.2-95.4-10.8-195.8-47.7-195.8-212.4 0-46.9 16.8-85.3 44.2-115.4-4.4-10.8-19.2-54.2 4.2-113 0 0 36.2-11.6 118.8 44.1 34.4-9.6 71.4-14.4 108.2-14.6 36.8 0.2 73.8 5 108.2 14.6 82.6-55.7 118.8-44.1 118.8-44.1 23.4 58.8 8.6 102.2 4.2 113 27.4 30.1 44.2 68.5 44.2 115.4 0 164.9-100.6 201.5-196.2 212.1 15.4 13.2 29.2 39.2 29.2 79.1 0 57.1-0.5 103.2-0.5 117.3 0 11.3 7.7 24.6 29.4 20.4C820.8 859.4 944 698.2 944 508 944 272.6 747.4 76 512 76z" p-id="8889" fill="#181616"></path></svg></span></li>');
|
||||
|
||||
// alert类按钮
|
||||
var alertTypes = [
|
||||
{id: 'success', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(100,205,138,1)"><path d="M9.9997 15.1709L19.1921 5.97852L20.6063 7.39273L9.9997 17.9993L3.63574 11.6354L5.04996 10.2212L9.9997 15.1709Z"></path></svg>', label: '成功'},
|
||||
{id: 'primary', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(240,187,64,1)"><path d="M11.9996 0.5L16.2256 6.68342L23.4123 8.7918L18.8374 14.7217L19.053 22.2082L11.9996 19.6897L4.94617 22.2082L5.16179 14.7217L0.586914 8.7918L7.7736 6.68342L11.9996 0.5ZM9.99959 12H7.99959C7.99959 14.2091 9.79045 16 11.9996 16C14.1418 16 15.8907 14.316 15.9947 12.1996L15.9996 12H13.9996C13.9996 13.1046 13.1042 14 11.9996 14C10.9452 14 10.0814 13.1841 10.0051 12.1493L9.99959 12Z"></path></svg>', label: '重要'},
|
||||
{id: 'danger', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(251,12,12,1)"><path d="M10.5859 12L2.79297 4.20706L4.20718 2.79285L12.0001 10.5857L19.793 2.79285L21.2072 4.20706L13.4143 12L21.2072 19.7928L19.793 21.2071L12.0001 13.4142L4.20718 21.2071L2.79297 19.7928L10.5859 12Z"></path></svg>', label: '危险'},
|
||||
{id: 'warning', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M4.00001 20V14C4.00001 9.58172 7.58173 6 12 6C16.4183 6 20 9.58172 20 14V20H21V22H3.00001V20H4.00001ZM6.00001 14H8.00001C8.00001 11.7909 9.79087 10 12 10V8C8.6863 8 6.00001 10.6863 6.00001 14ZM11 2H13V5H11V2ZM19.7782 4.80761L21.1924 6.22183L19.0711 8.34315L17.6569 6.92893L19.7782 4.80761ZM2.80762 6.22183L4.22183 4.80761L6.34315 6.92893L4.92894 8.34315L2.80762 6.22183Z"></path></svg>', label: '警告'},
|
||||
{id: 'info', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM11 11V17H13V11H11ZM11 7V9H13V7H11Z"></path></svg>', label: '关于'},
|
||||
{id: 'dark', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3Z"></path></svg>', label: '黑色'}
|
||||
];
|
||||
alertTypes.forEach(function(type) {
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-alert-' + type.id + '" title="插入' + type.label + '提示"><span>' + type.icon + '</span></li>');
|
||||
$(document).on('click', '#wmd-alert-' + type.id, function() {
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
var selected = value.substring(start, end) || type.label;
|
||||
var text = '[' + type.id + ']' + selected + '[/' + type.id + ']';
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
$('#text').trigger('change');
|
||||
});
|
||||
});
|
||||
// 折叠面板按钮
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-collapse-button" title="插入折叠面板"><span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M19 12C20.0929 12 21.1175 12.2922 22 12.8027V6C22 5.44772 21.5523 5 21 5H12.4142L10.4142 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H13.8027C13.2922 20.1175 13 19.0929 13 18C13 14.6863 15.6863 12 19 12ZM20.4143 17.9999L22.5356 20.1212L21.1214 21.5354L19.0001 19.4141L16.8788 21.5354L15.4646 20.1212L17.5859 17.9999L15.4646 15.8786L16.8788 14.4644L19.0001 16.5857L21.1214 14.4644L22.5356 15.8786L20.4143 17.9999Z"></path></svg></span></li>');
|
||||
$(document).on('click', '#wmd-collapse-button', function() {
|
||||
var title = prompt('请输入折叠面板标题:', '折叠标题');
|
||||
if (title !== null) {
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
var selected = value.substring(start, end) || '这里是折叠内容';
|
||||
var text = "[collapse title='" + title + "']" + selected + "[/collapse]";
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
$('#text').trigger('change');
|
||||
}
|
||||
});
|
||||
// 下载按钮
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-download-button" title="插入下载信息"><span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M3 19H21V21H3V19ZM13 9H20L12 17L4 9H11V1H13V9Z"></path></svg></span></li>');
|
||||
$(document).on('click', '#wmd-download-button', function() {
|
||||
var file = prompt('请输入文件名:', 'xxx.zip');
|
||||
if (file === null) return;
|
||||
var size = prompt('请输入文件大小:', '12MB');
|
||||
if (size === null) return;
|
||||
var url = prompt('请输入下载地址:', 'https://example.com/file.zip');
|
||||
if (url === null) return;
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
var text = "[download file='" + file + "' size='" + size + "']" + url + "[/download]";
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
$('#text').trigger('change');
|
||||
});
|
||||
// 回复可见按钮
|
||||
$('#wmd-button-row').append('<li class="wmd-button" id="wmd-reply-button" title="插入回复可见"><span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M10.1305 15.8421L9.34268 18.7821L7.41083 18.2645L8.1983 15.3256C7.00919 14.8876 5.91661 14.2501 4.96116 13.4536L2.80783 15.6069L1.39362 14.1927L3.54695 12.0394C2.35581 10.6105 1.52014 8.8749 1.17578 6.96843L2.07634 6.80469C4.86882 8.81573 8.29618 10.0003 12.0002 10.0003C15.7043 10.0003 19.1316 8.81573 21.9241 6.80469L22.8247 6.96843C22.4803 8.8749 21.6446 10.6105 20.4535 12.0394L22.6068 14.1927L21.1926 15.6069L19.0393 13.4536C18.0838 14.2501 16.9912 14.8876 15.8021 15.3256L16.5896 18.2645L14.6578 18.7821L13.87 15.8421C13.2623 15.9461 12.6376 16.0003 12.0002 16.0003C11.3629 16.0003 10.7381 15.9461 10.1305 15.8421Z"></path></svg></span></li>');
|
||||
$(document).on('click', '#wmd-reply-button', function() {
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
var selected = value.substring(start, end) || '这里是隐藏内容';
|
||||
var text = '[reply]' + selected + '[/reply]';
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
$('#text').trigger('change');
|
||||
});
|
||||
|
||||
$('#wmd-article-button').click(function() {
|
||||
var articleId = prompt("请输入要引用的文章ID:");
|
||||
if (articleId) {
|
||||
var text = "[article id=\"" + articleId + "\"]";
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
// 将光标移动到插入的文本之后
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
// 触发change事件,确保编辑器更新
|
||||
$('#text').trigger('change');
|
||||
}
|
||||
});
|
||||
// github按钮插入
|
||||
$('#wmd-github-button').click(function() {
|
||||
var repo = prompt("请输入GitHub仓库名(如 jkjoy/typecho):");
|
||||
if (repo) {
|
||||
var text = "[github=" + repo + "]";
|
||||
var textarea = $('#text')[0];
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var value = textarea.value;
|
||||
textarea.value = value.substring(0, start) + text + value.substring(end);
|
||||
textarea.setSelectionRange(start + text.length, start + text.length);
|
||||
textarea.focus();
|
||||
$('#text').trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
// 注册编辑器按钮钩子
|
||||
// 避免重复注册,在最后执行
|
||||
if (!Typecho_Plugin::exists('Widget_Abstract_Contents', 'editor')) {
|
||||
Typecho_Plugin::factory('admin/write-post.php')->bottom = array('EditorButton', 'render');
|
||||
Typecho_Plugin::factory('admin/write-page.php')->bottom = array('EditorButton', 'render');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章摘要
|
||||
* @param $post Widget_Abstract_Contents|array
|
||||
* @param int $length 摘要长度
|
||||
* @return string
|
||||
*/
|
||||
function get_article_summary($post, $length = 100) {
|
||||
// 如果有自定义摘要字段
|
||||
if (is_array($post) && isset($post['fields']) && !empty($post['fields']['summary'])) {
|
||||
return $post['fields']['summary'];
|
||||
}
|
||||
if (is_object($post) && isset($post->fields) && !empty($post->fields->summary)) {
|
||||
return $post->fields->summary;
|
||||
}
|
||||
// 否则自动截取正文
|
||||
$text = is_array($post) ? $post['text'] : (isset($post->text) ? $post->text : '');
|
||||
$text = strip_tags($text); // 去除HTML标签
|
||||
$text = str_replace(["\r", "\n", "\t"], '', $text); // 去除换行和制表
|
||||
if (mb_strlen($text, 'UTF-8') > $length) {
|
||||
return mb_substr($text, 0, $length, 'UTF-8') . '...';
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点统计信息(包含当前登录用户信息)
|
||||
* @return array
|
||||
*/
|
||||
function get_site_statistics() {
|
||||
$db = Typecho_Db::get();
|
||||
$prefix = $db->getPrefix();
|
||||
|
||||
// 获取当前登录用户对象
|
||||
$currentUser = Typecho_Widget::widget('Widget_User');
|
||||
|
||||
// 如果用户未登录,获取默认uid=1的用户
|
||||
if (!$currentUser->hasLogin()) {
|
||||
$defaultUser = $db->fetchRow($db->select()->from('table.users')->where('uid = ?', 1));
|
||||
$email = $defaultUser['mail'];
|
||||
$nickname = $defaultUser['screenName'];
|
||||
} else {
|
||||
// 用户已登录,使用当前用户信息
|
||||
$email = $currentUser->mail;
|
||||
$nickname = $currentUser->screenName;
|
||||
}
|
||||
|
||||
// 获取用户设置的 Gravatar 镜像
|
||||
$cnavatar = Helper::options()->cnavatar ? Helper::options()->cnavatar : 'https://cravatar.cn/avatar/';
|
||||
$hash = md5($email);
|
||||
$avatar = rtrim($cnavatar, '/') . '/' . $hash . '?s=80&d=identicon';
|
||||
|
||||
// 其余统计信息保持不变
|
||||
$userCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.users'))->num;
|
||||
$postCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.contents')->where('type = ?', 'post')->where('status = ?', 'publish'))->num;
|
||||
$commentCount = $db->fetchObject($db->select(array('COUNT(*)' => 'num'))->from('table.comments'))->num;
|
||||
$totalViews = $db->fetchObject(
|
||||
$db->select(array('SUM(views)' => 'viewsum'))->from('table.contents')->where('type = ?', 'post')
|
||||
)->viewsum;
|
||||
if ($totalViews === null) $totalViews = 0;
|
||||
|
||||
return [
|
||||
'email' => $email,
|
||||
'nickname' => $nickname,
|
||||
'avatar' => $avatar,
|
||||
'userCount' => $userCount,
|
||||
'postCount' => $postCount,
|
||||
'commentCount' => $commentCount,
|
||||
'totalViews' => $totalViews,
|
||||
'isLogin' => $currentUser->hasLogin() // 添加一个是否登录的标志
|
||||
];
|
||||
}
|
||||
|
||||
// Typecho AJAX 登录接口,支持前端 AJAX 提交并返回 JSON
|
||||
//if (!empty($_GET['ajaxLogin']) && $_GET['ajaxLogin'] == 1) {
|
||||
// header('Content-Type: application/json; charset=utf-8');
|
||||
// if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
// echo json_encode(['success' => false, 'msg' => '请求方式错误']);
|
||||
// exit;
|
||||
// }
|
||||
// $name = isset($_POST['name']) ? trim($_POST['name']) : '';
|
||||
// $password = isset($_POST['password']) ? $_POST['password'] : '';
|
||||
// $referer = isset($_POST['referer']) ? $_POST['referer'] : '/';
|
||||
// if (!$name || !$password) {
|
||||
// echo json_encode(['success' => false, 'msg' => '用户名或密码不能为空']);
|
||||
// exit;
|
||||
// }
|
||||
// $user = Typecho_Widget::widget('Widget_User');
|
||||
// try {
|
||||
// $user->login($name, $password, isset($_POST['remember']) ? 1 : 0);
|
||||
// echo json_encode(['success' => true, 'msg' => '登录成功', 'redirect' => $referer]);
|
||||
// } catch (Typecho_Exception $e) {
|
||||
// echo json_encode(['success' => false, 'msg' => $e->getMessage()]);
|
||||
// }
|
||||
// exit;
|
||||
//}
|
||||
+28
-8
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* @package Typecho Pouck Theme
|
||||
* @author 老孙博客
|
||||
* @version 1.1
|
||||
* @version 1.2.0
|
||||
* @link http://www.imsun.org
|
||||
*/
|
||||
|
||||
@@ -82,7 +82,7 @@ if ($pageprev == '1' && $this->have()):
|
||||
'wrapClass' => 'pagination comment-ajax-load',
|
||||
'itemTag' => 'li',
|
||||
'textTag' => 'span',
|
||||
'currentClass' => 'active',
|
||||
'currentClass' => 'cur',
|
||||
'prevClass' => 'prev',
|
||||
'nextClass' => 'next'
|
||||
)); ?>
|
||||
|
||||
@@ -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'); ?>
|
||||
+55
-53
@@ -13,7 +13,7 @@ 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">
|
||||
<div id="posts" class="col-lg-8 col-md-12 animated fadeInLeft ">
|
||||
<?php $tooot = $this->fields->tooot ? $this->fields->tooot : 'https://www.imsun.org/toot.json'; ?>
|
||||
@@ -21,66 +21,59 @@ if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
|
||||
<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 +86,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;
|
||||
@@ -161,6 +163,6 @@ img {
|
||||
}
|
||||
</style>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
@@ -61,4 +61,4 @@ if($days > 180){
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php $this->need('sidebar.php'); ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
<?php $this->need('footer.php'); ?>
|
||||
@@ -90,6 +90,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,69 +121,98 @@ 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>
|
||||
|
||||
+21
-47
@@ -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>
|
||||
@@ -138,21 +115,18 @@ if ($totalViews === null) $totalViews = 0;
|
||||
<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