Files
hfb_sys/frontend/src/features/admin/views/AdminDisputesView.vue
T

257 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { arbitrateDispute, fetchAdminDisputes, type Dispute } from '@/features/disputes'
import { fetchAdminFileBlob } from '@/shared/api/files'
import { disputeStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const submitting = ref(false)
const disputes = ref<Dispute[]>([])
const activeDispute = ref<Dispute | null>(null)
const evidenceDispute = ref<Dispute | null>(null)
const result = ref('release_deposit')
const remark = ref('')
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)
async function loadDisputes() {
loading.value = true
try {
const res = await fetchAdminDisputes(currentPage.value, currentPageSize.value)
disputes.value = res.items
total.value = res.total
} finally {
loading.value = false
}
}
async function handlePageChange() {
await loadDisputes()
}
function openArbitration(row: Dispute) {
activeDispute.value = row
result.value = row.arbitration_result || 'release_deposit'
remark.value = row.arbitration_remark || ''
amount.value = undefined
}
function evidenceItems(row: Dispute | null) {
const raw = row?.evidence_urls
if (!raw) return []
if (Array.isArray(raw)) return raw
return []
}
function extractObjectKey(url: string) {
try {
const parsed = new URL(url, window.location.origin)
return parsed.searchParams.get('key') || ''
} catch {
return ''
}
}
async function openEvidence(url: string) {
const key = extractObjectKey(url)
if (!key) {
window.open(url, '_blank')
return
}
try {
const blob = await fetchAdminFileBlob(key)
const objectURL = URL.createObjectURL(blob)
window.open(objectURL, '_blank')
window.setTimeout(() => URL.revokeObjectURL(objectURL), 60_000)
} catch (error) {
ElMessage.error(readError(error, '证据文件打开失败'))
}
}
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.trim(),
amount: amount.value,
})
ElMessage.success('仲裁结果已保存,双方已收到通知')
activeDispute.value = null
remark.value = ''
await loadDisputes()
} catch (error) {
ElMessage.error(readError(error, '仲裁失败'))
} finally {
submitting.value = false
}
}
function readError(error: unknown, fallback: string) {
if (typeof error === 'object' && error && 'response' in error) {
const response = (error as { response?: { data?: { message?: string } } }).response
return response?.data?.message || fallback
}
return fallback
}
</script>
<template>
<section class="page">
<div class="page-header">
<p class="eyebrow">Arbitration</p>
<h1>仲裁中心</h1>
<p>处理无法登录、资产损失、哈夫币争议和结账争议。</p>
</div>
<el-table v-loading="loading" class="table-panel" :data="disputes">
<el-table-column label="商品编号" width="140">
<template #default="{ row }">{{ formatListingNo(row.listing_no) }}</template>
</el-table-column>
<el-table-column prop="order_no" label="订单号" min-width="210" />
<el-table-column prop="title" label="账号" min-width="160" />
<el-table-column prop="type" label="类型" width="150" />
<el-table-column label="状态" width="110">
<template #default="{ row }">{{ disputeStatusLabel(row.status) }}</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
prop="arbitration_result"
label="仲裁结果"
width="150"
show-overflow-tooltip
/>
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button
size="small"
:disabled="evidenceItems(row).length === 0"
@click="evidenceDispute = row"
>证据</el-button
>
<el-button
size="small"
:disabled="row.status === 'resolved'"
@click="openArbitration(row)"
>仲裁</el-button
>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:loading="loading"
@page-change="handlePageChange"
/>
<el-dialog
:model-value="!!activeDispute"
title="申诉仲裁"
width="560px"
@update:model-value="activeDispute = null"
>
<div v-if="activeDispute" class="dialog-body">
<p>
<strong>{{ activeDispute.order_no }}</strong> · 商品编号
{{ formatListingNo(activeDispute.listing_no) }} · {{ activeDispute.title }}
</p>
<p>{{ activeDispute.description }}</p>
<el-select v-model="result" class="full-control" placeholder="选择裁决结果">
<el-option label="全额退款" value="full_refund" />
<el-option label="部分退款" value="partial_refund" />
<el-option label="扣押金" value="deduct_deposit" />
<el-option label="释放押金" value="release_deposit" />
<el-option label="赔付号主" value="compensate_owner" />
<el-option label="关闭订单" value="order_close" />
<el-option label="标记异常" value="mark_abnormal" />
</el-select>
<el-input-number
v-if="['partial_refund', 'deduct_deposit', 'compensate_owner'].includes(result)"
v-model="amount"
class="full-control panel-action"
:min="0"
:precision="0"
:step="10"
placeholder="裁决金额"
/>
<p v-if="result === 'partial_refund'">
部分退款金额表示退给租客的金额,剩余冻结金额结算给号主。
</p>
<p v-if="['deduct_deposit', 'compensate_owner'].includes(result)">
金额表示从押金中赔付给号主的部分;不填则默认处理全额押金。
</p>
<el-input
v-model="remark"
class="panel-action"
type="textarea"
:rows="4"
placeholder="填写客服裁决说明"
/>
</div>
<template #footer>
<el-button :disabled="submitting" @click="activeDispute = null">取消</el-button>
<el-button
type="primary"
:loading="submitting"
:disabled="!canSubmitArbitration"
@click="handleArbitrate"
>保存裁决</el-button
>
</template>
</el-dialog>
<el-dialog
:model-value="!!evidenceDispute"
title="申诉证据"
width="640px"
@update:model-value="evidenceDispute = null"
>
<div v-if="evidenceDispute" class="dialog-body">
<p>
<strong>{{ evidenceDispute.order_no }}</strong> · 商品编号
{{ formatListingNo(evidenceDispute.listing_no) }} · {{ evidenceDispute.title }}
</p>
<div v-for="item in evidenceItems(evidenceDispute)" :key="item" class="evidence-row">
<span>{{ item }}</span>
<el-button size="small" @click="openEvidence(item)">打开</el-button>
</div>
</div>
<template #footer>
<el-button type="primary" @click="evidenceDispute = null">关闭</el-button>
</template>
</el-dialog>
</section>
</template>