Files
wordpress-theme-aksrt/inc/classes/shop/class-aksrt-product-meta-box.php
T
kate522 713b368475 feat(admin): expand native product commerce meta fields
Align product edit meta box with REST-backed shipping/service/limit fields:
fee type, free-shipping threshold, delivery/service tips, rebate, params,
and buyer required info.
2026-07-15 03:06:39 +08:00

342 lines
22 KiB
PHP

<?php
/**
* Native product edit meta box for aksrt_product (SPA ProductEditorDrawer parity).
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Product_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'], 10, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_commerce',
__('商品商业设置', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'high'
);
}
public function render(WP_Post $post): void {
wp_nonce_field('aksrt_product_meta_save', 'aksrt_product_meta_nonce');
$price = (float) get_post_meta($post->ID, '_aksrt_price', true);
$stock = (int) get_post_meta($post->ID, '_aksrt_stock', true);
$image = (string) get_post_meta($post->ID, '_aksrt_product_image', true);
$gallery = get_post_meta($post->ID, '_aksrt_product_gallery', true);
$gallery = is_array($gallery) ? $gallery : [];
$shipping_mode = (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true);
$shipping_fee = (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true);
$shipping_fee_type = (string) (get_post_meta($post->ID, '_aksrt_shipping_fee_type', true) ?: 'fixed');
$shipping_fee_free = (float) get_post_meta($post->ID, '_aksrt_shipping_fee_free_amount', true);
$shipping_delivery = (string) get_post_meta($post->ID, '_aksrt_shipping_delivery_desc', true);
$shipping_tip = (string) get_post_meta($post->ID, '_aksrt_shipping_tip', true);
$after_sale_policy = (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true);
$support_refund = get_post_meta($post->ID, '_aksrt_support_refund', true);
$support_refund = $support_refund === '' ? '1' : (string) $support_refund;
$service = (string) get_post_meta($post->ID, '_aksrt_service', true);
$service_tip = (string) get_post_meta($post->ID, '_aksrt_service_tip', true);
$limit_buy = get_post_meta($post->ID, '_aksrt_limit_buy', true);
$limit_buy = $limit_buy === '' ? -1 : (int) $limit_buy;
$limit_buy_note = (string) get_post_meta($post->ID, '_aksrt_limit_buy_note', true);
$stock_type = (string) (get_post_meta($post->ID, '_aksrt_stock_type', true) ?: 'all');
$pay_modo = (string) (get_post_meta($post->ID, '_aksrt_pay_modo', true) ?: '0');
$start_price = (float) get_post_meta($post->ID, '_aksrt_start_price', true);
$product_rebate = (string) get_post_meta($post->ID, '_aksrt_product_rebate', true);
$product_params = (string) get_post_meta($post->ID, '_aksrt_product_params', true);
$user_required = (string) get_post_meta($post->ID, '_aksrt_user_required', true);
$delivery_note = (string) get_post_meta($post->ID, '_aksrt_delivery_note', true);
$marketing = (string) get_post_meta($post->ID, '_aksrt_marketing_headline', true);
$shipping_modes = [
'' => __('默认(继承设置)', 'aksrt'),
'express' => __('快递物流', 'aksrt'),
'auto' => __('自动发货', 'aksrt'),
'manual' => __('手动发货', 'aksrt'),
'virtual' => __('虚拟商品', 'aksrt'),
];
$stock_types = [
'all' => __('统一库存', 'aksrt'),
'opts' => __('按选项/变体', 'aksrt'),
];
$fee_types = [
'free' => __('包邮', 'aksrt'),
'fixed' => __('固定运费', 'aksrt'),
'amount_free' => __('满额包邮', 'aksrt'),
];
?>
<style>
.aksrt-pmb-product .row { display:grid; grid-template-columns: 140px 1fr; gap: 10px 14px; margin: 0 0 12px; align-items:start; }
.aksrt-pmb-product .row label { font-weight:600; padding-top:6px; }
.aksrt-pmb-product input[type="text"],
.aksrt-pmb-product input[type="number"],
.aksrt-pmb-product input[type="url"],
.aksrt-pmb-product select,
.aksrt-pmb-product textarea { width:100%; max-width:560px; }
.aksrt-pmb-product .hint { color:#646970; font-size:12px; margin:4px 0 0; }
.aksrt-pmb-product .grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; max-width:720px; }
.aksrt-pmb-product h4 { margin:18px 0 10px; padding-top:12px; border-top:1px solid #dcdcde; }
</style>
<div class="aksrt-pmb-product">
<h4><?php esc_html_e('价格与库存', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_price"><?php esc_html_e('价格(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_price" name="aksrt_price" min="0" step="0.01" value="<?php echo esc_attr((string) $price); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<input type="number" id="aksrt_stock" name="aksrt_stock" min="-1" step="1" value="<?php echo esc_attr((string) $stock); ?>">
<p class="hint"><?php esc_html_e('-1 表示无限库存。', 'aksrt'); ?></p>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<select id="aksrt_pay_modo" name="aksrt_pay_modo">
<option value="0" <?php selected($pay_modo, '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected($pay_modo, 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_start_price"><?php esc_html_e('划线价 / 起售价', 'aksrt'); ?></label>
<input type="number" id="aksrt_start_price" name="aksrt_start_price" min="0" step="0.01" value="<?php echo esc_attr((string) $start_price); ?>">
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock_type"><?php esc_html_e('库存类型', 'aksrt'); ?></label>
<select id="aksrt_stock_type" name="aksrt_stock_type">
<?php foreach ($stock_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($stock_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_limit_buy"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<input type="number" id="aksrt_limit_buy" name="aksrt_limit_buy" min="-1" step="1" value="<?php echo esc_attr((string) $limit_buy); ?>">
<p class="hint"><?php esc_html_e('-1 不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_limit_buy_note"><?php esc_html_e('限购说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_limit_buy_note" name="aksrt_limit_buy_note" value="<?php echo esc_attr($limit_buy_note); ?>">
</div>
<div class="row">
<label for="aksrt_product_rebate"><?php esc_html_e('推广返佣', 'aksrt'); ?></label>
<div>
<input type="text" id="aksrt_product_rebate" name="aksrt_product_rebate" value="<?php echo esc_attr($product_rebate); ?>" placeholder="<?php esc_attr_e('如 10 或 10%', 'aksrt'); ?>">
<p class="hint"><?php esc_html_e('可填固定金额或百分比,具体解析由返佣服务处理。', 'aksrt'); ?></p>
</div>
</div>
<h4><?php esc_html_e('素材', 'aksrt'); ?></h4>
<div class="row">
<label for="aksrt_product_image"><?php esc_html_e('主图 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_product_image" name="aksrt_product_image" value="<?php echo esc_attr($image); ?>" placeholder="https://">
<p class="hint"><?php esc_html_e('也可使用特色图片;此处优先作为商品主图。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_product_gallery"><?php esc_html_e('图集 URL', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_gallery" name="aksrt_product_gallery" rows="4" placeholder="<?php esc_attr_e('每行一个图片 URL', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $gallery)); ?></textarea>
</div>
</div>
<div class="row">
<label for="aksrt_product_params"><?php esc_html_e('商品参数', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_params" name="aksrt_product_params" rows="4" placeholder="<?php esc_attr_e('每行一条参数,如 品牌:AKSRT', 'aksrt'); ?>"><?php echo esc_textarea($product_params); ?></textarea>
</div>
</div>
<h4><?php esc_html_e('发货与售后', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_mode"><?php esc_html_e('发货方式', 'aksrt'); ?></label>
<select id="aksrt_shipping_mode" name="aksrt_shipping_mode">
<?php foreach ($shipping_modes as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_mode, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_type"><?php esc_html_e('运费类型', 'aksrt'); ?></label>
<select id="aksrt_shipping_fee_type" name="aksrt_shipping_fee_type">
<?php foreach ($fee_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_fee_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee"><?php esc_html_e('运费金额(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee" name="aksrt_shipping_fee" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_free_amount"><?php esc_html_e('满额包邮门槛', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee_free_amount" name="aksrt_shipping_fee_free_amount" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee_free); ?>">
</div>
</div>
<div class="row">
<label for="aksrt_shipping_delivery_desc"><?php esc_html_e('发货时效说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_delivery_desc" name="aksrt_shipping_delivery_desc" value="<?php echo esc_attr($shipping_delivery); ?>" placeholder="<?php esc_attr_e('如 付款后 24 小时内发货', 'aksrt'); ?>">
</div>
<div class="row">
<label for="aksrt_shipping_tip"><?php esc_html_e('物流提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_tip" name="aksrt_shipping_tip" value="<?php echo esc_attr($shipping_tip); ?>">
</div>
<div class="row">
<label for="aksrt_after_sale_policy"><?php esc_html_e('售后说明', 'aksrt'); ?></label>
<textarea id="aksrt_after_sale_policy" name="aksrt_after_sale_policy" rows="3"><?php echo esc_textarea($after_sale_policy); ?></textarea>
</div>
<div class="row">
<label for="aksrt_support_refund"><?php esc_html_e('支持退款', 'aksrt'); ?></label>
<div>
<label>
<input type="checkbox" id="aksrt_support_refund" name="aksrt_support_refund" value="1" <?php checked($support_refund, '1'); ?>>
<?php esc_html_e('允许申请退款/售后', 'aksrt'); ?>
</label>
</div>
</div>
<div class="row">
<label for="aksrt_service"><?php esc_html_e('服务承诺', 'aksrt'); ?></label>
<textarea id="aksrt_service" name="aksrt_service" rows="3" placeholder="<?php esc_attr_e('每行一条服务承诺', 'aksrt'); ?>"><?php echo esc_textarea($service); ?></textarea>
</div>
<div class="row">
<label for="aksrt_service_tip"><?php esc_html_e('服务提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_service_tip" name="aksrt_service_tip" value="<?php echo esc_attr($service_tip); ?>">
</div>
<div class="row">
<label for="aksrt_user_required"><?php esc_html_e('下单需填信息', 'aksrt'); ?></label>
<textarea id="aksrt_user_required" name="aksrt_user_required" rows="3" placeholder="<?php esc_attr_e('每行一项,如 QQ / 游戏角色名', 'aksrt'); ?>"><?php echo esc_textarea($user_required); ?></textarea>
</div>
<div class="row">
<label for="aksrt_delivery_note"><?php esc_html_e('发货备注', 'aksrt'); ?></label>
<input type="text" id="aksrt_delivery_note" name="aksrt_delivery_note" value="<?php echo esc_attr($delivery_note); ?>">
</div>
<div class="row">
<label for="aksrt_marketing_headline"><?php esc_html_e('营销标题', 'aksrt'); ?></label>
<input type="text" id="aksrt_marketing_headline" name="aksrt_marketing_headline" value="<?php echo esc_attr($marketing); ?>">
</div>
<p class="hint">
<?php esc_html_e('SKU 变体请使用下方「商品变体」面板;列表列已显示价格/库存/销量。', 'aksrt'); ?>
</p>
</div>
<?php
}
public function save(int $post_id, WP_Post $post): void {
if (!isset($_POST['aksrt_product_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_meta_nonce'])), 'aksrt_product_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product') {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$price = isset($_POST['aksrt_price']) ? max(0, (float) wp_unslash($_POST['aksrt_price'])) : 0;
$stock = isset($_POST['aksrt_stock']) ? (int) wp_unslash($_POST['aksrt_stock']) : 0;
if ($stock < -1) {
$stock = -1;
}
$image = isset($_POST['aksrt_product_image']) ? esc_url_raw((string) wp_unslash($_POST['aksrt_product_image'])) : '';
$gallery_raw = isset($_POST['aksrt_product_gallery']) ? (string) wp_unslash($_POST['aksrt_product_gallery']) : '';
$gallery = array_values(array_filter(array_map(
static function ($line) {
return esc_url_raw(trim((string) $line));
},
preg_split('/\r\n|\r|\n/', $gallery_raw) ?: []
)));
$shipping_mode = isset($_POST['aksrt_shipping_mode']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_mode'])) : '';
$shipping_fee = isset($_POST['aksrt_shipping_fee']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee'])) : 0;
$shipping_fee_type = isset($_POST['aksrt_shipping_fee_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_fee_type'])) : 'fixed';
if (!in_array($shipping_fee_type, ['free', 'fixed', 'amount_free'], true)) {
$shipping_fee_type = 'fixed';
}
$shipping_fee_free = isset($_POST['aksrt_shipping_fee_free_amount']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee_free_amount'])) : 0;
$shipping_delivery = isset($_POST['aksrt_shipping_delivery_desc']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_delivery_desc'])) : '';
$shipping_tip = isset($_POST['aksrt_shipping_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_tip'])) : '';
$policy = isset($_POST['aksrt_after_sale_policy']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_after_sale_policy'])) : '';
$support_refund = !empty($_POST['aksrt_support_refund']) ? '1' : '0';
$service = isset($_POST['aksrt_service']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_service'])) : '';
$service_tip = isset($_POST['aksrt_service_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_service_tip'])) : '';
$limit_buy = isset($_POST['aksrt_limit_buy']) ? max(-1, min(9999, (int) wp_unslash($_POST['aksrt_limit_buy']))) : -1;
$limit_buy_note = isset($_POST['aksrt_limit_buy_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_limit_buy_note'])) : '';
$stock_type = isset($_POST['aksrt_stock_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_stock_type'])) : 'all';
if (!in_array($stock_type, ['all', 'opts'], true)) {
$stock_type = 'all';
}
$pay_modo = isset($_POST['aksrt_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_pay_modo'])) : '0';
if (!in_array($pay_modo, ['0', 'points'], true)) {
$pay_modo = '0';
}
$start_price = isset($_POST['aksrt_start_price']) ? max(0, (float) wp_unslash($_POST['aksrt_start_price'])) : 0;
$product_rebate = isset($_POST['aksrt_product_rebate']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_product_rebate'])) : '';
$product_params = isset($_POST['aksrt_product_params']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_product_params'])) : '';
$user_required = isset($_POST['aksrt_user_required']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_user_required'])) : '';
$delivery_note = isset($_POST['aksrt_delivery_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_delivery_note'])) : '';
$marketing = isset($_POST['aksrt_marketing_headline']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_marketing_headline'])) : '';
update_post_meta($post_id, '_aksrt_price', $price);
update_post_meta($post_id, '_aksrt_stock', $stock);
update_post_meta($post_id, '_aksrt_product_image', $image);
update_post_meta($post_id, '_aksrt_product_gallery', $gallery);
update_post_meta($post_id, '_aksrt_shipping_mode', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_type', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_fee', $shipping_fee);
update_post_meta($post_id, '_aksrt_shipping_fee_type', $shipping_fee_type);
update_post_meta($post_id, '_aksrt_shipping_fee_free_amount', $shipping_fee_free);
update_post_meta($post_id, '_aksrt_shipping_delivery_desc', $shipping_delivery);
update_post_meta($post_id, '_aksrt_shipping_tip', $shipping_tip);
update_post_meta($post_id, '_aksrt_after_sale_policy', $policy);
update_post_meta($post_id, '_aksrt_support_refund', $support_refund);
update_post_meta($post_id, '_aksrt_service', $service);
update_post_meta($post_id, '_aksrt_service_tip', $service_tip);
update_post_meta($post_id, '_aksrt_limit_buy', $limit_buy);
update_post_meta($post_id, '_aksrt_limit_buy_note', $limit_buy_note);
update_post_meta($post_id, '_aksrt_stock_type', $stock_type);
update_post_meta($post_id, '_aksrt_pay_modo', $pay_modo);
update_post_meta($post_id, '_aksrt_start_price', $start_price);
update_post_meta($post_id, '_aksrt_product_rebate', $product_rebate);
update_post_meta($post_id, '_aksrt_product_params', $product_params);
update_post_meta($post_id, '_aksrt_user_required', $user_required);
update_post_meta($post_id, '_aksrt_delivery_note', $delivery_note);
update_post_meta($post_id, '_aksrt_marketing_headline', $marketing);
// Keep sales counter present for list columns.
if (get_post_meta($post_id, '_aksrt_sales', true) === '') {
update_post_meta($post_id, '_aksrt_sales', 0);
}
}
}