734 lines
23 KiB
TypeScript
734 lines
23 KiB
TypeScript
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
|
||
import { fetchOrderChat } from '@/features/chats/api/chats'
|
||
import { cancelOrderDispute, createDispute } from '@/features/disputes/api/disputes'
|
||
import { uploadFile } from '@/shared/api/files'
|
||
import { readError } from '@/shared/utils/error'
|
||
import {
|
||
acceptCheckout,
|
||
cancelOrder,
|
||
confirmCheckout,
|
||
confirmReceive,
|
||
counterCheckout,
|
||
fetchHandoffRecords,
|
||
fetchOrder,
|
||
startOrderPayment,
|
||
submitCheckout,
|
||
submitHandoff,
|
||
type HandoffRecord,
|
||
type Order,
|
||
type PaymentOrder,
|
||
type PaymentPayWay,
|
||
} from '@/features/orders/api/orders'
|
||
import {
|
||
hydrateCounterFormFromCheckout,
|
||
linesToList,
|
||
orderEstimatedEndAt as readOrderEstimatedEndAt,
|
||
orderRentAmount,
|
||
ownerActualIncome,
|
||
useOrderCheckoutSnapshot,
|
||
} from '@/features/orders/composables/useOrderSnapshot'
|
||
import { useSessionStore } from '@/stores/session'
|
||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||
|
||
/**
|
||
* 订单详情交互 Composable
|
||
*
|
||
* 承载订单详情双视图(PC / Mobile)共享的业务逻辑:状态、计算属性、handler。
|
||
* UI 反馈(ElMessage / showToast)与二次确认(ElMessageBox / showDialog)通过 options 注入,
|
||
* 文案与业务流程(API 调用顺序、参数构造、表单 reset、autoPay 触发)归本 composable。
|
||
*
|
||
* 设计参考:{@link ./usePublishForm.ts}(PC / Mobile 共用 composable 范式)。
|
||
*/
|
||
export interface UseOrderActionsOptions {
|
||
/** 成功提示,注入端决定 UI 形态(ElMessage.success / showToast)。 */
|
||
notifySuccess: (message: string) => void
|
||
/** 错误提示,注入端决定 UI 形态。 */
|
||
notifyError: (message: string) => void
|
||
/** 警告提示,用于表单校验未通过等非致命场景。 */
|
||
notifyWarning: (message: string) => void
|
||
/** 二次确认。返回 true 表示用户确认继续,false 表示取消。 */
|
||
confirm: (opts: { title: string; message: string }) => Promise<boolean>
|
||
/** 需要走二次确认的 handler 名集合。 */
|
||
actionsNeedingConfirm: readonly string[]
|
||
/** 支付前选择支付方式。返回 null 表示用户取消支付。 */
|
||
selectPayWay: () => Promise<PaymentPayWay | null>
|
||
/** 订单列表跳转路径,PC '/orders' / Mobile '/m/orders'。 */
|
||
ordersPath: string
|
||
/** 订单群聊路由前缀,PC '/messages/' / Mobile '/m/chats/'。openOrderChat 内部拼接 `${prefix}${chat.id}`。 */
|
||
chatPathPrefix: string
|
||
/**
|
||
* 打开收银台 UI(二维码弹窗 / App 浏览器跳转)。由调用方注入。
|
||
* 仅在 payment 未支付时被调用;payment 已支付的情况由 composable 自行提示并 loadOrder。
|
||
* onPaid:支付完成(轮询发现 paid)时的回调,通常跳转到群消息。
|
||
*/
|
||
startCashier: (payment: PaymentOrder, onPaid: () => Promise<void>) => void | Promise<void>
|
||
/** 修改结账提交成功后触发(如关闭弹窗)。可选。 */
|
||
onCounterCheckoutSuccess?: () => void
|
||
/** 拒绝修正提交成功后触发(如关闭弹窗)。可选。 */
|
||
onRejectCheckoutSuccess?: () => void
|
||
/** 提交申诉成功后触发(如关闭弹窗)。可选。 */
|
||
onCreateDisputeSuccess?: () => void
|
||
}
|
||
|
||
/** handler 名常量,供 actionsNeedingConfirm 校验。 */
|
||
export const CONFIRMABLE_ACTIONS = [
|
||
'cancel',
|
||
'confirmCheckout',
|
||
'acceptCheckout',
|
||
'cancelDispute',
|
||
'submitCheckout',
|
||
] as const
|
||
|
||
export interface CheckoutFormState {
|
||
content: string
|
||
consumableAmountYuan: number
|
||
coin_consumed_m: number
|
||
otherAmountYuan: number
|
||
evidenceText: string
|
||
}
|
||
|
||
export interface CounterFormState {
|
||
consumableAmountYuan: number
|
||
coin_consumed_m: number
|
||
depositDeductAmountYuan: number
|
||
reason: string
|
||
evidenceText: string
|
||
}
|
||
|
||
function createCheckoutForm(): CheckoutFormState {
|
||
return {
|
||
content: '',
|
||
consumableAmountYuan: 0,
|
||
coin_consumed_m: 0,
|
||
otherAmountYuan: 0,
|
||
evidenceText: '',
|
||
}
|
||
}
|
||
|
||
function createCounterForm(): CounterFormState {
|
||
return {
|
||
consumableAmountYuan: 0,
|
||
coin_consumed_m: 0,
|
||
depositDeductAmountYuan: 0,
|
||
reason: '',
|
||
evidenceText: '',
|
||
}
|
||
}
|
||
|
||
export function useOrderActions(options: UseOrderActionsOptions) {
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const session = useSessionStore()
|
||
|
||
// Loading states
|
||
const loading = ref(false)
|
||
const cancelling = ref(false)
|
||
const startingPayment = ref(false)
|
||
const handoffing = ref(false)
|
||
const confirming = ref(false)
|
||
const returning = ref(false)
|
||
const completing = ref(false)
|
||
const countering = ref(false)
|
||
const acceptingCheckout = ref(false)
|
||
const rejectingCheckout = ref(false)
|
||
const disputing = ref(false)
|
||
const cancellingDispute = ref(false)
|
||
const uploadingEvidence = ref(false)
|
||
const openingChat = ref(false)
|
||
|
||
// Data
|
||
const order = ref<Order | null>(null)
|
||
const handoffRecords = ref<HandoffRecord[]>([])
|
||
const handoffContent = ref('')
|
||
|
||
// Forms
|
||
const checkoutForm = ref<CheckoutFormState>(createCheckoutForm())
|
||
const resourceUsage = ref<Record<string, number>>({})
|
||
const counterForm = ref<CounterFormState>(createCounterForm())
|
||
const rejectReason = ref('')
|
||
const disputeType = ref('cannot_login')
|
||
const disputeDescription = ref('')
|
||
const disputeEvidenceText = ref('')
|
||
let checkoutDefaultsHydratedOrderID: number | null = null
|
||
|
||
// 结账快照能力(资源金额、内容摘要、资源使用量规范化)
|
||
const {
|
||
checkoutResources,
|
||
resourceChargeAmount,
|
||
snapshotHafCoinM,
|
||
remainingHafCoinM,
|
||
hydrateResourceUsage,
|
||
resourceLineAmount,
|
||
checkoutContentWithSummary,
|
||
} = useOrderCheckoutSnapshot({ order, checkoutForm, resourceUsage })
|
||
|
||
// Computed
|
||
const isOwner = computed(() => order.value?.owner_id === session.userId)
|
||
const isRenter = computed(() => order.value?.renter_id === session.userId)
|
||
const listingCode = computed(() =>
|
||
order.value ? formatListingNo(order.value.listing_no, order.value.listing_id) : '-'
|
||
)
|
||
const orderAmountLabel = computed(() => (isOwner.value ? '预计租金' : '支付租金'))
|
||
const orderRentDisplayAmount = computed(() =>
|
||
order.value ? orderRentAmount(order.value, session.userId) : 0
|
||
)
|
||
const renterDiscountDisplayCent = computed(() => {
|
||
if (!order.value) return 0
|
||
if (order.value.status === 'completed') {
|
||
return Number(order.value.actual_pure_coin_discount_cent || 0)
|
||
}
|
||
return Number(order.value.rent_discount_amount_cent || 0)
|
||
})
|
||
const ownerIncomeDisplayAmount = computed(() =>
|
||
order.value ? ownerActualIncome(order.value, session.userId) : null
|
||
)
|
||
const ownerIncomeLabel = computed(() =>
|
||
order.value?.status === 'completed' ? '实际到手' : '结账预计到手'
|
||
)
|
||
const canOpenDispute = computed(() => {
|
||
if (!order.value || (!isOwner.value && !isRenter.value)) return false
|
||
return ![
|
||
'completed',
|
||
'cancelled',
|
||
'closed',
|
||
'disputing',
|
||
'checkout_disputing',
|
||
'abnormal',
|
||
].includes(order.value.status)
|
||
})
|
||
const canCounterCheckout = computed(() => {
|
||
if (!order.value?.checkout) return false
|
||
return Boolean(order.value.checkout.can_counter)
|
||
})
|
||
const canAcceptCheckoutProposal = computed(() => {
|
||
if (!order.value?.checkout) return false
|
||
// 后端 can_accept 已含 shortfall 校验;无字段时按角色状态兜底
|
||
if (typeof order.value.checkout.can_accept === 'boolean') {
|
||
return order.value.checkout.can_accept
|
||
}
|
||
return (
|
||
(isOwner.value && order.value.status === 'pending_checkout_confirm') ||
|
||
(isRenter.value && order.value.status === 'pending_checkout_accept')
|
||
)
|
||
})
|
||
const checkoutShortfallCent = computed(() => Number(order.value?.checkout?.shortfall_cent || 0))
|
||
const checkoutOvershootCent = computed(() =>
|
||
Number(order.value?.checkout?.overshoot_amount_cent || 0)
|
||
)
|
||
const checkoutRoundLabel = computed(() => {
|
||
const c = order.value?.checkout
|
||
if (!c) return ''
|
||
const round = Number(c.round_count || 1)
|
||
const max = Number(c.max_rounds || 6)
|
||
return `第 ${round}/${max} 轮协商`
|
||
})
|
||
const canCancelDispute = computed(() => {
|
||
return (
|
||
!!order.value &&
|
||
(isOwner.value || isRenter.value) &&
|
||
['disputing', 'checkout_disputing'].includes(order.value.status) &&
|
||
order.value.active_dispute?.initiator_type !== 'admin' &&
|
||
order.value.active_dispute?.initiator_id === session.userId
|
||
)
|
||
})
|
||
const isCheckoutDisputeStage = computed(() => {
|
||
return (
|
||
!!order.value &&
|
||
['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value.status)
|
||
)
|
||
})
|
||
|
||
let autoPayHandled = false
|
||
|
||
function needsConfirm(action: string): boolean {
|
||
return options.actionsNeedingConfirm.includes(action)
|
||
}
|
||
|
||
function hydrateCounterForm() {
|
||
if (!order.value?.checkout) return
|
||
counterForm.value = hydrateCounterFormFromCheckout(order.value.checkout)
|
||
}
|
||
|
||
function hydrateCheckoutDefaults() {
|
||
if (!order.value || !isRenter.value || !['renting', 'overdue'].includes(order.value.status)) {
|
||
return
|
||
}
|
||
if (checkoutDefaultsHydratedOrderID === order.value.id) return
|
||
const defaultCoinConsumedM = snapshotHafCoinM.value
|
||
if (defaultCoinConsumedM > 0 && Number(checkoutForm.value.coin_consumed_m || 0) <= 0) {
|
||
checkoutForm.value = {
|
||
...checkoutForm.value,
|
||
coin_consumed_m: defaultCoinConsumedM,
|
||
}
|
||
}
|
||
checkoutDefaultsHydratedOrderID = order.value.id
|
||
}
|
||
|
||
async function loadOrder() {
|
||
loading.value = true
|
||
try {
|
||
order.value = await fetchOrder(String(route.params.id))
|
||
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
|
||
hydrateResourceUsage()
|
||
hydrateCheckoutDefaults()
|
||
hydrateCounterForm()
|
||
if (
|
||
!autoPayHandled &&
|
||
route.query.pay === '1' &&
|
||
order.value?.status === 'pending_payment' &&
|
||
order.value?.renter_id === session.userId
|
||
) {
|
||
autoPayHandled = true
|
||
void router.replace({ path: route.path })
|
||
void handlePay()
|
||
}
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '加载订单失败'))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleCancel() {
|
||
if (!order.value) return
|
||
const needsRefundReview = order.value.status === 'pending_handoff'
|
||
if (needsConfirm('cancel')) {
|
||
const ok = await options.confirm({
|
||
title: '取消订单',
|
||
message: needsRefundReview
|
||
? '确定申请取消订单吗?账号将在客服审核通过后释放,退款将原路退回。'
|
||
: '确定取消订单并释放账号吗?',
|
||
})
|
||
if (!ok) return
|
||
}
|
||
cancelling.value = true
|
||
try {
|
||
await cancelOrder(order.value.id)
|
||
options.notifySuccess(
|
||
needsRefundReview ? '退款申请已提交,等待客服审核' : '订单已取消,账号已释放'
|
||
)
|
||
await router.push(options.ordersPath)
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '取消失败'))
|
||
} finally {
|
||
cancelling.value = false
|
||
}
|
||
}
|
||
|
||
async function handlePay() {
|
||
if (!order.value) return
|
||
const payWay = await options.selectPayWay()
|
||
if (!payWay) return
|
||
startingPayment.value = true
|
||
try {
|
||
const payment = await startOrderPayment(order.value.id, { pay_way: payWay })
|
||
if (payment.paid) {
|
||
options.notifySuccess('支付成功,正在进入群消息')
|
||
await openOrderChatAfterPayment()
|
||
return
|
||
}
|
||
await options.startCashier(payment, openOrderChatAfterPayment)
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '支付失败'))
|
||
} finally {
|
||
startingPayment.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSubmitHandoff() {
|
||
if (!order.value) return
|
||
if (!handoffContent.value.trim()) {
|
||
options.notifyWarning('请填写交接说明')
|
||
return
|
||
}
|
||
handoffing.value = true
|
||
try {
|
||
await submitHandoff(order.value.id, handoffContent.value)
|
||
handoffContent.value = ''
|
||
options.notifySuccess('交接说明已提交')
|
||
await loadOrder()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '提交交接失败'))
|
||
} finally {
|
||
handoffing.value = false
|
||
}
|
||
}
|
||
|
||
async function handleConfirmReceive() {
|
||
if (!order.value) return
|
||
confirming.value = true
|
||
try {
|
||
await confirmReceive(order.value.id)
|
||
options.notifySuccess('已确认收号,订单进入使用中')
|
||
await loadOrder()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '确认收号失败'))
|
||
} finally {
|
||
confirming.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSubmitCheckout() {
|
||
if (!order.value) return
|
||
const depositDeductAmountYuan = Number(checkoutForm.value.otherAmountYuan || 0)
|
||
if (needsConfirm('submitCheckout')) {
|
||
const depositWarning =
|
||
depositDeductAmountYuan > 0
|
||
? `\n\n注意:押金赔付扣除 ¥${depositDeductAmountYuan.toFixed(2)} 将直接从预收押金中扣除,并计入号主赔付。`
|
||
: ''
|
||
const ok = await options.confirm({
|
||
title: '确认提交结账',
|
||
message:
|
||
'结账数据会影响租金、押金赔付和双方结算金额。请确认哈夫币消耗、额外消耗品、押金赔付和证据链接已认真核对,提交后将发送给号主确认。' +
|
||
depositWarning,
|
||
})
|
||
if (!ok) return
|
||
}
|
||
returning.value = true
|
||
try {
|
||
const consumableAmount = resourceChargeAmount.value
|
||
checkoutForm.value.consumableAmountYuan = consumableAmount
|
||
await submitCheckout(order.value.id, {
|
||
content: checkoutContentWithSummary(),
|
||
consumableAmountYuan: consumableAmount,
|
||
coin_consumed_m: checkoutForm.value.coin_consumed_m,
|
||
otherAmountYuan: checkoutForm.value.otherAmountYuan,
|
||
evidence_urls: linesToList(checkoutForm.value.evidenceText),
|
||
})
|
||
checkoutForm.value = createCheckoutForm()
|
||
resourceUsage.value = {}
|
||
options.notifySuccess('结账已发起,等待号主确认')
|
||
await loadOrder()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '发起结账失败'))
|
||
} finally {
|
||
returning.value = false
|
||
}
|
||
}
|
||
|
||
async function handleConfirmCheckout() {
|
||
if (!order.value) return
|
||
if (checkoutShortfallCent.value > 0) {
|
||
options.notifyWarning('押金不足以覆盖打超/赔付,无法自动完结,请修改方案或发起争议由人工处理')
|
||
return
|
||
}
|
||
if (needsConfirm('confirmCheckout')) {
|
||
const ok = await options.confirm({
|
||
title: '确认结账',
|
||
message: '确认账号状态和扣款金额无误吗?确认后订单将完成。',
|
||
})
|
||
if (!ok) return
|
||
}
|
||
completing.value = true
|
||
try {
|
||
await confirmCheckout(order.value.id)
|
||
options.notifySuccess('结账已确认,订单完成')
|
||
await loadOrder()
|
||
if (isRenter.value) {
|
||
await session.loadMe().catch(() => undefined)
|
||
}
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '确认结账失败'))
|
||
} finally {
|
||
completing.value = false
|
||
}
|
||
}
|
||
|
||
async function handleCounterCheckout() {
|
||
if (!order.value) return
|
||
const reasonText = counterForm.value.reason.trim()
|
||
if (!reasonText) {
|
||
options.notifyWarning('请填写修改原因')
|
||
return
|
||
}
|
||
if (!canCounterCheckout.value) {
|
||
options.notifyWarning('当前不能修改,可能已达 6 轮上限或未轮到你')
|
||
return
|
||
}
|
||
countering.value = true
|
||
try {
|
||
await counterCheckout(order.value.id, {
|
||
content: reasonText,
|
||
consumableAmountYuan: counterForm.value.consumableAmountYuan,
|
||
coin_consumed_m: counterForm.value.coin_consumed_m,
|
||
otherAmountYuan: counterForm.value.depositDeductAmountYuan,
|
||
depositDeductAmountYuan: counterForm.value.depositDeductAmountYuan,
|
||
reason: reasonText,
|
||
evidence_urls: linesToList(counterForm.value.evidenceText),
|
||
})
|
||
options.notifySuccess(
|
||
isOwner.value ? '结账修正已提交,等待租客处理' : '结账还价已提交,等待号主处理'
|
||
)
|
||
await loadOrder()
|
||
options.onCounterCheckoutSuccess?.()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '修改结账失败'))
|
||
} finally {
|
||
countering.value = false
|
||
}
|
||
}
|
||
|
||
async function handleAcceptCheckout() {
|
||
if (!order.value) return
|
||
if (checkoutShortfallCent.value > 0) {
|
||
options.notifyWarning('押金不足以覆盖打超/赔付,无法自动完结,请修改方案或发起争议由人工处理')
|
||
return
|
||
}
|
||
if (needsConfirm('acceptCheckout')) {
|
||
const ok = await options.confirm({
|
||
title: '同意结账方案',
|
||
message: '确定同意当前结账方案吗?同意后订单将完成结算。',
|
||
})
|
||
if (!ok) return
|
||
}
|
||
acceptingCheckout.value = true
|
||
try {
|
||
await acceptCheckout(order.value.id)
|
||
options.notifySuccess('已确认结账,订单完成')
|
||
await loadOrder()
|
||
if (isRenter.value) {
|
||
await session.loadMe().catch(() => undefined)
|
||
}
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '确认结账失败'))
|
||
} finally {
|
||
acceptingCheckout.value = false
|
||
}
|
||
}
|
||
|
||
async function handleRejectCheckout() {
|
||
if (!order.value) return
|
||
if (!rejectReason.value.trim()) {
|
||
options.notifyWarning('请填写不同意原因')
|
||
return
|
||
}
|
||
rejectingCheckout.value = true
|
||
try {
|
||
await createDispute(order.value.id, {
|
||
type: 'checkout_dispute',
|
||
description: rejectReason.value,
|
||
evidence_urls: [],
|
||
})
|
||
rejectReason.value = ''
|
||
options.notifySuccess('已拒绝修正并提交争议')
|
||
await loadOrder()
|
||
options.onRejectCheckoutSuccess?.()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '提交拒绝失败'))
|
||
} finally {
|
||
rejectingCheckout.value = false
|
||
}
|
||
}
|
||
|
||
async function handleCreateDispute() {
|
||
if (!order.value) return
|
||
if (!disputeDescription.value.trim()) {
|
||
options.notifyWarning('请填写争议经过说明')
|
||
return
|
||
}
|
||
disputing.value = true
|
||
try {
|
||
const evidence_urls = linesToList(disputeEvidenceText.value)
|
||
await createDispute(order.value.id, {
|
||
type: isCheckoutDisputeStage.value ? 'checkout_dispute' : disputeType.value,
|
||
description: disputeDescription.value,
|
||
evidence_urls,
|
||
})
|
||
disputeDescription.value = ''
|
||
disputeEvidenceText.value = ''
|
||
options.notifySuccess('申诉已提交,订单进入仲裁')
|
||
await loadOrder()
|
||
options.onCreateDisputeSuccess?.()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '提交申诉失败'))
|
||
} finally {
|
||
disputing.value = false
|
||
}
|
||
}
|
||
|
||
async function handleCancelDispute() {
|
||
if (!order.value) return
|
||
if (needsConfirm('cancelDispute')) {
|
||
const ok = await options.confirm({
|
||
title: order.value.status === 'checkout_disputing' ? '取消结账争议' : '取消申诉',
|
||
message: '取消后订单将恢复到发起申诉前的流程,确定继续吗?',
|
||
})
|
||
if (!ok) return
|
||
}
|
||
cancellingDispute.value = true
|
||
try {
|
||
await cancelOrderDispute(order.value.id)
|
||
options.notifySuccess('申诉已取消,订单已恢复')
|
||
await loadOrder()
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '取消申诉失败'))
|
||
} finally {
|
||
cancellingDispute.value = false
|
||
}
|
||
}
|
||
|
||
async function handleEvidenceUpload(event: Event) {
|
||
const input = event.target as HTMLInputElement
|
||
const file = input.files?.[0]
|
||
input.value = ''
|
||
if (!file) return
|
||
uploadingEvidence.value = true
|
||
try {
|
||
const uploaded = await uploadFile(file, 'dispute')
|
||
disputeEvidenceText.value = [disputeEvidenceText.value, uploaded.url]
|
||
.filter(Boolean)
|
||
.join('\n')
|
||
options.notifySuccess('证据文件已上传')
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '上传失败'))
|
||
} finally {
|
||
uploadingEvidence.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 反驳结账证据上传(Mobile 反驳弹窗独有:把 URL 写入 counterForm.evidenceText)。
|
||
* PC 不使用,保留以供 Mobile 视图调用。
|
||
*/
|
||
async function handleCounterEvidenceUpload(event: Event) {
|
||
const input = event.target as HTMLInputElement
|
||
const file = input.files?.[0]
|
||
input.value = ''
|
||
if (!file) return
|
||
uploadingEvidence.value = true
|
||
try {
|
||
const uploaded = await uploadFile(file, 'dispute')
|
||
counterForm.value.evidenceText = [counterForm.value.evidenceText, uploaded.url]
|
||
.filter(Boolean)
|
||
.join('\n')
|
||
options.notifySuccess('证据文件已上传')
|
||
} catch (error) {
|
||
options.notifyError(readError(error, '上传失败'))
|
||
} finally {
|
||
uploadingEvidence.value = false
|
||
}
|
||
}
|
||
|
||
async function openOrderChat() {
|
||
if (!order.value || openingChat.value) return
|
||
await navigateToOrderChat(order.value.id, '订单群聊暂不可用')
|
||
}
|
||
|
||
async function openOrderChatAfterPayment() {
|
||
if (!order.value) return
|
||
const opened = await navigateToOrderChat(
|
||
order.value.id,
|
||
'支付成功,但群消息暂不可用,请稍后从消息进入'
|
||
)
|
||
if (!opened) {
|
||
await loadOrder()
|
||
}
|
||
}
|
||
|
||
async function navigateToOrderChat(orderID: number, errorMessage: string) {
|
||
if (openingChat.value) return false
|
||
openingChat.value = true
|
||
try {
|
||
const chat = await fetchOrderChat(orderID)
|
||
await router.push(`${options.chatPathPrefix}${chat.id}`)
|
||
return true
|
||
} catch {
|
||
options.notifyError(errorMessage)
|
||
return false
|
||
} finally {
|
||
openingChat.value = false
|
||
}
|
||
}
|
||
|
||
function orderEstimatedEndAt() {
|
||
return readOrderEstimatedEndAt(order.value)
|
||
}
|
||
|
||
function orderRentedAt() {
|
||
return order.value?.rented_at
|
||
}
|
||
|
||
onMounted(loadOrder)
|
||
|
||
return {
|
||
// Loading states
|
||
loading,
|
||
cancelling,
|
||
startingPayment,
|
||
handoffing,
|
||
confirming,
|
||
returning,
|
||
completing,
|
||
countering,
|
||
acceptingCheckout,
|
||
rejectingCheckout,
|
||
disputing,
|
||
cancellingDispute,
|
||
uploadingEvidence,
|
||
openingChat,
|
||
|
||
// Data
|
||
order,
|
||
handoffRecords,
|
||
handoffContent,
|
||
|
||
// Forms
|
||
checkoutForm,
|
||
resourceUsage,
|
||
counterForm,
|
||
rejectReason,
|
||
disputeType,
|
||
disputeDescription,
|
||
disputeEvidenceText,
|
||
|
||
// Checkout snapshot
|
||
checkoutResources,
|
||
resourceChargeAmount,
|
||
snapshotHafCoinM,
|
||
remainingHafCoinM,
|
||
hydrateResourceUsage,
|
||
resourceLineAmount,
|
||
checkoutContentWithSummary,
|
||
|
||
// Computed
|
||
isOwner,
|
||
isRenter,
|
||
listingCode,
|
||
orderAmountLabel,
|
||
orderRentDisplayAmount,
|
||
renterDiscountDisplayCent,
|
||
ownerIncomeDisplayAmount,
|
||
ownerIncomeLabel,
|
||
canOpenDispute,
|
||
canCancelDispute,
|
||
canCounterCheckout,
|
||
canAcceptCheckoutProposal,
|
||
checkoutShortfallCent,
|
||
checkoutOvershootCent,
|
||
checkoutRoundLabel,
|
||
isCheckoutDisputeStage,
|
||
|
||
// Methods
|
||
loadOrder,
|
||
handleCancel,
|
||
handlePay,
|
||
handleSubmitHandoff,
|
||
handleConfirmReceive,
|
||
handleSubmitCheckout,
|
||
handleConfirmCheckout,
|
||
handleCounterCheckout,
|
||
handleAcceptCheckout,
|
||
handleRejectCheckout,
|
||
handleCreateDispute,
|
||
handleCancelDispute,
|
||
handleEvidenceUpload,
|
||
handleCounterEvidenceUpload,
|
||
openOrderChat,
|
||
orderEstimatedEndAt,
|
||
orderRentedAt,
|
||
hydrateCounterForm,
|
||
}
|
||
}
|