feat(admin): native plate moderators, product variants, discount term fields

Add WP-native ops UI without SPA: plate moderator list/application review
meta box, product SKU variants editor meta box, and shop_discount taxonomy
add/edit form fields. No admin build artifacts included.
This commit is contained in:
2026-07-15 02:19:06 +08:00
parent f4b72c8f70
commit 83f0467084
5 changed files with 497 additions and 0 deletions
+10
View File
@@ -298,6 +298,16 @@ if (is_admin() && class_exists('AKSRT_Admin_Shop_Controller')) {
if (is_admin() && class_exists('AKSRT_Product_Meta_Box')) {
AKSRT_Product_Meta_Box::get_instance()->init();
}
// 商品原生变体 meta box
if (is_admin() && class_exists('AKSRT_Product_Variants_Meta_Box')) {
AKSRT_Product_Variants_Meta_Box::get_instance()->init();
}
// 论坛版块版主 meta box
if (is_admin() && class_exists('AKSRT_Plate_Meta_Box')) {
AKSRT_Plate_Meta_Box::get_instance()->init();
}
// 商品评价/评分系统
if (class_exists('AKSRT_Product_Review')) {
+2
View File
@@ -321,6 +321,8 @@ require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-checkout-service.ph
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-address-service.php';
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-product-service.php';
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-product-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-product-variants-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-plate-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-2.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-3.php';
+157
View File
@@ -33,6 +33,12 @@ class AKSRT_Shop_Term_Meta {
public function init(): void {
add_action('init', [$this, 'register_discount_taxonomy'], 5);
add_action('init', [$this, 'register_term_meta_fields'], 10);
// Native term UI for shop_discount (zibll-style admin ops).
add_action('shop_discount_add_form_fields', [$this, 'render_discount_add_fields']);
add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1);
add_action('created_shop_discount', [$this, 'save_discount_term_fields']);
add_action('edited_shop_discount', [$this, 'save_discount_term_fields']);
}
// ─── Taxonomy Registration ───────────────────────────────────
@@ -408,4 +414,155 @@ class AKSRT_Shop_Term_Meta {
$defaults = $shop['product_defaults'] ?? [];
return $defaults[$param_key] ?? $product_value;
}
/**
* Add form fields for shop_discount taxonomy.
*/
public function render_discount_add_fields(): void {
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$default = $def['default'] ?? '';
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html($label) . '</label>';
$this->render_discount_field_control($key, $default, false);
echo '</div>';
}
}
/**
* Edit form fields for shop_discount taxonomy.
*/
public function render_discount_edit_fields($term): void {
if (!($term instanceof WP_Term)) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field">';
echo '<th scope="row"><label for="' . esc_attr($key) . '">' . esc_html($label) . '</label></th>';
echo '<td>';
$this->render_discount_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param string|int|float $value
*/
private function render_discount_field_control(string $key, $value, bool $edit): void {
$id = esc_attr($key);
$name = esc_attr($key);
$val = (string) $value;
if ($key === 'shop_discount_type') {
$opts = [
'reduction' => __('立减', 'aksrt'),
'discount' => __('折扣', 'aksrt'),
'gift' => __('赠品', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_scope') {
$opts = [
'item' => __('单品', 'aksrt'),
'product' => __('商品', 'aksrt'),
'author' => __('商家', 'aksrt'),
'order' => __('整单', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_user_limit') {
$opts = [
'' => __('不限', 'aksrt'),
'vip' => __('VIP1', 'aksrt'),
'vip_2' => __('VIP2', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
echo '<label><input type="checkbox" name="' . $name . '" id="' . $id . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if ($key === 'shop_discount_gift_config') {
echo '<textarea name="' . $name . '" id="' . $id . '" rows="3" class="large-text">' . esc_textarea($val) . '</textarea>';
return;
}
$type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text';
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) {
$type = 'datetime-local';
// Convert mysql-ish values to datetime-local if needed.
if ($val !== '' && str_contains($val, ' ')) {
$val = str_replace(' ', 'T', substr($val, 0, 16));
}
}
printf(
'<input type="%s" name="%s" id="%s" value="%s" class="%s" />',
esc_attr($type),
$name,
$id,
esc_attr($val),
$edit ? 'regular-text' : ''
);
}
public function save_discount_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) {
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
}
update_term_meta($term_id, $key, $value);
}
}
}
@@ -0,0 +1,140 @@
<?php
/**
* Native meta box for aksrt_forum_plate moderators & applications.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Plate_Meta_Box {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_forum_plate', [$this, 'save'], 20, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_plate_moderators',
__('版主与申请', 'aksrt'),
[$this, 'render'],
'aksrt_forum_plate',
'side',
'high'
);
}
public function render(WP_Post $post): void {
wp_nonce_field('aksrt_plate_meta_save', 'aksrt_plate_meta_nonce');
$moderator_ids = [];
$applications = [];
if (class_exists('AKSRT_Forum')) {
$forum = AKSRT_Forum::get_instance();
$moderator_ids = $forum->get_plate_moderators((int) $post->ID);
$applications = $forum->get_plate_moderator_applications((int) $post->ID);
} else {
$raw = get_post_meta($post->ID, '_aksrt_moderators', true);
$moderator_ids = is_array($raw) ? array_map('intval', $raw) : [];
$applications = get_post_meta($post->ID, '_aksrt_moderator_applications', true);
$applications = is_array($applications) ? $applications : [];
}
$moderator_labels = [];
foreach ($moderator_ids as $uid) {
$user = get_userdata((int) $uid);
$moderator_labels[] = $user
? sprintf('%s (#%d)', $user->user_login, (int) $uid)
: (string) (int) $uid;
}
?>
<p>
<label for="aksrt_plate_moderators"><strong><?php esc_html_e('版主用户 ID', 'aksrt'); ?></strong></label>
<textarea id="aksrt_plate_moderators" name="aksrt_plate_moderators" rows="3" class="widefat" placeholder="1,2,3"><?php echo esc_textarea(implode(',', array_map('intval', $moderator_ids))); ?></textarea>
<span class="description"><?php esc_html_e('逗号或换行分隔的用户 ID。超级版主为版块作者。', 'aksrt'); ?></span>
</p>
<?php if ($moderator_labels) : ?>
<p class="description"><?php echo esc_html(implode(' · ', $moderator_labels)); ?></p>
<?php endif; ?>
<hr>
<p><strong><?php esc_html_e('版主申请', 'aksrt'); ?></strong></p>
<?php
$pending = array_values(array_filter($applications, static function ($app) {
return is_array($app) && (($app['status'] ?? '') === 'pending' || ($app['status'] ?? '') === AKSRT_Forum::APPLICATION_PENDING);
}));
if (!$pending) {
echo '<p class="description">' . esc_html__('暂无待处理申请。', 'aksrt') . '</p>';
return;
}
echo '<ul style="margin:0;padding-left:16px;">';
foreach ($pending as $app) {
$uid = (int) ($app['user_id'] ?? 0);
$user = $uid > 0 ? get_userdata($uid) : null;
$aid = (string) ($app['id'] ?? '');
$reason = (string) ($app['reason'] ?? '');
echo '<li style="margin-bottom:10px;">';
echo '<div><code>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : (string) $uid) . '</code></div>';
if ($reason !== '') {
echo '<div class="description">' . esc_html($reason) . '</div>';
}
echo '<label style="display:block;margin-top:4px;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="keep" checked> ' . esc_html__('保留', 'aksrt') . '</label>';
echo '<label style="display:block;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="approved"> ' . esc_html__('通过', 'aksrt') . '</label>';
echo '<label style="display:block;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="rejected"> ' . esc_html__('拒绝', 'aksrt') . '</label>';
echo '</li>';
}
echo '</ul>';
}
public function save(int $post_id, WP_Post $post): void {
if (!isset($_POST['aksrt_plate_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_plate_meta_nonce'])), 'aksrt_plate_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_forum_plate' || !current_user_can('edit_post', $post_id)) {
return;
}
$raw = isset($_POST['aksrt_plate_moderators']) ? (string) wp_unslash($_POST['aksrt_plate_moderators']) : '';
$ids = preg_split('/[\s,;]+/', $raw) ?: [];
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
update_post_meta($post_id, '_aksrt_moderators', $ids);
$actions = isset($_POST['aksrt_plate_app']) && is_array($_POST['aksrt_plate_app'])
? wp_unslash($_POST['aksrt_plate_app'])
: [];
if (!$actions || !class_exists('AKSRT_Forum')) {
return;
}
$forum = AKSRT_Forum::get_instance();
foreach ($actions as $application_id => $action) {
$application_id = sanitize_text_field((string) $application_id);
$action = sanitize_key((string) $action);
if ($application_id === '' || $action === 'keep') {
continue;
}
if ($action === 'approved') {
$forum->review_moderator_application($application_id, AKSRT_Forum::APPLICATION_APPROVED, '', get_current_user_id());
} elseif ($action === 'rejected') {
$forum->review_moderator_application($application_id, AKSRT_Forum::APPLICATION_REJECTED, '', get_current_user_id());
}
}
}
}
@@ -0,0 +1,188 @@
<?php
/**
* Native SKU variants meta box for aksrt_product.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Product_Variants_Meta_Box {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 30, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_variants',
__('SKU 变体', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'default'
);
}
public function render(WP_Post $post): void {
wp_nonce_field('aksrt_product_variants_save', 'aksrt_product_variants_nonce');
$variants = [];
if (class_exists('AKSRT_Shop')) {
$variants = AKSRT_Shop::get_instance()->get_variants((int) $post->ID);
}
if (!$variants) {
$variants = [[
'sku' => '',
'attributes' => [],
'price' => 0,
'stock' => 0,
'image' => '',
]];
}
?>
<style>
.aksrt-variants-table { width:100%; border-collapse:collapse; }
.aksrt-variants-table th, .aksrt-variants-table td { border:1px solid #dcdcde; padding:6px; vertical-align:top; }
.aksrt-variants-table input { width:100%; }
.aksrt-variants-table .col-sku { width:14%; }
.aksrt-variants-table .col-attrs { width:28%; }
.aksrt-variants-table .col-price, .aksrt-variants-table .col-stock { width:12%; }
.aksrt-variants-table .col-image { width:28%; }
.aksrt-variants-hint { color:#646970; font-size:12px; margin:8px 0; }
</style>
<p class="aksrt-variants-hint">
<?php esc_html_e('属性格式:名称:值,多项用分号分隔,例如 颜色:红;尺寸:L。保存时会整体覆盖该商品变体列表。', 'aksrt'); ?>
</p>
<table class="aksrt-variants-table" id="aksrt-variants-table">
<thead>
<tr>
<th class="col-sku"><?php esc_html_e('SKU', 'aksrt'); ?></th>
<th class="col-attrs"><?php esc_html_e('属性', 'aksrt'); ?></th>
<th class="col-price"><?php esc_html_e('价格', 'aksrt'); ?></th>
<th class="col-stock"><?php esc_html_e('库存', 'aksrt'); ?></th>
<th class="col-image"><?php esc_html_e('图片 URL', 'aksrt'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $i => $row) :
$attrs = $row['attributes'] ?? [];
$attrs_text = '';
if (is_array($attrs)) {
$parts = [];
foreach ($attrs as $k => $v) {
if (is_int($k) && is_array($v)) {
$parts[] = ($v['name'] ?? $v['key'] ?? '') . ':' . ($v['value'] ?? '');
} else {
$parts[] = $k . ':' . $v;
}
}
$attrs_text = implode(';', array_filter($parts));
} elseif (is_string($attrs)) {
$attrs_text = $attrs;
}
?>
<tr>
<td><input type="text" name="aksrt_variant_sku[]" value="<?php echo esc_attr((string) ($row['sku'] ?? '')); ?>"></td>
<td><input type="text" name="aksrt_variant_attrs[]" value="<?php echo esc_attr($attrs_text); ?>" placeholder="颜色:红;尺寸:L"></td>
<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="<?php echo esc_attr((string) ($row['price'] ?? 0)); ?>"></td>
<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="<?php echo esc_attr((string) ($row['stock'] ?? 0)); ?>"></td>
<td><input type="url" name="aksrt_variant_image[]" value="<?php echo esc_attr((string) ($row['image'] ?? '')); ?>"></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="aksrt-add-variant-row"><?php esc_html_e('添加变体行', 'aksrt'); ?></button>
</p>
<script>
(function(){
var btn = document.getElementById('aksrt-add-variant-row');
var tbody = document.querySelector('#aksrt-variants-table tbody');
if (!btn || !tbody) return;
btn.addEventListener('click', function(){
var tr = document.createElement('tr');
tr.innerHTML = '<td><input type="text" name="aksrt_variant_sku[]" value=""></td>' +
'<td><input type="text" name="aksrt_variant_attrs[]" value="" placeholder="颜色:红;尺寸:L"></td>' +
'<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="0"></td>' +
'<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="0"></td>' +
'<td><input type="url" name="aksrt_variant_image[]" value=""></td>';
tbody.appendChild(tr);
});
})();
</script>
<?php
}
public function save(int $post_id, WP_Post $post): void {
if (!isset($_POST['aksrt_product_variants_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_variants_nonce'])), 'aksrt_product_variants_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product' || !current_user_can('edit_post', $post_id)) {
return;
}
if (!class_exists('AKSRT_Shop')) {
return;
}
$skus = isset($_POST['aksrt_variant_sku']) && is_array($_POST['aksrt_variant_sku']) ? wp_unslash($_POST['aksrt_variant_sku']) : [];
$attrs = isset($_POST['aksrt_variant_attrs']) && is_array($_POST['aksrt_variant_attrs']) ? wp_unslash($_POST['aksrt_variant_attrs']) : [];
$prices = isset($_POST['aksrt_variant_price']) && is_array($_POST['aksrt_variant_price']) ? wp_unslash($_POST['aksrt_variant_price']) : [];
$stocks = isset($_POST['aksrt_variant_stock']) && is_array($_POST['aksrt_variant_stock']) ? wp_unslash($_POST['aksrt_variant_stock']) : [];
$images = isset($_POST['aksrt_variant_image']) && is_array($_POST['aksrt_variant_image']) ? wp_unslash($_POST['aksrt_variant_image']) : [];
$variants = [];
$count = max(count($skus), count($attrs), count($prices), count($stocks), count($images));
for ($i = 0; $i < $count; $i++) {
$sku = sanitize_text_field((string) ($skus[$i] ?? ''));
$attr_raw = sanitize_text_field((string) ($attrs[$i] ?? ''));
$price = max(0, (float) ($prices[$i] ?? 0));
$stock = max(0, (int) ($stocks[$i] ?? 0));
$image = esc_url_raw((string) ($images[$i] ?? ''));
if ($sku === '' && $attr_raw === '' && $price <= 0 && $stock <= 0 && $image === '') {
continue;
}
$attributes = [];
if ($attr_raw !== '') {
foreach (preg_split('/\s*;\s*/', $attr_raw) ?: [] as $pair) {
if ($pair === '' || !str_contains($pair, ':')) {
continue;
}
[$k, $v] = array_map('trim', explode(':', $pair, 2));
if ($k !== '') {
$attributes[$k] = $v;
}
}
}
$variants[] = [
'sku' => $sku,
'attributes' => $attributes,
'price' => $price,
'stock' => $stock,
'image' => $image,
];
}
AKSRT_Shop::get_instance()->save_variants($post_id, $variants);
}
}