Files
kate522 ef718042d2 feat(admin): 新增后台文章列表销售数据列与快捷编辑配置项
「所有文章」列表此前只有互动数据列,缺少销售数据表头;快捷/批量编辑
展开后也只有原生三项。本次对照参考主题补齐这两处,不含专题链路。

- 新增 PostListEnhance:统管 sale/stats 两列、付费类型筛选、
  orderby=sales_volume 排序,以及四组快捷编辑字段的渲染与保存
- 新增 PostListEditFields:单独承载字段定义,主类职责保持单一
- 新增 admin-post-list.js:按 pay_type/pay_modo 勾选切换字段显隐
- 从 inc/core-hooks.php 迁出原有列实现(删 74 行),不再保留旧闭包
- 清理 admin-main.css 中确认无引用的 .aksrt-tab-pane 与 .aksrt-modal* 死块
- 新增 post-list-quick-edit-contract 测试,更新 stats/icon-picker 契约

快捷编辑锚点列用互动数据列而非参考主题的专题分类法列(本项目无专题)。
真实销量改为页面级批量查询,避免逐行查库。金额运算统一走 Ledger 的
「分」单位,不用裸浮点。保存走 edit_post 钩子,一次覆盖快捷与批量两条路径。
2026-10-06 15:54:47 +08:00

