按实际纯币金额计算租客优惠与积分

This commit is contained in:
yml2213
2026-07-27 10:45:41 +08:00
parent 99e8eb28af
commit 8af7333969
30 changed files with 983 additions and 154 deletions
@@ -26,6 +26,7 @@ const evidenceDispute = ref<Dispute | null>(null)
const result = ref('release_deposit')
const remark = ref('')
const amount = ref<number | undefined>()
const actualCoinConsumedM = ref<number | undefined>()
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
@@ -33,6 +34,15 @@ const keywordFilter = ref('')
const statusFilter = ref('')
const typeFilter = ref('')
const isPartialRefund = computed(() => result.value === 'partial_refund')
const isCompletedResult = computed(() =>
['deduct_deposit', 'release_deposit', 'compensate_owner'].includes(result.value)
)
const hasValidActualCoinConsumedM = computed(
() =>
typeof actualCoinConsumedM.value === 'number' &&
Number.isFinite(actualCoinConsumedM.value) &&
actualCoinConsumedM.value >= 0
)
const pendingCount = computed(
() => disputes.value.filter(item => ['open', 'processing'].includes(item.status)).length
)
@@ -46,6 +56,12 @@ const canSubmitArbitration = computed(() => {
if (submitting.value || !activeDispute.value) return false
if (!result.value || !remark.value.trim()) return false
if (isPartialRefund.value && (!amount.value || amount.value <= 0)) return false
if (
isCompletedResult.value &&
!activeDispute.value.checkout_id &&
!hasValidActualCoinConsumedM.value
)
return false
return true
})
function canArbitrate(row: Dispute) {
@@ -146,6 +162,7 @@ function openArbitration(row: Dispute) {
result.value = row.arbitration_result || 'release_deposit'
remark.value = row.arbitration_remark || ''
amount.value = undefined
actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined
}
function resetFilters() {
@@ -276,16 +293,26 @@ async function handleArbitrate() {
ElMessage.warning('请填写部分退款金额')
return
}
if (
isCompletedResult.value &&
!activeDispute.value.checkout_id &&
!hasValidActualCoinConsumedM.value
) {
ElMessage.warning('请填写最终实际哈夫币消耗 M')
return
}
submitting.value = true
try {
await arbitrateDispute(activeDispute.value.id, {
result: result.value,
remark: remark.value.trim(),
amount_cent: amount.value ? yuanToCent(amount.value) : undefined,
actual_coin_consumed_m: isCompletedResult.value ? actualCoinConsumedM.value : undefined,
})
ElMessage.success('仲裁结果已保存,双方已收到通知')
activeDispute.value = null
remark.value = ''
actualCoinConsumedM.value = undefined
await loadDisputes()
} catch (error) {
ElMessage.error(readError(error, '仲裁失败'))
@@ -488,6 +515,18 @@ async function handleArbitrate() {
<p v-if="['deduct_deposit', 'compensate_owner'].includes(result)">
金额表示从押金中赔付给号主的部分不填则默认处理全额押金
</p>
<div v-if="isCompletedResult" class="arbitration-field">
<span>最终实际哈夫币消耗M</span>
<el-input-number
v-model="actualCoinConsumedM"
class="full-control"
:min="0"
:max="1000000000"
:precision="2"
:step="1"
placeholder="实际消耗 M"
/>
</div>
<el-input
v-model="remark"
class="panel-action"
@@ -639,6 +678,18 @@ async function handleArbitrate() {
width: 280px;
}
.arbitration-field {
display: grid;
gap: 6px;
margin-top: 12px;
}
.arbitration-field span {
color: #475569;
font-size: 13px;
font-weight: 700;
}
.main-cell,
.party-cell {
display: grid;
@@ -156,8 +156,7 @@ const canOfflineSettlement = computed(() => offlineSettlementAction.value?.enabl
const canAdminCreateDispute = computed(() => canOperate.value && !order.value?.active_dispute)
const isPlatformManaged = computed(
() =>
order.value?.handoff_mode === 'platform' ||
order.value?.settlement_mode === 'platform_managed'
order.value?.handoff_mode === 'platform' || order.value?.settlement_mode === 'platform_managed'
)
const offlineSettlementStatus = computed(() => order.value?.offline_settlement_status || 'none')
// 押金暂扣:仅进行中、有实付押金、且未暂扣过的订单可暂扣。
@@ -267,6 +266,10 @@ const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== nu
const fundSplitRows = computed(() => {
if (!order.value) return []
const rows: FundSplitRow[] = []
rows.push(
{ label: '预收纯币原价', amountCent: order.value.pure_coin_original_amount_cent },
{ label: '预收额外物品', amountCent: order.value.extra_item_original_amount_cent }
)
if (Number(order.value.rent_discount_amount_cent || 0) > 0) {
rows.push(
{ label: '原始租金', amountCent: order.value.rent_original_amount_cent },
@@ -276,6 +279,16 @@ const fundSplitRows = computed(() => {
}
)
}
if (order.value.status === 'completed') {
rows.push(
{ label: '最终实际纯币原价', amountCent: order.value.actual_pure_coin_amount_cent },
{
label: '最终实际纯币优惠',
amountCent: -Number(order.value.actual_pure_coin_discount_cent || 0),
},
{ label: '积分计算纯币基数', amountCent: order.value.growth_points_basis_cent }
)
}
rows.push({ label: '租客租金', amountCent: order.value.rent_amount_cent })
if (hasOwnerRentBreakdown.value) {
rows.push(
@@ -525,7 +538,9 @@ async function submitPlatformCheckoutCounter() {
function openAdminDispute() {
adminDisputeForm.value = {
type: ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value?.status || '')
type: ['pending_checkout_confirm', 'pending_checkout_accept'].includes(
order.value?.status || ''
)
? 'checkout_amount'
: 'cannot_login',
targetRole: 'renter',
@@ -886,12 +901,7 @@ function paymentPaidAt(record: AdminPayment) {
<el-button v-if="canOfflineSettlement" type="success" @click="openOfflineSettlement">
{{ offlineSettlementAction?.label || '确认线下结算' }}
</el-button>
<el-button
v-if="canAdminCreateDispute"
type="warning"
plain
@click="openAdminDispute"
>
<el-button v-if="canAdminCreateDispute" type="warning" plain @click="openAdminDispute">
发起申诉
</el-button>
<el-button type="warning" :disabled="!canOperate" @click="openAction('abnormal')"
@@ -1176,6 +1186,18 @@ function paymentPaidAt(record: AdminPayment) {
<dt>结算租金</dt>
<dd>{{ moneyCent(order.checkout.display_amount_cent) }}</dd>
</div>
<div>
<dt>实际纯币原价</dt>
<dd>{{ moneyCent(order.checkout.pure_coin_amount_cent) }}</dd>
</div>
<div>
<dt>实际纯币优惠</dt>
<dd>{{ moneyCent(order.checkout.pure_coin_discount_cent) }}</dd>
</div>
<div>
<dt>折后纯币金额</dt>
<dd>{{ moneyCent(order.checkout.pure_coin_payable_cent) }}</dd>
</div>
<div>
<dt>退还租客</dt>
<dd>{{ moneyCent(order.checkout.renter_refund_amount_cent) }}</dd>
@@ -1196,6 +1218,14 @@ function paymentPaidAt(record: AdminPayment) {
<dt>哈夫币消耗</dt>
<dd>{{ quantity(order.checkout.coin_consumed_m) }}M</dd>
</div>
<div v-if="order.status === 'completed'">
<dt>积分计算基数</dt>
<dd>{{ moneyCent(order.growth_points_basis_cent) }}</dd>
</div>
<div v-if="order.status === 'completed'">
<dt>本单成长积分</dt>
<dd>{{ order.growth_points_awarded || 0 }}</dd>
</div>
<div>
<dt>发起人</dt>
<dd>ID {{ order.checkout.initiated_by }}</dd>
@@ -1364,12 +1394,7 @@ function paymentPaidAt(record: AdminPayment) {
</template>
</el-dialog>
<el-dialog
v-model="platformHandoffVisible"
title="客服代交接"
width="560px"
destroy-on-close
>
<el-dialog v-model="platformHandoffVisible" title="客服代交接" width="560px" destroy-on-close>
<div v-if="order" class="dialog-body">
<p>
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
@@ -1395,12 +1420,7 @@ function paymentPaidAt(record: AdminPayment) {
</template>
</el-dialog>
<el-dialog
v-model="adminDisputeVisible"
title="发起订单申诉"
width="620px"
destroy-on-close
>
<el-dialog v-model="adminDisputeVisible" title="发起订单申诉" width="620px" destroy-on-close>
<div v-if="order" class="dialog-body">
<p>
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
@@ -1440,11 +1460,7 @@ function paymentPaidAt(record: AdminPayment) {
</div>
<template #footer>
<el-button @click="adminDisputeVisible = false">取消</el-button>
<el-button
type="primary"
:loading="adminDisputeSubmitting"
@click="submitAdminDispute"
>
<el-button type="primary" :loading="adminDisputeSubmitting" @click="submitAdminDispute">
发起申诉
</el-button>
</template>
@@ -28,6 +28,7 @@ export interface Dispute {
previous_handoff_status?: string
previous_settlement_status?: string
checkout_id?: number
checkout_coin_consumed_m?: number
previous_checkout_status?: string
arbitration_result: string
arbitration_remark: string
@@ -93,7 +94,12 @@ export async function fetchAdminDisputes(page = 1, pageSize = 20, query: AdminDi
export async function arbitrateDispute(
id: number,
payload: { result: string; remark: string; amount_cent?: number }
payload: {
result: string
remark: string
amount_cent?: number
actual_coin_consumed_m?: number
}
) {
const { data } = await apiClient.post<ApiResponse<Dispute>>(
`/admin/disputes/${id}/arbitrate`,
@@ -32,9 +32,16 @@ export interface Order {
platform_fee_cent?: number
rent_original_amount_cent?: number
rent_discount_amount_cent?: number
pure_coin_original_amount_cent?: number
extra_item_original_amount_cent?: number
actual_coin_consumed_m?: number
actual_pure_coin_amount_cent?: number
actual_pure_coin_discount_cent?: number
renter_growth_level?: string
renter_growth_level_name?: string
renter_discount_bps?: number
growth_points_basis_cent?: number
growth_points_per_yuan?: number
growth_points_awarded?: number
growth_points_awarded_at?: string
account_snapshot?: Record<string, unknown>
@@ -107,6 +114,9 @@ export interface Checkout {
owner_rent_amount_cent?: number
platform_fee_cent?: number
deposit_amount_cent: number
pure_coin_amount_cent: number
pure_coin_discount_cent: number
pure_coin_payable_cent: number
consumable_amount_cent: number
coin_consumed_m: number
other_amount_cent: number
@@ -25,6 +25,22 @@ const checkout = computed(() => props.order.checkout)
title="实际结算租金"
:value="`¥${money(amountYuan(checkout.display_amount_cent))}`"
/>
<van-cell
v-if="props.isRenter"
title="实际纯币原价"
:value="`¥${money(amountYuan(checkout.pure_coin_amount_cent))}`"
/>
<van-cell
v-if="props.isRenter && Number(checkout.pure_coin_discount_cent || 0) > 0"
title="成长等级优惠"
:value="`-¥${money(amountYuan(checkout.pure_coin_discount_cent))}`"
value-class="green-text"
/>
<van-cell
v-if="props.isRenter"
title="折后纯币金额"
:value="`¥${money(amountYuan(checkout.pure_coin_payable_cent))}`"
/>
<van-cell
title="押金总额"
:label="
@@ -43,10 +59,7 @@ const checkout = computed(() => props.order.checkout)
:value="`¥${money(amountYuan(checkout.deposit_deduct_amount_cent))}`"
value-class="red-text"
/>
<van-cell
title="哈夫币实际消耗"
:value="`${checkout.coin_consumed_m}M`"
/>
<van-cell title="哈夫币实际消耗" :value="`${checkout.coin_consumed_m}M`" />
<van-cell
v-if="Number(checkout.overshoot_amount_cent || 0) > 0"
title="打超金额(从押金)"
@@ -77,6 +90,13 @@ const checkout = computed(() => props.order.checkout)
:value="`¥${money(amountYuan(checkout.owner_income_amount_cent))}`"
value-class="green-text"
/>
<van-cell
v-if="props.isRenter && props.order.status === 'completed'"
title="本单成长积分"
:label="`积分基数 ¥${money(amountYuan(props.order.growth_points_basis_cent))}`"
:value="`+${Number(props.order.growth_points_awarded || 0)}`"
value-class="green-text"
/>
<van-cell v-if="checkout.content" title="结账备注" :label="checkout.content" />
<van-cell
v-if="checkout.owner_adjustment_reason"
@@ -95,6 +115,21 @@ const checkout = computed(() => props.order.checkout)
<span>实际结算租金</span>
<strong>¥{{ money(amountYuan(checkout.display_amount_cent)) }}</strong>
</div>
<div v-if="props.isRenter" class="summary-row">
<span>实际纯币原价</span>
<strong>¥{{ money(amountYuan(checkout.pure_coin_amount_cent)) }}</strong>
</div>
<div
v-if="props.isRenter && Number(checkout.pure_coin_discount_cent || 0) > 0"
class="summary-row"
>
<span>成长等级优惠</span>
<strong class="amount">-¥{{ money(amountYuan(checkout.pure_coin_discount_cent)) }}</strong>
</div>
<div v-if="props.isRenter" class="summary-row">
<span>折后纯币金额</span>
<strong>¥{{ money(amountYuan(checkout.pure_coin_payable_cent)) }}</strong>
</div>
<div class="summary-row">
<span>预收押金</span>
<strong>
@@ -138,6 +173,15 @@ const checkout = computed(() => props.order.checkout)
<span>号主最终收入租金 + 押金赔付</span>
<strong class="amount">¥{{ money(amountYuan(checkout.owner_income_amount_cent)) }}</strong>
</div>
<div
v-if="props.isRenter && props.order.status === 'completed'"
class="summary-row highlight"
>
<span>
本单成长积分基数 ¥{{ money(amountYuan(props.order.growth_points_basis_cent)) }}
</span>
<strong class="amount">+{{ Number(props.order.growth_points_awarded || 0) }}</strong>
</div>
<div v-if="checkout.content" class="summary-note">
<label>说明</label>
<p>{{ checkout.content }}</p>
@@ -174,6 +174,13 @@ export function useOrderActions(options: UseOrderActionsOptions) {
const orderRentDisplayAmount = computed(() =>
order.value ? orderRentAmount(order.value, session.userId) : 0
)
const renterDiscountDisplayCent = computed(() => {
if (!order.value) return 0
if (order.value.status === 'completed') {
return Number(order.value.actual_pure_coin_discount_cent || 0)
}
return Number(order.value.rent_discount_amount_cent || 0)
})
const ownerIncomeDisplayAmount = computed(() =>
order.value ? ownerActualIncome(order.value, session.userId) : null
)
@@ -206,9 +213,7 @@ export function useOrderActions(options: UseOrderActionsOptions) {
(isRenter.value && order.value.status === 'pending_checkout_accept')
)
})
const checkoutShortfallCent = computed(() =>
Number(order.value?.checkout?.shortfall_cent || 0)
)
const checkoutShortfallCent = computed(() => Number(order.value?.checkout?.shortfall_cent || 0))
const checkoutOvershootCent = computed(() =>
Number(order.value?.checkout?.overshoot_amount_cent || 0)
)
@@ -410,6 +415,9 @@ export function useOrderActions(options: UseOrderActionsOptions) {
await confirmCheckout(order.value.id)
options.notifySuccess('结账已确认,订单完成')
await loadOrder()
if (isRenter.value) {
await session.loadMe().catch(() => undefined)
}
} catch (error) {
options.notifyError(readError(error, '确认结账失败'))
} finally {
@@ -469,6 +477,9 @@ export function useOrderActions(options: UseOrderActionsOptions) {
await acceptCheckout(order.value.id)
options.notifySuccess('已确认结账,订单完成')
await loadOrder()
if (isRenter.value) {
await session.loadMe().catch(() => undefined)
}
} catch (error) {
options.notifyError(readError(error, '确认结账失败'))
} finally {
@@ -676,6 +687,7 @@ export function useOrderActions(options: UseOrderActionsOptions) {
listingCode,
orderAmountLabel,
orderRentDisplayAmount,
renterDiscountDisplayCent,
ownerIncomeDisplayAmount,
ownerIncomeLabel,
canOpenDispute,
@@ -69,6 +69,7 @@ const {
listingCode,
orderAmountLabel,
orderRentDisplayAmount,
renterDiscountDisplayCent,
ownerIncomeDisplayAmount,
ownerIncomeLabel,
canOpenDispute,
@@ -188,12 +189,9 @@ async function copyListingCode() {
<div class="meta-item">
<span class="meta-label">{{ orderAmountLabel }}</span>
<strong class="meta-value accent">¥{{ money(orderRentDisplayAmount) }}</strong>
<span
v-if="isRenter && Number(order.rent_discount_amount_cent || 0) > 0"
class="meta-note"
>
<span v-if="isRenter && renterDiscountDisplayCent > 0" class="meta-note">
{{ order.renter_growth_level_name || '成长等级' }}优惠 ¥{{
money(amountYuan(order.rent_discount_amount_cent))
money(amountYuan(renterDiscountDisplayCent))
}}
</span>
</div>
@@ -212,6 +210,16 @@ async function copyListingCode() {
<span class="meta-label">交接状态</span>
<strong class="meta-value">{{ orderHandoffStatusLabel(order) }}</strong>
</div>
<div v-if="isRenter && order.status === 'completed' && !order.checkout" class="meta-item">
<span class="meta-label">实际纯币原价</span>
<strong class="meta-value accent"
>¥{{ money(amountYuan(order.actual_pure_coin_amount_cent)) }}</strong
>
</div>
<div v-if="isRenter && order.status === 'completed' && !order.checkout" class="meta-item">
<span class="meta-label">本单成长积分</span>
<strong class="meta-value income">+{{ order.growth_points_awarded || 0 }}</strong>
</div>
</div>
</section>
@@ -366,8 +374,10 @@ async function copyListingCode() {
>
<h3 class="section-title">结账处理</h3>
<p class="action-hint">
{{ checkoutRoundLabel || '请核对消耗与扣款' }}。确认无误后同意完结;也可修改方案交对方确认(最多
6 轮)。押金不足无法自动完结,请改方案或争议。
{{
checkoutRoundLabel || '请核对消耗与扣款'
}}。确认无误后同意完结;也可修改方案交对方确认(最多 6
轮)。押金不足无法自动完结,请改方案或争议。
</p>
<p v-if="checkoutShortfallCent > 0" class="action-hint" style="color: #ef4444">
押金不足差额 ¥{{ money(amountYuan(checkoutShortfallCent)) }},无法同意完结。
@@ -490,7 +500,9 @@ async function copyListingCode() {
</header>
<div class="popup-body">
<p class="form-hint-info">
{{ checkoutRoundLabel }}。可填实际哈夫币消耗(允许超过发布量),打超按比例从押金扣。提案将交给对方确认。
{{
checkoutRoundLabel
}}。可填实际哈夫币消耗(允许超过发布量),打超按比例从押金扣。提案将交给对方确认。
</p>
<van-cell-group :border="false">
<van-field label="额外消耗品已用">
@@ -68,6 +68,7 @@ const {
listingCode,
orderAmountLabel,
orderRentDisplayAmount,
renterDiscountDisplayCent,
ownerIncomeDisplayAmount,
ownerIncomeLabel,
canOpenDispute,
@@ -388,12 +389,9 @@ watch([() => route.query.focus, order, loading], () => {
<div class="metric-card highlight">
<span class="metric-label">{{ orderAmountLabel }}</span>
<strong class="metric-value amount">¥{{ money(orderRentDisplayAmount) }}</strong>
<span
v-if="isRenter && Number(order.rent_discount_amount_cent || 0) > 0"
class="metric-note"
>
<span v-if="isRenter && renterDiscountDisplayCent > 0" class="metric-note">
{{ order.renter_growth_level_name || '成长等级' }}优惠 ¥{{
money(amountYuan(order.rent_discount_amount_cent))
money(amountYuan(renterDiscountDisplayCent))
}}
</span>
</div>
@@ -410,6 +408,22 @@ watch([() => route.query.focus, order, loading], () => {
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</span
>
</div>
<div
v-if="isRenter && order.status === 'completed' && !order.checkout"
class="metric-card"
>
<span class="metric-label">实际纯币原价</span>
<strong class="metric-value amount"
>¥{{ money(amountYuan(order.actual_pure_coin_amount_cent)) }}</strong
>
</div>
<div
v-if="isRenter && order.status === 'completed' && !order.checkout"
class="metric-card income"
>
<span class="metric-label">本单成长积分</span>
<strong class="metric-value">+{{ order.growth_points_awarded || 0 }}</strong>
</div>
</div>
<div v-if="order" class="info-section">