Files
hfb_sys/frontend/src/views/account/OrderDetailView.vue
T
2026-05-22 18:50:32 +08:00

286 lines
9.6 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 { useRoute, useRouter } from 'vue-router'
import { createDispute } from '@/api/disputes'
import { uploadFile } from '@/api/files'
import {
cancelOrder,
confirmReceive,
confirmReturn,
fetchHandoffRecords,
fetchOrder,
submitHandoff,
submitReturn,
type HandoffRecord,
type Order,
} from '@/api/orders'
import { useSessionStore } from '@/stores/session'
const route = useRoute()
const router = useRouter()
const session = useSessionStore()
const loading = ref(false)
const cancelling = ref(false)
const handoffing = ref(false)
const confirming = ref(false)
const returning = ref(false)
const completing = ref(false)
const disputing = ref(false)
const uploadingEvidence = ref(false)
const order = ref<Order | null>(null)
const handoffRecords = ref<HandoffRecord[]>([])
const handoffContent = ref('')
const returnContent = ref('')
const disputeType = ref('cannot_login')
const disputeDescription = ref('')
const disputeEvidenceText = ref('')
const isOwner = computed(() => order.value?.owner_id === session.userId)
const isRenter = computed(() => order.value?.renter_id === session.userId)
const canOpenDispute = computed(() => {
if (!order.value || (!isOwner.value && !isRenter.value)) return false
return !['completed', 'cancelled', 'closed', 'disputing'].includes(order.value.status)
})
onMounted(loadOrder)
async function loadOrder() {
loading.value = true
try {
order.value = await fetchOrder(String(route.params.id))
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
} finally {
loading.value = false
}
}
async function handleCancel() {
if (!order.value) return
cancelling.value = true
try {
await cancelOrder(order.value.id)
ElMessage.success('订单已取消,账号已释放')
await router.push('/orders')
} catch (error) {
ElMessage.error(readError(error, '取消失败'))
} finally {
cancelling.value = false
}
}
async function handleSubmitHandoff() {
if (!order.value) return
handoffing.value = true
try {
await submitHandoff(order.value.id, handoffContent.value)
handoffContent.value = ''
ElMessage.success('交接说明已提交')
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '提交交接失败'))
} finally {
handoffing.value = false
}
}
async function handleConfirmReceive() {
if (!order.value) return
confirming.value = true
try {
await confirmReceive(order.value.id)
ElMessage.success('已确认收号,订单进入租赁中')
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '确认收号失败'))
} finally {
confirming.value = false
}
}
async function handleSubmitReturn() {
if (!order.value) return
returning.value = true
try {
await submitReturn(order.value.id, returnContent.value)
returnContent.value = ''
ElMessage.success('归还申请已提交,等待号主确认')
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '提交归还失败'))
} finally {
returning.value = false
}
}
async function handleConfirmReturn() {
if (!order.value) return
completing.value = true
try {
await confirmReturn(order.value.id)
ElMessage.success('归还已确认,订单完成')
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '确认归还失败'))
} finally {
completing.value = false
}
}
async function handleCreateDispute() {
if (!order.value) return
disputing.value = true
try {
const evidence_urls = disputeEvidenceText.value
.split('\n')
.map((item) => item.trim())
.filter(Boolean)
await createDispute(order.value.id, {
type: disputeType.value,
description: disputeDescription.value,
evidence_urls,
})
disputeDescription.value = ''
disputeEvidenceText.value = ''
ElMessage.success('申诉已提交,订单进入仲裁处理')
await loadOrder()
} catch (error) {
ElMessage.error(readError(error, '提交申诉失败'))
} finally {
disputing.value = false
}
}
async function handleEvidenceUpload(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
uploadingEvidence.value = true
try {
const uploaded = await uploadFile(file, 'dispute')
disputeEvidenceText.value = [disputeEvidenceText.value, uploaded.url].filter(Boolean).join('\n')
ElMessage.success('证据文件已上传')
} catch (error) {
ElMessage.error(readError(error, '上传失败'))
} finally {
uploadingEvidence.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" v-loading="loading">
<div v-if="order" class="page-header">
<p class="eyebrow">{{ order.order_no }}</p>
<h1>订单详情</h1>
<p>{{ order.title }} · {{ order.server_region }} / {{ order.login_platform }}</p>
</div>
<div v-if="order" class="detail-grid">
<div class="metric-card">
<span>状态</span>
<strong>{{ order.status }}</strong>
</div>
<div class="metric-card">
<span>交接</span>
<strong>{{ order.handoff_status }}</strong>
</div>
<div class="metric-card">
<span>租金</span>
<strong>¥{{ order.rent_amount }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ order.deposit_amount }}</strong>
</div>
</div>
<div v-if="order" class="order-panel">
<p>租期:{{ order.rent_hours }} 小时</p>
<p>开始:{{ order.rent_start_at || '未开始' }}</p>
<p>结束:{{ order.rent_end_at || '未设置' }}</p>
<el-button v-if="order.status === 'pending_handoff'" type="danger" :loading="cancelling" @click="handleCancel">
取消订单并释放账号
</el-button>
</div>
<div v-if="order" class="order-panel">
<h2>交接记录</h2>
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
<div v-for="record in handoffRecords" :key="record.id" class="timeline-item">
<strong>{{ record.type }}</strong>
<p>{{ record.content }}</p>
<span>{{ record.created_at }}</span>
</div>
</div>
<div v-if="order && isOwner && order.status === 'pending_handoff' && order.handoff_status === 'pending_owner'" class="order-panel">
<h2>提交交接说明</h2>
<el-input v-model="handoffContent" type="textarea" :rows="4" placeholder="填写登录方式、注意事项和交接说明" />
<el-button class="panel-action" type="primary" :loading="handoffing" @click="handleSubmitHandoff">提交交接</el-button>
</div>
<div
v-if="order && isRenter && order.status === 'pending_handoff' && order.handoff_status === 'pending_renter_confirm'"
class="order-panel"
>
<h2>确认收号</h2>
<p>确认账号可以正常登录后,订单会进入租赁中并重新计算租期结束时间。</p>
<el-button type="primary" :loading="confirming" @click="handleConfirmReceive">确认已收到账号</el-button>
</div>
<div v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)" class="order-panel">
<h2>提交归还</h2>
<el-input v-model="returnContent" type="textarea" :rows="4" placeholder="填写归还说明、租后资产状态或注意事项" />
<el-button class="panel-action" type="primary" :loading="returning" @click="handleSubmitReturn">提交归还</el-button>
</div>
<div v-if="order && isOwner && order.status === 'pending_return_confirm'" class="order-panel">
<h2>确认归还</h2>
<p>确认账号状态无误后,订单会完成,账号重新上架。</p>
<el-button type="primary" :loading="completing" @click="handleConfirmReturn">确认归还并完成订单</el-button>
</div>
<div v-if="order && canOpenDispute" class="order-panel">
<h2>发起申诉</h2>
<el-select v-model="disputeType" class="full-control" placeholder="选择申诉类型">
<el-option label="无法登录" value="cannot_login" />
<el-option label="虚假描述" value="false_description" />
<el-option label="账号被封" value="account_banned" />
<el-option label="资产损失" value="asset_loss" />
<el-option label="哈夫币争议" value="haf_coin_dispute" />
<el-option label="超时未交接" value="handoff_timeout" />
<el-option label="超时未归还" value="return_timeout" />
</el-select>
<el-input
v-model="disputeDescription"
class="panel-action"
type="textarea"
:rows="4"
placeholder="说明争议经过、时间点和希望客服核查的证据"
/>
<el-input
v-model="disputeEvidenceText"
class="panel-action"
type="textarea"
:rows="3"
placeholder="证据链接,一行一个。开发阶段可先填截图地址或备注链接"
/>
<div class="panel-action upload-line">
<input type="file" accept="image/jpeg,image/png,image/webp,application/pdf" :disabled="uploadingEvidence" @change="handleEvidenceUpload" />
</div>
<el-button class="panel-action" type="warning" :loading="disputing" @click="handleCreateDispute">提交申诉</el-button>
</div>
</section>
</template>