602 lines
23 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Theme Core Hooks
*
* Central registration for the theme's WordPress hooks.
*
* @package AKSRT
* @version 3.2.0
*/
defined('ABSPATH') || exit;
// ============================================================
// 评论功能开关
// ============================================================
add_filter('comments_open', function ($open, $post_id) {
if (class_exists('\AKSRT\Settings\Settings')) {
$display = \AKSRT\Settings\Settings::get_comment_display_settings();
if (!empty($display['close_comments'])) {
return false;
}
}
return $open;
}, 10, 2);
add_filter('comment_text', function ($comment_text, $comment = null, $args = []) {
if (function_exists('aksrt_render_comment_content')) {
return aksrt_render_comment_content((string) $comment_text);
}
return $comment_text;
}, 20, 3);
// ============================================================
// 文章预置参数:新建文章按区间随机填入浏览/点赞(参考主题 post_default_mate)
// ============================================================
add_action('save_post', static function ($post_id, $post = null): void {
$post_id = (int) $post_id;
if ($post_id <= 0 || wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
if (!($post instanceof \WP_Post) || $post->post_type !== 'post') {
return;
}
if (!class_exists('\AKSRT\Settings\Settings')) {
return;
}
$page = \AKSRT\Settings\Settings::get_post_page_settings();
$views_min = max(0, (int) ($page['default_views_min'] ?? 20));
$views_max = max($views_min, (int) ($page['default_views_max'] ?? 50));
$likes_min = max(0, (int) ($page['default_likes_min'] ?? 5));
$likes_max = max($likes_min, (int) ($page['default_likes_max'] ?? 15));
if ('' === (string) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true)) {
update_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, wp_rand($views_min, $views_max));
}
if ('' === (string) get_post_meta($post_id, '_aksrt_like_count', true)) {
update_post_meta($post_id, '_aksrt_like_count', wp_rand($likes_min, $likes_max));
}
}, 10, 2);
// ============================================================
// 前端隐藏 WordPress Admin Bar:由 SystemTools 按「扩展&增强 → 系统工具」
// 的 hide_admin_bar 设置门控(默认开启),不再无条件注入。
// ============================================================
// ============================================================
// SMTP 邮件配置注入
// ============================================================
add_filter('wp_mail_from', function ($from) {
if (!class_exists('\AKSRT\Settings\Settings')) return $from;
$mail = \AKSRT\Settings\Settings::get_smtp();
if (empty($mail['enabled'])) return $from;
$configured = sanitize_email((string) ($mail['from_address'] ?? ''));
return $configured !== '' ? $configured : $from;
});
add_filter('wp_mail_from_name', function ($from_name) {
if (!class_exists('\AKSRT\Settings\Settings')) return $from_name;
$mail = \AKSRT\Settings\Settings::get_smtp();
if (empty($mail['enabled'])) return $from_name;
$configured = trim((string) ($mail['from_name'] ?? ''));
return $configured !== '' ? $configured : $from_name;
});
add_action('phpmailer_init', function ($phpmailer) {
$mail = class_exists('\\AKSRT\\Settings\\Settings')
? \AKSRT\Settings\Settings::get_smtp()
: [];
if (empty($mail['enabled']) || empty($mail['host'])) return;
$phpmailer->isSMTP();
$phpmailer->Host = $mail['host'];
$phpmailer->Port = (int) ($mail['port'] ?? 465);
$phpmailer->SMTPAuth = !empty($mail['auth']) && !empty($mail['username']);
$phpmailer->Username = (string) ($mail['username'] ?? '');
$phpmailer->Password = (string) ($mail['password'] ?? '');
$phpmailer->SMTPSecure = (string) ($mail['encryption'] ?? 'ssl');
$phpmailer->Timeout = 10;
$phpmailer->Timelimit = 15;
});
// mail_title_prefix is applied by the canonical renderer and by third-party wp_mail callers.
add_filter('wp_mail', function ($atts) {
if (!class_exists('\AKSRT\Settings\Settings') || empty($atts['subject'])) return $atts;
$branding = \AKSRT\Settings\Settings::get_mail_branding();
$subject = (string) $atts['subject'];
$site = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
$subject = preg_replace('/^\[' . preg_quote($site, '/') . '\]\s*/u', '', $subject);
if ($branding['title_prefix'] !== '' && strpos($subject, $branding['title_prefix']) !== 0) {
$atts['subject'] = $branding['title_prefix'] . ' ' . $subject;
} else {
$atts['subject'] = $subject;
}
return $atts;
});
// ============================================================
// 分类 Term Meta 字段(封面图 + 推荐描述)
// ============================================================
/**
* 编辑分类页面 - 显示自定义字段
*/
function aksrt_category_meta_fields($term) {
$cover_id = (int) get_term_meta($term->term_id, 'aksrt_category_cover_id', true);
$cover_url = $cover_id > 0 ? wp_get_attachment_url($cover_id) : '';
$recommend_desc = (string) get_term_meta($term->term_id, 'aksrt_category_recommend_desc', true);
?>
<tr class="form-field">
<th scope="row"><label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label></th>
<td>
<div id="aksrt-cover-preview" style="margin-bottom:8px;">
<?php if ($cover_url) : ?>
<img src="<?php echo esc_url($cover_url); ?>" style="max-width:200px;height:auto;border-radius:6px;" alt="">
<?php endif; ?>
</div>
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="<?php echo esc_attr((string) $cover_id); ?>">
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
<button type="button" class="button" id="aksrt-remove-cover" <?php echo $cover_id > 0 ? '' : 'style="display:none;"'; ?>><?php esc_html_e('移除', 'aksrt'); ?></button>
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label></th>
<td>
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3" class="large-text"><?php echo esc_textarea($recommend_desc); ?></textarea>
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
</td>
</tr>
<?php
}
/**
* 新增分类页面 - 显示自定义字段
*/
function aksrt_category_meta_fields_add() {
?>
<div class="form-field">
<label for="aksrt_category_cover_id"><?php esc_html_e('分类封面图', 'aksrt'); ?></label>
<div id="aksrt-cover-preview" style="margin-bottom:8px;"></div>
<input type="hidden" id="aksrt_category_cover_id" name="aksrt_category_cover_id" value="">
<button type="button" class="button" id="aksrt-pick-cover"><?php esc_html_e('选择图片', 'aksrt'); ?></button>
<button type="button" class="button" id="aksrt-remove-cover" style="display:none;"><?php esc_html_e('移除', 'aksrt'); ?></button>
<p class="description"><?php esc_html_e('分类推荐模块的封面图片。', 'aksrt'); ?></p>
</div>
<div class="form-field">
<label for="aksrt_category_recommend_desc"><?php esc_html_e('推荐描述', 'aksrt'); ?></label>
<textarea id="aksrt_category_recommend_desc" name="aksrt_category_recommend_desc" rows="3"></textarea>
<p class="description"><?php esc_html_e('分类推荐模块中显示的描述文字。留空则使用分类默认描述。', 'aksrt'); ?></p>
</div>
<?php
}
add_action('category_edit_form_fields', 'aksrt_category_meta_fields', 10, 1);
add_action('category_add_form_fields', 'aksrt_category_meta_fields_add', 10, 1);
/**
* 保存分类 term meta
*/
function aksrt_save_category_meta(int $term_id) {
if (!current_user_can('manage_categories')) return;
$cover_id = isset($_POST['aksrt_category_cover_id']) ? absint($_POST['aksrt_category_cover_id']) : 0;
$recommend_desc = isset($_POST['aksrt_category_recommend_desc']) ? sanitize_textarea_field(wp_unslash($_POST['aksrt_category_recommend_desc'])) : '';
if ($cover_id > 0) {
update_term_meta($term_id, 'aksrt_category_cover_id', $cover_id);
} else {
delete_term_meta($term_id, 'aksrt_category_cover_id');
}
if ($recommend_desc !== '') {
update_term_meta($term_id, 'aksrt_category_recommend_desc', $recommend_desc);
} else {
delete_term_meta($term_id, 'aksrt_category_recommend_desc');
}
if (class_exists('\AKSRT\Settings\Settings')) {
\AKSRT\Settings\Settings::flush_category_recommend_cache();
}
}
add_action('edited_category', 'aksrt_save_category_meta', 10, 1);
add_action('created_category', 'aksrt_save_category_meta', 10, 1);
/**
* 分类编辑页加载媒体库脚本(wp_enqueue_media 仅在 AKSRT 面板加载,此处补上)
*/
add_action('admin_enqueue_scripts', function (string $hook_suffix) {
if ($hook_suffix === 'edit-tags.php' || $hook_suffix === 'term.php') {
wp_enqueue_media();
}
});
/**
* 分类编辑页内联 JS - 媒体选择器
*/
function aksrt_category_meta_admin_js() {
$screen = get_current_screen();
if (!$screen || $screen->taxonomy !== 'category') return;
?>
<script>
jQuery(function($) {
var frame;
$(document).on('click', '#aksrt-pick-cover', function(e) {
e.preventDefault();
if (typeof wp === 'undefined' || !wp.media) return;
if (frame) { frame.open(); return; }
frame = wp.media({
title: '<?php echo esc_js(__('选择分类封面图', 'aksrt')); ?>',
button: { text: '<?php echo esc_js(__('使用此图片', 'aksrt')); ?>' },
multiple: false
});
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
$('#aksrt_category_cover_id').val(attachment.id);
$('#aksrt-cover-preview').html('<img src="' + attachment.url + '" style="max-width:200px;height:auto;border-radius:6px;" alt="">');
$('#aksrt-remove-cover').show();
});
frame.open();
});
$(document).on('click', '#aksrt-remove-cover', function(e) {
e.preventDefault();
$('#aksrt_category_cover_id').val('');
$('#aksrt-cover-preview').html('');
$(this).hide();
});
});
</script>
<?php
}
add_action('admin_footer', 'aksrt_category_meta_admin_js');
// ============================================================
// 资源列表:/resources 虚拟归档(post + _aksrt_is_resource)
// ============================================================
add_action('init', function () {
add_rewrite_rule('^resources/?$', 'index.php?aksrt_is_resource_archive=1', 'top');
add_rewrite_rule('^resources/page/([0-9]+)/?$', 'index.php?aksrt_is_resource_archive=1&paged=$matches[1]', 'top');
// 旧单篇路径 /resource/{slug} → 301 到 post 永久链接
add_rewrite_rule('^resource/([^/]+)/?$', 'index.php?aksrt_legacy_resource=$matches[1]', 'top');
});
add_filter('query_vars', function ($vars) {
$vars[] = 'aksrt_is_resource_archive';
$vars[] = 'aksrt_legacy_resource';
$vars[] = 'type';
$vars[] = 'trem';
return $vars;
});
add_action('template_redirect', function () {
$slug = get_query_var('aksrt_legacy_resource');
if (!$slug) {
return;
}
$page = get_page_by_path((string) $slug, OBJECT, 'post');
if ($page) {
wp_redirect(get_permalink($page->ID), 301);
exit;
}
});
add_action('pre_get_posts', function ($query) {
if (is_admin() || !$query->is_main_query()) {
return;
}
if ((bool) $query->get('aksrt_is_resource_archive')) {
$query->set('post_type', 'post');
$query->set('post_status', 'publish');
$orderby = isset($_GET['orderby']) ? sanitize_key((string) $_GET['orderby']) : 'date';
if ($orderby === 'downloads') {
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY);
$query->set('orderby', 'meta_value_num');
$query->set('order', 'DESC');
} elseif ($orderby === 'views') {
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_VIEW_COUNT_KEY);
$query->set('orderby', 'meta_value_num');
$query->set('order', 'DESC');
}
$price_filter = isset($_GET['price']) ? sanitize_key((string) $_GET['price']) : '';
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : null;
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : null;
$meta_query = [
'relation' => 'AND',
[
'key' => \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY,
'value' => '1',
'compare' => '=',
],
];
if ($price_filter === 'free') {
$meta_query[] = [
'relation' => 'OR',
[
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => 0,
'compare' => '=',
'type' => 'NUMERIC',
],
[
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'compare' => 'NOT EXISTS',
],
];
} elseif ($price_filter === 'paid') {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
];
}
if ($price_min !== null || $price_max !== null) {
if ($price_min !== null && $price_max !== null) {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => [$price_min, $price_max],
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
];
} elseif ($price_min !== null) {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => $price_min,
'compare' => '>=',
'type' => 'NUMERIC',
];
} else {
$meta_query[] = [
'key' => \AKSRT\Core\DomainModel::PAY_PRICE_KEY,
'value' => $price_max,
'compare' => '<=',
'type' => 'NUMERIC',
];
}
}
$existing = $query->get('meta_query');
if (!is_array($existing)) {
$existing = [];
}
$query->set('meta_query', array_merge($existing, $meta_query));
$query->set('cat', 0);
$query->set('tag', '');
}
// 站内搜索:分类/标签名搜索(内容类型始终为 post)
if ($query->is_search() && empty($query->get('post_type'))) {
$search_type = sanitize_key((string) $query->get('type'));
$trem = absint($query->get('trem'));
if ($trem > 0) {
$query->set('cat', $trem);
}
if (in_array($search_type, ['category', 'tag'], true)) {
$taxonomy = $search_type === 'category' ? 'category' : 'post_tag';
$term_ids = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
'fields' => 'ids',
'number' => 20,
'search' => (string) $query->get('s'),
]);
$query->set('s', '');
if (is_wp_error($term_ids) || empty($term_ids)) {
$query->set('post__in', [0]);
} else {
$query->set('tax_query', [[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => array_map('absint', $term_ids),
]]);
}
}
}
});
add_filter('template_include', function ($template) {
if (!get_query_var('aksrt_is_resource_archive')) {
return $template;
}
$candidate = locate_template(['archive-resources.php']);
return $candidate ?: $template;
});
add_filter('document_title_parts', function ($parts) {
if (get_query_var('aksrt_is_resource_archive')) {
$parts['title'] = __('资源', 'aksrt');
}
return $parts;
});
// EnlighterJS 代码高亮 — 白名单 data-enlighter-* 属性
// ============================================================
add_filter('wp_kses_allowed_html', function ($allowed, $context) {
if ($context !== 'post') {
return $allowed;
}
// Allow data-enlighter-* attributes on <pre> and <code>
$enlighter_attrs = [
'data-enlighter-language' => true,
'data-enlighter-theme' => true,
'data-enlighter-group' => true,
'data-enlighter-title' => true,
'data-enlighter-linenumbers' => true,
'data-enlighter-highlight' => true,
'data-enlighter-lineoffset' => true,
];
if (isset($allowed['pre'])) {
$allowed['pre'] = array_merge($allowed['pre'], $enlighter_attrs);
}
if (isset($allowed['code'])) {
$allowed['code'] = array_merge($allowed['code'], $enlighter_attrs);
}
return $allowed;
}, 10, 2);
// ============================================================
// Gutenberg 自定义块注册
// ============================================================
// Register category server-side so it exists before JS registerBlockType runs.
// Keep untyped + defensive: some plugins pass unexpected shapes; typed params can 500 the editor.
add_filter('block_categories_all', static function ($categories, $context = null) {
if (!is_array($categories)) {
return $categories;
}
foreach ($categories as $category) {
$slug = '';
if (is_array($category)) {
$slug = (string) ($category['slug'] ?? '');
} elseif (is_object($category) && isset($category->slug)) {
$slug = (string) $category->slug;
}
if ($slug === 'aksrt-blocks') {
return $categories;
}
}
$categories[] = [
'slug' => 'aksrt-blocks',
'title' => 'AKSRT',
'icon' => null,
];
return $categories;
}, 10, 2);
add_action('enqueue_block_editor_assets', function () {
$base_uri = get_template_directory_uri();
$blocks_js = get_template_directory() . '/assets/js/gutenberg-blocks.js';
if (file_exists($blocks_js)) {
wp_enqueue_script(
'aksrt-gutenberg-blocks',
$base_uri . '/assets/js/gutenberg-blocks.js',
// wp-media-utils: MediaUpload for carousel block
['wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-media-utils', 'wp-data'],
(string) filemtime($blocks_js),
true
);
}
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
if (file_exists($blocks_css)) {
wp_enqueue_style(
'aksrt-gutenberg-blocks',
$base_uri . '/assets/css/gutenberg-blocks.css',
[],
(string) filemtime($blocks_css)
);
}
});
// Front-end styles for Gutenberg blocks
add_action('enqueue_block_assets', function () {
if (is_admin()) return; // Already loaded in editor
$posts = [];
$queried = get_queried_object();
if ($queried instanceof \WP_Post) {
$posts[$queried->ID] = $queried;
}
global $wp_query;
if ($wp_query instanceof \WP_Query) {
foreach ((array) $wp_query->posts as $post) {
if ($post instanceof \WP_Post) {
$posts[$post->ID] = $post;
}
}
}
$has_carousel = false;
foreach ($posts as $post) {
if (has_block('aksrt/carousel', $post)) {
$has_carousel = true;
break;
}
}
if (!$has_carousel) return;
$base_uri = get_template_directory_uri();
$blocks_css = get_template_directory() . '/assets/css/gutenberg-blocks.css';
if (file_exists($blocks_css)) {
wp_enqueue_style(
'aksrt-gutenberg-blocks-front',
$base_uri . '/assets/css/gutenberg-blocks.css',
[],
(string) filemtime($blocks_css)
);
}
});
// ============================================================
// Gutenberg Carousel Block 渲染回调
// ============================================================
add_action('init', function () {
if (!function_exists('register_block_type')) return;
register_block_type('aksrt/carousel', [
'render_callback' => function (array $attributes): string {
$images = $attributes['images'] ?? [];
if (empty($images)) return '';
$autoplay = !empty($attributes['autoplay']) ? 'true' : 'false';
$delay = absint($attributes['delay'] ?? 3000);
$effect = sanitize_text_field($attributes['effect'] ?? 'slide');
$slides = '';
foreach ($images as $img) {
if (empty($img['url'])) continue;
$alt = esc_attr($img['alt'] ?? '');
$url = esc_url($img['url']);
$slides .= '<div class="swiper-slide"><img' . aksrt_get_lazy_attr('lazy_sider', (string) $url, '', aksrt_get_lazy_thumb('lg')) . ' alt="' . $alt . '"></div>';
}
if (empty($slides)) return '';
return '<div class="aksrt-carousel-block swiper" data-autoplay="' . $autoplay . '" data-delay="' . $delay . '" data-effect="' . $effect . '">'
. '<div class="swiper-wrapper">' . $slides . '</div>'
. '<div class="swiper-pagination"></div>'
. '<div class="swiper-button-prev"></div><div class="swiper-button-next"></div>'
. '</div>';
},
]);
});
// ============================================================
// 付费内容系统:the_content 过滤器
// ============================================================
/**
* 未购买的图库/视频正文只保留公开预览;购买卡由 single 模板渲染。
*/
add_filter('the_content', 'aksrt_paid_content_filter', 20);