支持客服主动发起申诉

This commit is contained in:
yml2213
2026-07-26 09:54:27 +08:00
parent 4bc25b87d8
commit 9ccc16e068
20 changed files with 522 additions and 25 deletions
@@ -103,6 +103,13 @@ function userPhone(row: Dispute, userID: number) {
return '-'
}
function initiatorLabel(row: Dispute) {
if (row.initiator_type === 'admin') {
return row.initiator_admin_id ? `客服 ID ${row.initiator_admin_id}` : '客服'
}
return `${roleLabel(row, row.initiator_id)} ${userPhone(row, row.initiator_id)}`
}
function evidenceCount(row: Dispute) {
return evidenceItems(row).length
}
@@ -388,10 +395,7 @@ async function handleArbitrate() {
<el-table-column label="双方信息" min-width="240">
<template #default="{ row }">
<div class="party-cell">
<span
>申诉方{{ roleLabel(row, row.initiator_id) }}
{{ userPhone(row, row.initiator_id) }}</span
>
<span>申诉方{{ initiatorLabel(row) }}</span>
<span
>被申诉方{{ roleLabel(row, row.target_user_id) }}
{{ userPhone(row, row.target_user_id) }}</span
@@ -24,6 +24,7 @@ import {
type Order,
type RefundStatus,
} from '@/features/orders'
import { adminCreateOrderDispute } from '@/features/disputes'
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
import {
getSnapshotHafCoinM,
@@ -76,6 +77,14 @@ const platformCheckoutCounterForm = ref({
const offlineSettlementVisible = ref(false)
const offlineSettlementRemark = ref('')
const platformSubmitting = ref(false)
const adminDisputeVisible = ref(false)
const adminDisputeSubmitting = ref(false)
const adminDisputeForm = ref({
type: 'cannot_login',
targetRole: 'renter',
description: '',
evidenceText: '',
})
type FundSplitRow = {
label: string
@@ -144,6 +153,7 @@ const canPlatformCheckoutDispute = computed(
() => platformCheckoutDisputeAction.value?.enabled === true
)
const canOfflineSettlement = computed(() => offlineSettlementAction.value?.enabled === true)
const canAdminCreateDispute = computed(() => canOperate.value && !order.value?.active_dispute)
const isPlatformManaged = computed(
() =>
order.value?.handoff_mode === 'platform' ||
@@ -199,6 +209,16 @@ const depositReleaseActionTip = computed(() =>
const refundActionTip =
'仅发起后台人工原路退款,不关闭订单或调整商品/账号状态;押金已暂扣时仅退非暂扣部分。'
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
const disputeTypeOptions = [
{ label: '无法登录', value: 'cannot_login' },
{ label: '描述不符', value: 'false_description' },
{ label: '账号封禁', value: 'account_banned' },
{ label: '资产损失', value: 'asset_loss' },
{ label: '哈夫币争议', value: 'haf_coin_dispute' },
{ label: '交接超时', value: 'handoff_timeout' },
{ label: '归还超时', value: 'return_timeout' },
{ label: '结账金额争议', value: 'checkout_amount' },
]
const orderTotalCent = computed(
() => Number(order.value?.rent_amount_cent || 0) + Number(order.value?.deposit_amount_cent || 0)
)
@@ -493,6 +513,43 @@ async function submitPlatformCheckoutCounter() {
}
}
function openAdminDispute() {
adminDisputeForm.value = {
type: ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value?.status || '')
? 'checkout_amount'
: 'cannot_login',
targetRole: 'renter',
description: '',
evidenceText: '',
}
adminDisputeVisible.value = true
}
async function submitAdminDispute() {
if (!order.value) return
const form = adminDisputeForm.value
if (!form.description.trim()) {
ElMessage.warning('请填写申诉说明')
return
}
adminDisputeSubmitting.value = true
try {
await adminCreateOrderDispute(order.value.id, {
type: form.type,
target_role: form.targetRole,
description: form.description.trim(),
evidence_urls: linesToList(form.evidenceText),
})
ElMessage.success('申诉已发起,订单进入仲裁流程')
adminDisputeVisible.value = false
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '发起申诉失败'))
} finally {
adminDisputeSubmitting.value = false
}
}
function openOfflineSettlement() {
offlineSettlementRemark.value = ''
offlineSettlementVisible.value = true
@@ -706,10 +763,28 @@ function disputeTypeLabel(type: string) {
handoff: '交接申诉',
checkout: '结账争议',
order: '订单争议',
cannot_login: '无法登录',
false_description: '描述不符',
account_banned: '账号封禁',
asset_loss: '资产损失',
haf_coin_dispute: '哈夫币争议',
handoff_timeout: '交接超时',
return_timeout: '归还超时',
checkout_amount: '结账金额争议',
checkout_dispute: '结账争议',
}
return map[type] || type || '-'
}
function disputeInitiatorLabel() {
const active = order.value?.active_dispute
if (!active) return '-'
if (active.initiator_type === 'admin') {
return active.initiator_admin_id ? `客服 ID ${active.initiator_admin_id}` : '客服'
}
return `用户 ID ${active.initiator_id}`
}
function userDisplay(phone: string | undefined, id: number) {
return phone ? `${phone} / ID ${id}` : `ID ${id}`
}
@@ -796,6 +871,14 @@ 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>
<el-button type="warning" :disabled="!canOperate" @click="openAction('abnormal')"
>标记异常</el-button
>
@@ -1135,7 +1218,7 @@ function paymentPaidAt(record: AdminPayment) {
</div>
<div>
<dt>发起人</dt>
<dd>ID {{ order.active_dispute.initiator_id }}</dd>
<dd>{{ disputeInitiatorLabel() }}</dd>
</div>
<div>
<dt>当前状态</dt>
@@ -1297,6 +1380,61 @@ function paymentPaidAt(record: AdminPayment) {
</template>
</el-dialog>
<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 }}
</p>
<div class="form-grid">
<label>
<span>申诉类型</span>
<el-select v-model="adminDisputeForm.type" placeholder="选择申诉类型">
<el-option
v-for="item in disputeTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</label>
<label>
<span>被申诉方</span>
<el-select v-model="adminDisputeForm.targetRole" placeholder="选择被申诉方">
<el-option label="租客" value="renter" />
<el-option label="号主" value="owner" />
</el-select>
</label>
</div>
<el-input
v-model="adminDisputeForm.description"
type="textarea"
:rows="4"
placeholder="填写申诉说明,会通知订单双方并进入仲裁中心"
/>
<el-input
v-model="adminDisputeForm.evidenceText"
type="textarea"
:rows="3"
placeholder="证据链接,一行一个,可留空"
/>
</div>
<template #footer>
<el-button @click="adminDisputeVisible = false">取消</el-button>
<el-button
type="primary"
:loading="adminDisputeSubmitting"
@click="submitAdminDispute"
>
发起申诉
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="platformCheckoutCounterVisible"
title="客服修改结账方案"