- 哈夫币消耗
- 默认全部使用完成
+ 哈夫币实际消耗
+ 可超过发布量
- 快照 {{ quantity(props.snapshotHafCoinM) }}M,预计剩余
- {{ quantity(props.remainingHafCoinM) }}M
+ 发布 {{ quantity(props.snapshotHafCoinM) }}M
+
+ ,预计剩余 {{ quantity(props.remainingHafCoinM) }}M
+
+
+ ,打超 {{ quantity(-props.remainingHafCoinM) }}M(按比例从押金扣)
+
@@ -214,7 +222,6 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
:model-value="props.checkoutForm.coin_consumed_m"
class="checkout-number-input"
:min="0"
- :max="props.snapshotHafCoinM"
:precision="2"
controls-position="right"
@update:model-value="updateCheckoutForm({ coin_consumed_m: Number($event) || 0 })"
diff --git a/frontend/src/features/orders/composables/useOrderActions.ts b/frontend/src/features/orders/composables/useOrderActions.ts
index dc91d81..a311472 100644
--- a/frontend/src/features/orders/composables/useOrderActions.ts
+++ b/frontend/src/features/orders/composables/useOrderActions.ts
@@ -191,6 +191,34 @@ export function useOrderActions(options: UseOrderActionsOptions) {
'abnormal',
].includes(order.value.status)
})
+ const canCounterCheckout = computed(() => {
+ if (!order.value?.checkout) return false
+ return Boolean(order.value.checkout.can_counter)
+ })
+ const canAcceptCheckoutProposal = computed(() => {
+ if (!order.value?.checkout) return false
+ // 后端 can_accept 已含 shortfall 校验;无字段时按角色状态兜底
+ if (typeof order.value.checkout.can_accept === 'boolean') {
+ return order.value.checkout.can_accept
+ }
+ return (
+ (isOwner.value && order.value.status === 'pending_checkout_confirm') ||
+ (isRenter.value && order.value.status === 'pending_checkout_accept')
+ )
+ })
+ const checkoutShortfallCent = computed(() =>
+ Number(order.value?.checkout?.shortfall_cent || 0)
+ )
+ const checkoutOvershootCent = computed(() =>
+ Number(order.value?.checkout?.overshoot_amount_cent || 0)
+ )
+ const checkoutRoundLabel = computed(() => {
+ const c = order.value?.checkout
+ if (!c) return ''
+ const round = Number(c.round_count || 1)
+ const max = Number(c.max_rounds || 6)
+ return `第 ${round}/${max} 轮协商`
+ })
const canCancelDispute = computed(() => {
return (
!!order.value &&
@@ -365,6 +393,10 @@ export function useOrderActions(options: UseOrderActionsOptions) {
async function handleConfirmCheckout() {
if (!order.value) return
+ if (checkoutShortfallCent.value > 0) {
+ options.notifyWarning('押金不足以覆盖打超/赔付,无法自动完结,请修改方案或发起争议由人工处理')
+ return
+ }
if (needsConfirm('confirmCheckout')) {
const ok = await options.confirm({
title: '确认结账',
@@ -386,20 +418,29 @@ export function useOrderActions(options: UseOrderActionsOptions) {
async function handleCounterCheckout() {
if (!order.value) return
+ const reasonText = counterForm.value.reason.trim()
+ if (!reasonText) {
+ options.notifyWarning('请填写修改原因')
+ return
+ }
+ if (!canCounterCheckout.value) {
+ options.notifyWarning('当前不能修改,可能已达 6 轮上限或未轮到你')
+ return
+ }
countering.value = true
try {
- const reasonText = counterForm.value.reason.trim()
await counterCheckout(order.value.id, {
content: reasonText,
consumableAmountYuan: counterForm.value.consumableAmountYuan,
coin_consumed_m: counterForm.value.coin_consumed_m,
- // 号主修正只保留「押金赔付扣除」;与 other 同步写入,兼容旧字段语义
otherAmountYuan: counterForm.value.depositDeductAmountYuan,
depositDeductAmountYuan: counterForm.value.depositDeductAmountYuan,
reason: reasonText,
evidence_urls: linesToList(counterForm.value.evidenceText),
})
- options.notifySuccess('结账修正已提交,等待租客确认')
+ options.notifySuccess(
+ isOwner.value ? '结账修正已提交,等待租客处理' : '结账还价已提交,等待号主处理'
+ )
await loadOrder()
options.onCounterCheckoutSuccess?.()
} catch (error) {
@@ -411,20 +452,24 @@ export function useOrderActions(options: UseOrderActionsOptions) {
async function handleAcceptCheckout() {
if (!order.value) return
+ if (checkoutShortfallCent.value > 0) {
+ options.notifyWarning('押金不足以覆盖打超/赔付,无法自动完结,请修改方案或发起争议由人工处理')
+ return
+ }
if (needsConfirm('acceptCheckout')) {
const ok = await options.confirm({
- title: '同意修正',
- message: '确定同意号主的结账修正提案吗?同意后订单将完成结算。',
+ title: '同意结账方案',
+ message: '确定同意当前结账方案吗?同意后订单将完成结算。',
})
if (!ok) return
}
acceptingCheckout.value = true
try {
await acceptCheckout(order.value.id)
- options.notifySuccess('已确认修正结账,订单完成')
+ options.notifySuccess('已确认结账,订单完成')
await loadOrder()
} catch (error) {
- options.notifyError(readError(error, '确认修正失败'))
+ options.notifyError(readError(error, '确认结账失败'))
} finally {
acceptingCheckout.value = false
}
@@ -634,6 +679,11 @@ export function useOrderActions(options: UseOrderActionsOptions) {
ownerIncomeLabel,
canOpenDispute,
canCancelDispute,
+ canCounterCheckout,
+ canAcceptCheckoutProposal,
+ checkoutShortfallCent,
+ checkoutOvershootCent,
+ checkoutRoundLabel,
isCheckoutDisputeStage,
// Methods
diff --git a/frontend/src/features/orders/composables/useOrderSnapshot.ts b/frontend/src/features/orders/composables/useOrderSnapshot.ts
index 3d6cb3e..41974f0 100644
--- a/frontend/src/features/orders/composables/useOrderSnapshot.ts
+++ b/frontend/src/features/orders/composables/useOrderSnapshot.ts
@@ -35,9 +35,10 @@ export function useOrderCheckoutSnapshot(options: {
calculateResourceChargeAmount(checkoutResources.value, options.resourceUsage.value)
)
const snapshotHafCoinM = computed(() => getSnapshotHafCoinM(options.order.value))
+ // 允许为负:表示打超(实际消耗 > 发布量)
const remainingHafCoinM = computed(() =>
roundQuantity(
- Math.max(snapshotHafCoinM.value - Number(options.checkoutForm.value.coin_consumed_m || 0), 0)
+ snapshotHafCoinM.value - Number(options.checkoutForm.value.coin_consumed_m || 0)
)
)
@@ -167,11 +168,14 @@ export function buildCheckoutContentWithSummary(options: {
)
}
if (Number(options.coinConsumedM || 0) > 0) {
- lines.push(
- `哈夫币消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(
- options.remainingHafCoinM
- )}M`
- )
+ const remain = Number(options.remainingHafCoinM || 0)
+ if (remain >= 0) {
+ lines.push(`哈夫币实际消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(remain)}M`)
+ } else {
+ lines.push(
+ `哈夫币实际消耗:${quantity(options.coinConsumedM)}M,打超${quantity(-remain)}M(超额按比例从押金结算)`
+ )
+ }
}
if (lines.length === 0) {
lines.push('租客发起结账。')
diff --git a/frontend/src/features/orders/views/MobileOrderDetailView.vue b/frontend/src/features/orders/views/MobileOrderDetailView.vue
index 5044eb4..d261a57 100644
--- a/frontend/src/features/orders/views/MobileOrderDetailView.vue
+++ b/frontend/src/features/orders/views/MobileOrderDetailView.vue
@@ -1,5 +1,5 @@