Files
wordpress-theme-aksrt/inc/classes/class-aksrt-post-meta-box.php
T
kate522 f197745830 feat(admin): deeper order/income filters, post pay-type, topic sales meta
Add order user/payment filters, income/rebate user_id filter, posts list
pay_type dropdown, topic virtual_sales/coupon meta, and forum unlock
facade methods used by hidecontent/payshow.
2026-07-15 11:35:35 +08:00

762 lines
38 KiB
PHP
Raw 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 Post Meta Box
*
* 为文章(post)添加局部配置字段:
* - 附加封面图组(用于列表多图模式)
* - 自定义摘要
* - 列表布局覆盖
* - 文章头图模式
* - 目录树开关
* - 版权声明覆盖
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Post_Meta_Box {
private static $instance = null;
const META_KEY = AKSRT_Domain_Model::POST_META_KEY;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post', [$this, 'save_meta_box']);
// §3.8 Quick / Bulk edit
add_filter('manage_post_posts_columns', [$this, 'add_columns']);
add_action('manage_post_posts_custom_column', [$this, 'render_column'], 10, 2);
add_action('quick_edit_custom_box', [$this, 'render_quick_edit'], 10, 2);
add_action('bulk_edit_custom_box', [$this, 'render_quick_edit'], 10, 2);
add_action('admin_footer-edit.php', [$this, 'admin_quick_edit_script']);
// Pay-type dropdown on posts list (zibll zibpay_type filter).
add_action('restrict_manage_posts', [$this, 'render_pay_type_filter']);
add_action('pre_get_posts', [$this, 'filter_posts_by_pay_type']);
}
/**
* Get post meta with defaults.
*/
public static function get_meta(int $post_id): array {
$defaults = AKSRT_Domain_Model::default_post_meta();
$saved = get_post_meta($post_id, self::META_KEY, true);
if (!is_array($saved)) {
$saved = [];
}
$meta = array_merge($defaults, $saved);
// New route only: episodes is always a list of {url,title}.
$meta['featured_video_episode'] = self::sanitize_video_episodes($meta['featured_video_episode'] ?? []);
if (!in_array((string) ($meta['show_toc'] ?? ''), ['', '0', '1'], true)) {
$meta['show_toc'] = '';
}
return $meta;
}
/**
* Sanitize video episode list (new route: array of url/title rows).
*
* @param mixed $raw Raw form / meta value.
* @return array<int, array{url:string,title:string}>
*/
public static function sanitize_video_episodes($raw): array {
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$out[] = [
'url' => $url,
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
];
if (count($out) >= 50) {
break;
}
}
return $out;
}
/**
* Get a single post meta value.
*/
public static function get_meta_value(int $post_id, string $key, $default = '') {
$meta = self::get_meta($post_id);
return $meta[$key] ?? $default;
}
/**
* Register the meta box.
*/
public function register_meta_box(): void {
add_meta_box(
'aksrt_post_meta',
__('AKSRT 文章设置', 'aksrt'),
[$this, 'render_meta_box'],
'post',
'side',
'default'
);
add_meta_box(
'aksrt_page_meta',
__('AKSRT 页面设置', 'aksrt'),
[$this, 'render_page_meta_box'],
'page',
'side',
'default'
);
// §3.8 custom filter metabox (side)
$filter_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_post_filter_settings()
: ['custom_filter' => []];
if (!empty($filter_settings['custom_filter'])) {
add_meta_box(
'aksrt_custom_filter_meta',
__('高级筛选', 'aksrt'),
[$this, 'render_custom_filter_meta_box'],
'post',
'side',
'default'
);
}
}
/**
* Render the page meta box (page_header_style / cover override).
*/
public function render_page_meta_box(WP_Post $post): void {
$header_style = get_post_meta($post->ID, 'page_header_style', true);
$cover_img = get_post_meta($post->ID, 'page_header_cover_img', true);
wp_nonce_field('aksrt_page_meta', 'aksrt_page_meta_nonce');
?>
<p><strong><?php esc_html_e('页面标题样式覆盖', 'aksrt'); ?></strong></p>
<select name="page_header_style" class="widefat">
<option value="" <?php selected($header_style, ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="not" <?php selected($header_style, 'not'); ?>><?php esc_html_e('不显示', 'aksrt'); ?></option>
<option value="1" <?php selected($header_style, '1'); ?>><?php esc_html_e('简单', 'aksrt'); ?></option>
<option value="2" <?php selected($header_style, '2'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="3" <?php selected($header_style, '3'); ?>><?php esc_html_e('封面', 'aksrt'); ?></option>
</select>
<small class="description"><?php esc_html_e('覆盖全局「其它页面 → 统一页头」设置。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('页面封面图覆盖', 'aksrt'); ?></strong></p>
<input type="text" name="page_header_cover_img" value="<?php echo esc_attr($cover_img); ?>" class="widefat" placeholder="<?php esc_attr_e('图片 URL', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('覆盖全局默认封面图,仅封面样式(样式3)生效。', 'aksrt'); ?></small>
<?php
}
/**
* Render the meta box.
*/
public function render_meta_box(WP_Post $post): void {
$meta = self::get_meta($post->ID);
wp_nonce_field('aksrt_post_meta', 'aksrt_post_meta_nonce');
?>
<p><strong><?php esc_html_e('附加封面图', 'aksrt'); ?></strong></p>
<p>
<input type="text" name="aksrt_post_meta[cover_images]" value="<?php echo esc_attr($meta['cover_images']); ?>" class="widefat" placeholder="<?php esc_attr_e('附件ID,逗号分隔', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('用于列表多图模式,填写附件 ID(逗号分隔)。', 'aksrt'); ?></small>
</p>
<p><strong><?php esc_html_e('列表布局覆盖', 'aksrt'); ?></strong></p>
<select name="aksrt_post_meta[layout_override]" class="widefat">
<option value="" <?php selected($meta['layout_override'], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="card" <?php selected($meta['layout_override'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list" <?php selected($meta['layout_override'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini" <?php selected($meta['layout_override'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
</select>
<p><strong><?php esc_html_e('侧栏布局(Zibll show_layout)', 'aksrt'); ?></strong></p>
<?php
$show_layout = (string) get_post_meta($post->ID, '_aksrt_show_layout', true);
if (!in_array($show_layout, ['no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$show_layout = '';
}
?>
<select name="aksrt_show_layout" class="widefat">
<option value="" <?php selected($show_layout, ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="no_sidebar" <?php selected($show_layout, 'no_sidebar'); ?>><?php esc_html_e('无侧栏', 'aksrt'); ?></option>
<option value="sidebar_right" <?php selected($show_layout, 'sidebar_right'); ?>><?php esc_html_e('右侧栏', 'aksrt'); ?></option>
<option value="sidebar_left" <?php selected($show_layout, 'sidebar_left'); ?>><?php esc_html_e('左侧栏', 'aksrt'); ?></option>
</select>
<p class="description"><?php esc_html_e('覆盖全局侧栏开关与左右位置,仅影响本文/本页。', 'aksrt'); ?></p>
<p><strong><?php esc_html_e('头图模式', 'aksrt'); ?></strong></p>
<select name="aksrt_post_meta[header_image_mode]" class="widefat">
<option value="" <?php selected($meta['header_image_mode'], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="featured" <?php selected($meta['header_image_mode'], 'featured'); ?>><?php esc_html_e('特色图片', 'aksrt'); ?></option>
<option value="auto" <?php selected($meta['header_image_mode'], 'auto'); ?>><?php esc_html_e('自动(内容首图)', 'aksrt'); ?></option>
<option value="none" <?php selected($meta['header_image_mode'], 'none'); ?>><?php esc_html_e('不显示', 'aksrt'); ?></option>
</select>
<p>
<label for="aksrt_show_toc"><strong><?php esc_html_e('目录树', 'aksrt'); ?></strong></label>
<select name="aksrt_post_meta[show_toc]" id="aksrt_show_toc" class="widefat">
<option value="" <?php selected((string) ($meta['show_toc'] ?? ''), ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1" <?php selected((string) ($meta['show_toc'] ?? ''), '1'); ?>><?php esc_html_e('强制开启', 'aksrt'); ?></option>
<option value="0" <?php selected((string) ($meta['show_toc'] ?? ''), '0'); ?>><?php esc_html_e('强制关闭', 'aksrt'); ?></option>
</select>
</p>
<p><strong><?php esc_html_e('单篇交互覆盖', 'aksrt'); ?></strong></p>
<?php
$toggle_fields = [
'show_reading_progress' => __('阅读进度条', 'aksrt'),
'enable_lightbox' => __('图片灯箱', 'aksrt'),
'show_related' => __('相关推荐', 'aksrt'),
'enable_like' => __('点赞按钮', 'aksrt'),
'enable_favorite' => __('收藏按钮', 'aksrt'),
'enable_share' => __('分享按钮', 'aksrt'),
'enable_print' => __('打印按钮', 'aksrt'),
'enable_pdf' => __('PDF 导出按钮', 'aksrt'),
];
foreach ($toggle_fields as $field_key => $field_label) :
?>
<p>
<label for="aksrt_post_meta_<?php echo esc_attr($field_key); ?>" class="screen-reader-text"><?php echo esc_html($field_label); ?></label>
<select name="aksrt_post_meta[<?php echo esc_attr($field_key); ?>]" id="aksrt_post_meta_<?php echo esc_attr($field_key); ?>" class="widefat">
<option value="" <?php selected($meta[$field_key], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1" <?php selected($meta[$field_key], '1'); ?>><?php esc_html_e('强制开启', 'aksrt'); ?></option>
<option value="0" <?php selected($meta[$field_key], '0'); ?>><?php esc_html_e('强制关闭', 'aksrt'); ?></option>
</select>
<small class="description"><?php echo esc_html($field_label); ?></small>
</p>
<?php endforeach; ?>
<p><strong><?php esc_html_e('相关推荐数量覆盖', 'aksrt'); ?></strong></p>
<p>
<input type="number" name="aksrt_post_meta[related_count]" value="<?php echo esc_attr((string) (int) $meta['related_count']); ?>" class="widefat" min="0" max="20" step="1">
<small class="description"><?php esc_html_e('0 表示跟随全局设置。', 'aksrt'); ?></small>
</p>
<p>
<label>
<input type="checkbox" name="aksrt_post_meta[is_featured]" value="1" <?php checked(!empty($meta['is_featured'])); ?>>
<?php esc_html_e('标记为推荐文章', 'aksrt'); ?>
</label>
</p>
<p><strong><?php esc_html_e('自定义摘要', 'aksrt'); ?></strong></p>
<textarea name="aksrt_post_meta[custom_excerpt]" rows="3" class="widefat" placeholder="<?php esc_attr_e('留空使用自动摘要', 'aksrt'); ?>"><?php echo esc_textarea($meta['custom_excerpt']); ?></textarea>
<p><strong><?php esc_html_e('版权声明覆盖', 'aksrt'); ?></strong></p>
<textarea name="aksrt_post_meta[copyright_text]" rows="3" class="widefat" placeholder="<?php esc_attr_e('留空使用默认版权文案', 'aksrt'); ?>"><?php echo esc_textarea($meta['copyright_text']); ?></textarea>
<hr>
<p><strong><?php esc_html_e('副标题', 'aksrt'); ?></strong></p>
<input type="text" name="aksrt_post_meta[subtitle]" value="<?php echo esc_attr($meta['subtitle']); ?>" class="widefat" placeholder="<?php esc_attr_e('可选,显示在标题下方', 'aksrt'); ?>">
<p><strong><?php esc_html_e('外链特色图', 'aksrt'); ?></strong></p>
<input type="url" name="aksrt_post_meta[thumbnail_url]" value="<?php echo esc_url($meta['thumbnail_url']); ?>" class="widefat" placeholder="<?php esc_attr_e('外部图片 URL(覆盖特色图片)', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('填入外部图片 URL 将替代特色图片。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('幻灯片封面', 'aksrt'); ?></strong></p>
<input type="text" name="aksrt_post_meta[featured_slide]" value="<?php echo esc_attr($meta['featured_slide']); ?>" class="widefat" placeholder="<?php esc_attr_e('图片 URL,逗号分隔', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('多张图片 URL 用逗号分隔,用于封面轮播。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('特色视频', 'aksrt'); ?></strong></p>
<input type="url" name="aksrt_post_meta[featured_video]" value="<?php echo esc_url($meta['featured_video']); ?>" class="widefat" placeholder="<?php esc_attr_e('第 1 集视频 URL(直链)', 'aksrt'); ?>">
<p><strong><?php esc_html_e('视频剧集列表', 'aksrt'); ?></strong></p>
<?php
$episodes = is_array($meta['featured_video_episode'] ?? null) ? $meta['featured_video_episode'] : [];
// Always show at least 3 editable rows for new-route authoring.
$rows = max(3, count($episodes));
for ($i = 0; $i < $rows; $i++) :
$ep = is_array($episodes[$i] ?? null) ? $episodes[$i] : [];
$ep_url = (string) ($ep['url'] ?? '');
$ep_title = (string) ($ep['title'] ?? '');
?>
<p class="aksrt-episode-row">
<input type="text" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][title]" value="<?php echo esc_attr($ep_title); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集标题(可选)', 'aksrt'), $i + 1)); ?>" style="margin-bottom:4px;">
<input type="url" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][url]" value="<?php echo esc_url($ep_url); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集视频 URL', 'aksrt'), $i + 1)); ?>">
</p>
<?php endfor; ?>
<small class="description"><?php esc_html_e('新路线:每行一集(标题 + URL)。留空 URL 的行会被忽略。第 1 集可与上方「特色视频」二选一填写。', 'aksrt'); ?></small>
<hr>
<p><strong><?php esc_html_e('手动计数覆盖', 'aksrt'); ?></strong></p>
<p>
<label><?php esc_html_e('阅读量', 'aksrt'); ?></label>
<input type="number" name="aksrt_post_meta[views]" value="<?php echo esc_attr((string) (int) $meta['views']); ?>" class="widefat" min="0" step="1">
</p>
<p>
<label><?php esc_html_e('点赞数', 'aksrt'); ?></label>
<input type="number" name="aksrt_post_meta[likes]" value="<?php echo esc_attr((string) (int) $meta['likes']); ?>" class="widefat" min="0" step="1">
</p>
<small class="description"><?php esc_html_e('0 表示不覆盖,使用系统统计值。', 'aksrt'); ?></small>
<?php
}
/**
* Render the custom filter metabox (per-dimension value selectors).
* §3.8 — stores values as individual post meta keys (_aksrt_cf_{dim_key}).
*/
public function render_custom_filter_meta_box(WP_Post $post): void {
wp_nonce_field('aksrt_custom_filter_meta', 'aksrt_custom_filter_meta_nonce');
$filter_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_post_filter_settings()
: ['custom_filter' => []];
$dimensions = $filter_settings['custom_filter'];
if (empty($dimensions)) {
echo '<p class="description">' . esc_html__('尚未定义筛选维度,请前往 文章设置 → 高级筛选 添加。', 'aksrt') . '</p>';
return;
}
foreach ($dimensions as $dim) :
$meta_key = '_aksrt_cf_' . $dim['key'];
$current = get_post_meta($post->ID, $meta_key, true);
?>
<p>
<label for="<?php echo esc_attr($meta_key); ?>"><strong><?php echo esc_html($dim['name']); ?></strong></label>
<select name="<?php echo esc_attr($meta_key); ?>" id="<?php echo esc_attr($meta_key); ?>" class="widefat">
<option value=""><?php esc_html_e('未选择', 'aksrt'); ?></option>
<?php foreach ($dim['vals'] as $val) : ?>
<option value="<?php echo esc_attr($val['key']); ?>" <?php selected($current, $val['key']); ?>><?php echo esc_html($val['name']); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php
endforeach;
}
/**
* Save meta box data.
*/
public function save_meta_box(int $post_id): void {
// Save page meta (page_header_style / page_header_cover_img)
if (isset($_POST['aksrt_page_meta_nonce']) && wp_verify_nonce($_POST['aksrt_page_meta_nonce'], 'aksrt_page_meta')) {
if (!defined('DOING_AUTOSAVE') && current_user_can('edit_post', $post_id)) {
$style = isset($_POST['page_header_style']) ? sanitize_key((string) $_POST['page_header_style']) : '';
if (!in_array($style, ['', 'not', '1', '2', '3'], true)) {
$style = '';
}
if ($style !== '') {
update_post_meta($post_id, 'page_header_style', $style);
} else {
delete_post_meta($post_id, 'page_header_style');
}
$cover = isset($_POST['page_header_cover_img']) ? esc_url_raw((string) $_POST['page_header_cover_img']) : '';
if ($cover !== '') {
update_post_meta($post_id, 'page_header_cover_img', $cover);
} else {
delete_post_meta($post_id, 'page_header_cover_img');
}
}
}
// Save post meta (aksrt_post_meta group)
if (!isset($_POST['aksrt_post_meta_nonce'])) return;
if (!wp_verify_nonce($_POST['aksrt_post_meta_nonce'], 'aksrt_post_meta')) return;
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
// Zibll show_layout (independent of list layout_override).
$show_layout = isset($_POST['aksrt_show_layout']) ? sanitize_key((string) wp_unslash($_POST['aksrt_show_layout'])) : '';
if (!in_array($show_layout, ['no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$show_layout = '';
}
if ($show_layout !== '') {
update_post_meta($post_id, '_aksrt_show_layout', $show_layout);
} else {
delete_post_meta($post_id, '_aksrt_show_layout');
}
// §3.8 Quick / Bulk edit — separate code path (merge mode)
if (!empty($_POST['aksrt_qe_flag'])) {
$this->save_quick_edit($post_id);
return;
}
$raw = $_POST['aksrt_post_meta'] ?? [];
$sanitized = [
'cover_images' => isset($raw['cover_images']) ? sanitize_text_field($raw['cover_images']) : '',
'custom_excerpt' => isset($raw['custom_excerpt']) ? sanitize_textarea_field($raw['custom_excerpt']) : '',
'layout_override' => isset($raw['layout_override']) ? sanitize_key($raw['layout_override']) : '',
'header_image_mode' => isset($raw['header_image_mode']) ? sanitize_key($raw['header_image_mode']) : '',
// New route: '' follow | '1' force on | '0' force off.
'show_toc' => isset($raw['show_toc']) && in_array((string) $raw['show_toc'], ['', '0', '1'], true) ? (string) $raw['show_toc'] : '',
'show_reading_progress' => isset($raw['show_reading_progress']) ? sanitize_key((string) $raw['show_reading_progress']) : '',
'enable_lightbox' => isset($raw['enable_lightbox']) ? sanitize_key((string) $raw['enable_lightbox']) : '',
'show_related' => isset($raw['show_related']) ? sanitize_key((string) $raw['show_related']) : '',
'related_count' => isset($raw['related_count']) ? (int) $raw['related_count'] : 0,
'enable_like' => isset($raw['enable_like']) ? sanitize_key((string) $raw['enable_like']) : '',
'enable_favorite' => isset($raw['enable_favorite']) ? sanitize_key((string) $raw['enable_favorite']) : '',
'enable_share' => isset($raw['enable_share']) ? sanitize_key((string) $raw['enable_share']) : '',
'enable_print' => isset($raw['enable_print']) ? sanitize_key((string) $raw['enable_print']) : '',
'enable_pdf' => isset($raw['enable_pdf']) ? sanitize_key((string) $raw['enable_pdf']) : '',
'copyright_text' => isset($raw['copyright_text']) ? sanitize_textarea_field($raw['copyright_text']) : '',
'is_featured' => !empty($raw['is_featured']),
// §3.8 文章扩展字段
'thumbnail_url' => isset($raw['thumbnail_url']) ? esc_url_raw((string) $raw['thumbnail_url']) : '',
'featured_slide' => isset($raw['featured_slide']) ? sanitize_text_field($raw['featured_slide']) : '',
'featured_video' => isset($raw['featured_video']) ? esc_url_raw((string) $raw['featured_video']) : '',
'featured_video_episode' => self::sanitize_video_episodes($raw['featured_video_episode'] ?? []),
'subtitle' => isset($raw['subtitle']) ? sanitize_text_field($raw['subtitle']) : '',
'views' => isset($raw['views']) ? max(0, (int) $raw['views']) : 0,
'likes' => isset($raw['likes']) ? max(0, (int) $raw['likes']) : 0,
'maxheight' => isset($raw['maxheight']) ? max(0, (int) $raw['maxheight']) : 0,
];
$valid_modes = ['', 'card', 'list', 'mini', 'grid-cms'];
if (!in_array($sanitized['layout_override'], $valid_modes, true)) {
$sanitized['layout_override'] = '';
}
$valid_header_modes = ['', 'featured', 'auto', 'none'];
if (!in_array($sanitized['header_image_mode'], $valid_header_modes, true)) {
$sanitized['header_image_mode'] = '';
}
foreach (['show_reading_progress', 'enable_lightbox', 'show_related', 'enable_like', 'enable_favorite', 'enable_share', 'enable_print', 'enable_pdf'] as $toggle_key) {
if (!in_array($sanitized[$toggle_key], ['', '0', '1'], true)) {
$sanitized[$toggle_key] = '';
}
}
$sanitized['related_count'] = max(0, min(20, (int) $sanitized['related_count']));
update_post_meta($post_id, self::META_KEY, $sanitized);
// §3.8 Save custom filter dimension values as individual post meta
if (isset($_POST['aksrt_custom_filter_meta_nonce']) && wp_verify_nonce($_POST['aksrt_custom_filter_meta_nonce'], 'aksrt_custom_filter_meta')) {
if (!defined('DOING_AUTOSAVE') && current_user_can('edit_post', $post_id)) {
$cf_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_post_filter_settings()
: ['custom_filter' => []];
foreach ($cf_settings['custom_filter'] as $dim) {
$meta_key = '_aksrt_cf_' . $dim['key'];
$value = isset($_POST[$meta_key]) ? sanitize_key((string) $_POST[$meta_key]) : '';
if ($value !== '') {
update_post_meta($post_id, $meta_key, $value);
} else {
delete_post_meta($post_id, $meta_key);
}
}
}
}
}
/**
* §3.8 Add custom columns to the posts list table.
*/
public function add_columns(array $columns): array {
$columns['aksrt_views'] = __('阅读量', 'aksrt');
$columns['aksrt_likes'] = __('点赞数', 'aksrt');
$columns['aksrt_sales'] = __('销售数据', 'aksrt');
return $columns;
}
/**
* §3.8 Render custom column content.
*/
public function render_column(string $column, int $post_id): void {
$meta = self::get_meta($post_id);
if ($column === 'aksrt_views') {
$views = (int) ($meta['views'] ?? 0);
echo esc_html($views > 0 ? (string) $views : '—');
} elseif ($column === 'aksrt_likes') {
$likes = (int) ($meta['likes'] ?? 0);
$layout = esc_attr($meta['layout_override'] ?? '');
$toc = esc_attr($meta['show_toc'] ?? '');
$mh = (int) ($meta['maxheight'] ?? 0);
echo '<span class="aksrt-col-data" data-views="' . (int) ($meta['views'] ?? 0) . '" data-likes="' . $likes . '" data-layout="' . $layout . '" data-toc="' . $toc . '" data-maxheight="' . $mh . '">' . esc_html($likes > 0 ? (string) $likes : '—') . '</span>';
} elseif ($column === 'aksrt_sales') {
$this->render_sales_column($post_id);
}
}
/**
* Content-pay sales summary for posts list (zibll 销售数据 column parity).
*/
private function render_sales_column(int $post_id): void {
if (!function_exists('aksrt_get_pay_meta') || !class_exists('AKSRT_Domain_Model')) {
echo '—';
return;
}
$pay = aksrt_get_pay_meta($post_id);
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
echo '<span class="description">' . esc_html__('免费', 'aksrt') . '</span>';
return;
}
$labels = AKSRT_Domain_Model::pay_type_labels();
$type_label = $labels[$pay_type] ?? $pay_type;
$modo = (($pay['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
if ($modo === 'points') {
$price_text = sprintf(__('%d 积分', 'aksrt'), (int) ceil((float) ($pay['points_price'] ?? $pay['price'] ?? 0)));
} else {
$price_text = '¥' . number_format((float) ($pay['price'] ?? 0), 2);
}
$meta_sales = (int) get_post_meta($post_id, AKSRT_Domain_Model::RESOURCE_SALES_VOLUME_KEY, true);
$db_sales = 0;
$revenue = 0.0;
if (class_exists('AKSRT_Order_Repository')) {
$map = AKSRT_Order_Repository::get_instance()->get_content_sales_map(
[$post_id],
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE
);
if (isset($map[$post_id])) {
$db_sales = (int) ($map[$post_id]['sales_count'] ?? 0);
$revenue = (float) ($map[$post_id]['revenue'] ?? 0);
}
}
$orders_url = admin_url('admin.php?page=aksrt_orders&product_type=resource&product_id=' . $post_id);
$products_url = admin_url('admin.php?page=aksrt_pay_products&s=' . $post_id);
echo '<div style="font-size:12px;line-height:1.5;">';
echo '<strong style="color:#0c9cc8;">' . esc_html((string) $type_label) . '</strong>';
echo '<br>' . esc_html($price_text);
echo '<br>' . esc_html(sprintf(__('Meta销量 %d', 'aksrt'), max(0, $meta_sales)));
if ($db_sales > 0) {
printf(
'<br><a href="%s" title="%s">%s</a>',
esc_url($orders_url),
esc_attr__('查看销售明细', 'aksrt'),
esc_html(sprintf(__('真实销量 %d · ¥%s', 'aksrt'), $db_sales, number_format($revenue, 2)))
);
} else {
echo '<br><span class="description">' . esc_html__('暂无订单', 'aksrt') . '</span>';
}
echo '<br><a href="' . esc_url($products_url) . '">' . esc_html__('付费文章', 'aksrt') . '</a>';
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
$dl_url = admin_url('admin.php?page=aksrt_download_logs&post_id=' . $post_id);
echo ' · <a href="' . esc_url($dl_url) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
}
echo '</div>';
}
/**
* Dropdown filter for content pay_type on edit.php?post_type=post.
*/
public function render_pay_type_filter(string $post_type): void {
if ($post_type !== 'post' || !class_exists('AKSRT_Domain_Model')) {
return;
}
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
$labels = AKSRT_Domain_Model::pay_type_labels();
echo '<select name="aksrt_pay_type">';
echo '<option value="">' . esc_html__('全部付费类型', 'aksrt') . '</option>';
foreach ($labels as $key => $label) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($key),
selected($current, $key, false),
esc_html($label)
);
}
echo '</select>';
}
/**
* Apply aksrt_pay_type filter via denormalized _aksrt_pay_type meta.
*/
public function filter_posts_by_pay_type(\WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
$screen_post_type = $query->get('post_type');
if ($screen_post_type !== 'post') {
return;
}
if (empty($_GET['aksrt_pay_type'])) {
return;
}
$pay_type = sanitize_key((string) wp_unslash($_GET['aksrt_pay_type']));
if ($pay_type === '' || !class_exists('AKSRT_Domain_Model')) {
return;
}
$pay_type = AKSRT_Domain_Model::normalize_pay_type($pay_type);
$meta_query = (array) $query->get('meta_query');
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
$meta_query[] = [
'relation' => 'OR',
[
'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
'value' => AKSRT_Domain_Model::PAY_TYPE_NO,
'compare' => '=',
],
[
'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
'compare' => 'NOT EXISTS',
],
];
} else {
$meta_query[] = [
'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
'value' => $pay_type,
'compare' => '=',
];
}
$query->set('meta_query', $meta_query);
}
/**
* §3.8 Render quick edit and bulk edit fields.
*/
public function render_quick_edit(string $column_name, string $post_type): void {
if ($column_name !== 'aksrt_views' || $post_type !== 'post') {
return;
}
wp_nonce_field('aksrt_post_meta', 'aksrt_post_meta_nonce_qe');
?>
<fieldset class="inline-edit-col-right" id="aksrt-quick-edit">
<div class="inline-edit-col">
<h4><?php esc_html_e('AKSRT 文章设置', 'aksrt'); ?></h4>
<input type="hidden" name="aksrt_qe_flag" value="1">
<label class="alignleft">
<span class="title"><?php esc_html_e('阅读量', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_views" min="0" step="1" value="" class="aksrt-qe-views">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('点赞数', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_likes" min="0" step="1" value="" class="aksrt-qe-likes">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('布局', 'aksrt'); ?></span>
<select name="aksrt_qe_layout" class="aksrt-qe-layout">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value=""><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="card"><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list"><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini"><?php esc_html_e('迷你', 'aksrt'); ?></option>
<option value="grid-cms"><?php esc_html_e('CMS网格', 'aksrt'); ?></option>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('目录树', 'aksrt'); ?></span>
<select name="aksrt_qe_toc" class="aksrt-qe-toc">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value=""><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1"><?php esc_html_e('显示', 'aksrt'); ?></option>
<option value="0"><?php esc_html_e('隐藏', 'aksrt'); ?></option>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('限高(px)', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_maxheight" min="0" step="100" value="" class="aksrt-qe-maxheight" placeholder="<?php esc_attr_e('0=不限', 'aksrt'); ?>">
</label>
<br class="clear">
</div>
</fieldset>
<?php
}
/**
* §3.8 Quick / Bulk edit merge-mode save.
* Only updates fields that were actually submitted (non-sentinel, non-empty).
* Existing meta values for omitted fields are preserved.
*/
private function save_quick_edit(int $post_id): void {
$existing = self::get_meta($post_id);
$views = isset($_POST['aksrt_qe_views']) && $_POST['aksrt_qe_views'] !== ''
? max(0, (int) $_POST['aksrt_qe_views'])
: (int) ($existing['views'] ?? 0);
$likes = isset($_POST['aksrt_qe_likes']) && $_POST['aksrt_qe_likes'] !== ''
? max(0, (int) $_POST['aksrt_qe_likes'])
: (int) ($existing['likes'] ?? 0);
$layout = $existing['layout_override'] ?? '';
if (isset($_POST['aksrt_qe_layout']) && $_POST['aksrt_qe_layout'] !== '-1') {
$layout = sanitize_key((string) $_POST['aksrt_qe_layout']);
if (!in_array($layout, ['', 'card', 'list', 'mini', 'grid-cms'], true)) {
$layout = $existing['layout_override'] ?? '';
}
}
$toc = $existing['show_toc'] ?? '';
if (isset($_POST['aksrt_qe_toc']) && $_POST['aksrt_qe_toc'] !== '-1') {
$toc_v = sanitize_key((string) $_POST['aksrt_qe_toc']);
$toc = in_array($toc_v, ['', '0', '1'], true) ? $toc_v : ($existing['show_toc'] ?? '');
}
$mh = isset($_POST['aksrt_qe_maxheight']) && $_POST['aksrt_qe_maxheight'] !== ''
? max(0, (int) $_POST['aksrt_qe_maxheight'])
: (int) ($existing['maxheight'] ?? 0);
$existing['views'] = $views;
$existing['likes'] = $likes;
$existing['layout_override'] = $layout;
$existing['show_toc'] = $toc;
$existing['maxheight'] = $mh;
update_post_meta($post_id, self::META_KEY, $existing);
}
/**
* §3.8 Output inline JS on edit.php to populate quick edit fields from column data attributes.
*/
public function admin_quick_edit_script(): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->base !== 'edit' || $screen->post_type !== 'post') {
return;
}
?>
<script>
(function () {
var wp_inline_edit = window.inlineEditPost ? window.inlineEditPost.edit : null;
if (!wp_inline_edit) return;
window.inlineEditPost.edit = function (id) {
wp_inline_edit.apply(this, arguments);
var postId = 0;
if (typeof id === 'object') {
postId = parseInt(this.getId(id), 10);
}
if (!postId) return;
var row = document.getElementById('post-' + postId);
if (!row) return;
var viewsEl = row.querySelector('.column-aksrt_views');
var likesEl = row.querySelector('.column-aksrt_likes');
var data = (likesEl && likesEl.querySelector('.aksrt-col-data'))
|| (viewsEl && viewsEl.querySelector('.aksrt-col-data'));
if (!data) return;
var form = document.getElementById('edit-' + postId);
if (!form) return;
var v = data.getAttribute('data-views') || '';
var l = data.getAttribute('data-likes') || '';
var layout = data.getAttribute('data-layout') || '';
var toc = data.getAttribute('data-toc') || '';
var mh = data.getAttribute('data-maxheight') || '';
var qv = form.querySelector('.aksrt-qe-views');
var ql = form.querySelector('.aksrt-qe-likes');
if (qv) qv.value = v;
if (ql) ql.value = l;
var qLayout = form.querySelector('.aksrt-qe-layout');
if (qLayout) qLayout.value = layout;
var qToc = form.querySelector('.aksrt-qe-toc');
if (qToc) qToc.value = toc;
var qMh = form.querySelector('.aksrt-qe-maxheight');
if (qMh) qMh.value = mh;
};
})();
</script>
<?php
}
}