Files
hfb_sys/frontend/src/features/orders/views/OrderDetailView.vue
T

1601 lines
40 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 { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ChatDotRound, CopyDocument, Loading, Service } from '@element-plus/icons-vue'
import {
amountYuan,
money,
OrderAccountSnapshot,
OrderCheckoutSummary,
OrderHandoffTimeline,
OrderResourceUsageEditor,
} from '@/features/orders'
import { useOrderActions } from '@/features/orders/composables/useOrderActions'
import { useOrderPaymentCashier } from '@/features/orders/composables/useOrderPaymentCashier'
import { formatCent } from '@/shared/utils/money'
import { orderHandoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import type { PaymentPayWay } from '@/features/orders/api/orders'
// 支付收银台:二维码渲染 + 轮询查询。PC 不做 App 浏览器跳转,直接渲染二维码弹窗。
const route = useRoute()
const disputeDialogVisible = ref(false)
const cashier = useOrderPaymentCashier({
notifySuccess: message => ElMessage.success(message),
notifyError: message => ElMessage.error(message),
notifyInfo: message => ElMessage.info(message),
notifyFallback: message => ElMessage.warning(message),
paidMessage: '支付成功,正在进入群消息',
pollIntervalMs: 3000,
qrWidth: 240,
})
// 订单详情交互:业务逻辑、状态、计算属性、handler。
const {
loading,
cancelling,
startingPayment,
handoffing,
confirming,
returning,
completing,
countering,
acceptingCheckout,
rejectingCheckout,
disputing,
cancellingDispute,
uploadingEvidence,
openingChat,
order,
handoffRecords,
handoffContent,
checkoutForm,
resourceUsage,
counterForm,
rejectReason,
disputeType,
disputeDescription,
disputeEvidenceText,
checkoutResources,
resourceChargeAmount,
snapshotHafCoinM,
remainingHafCoinM,
resourceLineAmount,
isOwner,
isRenter,
listingCode,
orderAmountLabel,
orderRentDisplayAmount,
renterDiscountDisplayCent,
ownerIncomeDisplayAmount,
ownerIncomeLabel,
canOpenDispute,
canCancelDispute,
canCounterCheckout,
canAcceptCheckoutProposal,
checkoutShortfallCent,
checkoutOvershootCent,
checkoutRoundLabel,
isCheckoutDisputeStage,
handleCancel,
handlePay,
handleSubmitHandoff,
handleConfirmReceive,
handleSubmitCheckout,
handleConfirmCheckout,
handleCounterCheckout,
handleAcceptCheckout,
handleRejectCheckout,
handleCreateDispute,
handleCancelDispute,
handleEvidenceUpload,
openOrderChat,
orderEstimatedEndAt,
orderRentedAt,
} = useOrderActions({
notifySuccess: message => ElMessage.success(message),
notifyError: message => ElMessage.error(message),
notifyWarning: message => ElMessage.warning(message),
confirm: async ({ title, message }) => {
try {
await ElMessageBox.confirm(message, title, {
confirmButtonText: '确认提交',
cancelButtonText: '返回检查',
type: 'warning',
dangerouslyUseHTMLString: false,
})
return true
} catch {
return false
}
},
actionsNeedingConfirm: ['submitCheckout'],
selectPayWay,
ordersPath: '/orders',
chatPathPrefix: '/messages/',
startCashier: (payment, onPaid) => cashier.openPaymentCashier(payment, onPaid),
onCreateDisputeSuccess: closeDisputeDialog,
})
// 支付弹窗别名:保持 template 字段名不变。
const paymentDialogVisible = cashier.paymentPopupVisible
const activePayment = cashier.activePayment
const paymentQRCodeURL = cashier.paymentQRCodeURL
const qrGenerating = cashier.qrGenerating
const checkingPayment = cashier.checkingPayment
const paymentPayURL = cashier.payURL
const activePayWayLabel = computedPayWayLabel(activePayment)
const handoffFocused = ref(false)
const hasSidebarActions = computed(() => {
if (!order.value) return false
return (
(isOwner.value && order.value.status === 'pending_checkout_confirm') ||
(isRenter.value && order.value.status === 'pending_checkout_accept')
)
})
const showCheckoutCounterForm = computed(
() =>
!!order.value &&
canCounterCheckout.value &&
((isOwner.value && order.value.status === 'pending_checkout_confirm') ||
(isRenter.value && order.value.status === 'pending_checkout_accept'))
)
const disputeSupportTitle = computed(() => {
if (canCancelDispute.value) {
return order.value?.status === 'checkout_disputing' ? '结账争议处理中' : '申诉处理中'
}
return isCheckoutDisputeStage.value ? '结账金额有异议?' : '遇到交接问题?'
})
const disputeSupportMessage = computed(() => {
if (canCancelDispute.value) {
return '客服正在处理当前争议,如问题已解决可取消申诉。'
}
return isCheckoutDisputeStage.value
? '可提交结账争议,客服会核查双方证据。'
: '无法登录、超时交接等情况可申请客服介入。'
})
const disputeSupportActionText = computed(() => {
if (canCancelDispute.value) {
return order.value?.status === 'checkout_disputing' ? '取消结账争议' : '取消申诉'
}
return isCheckoutDisputeStage.value ? '发起结账争议' : '发起申诉'
})
// selectPayWay 在创建支付单前让用户选择支付宝或微信。
// 通过自定义对话框展示两个平级渠道卡片,返回 null 表示用户取消。
const payWayDialogVisible = ref(false)
let payWayResolver: ((value: PaymentPayWay | null) => void) | null = null
function selectPayWay(): Promise<PaymentPayWay | null> {
payWayDialogVisible.value = true
return new Promise(resolve => {
payWayResolver = resolve
})
}
function choosePayWay(payWay: PaymentPayWay) {
payWayResolver?.(payWay)
payWayResolver = null
payWayDialogVisible.value = false
}
// 对话框关闭(含点 X、按 ESC)时,未选择则视为取消。
function handlePayWayDialogClosed() {
payWayResolver?.(null)
payWayResolver = null
}
// computedPayWayLabel 根据当前支付单展示支付方式名称。
function computedPayWayLabel(paymentRef: typeof activePayment) {
return computed(() => {
const map: Record<string, string> = {
ZFBZF: '支付宝',
WXZF: '微信',
}
return map[paymentRef.value?.pay_way || ''] || '当前方式'
})
}
function handleRefreshPayment() {
void cashier.refreshPaymentStatus(false)
}
function stopPaymentPolling() {
cashier.stopPaymentPolling()
}
function getOrderStep(status: string) {
const stepMap: Record<string, number> = {
pending_payment: 0,
pending_handoff: 1,
renting: 2,
overdue: 2,
pending_checkout_confirm: 3,
pending_checkout_accept: 3,
completed: 5,
cancelled: 0,
closed: 4,
}
return stepMap[status] ?? 0
}
function openDisputeDialog() {
disputeDialogVisible.value = true
}
function closeDisputeDialog() {
disputeDialogVisible.value = false
}
function scrollToCounterCheckout() {
scrollToSection('.counter-checkout-section')
}
function scrollToRejectCheckout() {
scrollToSection('.reject-checkout-section')
}
function scrollToHandoff() {
scrollToSection('.handoff-workspace')
}
function scrollToSection(selector: string) {
const section = document.querySelector(selector)
if (section) {
section.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
async function focusHandoffFromQuery() {
if (handoffFocused.value || loading.value || !order.value || route.query.focus !== 'handoff') {
return
}
handoffFocused.value = true
await nextTick()
scrollToHandoff()
}
async function copyListingCode() {
if (!order.value) return
try {
await navigator.clipboard.writeText(listingCode.value)
ElMessage.success('商品编号已复制')
} catch {
ElMessage.error('复制失败')
}
}
onMounted(() => {
void focusHandoffFromQuery()
})
watch([() => route.query.focus, order, loading], () => {
void focusHandoffFromQuery()
})
</script>
<template>
<section class="page order-detail-page" v-loading="loading">
<div v-if="order" class="page-header" :class="{ centered: !hasSidebarActions }">
<div class="header-top">
<div class="header-info">
<p class="eyebrow">订单号{{ order.order_no }}</p>
<h1>{{ order.title }}</h1>
<p class="order-meta">{{ order.server_region }} / {{ order.login_platform }}</p>
<el-button size="small" plain :icon="CopyDocument" @click="copyListingCode">
商品编号 {{ listingCode }}
</el-button>
</div>
<el-button type="primary" :loading="openingChat" @click="openOrderChat">
<el-icon style="margin-right: 4px"><ChatDotRound /></el-icon>
联系对方
</el-button>
</div>
</div>
<div v-if="order" class="content-layout" :class="{ centered: !hasSidebarActions }">
<div class="main-content">
<div v-if="order" class="order-progress-section">
<el-steps
:active="getOrderStep(order.status)"
align-center
finish-status="success"
process-status="process"
>
<el-step
title="待支付"
:description="order.status === 'pending_payment' ? '等待租客支付' : ''"
/>
<el-step
title="待交接"
:description="
order.status === 'pending_handoff' ? orderHandoffStatusLabel(order) : ''
"
/>
<el-step
title="使用中"
:description="['renting', 'overdue'].includes(order.status) ? '租赁进行中' : ''"
/>
<el-step
title="结账中"
:description="
['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.status)
? '等待确认'
: ''
"
/>
<el-step title="已完成" :description="order.status === 'completed' ? '订单完成' : ''" />
</el-steps>
</div>
<div
v-if="
isOwner &&
order.status === 'pending_handoff' &&
order.handoff_status === 'pending_owner'
"
class="form-section handoff-action-section"
>
<div class="section-header compact">
<h2>当前待办:提交交接说明</h2>
<span>填写后租客即可确认收号</span>
</div>
<div class="handoff-action-card">
<el-input
v-model="handoffContent"
type="textarea"
:rows="4"
placeholder="填写登录方式、注意事项和交接说明"
/>
<el-button type="primary" :loading="handoffing" @click="handleSubmitHandoff">
提交交接
</el-button>
</div>
</div>
<div
v-if="
isRenter &&
order.status === 'pending_handoff' &&
order.handoff_status === 'pending_renter_confirm'
"
class="handoff-confirm-strip"
>
<div>
<strong>当前待办:确认收号</strong>
<span>确认账号可以正常登录后,订单会进入使用中并重新计算预计截止时间。</span>
</div>
<el-button type="primary" :loading="confirming" @click="handleConfirmReceive">
确认已收到账号
</el-button>
</div>
<OrderResourceUsageEditor
v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)"
v-model:checkout-form="checkoutForm"
v-model:resource-usage="resourceUsage"
:resources="checkoutResources"
:resource-charge-amount="resourceChargeAmount"
:snapshot-haf-coin-m="snapshotHafCoinM"
:remaining-haf-coin-m="remainingHafCoinM"
:returning="returning"
:resource-line-amount="resourceLineAmount"
@submit="handleSubmitCheckout"
/>
<div v-if="order" class="detail-grid">
<div class="metric-card primary">
<span class="metric-label">订单状态</span>
<strong class="metric-value">{{ orderStatusLabel(order.status) }}</strong>
</div>
<div class="metric-card">
<span class="metric-label">交接状态</span>
<strong class="metric-value">{{ orderHandoffStatusLabel(order) }}</strong>
</div>
<div class="metric-card highlight">
<span class="metric-label">{{ orderAmountLabel }}</span>
<strong class="metric-value amount">¥{{ money(orderRentDisplayAmount) }}</strong>
<span v-if="isRenter && renterDiscountDisplayCent > 0" class="metric-note">
{{ order.renter_growth_level_name || '成长等级' }}优惠 ¥{{
money(amountYuan(renterDiscountDisplayCent))
}}
</span>
</div>
<div v-if="isOwner && ownerIncomeDisplayAmount !== null" class="metric-card income">
<span class="metric-label">{{ ownerIncomeLabel }}</span>
<strong class="metric-value amount">¥{{ money(ownerIncomeDisplayAmount) }}</strong>
</div>
<div class="metric-card">
<span class="metric-label">押金</span>
<strong class="metric-value"
>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong
>
<span v-if="amountYuan(order.deposit_waived_amount_cent) > 0" class="metric-note"
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</span
>
</div>
<div
v-if="isRenter && order.status === 'completed' && !order.checkout"
class="metric-card"
>
<span class="metric-label">实际纯币原价</span>
<strong class="metric-value amount"
>¥{{ money(amountYuan(order.actual_pure_coin_amount_cent)) }}</strong
>
</div>
<div
v-if="isRenter && order.status === 'completed' && !order.checkout"
class="metric-card income"
>
<span class="metric-label">本单成长积分</span>
<strong class="metric-value">+{{ order.growth_points_awarded || 0 }}</strong>
</div>
</div>
<div v-if="order" class="info-section">
<div class="info-card">
<div class="info-row">
<span class="info-label">开始时间</span>
<span class="info-value">{{ formatDateTime(orderRentedAt(), '未开始') }}</span>
</div>
<div class="info-row">
<span class="info-label">预计截止</span>
<span class="info-value">{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</span>
</div>
</div>
<div v-if="order.status === 'pending_payment'" class="action-card warning">
<p class="action-message">订单待支付,支付后账号进入交接流程。</p>
<div class="action-buttons">
<el-button
v-if="isRenter"
type="primary"
size="large"
:loading="startingPayment"
@click="handlePay"
>
立即支付订单
</el-button>
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
取消订单并释放账号
</el-button>
</div>
</div>
<div
v-if="order.status === 'pending_handoff' || canOpenDispute || canCancelDispute"
class="secondary-actions"
>
<div v-if="order.status === 'pending_handoff'" class="secondary-action-card danger">
<div>
<strong>不继续交接?</strong>
<span>取消后将提交退款申请,客服审核通过后账号才会恢复可租。</span>
</div>
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
取消订单
</el-button>
</div>
<div v-if="canOpenDispute || canCancelDispute" class="secondary-action-card support">
<span class="support-dispute-icon">
<el-icon><Service /></el-icon>
</span>
<div class="support-dispute-copy">
<strong>{{ disputeSupportTitle }}</strong>
<span>{{ disputeSupportMessage }}</span>
</div>
<el-button
type="warning"
plain
:loading="cancellingDispute"
@click="canCancelDispute ? handleCancelDispute() : openDisputeDialog()"
>
{{ disputeSupportActionText }}
</el-button>
</div>
</div>
</div>
<div class="handoff-workspace">
<OrderHandoffTimeline
v-if="order"
:records="handoffRecords"
:listing-code="listingCode"
/>
</div>
<OrderCheckoutSummary
v-if="
order &&
order.checkout &&
[
'pending_checkout_confirm',
'pending_checkout_accept',
'checkout_disputing',
'completed',
].includes(order.status)
"
:order="order"
:is-owner="isOwner"
:is-renter="isRenter"
/>
<div v-if="showCheckoutCounterForm" class="form-section counter-checkout-section">
<div class="section-header">
<h2>修改结账方案</h2>
<span v-if="checkoutRoundLabel">{{ checkoutRoundLabel }}</span>
</div>
<div class="form-card">
<p class="form-hint">
双方最多协商 6 轮。可填写<strong>实际哈夫币消耗</strong>(允许超过发布量)。
押金不足时无法自动完结,需改方案或发起争议。
</p>
<p v-if="checkoutShortfallCent > 0" class="form-hint" style="color: #dc2626">
当前方案押金不足差额 ¥{{ money(amountYuan(checkoutShortfallCent)) }},无法同意完结。
</p>
<p v-else-if="checkoutOvershootCent > 0" class="form-hint">
当前打超 ¥{{ money(amountYuan(checkoutOvershootCent)) }},将从押金扣除。
</p>
<h3 class="sub-title">调整消耗与扣款</h3>
<el-form class="form-grid" label-position="top">
<el-form-item label="额外消耗品已用金额">
<el-input-number
v-model="counterForm.consumableAmountYuan"
class="full-control"
:min="0"
:precision="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="实际消耗哈夫币(M">
<el-input-number
v-model="counterForm.coin_consumed_m"
class="full-control"
:min="0"
:precision="2"
controls-position="right"
/>
</el-form-item>
<el-form-item label="押金赔付扣除(损坏/违规,元)">
<el-input-number
v-model="counterForm.depositDeductAmountYuan"
class="full-control"
:min="0"
:precision="0"
controls-position="right"
/>
</el-form-item>
</el-form>
<el-input
v-model="counterForm.reason"
type="textarea"
:rows="3"
placeholder="填写修改原因(必填)"
/>
<el-input
v-model="counterForm.evidenceText"
type="textarea"
:rows="3"
placeholder="修正证据链接,一行一个"
/>
<el-button
type="warning"
size="large"
:loading="countering"
@click="handleCounterCheckout"
>
{{ isOwner ? '提交修正给租客' : '提交还价给号主' }}
</el-button>
</div>
</div>
<div
v-if="order && isRenter && order.status === 'pending_checkout_accept'"
class="form-section reject-checkout-section"
>
<div class="section-header">
<h2>不同意并发起争议</h2>
</div>
<div class="form-card">
<p class="form-hint">
也可先在上方「修改结账方案」还价。若谈不拢或押金不足,填写原因进入人工争议。
</p>
<el-input
v-model="rejectReason"
type="textarea"
:rows="3"
placeholder="不同意时填写原因,会进入争议处理"
/>
<el-button
type="danger"
plain
size="large"
:loading="rejectingCheckout"
@click="handleRejectCheckout"
>发起结账争议</el-button
>
</div>
</div>
<!-- 账号快照放最下方,默认折叠 -->
<OrderAccountSnapshot v-if="order" :order="order" variant="desktop" />
</div>
<!-- 右侧悬浮操作区 -->
<div v-if="order && hasSidebarActions" class="action-sidebar">
<!-- 确认结账 -->
<div v-if="isOwner && order.status === 'pending_checkout_confirm'" class="sidebar-card">
<h3 class="sidebar-title">确认结账</h3>
<p v-if="checkoutRoundLabel" class="sidebar-hint">{{ checkoutRoundLabel }}</p>
<p v-if="checkoutShortfallCent > 0" class="sidebar-hint" style="color: #dc2626">
押金不足,无法同意完结
</p>
<el-button
type="primary"
size="large"
:loading="completing"
:disabled="!canAcceptCheckoutProposal"
@click="handleConfirmCheckout"
>
确认结账并完成订单
</el-button>
<div v-if="canCounterCheckout" class="section-divider-mini">
<span>或者</span>
</div>
<el-button
v-if="canCounterCheckout"
type="warning"
plain
size="large"
@click="scrollToCounterCheckout"
>
修改结账方案
</el-button>
<p class="sidebar-hint">双方可多轮协商(最多 6 轮);押金不足请改方案或争议</p>
</div>
<!-- 确认修正结账 -->
<div v-if="isRenter && order.status === 'pending_checkout_accept'" class="sidebar-card">
<h3 class="sidebar-title">确认结账方案</h3>
<p v-if="checkoutRoundLabel" class="sidebar-hint">{{ checkoutRoundLabel }}</p>
<p v-if="checkoutShortfallCent > 0" class="sidebar-hint" style="color: #dc2626">
押金不足,无法同意完结
</p>
<el-button
type="primary"
size="large"
:loading="acceptingCheckout"
:disabled="!canAcceptCheckoutProposal"
@click="handleAcceptCheckout"
>
同意并完成订单
</el-button>
<div v-if="canCounterCheckout" class="section-divider-mini">
<span>或者</span>
</div>
<el-button
v-if="canCounterCheckout"
type="warning"
plain
size="large"
@click="scrollToCounterCheckout"
>
还价修改方案
</el-button>
<div class="section-divider-mini">
<span>或者</span>
</div>
<el-button type="danger" plain size="large" @click="scrollToRejectCheckout">
发起争议(人工)
</el-button>
<p class="sidebar-hint">可还价或争议;押金不足无法自动完结</p>
</div>
</div>
</div>
<el-dialog
v-model="disputeDialogVisible"
:title="isCheckoutDisputeStage ? '发起结账争议' : '发起申诉'"
width="720px"
append-to-body
class="dispute-dialog"
>
<div v-if="order && canOpenDispute" class="dispute-dialog-body">
<div class="dispute-dialog-tip">
<span class="support-dispute-icon">
<el-icon><Service /></el-icon>
</span>
<p>
{{
isCheckoutDisputeStage
? '提交后客服会核查结账金额和双方证据。'
: '请尽量写清楚时间点、问题经过和证据,便于客服快速处理。'
}}
</p>
</div>
<el-select
v-if="!isCheckoutDisputeStage"
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"
type="textarea"
:rows="4"
placeholder="说明争议经过、时间点和希望客服核查的证据"
/>
<el-input
v-model="disputeEvidenceText"
type="textarea"
:rows="3"
placeholder="证据链接,一行一个。开发阶段可先填截图地址或备注链接"
/>
<div class="upload-line">
<input
type="file"
accept="image/jpeg,image/png,image/webp,application/pdf"
:disabled="uploadingEvidence"
@change="handleEvidenceUpload"
/>
</div>
</div>
<template #footer>
<div class="dispute-dialog-footer">
<el-button @click="closeDisputeDialog">取消</el-button>
<el-button type="warning" :loading="disputing" @click="handleCreateDispute">
{{ isCheckoutDisputeStage ? '提交结账争议' : '提交申诉' }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="payWayDialogVisible"
title="选择支付方式"
width="420px"
append-to-body
:z-index="4000"
class="pay-way-dialog"
@closed="handlePayWayDialogClosed"
>
<p class="pay-way-tip">选择渠道后将生成对应的支付二维码</p>
<div class="pay-way-options">
<button type="button" class="pay-way-option wechat" @click="choosePayWay('WXZF')">
<span class="pay-way-icon">
<van-icon name="wechat-pay" :size="30" />
</span>
<span class="pay-way-text">
<strong>微信支付</strong>
<small>使用微信扫码完成支付</small>
</span>
</button>
<button type="button" class="pay-way-option alipay" @click="choosePayWay('ZFBZF')">
<span class="pay-way-icon">
<van-icon name="alipay" :size="30" />
</span>
<span class="pay-way-text">
<strong>支付宝支付</strong>
<small>使用支付宝扫码完成支付</small>
</span>
</button>
</div>
</el-dialog>
<el-dialog
v-model="paymentDialogVisible"
title="订单支付"
width="560px"
append-to-body
:z-index="4000"
class="order-pay-dialog"
@closed="stopPaymentPolling"
>
<div v-if="activePayment" class="pay-dialog-body">
<div class="pay-summary">
<div class="pay-summary-row">
<span>支付金额</span>
<strong>¥{{ formatCent(activePayment.amount_cent) }}</strong>
</div>
</div>
<div v-if="paymentPayURL" class="pay-qr-section">
<div class="pay-qr-box">
<el-icon v-if="qrGenerating" class="is-loading" :size="32"><Loading /></el-icon>
<img v-else-if="paymentQRCodeURL" :src="paymentQRCodeURL" alt="支付二维码" />
</div>
<div class="pay-instructions">
<h3>请使用{{ activePayWayLabel }}扫码支付</h3>
<p>扫码完成后将自动刷新,也可手动点击下方按钮确认。</p>
</div>
</div>
<p v-else class="pay-hint">支付单已创建,请完成付款后刷新状态。</p>
</div>
<template #footer>
<div class="pay-dialog-footer">
<el-button type="primary" :loading="checkingPayment" @click="handleRefreshPayment">
我已支付刷新状态
</el-button>
</div>
</template>
</el-dialog>
</section>
</template>
<style scoped>
.order-detail-page {
max-width: 1280px;
margin: 0 auto;
}
.page-header {
margin-bottom: 18px;
}
.page-header.centered {
max-width: 940px;
margin-right: auto;
margin-left: auto;
}
.header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.header-info {
flex: 1;
}
.header-info h1 {
margin: 4px 0 0;
font-size: 26px;
line-height: 1.25;
}
.order-meta {
color: #64748b;
margin: 6px 0 10px;
}
.content-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 16px;
align-items: start;
}
.content-layout.centered {
grid-template-columns: minmax(0, 1fr);
max-width: 940px;
margin: 0 auto;
}
.main-content {
min-width: 0;
}
.action-sidebar {
position: sticky;
top: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.05);
}
.sidebar-card.highlight-card {
background: #f8fbff;
border-color: #3b82f6;
}
.sidebar-title {
font-size: 14px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.sidebar-hint {
margin: 0;
font-size: 12px;
color: #64748b;
line-height: 1.45;
}
.sidebar-card .el-button {
width: 100%;
}
.support-dispute-icon {
display: grid;
flex: none;
width: 32px;
height: 32px;
place-items: center;
border-radius: 8px;
background: #fff1d6;
color: #d97706;
font-size: 18px;
}
.support-dispute-copy {
display: grid;
gap: 2px;
min-width: 0;
flex: 1;
}
.support-dispute-copy strong {
color: #1f2937;
font-size: 14px;
line-height: 1.3;
}
.support-dispute-copy span {
color: #7c5a16;
font-size: 12px;
line-height: 1.45;
}
.order-progress-section {
margin-bottom: 12px;
padding: 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.metric-card {
display: flex;
flex-direction: column;
gap: 6px;
min-height: 86px;
padding: 14px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #ffffff;
transition: all 0.2s;
}
.metric-card:hover {
border-color: #cbd5e1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.metric-card.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: #ffffff;
}
.metric-card.primary .metric-label {
color: rgba(255, 255, 255, 0.9);
}
.metric-card.primary .metric-value {
color: #ffffff;
}
.metric-card.highlight {
background: linear-gradient(135deg, #ff6a00 0%, #ee0979 100%);
border: none;
color: #ffffff;
}
.metric-card.highlight .metric-label {
color: rgba(255, 255, 255, 0.9);
}
.metric-card.highlight .metric-value {
color: #ffffff;
}
.metric-card.income {
background: #f0fdf4;
border-color: #bbf7d0;
}
.metric-card.income .metric-label {
color: #15803d;
}
.metric-card.income .metric-value {
color: #16a34a;
}
.metric-label {
font-size: 12px;
color: #64748b;
font-weight: 500;
}
.metric-value {
font-size: 20px;
font-weight: 700;
color: #1f2937;
line-height: 1.2;
}
.metric-value.amount {
color: #ff6a00;
}
.metric-note {
font-size: 12px;
color: #2563eb;
}
.info-section {
margin-bottom: 16px;
}
.info-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 14px 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0;
}
.info-label {
font-size: 13px;
color: #64748b;
font-weight: 500;
}
.info-value {
font-size: 13px;
color: #1f2937;
font-weight: 600;
text-align: right;
}
.action-card {
margin-top: 12px;
padding: 14px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.action-card.warning {
background: #fffbeb;
border-color: #fde68a;
}
.action-message {
margin: 0 0 12px 0;
color: #1f2937;
font-size: 14px;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.section-header.compact span {
color: #64748b;
font-size: 12px;
}
.section-header h2 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.form-section {
margin-bottom: 20px;
}
.form-card {
display: grid;
gap: 12px;
padding: 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.handoff-action-section {
margin-bottom: 14px;
}
.handoff-action-card {
display: grid;
grid-template-columns: minmax(0, 1fr) 160px;
gap: 12px;
align-items: stretch;
padding: 16px;
border: 1px solid #bfdbfe;
border-radius: 8px;
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
}
.handoff-action-card .el-button {
height: 100%;
min-height: 96px;
}
.handoff-confirm-strip {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
padding: 16px;
border: 1px solid #bfdbfe;
border-radius: 8px;
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
}
.handoff-confirm-strip div {
display: grid;
gap: 4px;
flex: 1;
min-width: 0;
}
.handoff-confirm-strip strong {
color: #1f2937;
font-size: 14px;
}
.handoff-confirm-strip span {
color: #475569;
font-size: 12px;
line-height: 1.45;
}
.secondary-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 10px;
margin-top: 12px;
}
.secondary-action-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 8px;
}
.secondary-action-card > div {
display: grid;
gap: 2px;
min-width: 0;
flex: 1;
}
.secondary-action-card strong {
color: #1f2937;
font-size: 13px;
line-height: 1.3;
}
.secondary-action-card span {
color: #64748b;
font-size: 12px;
line-height: 1.45;
}
.secondary-action-card.danger {
border: 1px solid #fecaca;
background: #fff7f7;
}
.secondary-action-card.support {
border: 1px solid #f8d99b;
background: #fffaf0;
}
.form-hint {
margin: 0;
padding: 10px 12px;
background: #f8fafc;
border-left: 3px solid #3b82f6;
color: #475569;
font-size: 13px;
line-height: 1.5;
border-radius: 4px;
}
.section-divider {
display: flex;
align-items: center;
justify-content: center;
margin: 24px 0;
position: relative;
}
.section-divider::before,
.section-divider::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
.section-divider span {
padding: 0 16px;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
}
.section-divider-mini {
display: flex;
align-items: center;
justify-content: center;
margin: 8px 0;
position: relative;
}
.section-divider-mini::before,
.section-divider-mini::after {
content: '';
flex: 1;
height: 1px;
background: #e5e7eb;
}
.section-divider-mini span {
padding: 0 12px;
color: #9ca3af;
font-size: 12px;
font-weight: 500;
}
.sub-title {
font-size: 15px;
font-weight: 600;
color: #374151;
margin: 0 0 6px 0;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
}
.field-hint {
display: block;
margin-top: 6px;
color: #6b7280;
font-size: 12px;
}
.dispute-dialog-body {
display: grid;
gap: 12px;
}
.dispute-dialog-tip {
display: flex;
gap: 10px;
align-items: center;
padding: 10px 12px;
border: 1px solid #f8d99b;
border-radius: 8px;
background: #fffaf0;
}
.dispute-dialog-tip p {
margin: 0;
color: #7c5a16;
font-size: 13px;
line-height: 1.5;
}
.dispute-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.upload-line {
padding: 12px;
background: #f9fafb;
border: 1px dashed #cbd5e1;
border-radius: 6px;
}
.upload-line input[type='file'] {
width: 100%;
font-size: 14px;
}
:global(.dispute-dialog) {
border-radius: 12px;
}
:global(.dispute-dialog .el-dialog__body) {
padding: 14px 20px 8px;
}
:global(.dispute-dialog .el-dialog__footer) {
padding: 12px 20px 18px;
}
.pay-dialog-body {
display: grid;
gap: 20px;
}
.pay-way-tip {
margin: 0 0 16px 0;
color: #64748b;
font-size: 14px;
}
.pay-way-options {
display: grid;
gap: 12px;
}
.pay-way-option {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 18px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
text-align: left;
transition: all 0.18s ease;
}
.pay-way-option:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.pay-way-icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: 12px;
color: #ffffff;
flex-shrink: 0;
}
.pay-way-option.wechat:hover {
border-color: #07c160;
}
.pay-way-option.wechat .pay-way-icon {
background: #07c160;
}
.pay-way-option.alipay:hover {
border-color: #1677ff;
}
.pay-way-option.alipay .pay-way-icon {
background: #1677ff;
}
.pay-way-text {
display: flex;
flex-direction: column;
gap: 3px;
}
.pay-way-text strong {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.pay-way-text small {
font-size: 13px;
color: #94a3b8;
}
:global(.pay-way-dialog) {
border-radius: 16px;
}
:global(.pay-way-dialog .el-dialog__header) {
padding: 20px 24px 0;
}
:global(.pay-way-dialog .el-dialog__body) {
padding: 16px 24px 24px;
}
.pay-summary {
padding: 16px 20px;
background: #f8fafc;
border-radius: 8px;
}
.pay-summary-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.pay-summary-row span {
color: #64748b;
font-size: 14px;
}
.pay-summary-row strong {
color: #111a44;
font-size: 28px;
font-weight: 700;
}
.pay-qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 24px;
background: #f8fafc;
border-radius: 12px;
}
.pay-qr-box {
width: 240px;
height: 240px;
display: grid;
place-items: center;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.pay-qr-box img {
width: 220px;
height: 220px;
display: block;
}
.pay-instructions {
text-align: center;
}
.pay-instructions h3 {
margin: 0 0 8px 0;
color: #111a44;
font-size: 18px;
font-weight: 600;
}
.pay-instructions p {
margin: 0;
color: #64748b;
font-size: 14px;
line-height: 1.7;
}
.pay-hint {
margin: 0;
padding: 16px;
background: #f8fafc;
color: #64748b;
font-size: 14px;
text-align: center;
border-radius: 8px;
}
.pay-dialog-footer {
display: flex;
justify-content: center;
padding-top: 8px;
}
:global(.order-pay-dialog) {
border-radius: 16px;
}
:global(.order-pay-dialog .el-dialog__header) {
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
:global(.order-pay-dialog .el-dialog__body) {
padding: 24px;
}
:global(.order-pay-dialog .el-dialog__footer) {
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
}
@media (max-width: 768px) {
.content-layout {
grid-template-columns: 1fr;
}
.action-sidebar {
position: static;
order: -1;
}
.header-top {
flex-direction: column;
}
.detail-grid {
grid-template-columns: repeat(2, 1fr);
}
.form-grid {
grid-template-columns: 1fr;
}
.handoff-action-card,
.handoff-confirm-strip,
.secondary-action-card {
grid-template-columns: 1fr;
flex-direction: column;
align-items: stretch;
}
.handoff-action-card .el-button {
min-height: 40px;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .el-button {
width: 100%;
}
.pay-qr-section {
padding: 16px;
}
}
</style>