如何通过 Select 实时更新价格且不刷新页面
结论
可以。PHP 在页面首次加载时输出各个计费周期的价格和购买地址。JavaScript 监听每个下拉框的 change 事件,只更新该下拉框所属的产品区域,不需要提交表单或调用 location.reload()。
如果多个下拉框中只有第一个生效,通常是因为它们使用了相同的 id。getElementById() 只会取得第一个匹配元素。应将重复的 id 改为 class,再用 closest() 定位当前产品。
PHP 数组应这样访问:
echo $price[$billing];
不能写成:
echo $price['$billing'];
后一种写法查找的是名为 $billing 的字面量键,而不是变量 $billing 对应的值。
推荐实现
假设每个产品使用以下数据结构:
<?php
$currency = 'USD';
$cycles = [
'monthly' => 'Monthly',
'quarterly' => 'Quarterly',
'semi-annually' => 'Semi-Annually',
'annually' => 'Annually',
'biennially' => 'Biennially',
'triennially' => 'Triennially',
];
$products = [
[
'id' => 101,
'name' => 'Product A',
'prices' => [
'monthly' => 10.00,
'quarterly' => 28.00,
'semi-annually' => 54.00,
'annually' => 100.00,
'biennially' => 190.00,
'triennially' => 270.00,
],
],
[
'id' => 102,
'name' => 'Product B',
'prices' => [
'monthly' => 20.00,
'quarterly' => 57.00,
'semi-annually' => 110.00,
'annually' => 205.00,
],
],
];
?>
渲染产品时,将价格表和购买链接表写入当前产品容器的 data-* 属性:
<?php foreach ($products as $product): ?>
<?php
// 只保留当前产品实际支持的计费周期
$prices = array_intersect_key($product['prices'], $cycles);
if ($prices === []) {
continue;
}
$defaultBilling = isset($prices['monthly'])
? 'monthly'
: array_key_first($prices);
$purchaseLinks = [];
foreach (array_keys($prices) as $billing) {
$purchaseLinks[$billing] = '/purchase.php?' . http_build_query([
'product_id' => $product['id'],
'billing' => $billing,
'currency' => $currency,
]);
}
$pricesJson = htmlspecialchars(
json_encode($prices, JSON_THROW_ON_ERROR),
ENT_QUOTES,
'UTF-8'
);
$linksJson = htmlspecialchars(
json_encode($purchaseLinks, JSON_THROW_ON_ERROR),
ENT_QUOTES,
'UTF-8'
);
?>
<article
class="product-card"
data-prices="<?= $pricesJson ?>"
data-links="<?= $linksJson ?>"
data-currency="<?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?>"
>
<h2><?= htmlspecialchars($product['name'], ENT_QUOTES, 'UTF-8') ?></h2>
<label>
Billing cycle
<select class="billing-select">
<?php foreach ($cycles as $value => $label): ?>
<?php if (array_key_exists($value, $prices)): ?>
<option
value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>"
<?= $value === $defaultBilling ? 'selected' : '' ?>
>
<?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</label>
<p>
Price:
<strong class="product-price">
<?= htmlspecialchars(
$currency . ' ' . number_format((float) $prices[$defaultBilling], 2),
ENT_QUOTES,
'UTF-8'
) ?>
</strong>
</p>
<a
class="purchase-link"
href="<?= htmlspecialchars(
$purchaseLinks[$defaultBilling],
ENT_QUOTES,
'UTF-8'
) ?>"
>
Click here to purchase
</a>
</article>
<?php endforeach; ?>
再用 JavaScript 统一监听所有 .billing-select:
document.addEventListener('change', (event) => {
if (!event.target.matches('.billing-select')) {
return;
}
const select = event.target;
const card = select.closest('.product-card');
if (!card) {
return;
}
const billing = select.value;
let prices;
let links;
try {
prices = JSON.parse(card.dataset.prices);
links = JSON.parse(card.dataset.links);
} catch (error) {
console.error('Invalid product data:', error);
return;
}
if (!Object.prototype.hasOwnProperty.call(prices, billing)) {
console.error(`Unsupported billing cycle: ${billing}`);
return;
}
const numericPrice = Number(prices[billing]);
if (!Number.isFinite(numericPrice)) {
console.error(`Invalid price for billing cycle: ${billing}`);
return;
}
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: card.dataset.currency
});
const priceElement = card.querySelector('.product-price');
const purchaseLink = card.querySelector('.purchase-link');
priceElement.textContent = formatter.format(numericPrice);
if (links[billing]) {
purchaseLink.href = links[billing];
}
});
为什么多个下拉框可以独立工作
每个产品都包含在单独的 .product-card 中。用户更改选项后,代码先执行:
select.closest('.product-card')
找到当前产品,然后只查询这个产品内部的价格和购买链接:
card.querySelector('.product-price');
card.querySelector('.purchase-link');
因此,更改 Product B 的计费周期不会影响 Product A。页面中也没有重复的 id="form_billing"。
事件监听器绑定在 document 上。以后即使动态添加产品卡片,也不必为新卡片重新绑定监听器。
不需要 Session 和表单提交
在这套实现中,计费周期只是浏览器端的显示状态,不必在用户每次切换时写入:
$_SESSION['billing']
也不必执行:
document.getElementById('form_billing').submit();
如果需要在刷新页面后保留用户的选择,再考虑使用 Session、Cookie 或 localStorage。保留选择和实时更新价格是两个独立需求。
安全注意事项
用户可以修改前端显示的价格和购买链接,因此服务器不能将这些数据作为最终结算依据。purchase.php 必须重新验证以下内容:
product_id是否存在;billing是否属于允许的计费周期;- 当前产品是否支持所选周期;
- 实际价格是否与服务器端数据库或配置一致;
currency是否受支持。
例如:
<?php
$allowedBillingCycles = [
'monthly',
'quarterly',
'semi-annually',
'annually',
'biennially',
'triennially',
];
$billing = $_GET['billing'] ?? '';
if (!in_array($billing, $allowedBillingCycles, true)) {
http_response_code(400);
exit('Invalid billing cycle');
}
// 继续根据 product_id 和 billing 从服务器端重新查询真实价格
如果价格经常变化,不适合直接嵌入页面,可以在 change 事件中通过 fetch() 请求后端接口。即使改用接口,仍应使用 .product-card、closest() 和 class 定位不同产品,不能让多个元素共用同一个 id。
备注:内容仅供参考。
