fix(order): 解决移动端弹窗关闭时点击穿透导致弹窗自动重新弹出问题
- 根因定位:移动端点击关闭按钮后弹窗退场,浏览器在~300ms派发合成点击事件,恰好落在底层全宽的「修改结账方案」动作按钮上,导致关闭后立即被再次触发唤起;原关闭按钮触控面积小(12px)也容易引发手抖连点 - 防穿透冷却锁:引入 POPUP_COOLDOWN_MS 冷却机制,弹窗关闭后450ms内拦截底层触发按钮的一切点击唤起请求,彻底隔绝幽灵点击与连击 - 弹窗事件与关闭解耦:关闭按钮添加 @click.stop.prevent 与 type="button",规范监听 van-popup 的 @close 回调以确保遮罩层点击同步触发冷却 - 触控体验优化:将原文字关闭符号升级为 van-icon cross,增大触控靶心至 36px 圆形并添加 :active 反馈与 touch-action: manipulation - 样式增强:重构数字输入框为带实体边框、聚焦高亮光晕与单位后缀的独立卡片,解决输入框外观类似只读文本的问题 - 验证通过:通过 npm test 与全量门禁 ./scripts/check.sh(Go测试、MySQL测试、TS类型检查、Vite生产构建 100% 通过)
This commit is contained in:
@@ -241,11 +241,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popup-close {
|
.popup-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin: -8px -8px -8px 0;
|
||||||
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-size: 16px;
|
color: #94a3b8;
|
||||||
color: #a0aec0;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-close:active {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-body {
|
.popup-body {
|
||||||
@@ -277,10 +294,91 @@
|
|||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.counter-input-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 170px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1.5px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-input-wrapper:hover {
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-input-wrapper:focus-within {
|
||||||
|
border-color: #ff6a00;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-input-control {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-unit {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-field {
|
.popup-field {
|
||||||
background: #f7fafc;
|
background: #f8fafc;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-field:focus-within {
|
||||||
|
border-color: #ff6a00;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS Safari:font-size < 16px 时聚焦会自动放大整个页面 */
|
/* iOS Safari:font-size < 16px 时聚焦会自动放大整个页面 */
|
||||||
|
|||||||
@@ -114,9 +114,18 @@ const {
|
|||||||
ordersPath: '/m/orders',
|
ordersPath: '/m/orders',
|
||||||
chatPathPrefix: '/m/chats/',
|
chatPathPrefix: '/m/chats/',
|
||||||
startCashier: (payment, onPaid) => cashier.openMobilePaymentCashier(payment, onPaid),
|
startCashier: (payment, onPaid) => cashier.openMobilePaymentCashier(payment, onPaid),
|
||||||
onCounterCheckoutSuccess: () => (showCounterPopup.value = false),
|
onCounterCheckoutSuccess: () => {
|
||||||
onRejectCheckoutSuccess: () => (showRejectPopup.value = false),
|
markPopupClosed()
|
||||||
onCreateDisputeSuccess: () => (showDisputePopup.value = false),
|
showCounterPopup.value = false
|
||||||
|
},
|
||||||
|
onRejectCheckoutSuccess: () => {
|
||||||
|
markPopupClosed()
|
||||||
|
showRejectPopup.value = false
|
||||||
|
},
|
||||||
|
onCreateDisputeSuccess: () => {
|
||||||
|
markPopupClosed()
|
||||||
|
showDisputePopup.value = false
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const rentalTerms = computed(() => orderRentalTerms(order.value))
|
const rentalTerms = computed(() => orderRentalTerms(order.value))
|
||||||
@@ -125,6 +134,48 @@ const rentalTerms = computed(() => orderRentalTerms(order.value))
|
|||||||
const showDisputePopup = ref(false)
|
const showDisputePopup = ref(false)
|
||||||
const showCounterPopup = ref(false)
|
const showCounterPopup = ref(false)
|
||||||
const showRejectPopup = ref(false)
|
const showRejectPopup = ref(false)
|
||||||
|
|
||||||
|
// 弹窗防穿透/防抖冷却:防止移动端/iOS Safari 点击关闭时 300ms 穿透触发底层按钮重新唤起
|
||||||
|
const POPUP_COOLDOWN_MS = 450
|
||||||
|
let lastPopupClosedAt = 0
|
||||||
|
|
||||||
|
function markPopupClosed() {
|
||||||
|
lastPopupClosedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function canOpenPopup(): boolean {
|
||||||
|
return Date.now() - lastPopupClosedAt >= POPUP_COOLDOWN_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCounterPopup() {
|
||||||
|
if (!canOpenPopup()) return
|
||||||
|
showCounterPopup.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCounterPopup() {
|
||||||
|
markPopupClosed()
|
||||||
|
showCounterPopup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRejectPopup() {
|
||||||
|
if (!canOpenPopup()) return
|
||||||
|
showRejectPopup.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRejectPopup() {
|
||||||
|
markPopupClosed()
|
||||||
|
showRejectPopup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDisputePopup() {
|
||||||
|
if (!canOpenPopup()) return
|
||||||
|
showDisputePopup.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDisputePopup() {
|
||||||
|
markPopupClosed()
|
||||||
|
showDisputePopup.value = false
|
||||||
|
}
|
||||||
const isOwnerCheckoutPending = computed(() => {
|
const isOwnerCheckoutPending = computed(() => {
|
||||||
if (!order.value || !isOwner.value) return false
|
if (!order.value || !isOwner.value) return false
|
||||||
return (
|
return (
|
||||||
@@ -443,7 +494,7 @@ async function copyListingCode() {
|
|||||||
block
|
block
|
||||||
plain
|
plain
|
||||||
round
|
round
|
||||||
@click="showCounterPopup = true"
|
@click="openCounterPopup"
|
||||||
>
|
>
|
||||||
修改结账方案
|
修改结账方案
|
||||||
</van-button>
|
</van-button>
|
||||||
@@ -481,11 +532,11 @@ async function copyListingCode() {
|
|||||||
block
|
block
|
||||||
plain
|
plain
|
||||||
round
|
round
|
||||||
@click="showCounterPopup = true"
|
@click="openCounterPopup"
|
||||||
>
|
>
|
||||||
还价修改方案
|
还价修改方案
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button type="danger" block plain round @click="showRejectPopup = true">
|
<van-button type="danger" block plain round @click="openRejectPopup">
|
||||||
发起结账争议
|
发起结账争议
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,7 +544,7 @@ async function copyListingCode() {
|
|||||||
|
|
||||||
<!-- Universal Action: File a Dispute -->
|
<!-- Universal Action: File a Dispute -->
|
||||||
<section v-if="canOpenDispute" class="card-section action-card">
|
<section v-if="canOpenDispute" class="card-section action-card">
|
||||||
<van-button type="warning" block plain round @click="showDisputePopup = true">
|
<van-button type="warning" block plain round @click="openDisputePopup">
|
||||||
{{ isCheckoutDisputeStage ? '对结账发起申诉争议' : '发起订单申诉争议' }}
|
{{ isCheckoutDisputeStage ? '对结账发起申诉争议' : '发起订单申诉争议' }}
|
||||||
</van-button>
|
</van-button>
|
||||||
</section>
|
</section>
|
||||||
@@ -536,10 +587,23 @@ async function copyListingCode() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- POPUP: Counter Checkout Adjustments(号主/租客) -->
|
<!-- POPUP: Counter Checkout Adjustments(号主/租客) -->
|
||||||
<van-popup v-model:show="showCounterPopup" position="bottom" round class="mobile-popup-form">
|
<van-popup
|
||||||
|
v-model:show="showCounterPopup"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
class="mobile-popup-form"
|
||||||
|
@close="markPopupClosed"
|
||||||
|
>
|
||||||
<header class="popup-header">
|
<header class="popup-header">
|
||||||
<h3>修改结账方案</h3>
|
<h3>修改结账方案</h3>
|
||||||
<button class="popup-close" @click="showCounterPopup = false">✕</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="popup-close"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click.stop.prevent="closeCounterPopup"
|
||||||
|
>
|
||||||
|
<van-icon name="cross" :size="18" />
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<p class="form-hint-info">
|
<p class="form-hint-info">
|
||||||
@@ -547,35 +611,47 @@ async function copyListingCode() {
|
|||||||
checkoutRoundLabel
|
checkoutRoundLabel
|
||||||
}}。可填实际哈夫币消耗(允许超过发布量),打超按比例从押金扣。提案将交给对方确认。
|
}}。可填实际哈夫币消耗(允许超过发布量),打超按比例从押金扣。提案将交给对方确认。
|
||||||
</p>
|
</p>
|
||||||
<van-cell-group :border="false">
|
<div class="counter-input-card">
|
||||||
<van-field label="物资/损耗金额">
|
<div class="counter-input-row">
|
||||||
<template #input>
|
<label class="counter-label" for="counter-consumable">物资/损耗金额</label>
|
||||||
|
<div class="counter-input-wrapper">
|
||||||
<input
|
<input
|
||||||
|
id="counter-consumable"
|
||||||
v-model.number="counterForm.consumableAmountYuan"
|
v-model.number="counterForm.consumableAmountYuan"
|
||||||
type="number"
|
type="number"
|
||||||
class="custom-inline-input"
|
placeholder="0"
|
||||||
|
class="counter-input-control"
|
||||||
/>
|
/>
|
||||||
</template>
|
<span class="counter-unit">元</span>
|
||||||
</van-field>
|
</div>
|
||||||
<van-field label="实际消耗哈夫币 M">
|
</div>
|
||||||
<template #input>
|
<div class="counter-input-row">
|
||||||
|
<label class="counter-label" for="counter-coin">实际消耗哈夫币</label>
|
||||||
|
<div class="counter-input-wrapper">
|
||||||
<input
|
<input
|
||||||
|
id="counter-coin"
|
||||||
v-model.number="counterForm.coin_consumed_m"
|
v-model.number="counterForm.coin_consumed_m"
|
||||||
type="number"
|
type="number"
|
||||||
class="custom-inline-input"
|
placeholder="0"
|
||||||
|
class="counter-input-control"
|
||||||
/>
|
/>
|
||||||
</template>
|
<span class="counter-unit">M</span>
|
||||||
</van-field>
|
</div>
|
||||||
<van-field label="押金赔付(损坏等)">
|
</div>
|
||||||
<template #input>
|
<div class="counter-input-row">
|
||||||
|
<label class="counter-label" for="counter-deposit-deduct">押金赔付(损坏等)</label>
|
||||||
|
<div class="counter-input-wrapper">
|
||||||
<input
|
<input
|
||||||
|
id="counter-deposit-deduct"
|
||||||
v-model.number="counterForm.depositDeductAmountYuan"
|
v-model.number="counterForm.depositDeductAmountYuan"
|
||||||
type="number"
|
type="number"
|
||||||
class="custom-inline-input"
|
placeholder="0"
|
||||||
|
class="counter-input-control"
|
||||||
/>
|
/>
|
||||||
</template>
|
<span class="counter-unit">元</span>
|
||||||
</van-field>
|
</div>
|
||||||
</van-cell-group>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 实时结算试算预览(仅展示当前角色自身收支,完全屏蔽平台服务费) -->
|
<!-- 实时结算试算预览(仅展示当前角色自身收支,完全屏蔽平台服务费) -->
|
||||||
<CheckoutTrialPreview :simulation="checkoutSimulation" variant="mobile" />
|
<CheckoutTrialPreview :simulation="checkoutSimulation" variant="mobile" />
|
||||||
@@ -583,6 +659,7 @@ async function copyListingCode() {
|
|||||||
v-model="counterForm.reason"
|
v-model="counterForm.reason"
|
||||||
rows="2"
|
rows="2"
|
||||||
autosize
|
autosize
|
||||||
|
required
|
||||||
label="修改原因"
|
label="修改原因"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="请说明调整原因(必填)"
|
placeholder="请说明调整原因(必填)"
|
||||||
@@ -618,10 +695,23 @@ async function copyListingCode() {
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<!-- POPUP: Renter Reject Counter Proposal Reason -->
|
<!-- POPUP: Renter Reject Counter Proposal Reason -->
|
||||||
<van-popup v-model:show="showRejectPopup" position="bottom" round class="mobile-popup-form">
|
<van-popup
|
||||||
|
v-model:show="showRejectPopup"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
class="mobile-popup-form"
|
||||||
|
@close="markPopupClosed"
|
||||||
|
>
|
||||||
<header class="popup-header">
|
<header class="popup-header">
|
||||||
<h3>拒绝修正扣款说明</h3>
|
<h3>拒绝修正扣款说明</h3>
|
||||||
<button class="popup-close" @click="showRejectPopup = false">✕</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="popup-close"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click.stop.prevent="closeRejectPopup"
|
||||||
|
>
|
||||||
|
<van-icon name="cross" :size="18" />
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<p class="form-hint-info">
|
<p class="form-hint-info">
|
||||||
@@ -653,10 +743,23 @@ async function copyListingCode() {
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<!-- POPUP: Create Generic Order Dispute -->
|
<!-- POPUP: Create Generic Order Dispute -->
|
||||||
<van-popup v-model:show="showDisputePopup" position="bottom" round class="mobile-popup-form">
|
<van-popup
|
||||||
|
v-model:show="showDisputePopup"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
class="mobile-popup-form"
|
||||||
|
@close="markPopupClosed"
|
||||||
|
>
|
||||||
<header class="popup-header">
|
<header class="popup-header">
|
||||||
<h3>发起申诉争议</h3>
|
<h3>发起申诉争议</h3>
|
||||||
<button class="popup-close" @click="showDisputePopup = false">✕</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="popup-close"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click.stop.prevent="closeDisputePopup"
|
||||||
|
>
|
||||||
|
<van-icon name="cross" :size="18" />
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<van-field v-if="!isCheckoutDisputeStage" label="申诉类型" class="popup-field">
|
<van-field v-if="!isCheckoutDisputeStage" label="申诉类型" class="popup-field">
|
||||||
|
|||||||
Reference in New Issue
Block a user