修复仲裁退款并新增免押额度
This commit is contained in:
@@ -10,6 +10,9 @@ export interface AdminUserItem {
|
||||
realname_status: RealnameStatusValue
|
||||
risk_status: RiskStatus
|
||||
credit_score: number
|
||||
deposit_free_quota: number
|
||||
deposit_free_used: number
|
||||
deposit_free_remaining: number
|
||||
status: UserStatus
|
||||
order_count: number
|
||||
listing_count: number
|
||||
@@ -40,3 +43,11 @@ export async function unfreezeAdminUser(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(`/admin/users/${id}/unfreeze`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function setAdminUserDepositFreeQuota(id: number, amount: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(
|
||||
`/admin/users/${id}/deposit-free-quota`,
|
||||
{ amount }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { arbitrateDispute, fetchAdminDisputes, type Dispute } from '@/features/disputes'
|
||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||
@@ -20,6 +20,13 @@ const amount = ref<number | undefined>()
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const isPartialRefund = computed(() => result.value === 'partial_refund')
|
||||
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
|
||||
return true
|
||||
})
|
||||
|
||||
onMounted(loadDisputes)
|
||||
|
||||
@@ -79,11 +86,20 @@ async function openEvidence(url: string) {
|
||||
|
||||
async function handleArbitrate() {
|
||||
if (!activeDispute.value) return
|
||||
if (submitting.value) return
|
||||
if (!remark.value.trim()) {
|
||||
ElMessage.warning('请填写客服裁决说明')
|
||||
return
|
||||
}
|
||||
if (isPartialRefund.value && (!amount.value || amount.value <= 0)) {
|
||||
ElMessage.warning('请填写部分退款金额')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await arbitrateDispute(activeDispute.value.id, {
|
||||
result: result.value,
|
||||
remark: remark.value,
|
||||
remark: remark.value.trim(),
|
||||
amount: amount.value,
|
||||
})
|
||||
ElMessage.success('仲裁结果已保存,双方已收到通知')
|
||||
@@ -205,8 +221,12 @@ function readError(error: unknown, fallback: string) {
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="activeDispute = null">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleArbitrate"
|
||||
<el-button :disabled="submitting" @click="activeDispute = null">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!canSubmitArbitration"
|
||||
@click="handleArbitrate"
|
||||
>保存裁决</el-button
|
||||
>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ref } from 'vue'
|
||||
import {
|
||||
fetchAdminUsers,
|
||||
freezeAdminUser,
|
||||
setAdminUserDepositFreeQuota,
|
||||
unfreezeAdminUser,
|
||||
type AdminUserItem,
|
||||
} from '@/features/admin/api/adminUsers'
|
||||
@@ -15,7 +16,9 @@ import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
|
||||
const submitting = ref(false)
|
||||
const activeUser = ref<AdminUserItem | null>(null)
|
||||
const quotaUser = ref<AdminUserItem | null>(null)
|
||||
const freezeReason = ref('')
|
||||
const quotaAmount = ref(0)
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -33,6 +36,26 @@ function openFreeze(row: AdminUserItem) {
|
||||
freezeReason.value = ''
|
||||
}
|
||||
|
||||
function openDepositQuota(row: AdminUserItem) {
|
||||
quotaUser.value = row
|
||||
quotaAmount.value = Number(row.deposit_free_quota || 0)
|
||||
}
|
||||
|
||||
async function handleSetDepositQuota() {
|
||||
if (!quotaUser.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await setAdminUserDepositFreeQuota(quotaUser.value.id, quotaAmount.value)
|
||||
ElMessage.success('免押额度已更新')
|
||||
quotaUser.value = null
|
||||
await loadUsers()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '设置免押额度失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFreeze() {
|
||||
if (!activeUser.value) return
|
||||
submitting.value = true
|
||||
@@ -68,6 +91,10 @@ function readError(error: unknown, fallback: string) {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function money(value: number | string | undefined) {
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -88,11 +115,21 @@ function readError(error: unknown, fallback: string) {
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="免押额度" width="130">
|
||||
<template #default="{ row }">¥{{ money(row.deposit_free_quota) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已占用" width="120">
|
||||
<template #default="{ row }">¥{{ money(row.deposit_free_used) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余免押" width="120">
|
||||
<template #default="{ row }">¥{{ money(row.deposit_free_remaining) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="注册时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column label="操作" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openDepositQuota(row)">免押</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'active'"
|
||||
size="small"
|
||||
@@ -145,5 +182,36 @@ function readError(error: unknown, fallback: string) {
|
||||
<el-button type="danger" :loading="submitting" @click="handleFreeze">确认冻结</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:model-value="!!quotaUser"
|
||||
title="设置免押额度"
|
||||
width="520px"
|
||||
@update:model-value="quotaUser = null"
|
||||
>
|
||||
<div v-if="quotaUser" class="dialog-body">
|
||||
<p>
|
||||
<strong>{{ quotaUser.phone }}</strong> · {{ quotaUser.nickname }}
|
||||
</p>
|
||||
<p>
|
||||
已占用 ¥{{ money(quotaUser.deposit_free_used) }},剩余 ¥{{
|
||||
money(quotaUser.deposit_free_remaining)
|
||||
}}
|
||||
</p>
|
||||
<el-input-number
|
||||
v-model="quotaAmount"
|
||||
class="full-control"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
:step="100"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="submitting" @click="quotaUser = null">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSetDepositQuota"
|
||||
>保存额度</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface Order {
|
||||
rent_amount?: number
|
||||
owner_rent_amount?: number
|
||||
deposit_amount: number
|
||||
deposit_original_amount: number
|
||||
deposit_waived_amount: number
|
||||
platform_fee?: number
|
||||
account_snapshot?: Record<string, unknown>
|
||||
listing_snapshot?: string
|
||||
|
||||
@@ -664,6 +664,9 @@ async function copyListingCode() {
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">押金</span>
|
||||
<strong class="meta-value">¥{{ money(order.deposit_amount) }}</strong>
|
||||
<span v-if="order.deposit_waived_amount > 0" class="meta-note"
|
||||
>已免押 ¥{{ money(order.deposit_waived_amount) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">交接状态</span>
|
||||
@@ -914,7 +917,13 @@ async function copyListingCode() {
|
||||
<h3 class="section-title">结账结算账单</h3>
|
||||
<van-cell-group inset :border="false">
|
||||
<van-cell title="实际结算租金" :value="`¥${money(order.checkout.display_amount)}`" />
|
||||
<van-cell title="押金总额" :value="`¥${money(order.checkout.deposit_amount)}`" />
|
||||
<van-cell
|
||||
title="押金总额"
|
||||
:label="
|
||||
order.deposit_waived_amount > 0 ? `已免押 ¥${money(order.deposit_waived_amount)}` : ''
|
||||
"
|
||||
:value="`¥${money(order.checkout.deposit_amount)}`"
|
||||
/>
|
||||
<van-cell title="额外消耗品已用" :value="`¥${money(order.checkout.consumable_amount)}`" />
|
||||
<van-cell
|
||||
title="押金赔付扣除"
|
||||
@@ -1348,6 +1357,12 @@ async function copyListingCode() {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-note {
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,12 @@ async function copyListingCode(order: Order) {
|
||||
</div>
|
||||
<div class="price-item">
|
||||
<span class="price-label">押金金额</span>
|
||||
<span class="price-val deposit">¥{{ money(order.deposit_amount) }}</span>
|
||||
<span class="price-val deposit">
|
||||
¥{{ money(order.deposit_amount) }}
|
||||
<em v-if="order.deposit_waived_amount > 0"
|
||||
>免 ¥{{ money(order.deposit_waived_amount) }}</em
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -500,11 +505,21 @@ async function copyListingCode(order: Order) {
|
||||
}
|
||||
|
||||
.price-val {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #ff5f00;
|
||||
}
|
||||
|
||||
.price-val em {
|
||||
font-style: normal;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.price-val.deposit {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
@@ -766,6 +766,9 @@ async function copyListingCode() {
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">押金</span>
|
||||
<strong class="metric-value">¥{{ money(order.deposit_amount) }}</strong>
|
||||
<span v-if="order.deposit_waived_amount > 0" class="metric-note"
|
||||
>已免押 ¥{{ money(order.deposit_waived_amount) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -950,7 +953,12 @@ async function copyListingCode() {
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>预收押金</span>
|
||||
<strong>¥{{ money(order.checkout.deposit_amount) }}</strong>
|
||||
<strong>
|
||||
¥{{ money(order.checkout.deposit_amount) }}
|
||||
<em v-if="order.deposit_waived_amount > 0"
|
||||
>已免押 ¥{{ money(order.deposit_waived_amount) }}</em
|
||||
>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>额外消耗品已用</span>
|
||||
@@ -1441,6 +1449,11 @@ async function copyListingCode() {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.metric-note {
|
||||
font-size: 12px;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -1772,11 +1785,21 @@ async function copyListingCode() {
|
||||
}
|
||||
|
||||
.summary-row strong {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.summary-row strong em {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.summary-row strong.amount {
|
||||
color: #10b981;
|
||||
font-size: 20px;
|
||||
|
||||
@@ -260,7 +260,14 @@ function getCountdownMinutes(order: Order) {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="押金" width="100">
|
||||
<template #default="{ row }">¥{{ money(row.deposit_amount) }}</template>
|
||||
<template #default="{ row }">
|
||||
<div class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(row.deposit_amount) }}</span>
|
||||
<span v-if="row.deposit_waived_amount > 0" class="amount-label"
|
||||
>免 ¥{{ money(row.deposit_waived_amount) }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="身份" width="80">
|
||||
<template #default="{ row }">
|
||||
@@ -342,7 +349,12 @@ function getCountdownMinutes(order: Order) {
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">押金</span>
|
||||
<span class="meta-value">¥{{ money(order.deposit_amount) }}</span>
|
||||
<span class="meta-value">
|
||||
¥{{ money(order.deposit_amount) }}
|
||||
<em v-if="order.deposit_waived_amount > 0"
|
||||
>免 ¥{{ money(order.deposit_waived_amount) }}</em
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">创建时间</span>
|
||||
@@ -581,11 +593,20 @@ function getCountdownMinutes(order: Order) {
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-value em {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.meta-value.amount {
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user