1978 lines
63 KiB
Vue
1978 lines
63 KiB
Vue
<script setup lang="ts">
|
||
import { readError } from '@/shared/utils/error'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
|
||
import {
|
||
adminCloseOrder,
|
||
adminForceHandoff,
|
||
adminHoldDeposit,
|
||
adminMarkOfflineSettlement,
|
||
adminMarkOrderAbnormal,
|
||
adminPlatformCheckoutCounter,
|
||
adminPlatformCheckoutConfirm,
|
||
adminPlatformCheckoutDispute,
|
||
adminPlatformHandoff,
|
||
adminRefundOrder,
|
||
adminRefundStatus,
|
||
adminReleaseDeposit,
|
||
adminResetHandoff,
|
||
adminSealOrder,
|
||
fetchAdminHandoffRecords,
|
||
fetchAdminOrder,
|
||
type HandoffRecord,
|
||
type Order,
|
||
type RefundStatus,
|
||
} from '@/features/orders'
|
||
import { adminCreateOrderDispute } from '@/features/disputes'
|
||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||
import {
|
||
getSnapshotHafCoinM,
|
||
linesToList,
|
||
readSnapshot,
|
||
readSnapshotResources,
|
||
} from '@/features/orders/composables/useOrderSnapshot'
|
||
import { adminPath } from '@/shared/utils/adminPath'
|
||
import { centToYuan, formatCentWithSymbol } from '@/shared/utils/money'
|
||
import {
|
||
disputeStatusLabel,
|
||
orderHandoffStatusLabel,
|
||
orderStatusLabel,
|
||
refundStatusLabel,
|
||
settlementStatusLabel,
|
||
} from '@/shared/utils/statusLabels'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay'
|
||
|
||
const route = useRoute()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const order = ref<Order | null>(null)
|
||
const handoffRecords = ref<HandoffRecord[]>([])
|
||
const paymentRecords = ref<AdminPayment[]>([])
|
||
type OrderActionType =
|
||
| 'close'
|
||
| 'seal'
|
||
| 'abnormal'
|
||
| 'reset'
|
||
| 'platform_checkout_confirm'
|
||
| 'platform_checkout_dispute'
|
||
| 'deposit_hold'
|
||
| 'deposit_release'
|
||
| ''
|
||
const actionType = ref<OrderActionType>('')
|
||
const reason = ref('')
|
||
const refundStatus = ref<RefundStatus | null>(null)
|
||
const platformHandoffVisible = ref(false)
|
||
const platformHandoffContent = ref('')
|
||
const platformHandoffReason = ref('')
|
||
const forceHandoffVisible = ref(false)
|
||
const forceHandoffContent = ref('')
|
||
const forceHandoffReason = ref('')
|
||
const platformCheckoutCounterVisible = ref(false)
|
||
const platformCheckoutCounterForm = ref({
|
||
consumableAmountYuan: 0,
|
||
coin_consumed_m: 0,
|
||
depositDeductAmountYuan: 0,
|
||
reason: '',
|
||
evidenceText: '',
|
||
})
|
||
const offlineSettlementVisible = ref(false)
|
||
const offlineSettlementRemark = ref('')
|
||
const platformSubmitting = ref(false)
|
||
const adminDisputeVisible = ref(false)
|
||
const adminDisputeSubmitting = ref(false)
|
||
const adminDisputeForm = ref({
|
||
type: 'cannot_login',
|
||
targetRole: 'renter',
|
||
description: '',
|
||
evidenceText: '',
|
||
})
|
||
|
||
type FundSplitRow = {
|
||
label: string
|
||
amountCent: number | null | undefined
|
||
}
|
||
|
||
const listingCode = computed(() =>
|
||
order.value ? formatListingNo(order.value.listing_no, order.value.listing_id) : '-'
|
||
)
|
||
const returnTarget = computed(() => {
|
||
const from = firstQueryValue(route.query.from)
|
||
const chatID = firstQueryValue(route.query.chat_id)
|
||
const chatFilter = firstQueryValue(route.query.chat_filter)
|
||
const chatStage = firstQueryValue(route.query.chat_stage)
|
||
const chatKeyword = firstQueryValue(route.query.chat_keyword)
|
||
if (from === 'chat' && chatID) {
|
||
return {
|
||
path: adminPath('chats'),
|
||
query: {
|
||
chat_id: chatID,
|
||
...(chatFilter ? { chat_filter: chatFilter } : {}),
|
||
...(chatStage ? { chat_stage: chatStage } : {}),
|
||
...(chatKeyword ? { chat_keyword: chatKeyword } : {}),
|
||
},
|
||
}
|
||
}
|
||
return adminPath('orders')
|
||
})
|
||
const returnLabel = computed(() =>
|
||
firstQueryValue(route.query.from) === 'chat' && firstQueryValue(route.query.chat_id)
|
||
? '返回会话'
|
||
: '返回列表'
|
||
)
|
||
const refunding = ref(false)
|
||
|
||
const snapshotText = computed(() => JSON.stringify(order.value?.account_snapshot || {}, null, 2))
|
||
const canOperate = computed(
|
||
() => !!order.value && !['completed', 'cancelled', 'closed'].includes(order.value.status)
|
||
)
|
||
const resetAction = computed(() => order.value?.admin_actions?.reset_handoff)
|
||
const platformHandoffAction = computed(() => order.value?.admin_actions?.platform_handoff)
|
||
const forceHandoffAction = computed(() => order.value?.admin_actions?.force_handoff)
|
||
const platformCheckoutConfirmAction = computed(
|
||
() => order.value?.admin_actions?.platform_checkout_confirm
|
||
)
|
||
const platformCheckoutCounterAction = computed(
|
||
() => order.value?.admin_actions?.platform_checkout_counter
|
||
)
|
||
const platformCheckoutDisputeAction = computed(
|
||
() => order.value?.admin_actions?.platform_checkout_dispute
|
||
)
|
||
const offlineSettlementAction = computed(
|
||
() => order.value?.admin_actions?.platform_offline_settlement
|
||
)
|
||
const resetActionLabel = computed(() => {
|
||
return resetAction.value?.label || '重置'
|
||
})
|
||
const canResetHandoff = computed(() => resetAction.value?.enabled === true)
|
||
const canPlatformHandoff = computed(() => platformHandoffAction.value?.enabled === true)
|
||
const canForceHandoff = computed(() => forceHandoffAction.value?.enabled === true)
|
||
const forceHandoffNeedsContent = computed(
|
||
() =>
|
||
order.value?.handoff_status === 'pending_owner' ||
|
||
order.value?.handoff_status === 'owner_timeout'
|
||
)
|
||
const canPlatformCheckoutConfirm = computed(
|
||
() => platformCheckoutConfirmAction.value?.enabled === true
|
||
)
|
||
const canPlatformCheckoutCounter = computed(
|
||
() => platformCheckoutCounterAction.value?.enabled === true
|
||
)
|
||
const canPlatformCheckoutDispute = computed(
|
||
() => platformCheckoutDisputeAction.value?.enabled === true
|
||
)
|
||
const canOfflineSettlement = computed(() => offlineSettlementAction.value?.enabled === true)
|
||
const canAdminCreateDispute = computed(() => canOperate.value && !order.value?.active_dispute)
|
||
const isPlatformManaged = computed(
|
||
() =>
|
||
order.value?.handoff_mode === 'platform' || order.value?.settlement_mode === 'platform_managed'
|
||
)
|
||
const offlineSettlementStatus = computed(() => order.value?.offline_settlement_status || 'none')
|
||
// 押金暂扣:仅进行中、有实付押金、且未暂扣过的订单可暂扣。
|
||
const depositHoldStatus = computed(() => order.value?.deposit_hold_status || 'none')
|
||
const depositHoldAmountCent = computed(() => Number(order.value?.deposit_hold_amount_cent || 0))
|
||
const isDepositHeld = computed(() => depositHoldStatus.value === 'held')
|
||
const canHoldDeposit = computed(
|
||
() =>
|
||
canOperate.value &&
|
||
depositHoldStatus.value === 'none' &&
|
||
Number(order.value?.deposit_amount_cent || 0) > 0
|
||
)
|
||
const canReleaseDeposit = computed(() => isDepositHeld.value && depositHoldAmountCent.value > 0)
|
||
const depositHoldStatusLabel = computed(() => {
|
||
switch (depositHoldStatus.value) {
|
||
case 'held':
|
||
return '押金已暂扣'
|
||
case 'released':
|
||
return '押金已归还'
|
||
default:
|
||
return ''
|
||
}
|
||
})
|
||
const actionTitle = computed(() => {
|
||
if (actionType.value === 'close') return '客服关闭订单'
|
||
if (actionType.value === 'seal') return '封存订单'
|
||
if (actionType.value === 'reset') return `${resetActionLabel.value}(恢复到对应待办)`
|
||
if (actionType.value === 'platform_checkout_confirm') return '客服确认结账'
|
||
if (actionType.value === 'platform_checkout_dispute') return '发起结账争议'
|
||
if (actionType.value === 'deposit_hold') return '暂扣押金'
|
||
if (actionType.value === 'deposit_release') return '归还暂扣押金'
|
||
return '标记订单异常'
|
||
})
|
||
const actionConfirmButtonType = computed(() => {
|
||
if (['deposit_release', 'platform_checkout_confirm'].includes(actionType.value)) return 'primary'
|
||
if (actionType.value === 'platform_checkout_dispute') return 'warning'
|
||
return 'danger'
|
||
})
|
||
const closeActionTip =
|
||
'关闭订单并归档商品/账号;已支付订单会原路退款,押金已暂扣时押金部分会继续挂起。'
|
||
const sealActionTip =
|
||
'封存订单:终止订单并原路退款,自动解散关联群聊,关联商品永久封存;押金已暂扣时押金部分会继续挂起。'
|
||
const depositHoldActionTip = '暂扣后订单继续流转;后续本应退给租客的押金会挂起,需客服手动归还。'
|
||
const depositReleaseActionTip = computed(() =>
|
||
depositHoldAmountCent.value > 0
|
||
? `将已暂扣的 ${moneyCent(depositHoldAmountCent.value)} 押金原路归还租客。`
|
||
: '暂扣金额为 0,需订单结算/关闭/仲裁产生暂扣金额后才能归还。'
|
||
)
|
||
const refundActionTip =
|
||
'仅发起后台人工原路退款,不关闭订单或调整商品/账号状态;押金已暂扣时仅退非暂扣部分。'
|
||
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
|
||
const disputeTypeOptions = [
|
||
{ label: '无法登录', value: 'cannot_login' },
|
||
{ label: '描述不符', value: 'false_description' },
|
||
{ label: '账号封禁', value: 'account_banned' },
|
||
{ label: '资产损失', value: 'asset_loss' },
|
||
{ label: '哈夫币争议', value: 'haf_coin_dispute' },
|
||
{ label: '交接超时', value: 'handoff_timeout' },
|
||
{ label: '归还超时', value: 'return_timeout' },
|
||
{ label: '结账金额争议', value: 'checkout_amount' },
|
||
]
|
||
const orderTotalCent = computed(
|
||
() => Number(order.value?.rent_amount_cent || 0) + Number(order.value?.deposit_amount_cent || 0)
|
||
)
|
||
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 checkoutRemainingHafCoinM = computed(() => {
|
||
if (!order.value?.checkout) return 0
|
||
return Number(snapshotHafCoinM.value || 0) - Number(order.value.checkout.coin_consumed_m || 0)
|
||
})
|
||
const snapshotResources = computed(() => readSnapshotResources(order.value).slice(0, 6))
|
||
const assetSummary = computed(() => {
|
||
const snapshot = order.value?.account_snapshot
|
||
if (typeof snapshot !== 'object' || snapshot === null) return null
|
||
const summary = (snapshot as Record<string, unknown>).asset_summary
|
||
if (typeof summary !== 'object' || summary === null) return null
|
||
return summary as Record<string, unknown>
|
||
})
|
||
const snapshotPriceBreakdown = computed(() => {
|
||
const breakdown = assetSummary.value?.price_breakdown
|
||
if (typeof breakdown !== 'object' || breakdown === null) return null
|
||
return breakdown as Record<string, unknown>
|
||
})
|
||
const snapshotRatio = computed(() => {
|
||
const row = snapshotPriceBreakdown.value
|
||
if (!row) return null
|
||
const sellerRatio = Number(row.seller_ratio || 0)
|
||
const buyerRatio = Number(row.buyer_ratio || 0)
|
||
const publishRatio = Number(assetSummary.value?.publish_ratio || 0)
|
||
return {
|
||
recycleRatio: sellerRatio > 0 ? sellerRatio : publishRatio > 0 ? publishRatio : 0,
|
||
sellRatio: buyerRatio > 0 ? buyerRatio : 0,
|
||
}
|
||
})
|
||
const ownerCoinBasePriceCent = computed(() => readPriceBreakdownCent('seller_coin_base_price'))
|
||
const ownerLossPriceCent = computed(() => {
|
||
const consumableCent = readPriceBreakdownCent('consumable_price')
|
||
if (consumableCent !== null) return consumableCent
|
||
if (ownerCoinBasePriceCent.value === null || !order.value?.owner_rent_amount_cent) return null
|
||
return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0)
|
||
})
|
||
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
|
||
const fundSplitRows = computed(() => {
|
||
if (!order.value) return []
|
||
const rows: FundSplitRow[] = []
|
||
rows.push(
|
||
{ label: '预收纯币原价', amountCent: order.value.pure_coin_original_amount_cent },
|
||
{ label: '预收额外物品', amountCent: order.value.extra_item_original_amount_cent }
|
||
)
|
||
if (Number(order.value.rent_discount_amount_cent || 0) > 0) {
|
||
rows.push(
|
||
{ label: '原始租金', amountCent: order.value.rent_original_amount_cent },
|
||
{
|
||
label: `${order.value.renter_growth_level_name || '成长等级'}优惠`,
|
||
amountCent: -Number(order.value.rent_discount_amount_cent || 0),
|
||
}
|
||
)
|
||
}
|
||
if (order.value.status === 'completed') {
|
||
rows.push(
|
||
{ label: '最终实际纯币原价', amountCent: order.value.actual_pure_coin_amount_cent },
|
||
{
|
||
label: '最终实际纯币优惠',
|
||
amountCent: -Number(order.value.actual_pure_coin_discount_cent || 0),
|
||
},
|
||
{ label: '积分计算纯币基数', amountCent: order.value.growth_points_basis_cent }
|
||
)
|
||
}
|
||
rows.push({ label: '租客租金', amountCent: order.value.rent_amount_cent })
|
||
if (hasOwnerRentBreakdown.value) {
|
||
rows.push(
|
||
{ label: '号主纯币价格', amountCent: ownerCoinBasePriceCent.value },
|
||
{ label: '号主损耗', amountCent: ownerLossPriceCent.value ?? 0 },
|
||
{ label: '号主租金合计', amountCent: order.value.owner_rent_amount_cent }
|
||
)
|
||
} else {
|
||
rows.push({ label: '号主租金', amountCent: order.value.owner_rent_amount_cent })
|
||
}
|
||
rows.push(
|
||
{ label: '平台费用', amountCent: order.value.platform_fee_cent },
|
||
{ label: '实付押金', amountCent: order.value.deposit_amount_cent },
|
||
{ label: '原始押金', amountCent: order.value.deposit_original_amount_cent },
|
||
{ label: '免押额度', amountCent: order.value.deposit_waived_amount_cent }
|
||
)
|
||
return rows
|
||
})
|
||
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: formatGameName(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)
|
||
|
||
async function loadOrder() {
|
||
loading.value = true
|
||
try {
|
||
order.value = await fetchAdminOrder(String(route.params.id))
|
||
handoffRecords.value = await fetchAdminHandoffRecords(String(route.params.id))
|
||
paymentRecords.value = (await fetchAdminPayments({ order_id: String(route.params.id) })).items
|
||
await loadRefundStatus()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadRefundStatus() {
|
||
try {
|
||
refundStatus.value = await adminRefundStatus(Number(route.params.id))
|
||
} catch {
|
||
refundStatus.value = null
|
||
}
|
||
}
|
||
|
||
function openAction(type: Exclude<OrderActionType, ''>) {
|
||
actionType.value = type
|
||
reason.value = ''
|
||
}
|
||
|
||
async function confirmCloseAction() {
|
||
if (!order.value || !canOperate.value) return
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'客服关闭会终止订单,归档关联商品和账号;若订单已支付,将发起原路退款。若押金已暂扣,押金部分会继续挂起。确认继续?',
|
||
'确认客服关闭',
|
||
{
|
||
confirmButtonText: '继续关闭',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
openAction('close')
|
||
}
|
||
|
||
async function confirmSealAction() {
|
||
if (!order.value || !canOperate.value) return
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'封存会终止订单并发起原路退款,自动解散关联群聊,关联商品将被永久封存。若押金已暂扣,押金部分会继续挂起。确认继续?',
|
||
'确认封存订单',
|
||
{
|
||
confirmButtonText: '继续封存',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
openAction('seal')
|
||
}
|
||
|
||
async function confirmHoldDeposit() {
|
||
if (!order.value || !canHoldDeposit.value) return
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'暂扣后订单继续流转;后续结算、关闭或仲裁中本应退给租客的押金会被挂起,需客服手动归还。确认继续?',
|
||
'确认暂扣押金',
|
||
{
|
||
confirmButtonText: '继续暂扣',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
openAction('deposit_hold')
|
||
}
|
||
|
||
async function confirmReleaseDeposit() {
|
||
if (!order.value || !isDepositHeld.value) return
|
||
if (!canReleaseDeposit.value) {
|
||
ElMessage.warning('暂扣押金尚未形成可归还金额')
|
||
return
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将已暂扣的 ${moneyCent(depositHoldAmountCent.value)} 押金原路归还租客。确认继续?`,
|
||
'确认归还押金',
|
||
{
|
||
confirmButtonText: '继续归还',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
openAction('deposit_release')
|
||
}
|
||
|
||
async function submitAction() {
|
||
if (!order.value || !actionType.value) return
|
||
submitting.value = true
|
||
try {
|
||
if (actionType.value === 'close') {
|
||
await adminCloseOrder(order.value.id, reason.value)
|
||
ElMessage.success('订单已关闭')
|
||
} else if (actionType.value === 'seal') {
|
||
await adminSealOrder(order.value.id, reason.value)
|
||
ElMessage.success('订单已封存,群聊已解散')
|
||
} else if (actionType.value === 'reset') {
|
||
await adminResetHandoff(order.value.id, reason.value)
|
||
ElMessage.success(`${resetActionLabel.value}成功`)
|
||
} else if (actionType.value === 'platform_checkout_confirm') {
|
||
await adminPlatformCheckoutConfirm(order.value.id, reason.value)
|
||
ElMessage.success('结账已确认')
|
||
} else if (actionType.value === 'platform_checkout_dispute') {
|
||
await adminPlatformCheckoutDispute(order.value.id, reason.value)
|
||
ElMessage.success('已发起结账争议')
|
||
} else if (actionType.value === 'deposit_hold') {
|
||
await adminHoldDeposit(order.value.id, reason.value)
|
||
ElMessage.success('押金已暂扣')
|
||
} else if (actionType.value === 'deposit_release') {
|
||
await adminReleaseDeposit(order.value.id, reason.value)
|
||
ElMessage.success('暂扣押金已发起归还')
|
||
} else {
|
||
await adminMarkOrderAbnormal(order.value.id, reason.value)
|
||
ElMessage.success('订单已标记异常')
|
||
}
|
||
actionType.value = ''
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '操作失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function openPlatformHandoff() {
|
||
platformHandoffContent.value = ''
|
||
platformHandoffReason.value = ''
|
||
platformHandoffVisible.value = true
|
||
}
|
||
|
||
async function submitPlatformHandoff() {
|
||
if (!order.value) return
|
||
if (!platformHandoffContent.value.trim() || !platformHandoffReason.value.trim()) {
|
||
ElMessage.warning('请填写交接说明和操作原因')
|
||
return
|
||
}
|
||
platformSubmitting.value = true
|
||
try {
|
||
await adminPlatformHandoff(
|
||
order.value.id,
|
||
platformHandoffContent.value.trim(),
|
||
platformHandoffReason.value.trim()
|
||
)
|
||
ElMessage.success('客服代交接已提交')
|
||
platformHandoffVisible.value = false
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '代交接失败'))
|
||
} finally {
|
||
platformSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function openForceHandoff() {
|
||
forceHandoffContent.value = ''
|
||
forceHandoffReason.value = ''
|
||
forceHandoffVisible.value = true
|
||
}
|
||
|
||
async function submitForceHandoff() {
|
||
if (!order.value) return
|
||
if (!forceHandoffReason.value.trim()) {
|
||
ElMessage.warning('请填写客服操作原因')
|
||
return
|
||
}
|
||
if (forceHandoffNeedsContent.value && !forceHandoffContent.value.trim()) {
|
||
ElMessage.warning('当前尚无交接说明,请填写交接内容')
|
||
return
|
||
}
|
||
platformSubmitting.value = true
|
||
try {
|
||
await adminForceHandoff(
|
||
order.value.id,
|
||
forceHandoffContent.value.trim(),
|
||
forceHandoffReason.value.trim()
|
||
)
|
||
ElMessage.success('客服已确认交接,订单进入使用中')
|
||
forceHandoffVisible.value = false
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '客服一键交接失败'))
|
||
} finally {
|
||
platformSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function openPlatformCheckoutCounter() {
|
||
const checkout = order.value?.checkout
|
||
platformCheckoutCounterForm.value = {
|
||
consumableAmountYuan: centToYuan(checkout?.consumable_amount_cent || 0),
|
||
coin_consumed_m: Number(checkout?.coin_consumed_m || snapshotHafCoinM.value || 0),
|
||
depositDeductAmountYuan: centToYuan(
|
||
checkout?.deposit_deduct_amount_cent || checkout?.other_amount_cent || 0
|
||
),
|
||
reason: '',
|
||
evidenceText: Array.isArray(checkout?.evidence_urls) ? checkout.evidence_urls.join('\n') : '',
|
||
}
|
||
platformCheckoutCounterVisible.value = true
|
||
}
|
||
|
||
async function submitPlatformCheckoutCounter() {
|
||
if (!order.value) return
|
||
const form = platformCheckoutCounterForm.value
|
||
if (!form.reason.trim()) {
|
||
ElMessage.warning('请填写修改原因')
|
||
return
|
||
}
|
||
platformSubmitting.value = true
|
||
try {
|
||
await adminPlatformCheckoutCounter(order.value.id, {
|
||
content: form.reason.trim(),
|
||
consumableAmountYuan: form.consumableAmountYuan,
|
||
coin_consumed_m: form.coin_consumed_m,
|
||
otherAmountYuan: form.depositDeductAmountYuan,
|
||
depositDeductAmountYuan: form.depositDeductAmountYuan,
|
||
reason: form.reason.trim(),
|
||
evidence_urls: linesToList(form.evidenceText),
|
||
})
|
||
ElMessage.success('结账方案已修改,等待租客确认')
|
||
platformCheckoutCounterVisible.value = false
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '修改结账方案失败'))
|
||
} finally {
|
||
platformSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function openAdminDispute() {
|
||
adminDisputeForm.value = {
|
||
type: ['pending_checkout_confirm', 'pending_checkout_accept'].includes(
|
||
order.value?.status || ''
|
||
)
|
||
? 'checkout_amount'
|
||
: 'cannot_login',
|
||
targetRole: 'renter',
|
||
description: '',
|
||
evidenceText: '',
|
||
}
|
||
adminDisputeVisible.value = true
|
||
}
|
||
|
||
async function submitAdminDispute() {
|
||
if (!order.value) return
|
||
const form = adminDisputeForm.value
|
||
if (!form.description.trim()) {
|
||
ElMessage.warning('请填写申诉说明')
|
||
return
|
||
}
|
||
adminDisputeSubmitting.value = true
|
||
try {
|
||
await adminCreateOrderDispute(order.value.id, {
|
||
type: form.type,
|
||
target_role: form.targetRole,
|
||
description: form.description.trim(),
|
||
evidence_urls: linesToList(form.evidenceText),
|
||
})
|
||
ElMessage.success('申诉已发起,订单进入仲裁流程')
|
||
adminDisputeVisible.value = false
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '发起申诉失败'))
|
||
} finally {
|
||
adminDisputeSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function openOfflineSettlement() {
|
||
offlineSettlementRemark.value = ''
|
||
offlineSettlementVisible.value = true
|
||
}
|
||
|
||
async function submitOfflineSettlement() {
|
||
if (!order.value) return
|
||
platformSubmitting.value = true
|
||
try {
|
||
await adminMarkOfflineSettlement(order.value.id, offlineSettlementRemark.value.trim())
|
||
ElMessage.success('线下结算已确认')
|
||
offlineSettlementVisible.value = false
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '确认线下结算失败'))
|
||
} finally {
|
||
platformSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function orderRentedAt() {
|
||
return order.value?.rented_at
|
||
}
|
||
|
||
function orderEstimatedEndAt() {
|
||
if (!order.value) return undefined
|
||
if (order.value.estimated_end_at) return order.value.estimated_end_at
|
||
const rentedAt = orderRentedAt()
|
||
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
||
if (!rentedAt || durationHours <= 0) return undefined
|
||
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
|
||
}
|
||
|
||
async function handleRefund() {
|
||
if (!order.value) return
|
||
refunding.value = true
|
||
try {
|
||
refundStatus.value = await adminRefundOrder(order.value.id)
|
||
ElMessage.success('退款已发起')
|
||
await loadOrder()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '退款失败'))
|
||
} finally {
|
||
refunding.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmRefund() {
|
||
if (!order.value || refundButtonDisabled.value) return
|
||
const hasRefundInProgress = ['pending', 'refunding'].includes(
|
||
refundStatus.value?.refund_status || ''
|
||
)
|
||
const message = hasRefundInProgress
|
||
? '当前订单已有退款处理中,继续人工退款可能导致重复全量退款。确认仍要发起?'
|
||
: isDepositHeld.value
|
||
? '人工退款不会关闭订单或调整商品状态;当前押金已暂扣,本次仅退非暂扣部分。确认继续?'
|
||
: '人工退款只会发起原路退款,不会关闭订单或调整商品状态。请确认该订单确实需要补发退款。'
|
||
try {
|
||
await ElMessageBox.confirm(message, '确认人工退款', {
|
||
confirmButtonText: '确认退款',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
await handleRefund()
|
||
}
|
||
|
||
function paymentStatusLabel(status: string) {
|
||
const map: Record<string, string> = {
|
||
created: '已创建',
|
||
paying: '支付中',
|
||
paid: '已支付',
|
||
refunding: '退款中',
|
||
refunded: '已退款',
|
||
failed: '失败',
|
||
}
|
||
return map[status] || status
|
||
}
|
||
|
||
function paymentStatusType(status: string) {
|
||
if (['paid', 'refunded'].includes(status)) return 'success'
|
||
if (status === 'failed') return 'danger'
|
||
if (['paying', 'refunding'].includes(status)) return 'warning'
|
||
return 'info'
|
||
}
|
||
|
||
function paymentBizTypeLabel(type: string) {
|
||
const map: Record<string, string> = {
|
||
order_pay: '订单支付',
|
||
checkout_refund: '结账退款',
|
||
arbitration_refund: '仲裁退款',
|
||
admin_refund: '人工退款',
|
||
cancel_refund: '取消退款',
|
||
admin_close_refund: '客服关闭退款',
|
||
admin_seal_refund: '封存退款',
|
||
deposit_refund: '暂扣押金归还',
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
function moneyCent(value: number | undefined | null) {
|
||
return formatCentWithSymbol(Number(value || 0))
|
||
}
|
||
|
||
function readPriceBreakdownCent(key: string) {
|
||
const raw = snapshotPriceBreakdown.value?.[key]
|
||
if (raw === undefined || raw === null || raw === '') return null
|
||
const value = Number(raw)
|
||
if (!Number.isFinite(value)) return null
|
||
return Math.round(value * 100)
|
||
}
|
||
|
||
function firstQueryValue(value: unknown) {
|
||
if (Array.isArray(value)) return typeof value[0] === 'string' ? value[0] : ''
|
||
return typeof value === 'string' ? value : ''
|
||
}
|
||
|
||
function formatHandoffRecordType(type: string) {
|
||
const typeMap: Record<string, string> = {
|
||
owner_handoff: '卖家交接',
|
||
platform_handoff: '客服代交接',
|
||
renter_checkout: '买家结账',
|
||
owner_counter_checkout: '卖家反驳结账',
|
||
platform_checkout_counter: '客服修改结账',
|
||
renter_confirm_checkout: '买家确认结账',
|
||
owner_accept_checkout: '卖家接受结账',
|
||
admin_arbitration: '客服仲裁',
|
||
platform_checkout_dispute_opened: '客服发起结账争议',
|
||
}
|
||
return typeMap[type] || type
|
||
}
|
||
|
||
function handoffModeLabel(mode?: string) {
|
||
const map: Record<string, string> = {
|
||
owner: '号主交接',
|
||
platform: '平台代管',
|
||
}
|
||
return map[mode || 'owner'] || mode || '-'
|
||
}
|
||
|
||
function settlementModeLabel(mode?: string) {
|
||
const map: Record<string, string> = {
|
||
owner_wallet: '号主钱包',
|
||
platform_managed: '线下结算',
|
||
}
|
||
return map[mode || 'owner_wallet'] || mode || '-'
|
||
}
|
||
|
||
function offlineSettlementStatusLabel(status?: string) {
|
||
const map: Record<string, string> = {
|
||
none: '无需线下结算',
|
||
pending: '待线下结算',
|
||
settled: '已线下结算',
|
||
}
|
||
return map[status || 'none'] || status || '-'
|
||
}
|
||
|
||
function offlineSettlementStatusType(status?: string) {
|
||
if (status === 'settled') return 'success'
|
||
if (status === 'pending') return 'warning'
|
||
return 'info'
|
||
}
|
||
|
||
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: '订单争议',
|
||
cannot_login: '无法登录',
|
||
false_description: '描述不符',
|
||
account_banned: '账号封禁',
|
||
asset_loss: '资产损失',
|
||
haf_coin_dispute: '哈夫币争议',
|
||
handoff_timeout: '交接超时',
|
||
return_timeout: '归还超时',
|
||
checkout_amount: '结账金额争议',
|
||
checkout_dispute: '结账争议',
|
||
}
|
||
return map[type] || type || '-'
|
||
}
|
||
|
||
function disputeInitiatorLabel() {
|
||
const active = order.value?.active_dispute
|
||
if (!active) return '-'
|
||
if (active.initiator_type === 'admin') {
|
||
return active.initiator_admin_id ? `客服 ID ${active.initiator_admin_id}` : '客服'
|
||
}
|
||
return `用户 ID ${active.initiator_id}`
|
||
}
|
||
|
||
function userDisplay(phone: string | undefined, id: number) {
|
||
return phone ? `${phone} / ID ${id}` : `ID ${id}`
|
||
}
|
||
|
||
function sellerDisplay(row: Order) {
|
||
if (row.handoff_mode === 'platform' || row.settlement_mode === 'platform_managed') {
|
||
return row.managed_admin_id ? `平台代管 / 客服ID ${row.managed_admin_id}` : '平台代管'
|
||
}
|
||
return userDisplay(row.owner_phone, row.owner_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 formatRatioNumber(value: number) {
|
||
const rounded = Math.round(value * 100) / 100
|
||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
|
||
}
|
||
|
||
function paymentPaidAt(record: AdminPayment) {
|
||
return record.paid_at || record.updated_at || record.created_at
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page" v-loading="loading">
|
||
<div v-if="order" class="page-header-row">
|
||
<div class="page-header">
|
||
<p class="eyebrow">{{ order.order_no }}</p>
|
||
<h1>订单详情</h1>
|
||
<p>
|
||
商品编号 {{ listingCode }} · {{ order.title }} · {{ order.server_region }} /
|
||
{{ order.login_platform }}
|
||
</p>
|
||
</div>
|
||
<div class="toolbar-actions">
|
||
<RouterLink :to="returnTarget">
|
||
<el-button>{{ returnLabel }}</el-button>
|
||
</RouterLink>
|
||
<el-button v-if="canResetHandoff" type="success" @click="openAction('reset')">
|
||
{{ resetActionLabel }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="canPlatformHandoff"
|
||
class="platform-handoff-button"
|
||
type="primary"
|
||
@click="openPlatformHandoff"
|
||
>
|
||
{{ platformHandoffAction?.label || '客服代交接' }}
|
||
</el-button>
|
||
<el-button v-if="canForceHandoff" type="warning" @click="openForceHandoff">
|
||
{{ forceHandoffAction?.label || '客服一键交接' }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="canPlatformCheckoutConfirm"
|
||
type="primary"
|
||
@click="openAction('platform_checkout_confirm')"
|
||
>
|
||
{{ platformCheckoutConfirmAction?.label || '客服确认结账' }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="canPlatformCheckoutCounter"
|
||
type="warning"
|
||
plain
|
||
@click="openPlatformCheckoutCounter"
|
||
>
|
||
{{ platformCheckoutCounterAction?.label || '客服修改结账方案' }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="canPlatformCheckoutDispute"
|
||
type="warning"
|
||
@click="openAction('platform_checkout_dispute')"
|
||
>
|
||
{{ platformCheckoutDisputeAction?.label || '发起结账争议' }}
|
||
</el-button>
|
||
<el-button v-if="canOfflineSettlement" type="success" @click="openOfflineSettlement">
|
||
{{ offlineSettlementAction?.label || '确认线下结算' }}
|
||
</el-button>
|
||
<el-button v-if="canAdminCreateDispute" type="warning" plain @click="openAdminDispute">
|
||
发起申诉
|
||
</el-button>
|
||
<el-button type="warning" :disabled="!canOperate" @click="openAction('abnormal')"
|
||
>标记异常</el-button
|
||
>
|
||
<el-tooltip :content="closeActionTip" placement="top">
|
||
<span>
|
||
<el-button type="danger" :disabled="!canOperate" @click="confirmCloseAction"
|
||
>客服关闭</el-button
|
||
>
|
||
</span>
|
||
</el-tooltip>
|
||
<el-tooltip :content="sealActionTip" placement="top">
|
||
<span>
|
||
<el-button type="danger" plain :disabled="!canOperate" @click="confirmSealAction"
|
||
>封存订单</el-button
|
||
>
|
||
</span>
|
||
</el-tooltip>
|
||
<el-tooltip v-if="canHoldDeposit" :content="depositHoldActionTip" placement="top">
|
||
<span>
|
||
<el-button type="warning" plain @click="confirmHoldDeposit"> 暂扣押金 </el-button>
|
||
</span>
|
||
</el-tooltip>
|
||
<el-tooltip v-if="isDepositHeld" :content="depositReleaseActionTip" placement="top">
|
||
<span>
|
||
<el-button
|
||
type="success"
|
||
plain
|
||
:disabled="!canReleaseDeposit"
|
||
@click="confirmReleaseDeposit"
|
||
>
|
||
归还押金
|
||
</el-button>
|
||
</span>
|
||
</el-tooltip>
|
||
<el-tooltip :content="refundActionTip" placement="top">
|
||
<span>
|
||
<el-button
|
||
type="primary"
|
||
:loading="refunding"
|
||
:disabled="refundButtonDisabled"
|
||
@click="confirmRefund"
|
||
>
|
||
{{ refundButtonDisabled ? '已退款' : '人工退款' }}
|
||
</el-button>
|
||
</span>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="order" class="metric-grid dashboard-metrics order-metrics">
|
||
<div class="metric-card">
|
||
<span>订单状态</span>
|
||
<strong>{{ orderStatusLabel(order.status) }}</strong>
|
||
<small>{{ orderHandoffStatusLabel(order) }}</small>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>结算状态</span>
|
||
<strong>{{ settlementStatusLabel(order.settlement_status) }}</strong>
|
||
<small>更新 {{ formatDateTime(order.updated_at) }}</small>
|
||
</div>
|
||
<div v-if="isPlatformManaged" class="metric-card">
|
||
<span>线下结算</span>
|
||
<strong>{{ offlineSettlementStatusLabel(offlineSettlementStatus) }}</strong>
|
||
<small>{{ moneyCent(order.offline_settlement_amount_cent) }}</small>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>订单总额</span>
|
||
<strong>{{ moneyCent(orderTotalCent) }}</strong>
|
||
<small
|
||
>租金 {{ moneyCent(order.rent_amount_cent) }} / 押金
|
||
{{ moneyCent(order.deposit_amount_cent) }}</small
|
||
>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>支付状态</span>
|
||
<strong>{{ latestPayment ? paymentStatusLabel(latestPayment.status) : '暂无流水' }}</strong>
|
||
<small v-if="paidOrderPayment">支付 {{ moneyCent(paidOrderPayment.amount_cent) }}</small>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>退款状态</span>
|
||
<strong>{{ refundStatusLabel(refundStatus?.refund_status || '') }}</strong>
|
||
<small v-if="refundStatus?.refund_amount_cent">
|
||
{{ moneyCent(refundStatus.refund_amount_cent) }}
|
||
</small>
|
||
</div>
|
||
<div class="metric-card">
|
||
<span>预计截止</span>
|
||
<strong>{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</strong>
|
||
<small>{{ order.estimated_duration_hours }} 小时</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="order" class="order-detail-grid">
|
||
<section class="dashboard-panel detail-panel">
|
||
<div class="panel-heading">
|
||
<h2>订单概览</h2>
|
||
<div class="status-tags">
|
||
<el-tag :type="orderStatusType(order.status)" effect="light">
|
||
{{ orderStatusLabel(order.status) }}
|
||
</el-tag>
|
||
<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>
|
||
|
||
<section class="dashboard-panel detail-panel">
|
||
<div class="panel-heading">
|
||
<h2>用户与时间</h2>
|
||
</div>
|
||
<dl class="detail-list">
|
||
<div>
|
||
<dt>租客</dt>
|
||
<dd>{{ userDisplay(order.renter_phone, order.renter_id) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>号主</dt>
|
||
<dd>{{ sellerDisplay(order) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>创建时间</dt>
|
||
<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 v-if="isPlatformManaged" class="dashboard-panel detail-panel">
|
||
<div class="panel-heading">
|
||
<h2>平台代管</h2>
|
||
<el-tag :type="offlineSettlementStatusType(offlineSettlementStatus)" effect="light">
|
||
{{ offlineSettlementStatusLabel(offlineSettlementStatus) }}
|
||
</el-tag>
|
||
</div>
|
||
<dl class="detail-list">
|
||
<div>
|
||
<dt>交接模式</dt>
|
||
<dd>{{ handoffModeLabel(order.handoff_mode) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>结算模式</dt>
|
||
<dd>{{ settlementModeLabel(order.settlement_mode) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>负责客服</dt>
|
||
<dd>{{ order.managed_admin_id ? `ID ${order.managed_admin_id}` : '-' }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>待打款金额</dt>
|
||
<dd>{{ moneyCent(order.offline_settlement_amount_cent) }}</dd>
|
||
</div>
|
||
<div v-if="order.offline_settled_by">
|
||
<dt>确认客服</dt>
|
||
<dd>ID {{ order.offline_settled_by }}</dd>
|
||
</div>
|
||
<div v-if="order.offline_settled_at">
|
||
<dt>确认时间</dt>
|
||
<dd>{{ formatDateTime(order.offline_settled_at) }}</dd>
|
||
</div>
|
||
<div v-if="order.offline_settlement_remark" class="wide">
|
||
<dt>线下备注</dt>
|
||
<dd>{{ order.offline_settlement_remark }}</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 v-for="row in fundSplitRows" :key="row.label" class="money-row">
|
||
<span>{{ row.label }}</span>
|
||
<strong>{{ moneyCent(row.amountCent) }}</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>
|
||
</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>
|
||
<div>
|
||
<dt>押金暂扣</dt>
|
||
<dd>{{ depositHoldStatusLabel || '未暂扣' }}</dd>
|
||
</div>
|
||
<div v-if="depositHoldStatus !== 'none'">
|
||
<dt>已暂扣金额</dt>
|
||
<dd>{{ moneyCent(depositHoldAmountCent) }}</dd>
|
||
</div>
|
||
<div v-if="order.deposit_hold_reason" class="wide">
|
||
<dt>暂扣原因</dt>
|
||
<dd>{{ order.deposit_hold_reason }}</dd>
|
||
</div>
|
||
<div v-if="order.deposit_held_at">
|
||
<dt>暂扣时间</dt>
|
||
<dd>{{ formatDateTime(order.deposit_held_at) }}</dd>
|
||
</div>
|
||
<div v-if="order.deposit_hold_released_at">
|
||
<dt>归还时间</dt>
|
||
<dd>{{ formatDateTime(order.deposit_hold_released_at) }}</dd>
|
||
</div>
|
||
</dl>
|
||
</section>
|
||
|
||
<section v-if="order.checkout" class="dashboard-panel detail-panel">
|
||
<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.pure_coin_amount_cent) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>实际纯币优惠</dt>
|
||
<dd>{{ moneyCent(order.checkout.pure_coin_discount_cent) }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>折后纯币金额</dt>
|
||
<dd>{{ moneyCent(order.checkout.pure_coin_payable_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 v-if="checkoutRemainingHafCoinM >= 0">
|
||
{{ quantity(checkoutRemainingHafCoinM) }}M
|
||
</dd>
|
||
<dd v-else class="danger-text">
|
||
打超 {{ quantity(-checkoutRemainingHafCoinM) }}M(按比例从押金扣)
|
||
</dd>
|
||
</div>
|
||
<div v-if="order.status === 'completed'">
|
||
<dt>积分计算基数</dt>
|
||
<dd>{{ moneyCent(order.growth_points_basis_cent) }}</dd>
|
||
</div>
|
||
<div v-if="order.status === 'completed'">
|
||
<dt>本单成长积分</dt>
|
||
<dd>{{ order.growth_points_awarded || 0 }}</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>{{ disputeInitiatorLabel() }}</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="暂无交接记录" />
|
||
<div v-else class="record-list">
|
||
<article v-for="record in handoffRecords" :key="record.id" class="record-item">
|
||
<div class="record-main">
|
||
<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>
|
||
</section>
|
||
|
||
<section class="dashboard-panel detail-panel order-wide-panel">
|
||
<div class="panel-heading">
|
||
<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="snapshotRatio && (snapshotRatio.recycleRatio > 0 || snapshotRatio.sellRatio > 0)"
|
||
class="ratio-row"
|
||
>
|
||
<div v-if="snapshotRatio.recycleRatio > 0" class="ratio-item">
|
||
<span class="ratio-label">回收比例</span>
|
||
<strong class="ratio-value"
|
||
>1:{{ formatRatioNumber(snapshotRatio.recycleRatio) }}</strong
|
||
>
|
||
</div>
|
||
<div v-if="snapshotRatio.sellRatio > 0" class="ratio-item">
|
||
<span class="ratio-label">售卖比例</span>
|
||
<strong class="ratio-value">1:{{ formatRatioNumber(snapshotRatio.sellRatio) }}</strong>
|
||
</div>
|
||
</div>
|
||
<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>
|
||
|
||
<el-dialog
|
||
:model-value="!!actionType"
|
||
:title="actionTitle"
|
||
width="560px"
|
||
@update:model-value="actionType = ''"
|
||
>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||
</p>
|
||
<el-input
|
||
v-model="reason"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写客服操作原因,会写入审计日志并通知双方"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="actionType = ''">取消</el-button>
|
||
<el-button :type="actionConfirmButtonType" :loading="submitting" @click="submitAction">
|
||
确认操作
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="platformHandoffVisible" title="客服代交接" width="560px" destroy-on-close>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||
</p>
|
||
<el-input
|
||
v-model="platformHandoffContent"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="填写发给租客的交接说明"
|
||
/>
|
||
<el-input
|
||
v-model="platformHandoffReason"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="填写客服操作原因"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="platformHandoffVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="platformSubmitting" @click="submitPlatformHandoff">
|
||
提交交接
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="forceHandoffVisible" title="客服一键交接" width="560px" destroy-on-close>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||
</p>
|
||
<el-alert
|
||
title="确认后将跳过号主交接与租客确认步骤,订单会立即进入使用中并开始计算租期。"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
<el-input
|
||
v-if="forceHandoffNeedsContent"
|
||
v-model="forceHandoffContent"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="填写可供租客查看的交接说明"
|
||
/>
|
||
<el-input
|
||
v-model="forceHandoffReason"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="填写客服确认双方已完成交接的原因"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="forceHandoffVisible = false">取消</el-button>
|
||
<el-button type="warning" :loading="platformSubmitting" @click="submitForceHandoff">
|
||
确认一键交接
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="adminDisputeVisible" title="发起订单申诉" width="620px" destroy-on-close>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||
</p>
|
||
<div class="form-grid">
|
||
<label>
|
||
<span>申诉类型</span>
|
||
<el-select v-model="adminDisputeForm.type" placeholder="选择申诉类型">
|
||
<el-option
|
||
v-for="item in disputeTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</label>
|
||
<label>
|
||
<span>被申诉方</span>
|
||
<el-select v-model="adminDisputeForm.targetRole" placeholder="选择被申诉方">
|
||
<el-option label="租客" value="renter" />
|
||
<el-option label="号主" value="owner" />
|
||
</el-select>
|
||
</label>
|
||
</div>
|
||
<el-input
|
||
v-model="adminDisputeForm.description"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写申诉说明,会通知订单双方并进入仲裁中心"
|
||
/>
|
||
<el-input
|
||
v-model="adminDisputeForm.evidenceText"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="证据链接,一行一个,可留空"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="adminDisputeVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="adminDisputeSubmitting" @click="submitAdminDispute">
|
||
发起申诉
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="platformCheckoutCounterVisible"
|
||
title="客服修改结账方案"
|
||
width="620px"
|
||
destroy-on-close
|
||
>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
<strong>{{ order.order_no }}</strong> · 当前号主收入
|
||
{{ moneyCent(order.checkout?.owner_income_amount_cent) }}
|
||
</p>
|
||
<div class="form-grid">
|
||
<label>
|
||
<span>消耗品金额(元)</span>
|
||
<el-input-number
|
||
v-model="platformCheckoutCounterForm.consumableAmountYuan"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="1"
|
||
controls-position="right"
|
||
/>
|
||
</label>
|
||
<label>
|
||
<span>哈夫币消耗(M)</span>
|
||
<el-input-number
|
||
v-model="platformCheckoutCounterForm.coin_consumed_m"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="1"
|
||
controls-position="right"
|
||
/>
|
||
</label>
|
||
<label>
|
||
<span>押金赔付扣除(元)</span>
|
||
<el-input-number
|
||
v-model="platformCheckoutCounterForm.depositDeductAmountYuan"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="1"
|
||
controls-position="right"
|
||
/>
|
||
</label>
|
||
</div>
|
||
<el-input
|
||
v-model="platformCheckoutCounterForm.reason"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写修改原因,将同步给租客并写入审计日志"
|
||
/>
|
||
<el-input
|
||
v-model="platformCheckoutCounterForm.evidenceText"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="证据链接,一行一个,可留空"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="platformCheckoutCounterVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="platformSubmitting"
|
||
@click="submitPlatformCheckoutCounter"
|
||
>
|
||
提交修改
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="offlineSettlementVisible"
|
||
title="确认线下结算"
|
||
width="520px"
|
||
destroy-on-close
|
||
>
|
||
<div v-if="order" class="dialog-body">
|
||
<p>
|
||
待打款金额
|
||
<strong>{{ moneyCent(order.offline_settlement_amount_cent) }}</strong>
|
||
</p>
|
||
<el-input
|
||
v-model="offlineSettlementRemark"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写线下转账备注,可留空"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="offlineSettlementVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="platformSubmitting" @click="submitOfflineSettlement">
|
||
确认已结算
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</section>
|
||
</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;
|
||
}
|
||
|
||
.platform-handoff-button {
|
||
min-width: 96px;
|
||
border-color: #2563eb;
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
font-weight: 700;
|
||
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.18);
|
||
}
|
||
|
||
.platform-handoff-button:hover,
|
||
.platform-handoff-button:focus {
|
||
border-color: #1d4ed8;
|
||
background: #1d4ed8;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.platform-handoff-button:active {
|
||
border-color: #1e40af;
|
||
background: #1e40af;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.ratio-row {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 14px;
|
||
padding: 12px 14px;
|
||
border: 1px solid #eef2f7;
|
||
border-radius: 8px;
|
||
background: #fbfcff;
|
||
}
|
||
|
||
.ratio-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.ratio-label {
|
||
font-size: 12px;
|
||
color: #8f9bba;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ratio-value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #1b2559;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.dialog-body {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.dialog-body p {
|
||
margin: 0;
|
||
color: #52616f;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-grid label {
|
||
display: grid;
|
||
gap: 6px;
|
||
color: #52616f;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.form-grid :deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
|
||
@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>
|