一、头像 / 昵称 / 简介原来**只读主题设置**,挂件表单里没有字段
· 用户点开侧栏「个人简介」只能改背景图,头像和那句话得跑去
「主题设置 → 站长信息」改全站共用的那份。
· 现在四项都是挂件实例字段:avatar / nickname / introduce / background。
· 取值顺序:实例 → 主题设置 → 内置兜底,**留空即回落**,
老站点升级后显示一点不变。
· pick() 判空统一用「trim 后是不是空串」,不用 empty()
(!empty('0') 是 false,昵称就叫「0」会被当没填)。
· 简介用 sanitize_textarea_field(保留换行),URL 用 esc_url_raw
(不做 HTML 实体化,避免与 esc_attr 双重转义)。
二、后台 form() 补齐五个字段
· 头像 / 背景各带「选择图片」按钮(调媒体库),背景另有「清除」。
· 输入框显示用户填的原始值,缩略图显示前台最终会用的那张,刻意分开。
· 资源加载统一到 yang_widget_admin_assets(),不再每个挂件各挂一次。
三、大图撑爆挂件的真因:CSS 选择器依赖三层祖先
· 老规则是 `.k-main .sidebar .w-about .wrapper img`,而后台
「外观 → 小工具」预览里没有 `.k-main .sidebar` 这层祖先,
规则一条都不生效 ⇒ 头像按原图 2136×2136 铺开、背景塌成 0。
前台看着正常是因为前台恰好有那层祖先,问题被完全掩盖。
· 改为只管 `.w-about` 内部元素,并补 max-width/max-height 双保险。
· 老规则必须**删掉**而不是并存:它是 (0,3,1),新规则是 (0,2,1),
并存时老规则在前台永远赢,新规则只对后台生效。
· 保留 `.k-main .sidebar .w-about { padding:0 }`:通用规则
`.k-main .sidebar .widget` 是 (0,3,0),少了这条会多出内边距、整体高 57px。
· 头像保持 inline(display:block 会让它脱离 text-align:center 的居中)。
· 顺带:object-fit 从 fill 改 cover(超宽图不再压扁);
背景 url() 补引号(文件名带空格时不再整条声明作废)。
验证
· 行为测试 67 条全通过;反向验证 13 组全部真红。
· 浏览器几何 76 条全通过(前台/后台 × 四种极端原图)。
· 修复前 CSS 跑同一套判据:38 条报红,精确复现用户症状
(后台挂件高 2217、头像 2136×2136、横向滚动 2146)。
· 线上真实页面 + 新版 CSS:16 条全通过。
986 lines
48 KiB
PHP
986 lines
48 KiB
PHP
<?php
|
||
/**
|
||
* 侧栏小工具
|
||
* @author Yongquan Yang <yangyq.net>
|
||
* @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.51)
|
||
*
|
||
* ── 为什么不再各个挂件自己挂一份 ────────────────────────────────────
|
||
* 原来 `widget_about` / `widget_ad` 各自在构造函数里 add_action 一个
|
||
* `admin_enqueue_scripts`,动作里 enqueue 媒体库 + widget.min.js。
|
||
* 这套写法有两个毛病:
|
||
*
|
||
* ① 两个挂件都启用时,同一份资源被 enqueue 两次(id 相同,WP 会去重,
|
||
* 但每次都会多跑一次 wp_enqueue_media,白白初始化一遍媒体框);
|
||
* ② 更要紧的:媒体框的 JS 只在**挂件实例展开**用得上,可 WP 的旧版
|
||
* 挂件页会把所有实例的 form() 一次性渲染进页面 —— 资源没到就等于
|
||
* 「选择图片」按钮点了没反应。
|
||
*
|
||
* 现在统一挂在一个函数里,只在挂件相关页面加载。
|
||
*
|
||
* 🔴 缩略图预览这部分样式用 wp_add_inline_style 而不是去改
|
||
* assets/css/widget.min.css:那个文件**没有源文件**(仓库里只有压缩产物),
|
||
* 为了加几条规则去手改一行压缩 CSS,下次别人再改就无从维护。
|
||
* ====================================================================== */
|
||
|
||
if (!function_exists('yang_widget_admin_assets')):
|
||
|
||
function yang_widget_admin_assets($hook)
|
||
{
|
||
// 只在挂件页 / 自定义器加载。别的后台页面不需要媒体框。
|
||
if (!in_array($hook, array('widgets.php', 'customize.php'), true)) {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_media();
|
||
wp_enqueue_script('widget_scripts', ASSET_PATH . '/assets/js/widget.min.js', array('jquery'), THEME_VERSION, true);
|
||
wp_enqueue_style('widget_css', ASSET_PATH . '/assets/css/widget.min.css', array(), THEME_VERSION);
|
||
|
||
$css = '
|
||
/* 挂件表单里的图片缩略图。
|
||
没有它的话,用户只能看到一串 URL,根本不知道那张图长什么样 ——
|
||
「背景图在哪改」这个问题有一半是"看不见"造成的。 */
|
||
.yang-media-preview{display:block;margin:8px 0 0;line-height:0}
|
||
.yang-media-preview img{display:block;width:64px;height:64px;object-fit:cover;border:1px solid #dcdcde;border-radius:4px;background:#f6f7f7}
|
||
.yang-media-preview-wide img{width:100%;max-width:260px;height:auto;aspect-ratio:16/9}
|
||
/* 「清除」按钮跟「选择图片」贴在一起,别让它单独换行成一个大按钮 */
|
||
.media-widget-control .yang-media-clear{margin-left:6px}
|
||
';
|
||
wp_register_style('yang-widget-admin', false, array('widget_css'), THEME_VERSION);
|
||
wp_enqueue_style('yang-widget-admin');
|
||
wp_add_inline_style('yang-widget-admin', $css);
|
||
|
||
/* 「清除」按钮:把输入框清空,并顺手把缩略图去掉。
|
||
*
|
||
* 🔴 为什么必须同时清缩略图:只清 input 的话,图片还挂在那儿,
|
||
* 用户会以为没清掉,再点一次又点回原来的图。
|
||
* 🔴 为什么用 change 事件触发:主题的 widget.min.js 靠
|
||
* `.change()` 让 WP 把表单标记为"已修改",否则直接
|
||
* 改 .val() 不会点亮「保存」按钮,用户点保存时改动丢失。 */
|
||
$js = <<<'JS'
|
||
(function(){
|
||
if (typeof jQuery === 'undefined') { return; }
|
||
jQuery(function($){
|
||
$(document).on('click', '.yang-media-clear', function(e){
|
||
e.preventDefault();
|
||
var id = $(this).attr('data-target');
|
||
if (!id) { return; }
|
||
var $input = $('#' + id);
|
||
$input.val('').trigger('change');
|
||
$input.closest('p').find('.yang-media-preview').remove();
|
||
$(this).remove();
|
||
});
|
||
});
|
||
})();
|
||
JS;
|
||
wp_add_inline_script('widget_scripts', $js);
|
||
}
|
||
|
||
endif;
|
||
add_action('admin_enqueue_scripts', 'yang_widget_admin_assets');
|
||
|
||
/* ==========================================================================
|
||
* 挂件标题与外框(3.2.50)
|
||
*
|
||
* ── 为什么要有这两个函数 ──────────────────────────────────────────────
|
||
* register_sidebar() 里已经声明了版式:
|
||
*
|
||
* 'before_title' => '<div class="title">',
|
||
* 'after_title' => '</div>',
|
||
*
|
||
* 但主题自己的挂件**全都绕开了它**:`widget_tags` 把「标签聚合」硬编码在
|
||
* 代码里,`widget_ad` / `widget_about` / `widget_posts` 干脆没有标题字段,
|
||
* `widget_search` 有字段却自己手写了一个 `<div class="title">`。
|
||
*
|
||
* 结果是:在后台改侧栏版式(换标题标签、加装饰)对这四个挂件无效,
|
||
* 而且用户根本没地方给它们起标题。这两个函数把「标题」收敛到一处,
|
||
* 保证所有挂件走同一条路径。
|
||
*
|
||
* 🔴 空标题必须**不输出任何节点**,而不是输出空的 `<div class="title"></div>` ——
|
||
* 后者会留下一条只有内边距的空白条,视觉上像"多了一块间距"。
|
||
* ====================================================================== */
|
||
|
||
if (!function_exists('yang_widget_title')):
|
||
|
||
/**
|
||
* 输出挂件标题(走 sidebar 注册的 before_title / after_title)。
|
||
*
|
||
* @param array $args widget() 收到的 $args
|
||
* @param string $instance 实例里的 title
|
||
* @param string $fallback 没设置标题时的默认值;传 '' 表示默认不显示标题
|
||
* @param string $id_base 挂件 id_base,仅用于 widget_title 过滤器
|
||
*/
|
||
function yang_widget_title($args, $instance, $fallback = '', $id_base = '')
|
||
{
|
||
$title = isset($instance['title']) ? trim((string) $instance['title']) : '';
|
||
if ($title === '') {
|
||
$title = $fallback;
|
||
}
|
||
|
||
/* 兼容 WP 惯例:主题/插件可以用 widget_title 过滤器改标题。
|
||
* 这里保留它,否则装过「改挂件标题」类插件的站点行为会变。 */
|
||
if (function_exists('apply_filters')) {
|
||
$title = apply_filters('widget_title', $title, (array) $instance, $id_base);
|
||
$title = trim((string) $title);
|
||
}
|
||
|
||
if ($title === '') {
|
||
return;
|
||
}
|
||
|
||
$before = isset($args['before_title']) ? $args['before_title'] : '<div class="title">';
|
||
$after = isset($args['after_title']) ? $args['after_title'] : '</div>';
|
||
|
||
echo $before . esc_html($title) . $after;
|
||
}
|
||
|
||
endif;
|
||
|
||
if (!function_exists('yang_widget_uid')):
|
||
|
||
/**
|
||
* 生成挂件内 HTML id 的唯一前缀。
|
||
*
|
||
* 🔴 为什么不能用写死的 id:挂件可以添加多个实例,同一页面上
|
||
* 两个「文章聚合」就会产生两套重复 id(HTML 非法,
|
||
* aria-labelledby 也只能指到第一个)。
|
||
* `$this->id` 由 WP_Widget 分配(形如 `widget_posts-2`),
|
||
* 同一请求内唯一;但在单元测试/旧版环境里可能不存在,所以做兜底。
|
||
*
|
||
* @param object $widget 挂件实例
|
||
* @return string 形如 `yang-widget_posts-2`
|
||
*/
|
||
function yang_widget_uid($widget)
|
||
{
|
||
$id = '';
|
||
if (is_object($widget) && isset($widget->id) && is_string($widget->id) && $widget->id !== '') {
|
||
$id = $widget->id;
|
||
} elseif (is_object($widget) && isset($widget->id_base) && is_string($widget->id_base) && $widget->id_base !== '') {
|
||
$id = $widget->id_base;
|
||
} else {
|
||
$id = 'widget';
|
||
}
|
||
|
||
return 'yang-' . preg_replace('/[^a-z0-9_-]+/i', '-', $id);
|
||
}
|
||
|
||
endif;
|
||
|
||
if (!function_exists('yang_widget_open')):
|
||
|
||
/**
|
||
* 挂件外框起始标签。
|
||
*
|
||
* 侧栏注册的 before_widget 是 `<div class="widget %2$s">`,其中 %2$s 是
|
||
* 挂件类名(classname)。这里故意**不用**它,而是自己拼类名:
|
||
* 主题的挂件样式全部写成 `.k-main .sidebar .w-about` 这种以 `w-` 开头的
|
||
* 短类名,用 classname 会变成 `widget_widget_about` 之类,样式一条都不生效。
|
||
* 保留 `widget` 这个基类(主题里 `.k-main .sidebar .widget` 提供白底与内边距)。
|
||
*
|
||
* @param string $modifier 形如 'w-about'
|
||
* @return string
|
||
*/
|
||
function yang_widget_class($modifier)
|
||
{
|
||
return 'widget ' . $modifier;
|
||
}
|
||
|
||
endif;
|
||
|
||
/* ==========================================================================
|
||
* 热度算法(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 返回条数
|
||
*/
|
||
function most_comm_posts($days = 30, $nums = 6)
|
||
{
|
||
$posts = yang_hot_posts($nums);
|
||
|
||
$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>';
|
||
}
|
||
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 ) {
|
||
/* 🔴 id 必须带实例后缀。
|
||
* 原来写死 id="searchform" / id="search" / id="searchsubmit",
|
||
* 挂两个搜索挂件时全撞;主题别处或核心再放一个搜索表单也会撞。 */
|
||
$uid = yang_widget_uid($this);
|
||
|
||
echo '<div class="' . esc_attr(yang_widget_class('w-search')) . '">';
|
||
yang_widget_title($args, $instance, '', $this->id_base);
|
||
?>
|
||
<div class="item">
|
||
<form role="search" method="get" id="<?php echo esc_attr($uid); ?>-form"
|
||
class="searchform" action="<?php echo esc_url(home_url('/')); ?>">
|
||
<div class="input-group mt-2 mb-2">
|
||
<label class="sr-only" for="<?php echo esc_attr($uid); ?>-field"><?php _e('搜索', 'kratos'); ?></label>
|
||
<input type="text" name="s" id="<?php echo esc_attr($uid); ?>-field" class="form-control"
|
||
placeholder="<?php esc_attr_e('搜点什么呢?', 'kratos'); ?>">
|
||
<div class="input-group-append">
|
||
<button class="btn btn-primary btn-search" type="submit" id="<?php echo esc_attr($uid); ?>-submit"><?php _e('搜索', 'kratos'); ?></button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
echo '</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()
|
||
{
|
||
$widget_ops = array(
|
||
'name' => __('图片广告', 'kratos'),
|
||
'description' => __('显示自定义图片广告的工具', 'kratos'),
|
||
);
|
||
|
||
parent::__construct(false, false, $widget_ops);
|
||
}
|
||
|
||
public function widget($args, $instance)
|
||
{
|
||
$subtitle = !empty($instance['subtitle']) ? $instance['subtitle'] : __('广告', 'kratos');
|
||
$image = !empty($instance['image']) ? $instance['image'] : '';
|
||
$url = !empty($instance['url']) ? $instance['url'] : '';
|
||
|
||
/* 🔴 原来这三处都是裸拼:
|
||
* '<a href="' . $url . '"...><img src="' . $image . '"><div class="prompt">' . $subtitle . '</div></a>'
|
||
* ① $url 没 esc_url —— 后台填个带引号的地址就能截断 href 属性;
|
||
* ② img 的 src 值**没有引号** —— 图片 URL 里只要有一个空格,
|
||
* 后半截就会被解析成另一个属性(常见于文件名带空格的媒体库图片);
|
||
* ③ $subtitle 没转义 —— 后台填 < 或 & 会破坏结构。
|
||
* 下面全部补齐;alt 取副标题,避免读屏软件只念文件名。 */
|
||
echo '<div class="' . esc_attr(yang_widget_class('w-ad')) . '">';
|
||
yang_widget_title($args, $instance, '', $this->id_base);
|
||
|
||
$imgAlt = wp_strip_all_tags($subtitle);
|
||
echo '<a href="' . esc_url($url) . '" target="_blank" rel="noreferrer noopener">'
|
||
. '<img src="' . esc_url($image) . '" alt="' . esc_attr($imgAlt) . '">'
|
||
. '<div class="prompt">' . esc_html($subtitle) . '</div>'
|
||
. '</a>';
|
||
echo '</div>';
|
||
}
|
||
|
||
public function update($new_instance, $old_instance)
|
||
{
|
||
$instance = array();
|
||
|
||
$instance['title'] = (!empty($new_instance['title'])) ? sanitize_text_field($new_instance['title']) : '';
|
||
$instance['subtitle'] = (!empty($new_instance['subtitle'])) ? sanitize_text_field($new_instance['subtitle']) : '';
|
||
$instance['image'] = (!empty($new_instance['image'])) ? esc_url_raw($new_instance['image']) : '';
|
||
$instance['url'] = (!empty($new_instance['url'])) ? esc_url_raw($new_instance['url']) : '';
|
||
|
||
return $instance;
|
||
}
|
||
|
||
public function form($instance)
|
||
{
|
||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||
$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('title'); ?>"><?php _e('标题:', 'kratos');?></label>
|
||
<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); ?>">
|
||
<span class="description"><?php _e('留空则不显示标题。', 'kratos');?></span>
|
||
</p>
|
||
<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()
|
||
{
|
||
$widget_ops = array(
|
||
'name' => __('个人简介', 'kratos'),
|
||
'description' => __('可跳转后台的个人简介展示工具', 'kratos'),
|
||
);
|
||
|
||
parent::__construct(false, false, $widget_ops);
|
||
}
|
||
|
||
/**
|
||
* 取挂件实例里的某个值;空则返回回退值。
|
||
*
|
||
* 🔴 为什么不用 `!empty()` 判定:
|
||
* ① 用户在后台把字段清空后,update() 存进去的是空字符串 '',
|
||
* `!empty('')` 为 false —— 这一条两边一致,没问题;
|
||
* ② 但 `!empty('0')` 也是 **false**!昵称就叫「0」、简介只写「0」
|
||
* 的站点会莫名其妙被回落到主题设置。判空只能判「去掉空白后是空串」。
|
||
*
|
||
* 3.2.51 起 widget_about 的四个字段都走这里,取值顺序统一为:
|
||
* 实例自己的值 → $fallback(主题设置 / 主题自带默认图)
|
||
*
|
||
* @param array $instance 挂件实例
|
||
* @param string $key 字段名
|
||
* @param string $fallback 实例里没填时用的值
|
||
* @return string
|
||
*/
|
||
private function pick($instance, $key, $fallback)
|
||
{
|
||
$v = isset($instance[$key]) ? $instance[$key] : '';
|
||
if (!is_string($v)) {
|
||
return $fallback;
|
||
}
|
||
|
||
$v = trim($v);
|
||
|
||
return ($v === '') ? $fallback : $v;
|
||
}
|
||
|
||
public function widget($args, $instance)
|
||
{
|
||
/* ══════════════════════════════════════════════════════════════
|
||
* 3.2.51:这三项原来**只读主题设置**,挂件表单里压根没有对应字段。
|
||
*
|
||
* 老代码长这样(拿掉注释后的实质):
|
||
* $introduce = kratos_option('a_about', ...);
|
||
* $username = kratos_option('a_nickname', ...);
|
||
* $avatar = yang_option_image('a_gravatar');
|
||
*
|
||
* 于是用户点开侧栏「个人简介」,表单里只有「背景图片」一个字段,
|
||
* 头像 / 昵称 / 简介**无处可填**,只能跑去「主题设置 → 站长信息」
|
||
* 改那份全站共用的。同一侧栏想放两个人的简介就不可能。
|
||
*
|
||
* 取值顺序(实例优先,留空回落):
|
||
* ① 挂件实例自己填的
|
||
* ② 主题设置「站长信息」—— **必须保留**:线上站点正用着
|
||
* 「老杨 / 博士,非博学之士…」那一套,改成"空了就空着"
|
||
* 会让所有老站点升级后侧栏突然变空白。
|
||
* ③ 代码里的兜底文案
|
||
* ══════════════════════════════════════════════════════════════ */
|
||
$introduce = $this->pick(
|
||
$instance,
|
||
'introduce',
|
||
kratos_option('a_about', __('保持饥渴的专注,追求最佳的品质', 'kratos'))
|
||
);
|
||
$username = $this->pick(
|
||
$instance,
|
||
'nickname',
|
||
kratos_option('a_nickname', __('Kratos', 'kratos'))
|
||
);
|
||
$avatar = $this->pick(
|
||
$instance,
|
||
'avatar',
|
||
yang_option_image('a_gravatar')
|
||
);
|
||
$background = $this->pick(
|
||
$instance,
|
||
'background',
|
||
ASSET_PATH . '/assets/img/about-background.png'
|
||
);
|
||
|
||
/* 🔴 原来写的是 `background:url(' . $background . ') no-repeat ...`
|
||
* —— url() 里的地址**没有引号**。只要路径含空格、括号、单引号,
|
||
* CSS 解析器就会在那里截断,整条声明作废(背景变白)。
|
||
* 媒体库文件名带空格很常见。这里补引号。
|
||
*
|
||
* 🔴 这里**只拼一次转义**,而且拼完还要整体 esc_attr()。
|
||
* 第一版写的是 esc_url($background) 再套 esc_attr($bg) ——
|
||
* esc_url() 已经做过 HTML 实体化,esc_attr() 又来一次 ⇒
|
||
* `&` 被编成 `&amp;`,CSS 里拿到的是字面量 "amp",
|
||
* 背景图直接 404。测试里那条「URL 完整」判据抓到的就是它。
|
||
*
|
||
* 正确做法:URL 只做**协议/字符清洗**(esc_url_raw,不入实体),
|
||
* url() 内部用双引号包住,最后整个 style 属性交给 esc_attr()
|
||
* 统一转义一次。这样 `&` 只会被编成 `&`(正确),
|
||
* 浏览器解析回属性值后仍是原始的 `&`。
|
||
*
|
||
* 🔴 $introduce / $username / $avatar 现在都可能来自**后台表单**
|
||
* (用户自己填的),原来是裸 echo。个人简介里写个 < 就把后面的
|
||
* 结构吃掉了。简介保留换行(textarea 的自然语义),其余全部转义。 */
|
||
$bg = 'background-image:url("' . esc_url_raw($background) . '");'
|
||
. 'background-repeat:no-repeat;'
|
||
. 'background-position:center center;'
|
||
. 'background-size:cover;';
|
||
|
||
echo '<div class="' . esc_attr(yang_widget_class('w-about')) . '">';
|
||
yang_widget_title($args, $instance, '', $this->id_base);
|
||
|
||
echo '<div class="background" style="' . esc_attr($bg) . '"></div><div class="wrapper text-center">';
|
||
if (current_user_can('manage_options')) {
|
||
echo '<a href="' . esc_url(admin_url()) . '">';
|
||
} else {
|
||
echo '<a href="' . esc_url(wp_login_url()) . '">';
|
||
}
|
||
echo '<img src="' . esc_url($avatar) . '" alt="' . esc_attr(wp_strip_all_tags($username)) . '"></a>';
|
||
echo '</div><div class="textwidget text-center">'
|
||
. '<p class="username">' . esc_html($username) . '</p>'
|
||
. '<p class="about">' . nl2br(esc_html($introduce)) . '</p>'
|
||
. '</div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
/**
|
||
* 保存实例。
|
||
*
|
||
* 🔴 不能用 `!empty($new_instance[...])` 判"用户填了没有":
|
||
* `!empty('0')` 是 **false** ⇒ 昵称就叫「0」、简介只写「0」的站点
|
||
* 保存后会变成空串、前台回落主题设置,而且**后台看起来像没保存成功**。
|
||
* 测试 `_about_test.php` 的「昵称『0』能存下来」抓到的就是这个。
|
||
* 统一用「转成字符串、trim 后是不是空串」判断,与 pick() 一致。
|
||
*/
|
||
public function update($new_instance, $old_instance)
|
||
{
|
||
$instance = array();
|
||
|
||
$instance['title'] = $this->clean_text($new_instance, 'title');
|
||
$instance['background'] = $this->clean_url($new_instance, 'background');
|
||
|
||
/* 3.2.51 新增三项。
|
||
*
|
||
* 🔴 简介是 textarea,**不能**用 sanitize_text_field:
|
||
* 那个函数会去掉所有换行,用户分段的简介会挤成一整段。
|
||
* 要用 sanitize_textarea_field(WP 4.7+ 才有)。
|
||
* 主题没声明最低 WP 版本,所以对更老的版本做个兜底 ——
|
||
* 兜底只做「去标签 + 规整换行」,不追求完全等价。
|
||
*
|
||
* 🔴 头像/背景是 URL,用 esc_url_raw(只清洗协议与字符),
|
||
* 不要在这里 esc_url —— 存进库的值必须是可以直接用的原地址。 */
|
||
$instance['avatar'] = $this->clean_url($new_instance, 'avatar');
|
||
$instance['nickname'] = $this->clean_text($new_instance, 'nickname');
|
||
$instance['introduce'] = $this->clean_textarea($new_instance, 'introduce');
|
||
|
||
return $instance;
|
||
}
|
||
|
||
/**
|
||
* 取一个新提交的字段;没提交或只有空白 → 空串。
|
||
*
|
||
* @param array $src 新提交的实例数组
|
||
* @param string $key 字段名
|
||
* @return string
|
||
*/
|
||
private function submitted($src, $key)
|
||
{
|
||
if (!isset($src[$key])) {
|
||
return '';
|
||
}
|
||
$v = $src[$key];
|
||
if (!is_string($v)) {
|
||
return '';
|
||
}
|
||
|
||
return trim($v);
|
||
}
|
||
|
||
private function clean_text($src, $key)
|
||
{
|
||
$v = $this->submitted($src, $key);
|
||
|
||
return $v === '' ? '' : sanitize_text_field($v);
|
||
}
|
||
|
||
private function clean_url($src, $key)
|
||
{
|
||
$v = $this->submitted($src, $key);
|
||
|
||
return $v === '' ? '' : esc_url_raw($v);
|
||
}
|
||
|
||
private function clean_textarea($src, $key)
|
||
{
|
||
$v = $this->submitted($src, $key);
|
||
if ($v === '') {
|
||
return '';
|
||
}
|
||
|
||
if (function_exists('sanitize_textarea_field')) {
|
||
return sanitize_textarea_field($v);
|
||
}
|
||
|
||
return trim(preg_replace('/\r\n|\r/', "\n", wp_strip_all_tags($v)));
|
||
}
|
||
|
||
public function form($instance)
|
||
{
|
||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||
|
||
/* 输入框里显示**用户自己填的原始值**(没填就是空),
|
||
* 预览图显示**前台实际会用的那张**(含回落)。
|
||
*
|
||
* 两者刻意分开:
|
||
* · 输入框若直接显示回落值,用户会以为"我已经填了",
|
||
* 保存后才发现其实存的是主题设置那份;
|
||
* · 预览图画的是最终效果,用户一眼能看出现在用的是哪张。 */
|
||
$avatarRaw = isset($instance['avatar']) ? trim((string) $instance['avatar']) : '';
|
||
$bgRaw = isset($instance['background']) ? trim((string) $instance['background']) : '';
|
||
$nicknameRaw = isset($instance['nickname']) ? trim((string) $instance['nickname']) : '';
|
||
$introduceRaw = isset($instance['introduce']) ? (string) $instance['introduce'] : '';
|
||
|
||
$avatarShown = $this->pick($instance, 'avatar', yang_option_image('a_gravatar'));
|
||
$bgShown = $this->pick($instance, 'background', ASSET_PATH . '/assets/img/about-background.png');
|
||
?>
|
||
<div class="media-widget-control">
|
||
<p>
|
||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php _e('标题:', 'kratos');?></label>
|
||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||
<span class="description"><?php _e('留空则不显示标题。', 'kratos');?></span>
|
||
</p>
|
||
|
||
<p>
|
||
<label for="<?php echo esc_attr($this->get_field_id('avatar')); ?>"><?php _e('个人头像:', 'kratos');?></label>
|
||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('avatar')); ?>" name="<?php echo esc_attr($this->get_field_name('avatar')); ?>" type="text" value="<?php echo esc_attr($avatarRaw); ?>">
|
||
<button type="button" class="button-update-media upload_avatar"><?php _e('选择图片', 'kratos');?></button>
|
||
<span class="description"><?php _e('留空则使用「主题设置 → 站长信息 → 个人头像」。', 'kratos');?></span>
|
||
<?php if ($avatarShown !== '') : ?>
|
||
<span class="yang-media-preview"><img src="<?php echo esc_url($avatarShown); ?>" alt=""></span>
|
||
<?php endif; ?>
|
||
</p>
|
||
|
||
<p>
|
||
<label for="<?php echo esc_attr($this->get_field_id('nickname')); ?>"><?php _e('昵称:', 'kratos');?></label>
|
||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('nickname')); ?>" name="<?php echo esc_attr($this->get_field_name('nickname')); ?>" type="text" value="<?php echo esc_attr($nicknameRaw); ?>" placeholder="<?php echo esc_attr(kratos_option('a_nickname', __('Kratos', 'kratos'))); ?>">
|
||
<span class="description"><?php _e('留空则使用「主题设置 → 站长信息 → 个人昵称」。', 'kratos');?></span>
|
||
</p>
|
||
|
||
<p>
|
||
<label for="<?php echo esc_attr($this->get_field_id('introduce')); ?>"><?php _e('个人简介:', 'kratos');?></label>
|
||
<textarea class="widefat" rows="4" id="<?php echo esc_attr($this->get_field_id('introduce')); ?>" name="<?php echo esc_attr($this->get_field_name('introduce')); ?>" placeholder="<?php echo esc_attr(kratos_option('a_about', __('保持饥渴的专注,追求最佳的品质', 'kratos'))); ?>"><?php echo esc_textarea($introduceRaw); ?></textarea>
|
||
<span class="description"><?php _e('支持换行。留空则使用「主题设置 → 站长信息 → 个人简介」。', 'kratos');?></span>
|
||
</p>
|
||
|
||
<p>
|
||
<label for="<?php echo esc_attr($this->get_field_id('background')); ?>"><?php _e('背景图片:', 'kratos');?></label>
|
||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('background')); ?>" name="<?php echo esc_attr($this->get_field_name('background')); ?>" type="text" value="<?php echo esc_attr($bgRaw); ?>">
|
||
<button type="button" class="button-update-media upload_background"><?php _e('选择图片', 'kratos');?></button>
|
||
<?php if ($bgRaw !== '') : ?>
|
||
<button type="button" class="button yang-media-clear" data-target="<?php echo esc_attr($this->get_field_id('background')); ?>"><?php _e('清除', 'kratos');?></button>
|
||
<?php endif; ?>
|
||
<span class="description"><?php _e('留空则使用主题自带的默认背景图。', 'kratos');?></span>
|
||
<?php if ($bgShown !== '') : ?>
|
||
<span class="yang-media-preview yang-media-preview-wide"><img src="<?php echo esc_url($bgShown); ?>" alt=""></span>
|
||
<?php endif; ?>
|
||
</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,
|
||
)
|
||
);
|
||
/* 🔴 标题原来是硬编码的 __('标签聚合'),后台没有输入框也改不了。
|
||
* 现在走实例的 title,没设才回落到「标签聚合」——
|
||
* 这样老站点的显示**一点不变**,但用户可以改了。 */
|
||
echo '<div class="' . esc_attr(yang_widget_class('w-tags')) . '">';
|
||
yang_widget_title($args, $instance, __('标签聚合', 'kratos'), $this->id_base);
|
||
echo '<div class="item">' . $tags . '</div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
public function update($new_instance, $old_instance)
|
||
{
|
||
$instance = array();
|
||
|
||
$instance['title'] = (!empty($new_instance['title'])) ? sanitize_text_field($new_instance['title']) : '';
|
||
$instance['number'] = (!empty($new_instance['number'])) ? (int) $new_instance['number'] : 8;
|
||
$instance['order'] = (!empty($new_instance['order'])) ? $new_instance['order'] : 'RAND';
|
||
|
||
return $instance;
|
||
}
|
||
|
||
public function form($instance)
|
||
{
|
||
global $wpdb;
|
||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||
$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('title'); ?>"><?php _e('标题:', 'kratos');?></label>
|
||
<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); ?>" placeholder="<?php echo esc_attr__('标签聚合', 'kratos'); ?>">
|
||
<span class="description"><?php _e('留空则仍显示「标签聚合」。', 'kratos');?></span>
|
||
</p>
|
||
<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;
|
||
}
|
||
|
||
/* 🔴 id 唯一化(原来全是写死的)。
|
||
* 老代码把同一套标签页导航写了两份 —— 桌面一份带图标(d-none d-xl-flex)、
|
||
* 手机一份不带图标(d-xl-none)—— 于是 nav-tab / nav-new-tab /
|
||
* nav-hot-tab / nav-random-tab **四个 id 各出现两次**,HTML 非法;
|
||
* `aria-labelledby="nav-new-tab"` 也只能指到第一份。
|
||
* 挂两个「文章聚合」实例时撞得更彻底。
|
||
*
|
||
* 现在只渲染**一份**导航:图标用 CSS 在窄屏隐藏(见 kratos.css 的
|
||
* `.w-recommended .tab-icon`)。这样 id 天然唯一,DOM 也少一半。 */
|
||
$uid = yang_widget_uid($this);
|
||
|
||
echo '<div class="' . esc_attr(yang_widget_class('w-recommended')) . '">';
|
||
yang_widget_title($args, $instance, '', $this->id_base);
|
||
?>
|
||
<div class="nav nav-tabs" id="<?php echo esc_attr($uid); ?>-tab" role="tablist">
|
||
<a class="nav-item nav-link active" id="<?php echo esc_attr($uid); ?>-new-tab" data-toggle="tab" href="#<?php echo esc_attr($uid); ?>-new" role="tab" aria-controls="<?php echo esc_attr($uid); ?>-new" aria-selected="true"><i class="kicon i-tabnew tab-icon"></i><?php _e('最新', 'kratos');?></a>
|
||
<a class="nav-item nav-link" id="<?php echo esc_attr($uid); ?>-hot-tab" data-toggle="tab" href="#<?php echo esc_attr($uid); ?>-hot" role="tab" aria-controls="<?php echo esc_attr($uid); ?>-hot" aria-selected="false"><i class="kicon i-tabhot tab-icon"></i><?php _e('热点', 'kratos');?></a>
|
||
<a class="nav-item nav-link" id="<?php echo esc_attr($uid); ?>-random-tab" data-toggle="tab" href="#<?php echo esc_attr($uid); ?>-random" role="tab" aria-controls="<?php echo esc_attr($uid); ?>-random" aria-selected="false"><i class="kicon i-tabrandom tab-icon"></i><?php _e('随机', 'kratos');?></a>
|
||
</div>
|
||
<div class="tab-content" id="<?php echo esc_attr($uid); ?>-tabContent">
|
||
<div class="tab-pane fade show active" id="<?php echo esc_attr($uid); ?>-new" role="tabpanel" aria-labelledby="<?php echo esc_attr($uid); ?>-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>
|
||
<?php endforeach;?>
|
||
</div>
|
||
<div class="tab-pane fade" id="<?php echo esc_attr($uid); ?>-hot" role="tabpanel" aria-labelledby="<?php echo esc_attr($uid); ?>-hot-tab">
|
||
<?php
|
||
/* 热点:全站累计热度(views/love/评论 三项 log 加权),
|
||
* 不再受「最近 N 天发布」的时间窗限制。原因见 most_comm_posts()
|
||
* 上方的注释块。 */
|
||
if (function_exists('most_comm_posts')) {
|
||
most_comm_posts(0, $number);
|
||
} ?>
|
||
</div>
|
||
<div class="tab-pane fade" id="<?php echo esc_attr($uid); ?>-random" role="tabpanel" aria-labelledby="<?php echo esc_attr($uid); ?>-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>
|
||
<?php endforeach;?>
|
||
</div>
|
||
</div>
|
||
<?php echo '</div>';
|
||
}
|
||
|
||
public function update($new_instance, $old_instance)
|
||
{
|
||
$instance = array();
|
||
|
||
$instance['title'] = (!empty($new_instance['title'])) ? sanitize_text_field($new_instance['title']) : '';
|
||
$instance['number'] = (!empty($new_instance['number'])) ? (int) $new_instance['number'] : 6;
|
||
|
||
/* `days` 已废弃:旧实现拿它当「最近 N 天发布」的时间窗,
|
||
* 那正是热点只显示一条的根因。这里不再保存,
|
||
* 老实例里残留的 days 值会在下次保存时被丢掉。 */
|
||
return $instance;
|
||
}
|
||
public function form($instance)
|
||
{
|
||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||
$number = !empty($instance['number']) ? $instance['number'] : '6';
|
||
?>
|
||
<div class="media-widget-control">
|
||
<p>
|
||
<label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('标题:', 'kratos');?></label>
|
||
<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); ?>">
|
||
<span class="description"><?php _e('留空则不显示标题。', 'kratos');?></span>
|
||
</p>
|
||
<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'); ?>
|
||
</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');
|