1601 lines
40 KiB
Vue
1601 lines
40 KiB
Vue
<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>
|