添加订单押金暂扣与归还
This commit is contained in:
@@ -6,9 +6,11 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
adminCloseOrder,
|
||||
adminHoldDeposit,
|
||||
adminMarkOrderAbnormal,
|
||||
adminRefundOrder,
|
||||
adminRefundStatus,
|
||||
adminReleaseDeposit,
|
||||
adminResetHandoff,
|
||||
adminSealOrder,
|
||||
fetchAdminHandoffRecords,
|
||||
@@ -41,7 +43,15 @@ const submitting = ref(false)
|
||||
const order = ref<Order | null>(null)
|
||||
const handoffRecords = ref<HandoffRecord[]>([])
|
||||
const paymentRecords = ref<AdminPayment[]>([])
|
||||
const actionType = ref<'close' | 'seal' | 'abnormal' | 'reset' | ''>('')
|
||||
type OrderActionType =
|
||||
| 'close'
|
||||
| 'seal'
|
||||
| 'abnormal'
|
||||
| 'reset'
|
||||
| 'deposit_hold'
|
||||
| 'deposit_release'
|
||||
| ''
|
||||
const actionType = ref<OrderActionType>('')
|
||||
const reason = ref('')
|
||||
const refundStatus = ref<RefundStatus | null>(null)
|
||||
|
||||
@@ -88,16 +98,50 @@ const resetActionLabel = computed(() => {
|
||||
return resetAction.value?.label || '重置'
|
||||
})
|
||||
const canResetHandoff = computed(() => resetAction.value?.enabled === true)
|
||||
// 押金暂扣:仅进行中、有实付押金、且未暂扣过的订单可暂扣。
|
||||
const depositHoldStatus = computed(() => order.value?.deposit_hold_status || 'none')
|
||||
const depositHoldAmountCent = computed(() => Number(order.value?.deposit_hold_amount_cent || 0))
|
||||
const isDepositHeld = computed(() => depositHoldStatus.value === 'held')
|
||||
const canHoldDeposit = computed(
|
||||
() =>
|
||||
canOperate.value &&
|
||||
depositHoldStatus.value === 'none' &&
|
||||
Number(order.value?.deposit_amount_cent || 0) > 0
|
||||
)
|
||||
const canReleaseDeposit = computed(() => isDepositHeld.value && depositHoldAmountCent.value > 0)
|
||||
const depositHoldStatusLabel = computed(() => {
|
||||
switch (depositHoldStatus.value) {
|
||||
case 'held':
|
||||
return '押金已暂扣'
|
||||
case 'released':
|
||||
return '押金已归还'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const actionTitle = computed(() => {
|
||||
if (actionType.value === 'close') return '客服关闭订单'
|
||||
if (actionType.value === 'seal') return '封存订单'
|
||||
if (actionType.value === 'reset') return `${resetActionLabel.value}(恢复到对应待办)`
|
||||
if (actionType.value === 'deposit_hold') return '暂扣押金'
|
||||
if (actionType.value === 'deposit_release') return '归还暂扣押金'
|
||||
return '标记订单异常'
|
||||
})
|
||||
const closeActionTip = '关闭订单并归档商品/账号;已支付订单会按租金+实付押金全量原路退款。'
|
||||
const actionConfirmButtonType = computed(() =>
|
||||
actionType.value === 'deposit_release' ? 'primary' : 'danger'
|
||||
)
|
||||
const closeActionTip =
|
||||
'关闭订单并归档商品/账号;已支付订单会原路退款,押金已暂扣时押金部分会继续挂起。'
|
||||
const sealActionTip =
|
||||
'封存订单:终止订单并全量原路退款,自动解散关联群聊,关联商品永久封存,号主无法再次编辑或上架。适用于号主失联、无法联系的场景。'
|
||||
const refundActionTip = '仅发起后台人工全量原路退款,不关闭订单或调整商品/账号状态。'
|
||||
'封存订单:终止订单并原路退款,自动解散关联群聊,关联商品永久封存;押金已暂扣时押金部分会继续挂起。'
|
||||
const depositHoldActionTip = '暂扣后订单继续流转;后续本应退给租客的押金会挂起,需客服手动归还。'
|
||||
const depositReleaseActionTip = computed(() =>
|
||||
depositHoldAmountCent.value > 0
|
||||
? `将已暂扣的 ${moneyCent(depositHoldAmountCent.value)} 押金原路归还租客。`
|
||||
: '暂扣金额为 0,需订单结算/关闭/仲裁产生暂扣金额后才能归还。'
|
||||
)
|
||||
const refundActionTip =
|
||||
'仅发起后台人工原路退款,不关闭订单或调整商品/账号状态;押金已暂扣时仅退非暂扣部分。'
|
||||
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
|
||||
const orderTotalCent = computed(
|
||||
() => Number(order.value?.rent_amount_cent || 0) + Number(order.value?.deposit_amount_cent || 0)
|
||||
@@ -146,9 +190,7 @@ const ownerLossPriceCent = computed(() => {
|
||||
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
|
||||
const fundSplitRows = computed(() => {
|
||||
if (!order.value) return []
|
||||
const rows: FundSplitRow[] = [
|
||||
{ label: '租客租金', amountCent: order.value.rent_amount_cent },
|
||||
]
|
||||
const rows: FundSplitRow[] = [{ label: '租客租金', amountCent: order.value.rent_amount_cent }]
|
||||
if (hasOwnerRentBreakdown.value) {
|
||||
rows.push(
|
||||
{ label: '号主纯币价格', amountCent: ownerCoinBasePriceCent.value },
|
||||
@@ -205,7 +247,7 @@ async function loadRefundStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
function openAction(type: 'close' | 'seal' | 'abnormal' | 'reset') {
|
||||
function openAction(type: Exclude<OrderActionType, ''>) {
|
||||
actionType.value = type
|
||||
reason.value = ''
|
||||
}
|
||||
@@ -214,7 +256,7 @@ async function confirmCloseAction() {
|
||||
if (!order.value || !canOperate.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'客服关闭会终止订单,归档关联商品和账号;若订单已支付,将按租金加实付押金发起全量原路退款。确认继续?',
|
||||
'客服关闭会终止订单,归档关联商品和账号;若订单已支付,将发起原路退款。若押金已暂扣,押金部分会继续挂起。确认继续?',
|
||||
'确认客服关闭',
|
||||
{
|
||||
confirmButtonText: '继续关闭',
|
||||
@@ -232,7 +274,7 @@ async function confirmSealAction() {
|
||||
if (!order.value || !canOperate.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'封存会终止订单并全量原路退款,自动解散关联群聊,关联商品将被永久封存,号主无法再次编辑或上架。适用于号主失联场景。确认继续?',
|
||||
'封存会终止订单并发起原路退款,自动解散关联群聊,关联商品将被永久封存。若押金已暂扣,押金部分会继续挂起。确认继续?',
|
||||
'确认封存订单',
|
||||
{
|
||||
confirmButtonText: '继续封存',
|
||||
@@ -246,6 +288,46 @@ async function confirmSealAction() {
|
||||
openAction('seal')
|
||||
}
|
||||
|
||||
async function confirmHoldDeposit() {
|
||||
if (!order.value || !canHoldDeposit.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'暂扣后订单继续流转;后续结算、关闭或仲裁中本应退给租客的押金会被挂起,需客服手动归还。确认继续?',
|
||||
'确认暂扣押金',
|
||||
{
|
||||
confirmButtonText: '继续暂扣',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
openAction('deposit_hold')
|
||||
}
|
||||
|
||||
async function confirmReleaseDeposit() {
|
||||
if (!order.value || !isDepositHeld.value) return
|
||||
if (!canReleaseDeposit.value) {
|
||||
ElMessage.warning('暂扣押金尚未形成可归还金额')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将已暂扣的 ${moneyCent(depositHoldAmountCent.value)} 押金原路归还租客。确认继续?`,
|
||||
'确认归还押金',
|
||||
{
|
||||
confirmButtonText: '继续归还',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
openAction('deposit_release')
|
||||
}
|
||||
|
||||
async function submitAction() {
|
||||
if (!order.value || !actionType.value) return
|
||||
submitting.value = true
|
||||
@@ -259,6 +341,12 @@ async function submitAction() {
|
||||
} else if (actionType.value === 'reset') {
|
||||
await adminResetHandoff(order.value.id, reason.value)
|
||||
ElMessage.success(`${resetActionLabel.value}成功`)
|
||||
} else if (actionType.value === 'deposit_hold') {
|
||||
await adminHoldDeposit(order.value.id, reason.value)
|
||||
ElMessage.success('押金已暂扣')
|
||||
} else if (actionType.value === 'deposit_release') {
|
||||
await adminReleaseDeposit(order.value.id, reason.value)
|
||||
ElMessage.success('暂扣押金已发起归还')
|
||||
} else {
|
||||
await adminMarkOrderAbnormal(order.value.id, reason.value)
|
||||
ElMessage.success('订单已标记异常')
|
||||
@@ -306,7 +394,9 @@ async function confirmRefund() {
|
||||
)
|
||||
const message = hasRefundInProgress
|
||||
? '当前订单已有退款处理中,继续人工退款可能导致重复全量退款。确认仍要发起?'
|
||||
: '人工退款只会发起全量原路退款,不会关闭订单或调整商品状态。请确认该订单确实需要补发全额退款。'
|
||||
: isDepositHeld.value
|
||||
? '人工退款不会关闭订单或调整商品状态;当前押金已暂扣,本次仅退非暂扣部分。确认继续?'
|
||||
: '人工退款只会发起原路退款,不会关闭订单或调整商品状态。请确认该订单确实需要补发退款。'
|
||||
try {
|
||||
await ElMessageBox.confirm(message, '确认人工退款', {
|
||||
confirmButtonText: '确认退款',
|
||||
@@ -346,6 +436,8 @@ function paymentBizTypeLabel(type: string) {
|
||||
admin_refund: '人工退款',
|
||||
cancel_refund: '取消退款',
|
||||
admin_close_refund: '客服关闭退款',
|
||||
admin_seal_refund: '封存退款',
|
||||
deposit_refund: '暂扣押金归还',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
@@ -499,6 +591,23 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="canHoldDeposit" :content="depositHoldActionTip" placement="top">
|
||||
<span>
|
||||
<el-button type="warning" plain @click="confirmHoldDeposit"> 暂扣押金 </el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="isDepositHeld" :content="depositReleaseActionTip" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
:disabled="!canReleaseDeposit"
|
||||
@click="confirmReleaseDeposit"
|
||||
>
|
||||
归还押金
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="refundActionTip" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
@@ -670,6 +779,26 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
<dt>支付流水</dt>
|
||||
<dd>{{ paymentRecords.length }} 条</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>押金暂扣</dt>
|
||||
<dd>{{ depositHoldStatusLabel || '未暂扣' }}</dd>
|
||||
</div>
|
||||
<div v-if="depositHoldStatus !== 'none'">
|
||||
<dt>已暂扣金额</dt>
|
||||
<dd>{{ moneyCent(depositHoldAmountCent) }}</dd>
|
||||
</div>
|
||||
<div v-if="order.deposit_hold_reason" class="wide">
|
||||
<dt>暂扣原因</dt>
|
||||
<dd>{{ order.deposit_hold_reason }}</dd>
|
||||
</div>
|
||||
<div v-if="order.deposit_held_at">
|
||||
<dt>暂扣时间</dt>
|
||||
<dd>{{ formatDateTime(order.deposit_held_at) }}</dd>
|
||||
</div>
|
||||
<div v-if="order.deposit_hold_released_at">
|
||||
<dt>归还时间</dt>
|
||||
<dd>{{ formatDateTime(order.deposit_hold_released_at) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -816,10 +945,15 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
<dd>{{ item.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div v-if="snapshotRatio && (snapshotRatio.recycleRatio > 0 || snapshotRatio.sellRatio > 0)" class="ratio-row">
|
||||
<div
|
||||
v-if="snapshotRatio && (snapshotRatio.recycleRatio > 0 || snapshotRatio.sellRatio > 0)"
|
||||
class="ratio-row"
|
||||
>
|
||||
<div v-if="snapshotRatio.recycleRatio > 0" class="ratio-item">
|
||||
<span class="ratio-label">回收比例</span>
|
||||
<strong class="ratio-value">1:{{ formatRatioNumber(snapshotRatio.recycleRatio) }}</strong>
|
||||
<strong class="ratio-value"
|
||||
>1:{{ formatRatioNumber(snapshotRatio.recycleRatio) }}</strong
|
||||
>
|
||||
</div>
|
||||
<div v-if="snapshotRatio.sellRatio > 0" class="ratio-item">
|
||||
<span class="ratio-label">售卖比例</span>
|
||||
@@ -860,7 +994,9 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="actionType = ''">取消</el-button>
|
||||
<el-button type="danger" :loading="submitting" @click="submitAction">确认操作</el-button>
|
||||
<el-button :type="actionConfirmButtonType" :loading="submitting" @click="submitAction">
|
||||
确认操作
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
|
||||
@@ -36,6 +36,11 @@ export interface Order {
|
||||
settlement_status: SettlementStatus
|
||||
refund_status?: string
|
||||
refund_amount_cent?: number
|
||||
deposit_hold_status?: string
|
||||
deposit_hold_amount_cent?: number
|
||||
deposit_hold_reason?: string
|
||||
deposit_held_at?: string
|
||||
deposit_hold_released_at?: string
|
||||
active_dispute?: ActiveDispute
|
||||
checkout?: Checkout
|
||||
admin_actions?: AdminActions
|
||||
@@ -387,6 +392,22 @@ export async function adminRejectRefund(id: number, action: 'restore' | 'close')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminHoldDeposit(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ held: boolean }>>(
|
||||
`/admin/orders/${id}/deposit-hold`,
|
||||
{ reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminReleaseDeposit(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ released: boolean }>>(
|
||||
`/admin/orders/${id}/deposit-release`,
|
||||
{ reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchPendingRefundOrders() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Order[] }>>(
|
||||
'/admin/orders/refund-pending'
|
||||
|
||||
Reference in New Issue
Block a user