feat: 实现混装产品插件基础框架和核心功能
添加 WooCommerce 混装产品插件的基础框架,包括: - 主插件文件和产品类型注册 - 后台配置界面和前端展示模板 - 购物车逻辑和价格计算 - 文档和资源文件 插件允许创建混装产品类型,配置可混装的简单商品和最小数量,并在前端按分类展示选择表单。加入购物车时自动计算所选商品总价并显示组件摘要。
This commit is contained in:
commit
a1c47d3a57
|
|
@ -0,0 +1,96 @@
|
|||
# Yoone Product Bundles (混装产品)
|
||||
|
||||
为 WooCommerce 提供一个基础的“混装产品(Bundle)”类型:
|
||||
|
||||
- 后台为某个产品配置:可混装的简单商品列表、最小混装数量、展示用的分类分组;
|
||||
- 前端在该混装产品的产品页显示一个表单,按分类分组列出可混装商品,用户为每个商品填写数量;
|
||||
- 当总数量达到或者超过“最小混装数量”时,允许加入购物车;
|
||||
- 购物车中该混装产品的行项目价格 = 所选简单商品单价 × 数量 的总和;
|
||||
- 购物车与订单行项目显示所选组件的摘要;
|
||||
|
||||
本插件是一个可运行的 MVP,结构清晰、注释完整,方便后续扩展(动态价格、库存校验、分组规则、变体商品支持、打包折扣等)。
|
||||
|
||||
## 安装与启用
|
||||
|
||||
1. 将插件目录 `yoone-product-bundles` 放置到站点的 `wp-content/plugins/` 下。
|
||||
2. 在 WordPress 后台 -> 插件 中启用 “Yoone Product Bundles (混装产品)”。
|
||||
3. 确保 WooCommerce 已安装并启用。
|
||||
|
||||
## 使用步骤(后台)
|
||||
|
||||
1. 新建或编辑一个产品,在“产品类型”下拉中选择 “混装产品 (Yoone Bundle)”。
|
||||
2. 在出现的 “混装产品” 标签页中配置:
|
||||
- 可混装商品:搜索并选择多个 simple product;
|
||||
- 最小混装数量:顾客选择的总数需不小于此值;
|
||||
- 展示分类:用于在前端按分类分组显示可混装商品(仅显示属于所选分类的商品)。
|
||||
3. 发布或更新该产品。
|
||||
|
||||
## 前端体验
|
||||
|
||||
访问该混装产品的产品页:
|
||||
|
||||
- 页面按分类分组列出可混装商品,并显示每个商品的价格;
|
||||
- 用户为每个商品填写数量,页面实时展示“当前选择数量”;
|
||||
- 当选择总数达到最小混装数量后,“加入购物车”按钮自动可用;
|
||||
- 加入购物车后,购物车中该混装产品的价格为所选商品价格总和,并显示所选内容的摘要;
|
||||
|
||||
## 技术实现概览
|
||||
|
||||
- 产品类型注册:`Yoone_Product_Bundles` 在 `product_type_selector` 中注册类型标识 `yoone_bundle`,并通过 `woocommerce_product_class` 映射到 `WC_Product_Yoone_Bundle`;
|
||||
- 后台配置面板:`Yoone_Product_Bundles_Admin` 在产品编辑页添加一个标签页与面板,使用 WooCommerce 原生的 `wc-product-search` 组件搜索选择产品;数据保存在以下 postmeta:
|
||||
- `_yoone_bundle_allowed_products` (int[]) 可混装的产品ID列表;
|
||||
- `_yoone_bundle_min_quantity` (int) 最小混装数量;
|
||||
- `_yoone_bundle_categories` (int[]) 用于分组显示的 product_cat 分类ID列表;
|
||||
- 前端表单与购物车:`Yoone_Product_Bundles_Frontend`
|
||||
- 通过 `woocommerce_yoone_bundle_add_to_cart` 钩子渲染模板 `templates/single-product/add-to-cart/yoone-bundle.php`;
|
||||
- 在 `woocommerce_add_to_cart_validation` 中校验总数量是否达到最小值;
|
||||
- 在 `woocommerce_add_cart_item_data` 中把所选组件(pid=>qty)存入购物车项;
|
||||
- 在 `woocommerce_get_item_data` 中展示购物车行项目的组件摘要;
|
||||
- 在 `woocommerce_before_calculate_totals` 中根据组件动态设置该行项目价格(基础价,不含税),确保结算金额正确;
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
yoone-product-bundles/
|
||||
├── yoone-product-bundles.php // 插件入口
|
||||
├── includes/
|
||||
│ ├── class-yoone-product-bundles.php // 核心:类型注册、读取配置
|
||||
│ ├── class-yoone-product-type-bundle.php // 产品类型对象 WC_Product_Yoone_Bundle
|
||||
│ ├── admin/
|
||||
│ │ └── class-yoone-product-bundles-admin.php
|
||||
│ └── frontend/
|
||||
│ └── class-yoone-product-bundles-frontend.php
|
||||
├── templates/
|
||||
│ └── single-product/add-to-cart/yoone-bundle.php // 前端表单模板
|
||||
└── assets/
|
||||
├── css/{admin.css, frontend.css}
|
||||
└── js/{admin.js, frontend.js}
|
||||
```
|
||||
|
||||
## 钩子与扩展点
|
||||
|
||||
- 读取配置:`Yoone_Product_Bundles::get_bundle_config($product)`,返回 `allowed_products/min_qty/categories`;
|
||||
- 可在 `Yoone_Product_Bundles_Frontend::adjust_bundle_price()` 中替换价格计算逻辑,例如引入折扣、不同计价规则;
|
||||
- 可添加库存校验:在 `validate_add_to_cart()` 中检验每个组件的可售数量;
|
||||
- 可将组件拆分为独立行项目:在 `add_to_cart` 阶段为每个组件添加到购物车,并将 bundle 项设为 0 价(当前实现为一个行项目合计价格)。
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. 为什么“加入购物车”不可用?
|
||||
- 需要先达到“最小混装数量”;
|
||||
- 或者后台未配置可混装商品。
|
||||
2. 价格是否含税?
|
||||
- 当前以 `get_price()` 基础价计算总价,不含税;实际显示与结算是否含税取决于 WooCommerce 税设置。可在价格调整钩子中自定义。
|
||||
3. 是否支持变体商品?
|
||||
- MVP 仅支持 simple product;后续可扩展到变体商品与多层组合。
|
||||
|
||||
## 参考实现
|
||||
|
||||
可参考以下现有插件:
|
||||
|
||||
- `woocommerce-product-bundles_v8.5.2`
|
||||
- `wpc-composite-products-premium_v7.6.2`
|
||||
- `woocommerce-composite-products_v11.0.1`
|
||||
- `woo-product-bundle-premium_v8.3.5`
|
||||
|
||||
这些插件实现更复杂的组合逻辑与定价;本插件以轻量的方式提供一个可扩展的基础框架。
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/* Yoone Product Bundles 后台样式(简版) */
|
||||
#yoone_bundle_data .description { color: #666; display: block; margin-top: 6px; }
|
||||
#yoone_bundle_data .form-field { margin-bottom: 12px; }
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/* Yoone Product Bundles 前端样式(简版) */
|
||||
.yoone-bundle-form { margin-top: 1em; }
|
||||
.yoone-bundle-group { margin-bottom: 1.5em; }
|
||||
.yoone-bundle-group-title { margin: 0.5em 0; font-weight: 600; }
|
||||
.yoone-bundle-table { width: 100%; border-collapse: collapse; }
|
||||
.yoone-bundle-table th, .yoone-bundle-table td { border-bottom: 1px solid #eee; padding: 8px; }
|
||||
.yoone-bundle-meta { display: flex; gap: 1em; align-items: center; }
|
||||
.yoone-bundle-actions { margin-top: 1em; }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function($){
|
||||
// 预留:后台增强脚本(例如联动过滤 simple products、校验最小数量等)。
|
||||
})(jQuery);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function($){
|
||||
// 预留:如需在前端动态计算价格或增强交互,可在此添加脚本。
|
||||
})(jQuery);
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
开发一个名为 `yoone-product-bundles` 的 WooCommerce 插件,实现混装产品管理功能。以下是详细需求说明和技术实现方案:
|
||||
|
||||
1. 插件基础框架:
|
||||
- 创建标准的 WordPress 插件目录结构
|
||||
- 包含主插件文件 `yoone-product-bundles.php` 并添加必要的插件头信息
|
||||
- 实现 WooCommerce 插件激活/卸载钩子
|
||||
- 建立国际化支持(textdomain: yoone-product-bundles)
|
||||
|
||||
2. 产品类型管理:
|
||||
- 注册新的产品类型 'bundle'
|
||||
- 实现产品类型类继承 WC_Product
|
||||
- 添加必要的产品数据存储字段
|
||||
- 创建产品编辑界面的 metaboxes
|
||||
|
||||
1. 混装产品配置:
|
||||
- 后台配置界面:
|
||||
* 可添加的 simple product 选择器(支持多选)
|
||||
* 每个产品的最小数量配置(正整数)
|
||||
* 混装产品分类配置(支持多级分类)
|
||||
* 价格计算规则配置(固定价或组件总和)
|
||||
- 数据存储:
|
||||
* 使用 WooCommerce 标准数据存储机制
|
||||
* 创建必要的数据库表扩展
|
||||
* 实现数据验证和清理
|
||||
|
||||
1. 前端功能:
|
||||
- 混装产品页面模板:
|
||||
* 显示配置的分类层级
|
||||
* 每个产品项显示:
|
||||
- 产品图片
|
||||
- 产品名称
|
||||
- 数量选择器(带最小值验证)
|
||||
- 实时价格计算
|
||||
* 加入购物车按钮(满足最小数量时启用)
|
||||
- 购物车/订单显示:
|
||||
* 显示混装产品组成明细
|
||||
* 保持组件产品信息关联
|
||||
|
||||
1. 代码规范:
|
||||
- 遵循 WordPress 编码标准
|
||||
- 所有方法添加详细注释,包括:
|
||||
* 功能说明
|
||||
* 参数说明
|
||||
* 返回值说明
|
||||
* 涉及的 WooCommerce 钩子
|
||||
- 关键操作添加日志记录
|
||||
- 实现必要的安全验证(nonce、权限检查等)
|
||||
|
||||
1. 文档:
|
||||
- 完整的 README.md 包含:
|
||||
* 插件功能概述
|
||||
* 安装说明
|
||||
* 配置指南
|
||||
* 截图示例
|
||||
* 常见问题
|
||||
- 代码内文档(PHPDoc 标准)
|
||||
|
||||
1. 参考实现:
|
||||
- woocommerce-product-bundles_v8.5.2
|
||||
- wpc-composite-products-premium_v7.6.2
|
||||
- woo-product-bundle-premium_v8.3.5
|
||||
- yith-woocommerce-product-bundles-premium
|
||||
1. 测试要求:
|
||||
- 单元测试覆盖核心功能
|
||||
- 集成测试验证 WooCommerce 兼容性
|
||||
- 前端兼容性测试(响应式设计)
|
||||
- 性能测试(大数据量场景)
|
||||
|
||||
请按照以上需求实现插件,保持代码结构清晰并确保所有功能点都有详细注释说明实现逻辑。
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
/**
|
||||
* 后台:为混装产品提供配置界面(可混装商品、最小数量、分类)。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Product_Bundles_Admin {
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// 添加一个产品数据标签页
|
||||
add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tab'));
|
||||
// 对应的面板内容
|
||||
add_action('woocommerce_product_data_panels', array($this, 'render_product_data_panel'));
|
||||
// 保存配置
|
||||
add_action('woocommerce_admin_process_product_meta', array($this, 'save_product_meta'));
|
||||
}
|
||||
|
||||
public function add_product_data_tab($tabs) {
|
||||
$tabs['yoone_bundle'] = array(
|
||||
'label' => __('混装产品', 'yoone-product-bundles'),
|
||||
'target' => 'yoone_bundle_data',
|
||||
'class' => array('show_if_yoone_bundle'), // 仅当类型为 yoone_bundle 时显示
|
||||
'priority' => 70,
|
||||
);
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
public function render_product_data_panel() {
|
||||
global $post;
|
||||
$product = wc_get_product($post->ID);
|
||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||
$allowed = $config['allowed_products'];
|
||||
$min_qty = $config['min_qty'];
|
||||
$cats = $config['categories'];
|
||||
|
||||
echo '<div id="yoone_bundle_data" class="panel woocommerce_options_panel hidden">';
|
||||
echo '<div class="options_group">';
|
||||
echo '<p>' . esc_html__('配置可混装的简单商品、最小混装数量以及前端展示的分类分组。', 'yoone-product-bundles') . '</p>';
|
||||
|
||||
// 可混装商品:使用 Woo 的产品搜索(select2),multiple
|
||||
echo '<p class="form-field"><label>' . esc_html__('可混装商品', 'yoone-product-bundles') . '</label>';
|
||||
echo '<select class="wc-product-search" multiple style="width: 90%;" name="yoone_bundle_allowed_products[]" data-placeholder="' . esc_attr__('选择可混装的简单商品…', 'yoone-product-bundles') . '" data-action="woocommerce_json_search_products_and_variations">';
|
||||
if (! empty($allowed)) {
|
||||
foreach ($allowed as $pid) {
|
||||
$p = wc_get_product($pid);
|
||||
if ($p) {
|
||||
printf('<option value="%d" selected>%s</option>', $pid, esc_html($p->get_formatted_name()));
|
||||
}
|
||||
}
|
||||
}
|
||||
echo '</select>';
|
||||
echo '<span class="description">' . esc_html__('仅支持 simple product;变体商品可在后续版本支持。', 'yoone-product-bundles') . '</span>';
|
||||
echo '</p>';
|
||||
|
||||
// 最小混装数量
|
||||
woocommerce_wp_text_input(array(
|
||||
'id' => 'yoone_bundle_min_quantity',
|
||||
'label' => __('最小混装数量', 'yoone-product-bundles'),
|
||||
'type' => 'number',
|
||||
'desc_tip' => true,
|
||||
'description' => __('顾客选择的总数量需不小于该值,方可加入购物车。', 'yoone-product-bundles'),
|
||||
'value' => $min_qty,
|
||||
'custom_attributes' => array('min' => '0'),
|
||||
));
|
||||
|
||||
// 展示分类(product_cat)
|
||||
echo '<p class="form-field"><label>' . esc_html__('前端展示分类', 'yoone-product-bundles') . '</label>';
|
||||
echo '<select class="wc-enhanced-select" multiple style="width: 90%;" name="yoone_bundle_categories[]" data-placeholder="' . esc_attr__('选择用于分组展示的分类…', 'yoone-product-bundles') . '">';
|
||||
$terms = get_terms(array('taxonomy' => 'product_cat', 'hide_empty' => false));
|
||||
foreach ($terms as $t) {
|
||||
$selected = in_array($t->term_id, $cats, true) ? 'selected' : '';
|
||||
printf('<option value="%d" %s>%s</option>', $t->term_id, $selected, esc_html($t->name));
|
||||
}
|
||||
echo '</select>';
|
||||
echo '<span class="description">' . esc_html__('用于在前端页面按分类分组展示可混装商品(仅展示与所选分类匹配的可混装商品)。', 'yoone-product-bundles') . '</span>';
|
||||
echo '</p>';
|
||||
|
||||
echo '</div>'; // options_group
|
||||
echo '</div>'; // panel
|
||||
}
|
||||
|
||||
public function save_product_meta($post_id) {
|
||||
$product = wc_get_product($post_id);
|
||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return;
|
||||
|
||||
// 保存 allowed products
|
||||
$allowed = isset($_POST['yoone_bundle_allowed_products']) ? (array) $_POST['yoone_bundle_allowed_products'] : array();
|
||||
$allowed = array_values(array_filter(array_map('absint', $allowed)));
|
||||
update_post_meta($post_id, Yoone_Product_Bundles::META_ALLOWED_PRODUCTS, $allowed);
|
||||
|
||||
// 保存 min qty
|
||||
$min_qty = isset($_POST['yoone_bundle_min_quantity']) ? absint($_POST['yoone_bundle_min_quantity']) : 0;
|
||||
update_post_meta($post_id, Yoone_Product_Bundles::META_MIN_QTY, max(0, $min_qty));
|
||||
|
||||
// 保存 categories
|
||||
$cats = isset($_POST['yoone_bundle_categories']) ? (array) $_POST['yoone_bundle_categories'] : array();
|
||||
$cats = array_values(array_filter(array_map('absint', $cats)));
|
||||
update_post_meta($post_id, Yoone_Product_Bundles::META_CATEGORIES, $cats);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
/**
|
||||
* 核心:注册产品类型、常量、工具方法。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Product_Bundles {
|
||||
const TYPE = 'yoone_bundle';
|
||||
|
||||
// 配置的 postmeta 键名
|
||||
const META_ALLOWED_PRODUCTS = '_yoone_bundle_allowed_products'; // array<int>
|
||||
const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int
|
||||
const META_CATEGORIES = '_yoone_bundle_categories'; // array<int> product_cat term_ids
|
||||
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// 将产品类型加入选择器
|
||||
add_filter('product_type_selector', array($this, 'register_product_type_in_selector'));
|
||||
// 将类型映射到我们的 WC_Product 派生类
|
||||
add_filter('woocommerce_product_class', array($this, 'map_product_class'), 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在后台“产品类型”下拉中添加“混装产品”。
|
||||
*/
|
||||
public function register_product_type_in_selector($types) {
|
||||
$types[self::TYPE] = __('混装产品 (Yoone Bundle)', 'yoone-product-bundles');
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将类型映射到类名,WooCommerce 会实例化对应产品对象。
|
||||
*/
|
||||
public function map_product_class($classname, $product_type) {
|
||||
if ($product_type === self::TYPE) {
|
||||
$classname = 'WC_Product_Yoone_Bundle';
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并规范化 bundle 配置。
|
||||
* @param int|WC_Product $product
|
||||
* @return array{allowed_products:int[],min_qty:int,categories:int[]}
|
||||
*/
|
||||
public static function get_bundle_config($product) {
|
||||
$product = is_numeric($product) ? wc_get_product($product) : $product;
|
||||
if (! $product) return array('allowed_products' => array(), 'min_qty' => 0, 'categories' => array());
|
||||
$pid = $product->get_id();
|
||||
$allowed = get_post_meta($pid, self::META_ALLOWED_PRODUCTS, true);
|
||||
$min = absint(get_post_meta($pid, self::META_MIN_QTY, true));
|
||||
$cats = get_post_meta($pid, self::META_CATEGORIES, true);
|
||||
$allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array();
|
||||
$cats = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
||||
return array(
|
||||
'allowed_products' => $allowed,
|
||||
'min_qty' => max(0, $min),
|
||||
'categories' => $cats,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* 产品类型对象:WC_Product_Yoone_Bundle
|
||||
* - 定义产品类型标识;
|
||||
* - 可选择设置为售卖方式(例如仅允许单件购买)。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class WC_Product_Yoone_Bundle extends WC_Product {
|
||||
public function __construct($product) {
|
||||
parent::__construct($product);
|
||||
$this->set_virtual(true); // 混装产品不作为单独实物发货,实际发货明细由所选组件决定(MVP 简化)
|
||||
}
|
||||
|
||||
public function get_type() {
|
||||
return Yoone_Product_Bundles::TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 混装产品通常只允许单件购买(其内部包含多个组件)。
|
||||
*/
|
||||
public function is_sold_individually() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
/**
|
||||
* 前端:渲染混装产品的自定义 add-to-cart 表单、校验并加入购物车。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Product_Bundles_Frontend {
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// 用我们的模板替换默认的 add-to-cart 按钮
|
||||
add_action('woocommerce_' . Yoone_Product_Bundles::TYPE . '_add_to_cart', array($this, 'render_add_to_cart')); // 显示表单
|
||||
|
||||
// 处理加入购物车:校验数量并打包所选组件数据
|
||||
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 6);
|
||||
add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
|
||||
|
||||
// 在购物车/订单中显示组件摘要
|
||||
add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2);
|
||||
|
||||
// 结算前动态调整价格
|
||||
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_bundle_price'), 20, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染自定义表单:按所选分类分组显示允许的简单商品,提供数量输入。
|
||||
*/
|
||||
public function render_add_to_cart() {
|
||||
wc_get_template('single-product/add-to-cart/yoone-bundle.php', array(), '', YOONE_PB_PATH . 'templates/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验:总选择数量 >= min_qty
|
||||
*/
|
||||
public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id, $variations, $cart_item_data) {
|
||||
$product = wc_get_product($product_id);
|
||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return $passed;
|
||||
|
||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||
$min = max(0, absint($config['min_qty']));
|
||||
|
||||
// 从 POST 中收集数量
|
||||
$selected = isset($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array();
|
||||
$total_qty = 0;
|
||||
foreach ($selected as $pid => $qty) {
|
||||
$qty = absint($qty);
|
||||
if ($qty > 0) $total_qty += $qty;
|
||||
}
|
||||
|
||||
if ($total_qty < $min) {
|
||||
wc_add_notice(sprintf(__('请至少选择 %d 件混装组件后再加入购物车。', 'yoone-product-bundles'), $min), 'error');
|
||||
return false;
|
||||
}
|
||||
return $passed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所选组件数据存入购物车项。
|
||||
*/
|
||||
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) return $cart_item_data;
|
||||
|
||||
$selected = isset($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array();
|
||||
// 仅保留大于 0 的数量
|
||||
$components = array();
|
||||
foreach ($selected as $pid => $qty) {
|
||||
$pid = absint($pid);
|
||||
$qty = absint($qty);
|
||||
if ($pid && $qty > 0) $components[$pid] = $qty;
|
||||
}
|
||||
if (! empty($components)) {
|
||||
$cart_item_data['yoone_bundle_components'] = $components;
|
||||
}
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在购物车行项目中展示所选组件摘要。
|
||||
*/
|
||||
public function display_cart_item_data($item_data, $cart_item) {
|
||||
if (! empty($cart_item['yoone_bundle_components'])) {
|
||||
$lines = array();
|
||||
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
||||
$p = wc_get_product($pid);
|
||||
if ($p) {
|
||||
$lines[] = sprintf('%s × %d', $p->get_name(), $qty);
|
||||
}
|
||||
}
|
||||
if (! empty($lines)) {
|
||||
$item_data[] = array(
|
||||
'key' => __('混装内容', 'yoone-product-bundles'),
|
||||
'value' => implode("\n", $lines),
|
||||
'display' => implode('<br/>', array_map('esc_html', $lines)),
|
||||
);
|
||||
}
|
||||
}
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算前根据组件动态调整混装产品的行项目价格(= 所选简单商品单价 × 数量 的总和)。
|
||||
* 注意:这里使用的是产品当前价格(不含税的基础价),是否包含税由 WooCommerce 税设置决定。
|
||||
*/
|
||||
public function adjust_bundle_price($cart) {
|
||||
if (is_admin() && ! defined('DOING_AJAX')) {
|
||||
return; // 后台不处理
|
||||
}
|
||||
if (empty($cart) || ! method_exists($cart, 'get_cart')) return;
|
||||
|
||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||
if (empty($cart_item['yoone_bundle_components'])) continue;
|
||||
$product = isset($cart_item['data']) ? $cart_item['data'] : null;
|
||||
if (! $product || ! is_a($product, 'WC_Product')) continue;
|
||||
if ($product->get_type() !== Yoone_Product_Bundles::TYPE) continue;
|
||||
|
||||
$total = 0.0;
|
||||
foreach ($cart_item['yoone_bundle_components'] as $pid => $qty) {
|
||||
$p = wc_get_product($pid);
|
||||
if (! $p) continue;
|
||||
$unit = floatval($p->get_price()); // 基础价
|
||||
$total += $unit * absint($qty);
|
||||
}
|
||||
// 将产品行项目价格设为总价
|
||||
$product->set_price($total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
/**
|
||||
* 模板:单个混装产品的 add-to-cart 表单
|
||||
* 路径:templates/single-product/add-to-cart/yoone-bundle.php
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
global $product;
|
||||
if (! $product || $product->get_type() !== Yoone_Product_Bundles::TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = Yoone_Product_Bundles::get_bundle_config($product);
|
||||
$allowed = $config['allowed_products'];
|
||||
$min_qty = max(0, absint($config['min_qty']));
|
||||
$cat_ids = $config['categories'];
|
||||
|
||||
// 收集允许的商品对象,且必须是 simple product
|
||||
$allowed_products = array();
|
||||
foreach ($allowed as $pid) {
|
||||
$p = wc_get_product($pid);
|
||||
if ($p && $p->is_type('simple')) {
|
||||
$allowed_products[$pid] = $p;
|
||||
}
|
||||
}
|
||||
|
||||
// 按分类分组(如果未配置分类,则全部在一个组内)
|
||||
$groups = array();
|
||||
if (! empty($cat_ids)) {
|
||||
foreach ($allowed_products as $pid => $p) {
|
||||
$terms = get_the_terms($pid, 'product_cat');
|
||||
$matched = false;
|
||||
if (is_array($terms)) {
|
||||
foreach ($terms as $t) {
|
||||
if (in_array($t->term_id, $cat_ids, true)) {
|
||||
$groups[$t->term_id]['term'] = $t;
|
||||
$groups[$t->term_id]['items'][] = $p;
|
||||
$matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 若商品不属于所选分类,则不显示
|
||||
if (! $matched) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$groups[0] = array('term' => null, 'items' => array_values($allowed_products));
|
||||
}
|
||||
|
||||
// 表单开始
|
||||
?>
|
||||
<form class="cart yoone-bundle-form" action="<?php echo esc_url( apply_filters( 'woocommerce_add_to_cart_form_action', $product->get_permalink() ) ); ?>" method="post" enctype="multipart/form-data">
|
||||
<div class="yoone-bundle-meta">
|
||||
<p class="yoone-bundle-min"><?php printf( esc_html__('至少选择 %d 件混装组件', 'yoone-product-bundles'), $min_qty ); ?></p>
|
||||
<p class="yoone-bundle-selected"><?php esc_html_e('当前选择数量:', 'yoone-product-bundles'); ?><span class="yoone-bundle-selected-count">0</span></p>
|
||||
</div>
|
||||
|
||||
<?php if (empty($groups)) : ?>
|
||||
<p><?php esc_html_e('暂无可混装商品,请在后台为该混装产品配置。', 'yoone-product-bundles'); ?></p>
|
||||
<?php else : ?>
|
||||
<?php foreach ($groups as $gid => $group) : ?>
|
||||
<div class="yoone-bundle-group">
|
||||
<?php if (! empty($group['term'])) : ?>
|
||||
<h3 class="yoone-bundle-group-title"><?php echo esc_html($group['term']->name); ?></h3>
|
||||
<?php else : ?>
|
||||
<h3 class="yoone-bundle-group-title"><?php esc_html_e('可混装商品', 'yoone-product-bundles'); ?></h3>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="yoone-bundle-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('商品', 'yoone-product-bundles'); ?></th>
|
||||
<th><?php esc_html_e('价格', 'yoone-product-bundles'); ?></th>
|
||||
<th style="width:140px;"><?php esc_html_e('数量', 'yoone-product-bundles'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($group['items'] as $item) : ?>
|
||||
<?php $pid = $item->get_id(); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?php echo esc_url(get_permalink($pid)); ?>" target="_blank"><?php echo esc_html($item->get_name()); ?></a>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo wp_kses_post($item->get_price_html()); ?>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" min="0" step="1" class="yoone-bundle-qty" name="yoone_bundle_components[<?php echo esc_attr($pid); ?>]" value="0" />
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="yoone-bundle-actions">
|
||||
<input type="hidden" name="add-to-cart" value="<?php echo esc_attr($product->get_id()); ?>" />
|
||||
<button type="submit" class="single_add_to_cart_button button alt" disabled><?php esc_html_e('加入购物车', 'yoone-product-bundles'); ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
function updateState(){
|
||||
var total = 0;
|
||||
$('.yoone-bundle-qty').each(function(){
|
||||
var v = parseInt($(this).val(), 10);
|
||||
if (!isNaN(v) && v > 0) total += v;
|
||||
});
|
||||
$('.yoone-bundle-selected-count').text(total);
|
||||
var min = <?php echo (int) $min_qty; ?>;
|
||||
var btn = $('.yoone-bundle-actions .single_add_to_cart_button');
|
||||
if (total >= min) {
|
||||
btn.prop('disabled', false);
|
||||
} else {
|
||||
btn.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
$(document).on('change keyup', '.yoone-bundle-qty', updateState);
|
||||
$(document).ready(updateState);
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Yoone Product Bundles (混装产品)
|
||||
* Description: 为 WooCommerce 提供“混装产品(Bundle)”类型:在后台为某个产品配置可混装的简单商品与最小混装数量;前端允许顾客按分类选择混装商品与数量,当达到最小数量时可加入购物车结算,价格为所选商品的价格总和。
|
||||
* Author: Yoone
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 6.0
|
||||
* WC tested up to: 8.x
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// 简单防御:确保 WooCommerce 已激活
|
||||
if (! function_exists('WC')) {
|
||||
add_action('admin_notices', function () {
|
||||
echo '<div class="notice notice-error"><p>Yoone Product Bundles 需要启用 WooCommerce。</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 插件常量(路径)
|
||||
define('YOONE_PB_PATH', plugin_dir_path(__FILE__));
|
||||
define('YOONE_PB_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
// 自动加载(简单版):按需引入类文件
|
||||
require_once YOONE_PB_PATH . 'includes/class-yoone-product-bundles.php';
|
||||
require_once YOONE_PB_PATH . 'includes/class-yoone-product-type-bundle.php';
|
||||
require_once YOONE_PB_PATH . 'includes/admin/class-yoone-product-bundles-admin.php';
|
||||
require_once YOONE_PB_PATH . 'includes/frontend/class-yoone-product-bundles-frontend.php';
|
||||
|
||||
// 引导插件
|
||||
add_action('plugins_loaded', function () {
|
||||
// 注册产品类型
|
||||
Yoone_Product_Bundles::instance();
|
||||
Yoone_Product_Bundles_Admin::instance();
|
||||
Yoone_Product_Bundles_Frontend::instance();
|
||||
});
|
||||
|
||||
// 资源加载(前端样式/脚本)
|
||||
add_action('wp_enqueue_scripts', function () {
|
||||
wp_register_style('yoone-pb-frontend', YOONE_PB_URL . 'assets/css/frontend.css', array(), '0.1.0');
|
||||
wp_register_script('yoone-pb-frontend', YOONE_PB_URL . 'assets/js/frontend.js', array('jquery'), '0.1.0', true);
|
||||
});
|
||||
|
||||
// 后台资源
|
||||
add_action('admin_enqueue_scripts', function ($hook) {
|
||||
// 仅在产品编辑页加载(post.php/post-new.php 且 post_type=product)
|
||||
if (strpos($hook, 'post.php') !== false || strpos($hook, 'post-new.php') !== false) {
|
||||
$screen = get_current_screen();
|
||||
if ($screen && 'product' === $screen->post_type) {
|
||||
wp_enqueue_style('woocommerce_admin_styles');
|
||||
wp_enqueue_script('wc-product-search'); // Woo 的 select2 产品搜索
|
||||
wp_enqueue_style('yoone-pb-admin', YOONE_PB_URL . 'assets/css/admin.css', array(), '0.1.0');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 激活钩子:无需建表,使用 postmeta 保存配置;此处可预置默认值或做数据迁移
|
||||
register_activation_hook(__FILE__, function () {
|
||||
// 预留:未来若需要自定义 DB 表,可在此创建。
|
||||
});
|
||||
Loading…
Reference in New Issue