Files
wordpress-kratos-yang/inc/theme-widgets.php
T

548 lines
25 KiB
PHP
Raw Normal View History

2022-01-07 11:15:42 +08:00
<?php
/**
* 侧栏小工具
* @author Yongquan Yang <yangyq.net>
2022-01-07 11:15:42 +08:00
* @license MIT License
* @version 2020.08.03
*/
// 添加小工具
function widgets_init()
{
register_sidebar(array(
'name' => __('侧边栏工具', 'kratos'),
'id' => 'sidebar_tool',
'before_widget' => '<div class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<div class="title">',
'after_title' => '</div>',
));
// 去掉默认小工具
$wp_widget = array(
'WP_Widget_Pages',
'WP_Widget_Meta',
'WP_Widget_Recent_Posts',
'WP_Widget_Recent_Comments',
'WP_Widget_RSS',
'WP_Widget_Search',
'WP_Widget_Tag_Cloud',
'WP_Nav_Menu_Widget',
);
foreach ($wp_widget as $wp_widget) {
unregister_widget($wp_widget);
}
}
add_action('widgets_init', 'widgets_init');
/* ==========================================================================
* 热度算法(3.2.49)
*
* ── 为什么废弃旧的 most_comm_posts() ──────────────────────────────────
* 旧实现的候选池是「最近 N 天(默认 30)**发布**的文章」:
*
* WHERE post_date BETWEEN '$daysago' AND '$today'
* ORDER BY comment_count DESC LIMIT 6
*
* 对日更站点没问题,对低频更新的站点直接失效。本站实测(2026-10-26):
* 7 天内发布 0 篇 / 30 天内 1 篇 / 90 天内 1 篇 / 180 天内 6 篇
* 30 天候选池只有 1 篇 ⇒ LIMIT 6 也只能返回 1 条。
* 这就是侧栏「热点」永远只显示一条的真正原因 —— **不是排序错,是池子空**。
*
* 而且「最近 30 天发布」本身就不是「热」的定义:热是累计互动,不是新鲜度。
*
* ── 现在的算法 ────────────────────────────────────────────────────────
* 候选池:全站所有已发布 post(不含 page —— 「留言本」这种页面 views 214 万,
* 它的热度对"最热文章"没有意义;`post_type='post'` 天然挡掉)。
* 打分:三个指标各取 log10 后加权求和
*
* score = 3*log10(views+1) + 5*log10(love+1) + 4*log10(comments+1)
*
* 为什么取 log10:本站 views 中位数 4981、最大 56075(差 11 倍),
* 但 love 最大只有 40、评论最大 93。不取对数的话 views 一个指标就吃掉
* 全部权重,点赞和评论等于没参与。取对数后三者量级被拉平到可比的区间。
*
* 为什么权重是 3 / 5 / 4(不是常见的 1 / 3 / 5):
* 用通用值实测,前 8 名有 6 篇是 2009 年的老随笔 —— 因为那批文章评论
* 80~93 条,仅评论一项(1.97×5=9.86)就压过 views 最大值的贡献(4.75×1=4.75),
* 结果变成"评论数排行榜"。把 views 权重提到 3 之后三个指标影响力均衡:
* 点赞最高(5)—— 点👍 是比评论更强的认可信号,且本站只有 197 篇有点赞,
* 稀缺性让它更有区分度;评论次之(4)—— 要付出打字成本;浏览最轻(3)。
*
* 实测这组权重的排名(前 8):Anaconda多用户安装(love=40) · kratos主题发布(love=9,评论37)
* · LaTex上划线(love=19) · 那一年2009 · LaTex字体(love=12) · Windows7自带WMP(评论93)
* → 技术文章与高互动随笔混合出现,2009 年那批也没有被完全埋掉。
*
* ── 有意不加时间衰减 ──────────────────────────────────────────────────
* 「全站历史最热」是累计榜,不是时效榜。Hacker News / Reddit 那套
* gravity 衰减是给「新内容榜」用的,加上去会把老文章压到看不见,
* 那就又回到"只有最近几条"的老问题。要时效榜是另一个功能,不塞在这里。
*
* ── 顺手修掉的两个坑 ──────────────────────────────────────────────────
* ① 旧代码里 `date_default_timezone_set("PRC")` 直接改 **PHP 全局时区**,
* 影响同一请求里之后所有时间函数。已删。
* ② 旧代码把 `$title` 原样插进 `title="..."` 属性,标题里有双引号就会
* 截断属性(甚至注入)。已改用 esc_attr()。
* ====================================================================== */
if (!function_exists('yang_hot_score')):
/**
* 单篇文章的热度分。
*
* @param int $views 浏览数
* @param int $love 点赞数
* @param int $comments 评论数
* @return float
*/
function yang_hot_score($views, $love, $comments)
{
return 3 * log10((int) $views + 1)
+ 5 * log10((int) $love + 1)
+ 4 * log10((int) $comments + 1);
}
endif;
if (!function_exists('yang_hot_posts')):
/**
* 取热度最高的文章。
*
* 为什么在 PHP 里算分、不在 SQL 里 ORDER BY:
* 分值是三项 log 加权和,SQL 里要写三段 LOG10 + COALESCE + 两个
* LEFT JOIN 到 postmeta,可读性极差,而且没法复用 yang_hot_score()
* (测试和产品必须是同一份实现,不能各写一遍)。
* 代价是要把候选行取回来 —— 本站 967 篇 post,一次取 4 个字段
* 内存占用可忽略;如果将来文章上万,再加 LIMIT / 缓存。
* 为防止极端情况,SQL 侧先用 views 粗排取前 N*10(对数排序里
* 真正的高分不会来自 views 极低的文章,这个粗排是安全的)。
*
* @param int $nums 返回条数
* @return array 文章对象数组,每项含 ID/post_title/views/love/comments/score
*/
function yang_hot_posts($nums = 6)
{
global $wpdb;
$nums = max(1, (int) $nums);
// 粗排窗口:给足余量。views 前 200 名以外不可能靠 love/评论翻盘
// (log10 域下,love 满分 1.61×5=8.05,补不回 views 差 1 个数量级的 3 分)。
$pool = max(200, $nums * 20);
$sql = $wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_date, p.comment_count,
COALESCE(CAST(mv.meta_value AS UNSIGNED), 0) AS views,
COALESCE(CAST(ml.meta_value AS UNSIGNED), 0) AS love
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} mv ON mv.post_id = p.ID AND mv.meta_key = 'views'
LEFT JOIN {$wpdb->postmeta} ml ON ml.post_id = p.ID AND ml.meta_key = 'love'
WHERE p.post_status = 'publish'
AND p.post_type = 'post'
ORDER BY views DESC
LIMIT %d",
$pool
);
$rows = $wpdb->get_results($sql);
if (empty($rows)) {
return array();
}
$scored = array();
foreach ($rows as $r) {
$r->views = (int) $r->views;
$r->love = (int) $r->love;
$r->comments = (int) $r->comment_count;
$r->score = yang_hot_score($r->views, $r->love, $r->comments);
$scored[] = $r;
}
usort($scored, function ($a, $b) {
if ($a->score == $b->score) {
// 同分按浏览数兜底,保证顺序稳定(否则每次刷新顺序会跳)
return $b->views <=> $a->views;
}
return $b->score <=> $a->score;
});
return array_slice($scored, 0, $nums);
}
endif;
/**
* 小工具文章聚合 —— 热点文章
*
* ⚠️ 这个函数名和签名保留着,因为 `widget_posts::widget()` 里有一句
* `if (function_exists('most_comm_posts'))` 调用它。留着旧名不破兼容,
* 但内部已经完全换成 yang_hot_posts()。
*
* @param int $days 已废弃。旧实现用它当"最近 N 天发布"的时间窗,
* 那正是只显示一条的根因。保留参数只为不破坏调用签名。
* @param int $nums 返回条数
*/
2022-01-07 11:15:42 +08:00
function most_comm_posts($days = 30, $nums = 6)
{
$posts = yang_hot_posts($nums);
2022-01-07 11:15:42 +08:00
$output = '';
foreach ($posts as $p) {
$output .= '<a class="bookmark-item" title="' . esc_attr($p->post_title) . '"'
. ' href="' . esc_url(get_permalink($p->ID)) . '" rel="bookmark">'
. '<i class="kicon i-book"></i>'
. esc_html(wp_strip_all_tags($p->post_title))
. '</a>';
2022-01-07 11:15:42 +08:00
}
echo $output;
}
class widget_search extends WP_Widget {
public function __construct() {
$widget_ops = array(
'classname' => 'widget_search',
'description' => __( 'A search form for your site.' ),
'customize_selective_refresh' => true,
);
parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops );
}
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
echo '<div class="widget w-search">';
if ( $title ) {
echo '<div class="title">'. $title .'</div>';
}
echo '<div class="item"> <form role="search" method="get" id="searchform" class="searchform" action="'. home_url('/') .'"> <div class="input-group mt-2 mb-2"> <input type="text" name="s" id="search" class="form-control" placeholder="'. __('搜点什么呢?', 'kratos') .'"> <div class="input-group-append"> <button class="btn btn-primary btn-search" type="submit" id="searchsubmit">'. __('搜索', 'kratos') .'</button> </div> </div> </form>';
echo '</div></div>';
}
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, array( 'title' => '' ) );
$title = $instance['title'];
?>
<p><label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?> <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" /></label></p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = $old_instance;
$new_instance = wp_parse_args( (array) $new_instance, array( 'title' => '' ) );
$instance['title'] = sanitize_text_field( $new_instance['title'] );
return $instance;
}
}
class widget_ad extends WP_Widget
{
public function __construct()
{
add_action('admin_enqueue_scripts', array($this, 'scripts'));
$widget_ops = array(
'name' => __('图片广告', 'kratos'),
'description' => __('显示自定义图片广告的工具', 'kratos'),
);
parent::__construct(false, false, $widget_ops);
}
public function scripts()
{
wp_enqueue_script('media-upload');
wp_enqueue_media();
wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery'));
wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array());
}
public function widget($args, $instance)
{
$subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos');
$image = !empty($instance['image']) ? $instance['image'] : '';
$url = !empty($instance['url']) ? $instance['url'] : '';
echo '<div class="widget w-ad">';
echo '<a href="' . $url . '" target="_blank" rel="noreferrer"><img src="' . $image . '"><div class="prompt">' . $subtitle . '</div></a>';
echo '</div>';
}
public function update($new_instance, $old_instance)
{
$instance = array();
$instance['subtitle'] = (!empty($new_instance['subtitle'])) ? $new_instance['subtitle'] : '';
$instance['image'] = (!empty($new_instance['image'])) ? $new_instance['image'] : '';
$instance['url'] = (!empty($new_instance['url'])) ? $new_instance['url'] : '';
return $instance;
}
public function form($instance)
{
$subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos');
$image = !empty($instance['image']) ? $instance['image'] : '';
$url = !empty($instance['url']) ? $instance['url'] : '';
?>
<div class="media-widget-control">
<p>
<label for="<?php echo $this->get_field_id('subtitle'); ?>"><?php _e('副标题:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('subtitle'); ?>" name="<?php echo $this->get_field_name('subtitle'); ?>" type="text" value="<?php echo esc_attr($subtitle); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('url'); ?>"><?php _e('链接地址:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('url'); ?>" name="<?php echo $this->get_field_name('url'); ?>" type="text" value="<?php echo esc_attr($url); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('image'); ?>"><?php _e('广告图片:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('image'); ?>" name="<?php echo $this->get_field_name('image'); ?>" type="text" value="<?php echo esc_url($image); ?>" />
<button type="button" class="button-update-media upload_ad"><?php _e('选择图片', 'kratos');?></button>
</p>
</div>
<?php
}
}
class widget_about extends WP_Widget
{
public function __construct()
{
add_action('admin_enqueue_scripts', array($this, 'scripts'));
$widget_ops = array(
'name' => __('个人简介', 'kratos'),
'description' => __('可跳转后台的个人简介展示工具', 'kratos'),
);
parent::__construct(false, false, $widget_ops);
}
public function scripts()
{
wp_enqueue_script('media-upload');
wp_enqueue_media();
wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery'));
wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array());
}
public function widget($args, $instance)
{
$introduce = kratos_option('a_about', __('保持饥渴的专注,追求最佳的品质', 'kratos'));
$username = kratos_option('a_nickname', __('Kratos', 'kratos'));
$avatar = yang_option_image('a_gravatar');
2022-01-07 11:15:42 +08:00
$background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png';
echo '<div class="widget w-about">';
echo '<div class="background" style="background:url(' . $background . ') no-repeat center center;-webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover;"></div><div class="wrapper text-center">';
if (current_user_can('manage_options')) {
echo '<a href="' . admin_url() . '">';
} else {
echo '<a href="' . wp_login_url() . '">';
}
echo '<img src="' . $avatar . '"></a>';
echo '</div><div class="textwidget text-center"><p class="username">'. $username .'</p><p class="about">' . $introduce . '</p></div>';
echo '</div>';
}
public function update($new_instance, $old_instance)
{
$instance = array();
$instance['background'] = (!empty($new_instance['background'])) ? $new_instance['background'] : '';
return $instance;
}
public function form($instance)
{
$background = !empty($instance['background']) ? $instance['background'] : ASSET_PATH . '/assets/img/about-background.png';
?>
<div class="media-widget-control">
<p>
<label for="<?php echo $this->get_field_id('background'); ?>"><?php _e('背景图片:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('background'); ?>" name="<?php echo $this->get_field_name('background'); ?>" type="text" value="<?php echo esc_attr($background); ?>">
<button type="button" class="button-update-media upload_background"><?php _e('选择图片', 'kratos');?></button>
</p>
</div>
<?php
}
}
class widget_tags extends WP_Widget
{
public function __construct()
{
$widget_ops = array(
'name' => __('标签聚合', 'kratos'),
'description' => __('文章标签的展示工具', 'kratos'),
);
parent::__construct(false, false, $widget_ops);
}
public function widget($args, $instance)
{
$number = !empty($instance['number']) ? $instance['number'] : '8';
$order = !empty($instance['order']) ? $instance['order'] : 'RAND';
$tags = wp_tag_cloud(array(
'unit' => 'px',
'smallest' => 14,
'largest' => 14,
'number' => $number,
'format' => 'flat',
'orderby' => 'count',
'order' => $order,
'echo' => false,
)
);
echo '<div class="widget w-tags">';
echo '<div class="title">' . __('标签聚合', 'kratos') . '</div>';
echo '<div class="item">' . $tags . '</div>';
echo '</div>';
}
public function update($new_instance, $old_instance)
{
$instance = array();
$instance['number'] = (!empty($new_instance['number'])) ? $new_instance['number'] : '';
$instance['order'] = (!empty($new_instance['order'])) ? $new_instance['order'] : '';
return $instance;
}
public function form($instance)
{
global $wpdb;
$number = !empty($instance['number']) ? $instance['number'] : '8';
$order = !empty($instance['order']) ? $instance['order'] : 'RAND';
?>
<div class="media-widget-control">
<p>
<label for="<?php echo $this->get_field_id('number'); ?>"><?php _e('显示数量:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('number'); ?>" name="<?php echo $this->get_field_name('number'); ?>" type="text" value="<?php echo esc_attr($number); ?>" />
</p>
<p>
<label for="<?php echo $this->get_field_id('order'); ?>"><?php _e('显示排序:', 'kratos');?></label>
<select name="<?php echo $this->get_field_name("order"); ?>" id='<?php echo $this->get_field_id("order"); ?>'>
<option value="DESC" <?php echo ($order == 'DESC') ? 'selected' : ''; ?>><?php _e('降序', 'kratos');?></option>
<option value="ASC" <?php echo ($order == 'ASC') ? 'selected' : ''; ?>><?php _e('升序', 'kratos');?></option>
<option value="RAND" <?php echo ($order == 'RAND') ? 'selected' : ''; ?>><?php _e('随机', 'kratos');?></option>
</select>
</p>
</div>
<?php
}
}
class widget_posts extends WP_Widget
{
public function __construct()
{
$widget_ops = array(
'name' => __('文章聚合', 'kratos'),
'description' => __('展示最热、随机、最新文章的工具', 'kratos'),
);
parent::__construct(false, false, $widget_ops);
}
public function widget($args, $instance)
{
$number = !empty($instance['number']) ? (int) $instance['number'] : 6;
if ($number < 1) {
$number = 6;
}
2022-01-07 11:15:42 +08:00
echo '<div class="widget w-recommended">';
?>
<div class="nav nav-tabs d-none d-xl-flex" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-new-tab" data-toggle="tab" href="#nav-new" role="tab" aria-controls="nav-new" aria-selected="false"><i class="kicon i-tabnew"></i><?php _e('最新', 'kratos');?></a>
<a class="nav-item nav-link" id="nav-hot-tab" data-toggle="tab" href="#nav-hot" role="tab" aria-controls="nav-hot" aria-selected="true"><i class="kicon i-tabhot"></i><?php _e('热点', 'kratos');?></a>
<a class="nav-item nav-link" id="nav-random-tab" data-toggle="tab" href="#nav-random" role="tab" aria-controls="nav-random" aria-selected="false"><i class="kicon i-tabrandom"></i><?php _e('随机', 'kratos');?></a>
</div>
<div class="nav nav-tabs d-xl-none" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-new-tab" data-toggle="tab" href="#nav-new" role="tab" aria-controls="nav-new" aria-selected="false"><?php _e('最新', 'kratos');?></a>
<a class="nav-item nav-link" id="nav-hot-tab" data-toggle="tab" href="#nav-hot" role="tab" aria-controls="nav-hot" aria-selected="true"><?php _e('热点', 'kratos');?></a>
<a class="nav-item nav-link" id="nav-random-tab" data-toggle="tab" href="#nav-random" role="tab" aria-controls="nav-random" aria-selected="false"><?php _e('随机', 'kratos');?></a>
</div>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-new" role="tabpanel" aria-labelledby="nav-new-tab">
<?php
/* 🔴 原来写的是 `'numberposts=' . $number . ' & offset=0'` ——
* 字符串里带空格,WP_Query 解析时参数名变成 `' offset'`(前面
* 有空格),`offset=0` 实际没生效。offset=0 本身是默认值,
* 所以症状看不出来,但这是错的写法。改成数组传参。 */
$myposts = get_posts(array(
'numberposts' => $number,
'offset' => 0,
));
foreach ($myposts as $post): ?>
<a class="bookmark-item" title="<?php echo esc_attr($post->post_title); ?>" href="<?php echo esc_url(get_permalink($post->ID)); ?>" rel="bookmark"><i class="kicon i-book"></i><?php echo esc_html(wp_strip_all_tags($post->post_title)) ?></a>
2022-01-07 11:15:42 +08:00
<?php endforeach;?>
</div>
<div class="tab-pane fade" id="nav-hot" role="tabpanel" aria-labelledby="nav-hot-tab">
<?php
/* 热点:全站累计热度(views/love/评论 三项 log 加权),
* 不再受「最近 N 天发布」的时间窗限制。原因见 most_comm_posts()
* 上方的注释块。 */
if (function_exists('most_comm_posts')) {
most_comm_posts(0, $number);
} ?>
2022-01-07 11:15:42 +08:00
</div>
<div class="tab-pane fade" id="nav-random" role="tabpanel" aria-labelledby="nav-random-tab">
<?php
$myposts = get_posts(array(
'numberposts' => $number,
'offset' => 0,
'orderby' => 'rand',
));
foreach ($myposts as $post): ?>
<a class="bookmark-item" title="<?php echo esc_attr($post->post_title); ?>" href="<?php echo esc_url(get_permalink($post->ID)); ?>" rel="bookmark"><i class="kicon i-book"></i><?php echo esc_html(wp_strip_all_tags($post->post_title)) ?></a>
2022-01-07 11:15:42 +08:00
<?php endforeach;?>
</div>
</div>
<?php echo '</div>';
}
public function update($new_instance, $old_instance)
{
$instance = array();
$instance['number'] = (!empty($new_instance['number'])) ? (int) $new_instance['number'] : 6;
2022-01-07 11:15:42 +08:00
/* `days` 已废弃:旧实现拿它当「最近 N 天发布」的时间窗,
* 那正是热点只显示一条的根因。这里不再保存,
* 老实例里残留的 days 值会在下次保存时被丢掉。 */
2022-01-07 11:15:42 +08:00
return $instance;
}
public function form($instance)
{
$number = !empty($instance['number']) ? $instance['number'] : '6';
?>
<div class="media-widget-control">
<p>
<label for="<?php echo $this->get_field_id('number'); ?>"><?php _e('展示数量:', 'kratos');?></label>
<input class="widefat" id="<?php echo $this->get_field_id('number'); ?>" name="<?php echo $this->get_field_name('number'); ?>" type="text" value="<?php echo esc_attr($number); ?>" />
</p>
<p class="description">
<?php _e('「最新」「随机」「热点」三个标签页都用这个数量。', 'kratos'); ?><br>
<?php _e('「热点」按全站累计热度排序:浏览量、点赞数、评论数三项分别取对数后加权(3 : 5 : 4),不受发布时间限制。', 'kratos'); ?>
2022-01-07 11:15:42 +08:00
</p>
</div>
<?php
}
}
function register_widgets()
{
register_widget('widget_ad');
register_widget('widget_about');
register_widget('widget_tags');
register_widget('widget_search');
register_widget('widget_posts');
}
add_action('widgets_init', 'register_widgets');