优化订单详情界面
This commit is contained in:
@@ -17,9 +17,19 @@ import {
|
|||||||
type RefundStatus,
|
type RefundStatus,
|
||||||
} from '@/features/orders'
|
} from '@/features/orders'
|
||||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||||
|
import {
|
||||||
|
getSnapshotHafCoinM,
|
||||||
|
readSnapshot,
|
||||||
|
readSnapshotResources,
|
||||||
|
} from '@/features/orders/composables/useOrderSnapshot'
|
||||||
import { adminPath } from '@/shared/utils/adminPath'
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import { centToYuan, formatCentWithSymbol, formatMoney } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
|
import {
|
||||||
|
disputeStatusLabel,
|
||||||
|
handoffStatusLabel,
|
||||||
|
orderStatusLabel,
|
||||||
|
settlementStatusLabel,
|
||||||
|
} from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||||||
|
|
||||||
@@ -78,6 +88,36 @@ const actionTitle = computed(() => {
|
|||||||
const closeActionTip = '关闭订单并归档商品/账号;已支付订单会按租金+实付押金全量原路退款。'
|
const closeActionTip = '关闭订单并归档商品/账号;已支付订单会按租金+实付押金全量原路退款。'
|
||||||
const refundActionTip = '仅发起后台人工全量原路退款,不关闭订单或调整商品/账号状态。'
|
const refundActionTip = '仅发起后台人工全量原路退款,不关闭订单或调整商品/账号状态。'
|
||||||
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
|
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)
|
||||||
|
)
|
||||||
|
const latestPayment = computed(() => {
|
||||||
|
return [...paymentRecords.value].sort(
|
||||||
|
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
)[0]
|
||||||
|
})
|
||||||
|
const paidOrderPayment = computed(() =>
|
||||||
|
paymentRecords.value.find(item => item.biz_type === 'order_pay' && item.status === 'paid')
|
||||||
|
)
|
||||||
|
const currentSnapshot = computed(() => readSnapshot(order.value))
|
||||||
|
const snapshotHafCoinM = computed(() => getSnapshotHafCoinM(order.value))
|
||||||
|
const snapshotResources = computed(() => readSnapshotResources(order.value).slice(0, 6))
|
||||||
|
const snapshotSeasonTags = computed(() => {
|
||||||
|
const tags = currentSnapshot.value?.season_tags
|
||||||
|
return Array.isArray(tags) ? tags.map(item => String(item)).filter(Boolean) : []
|
||||||
|
})
|
||||||
|
const accountSnapshotItems = computed(() => {
|
||||||
|
const snapshot = currentSnapshot.value
|
||||||
|
if (!snapshot) return []
|
||||||
|
return [
|
||||||
|
{ label: '账号ID', value: displayValue(snapshot.account_id) },
|
||||||
|
{ label: '账号标题', value: displayValue(snapshot.title) },
|
||||||
|
{ label: '游戏名称', value: displayValue(snapshot.game_name) },
|
||||||
|
{ label: '所在区服', value: displayValue(snapshot.server_region) },
|
||||||
|
{ label: '登录平台', value: displayValue(snapshot.login_platform) },
|
||||||
|
{ label: '段位等级', value: displayValue(snapshot.rank_level) },
|
||||||
|
].filter(item => item.value !== '-')
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(loadOrder)
|
onMounted(loadOrder)
|
||||||
|
|
||||||
@@ -160,15 +200,6 @@ function orderEstimatedEndAt() {
|
|||||||
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
|
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function money(value: unknown) {
|
|
||||||
return formatMoney(Number(value || 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
function amountYuan(cent: unknown) {
|
|
||||||
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRefund() {
|
async function handleRefund() {
|
||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
refunding.value = true
|
refunding.value = true
|
||||||
@@ -244,8 +275,8 @@ function paymentBizTypeLabel(type: string) {
|
|||||||
return map[type] || type
|
return map[type] || type
|
||||||
}
|
}
|
||||||
|
|
||||||
function moneyCent(value: number) {
|
function moneyCent(value: number | undefined | null) {
|
||||||
return formatCentWithSymbol(value)
|
return formatCentWithSymbol(Number(value || 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstQueryValue(value: unknown) {
|
function firstQueryValue(value: unknown) {
|
||||||
@@ -264,6 +295,85 @@ function formatHandoffRecordType(type: string) {
|
|||||||
}
|
}
|
||||||
return typeMap[type] || type
|
return typeMap[type] || type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function orderStatusType(status: string) {
|
||||||
|
if (['completed', 'renting'].includes(status)) return 'success'
|
||||||
|
if (['overdue', 'abnormal', 'checkout_disputing'].includes(status)) return 'danger'
|
||||||
|
if (['pending_payment', 'pending_handoff', 'pending_checkout_confirm'].includes(status)) {
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
if (['cancelled', 'closed'].includes(status)) return 'info'
|
||||||
|
return 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
function settlementStatusType(status: string) {
|
||||||
|
if (['settled', 'refunded', 'arbitrated'].includes(status)) return 'success'
|
||||||
|
if (['disputed', 'frozen'].includes(status)) return 'danger'
|
||||||
|
if (['pending', 'unsettled'].includes(status)) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function refundStatusType(status: string) {
|
||||||
|
if (status === 'refunded') return 'success'
|
||||||
|
if (status === 'failed') return 'danger'
|
||||||
|
if (['pending', 'refunding'].includes(status)) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkoutStatusLabel(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
submitted: '待确认',
|
||||||
|
countered: '号主已修正',
|
||||||
|
accepted: '已接受',
|
||||||
|
disputed: '争议中',
|
||||||
|
}
|
||||||
|
return map[status] || settlementStatusLabel(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function priceRoleLabel(role?: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
renter: '租客视角',
|
||||||
|
owner: '号主视角',
|
||||||
|
admin: '客服处理',
|
||||||
|
}
|
||||||
|
return map[role || ''] || role || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function disputeTypeLabel(type: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
handoff: '交接申诉',
|
||||||
|
checkout: '结账争议',
|
||||||
|
order: '订单争议',
|
||||||
|
}
|
||||||
|
return map[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function userDisplay(phone: string | undefined, id: number) {
|
||||||
|
return phone ? `${phone} / ID ${id}` : `ID ${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayValue(value: unknown) {
|
||||||
|
if (value === undefined || value === null || value === '') return '-'
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return (
|
||||||
|
value
|
||||||
|
.map(item => String(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('、') || '-'
|
||||||
|
)
|
||||||
|
if (typeof value === 'object') return JSON.stringify(value)
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function quantity(value: unknown) {
|
||||||
|
return Number(value || 0).toLocaleString('zh-CN', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function paymentPaidAt(record: AdminPayment) {
|
||||||
|
return record.paid_at || record.updated_at || record.created_at
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -309,81 +419,341 @@ function formatHandoffRecordType(type: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="order" class="metric-grid dashboard-metrics">
|
<div v-if="order" class="metric-grid dashboard-metrics order-metrics">
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>订单状态</span>
|
<span>订单状态</span>
|
||||||
<strong>{{ orderStatusLabel(order.status) }}</strong>
|
<strong>{{ orderStatusLabel(order.status) }}</strong>
|
||||||
|
<small>{{ handoffStatusLabel(order.handoff_status) }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>交接状态</span>
|
<span>结算状态</span>
|
||||||
<strong>{{ handoffStatusLabel(order.handoff_status) }}</strong>
|
<strong>{{ settlementStatusLabel(order.settlement_status) }}</strong>
|
||||||
|
<small>更新 {{ formatDateTime(order.updated_at) }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>订单金额</span>
|
<span>订单总额</span>
|
||||||
<strong>¥{{ money(amountYuan(order.rent_amount_cent)) }}</strong>
|
<strong>{{ moneyCent(orderTotalCent) }}</strong>
|
||||||
|
<small
|
||||||
|
>租金 {{ moneyCent(order.rent_amount_cent) }} / 押金
|
||||||
|
{{ moneyCent(order.deposit_amount_cent) }}</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>平台费用</span>
|
<span>支付状态</span>
|
||||||
<strong>¥{{ money(amountYuan(order.platform_fee_cent)) }}</strong>
|
<strong>{{ latestPayment ? paymentStatusLabel(latestPayment.status) : '暂无流水' }}</strong>
|
||||||
|
<small v-if="paidOrderPayment">支付 {{ moneyCent(paidOrderPayment.amount_cent) }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>押金</span>
|
|
||||||
<strong>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div v-if="refundStatus" class="metric-card">
|
|
||||||
<span>退款状态</span>
|
<span>退款状态</span>
|
||||||
<strong>{{ refundStatusLabel(refundStatus.refund_status) }}</strong>
|
<strong>{{ refundStatusLabel(refundStatus?.refund_status || '') }}</strong>
|
||||||
<small v-if="refundStatus.refund_amount_cent > 0">{{
|
<small v-if="refundStatus?.refund_amount_cent">
|
||||||
moneyCent(refundStatus.refund_amount_cent)
|
{{ moneyCent(refundStatus.refund_amount_cent) }}
|
||||||
}}</small>
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>预计截止</span>
|
||||||
|
<strong>{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</strong>
|
||||||
|
<small>{{ order.estimated_duration_hours }} 小时</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="order" class="dashboard-panels">
|
<div v-if="order" class="order-detail-grid">
|
||||||
<div class="order-panel dashboard-panel">
|
<section class="dashboard-panel detail-panel">
|
||||||
<h2>用户信息</h2>
|
<div class="panel-heading">
|
||||||
<p>商品编号:{{ listingCode }}</p>
|
<h2>订单概览</h2>
|
||||||
<p>租客:{{ order.renter_phone || order.renter_id }}</p>
|
<div class="status-tags">
|
||||||
<p>号主:{{ order.owner_phone || order.owner_id }}</p>
|
<el-tag :type="orderStatusType(order.status)" effect="light">
|
||||||
<p>开始:{{ formatDateTime(orderRentedAt(), '未开始') }}</p>
|
{{ orderStatusLabel(order.status) }}
|
||||||
<p>预计截止:{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</p>
|
</el-tag>
|
||||||
</div>
|
<el-tag :type="settlementStatusType(order.settlement_status)" effect="light">
|
||||||
|
{{ settlementStatusLabel(order.settlement_status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>订单编号</dt>
|
||||||
|
<dd>{{ order.order_no }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>订单ID</dt>
|
||||||
|
<dd>{{ order.id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>商品编号</dt>
|
||||||
|
<dd>{{ listingCode }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>账号ID</dt>
|
||||||
|
<dd>{{ order.account_id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>游戏区服</dt>
|
||||||
|
<dd>{{ order.server_region }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>登录平台</dt>
|
||||||
|
<dd>{{ order.login_platform }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="wide">
|
||||||
|
<dt>商品标题</dt>
|
||||||
|
<dd>{{ order.title }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="order-panel dashboard-panel">
|
<section class="dashboard-panel detail-panel">
|
||||||
<h2>支付与退款</h2>
|
<div class="panel-heading">
|
||||||
<el-empty v-if="paymentRecords.length === 0" description="暂无支付流水" />
|
<h2>用户与时间</h2>
|
||||||
<div v-for="record in paymentRecords" :key="record.id" class="timeline-item">
|
</div>
|
||||||
<strong>
|
<dl class="detail-list">
|
||||||
{{ paymentBizTypeLabel(record.biz_type) }} · {{ moneyCent(record.amount_cent) }}
|
<div>
|
||||||
</strong>
|
<dt>租客</dt>
|
||||||
<p>
|
<dd>{{ userDisplay(order.renter_phone, order.renter_id) }}</dd>
|
||||||
{{ record.provider || '-' }} · {{ record.third_order_id }}
|
</div>
|
||||||
<span v-if="record.provider_order_id"> / {{ record.provider_order_id }}</span>
|
<div>
|
||||||
</p>
|
<dt>号主</dt>
|
||||||
<p v-if="record.error_message" class="danger-text">
|
<dd>{{ userDisplay(order.owner_phone, order.owner_id) }}</dd>
|
||||||
{{ record.error_code }} {{ record.error_message }}
|
</div>
|
||||||
</p>
|
<div>
|
||||||
<el-tag :type="paymentStatusType(record.status)">
|
<dt>创建时间</dt>
|
||||||
{{ paymentStatusLabel(record.status) }}
|
<dd>{{ formatDateTime(order.created_at) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>支付截止</dt>
|
||||||
|
<dd>{{ formatDateTime(order.payment_deadline_at, '未设置') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>开始租用</dt>
|
||||||
|
<dd>{{ formatDateTime(orderRentedAt(), '未开始') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>预计结束</dt>
|
||||||
|
<dd>{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dashboard-panel detail-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>资金拆分</h2>
|
||||||
|
<span class="panel-subtitle">{{ priceRoleLabel(order.price_role) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="money-list">
|
||||||
|
<div class="money-row">
|
||||||
|
<span>租客租金</span>
|
||||||
|
<strong>{{ moneyCent(order.rent_amount_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row">
|
||||||
|
<span>号主租金</span>
|
||||||
|
<strong>{{ moneyCent(order.owner_rent_amount_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row">
|
||||||
|
<span>平台费用</span>
|
||||||
|
<strong>{{ moneyCent(order.platform_fee_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row">
|
||||||
|
<span>实付押金</span>
|
||||||
|
<strong>{{ moneyCent(order.deposit_amount_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row">
|
||||||
|
<span>原始押金</span>
|
||||||
|
<strong>{{ moneyCent(order.deposit_original_amount_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row">
|
||||||
|
<span>免押额度</span>
|
||||||
|
<strong>{{ moneyCent(order.deposit_waived_amount_cent) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="money-row total">
|
||||||
|
<span>订单实付合计</span>
|
||||||
|
<strong>{{ moneyCent(orderTotalCent) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dashboard-panel detail-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>退款概况</h2>
|
||||||
|
<el-tag :type="refundStatusType(refundStatus?.refund_status || '')" effect="light">
|
||||||
|
{{ refundStatusLabel(refundStatus?.refund_status || '') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span>{{ formatDateTime(record.created_at) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<dl class="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>退款金额</dt>
|
||||||
|
<dd>{{ moneyCent(refundStatus?.refund_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>可退上限</dt>
|
||||||
|
<dd>{{ moneyCent(orderTotalCent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>退款完成</dt>
|
||||||
|
<dd>{{ formatDateTime(refundStatus?.refunded_at, '未完成') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>支付流水</dt>
|
||||||
|
<dd>{{ paymentRecords.length }} 条</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="order-panel dashboard-panel">
|
<section v-if="order.checkout" class="dashboard-panel detail-panel">
|
||||||
<h2>交接记录</h2>
|
<div class="panel-heading">
|
||||||
|
<h2>结账明细</h2>
|
||||||
|
<el-tag effect="light">{{ checkoutStatusLabel(order.checkout.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>结算租金</dt>
|
||||||
|
<dd>{{ moneyCent(order.checkout.display_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>退还租客</dt>
|
||||||
|
<dd>{{ moneyCent(order.checkout.renter_refund_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>号主收入</dt>
|
||||||
|
<dd>{{ moneyCent(order.checkout.owner_income_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>押金扣除</dt>
|
||||||
|
<dd>{{ moneyCent(order.checkout.deposit_deduct_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>消耗品金额</dt>
|
||||||
|
<dd>{{ moneyCent(order.checkout.consumable_amount_cent) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>哈夫币消耗</dt>
|
||||||
|
<dd>{{ quantity(order.checkout.coin_consumed_m) }}M</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>发起人</dt>
|
||||||
|
<dd>ID {{ order.checkout.initiated_by }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>凭证数量</dt>
|
||||||
|
<dd>{{ order.checkout.evidence_urls?.length || 0 }} 张</dd>
|
||||||
|
</div>
|
||||||
|
<div v-if="order.checkout.content" class="wide">
|
||||||
|
<dt>结账说明</dt>
|
||||||
|
<dd>{{ order.checkout.content }}</dd>
|
||||||
|
</div>
|
||||||
|
<div v-if="order.checkout.owner_adjustment_reason" class="wide">
|
||||||
|
<dt>号主修正</dt>
|
||||||
|
<dd>{{ order.checkout.owner_adjustment_reason }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="order.active_dispute" class="dashboard-panel detail-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>争议信息</h2>
|
||||||
|
<el-tag type="warning" effect="light">
|
||||||
|
{{ disputeStatusLabel(order.active_dispute.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div>
|
||||||
|
<dt>争议ID</dt>
|
||||||
|
<dd>{{ order.active_dispute.id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>争议类型</dt>
|
||||||
|
<dd>{{ disputeTypeLabel(order.active_dispute.type) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>发起人</dt>
|
||||||
|
<dd>ID {{ order.active_dispute.initiator_id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>当前状态</dt>
|
||||||
|
<dd>{{ disputeStatusLabel(order.active_dispute.status) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dashboard-panel detail-panel order-wide-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>支付与退款流水</h2>
|
||||||
|
<span class="panel-subtitle">共 {{ paymentRecords.length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="paymentRecords.length === 0" description="暂无支付流水" />
|
||||||
|
<div v-else class="record-list">
|
||||||
|
<article v-for="record in paymentRecords" :key="record.id" class="record-item">
|
||||||
|
<div class="record-main">
|
||||||
|
<strong>
|
||||||
|
{{ paymentBizTypeLabel(record.biz_type) }} · {{ moneyCent(record.amount_cent) }}
|
||||||
|
</strong>
|
||||||
|
<el-tag :type="paymentStatusType(record.status)" size="small" effect="light">
|
||||||
|
{{ paymentStatusLabel(record.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="record-meta">
|
||||||
|
<span>流水号:{{ record.payment_no }}</span>
|
||||||
|
<span>用户:{{ record.user_phone || record.user_id }}</span>
|
||||||
|
<span>渠道:{{ record.provider || '-' }}</span>
|
||||||
|
<span>商户:{{ record.merchant_id || '-' }}</span>
|
||||||
|
<span>三方单号:{{ record.third_order_id || '-' }}</span>
|
||||||
|
<span>渠道单号:{{ record.provider_order_id || '-' }}</span>
|
||||||
|
<span>创建:{{ formatDateTime(record.created_at) }}</span>
|
||||||
|
<span>完成:{{ formatDateTime(paymentPaidAt(record), '-') }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="record.error_message" class="danger-text">
|
||||||
|
{{ record.error_code }} {{ record.error_message }}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dashboard-panel detail-panel order-wide-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>交接记录</h2>
|
||||||
|
<span class="panel-subtitle">共 {{ handoffRecords.length }} 条</span>
|
||||||
|
</div>
|
||||||
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
|
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
|
||||||
<div v-for="record in handoffRecords" :key="record.id" class="timeline-item">
|
<div v-else class="record-list">
|
||||||
<strong>{{ formatHandoffRecordType(record.type) }}</strong>
|
<article v-for="record in handoffRecords" :key="record.id" class="record-item">
|
||||||
<p>{{ record.content }}</p>
|
<div class="record-main">
|
||||||
<span>{{ formatDateTime(record.created_at) }}</span>
|
<strong>{{ formatHandoffRecordType(record.type) }}</strong>
|
||||||
|
<span>{{ formatDateTime(record.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<p>{{ record.content }}</p>
|
||||||
|
<div class="record-meta">
|
||||||
|
<span>发送:ID {{ record.from_user_id }}</span>
|
||||||
|
<span>接收:ID {{ record.to_user_id }}</span>
|
||||||
|
<span>租客确认:{{ formatDateTime(record.confirmed_by_renter_at, '-') }}</span>
|
||||||
|
<span>号主确认:{{ formatDateTime(record.confirmed_by_owner_at, '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="order" class="order-panel dashboard-panel code-panel">
|
<section class="dashboard-panel detail-panel order-wide-panel">
|
||||||
<h2>账号快照</h2>
|
<div class="panel-heading">
|
||||||
<pre>{{ snapshotText }}</pre>
|
<h2>账号快照</h2>
|
||||||
|
<span class="panel-subtitle">哈夫币 {{ quantity(snapshotHafCoinM) }}M</span>
|
||||||
|
</div>
|
||||||
|
<dl v-if="accountSnapshotItems.length" class="detail-list snapshot-detail-list">
|
||||||
|
<div v-for="item in accountSnapshotItems" :key="item.label">
|
||||||
|
<dt>{{ item.label }}</dt>
|
||||||
|
<dd>{{ item.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div v-if="snapshotSeasonTags.length" class="snapshot-tags">
|
||||||
|
<el-tag v-for="tag in snapshotSeasonTags" :key="tag" size="small" effect="plain">
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="snapshotResources.length" class="snapshot-resources">
|
||||||
|
<div v-for="resource in snapshotResources" :key="resource.key" class="resource-item">
|
||||||
|
<strong>{{ resource.label }}</strong>
|
||||||
|
<span>{{ quantity(resource.quantity) }} · {{ resource.price || resource.mode }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre class="snapshot-json">{{ snapshotText }}</pre>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -410,3 +780,246 @@ function formatHandoffRecordType(type: string) {
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-metrics {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-metrics .metric-card strong {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #8f9bba;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-wide-panel {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-subtitle {
|
||||||
|
color: #8f9bba;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tags,
|
||||||
|
.snapshot-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px 18px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list div {
|
||||||
|
min-width: 0;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list .wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list dt {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #8f9bba;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list dd {
|
||||||
|
margin: 0;
|
||||||
|
color: #1b2559;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
padding-top: 9px;
|
||||||
|
color: #52616f;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-row strong {
|
||||||
|
color: #1b2559;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-row.total {
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top-color: #d8e2f0;
|
||||||
|
color: #1b2559;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-row.total strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcff;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-main strong {
|
||||||
|
color: #1b2559;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-main span {
|
||||||
|
color: #8f9bba;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 6px 12px;
|
||||||
|
color: #52616f;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item p {
|
||||||
|
margin: 0;
|
||||||
|
color: #52616f;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-detail-list {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-tags {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-resources {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcff;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item strong {
|
||||||
|
color: #1b2559;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item span {
|
||||||
|
color: #52616f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-json {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 360px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #111827;
|
||||||
|
color: #dbe4ee;
|
||||||
|
padding: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.order-detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.panel-heading,
|
||||||
|
.record-main {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tags {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user