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 钩子,一次覆盖快捷与批量两条路径。
This commit is contained in:
@@ -668,95 +668,6 @@ code {
|
|||||||
|
|
||||||
/**---------------------通知结束-------------**/
|
/**---------------------通知结束-------------**/
|
||||||
|
|
||||||
/**模态框------*/
|
|
||||||
.aksrt-modal-backdrop,
|
|
||||||
.aksrt-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100050;
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-backdrop {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in {
|
|
||||||
opacity: 1;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in .aksrt-modal-backdrop {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-dialog {
|
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
margin: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
transition: transform 0.5s cubic-bezier(0.32, 0.85, 0.45, 1.18), width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-dialog.modal-mini {
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal.in .aksrt-modal-dialog {
|
|
||||||
transform: scale(1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .close.abs-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .border-title {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-content {
|
|
||||||
max-height: calc(100vh - 20px);
|
|
||||||
overflow: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal-body {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .loading-mask {
|
|
||||||
z-index: 1;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-modal .hide-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 6px;
|
|
||||||
font-size: 25px;
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**---------批量编辑------------**/
|
/**---------批量编辑------------**/
|
||||||
.aksrt-bulk-edit-box {
|
.aksrt-bulk-edit-box {
|
||||||
margin: -1px;
|
margin: -1px;
|
||||||
@@ -774,19 +685,18 @@ code {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**---------------TAB-栏目------------**/
|
.aksrt-bulk-edit-box select,
|
||||||
.aksrt-tab-pane {
|
.aksrt-bulk-edit-box input[type="text"] {
|
||||||
display: none;
|
max-width: 100%;
|
||||||
opacity: 0;
|
margin-right: 4px;
|
||||||
transition: 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-tab-pane.active {
|
.inline-edit-row fieldset .aksrt-bulk-edit-box {
|
||||||
display: block;
|
min-width: 168px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-tab-pane.in {
|
.aksrt-bulk-edit-desc {
|
||||||
opacity: 1;
|
color: #787c82;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**---------------后台设置项目-修改或隐藏-----------**/
|
/**---------------后台设置项目-修改或隐藏-----------**/
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Post list quick-edit dependency toggling.
|
||||||
|
*
|
||||||
|
* The pay quick-edit box hides fields whose dependency rules no longer match
|
||||||
|
* the selected 付费模式 / 支付类型, mirroring the reference theme's field
|
||||||
|
* visibility. Runs on the WP inline-edit row when it is opened or changed.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function readControl(row, fieldId) {
|
||||||
|
var input = row.querySelector(
|
||||||
|
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"],' +
|
||||||
|
' [name="aksrt_bulk_edit[pay][' + fieldId + '][operation]"]'
|
||||||
|
);
|
||||||
|
if (!input) return null;
|
||||||
|
if (input.tagName === 'SELECT') return input.value;
|
||||||
|
var checked = row.querySelector(
|
||||||
|
'[name="aksrt_bulk_edit[pay][' + fieldId + ']"]:checked'
|
||||||
|
);
|
||||||
|
if (input.type === 'radio' && checked) return checked.value;
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(actual, operator, expected) {
|
||||||
|
if (actual === null) return true;
|
||||||
|
if (operator === '==') return actual === expected;
|
||||||
|
if (operator === '!=') return actual !== expected;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRow(row) {
|
||||||
|
var boxes = row.querySelectorAll('.aksrt-bulk-edit-box[data-dependency]');
|
||||||
|
Array.prototype.forEach.call(boxes, function (box) {
|
||||||
|
var rules;
|
||||||
|
try {
|
||||||
|
rules = JSON.parse(box.getAttribute('data-dependency'));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(rules)) return;
|
||||||
|
|
||||||
|
var visible = rules.every(function (rule) {
|
||||||
|
return compare(readControl(row, rule[0]), rule[1], String(rule[2]));
|
||||||
|
});
|
||||||
|
box.style.display = visible ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRows() {
|
||||||
|
var rows = document.querySelectorAll('.inline-edit-row, #bulk-edit-row');
|
||||||
|
Array.prototype.forEach.call(rows, function (row) {
|
||||||
|
if (row.querySelector('[data-dependency]')) updateRow(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('change', function (event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.name || target.name.indexOf('aksrt_bulk_edit[pay]') !== 0) return;
|
||||||
|
var row = target.closest('.inline-edit-row, #bulk-edit-row');
|
||||||
|
if (row) updateRow(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WP swaps the inline-edit row without a change event.
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.closest) return;
|
||||||
|
if (target.closest('.editinline, .bulk-action')) window.setTimeout(syncRows, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
syncRows();
|
||||||
|
})();
|
||||||
@@ -413,6 +413,11 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
|
|||||||
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
|
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后台文章列表增强:互动/销售数据列 + 快捷编辑配置项
|
||||||
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\PostListEnhance')) {
|
||||||
|
\AKSRT\Admin\Native\PostListEnhance::get_instance()->init();
|
||||||
|
}
|
||||||
|
|
||||||
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
|
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
|
||||||
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
|
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
|
||||||
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
|
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
|
||||||
|
|||||||
@@ -459,80 +459,6 @@ add_filter('wp_kses_allowed_html', function ($allowed, $context) {
|
|||||||
return $allowed;
|
return $allowed;
|
||||||
}, 10, 2);
|
}, 10, 2);
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// WP 后台文章列表增强列 — 浏览/点赞/收藏/下载
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
add_filter('manage_post_posts_columns', function ($columns) {
|
|
||||||
$new_columns = [];
|
|
||||||
foreach ($columns as $key => $label) {
|
|
||||||
$new_columns[$key] = $label;
|
|
||||||
if ($key === 'title') {
|
|
||||||
$new_columns['aksrt_stats'] = __('互动数据', 'aksrt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $new_columns;
|
|
||||||
});
|
|
||||||
|
|
||||||
add_action('admin_enqueue_scripts', function ($hook) {
|
|
||||||
if ($hook !== 'edit.php') return;
|
|
||||||
aksrt_enqueue_fontawesome();
|
|
||||||
});
|
|
||||||
|
|
||||||
add_action('manage_post_posts_custom_column', function ($column, $post_id) {
|
|
||||||
if ($column !== 'aksrt_stats') return;
|
|
||||||
|
|
||||||
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
|
|
||||||
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : (int) get_post_meta($post_id, '_aksrt_like_count', true);
|
|
||||||
$favorites = function_exists('aksrt_get_post_favorites') ? (int) aksrt_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
|
|
||||||
$downloads = (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY, true);
|
|
||||||
|
|
||||||
if ($downloads <= 0) {
|
|
||||||
$legacy_downloads = (int) get_post_meta($post_id, 'download_count', true);
|
|
||||||
if ($legacy_downloads > 0) {
|
|
||||||
$downloads = $legacy_downloads;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$format = function (int $n): string {
|
|
||||||
if (function_exists('aksrt_format_number')) {
|
|
||||||
return (string) aksrt_format_number($n);
|
|
||||||
}
|
|
||||||
if ($n >= 10000) return round($n / 10000, 1) . 'W+';
|
|
||||||
if ($n >= 1000) return round($n / 1000, 1) . 'K';
|
|
||||||
return (string) $n;
|
|
||||||
};
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
|
||||||
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
|
||||||
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
|
||||||
|
|
||||||
if ($downloads > 0) {
|
|
||||||
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo implode(' · ', $parts);
|
|
||||||
}, 10, 2);
|
|
||||||
|
|
||||||
// Support sorting by views/likes/favorites in admin
|
|
||||||
add_action('pre_get_posts', function ($query) {
|
|
||||||
if (!is_admin() || !$query->is_main_query()) return;
|
|
||||||
|
|
||||||
$orderby = $query->get('orderby');
|
|
||||||
$allowed = [
|
|
||||||
'views' => \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY,
|
|
||||||
'like' => '_aksrt_like_count',
|
|
||||||
'favorite' => '_aksrt_favorite_count',
|
|
||||||
'download_count' => \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($allowed[$orderby])) {
|
|
||||||
$query->set('meta_key', $allowed[$orderby]);
|
|
||||||
$query->set('orderby', 'meta_value_num');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Gutenberg 自定义块注册
|
// Gutenberg 自定义块注册
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Quick-edit field definitions for the admin post list.
|
||||||
|
*
|
||||||
|
* Column registration, rendering and saving live in PostListEnhance; this file
|
||||||
|
* only describes the field groups so the two concerns stay separately readable.
|
||||||
|
* Every key here must exist in DomainModel::default_pay_meta(),
|
||||||
|
* DomainModel::default_post_meta() or the custom-filter settings — the
|
||||||
|
* post-list-quick-edit contract test enforces that pairing.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class PostListEditFields {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interaction counts + article display config (saved to post meta).
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function extended(): array {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => 'views',
|
||||||
|
'type' => 'number',
|
||||||
|
'title' => __('阅读量', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'like',
|
||||||
|
'type' => 'number',
|
||||||
|
'title' => __('点赞数', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'show_layout',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('显示布局', 'aksrt'),
|
||||||
|
'default' => 'false',
|
||||||
|
'options' => [
|
||||||
|
'false' => __('跟随主题', 'aksrt'),
|
||||||
|
'no_sidebar' => __('无侧边栏', 'aksrt'),
|
||||||
|
'sidebar_left' => __('侧边栏靠左', 'aksrt'),
|
||||||
|
'sidebar_right' => __('侧边栏靠右', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'no_article-navs',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('不显示目录树', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'article_maxheight_xz',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('限制内容最大高度', 'aksrt'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay parameters (saved into the _aksrt_pay meta bag).
|
||||||
|
*
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function pay(string $currency): array {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'id' => 'pay_type',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('付费模式', 'aksrt'),
|
||||||
|
'options' => \AKSRT\Core\DomainModel::pay_type_labels(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'pay_limit',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('购买权限', 'aksrt'),
|
||||||
|
'options' => [
|
||||||
|
'0' => __('所有人可购买', 'aksrt'),
|
||||||
|
'1' => __('VIP1 及以上会员可购买', 'aksrt'),
|
||||||
|
'2' => __('仅 VIP2 可购买', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'pay_modo',
|
||||||
|
'type' => 'radio',
|
||||||
|
'title' => __('支付类型', 'aksrt'),
|
||||||
|
'options' => [
|
||||||
|
'0' => __('普通商品(金钱购买)', 'aksrt'),
|
||||||
|
'points' => __('积分商品(积分购买,依赖于用户积分功能)', 'aksrt'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'id' => 'points_price',
|
||||||
|
'title' => __('积分售价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'title' => __('VIP1 积分售价', 'aksrt'),
|
||||||
|
'id' => 'vip1_points',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '==', 'points']],
|
||||||
|
'title' => __('VIP2 积分售价', 'aksrt'),
|
||||||
|
'id' => 'vip2_points',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
|
||||||
|
'desc' => __('会员价格不能高于售价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => __('积分', 'aksrt'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'id' => 'price',
|
||||||
|
'title' => __('执行价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'id' => 'original_price',
|
||||||
|
'title' => __('原价', 'aksrt'),
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('显示在执行价格前面,并划掉', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'title' => __('VIP1 价格', 'aksrt'),
|
||||||
|
'id' => 'vip1_price',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP1 免费', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no'], ['pay_modo', '!=', 'points']],
|
||||||
|
'title' => __('VIP2 价格', 'aksrt'),
|
||||||
|
'id' => 'vip2_price',
|
||||||
|
'class' => 'compact',
|
||||||
|
'subtitle' => __('填 0 则为 VIP2 免费', 'aksrt'),
|
||||||
|
'desc' => __('会员价格不能高于执行价', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
'unit' => $currency,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'pay_count',
|
||||||
|
'title' => __('销量浮动', 'aksrt'),
|
||||||
|
'subtitle' => __('为真实销量增加或减少的数量', 'aksrt'),
|
||||||
|
'type' => 'number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'show_sales',
|
||||||
|
'title' => __('显示销量', 'aksrt'),
|
||||||
|
'default' => true,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'coupon_s',
|
||||||
|
'title' => __('允许使用优惠码', 'aksrt'),
|
||||||
|
'default' => false,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dependency' => [['pay_type', '!=', 'no']],
|
||||||
|
'id' => 'order_expired_s',
|
||||||
|
'title' => __('订单时效', 'aksrt'),
|
||||||
|
'default' => false,
|
||||||
|
'type' => 'switcher',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_expired_time',
|
||||||
|
'title' => __('订单时效有效期数值', 'aksrt'),
|
||||||
|
'default' => 0,
|
||||||
|
'type' => 'number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => __('订单时效有效期单位', 'aksrt'),
|
||||||
|
'type' => 'select',
|
||||||
|
'class' => 'compact',
|
||||||
|
'id' => 'order_expired_unit',
|
||||||
|
'options' => [
|
||||||
|
'day' => __('天', 'aksrt'),
|
||||||
|
'hour' => __('小时', 'aksrt'),
|
||||||
|
'minute' => __('分钟', 'aksrt'),
|
||||||
|
],
|
||||||
|
'default' => 'day',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,791 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin post list enhancements: interaction + sales columns and the
|
||||||
|
* quick/bulk edit fields behind them.
|
||||||
|
*
|
||||||
|
* Mirrors the reference theme's post list: a sortable “销售数据” column, a
|
||||||
|
* product-type filter, and four quick-edit field groups (interaction data,
|
||||||
|
* article config, pay parameters, custom filters). The reference theme hooks
|
||||||
|
* those boxes onto its topics taxonomy column; this project has no topics
|
||||||
|
* taxonomy, so they hang off the interaction-data column instead.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin\Native;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class PostListEnhance {
|
||||||
|
|
||||||
|
public const COLUMN_STATS = 'aksrt_stats';
|
||||||
|
public const COLUMN_SALES = 'aksrt_sales';
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/** Real sales counts for the current page, keyed by post ID. @var array<int,int>|null */
|
||||||
|
private $sales_map = null;
|
||||||
|
|
||||||
|
private function __construct() {}
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_filter('manage_post_posts_columns', [$this, 'register_columns']);
|
||||||
|
add_action('manage_post_posts_custom_column', [$this, 'render_column'], 10, 2);
|
||||||
|
add_filter('manage_edit-post_sortable_columns', [$this, 'register_sortable_columns']);
|
||||||
|
add_action('restrict_manage_posts', [$this, 'render_type_filter']);
|
||||||
|
add_action('pre_get_posts', [$this, 'apply_query_args'], 99);
|
||||||
|
|
||||||
|
add_action('quick_edit_custom_box', [$this, 'render_edit_boxes'], 50, 2);
|
||||||
|
add_action('bulk_edit_custom_box', [$this, 'render_edit_boxes'], 50, 2);
|
||||||
|
add_action('edit_post', [$this, 'save_edit_boxes'], 10, 2);
|
||||||
|
|
||||||
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Assets
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public function enqueue_assets(string $hook): void {
|
||||||
|
if ($hook !== 'edit.php') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
||||||
|
if (!$screen || $screen->post_type !== 'post' || $screen->base !== 'edit') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('aksrt_enqueue_fontawesome')) {
|
||||||
|
aksrt_enqueue_fontawesome();
|
||||||
|
}
|
||||||
|
|
||||||
|
$css = AKSRT_DIR . '/assets/css/admin-main.css';
|
||||||
|
if (is_file($css)) {
|
||||||
|
wp_enqueue_style('aksrt-admin-main', AKSRT_URI . '/assets/css/admin-main.css', ['aksrt-font-awesome'], (string) filemtime($css));
|
||||||
|
}
|
||||||
|
$js = AKSRT_DIR . '/assets/js/admin-post-list.js';
|
||||||
|
if (is_file($js)) {
|
||||||
|
wp_enqueue_script('aksrt-post-list', AKSRT_URI . '/assets/js/admin-post-list.js', [], (string) filemtime($js), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Columns
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert the interaction and sales columns after the title column.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $columns
|
||||||
|
* @return array<string,string>
|
||||||
|
*/
|
||||||
|
public function register_columns($columns) {
|
||||||
|
if (!is_array($columns)) {
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = $this->current_order();
|
||||||
|
$order_by = $this->current_orderby();
|
||||||
|
$o_icon = '<i class="opacity5 ml3 fa fa-long-arrow-' . ($order === 'asc' ? 'down' : 'up') . '"></i>';
|
||||||
|
|
||||||
|
$new_columns = [];
|
||||||
|
foreach ($columns as $key => $label) {
|
||||||
|
$new_columns[$key] = $label;
|
||||||
|
if ($key !== 'title') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$new_columns[self::COLUMN_STATS] = __('互动数据', 'aksrt');
|
||||||
|
$new_columns[self::COLUMN_SALES] = '<a href="' . esc_url(add_query_arg(['orderby' => 'sales_volume', 'order' => $order])) . '"><span>' . esc_html__('销售数据', 'aksrt') . ($order_by === 'sales_volume' ? $o_icon : '') . '</span></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $columns
|
||||||
|
* @return array<string,string>
|
||||||
|
*/
|
||||||
|
public function register_sortable_columns($columns) {
|
||||||
|
if (!is_array($columns)) {
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
$columns[self::COLUMN_STATS] = 'views';
|
||||||
|
$columns[self::COLUMN_SALES] = 'sales_volume';
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $column
|
||||||
|
* @param int $post_id
|
||||||
|
*/
|
||||||
|
public function render_column($column, $post_id): void {
|
||||||
|
if ((string) $column === self::COLUMN_STATS) {
|
||||||
|
$this->render_stats_column((int) $post_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((string) $column === self::COLUMN_SALES) {
|
||||||
|
$this->render_sales_column((int) $post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_stats_column(int $post_id): void {
|
||||||
|
$views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true);
|
||||||
|
$likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : (int) get_post_meta($post_id, '_aksrt_like_count', true);
|
||||||
|
$favorites = function_exists('aksrt_get_post_favorites') ? (int) aksrt_get_post_favorites($post_id) : (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
|
||||||
|
$downloads = (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY, true);
|
||||||
|
if ($downloads <= 0) {
|
||||||
|
$legacy_downloads = (int) get_post_meta($post_id, 'download_count', true);
|
||||||
|
if ($legacy_downloads > 0) {
|
||||||
|
$downloads = $legacy_downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = static function (int $n): string {
|
||||||
|
if (function_exists('aksrt_format_number')) {
|
||||||
|
return (string) aksrt_format_number($n);
|
||||||
|
}
|
||||||
|
if ($n >= 10000) {
|
||||||
|
return round($n / 10000, 1) . 'W+';
|
||||||
|
}
|
||||||
|
if ($n >= 1000) {
|
||||||
|
return round($n / 1000, 1) . 'K';
|
||||||
|
}
|
||||||
|
return (string) $n;
|
||||||
|
};
|
||||||
|
|
||||||
|
$parts = [];
|
||||||
|
$parts[] = '<span title="' . esc_attr__('浏览', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-eye" style="color:#8a909c;font-size:12px;"></i> ' . esc_html($format($views)) . '</span>';
|
||||||
|
$parts[] = '<span title="' . esc_attr__('点赞', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-thumbs-up" style="color:#1890ff;font-size:12px;"></i> ' . esc_html($format($likes)) . '</span>';
|
||||||
|
$parts[] = '<span title="' . esc_attr__('收藏', 'aksrt') . '" style="margin-right:4px;"><i class="fa fas fa-star" style="color:#f39c12;font-size:12px;"></i> ' . esc_html($format($favorites)) . '</span>';
|
||||||
|
|
||||||
|
if ($downloads > 0) {
|
||||||
|
$parts[] = '<span title="' . esc_attr__('下载', 'aksrt') . '"><i class="fa fas fa-download" style="color:#52c41a;font-size:12px;"></i> ' . esc_html($format($downloads)) . '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo implode(' · ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_sales_column(int $post_id): void {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
|
||||||
|
$pay_type = (string) ($pay_meta['pay_type'] ?? '');
|
||||||
|
if ($pay_type === '' || $pay_type === \AKSRT\Core\DomainModel::PAY_TYPE_NO) {
|
||||||
|
echo '<div style="font-size: 12px;">' . esc_html__('未开启付费', 'aksrt') . '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_modo = (string) ($pay_meta['pay_modo'] ?? '0');
|
||||||
|
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
|
$type_label = $type_labels[$pay_type] ?? $pay_type;
|
||||||
|
$type_color = $this->pay_type_color($pay_type);
|
||||||
|
|
||||||
|
if ($pay_modo === 'points') {
|
||||||
|
$price_text = sprintf(__('积分:%s', 'aksrt'), (string) max(0, (int) ($pay_meta['points_price'] ?? 0)));
|
||||||
|
} else {
|
||||||
|
$price_text = $this->currency_symbol() . ':' . (string) ($pay_meta['price'] ?? '0.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sales_volume = max(0, (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true));
|
||||||
|
$real_sales = $this->real_sales_count($post_id);
|
||||||
|
$real_html = $real_sales > 0
|
||||||
|
? ' <a title="' . esc_attr__('点击查看销售明细', 'aksrt') . '" href="' . esc_url($this->orders_url($post_id)) . '">[' . esc_html(sprintf(__('真实销量%s', 'aksrt'), $real_sales)) . ']</a>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$link = add_query_arg(['aksrt_pay_type' => $pay_type]);
|
||||||
|
echo '<div>';
|
||||||
|
echo '<a style="color: ' . esc_attr($type_color) . ';" href="' . esc_url($link) . '">' . esc_html($type_label) . '</a>';
|
||||||
|
echo '<div style="font-size: 12px;">' . esc_html($price_text) . ' · ' . esc_html__('销量', 'aksrt') . esc_html((string) $sales_volume) . wp_kses_post($real_html) . '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color palette for the pay-type link, mirroring the reference theme.
|
||||||
|
*/
|
||||||
|
private function pay_type_color(string $pay_type): string {
|
||||||
|
$palette = [
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_READING => '#ff4747',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD => '#ee5307',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_GALLERY => '#1e8608',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_VIDEO => '#086ae8',
|
||||||
|
\AKSRT\Core\DomainModel::PAY_TYPE_VIP => '#3353fd',
|
||||||
|
];
|
||||||
|
return $palette[$pay_type] ?? '#464646';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Filter + query
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public function render_type_filter(string $post_type): void {
|
||||||
|
if ($post_type !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
|
||||||
|
$labels = \AKSRT\Core\DomainModel::pay_type_labels();
|
||||||
|
unset($labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
|
||||||
|
|
||||||
|
echo '<select name="aksrt_pay_type">';
|
||||||
|
echo '<option value="">' . esc_html__('商品类型', 'aksrt') . '</option>';
|
||||||
|
foreach ($labels as $key => $label) {
|
||||||
|
printf('<option %s value="%s">%s</option>', selected($current, $key, false), esc_attr($key), esc_html($label));
|
||||||
|
}
|
||||||
|
echo '</select>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the header orderby links and the product-type filter onto the query.
|
||||||
|
*/
|
||||||
|
public function apply_query_args($query): void {
|
||||||
|
if (!$query instanceof \WP_Query || !is_admin() || !$query->is_main_query()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($_GET['post_type']) || sanitize_key((string) wp_unslash($_GET['post_type'])) !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : '';
|
||||||
|
if ($orderby === 'sales_volume') {
|
||||||
|
$query->set('meta_key', \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY);
|
||||||
|
$query->set('orderby', 'meta_value_num');
|
||||||
|
} elseif ($orderby === 'views' || $orderby === 'like' || $orderby === 'favorite' || $orderby === 'download_count') {
|
||||||
|
$query->set('meta_key', $this->stats_meta_key($orderby));
|
||||||
|
$query->set('orderby', 'meta_value_num');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_type = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
|
||||||
|
if ($pay_type !== '' && isset(\AKSRT\Core\DomainModel::pay_type_labels()[$pay_type])) {
|
||||||
|
$meta_query = $query->get('meta_query');
|
||||||
|
$meta_query = is_array($meta_query) ? $meta_query : [];
|
||||||
|
$meta_query[] = [
|
||||||
|
'relation' => 'OR',
|
||||||
|
[
|
||||||
|
'key' => \AKSRT\Core\DomainModel::PAY_TYPE_KEY,
|
||||||
|
'value' => $pay_type,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => \AKSRT\Core\DomainModel::PAY_META_KEY,
|
||||||
|
'value' => '"pay_type";s:' . strlen($pay_type) . ':"' . $pay_type . '"',
|
||||||
|
'compare' => 'LIKE',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$query->set('meta_query', $meta_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public orderby key → stored meta key for the interaction column.
|
||||||
|
*/
|
||||||
|
private function stats_meta_key(string $orderby): string {
|
||||||
|
$map = [
|
||||||
|
'views' => \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY,
|
||||||
|
'like' => '_aksrt_like_count',
|
||||||
|
'favorite' => '_aksrt_favorite_count',
|
||||||
|
'download_count' => \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY,
|
||||||
|
];
|
||||||
|
return $map[$orderby] ?? \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Quick / bulk edit boxes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $column_name
|
||||||
|
* @param string $post_type
|
||||||
|
*/
|
||||||
|
public function render_edit_boxes($column_name, $post_type): void {
|
||||||
|
if ($post_type !== 'post' || (string) $column_name !== self::COLUMN_STATS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo wp_kses_post($this->edit_boxes_html());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function edit_boxes_html(): string {
|
||||||
|
$html = $this->fields_html(PostListEditFields::extended(), 'extended');
|
||||||
|
$html .= $this->fields_html(PostListEditFields::pay($this->currency_symbol()), 'pay', __('付费参数', 'aksrt'), '<div class="c-yellow">' . esc_html__('批量设置付费参数,请仔细检查配置内容,更新后无法撤回。请注意配置逻辑,例如会员价不能高于普通价,执行价不能高于原价等', 'aksrt') . '</div>');
|
||||||
|
$html .= $this->filter_fields_html();
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom-filter dimensions come from settings, so they are rendered
|
||||||
|
* separately from the static field lists.
|
||||||
|
*/
|
||||||
|
private function filter_fields_html(): string {
|
||||||
|
$dimensions = [];
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
|
||||||
|
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
|
||||||
|
}
|
||||||
|
if (!$dimensions) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = [];
|
||||||
|
foreach ($dimensions as $dimension) {
|
||||||
|
$key = sanitize_key((string) ($dimension['key'] ?? ''));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$options = [];
|
||||||
|
foreach ((array) ($dimension['vals'] ?? []) as $value) {
|
||||||
|
$value_key = sanitize_key((string) ($value['key'] ?? ''));
|
||||||
|
if ($value_key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$options[$value_key] = (string) ($value['name'] ?? $value_key);
|
||||||
|
}
|
||||||
|
if (!$options) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fields[] = [
|
||||||
|
'id' => $key,
|
||||||
|
'title' => (string) ($dimension['name'] ?? $key),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'options' => $options,
|
||||||
|
'placeholder' => sprintf(__('请选择%s', 'aksrt'), (string) ($dimension['name'] ?? $key)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$fields) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fields_html($fields, 'filter', __('高级筛选', 'aksrt'), __('让文章实现更加精细化的分类,同时方便用户进行文章筛选', 'aksrt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render one field group: the shared builder for every quick-edit box.
|
||||||
|
*
|
||||||
|
* @param array<int,array<string,mixed>> $fields
|
||||||
|
*/
|
||||||
|
private function fields_html(array $fields, string $id, string $title = '', string $desc = ''): string {
|
||||||
|
$item_html = '';
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$options_html = ' ';
|
||||||
|
switch ($field['type']) {
|
||||||
|
case 'number':
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = '<div class="flex ac hh">'
|
||||||
|
. '<select name="' . esc_attr($name) . '[operation]">'
|
||||||
|
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
|
||||||
|
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
|
||||||
|
. '<option value="plus">' . esc_html__('加', 'aksrt') . '</option>'
|
||||||
|
. '<option value="subtract">' . esc_html__('减', 'aksrt') . '</option>'
|
||||||
|
. '<option value="multiply">' . esc_html__('乘', 'aksrt') . '</option>'
|
||||||
|
. '<option value="division">' . esc_html__('除', 'aksrt') . '</option>'
|
||||||
|
. '</select>'
|
||||||
|
. '<input type="text" name="' . esc_attr($name) . '[val]" value="">'
|
||||||
|
. '</div>';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
$select_options = !empty($field['options']) ? $field['options'] : [];
|
||||||
|
if ($select_options) {
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = $this->checkbox_options_html($name, $select_options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$field['options'] = [
|
||||||
|
'ignore' => __('不更改', 'aksrt'),
|
||||||
|
'1' => __('开启', 'aksrt'),
|
||||||
|
'0' => __('关闭', 'aksrt'),
|
||||||
|
];
|
||||||
|
// Falls through to the switcher renderer below.
|
||||||
|
|
||||||
|
case 'switcher':
|
||||||
|
$field['options'] = [
|
||||||
|
'ignore' => __('不更改', 'aksrt'),
|
||||||
|
'1' => __('开启', 'aksrt'),
|
||||||
|
'0' => __('关闭', 'aksrt'),
|
||||||
|
];
|
||||||
|
// Falls through to the select renderer below.
|
||||||
|
|
||||||
|
case 'radio':
|
||||||
|
case 'select':
|
||||||
|
$options = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];
|
||||||
|
$name = 'aksrt_bulk_edit[' . $id . '][' . $field['id'] . ']';
|
||||||
|
$options_html = '<div class="flex ac hh">'
|
||||||
|
. '<select name="' . esc_attr($name) . '">'
|
||||||
|
. '<option selected="selected" value="ignore">--' . esc_html__('不更改', 'aksrt') . '--</option>';
|
||||||
|
foreach ($options as $key => $value) {
|
||||||
|
$options_html .= '<option value="' . esc_attr((string) $key) . '">' . esc_html((string) $value) . '</option>';
|
||||||
|
}
|
||||||
|
$options_html .= '</select></div>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($field['id']) || $options_html === ' ') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title_html = !empty($field['title']) ? (string) $field['title'] : (!empty($field['label']) ? (string) $field['label'] : '');
|
||||||
|
$desc_html = !empty($field['desc']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['desc']) . '</div>' : '';
|
||||||
|
$subtitle = !empty($field['subtitle']) ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html((string) $field['subtitle']) . '</div>' : '';
|
||||||
|
$dependency = !empty($field['dependency']) ? ' data-dependency="' . esc_attr(wp_json_encode($field['dependency'])) . '"' : '';
|
||||||
|
|
||||||
|
$item_html .= '<div class="aksrt-bulk-edit-box"' . $dependency . '>'
|
||||||
|
. '<div>' . esc_html($title_html) . '</div>'
|
||||||
|
. $options_html . $subtitle . $desc_html
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($item_html === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$legend = $title !== '' ? '<legend class="inline-edit-legend">' . esc_html($title) . '</legend>' : '';
|
||||||
|
$desc = $desc !== '' ? '<div class="px12 aksrt-bulk-edit-desc">' . esc_html($desc) . '</div>' : '';
|
||||||
|
|
||||||
|
return '<fieldset style="margin-top: 20px">' . $legend . '<div class="flex hh">' . $item_html . '</div>' . $desc . '</fieldset>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $options
|
||||||
|
*/
|
||||||
|
private function checkbox_options_html(string $name, array $options): string {
|
||||||
|
$html = '<select class="mr10" name="' . esc_attr($name) . '[operation]">'
|
||||||
|
. '<option value="ignore" selected="selected">' . esc_html__('不更改', 'aksrt') . '</option>'
|
||||||
|
. '<option value="set">' . esc_html__('设置为', 'aksrt') . '</option>'
|
||||||
|
. '</select>';
|
||||||
|
|
||||||
|
foreach ($options as $key => $value) {
|
||||||
|
$html .= '<div class="mr10"><label><input value="' . esc_attr((string) $key) . '" type="checkbox" name="' . esc_attr($name) . '[val][]"> ' . esc_html((string) $value) . '</label></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="flex ac hh">' . $html . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Save
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist quick/bulk edit submissions for the current post.
|
||||||
|
*
|
||||||
|
* @param int $post_id
|
||||||
|
* @param \WP_Post $post
|
||||||
|
*/
|
||||||
|
public function save_edit_boxes(int $post_id, $post): void {
|
||||||
|
if (!$post instanceof \WP_Post || $post->post_type !== 'post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wp_is_post_revision($post_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!current_user_can('edit_post', $post_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$screen = isset($_POST['screen']) ? sanitize_key((string) wp_unslash($_POST['screen'])) : '';
|
||||||
|
if ($screen !== 'edit-post') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$submitted = isset($_POST['aksrt_bulk_edit']) && is_array($_POST['aksrt_bulk_edit']) ? wp_unslash($_POST['aksrt_bulk_edit']) : [];
|
||||||
|
if (!$submitted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($submitted['extended'])) {
|
||||||
|
$this->save_extended($post_id, (array) $submitted['extended']);
|
||||||
|
}
|
||||||
|
if (!empty($submitted['pay'])) {
|
||||||
|
$this->save_pay($post_id, (array) $submitted['pay']);
|
||||||
|
}
|
||||||
|
if (!empty($submitted['filter'])) {
|
||||||
|
$this->save_filters($post_id, (array) $submitted['filter']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_extended(int $post_id, array $fields): void {
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
switch ($field_id) {
|
||||||
|
case 'views':
|
||||||
|
case 'like':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$value = (float) $field_value['val'];
|
||||||
|
$old = (float) get_post_meta($post_id, $this->stats_meta_key($field_id), true);
|
||||||
|
$new = $this->apply_operation($old, $value, $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
update_post_meta($post_id, $this->stats_meta_key($field_id), max(0, (int) $new));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'show_layout':
|
||||||
|
case 'no_article-navs':
|
||||||
|
case 'article_maxheight_xz':
|
||||||
|
$value = is_scalar($field_value) ? (string) $field_value : 'ignore';
|
||||||
|
if ($value === 'ignore' || $value === '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($field_id === 'show_layout') {
|
||||||
|
$value = in_array($value, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true) ? $value : 'false';
|
||||||
|
} else {
|
||||||
|
$value = in_array($value, ['1', '0'], true) ? $value : '0';
|
||||||
|
}
|
||||||
|
$this->update_post_meta_bag($post_id, $field_id, $value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_pay(int $post_id, array $fields): void {
|
||||||
|
$pay_meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : [];
|
||||||
|
if (!is_array($pay_meta) || $pay_meta === []) {
|
||||||
|
$pay_meta = \AKSRT\Core\DomainModel::default_pay_meta();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
switch ($field_id) {
|
||||||
|
case 'pay_type':
|
||||||
|
case 'pay_limit':
|
||||||
|
case 'pay_modo':
|
||||||
|
if (is_scalar($field_value) && (string) $field_value !== 'ignore') {
|
||||||
|
$pay_meta[$field_id] = sanitize_key((string) $field_value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'coupon_s':
|
||||||
|
case 'order_expired_s':
|
||||||
|
case 'show_sales':
|
||||||
|
if (is_scalar($field_value) && in_array((string) $field_value, ['1', '0'], true)) {
|
||||||
|
$pay_meta[$field_id] = (string) $field_value === '1';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'order_expired_unit':
|
||||||
|
if (is_scalar($field_value) && in_array((string) $field_value, ['day', 'hour', 'minute'], true)) {
|
||||||
|
$pay_meta[$field_id] = (string) $field_value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'points_price':
|
||||||
|
case 'vip1_points':
|
||||||
|
case 'vip2_points':
|
||||||
|
case 'order_expired_time':
|
||||||
|
case 'pay_count':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$current = (float) ($pay_meta[$field_id] ?? 0);
|
||||||
|
$new = $this->apply_operation($current, (float) $field_value['val'], $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
$pay_meta[$field_id] = max(0, (int) $new);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'price':
|
||||||
|
case 'original_price':
|
||||||
|
case 'vip1_price':
|
||||||
|
case 'vip2_price':
|
||||||
|
if (!is_array($field_value) || empty($field_value['operation']) || empty($field_value['val']) || !is_numeric($field_value['val'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$operation = sanitize_key((string) $field_value['operation']);
|
||||||
|
if ($operation === 'ignore') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$current = (string) ($pay_meta[$field_id] ?? '0.00');
|
||||||
|
try {
|
||||||
|
$current_minor = (float) \AKSRT\Ledger\AssetAmount::balance_minor($current);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$current_minor = 0.0;
|
||||||
|
}
|
||||||
|
$new = $this->apply_operation($current_minor, (float) $field_value['val'], $operation);
|
||||||
|
if ($new !== null) {
|
||||||
|
$pay_meta[$field_id] = \AKSRT\Ledger\AssetAmount::decimal_from_minor(max(0, (int) round($new)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pay_meta = \AKSRT\Core\DomainModel::sanitize_pay_meta($pay_meta);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_META_KEY, $pay_meta);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::PAY_PRICE_KEY, $pay_meta['pay_modo'] === 'points' ? (int) $pay_meta['points_price'] : (string) $pay_meta['price']);
|
||||||
|
update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']);
|
||||||
|
update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']);
|
||||||
|
update_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_MARKER_KEY, \AKSRT\Core\DomainModel::is_resource_pay_type((string) $pay_meta['pay_type']) ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $fields
|
||||||
|
*/
|
||||||
|
private function save_filters(int $post_id, array $fields): void {
|
||||||
|
$dimensions = [];
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
|
||||||
|
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
|
||||||
|
}
|
||||||
|
if (!$dimensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_dimensions = [];
|
||||||
|
foreach ($dimensions as $dimension) {
|
||||||
|
$key = sanitize_key((string) ($dimension['key'] ?? ''));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$allowed_dimensions[$key] = array_values(array_filter(array_map(static function ($value): string {
|
||||||
|
return sanitize_key((string) ($value['key'] ?? ''));
|
||||||
|
}, (array) ($dimension['vals'] ?? []))));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field_id => $field_value) {
|
||||||
|
$field_id = sanitize_key((string) $field_id);
|
||||||
|
if (!isset($allowed_dimensions[$field_id]) || !is_array($field_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty($field_value['operation']) || sanitize_key((string) $field_value['operation']) !== 'set') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$values = isset($field_value['val']) && is_array($field_value['val'])
|
||||||
|
? array_map('sanitize_key', $field_value['val'])
|
||||||
|
: [];
|
||||||
|
$values = array_values(array_intersect($allowed_dimensions[$field_id], $values));
|
||||||
|
if ($values) {
|
||||||
|
update_post_meta($post_id, '_aksrt_cf_' . $field_id, $values);
|
||||||
|
} else {
|
||||||
|
delete_post_meta($post_id, '_aksrt_cf_' . $field_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge one field into the post-meta bag shared with the article extension
|
||||||
|
* meta box.
|
||||||
|
*/
|
||||||
|
private function update_post_meta_bag(int $post_id, string $key, string $value): void {
|
||||||
|
$bag_key = \AKSRT\Core\PostMetaBox::META_KEY;
|
||||||
|
$bag = get_post_meta($post_id, $bag_key, true);
|
||||||
|
$bag = is_array($bag) ? $bag : [];
|
||||||
|
$bag[$key] = $value;
|
||||||
|
update_post_meta($post_id, $bag_key, $bag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an operation, returning null when it must be ignored.
|
||||||
|
*/
|
||||||
|
private function apply_operation(float $current, float $value, string $operation): ?float {
|
||||||
|
switch ($operation) {
|
||||||
|
case 'set':
|
||||||
|
return $value;
|
||||||
|
case 'plus':
|
||||||
|
return round($current + $value, 2);
|
||||||
|
case 'subtract':
|
||||||
|
return round($current - $value, 2);
|
||||||
|
case 'multiply':
|
||||||
|
return round($current * $value, 2);
|
||||||
|
case 'division':
|
||||||
|
return ($current != 0 && $value != 0) ? round($current / $value, 2) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured cash symbol, shared by the sales column and the pay fields.
|
||||||
|
*/
|
||||||
|
private function currency_symbol(): string {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||||
|
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
|
||||||
|
return (string) ($paid['display']['currency_symbol'] ?? '¥');
|
||||||
|
}
|
||||||
|
return '¥';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function current_order(): string {
|
||||||
|
$order = isset($_REQUEST['order']) ? sanitize_key((string) wp_unslash($_REQUEST['order'])) : 'desc';
|
||||||
|
return $order === 'asc' ? 'asc' : 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function current_orderby(): string {
|
||||||
|
return isset($_REQUEST['orderby']) ? sanitize_key((string) wp_unslash($_REQUEST['orderby'])) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real paid orders for one post, batched across the current page.
|
||||||
|
*/
|
||||||
|
private function real_sales_count(int $post_id): int {
|
||||||
|
if ($this->sales_map === null) {
|
||||||
|
$this->sales_map = $this->load_sales_map();
|
||||||
|
}
|
||||||
|
return (int) ($this->sales_map[$post_id] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,int>
|
||||||
|
*/
|
||||||
|
private function load_sales_map(): array {
|
||||||
|
global $wp_query;
|
||||||
|
if (!$wp_query instanceof \WP_Query || empty($wp_query->posts)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = [];
|
||||||
|
foreach ($wp_query->posts as $post) {
|
||||||
|
if ($post instanceof \WP_Post) {
|
||||||
|
$ids[] = (int) $post->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$ids) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
|
||||||
|
$ids,
|
||||||
|
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
|
||||||
|
);
|
||||||
|
$out = [];
|
||||||
|
foreach ($map as $pid => $row) {
|
||||||
|
$out[(int) $pid] = (int) ($row['sales_count'] ?? 0);
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function orders_url(int $post_id): string {
|
||||||
|
return add_query_arg(
|
||||||
|
[
|
||||||
|
'page' => 'aksrt_orders',
|
||||||
|
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
|
||||||
|
'product_id' => $post_id,
|
||||||
|
],
|
||||||
|
admin_url('admin.php')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,61 +6,89 @@ const path = require('node:path')
|
|||||||
const root = path.resolve(__dirname, '../..')
|
const root = path.resolve(__dirname, '../..')
|
||||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
|
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
|
||||||
|
|
||||||
const hooks = read('inc/core-hooks.php')
|
const list = read('src/Admin/Native/PostListEnhance.php')
|
||||||
const domain = read('src/Core/DomainModel.php')
|
const domain = read('src/Core/DomainModel.php')
|
||||||
const tags = read('inc/helpers/aksrt-frontend-tags.php')
|
const tags = read('inc/helpers/aksrt-frontend-tags.php')
|
||||||
|
const pay = read('inc/helpers/aksrt-pay.php')
|
||||||
|
|
||||||
test('admin post list has the interaction stats column', () => {
|
test('admin post list has the interaction stats column', () => {
|
||||||
assert.match(hooks, /manage_post_posts_columns/)
|
assert.match(list, /manage_post_posts_columns/)
|
||||||
assert.match(hooks, /互动数据/)
|
assert.match(list, /互动数据/)
|
||||||
assert.match(hooks, /manage_post_posts_custom_column/)
|
assert.match(list, /manage_post_posts_custom_column/)
|
||||||
assert.match(hooks, /\$column !== 'aksrt_stats'/)
|
assert.match(list, /COLUMN_STATS/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('admin post list has the sales data column with a sortable header', () => {
|
||||||
|
// 表头「销售数据」必须是可点击排序链接,排序键为 sales_volume。
|
||||||
|
assert.match(list, /销售数据/)
|
||||||
|
assert.match(list, /'aksrt_sales'/)
|
||||||
|
assert.match(list, /orderby.*sales_volume/)
|
||||||
|
|
||||||
|
// 排序列必须在表头注册,并把 sales_volume 映射回真实存储键。
|
||||||
|
assert.match(list, /manage_edit-post_sortable_columns/)
|
||||||
|
assert.match(list, /sales_volume/)
|
||||||
|
assert.match(list, /RESOURCE_SALES_VOLUME_KEY/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sales column renders pay type, price and both sales counts', () => {
|
||||||
|
// 未开启付费 → 提示;已开启 → 类型链接 + 价格 + 销量。
|
||||||
|
assert.match(list, /未开启付费/)
|
||||||
|
assert.match(list, /pay_type_labels\(\)/)
|
||||||
|
assert.match(list, /真实销量/)
|
||||||
|
assert.match(list, /get_content_sales_map/)
|
||||||
|
// 第一行取自 _aksrt_pay,第二行的销量取自 _aksrt_sales_volume。
|
||||||
|
assert.match(list, /aksrt_get_pay_meta/)
|
||||||
|
assert.match(list, /DomainModel::RESOURCE_SALES_VOLUME_KEY/)
|
||||||
|
// 真实销量链接必须跳到订单页(aksrt_orders)并按商品 ID 过滤。
|
||||||
|
assert.match(list, /'aksrt_orders'/)
|
||||||
|
assert.match(list, /'product_id'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('interaction counts are read from the keys the theme actually writes', () => {
|
test('interaction counts are read from the keys the theme actually writes', () => {
|
||||||
// 计数一律落在这几个 meta key 上:aksrt_post_views / _aksrt_like_count /
|
// 计数一律落在这几个 meta key 上:aksrt_post_views / _aksrt_like_count /
|
||||||
// _aksrt_favorite_count / _aksrt_res_download_count。列回调只能用同一套键,
|
// _aksrt_favorite_count / _aksrt_res_download_count。列回调只能用同一套键,
|
||||||
// 否则全列恒为 0。旧实现读 'views' / 'like' / 'favorite',仓库里没有任何写入方。
|
// 否则全列恒为 0。旧实现读 'views' / 'like' / 'favorite',仓库里没有任何写入方。
|
||||||
assert.match(tags, /'_aksrt_like_count'/)
|
assert.match(tags, /'_aksrt_like_count'/)
|
||||||
assert.match(tags, /'_aksrt_favorite_count'/)
|
assert.match(tags, /'_aksrt_favorite_count'/)
|
||||||
assert.match(tags, /'aksrt_post_views'/)
|
assert.match(tags, /'aksrt_post_views'/)
|
||||||
|
|
||||||
assert.match(hooks, /aksrt_get_post_views\(\$post_id\)/)
|
assert.match(list, /aksrt_get_post_views\(\$post_id\)/)
|
||||||
assert.match(hooks, /aksrt_get_post_likes\(\$post_id\)/)
|
assert.match(list, /aksrt_get_post_likes\(\$post_id\)/)
|
||||||
assert.match(hooks, /aksrt_get_post_favorites\(\$post_id\)/)
|
assert.match(list, /aksrt_get_post_favorites\(\$post_id\)/)
|
||||||
|
|
||||||
// 资源下载量只有 _aksrt_res_download_count 一个写入键。
|
// 资源下载量只有 _aksrt_res_download_count 一个写入键。
|
||||||
assert.match(hooks, /DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
assert.match(list, /DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
||||||
assert.match(domain, /RESOURCE_DOWNLOAD_COUNT_KEY\s*=\s*'_aksrt_res_download_count'/)
|
assert.match(domain, /RESOURCE_DOWNLOAD_COUNT_KEY\s*=\s*'_aksrt_res_download_count'/)
|
||||||
|
|
||||||
// 不得再直接读这些不存在的键('download_count' 只作为历史遗留键回退读取)。
|
// 不得再直接读这些不存在的键('download_count' 只作为历史遗留键回退读取)。
|
||||||
const column = hooks.slice(hooks.indexOf('manage_post_posts_custom_column'), hooks.indexOf('Support sorting by'))
|
const column = list.slice(list.indexOf('private function render_stats_column'), list.indexOf('private function render_sales_column'))
|
||||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'views'/)
|
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'views'/)
|
||||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'like'/)
|
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'like'/)
|
||||||
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'favorite'/)
|
assert.doesNotMatch(column, /get_post_meta\(\$post_id, 'favorite'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('admin stats column shares the aksrt_format_number abbreviation', () => {
|
test('admin stats column shares the aksrt_format_number abbreviation', () => {
|
||||||
// 后台列与前台必须同一套缩写口径(≥10000 → 1.2W+,≥1000 → 1.5K)。
|
// 后台列与前台必须同一套缩写口径(≥10000 → 1.2W+,≥1000 → 1.5K)。
|
||||||
// 旧实现用了前台从未用过的「万」后缀。
|
assert.match(list, /aksrt_format_number\(\$n\)/)
|
||||||
assert.match(hooks, /aksrt_format_number\(\$n\)/)
|
const column = list.slice(list.indexOf('private function render_stats_column'), list.indexOf('private function render_sales_column'))
|
||||||
const column = hooks.slice(hooks.indexOf('manage_post_posts_custom_column'), hooks.indexOf('Support sorting by'))
|
|
||||||
assert.doesNotMatch(column, /'万'/)
|
assert.doesNotMatch(column, /'万'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sortable meta maps the public orderby keys to the stored keys', () => {
|
test('sortable meta maps the public orderby keys to the stored keys', () => {
|
||||||
assert.match(hooks, /'views'\s*=>\s*\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/)
|
assert.match(list, /'views'\s*=>\s*\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/)
|
||||||
assert.match(hooks, /'like'\s*=>\s*'_aksrt_like_count'/)
|
assert.match(list, /'like'\s*=>\s*'_aksrt_like_count'/)
|
||||||
assert.match(hooks, /'favorite'\s*=>\s*'_aksrt_favorite_count'/)
|
assert.match(list, /'favorite'\s*=>\s*'_aksrt_favorite_count'/)
|
||||||
assert.match(hooks, /'download_count'\s*=>\s*\\AKSRT\\Core\\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
assert.match(list, /'download_count'\s*=>\s*\\AKSRT\\Core\\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY/)
|
||||||
// meta_key 必须是真实存储键,而不是用户可见的排序键。
|
// meta_key 必须是真实存储键,而不是用户可见的排序键。
|
||||||
assert.match(hooks, /\$query->set\('meta_key', \$allowed\[\$orderby\]\)/)
|
assert.match(list, /\$query->set\('meta_key', \$this->stats_meta_key\(\$orderby\)\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('every counter meta key the admin column renders has a writer', () => {
|
test('every counter meta key the admin column renders has a writer', () => {
|
||||||
// 逐键核对:列里展示的每项计数都必须在某个 helper 里真正被写入过。
|
// 逐键核对:列里展示的每项计数都必须在某个 helper 里真正被写入过。
|
||||||
|
// _aksrt_sales_volume 的写入方是付费订单流水(PayOrders),不在这份清单里,
|
||||||
|
// 由下一个用例单独覆盖。
|
||||||
const sources = [
|
const sources = [
|
||||||
'inc/core-hooks.php',
|
'src/Admin/Native/PostListEnhance.php',
|
||||||
'inc/helpers/aksrt-frontend-tags.php',
|
'inc/helpers/aksrt-frontend-tags.php',
|
||||||
'src/Core/PostMetaBox.php',
|
'src/Core/PostMetaBox.php',
|
||||||
'src/Frontend/PageAdminSet.php',
|
'src/Frontend/PageAdminSet.php',
|
||||||
@@ -69,31 +97,38 @@ test('every counter meta key the admin column renders has a writer', () => {
|
|||||||
for (const key of ['aksrt_post_views', '_aksrt_like_count', '_aksrt_favorite_count']) {
|
for (const key of ['aksrt_post_views', '_aksrt_like_count', '_aksrt_favorite_count']) {
|
||||||
assert.ok(sources.includes(`'${key}'`), `${key} is written somewhere`)
|
assert.ok(sources.includes(`'${key}'`), `${key} is written somewhere`)
|
||||||
}
|
}
|
||||||
// 资源帖的浏览有独立键,aksrt_get_post_views() 取两者较大值。
|
// 资源帖的浏览有独立键,aksrt_get_post_views() 取两者较大值。
|
||||||
assert.match(tags, /_aksrt_res_view_count/)
|
assert.match(tags, /_aksrt_res_view_count/)
|
||||||
assert.match(read('inc/helpers/aksrt-resource.php'), /RESOURCE_VIEW_COUNT_KEY|_aksrt_res_view_count/)
|
assert.match(read('inc/helpers/aksrt-resource.php'), /RESOURCE_VIEW_COUNT_KEY|_aksrt_res_view_count/)
|
||||||
|
|
||||||
// 资源下载走普通外链,主题内没有任何写入方,所以下载项只在 >0 时出现。
|
// 资源下载走普通外链,主题内没有任何写入方,所以下载项只在 >0 时出现。
|
||||||
// 万一将来接了下载统计,写入方必须落在同一个键上。
|
assert.match(list, /\$downloads <= 0/)
|
||||||
assert.match(hooks, /\$downloads <= 0/)
|
assert.match(list, /\$legacy_downloads/)
|
||||||
assert.match(hooks, /\$legacy_downloads/)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('download count stays read-only until a writer exists', () => {
|
test('sales volume is written by the order pipeline', () => {
|
||||||
// 目前没有任何 PHP 写入 _aksrt_res_download_count;若哪天新增了写入方,
|
// _aksrt_sales_volume 的写入方在付费助手与订单处理器里,列本身只读。
|
||||||
// 这里会失败,提示把该键也纳入后台列的统计口径。
|
assert.match(pay, /aksrt_bump_content_sales_volume/)
|
||||||
const php = ['inc', 'src', 'templates'].flatMap(dir =>
|
assert.match(pay, /RESOURCE_SALES_VOLUME_KEY/)
|
||||||
fs.readdirSync(path.join(root, dir), { withFileTypes: true })
|
assert.match(read('src/Content/PayOrders.php'), /aksrt_bump_content_sales_volume/)
|
||||||
.filter(e => e.isFile() && e.name.endsWith('.php'))
|
|
||||||
.map(e => `${dir}/${e.name}`)
|
|
||||||
).filter(f => !f.includes('core-hooks') && !f.includes('DomainModel')).map(read).join('\n')
|
|
||||||
const writers = php.match(/(?:update|add)_post_meta\([^)]*_aksrt_res_download_count/g) || []
|
|
||||||
assert.equal(writers.length, 0, 'download count still has no writer')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('admin column icons stay behind the Font Awesome enqueue on edit.php', () => {
|
test('admin column icons stay behind the Font Awesome enqueue on edit.php', () => {
|
||||||
assert.match(hooks, /admin_enqueue_scripts[\s\S]*?edit\.php[\s\S]*?aksrt_enqueue_fontawesome\(\)/)
|
assert.match(list, /admin_enqueue_scripts[\s\S]*?edit\.php[\s\S]*?aksrt_enqueue_fontawesome\(\)/)
|
||||||
for (const klass of ['fa-eye', 'fa-thumbs-up', 'fa-star', 'fa-download']) {
|
for (const klass of ['fa-eye', 'fa-thumbs-up', 'fa-star', 'fa-download']) {
|
||||||
assert.match(hooks, new RegExp(klass))
|
assert.match(list, new RegExp(klass))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the old closure-based column implementation is gone from core-hooks', () => {
|
||||||
|
// 列与排序逻辑统一收在 src/Admin/Native/PostListEnhance.php,inc/core-hooks.php
|
||||||
|
// 不再重复注册,否则同一列会被渲染两次。
|
||||||
|
const hooks = read('inc/core-hooks.php')
|
||||||
|
assert.doesNotMatch(hooks, /manage_post_posts_columns/)
|
||||||
|
assert.doesNotMatch(hooks, /manage_post_posts_custom_column/)
|
||||||
|
assert.doesNotMatch(hooks, /aksrt_stats/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PostListEnhance is wired up from functions.php', () => {
|
||||||
|
assert.match(read('functions.php'), /PostListEnhance::get_instance\(\)->init\(\)/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -78,16 +78,19 @@ test('icon values sanitize through the SVG-aware helper on every save path', ()
|
|||||||
test('FA5 loads under a single canonical handle everywhere', () => {
|
test('FA5 loads under a single canonical handle everywhere', () => {
|
||||||
const picker = read('inc/helpers/aksrt-icon-picker.php');
|
const picker = read('inc/helpers/aksrt-icon-picker.php');
|
||||||
const admin = read('src/Admin/Admin.php');
|
const admin = read('src/Admin/Admin.php');
|
||||||
const coreHooks = read('inc/core-hooks.php');
|
// 文章列表(edit.php)的 FA 入队随列增强迁到了 PostListEnhance。
|
||||||
|
const postList = read('src/Admin/Native/PostListEnhance.php');
|
||||||
const part1b = read('src/Core/ScriptsPart1b.php');
|
const part1b = read('src/Core/ScriptsPart1b.php');
|
||||||
assert.match(picker, /aksrt_enqueue_fontawesome\(\)/);
|
assert.match(picker, /aksrt_enqueue_fontawesome\(\)/);
|
||||||
assert.match(admin, /aksrt_enqueue_fontawesome\(\)/);
|
assert.match(admin, /aksrt_enqueue_fontawesome\(\)/);
|
||||||
assert.match(coreHooks, /aksrt_enqueue_fontawesome\(\)/);
|
assert.match(postList, /aksrt_enqueue_fontawesome\(\)/);
|
||||||
assert.match(part1b, /aksrt_enqueue_fontawesome\(\['bootstrap'\]\)/);
|
assert.match(part1b, /aksrt_enqueue_fontawesome\(\['bootstrap'\]\)/);
|
||||||
assert.match(part1b, /\['aksrt-font-awesome'\]/, 'public main css depends on the canonical handle');
|
assert.match(part1b, /\['aksrt-font-awesome'\]/, 'public main css depends on the canonical handle');
|
||||||
for (const file of [picker, admin, coreHooks, part1b]) {
|
for (const file of [picker, admin, postList, part1b]) {
|
||||||
assert.doesNotMatch(file, /wp_enqueue_style\(\s*'aksrt-font-awesome'/, 'no duplicate FA registrations');
|
assert.doesNotMatch(file, /wp_enqueue_style\(\s*'aksrt-font-awesome'/, 'no duplicate FA registrations');
|
||||||
}
|
}
|
||||||
|
// core-hooks.php 不该再自己注册或重复入队 FA。
|
||||||
|
assert.doesNotMatch(read('inc/core-hooks.php'), /aksrt_enqueue_fontawesome|wp_enqueue_style\(\s*'aksrt-font-awesome'/);
|
||||||
for (const file of ['inc/helpers/aksrt-widgets-markup.php', 'src/WidgetSystem/Renderers/ContentRenderer.php']) {
|
for (const file of ['inc/helpers/aksrt-widgets-markup.php', 'src/WidgetSystem/Renderers/ContentRenderer.php']) {
|
||||||
const source = read(file);
|
const source = read(file);
|
||||||
assert.doesNotMatch(source, /fa-clock-o|fa-folder-open-o|fa-file-text-o/, `${file} uses FA5 names`);
|
assert.doesNotMatch(source, /fa-clock-o|fa-folder-open-o|fa-file-text-o/, `${file} uses FA5 names`);
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
const test = require('node:test')
|
||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '../..')
|
||||||
|
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
|
||||||
|
|
||||||
|
const list = read('src/Admin/Native/PostListEnhance.php')
|
||||||
|
const fields = read('src/Admin/Native/PostListEditFields.php')
|
||||||
|
const domain = read('src/Core/DomainModel.php')
|
||||||
|
const pay = read('inc/helpers/aksrt-pay.php')
|
||||||
|
const metabox = read('src/Core/PostMetaBox.php')
|
||||||
|
const resourceBox = read('src/Core/ResourceMetaBox.php')
|
||||||
|
|
||||||
|
test('quick and bulk edit boxes hook the post list with no topics column', () => {
|
||||||
|
// 参考主题把配置项挂在「专题」分类法列上;本项目没有专题,统一挂在互动数据列。
|
||||||
|
assert.match(list, /quick_edit_custom_box/)
|
||||||
|
assert.match(list, /bulk_edit_custom_box/)
|
||||||
|
assert.match(list, /COLUMN_STATS/)
|
||||||
|
// 代码里不得再出现专题分类法列(类文档块的说明除外,只看 namespace 之后的部分)。
|
||||||
|
const code = list.slice(list.indexOf('namespace AKSRT'))
|
||||||
|
assert.doesNotMatch(code, /taxonomy-topics/)
|
||||||
|
assert.doesNotMatch(code, /topics/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('every quick-edit field group is rendered', () => {
|
||||||
|
// 四组字段的入口函数与分组标识都要存在,缺一组该列就少一块配置。
|
||||||
|
assert.match(list, /render_edit_boxes/)
|
||||||
|
assert.match(list, /PostListEditFields::extended\(\)/)
|
||||||
|
assert.match(list, /PostListEditFields::pay\(\$this->currency_symbol\(\)\)/)
|
||||||
|
assert.match(list, /filter_fields_html\(\)/)
|
||||||
|
assert.match(list, /'pay'/)
|
||||||
|
assert.match(list, /'filter'/)
|
||||||
|
// 字段定义本体放在独立的 PostListEditFields,两个静态方法必须真的存在。
|
||||||
|
assert.match(fields, /function extended\(\): array/)
|
||||||
|
assert.match(fields, /function pay\(string \$currency\): array/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('quick-edit inputs are namespaced under aksrt_bulk_edit', () => {
|
||||||
|
// 字段名统一 aksrt_bulk_edit[{id}][{field}],避免与 WP 原生数据冲突。
|
||||||
|
assert.match(list, /aksrt_bulk_edit\[' \. \$id \. '\]\[' \. \$field\['id'\] \. '\]/)
|
||||||
|
// number 类型带运算子字段。
|
||||||
|
assert.match(list, /\[operation\]/)
|
||||||
|
assert.match(list, /\[val\]/)
|
||||||
|
})
|
||||||
|
test('number fields support the full operation set', () => {
|
||||||
|
for (const op of ['ignore', 'set', 'plus', 'subtract', 'multiply', 'division']) {
|
||||||
|
assert.match(list, new RegExp(`value="${op}"`), `${op} operation exists`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('all control types used by the fields exist in the renderer', () => {
|
||||||
|
for (const type of ['number', 'checkbox', 'switcher', 'radio', 'select']) {
|
||||||
|
assert.match(list, new RegExp(`case '${type}':`), `${type} renderer exists`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('interaction quick-edit fields write to the keys the theme reads', () => {
|
||||||
|
// 阅读量/点赞数是唯一可批量设置的互动项,键必须与前端 helper 一致。
|
||||||
|
assert.match(fields, /'views'/)
|
||||||
|
assert.match(fields, /'like'/)
|
||||||
|
assert.match(metabox, /DomainModel::POST_VIEW_COUNT_KEY, max\(0, \(int\) \(\$raw\['views'\]/)
|
||||||
|
assert.match(metabox, /'_aksrt_like_count', max\(0, \(int\) \(\$raw\['like'\]/)
|
||||||
|
// 保存侧按这两个键分流到统计 meta。
|
||||||
|
assert.match(list, /stats_meta_key/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('article config quick-edit fields match the extension meta box keys', () => {
|
||||||
|
// 显示布局 + 两个开关必须与「文章扩展」metabox 的键名一致。
|
||||||
|
assert.match(metabox, /show_layout/)
|
||||||
|
assert.match(metabox, /no_article-navs/)
|
||||||
|
assert.match(metabox, /article_maxheight_xz/)
|
||||||
|
|
||||||
|
// 布局取值白名单:metabox 的 sanitize 是裸数组,快捷编辑提供同四值的单选。
|
||||||
|
assert.match(metabox, /\['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'\]/)
|
||||||
|
assert.match(fields, /'false'\s+=> __\('跟随主题', 'aksrt'\)/)
|
||||||
|
assert.match(fields, /'no_sidebar'\s+=> __\('无侧边栏', 'aksrt'\)/)
|
||||||
|
assert.match(fields, /'sidebar_left'\s+=> __\('侧边栏靠左', 'aksrt'\)/)
|
||||||
|
assert.match(fields, /'sidebar_right'\s+=> __\('侧边栏靠右', 'aksrt'\)/)
|
||||||
|
// 保存侧再用同一份白名单收敛,避免快捷编辑写入未知布局。
|
||||||
|
assert.match(list, /\['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'\]/)
|
||||||
|
|
||||||
|
// 落到 aksrt_post_meta 这个共享 bag 上,与 metabox 同一键(self::META_KEY)。
|
||||||
|
assert.match(metabox, /post_meta/)
|
||||||
|
assert.match(list, /PostMetaBox::META_KEY/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pay quick-edit fields map onto the real pay meta schema', () => {
|
||||||
|
// 逐字段核对:快捷编辑写入的键必须存在于默认付费 meta / metabox 中。
|
||||||
|
const payKeys = [
|
||||||
|
'pay_type', 'pay_modo', 'pay_limit',
|
||||||
|
'points_price', 'vip1_points', 'vip2_points',
|
||||||
|
'price', 'original_price', 'vip1_price', 'vip2_price',
|
||||||
|
'pay_count', 'coupon_s', 'order_expired_s',
|
||||||
|
'order_expired_time', 'order_expired_unit', 'show_sales',
|
||||||
|
]
|
||||||
|
for (const key of payKeys) {
|
||||||
|
assert.match(domain, new RegExp(`'${key}'\\s*=>`), `pay meta has ${key}`)
|
||||||
|
assert.match(fields, new RegExp(`'${key}'`), `quick edit offers ${key}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pay quick-edit save sanitizes through the shared sanitizer', () => {
|
||||||
|
// 任何批量写入都必须过 sanitize_pay_meta,再回写派生键,与 metabox 保存一致。
|
||||||
|
assert.match(list, /DomainModel::sanitize_pay_meta/)
|
||||||
|
assert.match(list, /DomainModel::PAY_META_KEY, \$pay_meta/)
|
||||||
|
assert.match(list, /DomainModel::PAY_TYPE_KEY/)
|
||||||
|
assert.match(list, /DomainModel::PAY_PRICE_KEY/)
|
||||||
|
assert.match(list, /'_aksrt_pay_modo'/)
|
||||||
|
assert.match(list, /'_aksrt_pay_limit'/)
|
||||||
|
assert.match(list, /RESOURCE_MARKER_KEY/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pay prices go through the ledger, not raw floats', () => {
|
||||||
|
// 金额字段运算在「分」上进行,避免浮点误差。
|
||||||
|
assert.match(list, /balance_minor|price_to_minor/)
|
||||||
|
assert.match(list, /decimal_from_minor/)
|
||||||
|
assert.match(list, /\$pay_meta\[\$field_id\] = \\AKSRT\\Ledger\\AssetAmount::decimal_from_minor/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('quick-edit save ignores invalid operations and clamps negatives', () => {
|
||||||
|
assert.match(list, /apply_operation/)
|
||||||
|
assert.match(list, /case 'set'/)
|
||||||
|
assert.match(list, /case 'plus'/)
|
||||||
|
assert.match(list, /case 'subtract'/)
|
||||||
|
assert.match(list, /case 'multiply'/)
|
||||||
|
assert.match(list, /case 'division'/)
|
||||||
|
// 除法要防零。
|
||||||
|
assert.match(list, /\$current != 0 && \$value != 0/)
|
||||||
|
// 负值归零。
|
||||||
|
assert.match(list, /max\(0, \(int\) \$new\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('custom filter quick-edit fields reuse the settings dimensions', () => {
|
||||||
|
// 高级筛选项从同一份设置里动态生成,键名前缀与 metabox 保存一致。
|
||||||
|
assert.match(list, /get_post_filter_settings/)
|
||||||
|
assert.match(list, /custom_filter/)
|
||||||
|
assert.match(list, /'_aksrt_cf_' \. \$field_id/)
|
||||||
|
assert.match(metabox, /'_aksrt_cf_' \. \$key/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('quick-edit save is guarded by screen, autosave, revision and capability', () => {
|
||||||
|
assert.match(list, /DOING_AUTOSAVE/)
|
||||||
|
assert.match(list, /wp_is_post_revision/)
|
||||||
|
assert.match(list, /current_user_can\('edit_post', \$post_id\)/)
|
||||||
|
assert.match(list, /edit-post/)
|
||||||
|
// 只处理 post 类型,不动页面、CPT 与其他列表。
|
||||||
|
assert.match(list, /\$post->post_type !== 'post'/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no legacy zib-style field names or topic links survive', () => {
|
||||||
|
assert.doesNotMatch(list, /zib_bulk_edit/)
|
||||||
|
assert.doesNotMatch(list, /posts_zibpay/)
|
||||||
|
assert.doesNotMatch(list, /zibpay_type/)
|
||||||
|
assert.doesNotMatch(list, /pay_cuont/)
|
||||||
|
// 本项目不使用「自动售卡」类目。
|
||||||
|
assert.doesNotMatch(list, /售卡/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the old inline column code was fully migrated', () => {
|
||||||
|
// inc/core-hooks.php 不再有任何文章列表列/排序逻辑,全部由新类负责。
|
||||||
|
// (L295 的 pre_get_posts 属于前台查询,不是后台排序列增强;这里只锁列表增强的部分。)
|
||||||
|
const hooks = read('inc/core-hooks.php')
|
||||||
|
assert.doesNotMatch(hooks, /manage_post_posts_columns/)
|
||||||
|
assert.doesNotMatch(hooks, /manage_post_posts_custom_column/)
|
||||||
|
assert.doesNotMatch(hooks, /manage_edit-post_sortable_columns/)
|
||||||
|
assert.doesNotMatch(hooks, /aksrt_enqueue_fontawesome/)
|
||||||
|
assert.doesNotMatch(hooks, /aksrt_stats/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the dead tab-pane and modal css blocks are removed', () => {
|
||||||
|
// 这些选择器无任何引用,已确认删除;文件仍保留 quick-edit 需要的轮子类。
|
||||||
|
const css = read('assets/css/admin-main.css')
|
||||||
|
assert.doesNotMatch(css, /aksrt-tab-pane/)
|
||||||
|
assert.doesNotMatch(css, /\.aksrt-modal\b/)
|
||||||
|
assert.doesNotMatch(css, /\.aksrt-modal-backdrop\b/)
|
||||||
|
// 快捷编辑卡片与依赖样式仍在。
|
||||||
|
assert.match(css, /\.aksrt-bulk-edit-box/)
|
||||||
|
assert.match(css, /\.aksrt-bulk-edit-desc/)
|
||||||
|
assert.match(css, /\.flex\s*\{/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the quick-edit dependency script exists and toggles by data-dependency', () => {
|
||||||
|
assert.ok(fs.existsSync(path.join(root, 'assets/js/admin-post-list.js')))
|
||||||
|
const js = read('assets/js/admin-post-list.js')
|
||||||
|
assert.match(js, /data-dependency/)
|
||||||
|
assert.match(js, /aksrt_bulk_edit\[pay\]/)
|
||||||
|
assert.match(js, /inline-edit-row/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user