Filament 订单金额不更新问题解析及解决方案
2024-12-16 03:27:01
Filament资源:解决产品/数量更改时总计不更新的问题
在开发电商平台或订单管理系统时,动态计算并更新订单总金额是核心功能之一。当使用Filament构建这类应用时,如果遇到产品数量或种类变更后,订单总金额未实时更新的问题,会严重影响用户体验和数据的准确性。本文将深入分析该问题,并提供多种解决方案。
问题分析
提供的代码片段中,OrderResource
使用 Filament 的 Repeater
组件来管理订单明细。order_details
关系中包含了 product_id
, quantity
和 total
字段。代码逻辑通过 afterStateUpdated
钩子函数来监听字段变化并重新计算总金额。具体来说:
updateProductSelection
函数在产品选择变更时,更新产品单价并重新计算总金额。updateProductTotal
函数在产品数量变更时,更新单个产品总价并重新计算总金额。recalculateGrandTotal
函数计算最终的总金额,包括折扣。
问题在于,尽管日志显示产品和数量的更新是正确的,但总金额却停留在0,或者只有在折扣字段更改时才更新。这通常表明 recalculateGrandTotal
函数没有被正确触发,或者触发时获取到的 orderDetails
数据不正确。
解决方案
方案一:确保 Repeater
数据的同步性
Repeater
组件的数据处理有时会存在延迟,或者在 afterStateUpdated
钩子触发时,数据尚未完全同步。我们可以通过强制刷新数据或稍作延迟来解决此问题。
代码示例:
// OrderResource.php
use Filament\Forms;
use Filament\Resources\Form;
use Filament\Resources\Resource;
use App\Models\Order;
use App\Models\Product;
class OrderResource extends Resource
{
// ... 其他代码 ...
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Repeater::make('order_details')
->relationship('orderDetails')
->schema([
// ... 其他字段 ...
])
->afterStateUpdated(function(callable $get, callable $set){
// 稍作延迟后重新计算总金额
dispatch(function () use ($get, $set){
self::recalculateGrandTotal($get, $set);
})->delay(0.1);
})
,
// ... 其他字段 ...
]);
}
// ... 其他函数 ...
protected static function recalculateGrandTotal(callable $get, callable $set)
{
// 重新加载订单详情数据,确保数据同步
$orderDetails = [];
if (filled($get('order_details'))) {
foreach ($get('order_details') as $orderDetail){
if (is_array($orderDetail) && isset($orderDetail['product_id']) && isset($orderDetail['quantity'])) {
$product = Product::find($orderDetail['product_id']);
if ($product) {
$orderDetails[] = [
'total' => $product->price * $orderDetail['quantity']
];
}
} else if (is_int($orderDetail)){
$orderDetailModel = \App\Models\OrderDetail::find($orderDetail);
if ($orderDetailModel) {
$orderDetails[] = [
'total' => $orderDetailModel->total
];
}
}
}
}
$discount = (float) $get('discount') ?? 0;
$grandTotal = collect($orderDetails)->sum('total');
if ($discount > 0) {
$grandTotal -= ($grandTotal * $discount) / 100;
}
$set('grand_total', $grandTotal);
\Illuminate\Support\Facades\Log::info('Grand Total recalculated:', ['grandTotal' => $grandTotal, 'orderDetails' => $orderDetails]);
}
}
操作步骤:
- 在
OrderResource
文件中,修改form
函数,为order_details
Repeater
组件添加afterStateUpdated
钩子。 - 将原有的
recalculateGrandTotal
调用封装在dispatch
函数中,并添加短暂延迟。 - 修改
recalculateGrandTotal
函数,在函数顶部重新从数据库加载订单详情数据。
原理说明:
通过 dispatch
函数,我们可以将总金额的重新计算操作放入队列中,稍作延迟执行,确保此时 Repeater
组件的数据已经完成同步。同时,通过在recalculateGrandTotal
函数开始时重新加载订单详情数据,保证了计算总金额时使用的是最新的、已同步的数据。另外通过对 orderDetail
不同状态的判断(数组和直接的模型ID),可以处理不同的数据存储格式,增加代码的鲁棒性。
安全建议:
- 控制延迟时间,避免过长的延迟影响用户体验。
- 在实际应用中,可能需要结合具体的业务场景和数据量大小调整延迟时间。
- 考虑数据校验,确保
orderDetails
中的数据格式和内容是正确的,防止因数据错误导致计算错误或安全漏洞。
方案二:使用 JavaScript 触发计算
如果上述方案无法解决问题,或者希望实现更精细的控制,可以考虑使用 JavaScript 来监听字段变化并触发计算。
代码示例:
// OrderResource.php
use Filament\Forms;
use Filament\Resources\Form;
use Filament\Resources\Resource;
use App\Models\Order;
use App\Models\Product;
use Filament\Forms\Components\View;
class OrderResource extends Resource
{
// ... 其他代码 ...
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Repeater::make('order_details')
->relationship('orderDetails')
->schema([
// ... 其他字段 ...
]),
// ... 其他字段 ...
Forms\Components\TextInput::make('grand_total')->disabled(),
View::make('filament.order-total-script') //引入JS视图
]);
}
// ... 其他函数 ...
public static function getPages(): array
{
return [
'index' => \App\Filament\Resources\OrderResource\Pages\ListOrders::route('/'),
'create' => \App\Filament\Resources\OrderResource\Pages\CreateOrder::route('/create'),
'edit' => \App\Filament\Resources\OrderResource\Pages\EditOrder::route('/{record}/edit'),
];
}
}
// resources/views/filament/order-total-script.blade.php
<script>
document.addEventListener('DOMContentLoaded', function() {
let repeater = document.querySelector('[data-repeater="order_details"]');
if(repeater){
let observerConfig = { childList: true, subtree: true };
let observer = new MutationObserver(calculateGrandTotal);
observer.observe(repeater, observerConfig);
let discountField = document.querySelector('[data-field-name="discount"] input');
if (discountField){
discountField.addEventListener('input', calculateGrandTotal);
}
calculateGrandTotal();
}
function calculateGrandTotal() {
let orderDetails = [];
document.querySelectorAll('[data-repeater-item="order_details"]').forEach(item => {
let productId = item.querySelector('[name*="[product_id]"]').value;
let quantity = item.querySelector('[name*="[quantity]"]').value;
let priceElement = item.querySelector('[name*="[product_id]"]');
if (priceElement && quantity > 0 ) {
let price = 0;
let options = priceElement.options;
if (options) {
for (let i = 0; i < options.length; i++) {
if (options[i].value == productId) {
price = @json(\App\Models\Product::pluck('price', 'id')->toArray())[productId] ?? 0 ;
break;
}
}
}else {
let parentRepeater = priceElement.closest('[data-repeater-item]');
if (parentRepeater) {
let hiddenProductIdInput = parentRepeater.querySelector('input[type="hidden"][name$="[product_id]"]');
if (hiddenProductIdInput){
productId = hiddenProductIdInput.value;
price = @json(\App\Models\Product::pluck('price', 'id')->toArray())[productId] ?? 0;
}
}
}
orderDetails.push({ quantity: quantity, price: price });
}