Remove redundant basic summary, fix empty a-alert content, refine track timeline with status and bilingual copy, and stretch drawers across scrollbar-gutter. Co-authored-by: Cursor <cursoragent@cursor.com>
3487 lines
105 KiB
Vue
3487 lines
105 KiB
Vue
<template>
|
||
<DrawerPro
|
||
:footer="false"
|
||
padding-bottom="16px"
|
||
@register="register"
|
||
@cancel="handleCancel"
|
||
>
|
||
<div class="order-detail-drawer order-detail-drawer-shell">
|
||
<div class="drawer-tabs">
|
||
<a-tabs
|
||
v-model:active-key="activeTab"
|
||
type="line"
|
||
size="medium"
|
||
@change="handleTabChange"
|
||
>
|
||
<a-tab-pane
|
||
v-for="tab in tabList"
|
||
:key="tab.key"
|
||
:title="tab.label"
|
||
/>
|
||
</a-tabs>
|
||
</div>
|
||
|
||
<div
|
||
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind'"
|
||
class="drawer-tab-toolbar"
|
||
>
|
||
<div class="toolbar-left">
|
||
<template v-if="activeTab === 'basic'">
|
||
<template v-if="basicEditing">
|
||
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
|
||
<a-button class="app-btn-search" :loading="basicSaving" @click="handleBasicEditSave">
|
||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||
</a-button>
|
||
</template>
|
||
<a-button v-else class="app-btn-search" @click="startBasicEdit">
|
||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||
</a-button>
|
||
<a-button class="app-btn-search" @click="handleAskAdvisor">
|
||
{{ $t('nanaAssistant.askAboutWaybill') }}
|
||
</a-button>
|
||
</template>
|
||
<template v-else-if="activeTab === 'weight'">
|
||
<template v-if="weightEditing">
|
||
<a-button class="app-btn-search" @click="cancelWeightEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
|
||
<a-button class="app-btn-search" :loading="weightSaving" @click="handleWeightEditSave">
|
||
{{ $t('order.myOrders.drawer.actions.save') }}
|
||
</a-button>
|
||
</template>
|
||
<a-button v-else class="app-btn-search" @click="startWeightEdit">
|
||
{{ $t('order.myOrders.drawer.actions.edit') }}
|
||
</a-button>
|
||
</template>
|
||
<template v-else-if="activeTab === 'fee'">
|
||
<a-button
|
||
v-if="canEditAdditionalServices"
|
||
class="app-btn-search"
|
||
:loading="additionalServicesLoading"
|
||
@click="openAdditionalServicesModal"
|
||
>
|
||
{{ $t('order.myOrders.drawer.actions.additionalServices') }}
|
||
</a-button>
|
||
<a-button class="app-btn-search" @click="openAddFeeModal">
|
||
{{ $t('order.myOrders.drawer.actions.addFee') }}
|
||
</a-button>
|
||
<a-button
|
||
class="app-btn-search"
|
||
:loading="recalcReceivableLoading"
|
||
@click="handleRecalculateReceivable"
|
||
>
|
||
{{ $t('order.myOrders.drawer.actions.recalculateReceivable') }}
|
||
</a-button>
|
||
<a-button
|
||
class="app-btn-search"
|
||
:loading="recalcPayableLoading"
|
||
@click="handleRecalculatePayable"
|
||
>
|
||
{{ $t('order.myOrders.drawer.actions.recalculatePayable') }}
|
||
</a-button>
|
||
</template>
|
||
<template v-else-if="activeTab === 'track'">
|
||
<span class="toolbar-meta">
|
||
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
|
||
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
|
||
</span>
|
||
<a-tag
|
||
v-if="currentOrderStatusMeta"
|
||
size="small"
|
||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||
>
|
||
{{ $t(currentOrderStatusMeta.labelKey) }}
|
||
</a-tag>
|
||
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
|
||
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
|
||
</a-tag>
|
||
<a-button
|
||
class="app-btn-search"
|
||
:loading="trackLoading"
|
||
@click="loadTrackTimeline"
|
||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||
</a-button>
|
||
</template>
|
||
<template v-else-if="activeTab === 'stockBind'">
|
||
<a-button
|
||
class="app-btn-search"
|
||
:loading="stockBindLoading"
|
||
@click="reloadStockBind"
|
||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||
</a-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-tab-content">
|
||
<!-- 基本信息 -->
|
||
<div v-if="activeTab === 'basic'" class="tab-panel basic-info">
|
||
<div v-if="predeclareFailGuide" class="predeclare-fail-guide">
|
||
<p class="predeclare-fail-guide__reason">{{ predeclareFailGuide.reason }}</p>
|
||
<p v-if="predeclareFailGuide.tips?.[0]" class="predeclare-fail-guide__tip">
|
||
<span class="predeclare-fail-guide__tip-label">
|
||
{{ $t('order.myOrders.drawer.failGuideTip') }}
|
||
</span>
|
||
{{ predeclareFailGuide.tips[0] }}
|
||
</p>
|
||
<details
|
||
v-if="predeclareFailGuide.rawReason !== predeclareFailGuide.reason"
|
||
class="predeclare-fail-guide__details"
|
||
>
|
||
<summary>{{ $t('order.myOrders.drawer.failGuideRaw') }}</summary>
|
||
<p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p>
|
||
</details>
|
||
<div class="predeclare-fail-guide__actions">
|
||
<a-button class="app-btn-search" @click="startBasicEdit">
|
||
{{ $t('order.myOrders.drawer.failGuideEdit') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="basic-view-switch">
|
||
<a-radio-group
|
||
v-model="basicViewMode"
|
||
type="button"
|
||
size="small"
|
||
>
|
||
<a-radio value="all">{{ $t('order.myOrders.drawer.basic.viewAll') }}</a-radio>
|
||
<a-radio
|
||
v-for="section in basicSectionDefs"
|
||
:key="section.key"
|
||
:value="section.key"
|
||
>
|
||
{{ $t(section.labelKey) }}
|
||
</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<template v-for="section in visibleBasicSections" :key="section.key">
|
||
<div class="detail-section" :class="section.key">
|
||
<div class="detail-section-header">
|
||
<div class="detail-section-title-wrap">
|
||
<span class="detail-section-dot" />
|
||
<span class="detail-section-title">{{ $t(section.labelKey) }}</span>
|
||
<a-tag
|
||
v-if="section.key === 'declaration'"
|
||
size="small"
|
||
color="arcoblue"
|
||
>
|
||
{{ displayDeclarationRows.length }}
|
||
</a-tag>
|
||
</div>
|
||
<a-button
|
||
v-if="section.key === 'declaration' && basicEditing"
|
||
class="app-btn-search"
|
||
@click="addDeclarationRow"
|
||
>
|
||
{{ $t('order.myOrders.drawer.declaration.addRow') }}
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 订单基本信息 -->
|
||
<template v-if="section.key === 'basic'">
|
||
<div
|
||
v-for="group in orderBasicFieldGroups"
|
||
:key="group.key"
|
||
class="detail-field-group"
|
||
>
|
||
<div class="detail-field-group-title">{{ $t(group.labelKey) }}</div>
|
||
<div class="detail-fields-grid">
|
||
<div
|
||
v-for="fieldKey in group.fields"
|
||
:key="fieldKey"
|
||
class="detail-field-item"
|
||
>
|
||
<span class="detail-field-label">{{ $t(getOrderFieldLabelKey(fieldKey)) }}</span>
|
||
<div class="detail-field-value">
|
||
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
||
<a-select
|
||
v-if="getOrderEditMeta(fieldKey)?.inputType === 'select'"
|
||
v-model="basicEditForm[fieldKey]"
|
||
allow-clear
|
||
size="small"
|
||
>
|
||
<a-option
|
||
v-for="opt in getOrderEditMeta(fieldKey)?.options"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ $t(opt.labelKey) }}
|
||
</a-option>
|
||
</a-select>
|
||
<a-switch
|
||
v-else-if="getOrderEditMeta(fieldKey)?.inputType === 'switch'"
|
||
v-model="basicEditForm[fieldKey]"
|
||
size="small"
|
||
/>
|
||
<a-input-number
|
||
v-else-if="getOrderEditMeta(fieldKey)?.inputType === 'number'"
|
||
v-model="basicEditForm[fieldKey]"
|
||
:min="0"
|
||
size="small"
|
||
style="width: 100%"
|
||
/>
|
||
<a-input
|
||
v-else
|
||
v-model="basicEditForm[fieldKey]"
|
||
allow-clear
|
||
size="small"
|
||
/>
|
||
</template>
|
||
<template v-else-if="fieldKey === 'orderStatus' && currentOrderStatusMeta">
|
||
<a-tag
|
||
size="small"
|
||
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
|
||
>
|
||
{{ getOrderValue(fieldKey) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="isOrderBooleanField(fieldKey)">
|
||
<a-tag
|
||
size="small"
|
||
:style="getOrderBooleanTagStyle(fieldKey)"
|
||
>
|
||
{{ getOrderValue(fieldKey) }}
|
||
</a-tag>
|
||
</template>
|
||
<span
|
||
v-else-if="fieldKey === 'channelName'"
|
||
class="detail-field-text detail-field-text--with-action"
|
||
>
|
||
{{ getOrderValue(fieldKey) }}
|
||
<a-button
|
||
v-if="canViewAllocationTrace"
|
||
type="text"
|
||
size="mini"
|
||
class="alloc-eye-btn"
|
||
:title="$t('order.myOrders.allocationTrace.view')"
|
||
@click.stop="openAllocationTrace"
|
||
>
|
||
<template #icon><icon-eye /></template>
|
||
</a-button>
|
||
</span>
|
||
<template v-else-if="fieldKey === 'additionalServices'">
|
||
<a-space v-if="orderAdditionalServices.length" wrap :size="4">
|
||
<a-tag
|
||
v-for="code in orderAdditionalServices"
|
||
:key="code"
|
||
size="small"
|
||
>
|
||
{{ labelAdditionalService(code) }}
|
||
</a-tag>
|
||
</a-space>
|
||
<span v-else class="detail-field-text">--</span>
|
||
</template>
|
||
<span v-else class="detail-field-text">{{ getOrderValue(fieldKey) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 发件人 -->
|
||
<template v-else-if="section.key === 'sender'">
|
||
<a-empty
|
||
v-if="!basicEditing && !hasSender"
|
||
:description="$t('order.myOrders.drawer.sender.empty')"
|
||
:image-style="{ height: '48px' }"
|
||
class="detail-section-empty"
|
||
/>
|
||
<div v-else class="detail-fields-grid">
|
||
<div
|
||
v-for="field in senderFields"
|
||
:key="field.key"
|
||
class="detail-field-item"
|
||
>
|
||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||
<div class="detail-field-value">
|
||
<template v-if="basicEditing && isSenderFieldEditable(field.key)">
|
||
<a-input
|
||
v-model="senderEditForm[getSenderFormKey(field.key)]"
|
||
allow-clear
|
||
size="small"
|
||
/>
|
||
</template>
|
||
<template v-else-if="field.key === 'isDefault'">
|
||
<a-tag
|
||
size="small"
|
||
:style="getBooleanTagStyle(getSenderValue(field.key) === $t('order.myOrders.drawer.yes'))"
|
||
>
|
||
{{ getSenderValue(field.key) }}
|
||
</a-tag>
|
||
</template>
|
||
<span v-else class="detail-field-text">{{ getSenderValue(field.key) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 收件人 -->
|
||
<template v-else-if="section.key === 'recipient'">
|
||
<div class="detail-fields-grid">
|
||
<div
|
||
v-for="field in recipientFields"
|
||
:key="field.key"
|
||
class="detail-field-item"
|
||
>
|
||
<span class="detail-field-label">{{ $t(field.labelKey) }}</span>
|
||
<div class="detail-field-value">
|
||
<template v-if="basicEditing && isRecipientFieldEditable(field.key)">
|
||
<a-input
|
||
v-model="receiverEditForm[getRecipientFormKey(field.key)]"
|
||
allow-clear
|
||
size="small"
|
||
/>
|
||
</template>
|
||
<span v-else class="detail-field-text">{{ getRecipientValue(field.key) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 申报信息 -->
|
||
<template v-else-if="section.key === 'declaration'">
|
||
<div class="declaration-summary detail-fields-grid">
|
||
<div class="detail-field-item">
|
||
<span class="detail-field-label">{{ $t('order.myOrders.drawer.declaredCurrency') }}</span>
|
||
<div class="detail-field-value">
|
||
<span class="detail-field-text">{{ getOrderValue('declaredCurrency') }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="detail-field-item">
|
||
<span class="detail-field-label">{{ $t('order.myOrders.drawer.declaredSum') }}</span>
|
||
<div class="detail-field-value">
|
||
<span class="detail-field-text">{{ getOrderValue('declaredSum') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="displayDeclarationRows.length" class="declaration-table-wrap">
|
||
<div class="declaration-table">
|
||
<div class="table-header">
|
||
<div
|
||
v-for="col in declarationColumns"
|
||
:key="col.key"
|
||
class="header-cell"
|
||
:style="{ minWidth: col.width ? `${col.width}px` : '80px' }"
|
||
>
|
||
{{ $t(col.labelKey) }}
|
||
</div>
|
||
<div v-if="basicEditing" class="header-cell action-cell">
|
||
{{ $t('order.myOrders.drawer.declaration.action') }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-for="(row, idx) in displayDeclarationRows"
|
||
:key="idx"
|
||
class="table-row"
|
||
>
|
||
<div
|
||
v-for="col in declarationColumns"
|
||
:key="col.key"
|
||
class="table-cell"
|
||
:style="{ minWidth: col.width ? `${col.width}px` : '80px' }"
|
||
>
|
||
<template v-if="basicEditing">
|
||
<a-switch
|
||
v-if="col.key === 'isMagnetic'"
|
||
v-model="row.isMagnetic"
|
||
size="small"
|
||
/>
|
||
<a-input-number
|
||
v-else-if="isDeclarationNumberCol(col.key)"
|
||
v-model="row[col.key]"
|
||
:min="0"
|
||
size="mini"
|
||
style="width: 100%"
|
||
/>
|
||
<a-input
|
||
v-else
|
||
v-model="row[col.key]"
|
||
size="mini"
|
||
allow-clear
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
<a-tag
|
||
v-if="col.key === 'isMagnetic'"
|
||
size="small"
|
||
:style="getBooleanTagStyle(!!row[col.key])"
|
||
>
|
||
{{
|
||
row[col.key] == null
|
||
? '--'
|
||
: (row[col.key] ? $t('order.myOrders.drawer.yes') : $t('order.myOrders.drawer.no'))
|
||
}}
|
||
</a-tag>
|
||
<span v-else>{{ row[col.key] ?? '--' }}</span>
|
||
</template>
|
||
</div>
|
||
<div v-if="basicEditing" class="table-cell action-cell">
|
||
<a-button
|
||
class="app-btn-search"
|
||
@click="removeDeclarationRow(idx)"
|
||
>
|
||
{{ $t('order.myOrders.drawer.declaration.delete') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-empty
|
||
v-else
|
||
:description="$t('order.myOrders.drawer.declaration.empty')"
|
||
:image-style="{ height: '48px' }"
|
||
class="detail-section-empty"
|
||
/>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 件重尺 -->
|
||
<div v-if="activeTab === 'weight'" class="tab-panel weight-info">
|
||
<div class="weight-summary">
|
||
<button
|
||
v-for="section in weightTypeDefs"
|
||
:key="section.key"
|
||
type="button"
|
||
class="weight-summary-card"
|
||
:class="[section.key, { active: weightViewMode === section.key }]"
|
||
@click="setWeightViewMode(section.key)"
|
||
>
|
||
<span class="weight-summary-label">{{ $t(section.labelKey) }}</span>
|
||
<span class="weight-summary-value">
|
||
{{ formatWeightChargeable(getWeightRowByType(section.key)) }}
|
||
</span>
|
||
<span class="weight-summary-meta">
|
||
{{ $t('order.myOrders.drawer.weight.actualMeta', {
|
||
value: formatWeightNumber(getWeightRowByType(section.key)?.actualWeight),
|
||
}) }}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="weight-view-switch">
|
||
<a-radio-group
|
||
v-model="weightViewMode"
|
||
type="button"
|
||
size="small"
|
||
>
|
||
<a-radio value="all">{{ $t('order.myOrders.drawer.weight.viewAll') }}</a-radio>
|
||
<a-radio
|
||
v-for="section in weightTypeDefs"
|
||
:key="section.key"
|
||
:value="section.key"
|
||
>
|
||
{{ $t(section.labelKey) }}
|
||
</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<template v-for="section in visibleWeightSections" :key="section.key">
|
||
<div class="weight-section" :class="section.key">
|
||
<div class="weight-section-header">
|
||
<div class="weight-section-title-wrap">
|
||
<span class="weight-section-dot" />
|
||
<span class="weight-section-title">{{ $t(section.labelKey) }}</span>
|
||
</div>
|
||
<span class="weight-section-total">
|
||
{{ formatWeightChargeable(section.row) }}
|
||
</span>
|
||
</div>
|
||
<div
|
||
v-if="section.hasData || (weightEditing && hasWeightRow(section.key))"
|
||
class="weight-metrics-grid"
|
||
>
|
||
<div
|
||
v-for="field in weightMetricFields"
|
||
:key="field.key"
|
||
class="weight-metric-item"
|
||
:class="{ highlight: field.highlight }"
|
||
>
|
||
<span class="metric-label">{{ $t(field.labelKey) }}</span>
|
||
<div class="metric-value-wrap">
|
||
<template v-if="weightEditing && field.editable && hasWeightRow(section.key)">
|
||
<a-input-number
|
||
v-model="section.row[field.key]"
|
||
:min="0"
|
||
size="small"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
<span
|
||
v-else
|
||
class="metric-value"
|
||
:class="{ 'metric-value-highlight': field.highlight }"
|
||
>
|
||
{{ formatWeightCellValue(section.row, field.key) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-empty
|
||
v-else
|
||
:description="$t(section.emptyKey)"
|
||
:image-style="{ height: '48px' }"
|
||
class="weight-section-empty"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<a-empty
|
||
v-if="!weightList.length && !weightEditing"
|
||
:description="$t('order.myOrders.drawer.weight.emptyAll')"
|
||
:image-style="{ height: '60px' }"
|
||
class="weight-empty-all"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 应收应付 -->
|
||
<div v-if="activeTab === 'fee'" class="tab-panel fee-info">
|
||
<div class="fee-summary">
|
||
<button
|
||
type="button"
|
||
class="fee-summary-card receivable"
|
||
:class="{ active: feeViewMode === 'receivable' }"
|
||
@click="setFeeViewMode('receivable')"
|
||
>
|
||
<span class="fee-summary-label">{{ $t('finance.receivablePayable.feeType.receivable') }}</span>
|
||
<span class="fee-summary-amount">{{ formatFeeTotals(receivableTotals) }}</span>
|
||
<span class="fee-summary-meta">
|
||
{{ $t('order.myOrders.drawer.fee.itemCount', { count: receivableFeeList.length }) }}
|
||
</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="fee-summary-card payable"
|
||
:class="{ active: feeViewMode === 'payable' }"
|
||
@click="setFeeViewMode('payable')"
|
||
>
|
||
<span class="fee-summary-label">{{ $t('finance.receivablePayable.feeType.payable') }}</span>
|
||
<span class="fee-summary-amount">{{ formatFeeTotals(payableTotals) }}</span>
|
||
<span class="fee-summary-meta">
|
||
{{ $t('order.myOrders.drawer.fee.itemCount', { count: payableFeeList.length }) }}
|
||
</span>
|
||
</button>
|
||
<div class="fee-summary-card gross-profit">
|
||
<span class="fee-summary-label">{{ $t('order.myOrders.drawer.fee.grossProfit') }}</span>
|
||
<span class="fee-summary-amount">{{ formatGrossProfitTotals(feeGrossProfitTotals) }}</span>
|
||
<span class="fee-summary-meta">{{ grossProfitMarginText }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fee-profit-formula">
|
||
<span class="fee-profit-formula-label">{{ $t('order.myOrders.drawer.fee.grossProfitFormula') }}</span>
|
||
<span class="fee-profit-formula-value">
|
||
<span class="amount-receivable">{{ formatFeeTotals(receivableTotals) }}</span>
|
||
<span class="fee-profit-operator">−</span>
|
||
<span class="amount-payable">{{ formatFeeTotals(payableTotals) }}</span>
|
||
<span class="fee-profit-operator">=</span>
|
||
<span class="fee-profit-result">
|
||
{{ formatGrossProfitTotals(feeGrossProfitTotals) }}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="showForecastDiff" class="fee-forecast-diff">
|
||
<div class="fee-forecast-diff-title">
|
||
{{ $t('order.myOrders.drawer.fee.forecastDiffTitle') }}
|
||
</div>
|
||
<div class="fee-forecast-diff-rows">
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.forecastReceivable') }}
|
||
</span>
|
||
<span>{{ formatPlainAmount(forecastReceivableAmount) }}</span>
|
||
</div>
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.currentReceivable') }}
|
||
</span>
|
||
<span>{{ formatCurrentReceivableText }}</span>
|
||
</div>
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.diff') }}
|
||
</span>
|
||
<span :class="{ 'fee-diff-up': (receivableForecastDiff ?? 0) > 0 }">
|
||
{{ formatSignedAmount(receivableForecastDiff) }}
|
||
</span>
|
||
</div>
|
||
<template v-if="forecastPayableAmount != null">
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.forecastPayable') }}
|
||
</span>
|
||
<span>{{ formatPlainAmount(forecastPayableAmount) }}</span>
|
||
</div>
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.currentPayable') }}
|
||
</span>
|
||
<span>{{ formatCurrentPayableText }}</span>
|
||
</div>
|
||
<div class="fee-forecast-diff-row">
|
||
<span class="fee-forecast-diff-label">
|
||
{{ $t('order.myOrders.drawer.fee.diff') }}
|
||
</span>
|
||
<span :class="{ 'fee-diff-up': (payableForecastDiff ?? 0) > 0 }">
|
||
{{ formatSignedAmount(payableForecastDiff) }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<a-alert
|
||
v-if="hasNegativeGrossProfit"
|
||
type="warning"
|
||
show-icon
|
||
>
|
||
{{ $t('order.myOrders.drawer.fee.negativeGrossProfitWarn') }}
|
||
</a-alert>
|
||
|
||
<div class="fee-view-switch">
|
||
<a-radio-group
|
||
v-model="feeViewMode"
|
||
type="button"
|
||
size="small"
|
||
>
|
||
<a-radio value="all">{{ $t('order.myOrders.drawer.fee.viewAll') }}</a-radio>
|
||
<a-radio value="receivable">{{ $t('finance.receivablePayable.feeType.receivable') }}</a-radio>
|
||
<a-radio value="payable">{{ $t('finance.receivablePayable.feeType.payable') }}</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<template v-for="section in visibleFeeSections" :key="section.key">
|
||
<div class="fee-section" :class="section.key">
|
||
<div class="fee-section-header">
|
||
<div class="fee-section-title-wrap">
|
||
<span class="fee-section-dot" />
|
||
<span class="fee-section-title">{{ $t(section.titleKey) }}</span>
|
||
<a-tag size="small" :color="section.tagColor">{{ section.list.length }}</a-tag>
|
||
</div>
|
||
<span class="fee-section-total">{{ formatFeeTotals(section.totals) }}</span>
|
||
</div>
|
||
<div v-if="section.list.length" class="fee-table-wrap">
|
||
<div class="fee-table">
|
||
<div class="table-header">
|
||
<div
|
||
v-for="col in feeDetailColumns"
|
||
:key="col.key"
|
||
class="header-cell"
|
||
:class="col.key"
|
||
>
|
||
{{ $t(col.labelKey) }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-for="(row, idx) in section.list"
|
||
:key="`${section.key}-${idx}`"
|
||
class="table-row"
|
||
>
|
||
<div
|
||
v-for="col in feeDetailColumns"
|
||
:key="col.key"
|
||
class="table-cell"
|
||
:class="col.key"
|
||
>
|
||
<template v-if="col.key === 'amount'">
|
||
<span class="fee-amount" :class="section.amountClass">
|
||
{{ formatFeeAmount(row.amount) }}
|
||
</span>
|
||
</template>
|
||
<template v-else-if="col.key === 'chargingStage'">
|
||
<a-tag
|
||
v-if="getChargingStage(row)"
|
||
size="small"
|
||
:color="getChargingStageColor(getChargingStage(row))"
|
||
>
|
||
{{ getChargingStageLabel(getChargingStage(row)) }}
|
||
</a-tag>
|
||
<span v-else>--</span>
|
||
</template>
|
||
<template v-else>
|
||
{{ getFeeCellValue(row, col) }}
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-empty
|
||
v-else
|
||
:description="$t(section.emptyKey)"
|
||
:image-style="{ height: '48px' }"
|
||
class="fee-section-empty"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 轨迹 -->
|
||
<div v-if="activeTab === 'track'" class="tab-panel track-info">
|
||
<a-spin :loading="trackLoading" class="track-spin">
|
||
<div v-if="displayTrackTimeline.length" class="track-timeline">
|
||
<div
|
||
v-for="(item, i) in displayTrackTimeline"
|
||
:key="i"
|
||
class="track-item"
|
||
:class="{ active: isTrackNodeActive(item, i) }"
|
||
>
|
||
<div class="track-left">
|
||
<span class="track-dot" :class="{ active: isTrackNodeActive(item, i) }" />
|
||
<span
|
||
v-if="i < displayTrackTimeline.length - 1"
|
||
class="track-line"
|
||
:class="{ active: isTrackNodeActive(item, i) }"
|
||
/>
|
||
</div>
|
||
<div class="track-right">
|
||
<div class="track-row-head">
|
||
<span class="track-status">{{ getTrackStatusLabel(item) }}</span>
|
||
<a-tag v-if="isTrackNodeActive(item, i)" size="small" color="green">
|
||
{{ $t('order.myOrders.drawer.track.latest') }}
|
||
</a-tag>
|
||
</div>
|
||
<div v-if="getTrackZhDetail(item)" class="track-content track-content--zh">
|
||
{{ getTrackZhDetail(item) }}
|
||
</div>
|
||
<div v-if="getTrackEnDetail(item)" class="track-content track-content--en">
|
||
{{ getTrackEnDetail(item) }}
|
||
</div>
|
||
<div class="track-meta-row">
|
||
<span v-if="formatTrackTimeDisplay(item)" class="track-meta-time">
|
||
{{ item.trackTimePrefix || '' }}{{ formatTrackTimeDisplay(item) }}
|
||
</span>
|
||
<span v-if="formatTrackPlace(item)" class="track-meta-place">
|
||
{{ formatTrackPlace(item) }}
|
||
</span>
|
||
<span v-if="item.trackTimeZone" class="track-meta-zone">
|
||
{{ item.trackTimeZone }}
|
||
</span>
|
||
<span v-if="item.trackCode" class="track-meta-code">{{ item.trackCode }}</span>
|
||
</div>
|
||
<div v-if="item.remark" class="track-remark">
|
||
{{ $t('order.myOrders.drawer.track.remark') }}:{{ item.remark }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-empty
|
||
v-else-if="!trackLoading"
|
||
:description="$t('order.myOrders.drawer.track.empty')"
|
||
:image-style="{ height: '60px' }"
|
||
class="track-empty"
|
||
/>
|
||
</a-spin>
|
||
</div>
|
||
|
||
<!-- 附件 -->
|
||
<div v-if="activeTab === 'attachments'" class="tab-panel attachments-info">
|
||
<OrderAttachmentsTab
|
||
:order-id="orderId"
|
||
:active="activeTab === 'attachments'"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 绑定包裹/库存 -->
|
||
<div v-if="activeTab === 'stockBind'" class="tab-panel stock-bind-info">
|
||
<OrderStockBindTab
|
||
ref="stockBindTabRef"
|
||
:order-id="orderId"
|
||
:active="activeTab === 'stockBind'"
|
||
hide-refresh
|
||
/>
|
||
</div>
|
||
|
||
<!-- 问题处理 -->
|
||
<div v-if="activeTab === 'issueHandling'" class="tab-panel issue-handling-info">
|
||
<OrderIssueHandlingTab
|
||
:order-id="orderId"
|
||
:active="activeTab === 'issueHandling'"
|
||
@changed="handleIssueChanged"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</DrawerPro>
|
||
|
||
<AddOrderFeeModal
|
||
v-model:visible="addFeeModalVisible"
|
||
:order-id="orderId"
|
||
:default-type="addFeeDefaultType"
|
||
@success="handleAddFeeSuccess"
|
||
/>
|
||
|
||
<AllocationTraceModal
|
||
v-model:visible="allocationTraceVisible"
|
||
:order-id="orderId"
|
||
/>
|
||
|
||
<a-modal
|
||
v-model:visible="additionalServicesModalVisible"
|
||
:title="$t('order.myOrders.drawer.fee.additionalServicesTitle')"
|
||
:ok-loading="additionalServicesSaving"
|
||
unmount-on-close
|
||
@ok="handleAdditionalServicesSubmit"
|
||
>
|
||
<a-spin :loading="additionalServicesLoading">
|
||
<a-checkbox-group
|
||
v-if="productAdditionalServiceOptions.length"
|
||
v-model="selectedAdditionalServices"
|
||
>
|
||
<a-checkbox
|
||
v-for="opt in productAdditionalServiceOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</a-checkbox>
|
||
</a-checkbox-group>
|
||
<a-empty
|
||
v-else
|
||
:description="$t('order.myOrders.drawer.fee.additionalServicesEmpty')"
|
||
/>
|
||
</a-spin>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, reactive } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { Message } from '@arco-design/web-vue';
|
||
import { IconRefresh, IconRobot, IconEye } from '@arco-design/web-vue/es/icon';
|
||
import useDrawer from '@/components/DrawerPro/useDrawer';
|
||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||
import {
|
||
postOrderUpdate,
|
||
postOrderWeightUpdate,
|
||
postOrderRecalculateReceivable,
|
||
postOrderRecalculatePayable,
|
||
postOrderAdditionalServicesUpdate,
|
||
} from '@/api/order';
|
||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||
import AddOrderFeeModal from './AddOrderFeeModal.vue';
|
||
import AllocationTraceModal from './AllocationTraceModal.vue';
|
||
import OrderStockBindTab from '../../myOrders/components/OrderStockBindTab.vue';
|
||
import OrderAttachmentsTab from '../../components/OrderAttachmentsTab.vue';
|
||
import OrderIssueHandlingTab from '../../components/OrderIssueHandlingTab.vue';
|
||
import {
|
||
ORDER_DETAIL_DECLARATION_COLUMNS,
|
||
ORDER_DETAIL_VALUE_FIELD_MAP,
|
||
buildDeclarationPayloadFromRows,
|
||
cloneDeclarationItemRows,
|
||
formatOrderDetailExtendedValue,
|
||
mergeOrderBasicFields,
|
||
parseAdditionalServiceCodes,
|
||
patchOrderBasicFieldGroups,
|
||
} from '../../components/orderDetailFieldConfig';
|
||
import { openAskAdvisor } from '@/composables/useAskAdvisor';
|
||
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
|
||
|
||
const { t } = useI18n();
|
||
const router = useRouter();
|
||
|
||
interface OrderRow {
|
||
id?: number;
|
||
waybillNo?: string;
|
||
customerOrderNo?: string;
|
||
trackingNo?: string;
|
||
[key: string]: any;
|
||
}
|
||
|
||
interface Props {
|
||
order?: OrderRow | null;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
order: null,
|
||
});
|
||
|
||
const emit = defineEmits<{
|
||
cancel: [];
|
||
updated: [order: OrderRow];
|
||
}>();
|
||
|
||
const detail = ref<OrderRow | null>(null);
|
||
const basicEditing = ref(false);
|
||
const weightEditing = ref(false);
|
||
const basicSaving = ref(false);
|
||
const weightSaving = ref(false);
|
||
const recalcReceivableLoading = ref(false);
|
||
const recalcPayableLoading = ref(false);
|
||
const addFeeModalVisible = ref(false);
|
||
const allocationTraceVisible = ref(false);
|
||
const addFeeDefaultType = ref<'receivable' | 'payable'>('receivable');
|
||
const additionalServicesModalVisible = ref(false);
|
||
const additionalServicesLoading = ref(false);
|
||
const additionalServicesSaving = ref(false);
|
||
const productAdditionalServiceOptions = ref<Array<{ value: string; label: string }>>([]);
|
||
const selectedAdditionalServices = ref<string[]>([]);
|
||
const productHasAdditionalServices = ref(false);
|
||
const additionalServiceDictRows = ref<DictDto[]>([]);
|
||
const exportCustomsDictRows = ref<DictDto[]>([]);
|
||
const stockBindTabRef = ref<{ reload?: () => Promise<void> | void } | null>(null);
|
||
const stockBindLoading = ref(false);
|
||
|
||
async function reloadStockBind() {
|
||
stockBindLoading.value = true;
|
||
try {
|
||
await stockBindTabRef.value?.reload?.();
|
||
} finally {
|
||
stockBindLoading.value = false;
|
||
}
|
||
}
|
||
|
||
type BasicViewMode = 'all' | 'basic' | 'sender' | 'recipient' | 'declaration';
|
||
const basicViewMode = ref<BasicViewMode>('all');
|
||
|
||
const basicSectionDefs = [
|
||
{ key: 'basic' as const, labelKey: 'order.myOrders.drawer.orderBasic' },
|
||
{ key: 'sender' as const, labelKey: 'order.myOrders.drawer.sender' },
|
||
{ key: 'recipient' as const, labelKey: 'order.myOrders.drawer.recipient' },
|
||
{ key: 'declaration' as const, labelKey: 'order.myOrders.drawer.declaration' },
|
||
];
|
||
|
||
const visibleBasicSections = computed(() => {
|
||
if (basicViewMode.value === 'all') return basicSectionDefs;
|
||
return basicSectionDefs.filter((section) => section.key === basicViewMode.value);
|
||
});
|
||
|
||
const currentOrderStatusMeta = computed(() => {
|
||
const code = resolveOrderStatusCode(detail.value);
|
||
return code ? getOrderStatusMeta(code) : null;
|
||
});
|
||
|
||
const predeclareFailGuide = computed(() => {
|
||
const status = resolveOrderStatusCode(detail.value);
|
||
if (status !== 'draft') return null;
|
||
const reason = String(getOrderValue('predeclareFailReason') || '').trim();
|
||
if (!reason) return null;
|
||
return buildFailGuideFromReason(reason);
|
||
});
|
||
|
||
type FieldInputType = 'text' | 'number' | 'select' | 'switch';
|
||
|
||
interface FormFieldDef {
|
||
key: string;
|
||
labelKey: string;
|
||
inputType?: FieldInputType;
|
||
span?: 2;
|
||
options?: Array<{ value: string; labelKey: string }>;
|
||
}
|
||
|
||
const orderEditFieldDefs: FormFieldDef[] = [
|
||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||
{ key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' },
|
||
{
|
||
key: 'cargoAttribute',
|
||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||
inputType: 'select',
|
||
options: [
|
||
{ value: 'general', labelKey: 'order.myOrders.drawer.cargoAttribute.general' },
|
||
{ value: 'electronics', labelKey: 'order.myOrders.drawer.cargoAttribute.electronics' },
|
||
{ value: 'sensitive', labelKey: 'order.myOrders.drawer.cargoAttribute.sensitive' },
|
||
],
|
||
},
|
||
{ key: 'isCod', labelKey: 'order.myOrders.drawer.isCod', inputType: 'switch' },
|
||
{ key: 'codCurrency', labelKey: 'order.myOrders.drawer.codCurrency' },
|
||
{ key: 'codSum', labelKey: 'order.myOrders.drawer.codSum', inputType: 'number' },
|
||
];
|
||
|
||
const orderEditMetaMap = Object.fromEntries(
|
||
orderEditFieldDefs.map((def) => [def.key, def])
|
||
) as Record<string, FormFieldDef>;
|
||
|
||
const SENDER_READONLY_KEYS = new Set(['isDefault']);
|
||
const SENDER_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
|
||
const RECIPIENT_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
|
||
|
||
function isOrderFieldEditable(key: string) {
|
||
return key in orderEditMetaMap;
|
||
}
|
||
|
||
function getOrderEditMeta(key: string) {
|
||
return orderEditMetaMap[key];
|
||
}
|
||
|
||
function isSenderFieldEditable(key: string) {
|
||
return !SENDER_READONLY_KEYS.has(key);
|
||
}
|
||
|
||
function getSenderFormKey(key: string) {
|
||
return SENDER_FORM_KEY_MAP[key] ?? key;
|
||
}
|
||
|
||
function isRecipientFieldEditable(_key: string) {
|
||
return true;
|
||
}
|
||
|
||
function getRecipientFormKey(key: string) {
|
||
return RECIPIENT_FORM_KEY_MAP[key] ?? key;
|
||
}
|
||
|
||
const basicEditForm = reactive({
|
||
clientNo: '',
|
||
trackNo: '',
|
||
channelCode: '',
|
||
cargoAttribute: 'general',
|
||
isCod: false,
|
||
codSum: undefined as number | undefined,
|
||
codCurrency: '',
|
||
});
|
||
|
||
const senderEditForm = reactive<Record<string, string>>({
|
||
senderName: '',
|
||
company: '',
|
||
countryCode: '',
|
||
province: '',
|
||
city: '',
|
||
zipCode: '',
|
||
streetAddress1: '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
houseNumber: '',
|
||
phone: '',
|
||
telePhone: '',
|
||
email: '',
|
||
});
|
||
|
||
const receiverEditForm = reactive<Record<string, string>>({
|
||
recipientsName: '',
|
||
companyName: '',
|
||
countryCode: '',
|
||
phone: '',
|
||
telePhone: '',
|
||
zipCode: '',
|
||
province: '',
|
||
city: '',
|
||
streetNumber: '',
|
||
streetAddress1: '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
email: '',
|
||
taxType: '',
|
||
taxNo: '',
|
||
certificateType: '',
|
||
certificateNo: '',
|
||
eori: '',
|
||
vat: '',
|
||
});
|
||
|
||
const declarationEditList = ref<any[]>([]);
|
||
|
||
const weightEditForm = ref<
|
||
Array<{
|
||
type: string;
|
||
length?: number;
|
||
width?: number;
|
||
height?: number;
|
||
actualWeight?: number;
|
||
}>
|
||
>([]);
|
||
|
||
const hasSender = computed(() => Object.keys(sender.value).length > 0);
|
||
const orderId = computed(() => detail.value?.id);
|
||
|
||
const canViewAllocationTrace = computed(() => {
|
||
const o: any = detail.value || {};
|
||
if (!orderId.value) return false;
|
||
const product = o.productCode ?? o.ProductCode;
|
||
const channel = o.channelCode ?? o.ChannelCode ?? o.channelName ?? o.ChannelName;
|
||
const pending = o.allocationPendingReason ?? o.AllocationPendingReason;
|
||
const hasTrace = o.hasAllocationTrace ?? o.HasAllocationTrace;
|
||
return Boolean(product || channel || pending || hasTrace);
|
||
});
|
||
|
||
function openAllocationTrace() {
|
||
if (!orderId.value) {
|
||
Message.warning(t('order.myOrders.allocationTrace.noOrder'));
|
||
return;
|
||
}
|
||
allocationTraceVisible.value = true;
|
||
}
|
||
|
||
const displayDeclarationRows = computed(() =>
|
||
basicEditing.value ? declarationEditList.value : declarationList.value
|
||
);
|
||
|
||
function getOrderFieldLabelKey(fieldKey: string) {
|
||
return orderBasicFields.find((field) => field.key === fieldKey)?.labelKey ?? fieldKey;
|
||
}
|
||
|
||
function isOrderBooleanField(fieldKey: string) {
|
||
return ['isCod', 'isIntercept', 'isRemote', 'isPaid'].includes(fieldKey);
|
||
}
|
||
|
||
function getOrderBooleanTagStyle(fieldKey: string) {
|
||
const val = getOrderValue(fieldKey);
|
||
const yes = t('order.myOrders.drawer.yes');
|
||
return getBooleanTagStyle(val === yes);
|
||
}
|
||
|
||
function isDeclarationNumberCol(key: string) {
|
||
return ['unitPrice', 'unitWeight', 'quantity', 'amount', 'weight'].includes(key);
|
||
}
|
||
|
||
function cloneDeclarationRows(rows: any[]) {
|
||
return cloneDeclarationItemRows(rows);
|
||
}
|
||
|
||
function addDeclarationRow() {
|
||
declarationEditList.value.push({
|
||
cnName: '',
|
||
enName: '',
|
||
sku: '',
|
||
unit: '',
|
||
quantity: 1,
|
||
isMagnetic: false,
|
||
});
|
||
}
|
||
|
||
function removeDeclarationRow(index: number) {
|
||
declarationEditList.value.splice(index, 1);
|
||
}
|
||
|
||
function buildDeclarationPayload() {
|
||
return buildDeclarationPayloadFromRows(declarationEditList.value);
|
||
}
|
||
|
||
function parseOptionalInt(value: string) {
|
||
const trimmed = String(value ?? '').trim();
|
||
if (!trimmed) return undefined;
|
||
const num = Number(trimmed);
|
||
return Number.isNaN(num) ? undefined : num;
|
||
}
|
||
|
||
const displayWeightRows = computed(() => {
|
||
if (!weightEditing.value) {
|
||
return weightList.value;
|
||
}
|
||
return weightEditForm.value.map((row) => {
|
||
const orig = weightList.value.find((w: any) => w.type === row.type) || {};
|
||
return { ...orig, ...row };
|
||
});
|
||
});
|
||
|
||
function resetEditState() {
|
||
basicEditing.value = false;
|
||
weightEditing.value = false;
|
||
}
|
||
|
||
const [register, { openDrawer: innerOpenDrawer, setDrawerProps, closeDrawer }] = useDrawer();
|
||
|
||
const activeTab = ref('basic');
|
||
const tabList = computed(() => [
|
||
{ key: 'basic', label: t('order.myOrders.drawer.tabs.basic') },
|
||
{ key: 'weight', label: t('order.myOrders.drawer.tabs.weight') },
|
||
{ key: 'fee', label: t('order.myOrders.drawer.tabs.fee') },
|
||
{ key: 'track', label: t('order.myOrders.drawer.tabs.track') },
|
||
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
||
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
||
{ key: 'issueHandling', label: t('order.myOrders.drawer.tabs.issueHandling') },
|
||
]);
|
||
|
||
// 订单基本信息字段 (t_order) - 按文档表结构
|
||
const orderBasicFields = mergeOrderBasicFields([
|
||
{ key: 'serviceNo', labelKey: 'order.myOrders.drawer.serviceNo' },
|
||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||
{ key: 'productCode', labelKey: 'order.myOrders.drawer.productCode' },
|
||
{ key: 'productName', labelKey: 'order.myOrders.drawer.productName' },
|
||
{ key: 'channelCode', labelKey: 'order.myOrders.drawer.channelCode' },
|
||
{ key: 'channelName', labelKey: 'order.myOrders.drawer.channelName' },
|
||
{ key: 'consigneeCountry', labelKey: 'order.myOrders.drawer.consigneeCountry' },
|
||
{ key: 'consigneeCountryName', labelKey: 'order.myOrders.drawer.consigneeCountryName' },
|
||
{ key: 'cargoAttribute', labelKey: 'order.myOrders.drawer.cargoAttribute' },
|
||
{ key: 'orderStatus', labelKey: 'order.myOrders.drawer.orderStatus' },
|
||
{ key: 'predeclareFailReason', labelKey: 'order.myOrders.drawer.predeclareFailReason' },
|
||
{ key: 'allocationPendingReason', labelKey: 'order.myOrders.drawer.allocationPendingReason' },
|
||
{ key: 'warehouse', labelKey: 'order.myOrders.drawer.warehouse' },
|
||
{ key: 'receiveTime', labelKey: 'order.myOrders.drawer.receiveTime' },
|
||
{ key: 'shipTime', labelKey: 'order.myOrders.drawer.shipTime' },
|
||
{ key: 'payTime', labelKey: 'order.myOrders.drawer.payTime' },
|
||
{ key: 'isPaid', labelKey: 'order.myOrders.drawer.isPaid' },
|
||
{ key: 'signTime', labelKey: 'order.myOrders.drawer.signTime' },
|
||
{ key: 'isCod', labelKey: 'order.myOrders.drawer.isCod' },
|
||
{ key: 'codCurrency', labelKey: 'order.myOrders.drawer.codCurrency' },
|
||
{ key: 'codSum', labelKey: 'order.myOrders.drawer.codSum' },
|
||
{ key: 'isIntercept', labelKey: 'order.myOrders.drawer.isIntercept' },
|
||
{ key: 'isRemote', labelKey: 'order.myOrders.drawer.isRemote' },
|
||
{ key: 'createTime', labelKey: 'order.myOrders.drawer.createTime' },
|
||
{ key: 'createBy', labelKey: 'order.myOrders.drawer.createBy' },
|
||
{ key: 'updateTime', labelKey: 'order.myOrders.drawer.updateTime' },
|
||
{ key: 'updateBy', labelKey: 'order.myOrders.drawer.updateBy' },
|
||
]);
|
||
|
||
const orderBasicFieldGroups = patchOrderBasicFieldGroups(
|
||
[
|
||
{
|
||
key: 'identity',
|
||
labelKey: 'order.myOrders.drawer.basic.group.identity',
|
||
fields: ['serviceNo', 'clientNo', 'trackNo', 'downstreamServiceNo'],
|
||
},
|
||
{
|
||
key: 'product',
|
||
labelKey: 'order.myOrders.drawer.basic.group.product',
|
||
fields: [
|
||
'productCode',
|
||
'productName',
|
||
'channelCode',
|
||
'channelName',
|
||
'warehouse',
|
||
'consigneeCountry',
|
||
'consigneeCountryName',
|
||
'cargoAttribute',
|
||
],
|
||
},
|
||
{
|
||
key: 'timeline',
|
||
labelKey: 'order.myOrders.drawer.basic.group.timeline',
|
||
fields: ['orderStatus', 'predeclareFailReason', 'allocationPendingReason', 'receiveTime', 'shipTime', 'signTime', 'payTime', 'isPaid'],
|
||
},
|
||
{
|
||
key: 'flags',
|
||
labelKey: 'order.myOrders.drawer.basic.group.flags',
|
||
fields: ['isCod', 'codCurrency', 'codSum', 'isIntercept', 'isRemote'],
|
||
},
|
||
{
|
||
key: 'audit',
|
||
labelKey: 'order.myOrders.drawer.basic.group.audit',
|
||
fields: ['createTime', 'createBy', 'updateTime', 'updateBy'],
|
||
},
|
||
],
|
||
{ includeChannel: true },
|
||
);
|
||
|
||
// 发件人表单字段 (t_order_sender) - 补齐全部字段
|
||
const senderFields = [
|
||
{ key: 'senderName', labelKey: 'order.myOrders.drawer.senderName' },
|
||
{ key: 'company', labelKey: 'order.myOrders.drawer.company' },
|
||
{ key: 'country', labelKey: 'order.myOrders.drawer.country' },
|
||
{ key: 'province', labelKey: 'order.myOrders.drawer.province' },
|
||
{ key: 'city', labelKey: 'order.myOrders.drawer.city' },
|
||
{ key: 'zipCode', labelKey: 'order.myOrders.drawer.zipCode' },
|
||
{ key: 'streetAddress1', labelKey: 'order.myOrders.drawer.streetAddress1' },
|
||
{ key: 'streetAddress2', labelKey: 'order.myOrders.drawer.streetAddress2' },
|
||
{ key: 'streetAddress3', labelKey: 'order.myOrders.drawer.streetAddress3' },
|
||
{ key: 'houseNumber', labelKey: 'order.myOrders.drawer.houseNumber' },
|
||
{ key: 'phone', labelKey: 'order.myOrders.drawer.phone' },
|
||
{ key: 'telePhone', labelKey: 'order.myOrders.drawer.telePhone' },
|
||
{ key: 'email', labelKey: 'order.myOrders.drawer.email' },
|
||
{ key: 'isDefault', labelKey: 'order.myOrders.drawer.isDefault' },
|
||
];
|
||
|
||
// 收件人表单字段 (t_order_recipient) - 补齐全部字段
|
||
const recipientFields = [
|
||
{ key: 'recipientsName', labelKey: 'order.myOrders.drawer.recipientsName' },
|
||
{ key: 'companyName', labelKey: 'order.myOrders.drawer.companyName' },
|
||
{ key: 'country', labelKey: 'order.myOrders.drawer.recipientCountry' },
|
||
{ key: 'phone', labelKey: 'order.myOrders.drawer.recipientPhone' },
|
||
{ key: 'telePhone', labelKey: 'order.myOrders.drawer.recipientTelePhone' },
|
||
{ key: 'zipCode', labelKey: 'order.myOrders.drawer.recipientZipCode' },
|
||
{ key: 'province', labelKey: 'order.myOrders.drawer.province' },
|
||
{ key: 'city', labelKey: 'order.myOrders.drawer.city' },
|
||
{ key: 'streetNumber', labelKey: 'order.myOrders.drawer.streetNumber' },
|
||
{ key: 'streetAddress1', labelKey: 'order.myOrders.drawer.streetAddress1' },
|
||
{ key: 'streetAddress2', labelKey: 'order.myOrders.drawer.streetAddress2' },
|
||
{ key: 'streetAddress3', labelKey: 'order.myOrders.drawer.streetAddress3' },
|
||
{ key: 'email', labelKey: 'order.myOrders.drawer.email' },
|
||
{ key: 'taxType', labelKey: 'order.myOrders.drawer.taxType' },
|
||
{ key: 'taxNo', labelKey: 'order.myOrders.drawer.taxNo' },
|
||
{ key: 'certificateType', labelKey: 'order.myOrders.drawer.certificateType' },
|
||
{ key: 'certificateNo', labelKey: 'order.myOrders.drawer.certificateNo' },
|
||
{ key: 'eori', labelKey: 'order.myOrders.drawer.eori' },
|
||
{ key: 'vat', labelKey: 'order.myOrders.drawer.vat' },
|
||
];
|
||
|
||
// 申报信息列表列 (t_order_declaration) - 补齐全部字段
|
||
const declarationColumns = [...ORDER_DETAIL_DECLARATION_COLUMNS];
|
||
|
||
// 件重尺:类型独立展示,明细区不再含类型列
|
||
type WeightViewMode = 'all' | 'forecast' | 'receive' | 'ship' | 'downstream';
|
||
const weightViewMode = ref<WeightViewMode>('all');
|
||
|
||
const weightTypeDefs = [
|
||
{
|
||
key: 'forecast' as const,
|
||
labelKey: 'order.myOrders.drawer.weightType.forecast',
|
||
emptyKey: 'order.myOrders.drawer.weight.empty.forecast',
|
||
},
|
||
{
|
||
key: 'receive' as const,
|
||
labelKey: 'order.myOrders.drawer.weightType.receive',
|
||
emptyKey: 'order.myOrders.drawer.weight.empty.receive',
|
||
},
|
||
{
|
||
key: 'ship' as const,
|
||
labelKey: 'order.myOrders.drawer.weightType.ship',
|
||
emptyKey: 'order.myOrders.drawer.weight.empty.ship',
|
||
},
|
||
{
|
||
key: 'downstream' as const,
|
||
labelKey: 'order.myOrders.drawer.weightType.downstream',
|
||
emptyKey: 'order.myOrders.drawer.weight.empty.downstream',
|
||
},
|
||
];
|
||
|
||
const weightMetricFields = [
|
||
{ key: 'length', labelKey: 'order.myOrders.drawer.weightLength', editable: true, highlight: false },
|
||
{ key: 'width', labelKey: 'order.myOrders.drawer.weightWidth', editable: true, highlight: false },
|
||
{ key: 'height', labelKey: 'order.myOrders.drawer.weightHeight', editable: true, highlight: false },
|
||
{ key: 'actualWeight', labelKey: 'order.myOrders.drawer.actualWeight', editable: true, highlight: false },
|
||
{ key: 'volumetricWeight', labelKey: 'order.myOrders.drawer.volumetricWeight', editable: false, highlight: false },
|
||
{ key: 'chargeableWeight', labelKey: 'order.myOrders.drawer.chargeableWeight', editable: false, highlight: true },
|
||
{ key: 'cubic', labelKey: 'order.myOrders.drawer.cubic', editable: false, highlight: false },
|
||
];
|
||
|
||
// 费用明细列(类型已从列表拆出,按应收/应付分区展示)
|
||
const feeDetailColumns = [
|
||
{ key: 'costItem', labelKey: 'order.myOrders.drawer.costItem' },
|
||
{ key: 'amount', labelKey: 'order.myOrders.drawer.amount' },
|
||
{ key: 'currency', labelKey: 'order.myOrders.drawer.currency' },
|
||
{ key: 'chargingStage', labelKey: 'order.myOrders.drawer.chargingStage' },
|
||
{ key: 'downstreamProvider', labelKey: 'order.myOrders.drawer.downstreamProvider' },
|
||
{ key: 'priceNo', labelKey: 'order.myOrders.drawer.priceNo' },
|
||
{ key: 'priceEffectiveTime', labelKey: 'order.myOrders.drawer.priceEffectiveTime' },
|
||
];
|
||
|
||
type FeeViewMode = 'all' | 'receivable' | 'payable';
|
||
const feeViewMode = ref<FeeViewMode>('all');
|
||
|
||
// 详情数据(从 order 或 API 获取)
|
||
const sender = ref<Record<string, any>>({});
|
||
const recipient = ref<Record<string, any>>({});
|
||
const declarationList = ref<any[]>([]);
|
||
const weightList = ref<any[]>([]);
|
||
const feeList = ref<any[]>([]);
|
||
const feeItemDictRows = ref<DictDto[]>([]);
|
||
let feeItemDictLoadPromise: Promise<void> | null = null;
|
||
const trackTimeline = ref<any[]>([]);
|
||
const trackLoading = ref(false);
|
||
|
||
const trackOrderNumber = computed(
|
||
() =>
|
||
detail.value?.trackNo ||
|
||
detail.value?.trackingNo ||
|
||
detail.value?.waybillNo ||
|
||
detail.value?.customerOrderNo ||
|
||
''
|
||
);
|
||
|
||
function getWeightRowByType(type: string) {
|
||
return displayWeightRows.value.find((row) => row.type === type);
|
||
}
|
||
|
||
function hasWeightRow(type: string) {
|
||
return displayWeightRows.value.some((row) => row.type === type);
|
||
}
|
||
|
||
function hasWeightData(row: any) {
|
||
if (!row) return false;
|
||
return weightMetricFields.some((field) => {
|
||
const val = row[field.key];
|
||
return val != null && val !== '';
|
||
});
|
||
}
|
||
|
||
const visibleWeightSections = computed(() => {
|
||
const sections = weightTypeDefs.map((def) => {
|
||
const row = getWeightRowByType(def.key) ?? { type: def.key };
|
||
return {
|
||
...def,
|
||
row,
|
||
hasData: hasWeightData(row),
|
||
};
|
||
});
|
||
if (weightViewMode.value === 'all') return sections;
|
||
return sections.filter((section) => section.key === weightViewMode.value);
|
||
});
|
||
|
||
function setWeightViewMode(mode: WeightViewMode) {
|
||
weightViewMode.value = weightViewMode.value === mode ? 'all' : mode;
|
||
}
|
||
|
||
function formatWeightNumber(value: unknown) {
|
||
const num = Number(value);
|
||
return Number.isFinite(num) ? num.toFixed(3).replace(/\.?0+$/, '') : '--';
|
||
}
|
||
|
||
function formatWeightChargeable(row?: { chargeableWeight?: number | string | null }) {
|
||
const val = row?.chargeableWeight;
|
||
if (val == null || val === '') return '--';
|
||
return `${formatWeightNumber(val)} kg`;
|
||
}
|
||
|
||
function formatWeightCellValue(row: any, key: string) {
|
||
const val = row?.[key];
|
||
if (val == null || val === '') return '--';
|
||
if (['length', 'width', 'height'].includes(key)) return `${formatWeightNumber(val)} cm`;
|
||
if (['actualWeight', 'volumetricWeight', 'chargeableWeight'].includes(key)) {
|
||
return `${formatWeightNumber(val)} kg`;
|
||
}
|
||
if (key === 'cubic') return `${formatWeightNumber(val)} m³`;
|
||
return String(val);
|
||
}
|
||
|
||
function handleAskAdvisor() {
|
||
const pick = (key: string) => {
|
||
const text = getOrderValue(key);
|
||
return text && text !== '--' ? text : undefined;
|
||
};
|
||
|
||
openAskAdvisor(router, {
|
||
orderId: orderId.value != null ? String(orderId.value) : undefined,
|
||
waybillNo: pick('serviceNo'),
|
||
trackingNo: pick('trackNo'),
|
||
customerOrderNo: pick('clientNo'),
|
||
from: 'waybillManage',
|
||
orderStatus: resolveOrderStatusCode(detail.value) || undefined,
|
||
});
|
||
}
|
||
|
||
function getOrderDetailFormatContext() {
|
||
return {
|
||
t,
|
||
resolveOrderStatusCode,
|
||
getOrderStatusMeta,
|
||
exportCustomsDict: exportCustomsDictRows.value,
|
||
additionalServiceDict: additionalServiceDictRows.value,
|
||
};
|
||
}
|
||
|
||
// 订单基本信息取值(兼容 API 驼峰与 mock 字段名)
|
||
function getOrderValue(key: string) {
|
||
const o = detail.value;
|
||
if (!o) return '--';
|
||
const fieldMap: Record<string, string[]> = {
|
||
serviceNo: ['serviceNo', 'waybillNo'],
|
||
clientNo: ['clientNo', 'customerOrderNo'],
|
||
trackNo: ['trackNo', 'trackingNo'],
|
||
downstreamServiceNo: ['downstreamServiceNo', 'DownstreamServiceNo'],
|
||
productCode: ['productCode'],
|
||
productName: ['productName', 'shippingMethod'],
|
||
channelCode: ['channelCode'],
|
||
channelName: ['channelName', 'serviceChannel'],
|
||
consigneeCountry: ['consigneeCountry'],
|
||
consigneeCountryName: ['consigneeCountryName'],
|
||
cargoAttribute: ['cargoAttribute'],
|
||
orderStatus: ['status', 'orderStatus'],
|
||
predeclareFailReason: ['predeclareFailReason', 'PredeclareFailReason'],
|
||
allocationPendingReason: ['allocationPendingReason', 'AllocationPendingReason'],
|
||
warehouse: ['warehouse', 'warehouseName'],
|
||
receiveTime: ['receiveTime'],
|
||
shipTime: ['shipTime'],
|
||
payTime: ['payTime'],
|
||
signTime: ['signTime'],
|
||
isPaid: ['isPaid', 'IsPaid'],
|
||
isCod: ['isCod'],
|
||
codCurrency: ['codCurrency'],
|
||
codSum: ['codSum'],
|
||
isIntercept: ['isIntercept'],
|
||
isRemote: ['isRemote'],
|
||
createTime: ['createTime'],
|
||
createBy: ['createBy'],
|
||
updateTime: ['updateTime'],
|
||
updateBy: ['updateBy'],
|
||
...ORDER_DETAIL_VALUE_FIELD_MAP,
|
||
};
|
||
const keys = fieldMap[key] || [key];
|
||
let val: any;
|
||
for (const k of keys) {
|
||
if (o[k] != null && o[k] !== '') {
|
||
val = o[k];
|
||
break;
|
||
}
|
||
}
|
||
if (val == null || val === '') return '--';
|
||
if (key === 'isCod' || key === 'isIntercept' || key === 'isRemote' || key === 'isPaid') {
|
||
return val ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no');
|
||
}
|
||
if (key === 'orderStatus') {
|
||
const code = resolveOrderStatusCode(o);
|
||
const meta = getOrderStatusMeta(code);
|
||
if (meta) return t(meta.labelKey);
|
||
const statusKey = `order.myOrders.status.${val}`;
|
||
const translated = t(statusKey);
|
||
return translated !== statusKey ? translated : String(val);
|
||
}
|
||
if (key === 'cargoAttribute') {
|
||
const cargoKey = `order.myOrders.drawer.cargoAttribute.${val}`;
|
||
const translated = t(cargoKey);
|
||
return translated !== cargoKey ? translated : String(val);
|
||
}
|
||
const extended = formatOrderDetailExtendedValue(key, val, o, getOrderDetailFormatContext());
|
||
if (extended != null) return extended;
|
||
return String(val);
|
||
}
|
||
|
||
function getSenderValue(key: string) {
|
||
if (key === 'country') {
|
||
return sender.value.country ?? sender.value.countryCode ?? '--';
|
||
}
|
||
const val = sender.value[key];
|
||
if (key === 'isDefault') {
|
||
return val == null ? '--' : (val ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'));
|
||
}
|
||
return val ?? '--';
|
||
}
|
||
|
||
function getRecipientValue(key: string) {
|
||
if (key === 'country') {
|
||
return recipient.value.country ?? recipient.value.countryCode ?? '--';
|
||
}
|
||
return recipient.value[key] ?? '--';
|
||
}
|
||
|
||
function formatTrackTime(timestamp?: number | string) {
|
||
if (timestamp == null || timestamp === '') return '';
|
||
const raw = typeof timestamp === 'string' ? Number(timestamp) : timestamp;
|
||
if (!Number.isFinite(raw)) return String(timestamp);
|
||
const d = new Date(raw);
|
||
if (Number.isNaN(d.getTime())) return String(timestamp);
|
||
return d.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false,
|
||
});
|
||
}
|
||
|
||
function formatTrackTimeDisplay(item: any) {
|
||
return formatTrackTime(item?.trackTime);
|
||
}
|
||
|
||
function normalizeTrackText(value: unknown) {
|
||
const text = value == null ? '' : String(value).trim();
|
||
return !text || text === '--' ? '' : text;
|
||
}
|
||
|
||
function formatTrackPlace(item: any) {
|
||
const structured = [item?.trackPlaceNation, item?.trackPlaceProvince, item?.trackPlaceCity]
|
||
.map(normalizeTrackText)
|
||
.filter(Boolean);
|
||
if (structured.length) return structured.join(' · ');
|
||
return normalizeTrackText(item?.trackPlace);
|
||
}
|
||
|
||
function getTrackStatusLabel(item: any) {
|
||
return (
|
||
normalizeTrackText(item?.trackStatus) ||
|
||
normalizeTrackText(item?.content) ||
|
||
t('order.myOrders.drawer.track.statusUnknown')
|
||
);
|
||
}
|
||
|
||
/** 有独立状态时,中文内容作为详情;否则中文已作标题 */
|
||
function getTrackZhDetail(item: any) {
|
||
const status = normalizeTrackText(item?.trackStatus);
|
||
const zh = normalizeTrackText(item?.content);
|
||
if (!zh || !status || zh === status) return '';
|
||
return zh;
|
||
}
|
||
|
||
/** 英文内容;与标题/中文相同则不重复展示 */
|
||
function getTrackEnDetail(item: any) {
|
||
const en = normalizeTrackText(item?.enContent);
|
||
if (!en) return '';
|
||
const zh = normalizeTrackText(item?.content);
|
||
const status = normalizeTrackText(item?.trackStatus);
|
||
if (en === zh || en === status) return '';
|
||
return en;
|
||
}
|
||
|
||
const displayTrackTimeline = computed(() => {
|
||
const list = [...trackTimeline.value];
|
||
if (!list.length) return [];
|
||
// 与顺丰/京东一致:最新在上
|
||
return list.sort((a, b) => Number(b?.trackTime || 0) - Number(a?.trackTime || 0));
|
||
});
|
||
|
||
function isTrackNodeActive(item: any, index: number) {
|
||
if (item?.isHighlight) return true;
|
||
const list = displayTrackTimeline.value;
|
||
if (list.some((node) => node?.isHighlight)) return false;
|
||
return index === 0;
|
||
}
|
||
|
||
function normalizeFeeType(row: any): 'receivable' | 'payable' | '' {
|
||
const raw = String(row?.type ?? row?.feeType ?? row?.FeeType ?? '').toLowerCase();
|
||
if (raw === 'receivable' || raw === 'payable') return raw;
|
||
return '';
|
||
}
|
||
|
||
function sumFeesByCurrency(list: any[]) {
|
||
const totals = new Map<string, number>();
|
||
for (const row of list) {
|
||
const currency = String(row?.currency ?? 'CNY').trim() || 'CNY';
|
||
const amount = Number(row?.amount);
|
||
if (!Number.isFinite(amount)) continue;
|
||
totals.set(currency, (totals.get(currency) ?? 0) + amount);
|
||
}
|
||
return totals;
|
||
}
|
||
|
||
const receivableFeeList = computed(() =>
|
||
feeList.value.filter((row) => normalizeFeeType(row) === 'receivable')
|
||
);
|
||
|
||
const payableFeeList = computed(() =>
|
||
feeList.value.filter((row) => normalizeFeeType(row) === 'payable')
|
||
);
|
||
|
||
const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value));
|
||
const payableTotals = computed(() => sumFeesByCurrency(payableFeeList.value));
|
||
|
||
function calcGrossProfitTotals(
|
||
receivable: Map<string, number>,
|
||
payable: Map<string, number>
|
||
) {
|
||
const currencies = new Set([...receivable.keys(), ...payable.keys()]);
|
||
const result = new Map<string, number>();
|
||
for (const currency of currencies) {
|
||
result.set(currency, (receivable.get(currency) ?? 0) - (payable.get(currency) ?? 0));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
const feeGrossProfitTotals = computed(() =>
|
||
calcGrossProfitTotals(receivableTotals.value, payableTotals.value)
|
||
);
|
||
|
||
const hasNegativeGrossProfit = computed(() =>
|
||
[...feeGrossProfitTotals.value.values()].some((amount) => amount < 0)
|
||
);
|
||
|
||
function pickPrimaryTotal(totals: Map<string, number>, fallback?: unknown) {
|
||
if (totals.size) {
|
||
const preferred = ['CNY', 'RMB', 'USD', 'EUR'];
|
||
for (const c of preferred) {
|
||
if (totals.has(c)) return { currency: c, amount: totals.get(c)! };
|
||
}
|
||
const [currency, amount] = [...totals.entries()][0];
|
||
return { currency, amount };
|
||
}
|
||
const n = Number(fallback);
|
||
if (Number.isFinite(n)) return { currency: '', amount: n };
|
||
return null;
|
||
}
|
||
|
||
const forecastReceivableAmount = computed(() => {
|
||
const raw = detail.value?.forecastReceivableAmount ?? detail.value?.ForecastReceivableAmount;
|
||
if (raw == null || raw === '') return null;
|
||
const n = Number(raw);
|
||
return Number.isFinite(n) ? n : null;
|
||
});
|
||
|
||
const forecastPayableAmount = computed(() => {
|
||
const raw = detail.value?.forecastPayableAmount ?? detail.value?.ForecastPayableAmount;
|
||
if (raw == null || raw === '') return null;
|
||
const n = Number(raw);
|
||
return Number.isFinite(n) ? n : null;
|
||
});
|
||
|
||
const showForecastDiff = computed(() => forecastReceivableAmount.value != null);
|
||
|
||
const currentReceivablePrimary = computed(() =>
|
||
pickPrimaryTotal(
|
||
receivableTotals.value,
|
||
detail.value?.receivableAmount ?? detail.value?.ReceivableAmount
|
||
)
|
||
);
|
||
|
||
const currentPayablePrimary = computed(() =>
|
||
pickPrimaryTotal(payableTotals.value, detail.value?.payableAmount ?? detail.value?.PayableAmount)
|
||
);
|
||
|
||
const receivableForecastDiff = computed(() => {
|
||
if (forecastReceivableAmount.value == null || !currentReceivablePrimary.value) return null;
|
||
return currentReceivablePrimary.value.amount - forecastReceivableAmount.value;
|
||
});
|
||
|
||
const payableForecastDiff = computed(() => {
|
||
if (forecastPayableAmount.value == null || !currentPayablePrimary.value) return null;
|
||
return currentPayablePrimary.value.amount - forecastPayableAmount.value;
|
||
});
|
||
|
||
const formatCurrentReceivableText = computed(() => {
|
||
const p = currentReceivablePrimary.value;
|
||
if (!p) return '--';
|
||
return p.currency ? `${p.currency} ${p.amount.toFixed(2)}` : p.amount.toFixed(2);
|
||
});
|
||
|
||
const formatCurrentPayableText = computed(() => {
|
||
const p = currentPayablePrimary.value;
|
||
if (!p) return '--';
|
||
return p.currency ? `${p.currency} ${p.amount.toFixed(2)}` : p.amount.toFixed(2);
|
||
});
|
||
|
||
const orderAdditionalServices = computed(() =>
|
||
parseAdditionalServiceCodes(
|
||
detail.value?.additionalServices ?? detail.value?.AdditionalServices
|
||
)
|
||
);
|
||
|
||
const orderHasAdditionalServicesField = computed(() => {
|
||
const o = detail.value;
|
||
if (!o) return false;
|
||
return (
|
||
Object.prototype.hasOwnProperty.call(o, 'additionalServices') ||
|
||
Object.prototype.hasOwnProperty.call(o, 'AdditionalServices') ||
|
||
o.additionalServices != null ||
|
||
o.AdditionalServices != null
|
||
);
|
||
});
|
||
|
||
const isOrderUnpaid = computed(() => {
|
||
const v = detail.value?.isPaid ?? detail.value?.IsPaid;
|
||
return !(v === true || v === 1 || v === '1');
|
||
});
|
||
|
||
const canEditAdditionalServices = computed(
|
||
() =>
|
||
isOrderUnpaid.value &&
|
||
(productHasAdditionalServices.value || orderHasAdditionalServicesField.value)
|
||
);
|
||
|
||
const grossProfitMarginText = computed(() => {
|
||
const margins: string[] = [];
|
||
for (const [currency, recv] of receivableTotals.value) {
|
||
if (!recv) continue;
|
||
const profit = feeGrossProfitTotals.value.get(currency) ?? 0;
|
||
margins.push(
|
||
t('order.myOrders.drawer.fee.grossMargin', {
|
||
value: `${((profit / recv) * 100).toFixed(1)}%`,
|
||
})
|
||
);
|
||
}
|
||
return margins.length ? margins.join(' / ') : t('order.myOrders.drawer.fee.grossMarginEmpty');
|
||
});
|
||
|
||
const visibleFeeSections = computed(() => {
|
||
const sections = [
|
||
{
|
||
key: 'receivable' as const,
|
||
titleKey: 'order.myOrders.drawer.fee.receivableTitle',
|
||
emptyKey: 'order.myOrders.drawer.fee.emptyReceivable',
|
||
list: receivableFeeList.value,
|
||
totals: receivableTotals.value,
|
||
tagColor: 'green',
|
||
amountClass: 'amount-receivable',
|
||
},
|
||
{
|
||
key: 'payable' as const,
|
||
titleKey: 'order.myOrders.drawer.fee.payableTitle',
|
||
emptyKey: 'order.myOrders.drawer.fee.emptyPayable',
|
||
list: payableFeeList.value,
|
||
totals: payableTotals.value,
|
||
tagColor: 'red',
|
||
amountClass: 'amount-payable',
|
||
},
|
||
];
|
||
if (feeViewMode.value === 'all') return sections;
|
||
return sections.filter((section) => section.key === feeViewMode.value);
|
||
});
|
||
|
||
function setFeeViewMode(mode: FeeViewMode) {
|
||
feeViewMode.value = feeViewMode.value === mode ? 'all' : mode;
|
||
}
|
||
|
||
function openAddFeeModal() {
|
||
if (orderId.value == null) return;
|
||
addFeeDefaultType.value =
|
||
feeViewMode.value === 'payable'
|
||
? 'payable'
|
||
: feeViewMode.value === 'receivable'
|
||
? 'receivable'
|
||
: 'receivable';
|
||
addFeeModalVisible.value = true;
|
||
}
|
||
|
||
function handleAddFeeSuccess(order: OrderRow) {
|
||
applyDetailFromApi(order);
|
||
void ensureFeeItemDictLoaded();
|
||
}
|
||
|
||
function handleIssueChanged() {
|
||
if (detail.value) {
|
||
emit('updated', detail.value as OrderRow);
|
||
}
|
||
}
|
||
|
||
function formatFeeAmount(value: unknown) {
|
||
const num = Number(value);
|
||
return Number.isFinite(num) ? num.toFixed(2) : '--';
|
||
}
|
||
|
||
function formatFeeTotals(totals: Map<string, number>) {
|
||
if (!totals.size) return '--';
|
||
return [...totals.entries()]
|
||
.map(([currency, amount]) => `${currency} ${amount.toFixed(2)}`)
|
||
.join(' + ');
|
||
}
|
||
|
||
function formatGrossProfitTotals(totals: Map<string, number>) {
|
||
if (!totals.size) return '--';
|
||
return [...totals.entries()]
|
||
.map(([currency, amount]) => {
|
||
const sign = amount > 0 ? '+' : '';
|
||
return `${sign}${currency} ${amount.toFixed(2)}`;
|
||
})
|
||
.join(' + ');
|
||
}
|
||
|
||
function formatPlainAmount(value: number | null | undefined) {
|
||
if (value == null || !Number.isFinite(value)) return '--';
|
||
return value.toFixed(2);
|
||
}
|
||
|
||
function formatSignedAmount(value: number | null | undefined) {
|
||
if (value == null || !Number.isFinite(value)) return '--';
|
||
const sign = value > 0 ? '+' : '';
|
||
return `${sign}${value.toFixed(2)}`;
|
||
}
|
||
|
||
function labelAdditionalService(code: string) {
|
||
const hit = additionalServiceDictRows.value.find((r) => r.dictCode === code);
|
||
return hit?.dictName || code;
|
||
}
|
||
|
||
async function ensureAdditionalServiceDictLoaded() {
|
||
const tasks: Promise<void>[] = [];
|
||
if (!additionalServiceDictRows.value.length) {
|
||
tasks.push(
|
||
getDictListAll({ dictType: 'additional_service' })
|
||
.then((rows) => {
|
||
additionalServiceDictRows.value = rows;
|
||
})
|
||
.catch(() => {
|
||
additionalServiceDictRows.value = [];
|
||
}),
|
||
);
|
||
}
|
||
if (!exportCustomsDictRows.value.length) {
|
||
tasks.push(
|
||
getDictListAll({ dictType: 'export_customs_mode' })
|
||
.then((rows) => {
|
||
exportCustomsDictRows.value = rows;
|
||
})
|
||
.catch(() => {
|
||
exportCustomsDictRows.value = [];
|
||
}),
|
||
);
|
||
}
|
||
if (tasks.length) {
|
||
await Promise.all(tasks);
|
||
}
|
||
}
|
||
|
||
async function resolveProductIdByCode(productCode: string): Promise<number | string | null> {
|
||
const code = String(productCode ?? '').trim();
|
||
if (!code) return null;
|
||
try {
|
||
const { items } = await getSalesProductList({
|
||
page: 1,
|
||
pageSize: 20,
|
||
keyword: code,
|
||
isActive: 2,
|
||
});
|
||
const hit = (items ?? []).find(
|
||
(it: any) =>
|
||
String(it?.productCode ?? it?.ProductCode ?? '').trim().toLowerCase() ===
|
||
code.toLowerCase()
|
||
);
|
||
return hit?.id ?? hit?.Id ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function refreshProductAdditionalServicesCapability() {
|
||
const code = String(detail.value?.productCode ?? detail.value?.ProductCode ?? '').trim();
|
||
if (!code) {
|
||
productHasAdditionalServices.value = false;
|
||
productAdditionalServiceOptions.value = [];
|
||
return;
|
||
}
|
||
try {
|
||
await ensureAdditionalServiceDictLoaded();
|
||
const productId = await resolveProductIdByCode(code);
|
||
if (productId == null) {
|
||
productHasAdditionalServices.value = false;
|
||
productAdditionalServiceOptions.value = [];
|
||
return;
|
||
}
|
||
const product = await getSalesProductDetail(productId);
|
||
const codes = parseAdditionalServiceCodes(
|
||
product?.additionalServices ?? product?.AdditionalServices
|
||
);
|
||
productHasAdditionalServices.value = codes.length > 0;
|
||
productAdditionalServiceOptions.value = codes.map((c) => ({
|
||
value: c,
|
||
label: labelAdditionalService(c),
|
||
}));
|
||
} catch {
|
||
productHasAdditionalServices.value = false;
|
||
productAdditionalServiceOptions.value = [];
|
||
}
|
||
}
|
||
|
||
async function openAdditionalServicesModal() {
|
||
additionalServicesModalVisible.value = true;
|
||
additionalServicesLoading.value = true;
|
||
try {
|
||
await refreshProductAdditionalServicesCapability();
|
||
const allowed = new Set(productAdditionalServiceOptions.value.map((o) => o.value));
|
||
selectedAdditionalServices.value = orderAdditionalServices.value.filter((c) =>
|
||
allowed.size ? allowed.has(c) : true
|
||
);
|
||
} finally {
|
||
additionalServicesLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleAdditionalServicesSubmit() {
|
||
const id = orderId.value;
|
||
if (id == null) return Promise.reject(new Error('missing order'));
|
||
additionalServicesSaving.value = true;
|
||
try {
|
||
const result = await postOrderAdditionalServicesUpdate({
|
||
id,
|
||
additionalServices: [...selectedAdditionalServices.value],
|
||
});
|
||
applyDetailFromApi(result);
|
||
additionalServicesModalVisible.value = false;
|
||
Message.success(t('order.myOrders.drawer.fee.additionalServicesSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('order.myOrders.drawer.fee.additionalServicesFailed'));
|
||
throw e;
|
||
} finally {
|
||
additionalServicesSaving.value = false;
|
||
}
|
||
}
|
||
|
||
function getChargingStage(row: any) {
|
||
const stage = row?.chargingStage ?? row?.ChargingStage;
|
||
return stage != null && stage !== '' ? String(stage) : '';
|
||
}
|
||
|
||
function getChargingStageLabel(stage: string) {
|
||
const key = `finance.receivablePayable.chargingStage.${stage}`;
|
||
const translated = t(key);
|
||
return translated !== key ? translated : stage;
|
||
}
|
||
|
||
function getChargingStageColor(stage: string) {
|
||
const map: Record<string, string> = {
|
||
forecast: 'arcoblue',
|
||
inbound: 'orange',
|
||
outbound: 'purple',
|
||
};
|
||
return map[stage] || 'gray';
|
||
}
|
||
|
||
async function ensureFeeItemDictLoaded() {
|
||
if (feeItemDictRows.value.length) return;
|
||
if (feeItemDictLoadPromise) {
|
||
await feeItemDictLoadPromise;
|
||
return;
|
||
}
|
||
feeItemDictLoadPromise = (async () => {
|
||
try {
|
||
feeItemDictRows.value = await getDictListAll({ dictType: 'fee_item' });
|
||
} catch {
|
||
feeItemDictRows.value = [];
|
||
}
|
||
})();
|
||
await feeItemDictLoadPromise;
|
||
}
|
||
|
||
function getCostItemLabel(row: any) {
|
||
const fromApi = row?.costItemName ?? row?.CostItemName;
|
||
if (fromApi != null && fromApi !== '') return String(fromApi);
|
||
|
||
const code = row?.costItem ?? row?.CostItem;
|
||
if (code == null || code === '') return '--';
|
||
|
||
const c = String(code).trim();
|
||
const hit = feeItemDictRows.value.find(
|
||
(r) => String(r.dictCode ?? (r as any).DictCode ?? '').trim() === c
|
||
);
|
||
if (hit?.dictName) return hit.dictName;
|
||
|
||
const byName = feeItemDictRows.value.find(
|
||
(r) => String(r.dictName ?? (r as any).DictName ?? '').trim() === c
|
||
);
|
||
return byName?.dictName || c;
|
||
}
|
||
|
||
function getFeeCellValue(row: any, col: { key: string }) {
|
||
if (col.key === 'costItem') return getCostItemLabel(row);
|
||
if (col.key === 'priceEffectiveTime') {
|
||
const v = row[col.key];
|
||
if (v == null) return '--';
|
||
if (typeof v === 'number') return formatTrackTime(v);
|
||
return String(v);
|
||
}
|
||
return row[col.key] ?? '--';
|
||
}
|
||
|
||
// 加载轨迹(复用 track/order 的接口)
|
||
async function loadTrackTimeline() {
|
||
const orderNumber = detail.value?.trackingNo || detail.value?.waybillNo || detail.value?.customerOrderNo;
|
||
if (!orderNumber) {
|
||
trackTimeline.value = [];
|
||
return;
|
||
}
|
||
trackLoading.value = true;
|
||
try {
|
||
const res: any = await postGetDetailTimeline({ orderNumber });
|
||
trackTimeline.value = res?.data || res || [];
|
||
} catch {
|
||
trackTimeline.value = [];
|
||
} finally {
|
||
trackLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 加载详情数据(优先使用 list 接口返回的 sender/receiver/items/weightList,与 parcel/create 对应)
|
||
function loadDetailData() {
|
||
const o = detail.value;
|
||
if (!o) return;
|
||
|
||
// 发件人
|
||
if (o.sender && typeof o.sender === 'object') {
|
||
sender.value = { ...o.sender, country: o.sender.country ?? o.sender.countryCode };
|
||
} else {
|
||
sender.value = {};
|
||
}
|
||
|
||
// 收件人:优先使用 list 返回的 receiver
|
||
if (o.receiver && typeof o.receiver === 'object') {
|
||
recipient.value = { ...o.receiver, country: o.receiver.country ?? o.receiver.countryCode };
|
||
} else {
|
||
recipient.value = {
|
||
recipientsName: o.consignee || '',
|
||
companyName: '',
|
||
country: o.consigneeCountry || '',
|
||
phone: o.consigneePhone || '',
|
||
telePhone: '',
|
||
zipCode: o.consigneePostcode || '',
|
||
province: o.consigneeState || '',
|
||
city: o.consigneeCity || '',
|
||
streetNumber: '',
|
||
streetAddress1: o.consigneeAddress || '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
email: '',
|
||
taxType: '',
|
||
taxNo: '',
|
||
certificateType: '',
|
||
certificateNo: '',
|
||
eori: o.eori || '',
|
||
vat: '',
|
||
};
|
||
}
|
||
|
||
// 申报信息
|
||
declarationList.value = Array.isArray(o.items) && o.items.length > 0
|
||
? cloneDeclarationItemRows(o.items)
|
||
: [];
|
||
|
||
// 件重尺
|
||
weightList.value = Array.isArray(o.weightList) && o.weightList.length > 0 ? o.weightList : [];
|
||
|
||
// 费用:接口字段可为 feeList / orderFees / FeeList
|
||
const fees = o.feeList ?? o.FeeList ?? o.orderFees ?? o.fees;
|
||
feeList.value = Array.isArray(fees) ? fees : [];
|
||
}
|
||
|
||
function applyDetailFromApi(raw: any) {
|
||
if (!raw || typeof raw !== 'object') return;
|
||
detail.value = { ...(detail.value || {}), ...raw };
|
||
loadDetailData();
|
||
emit('updated', detail.value as OrderRow);
|
||
}
|
||
|
||
function fillBasicEditForm() {
|
||
const o = detail.value;
|
||
if (!o?.id) return;
|
||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||
basicEditForm.channelCode = String(o.channelCode ?? '');
|
||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||
basicEditForm.codCurrency = String(o.codCurrency ?? '');
|
||
|
||
senderEditForm.senderName = String(sender.value.senderName ?? '');
|
||
senderEditForm.company = String(sender.value.company ?? '');
|
||
senderEditForm.countryCode = String(sender.value.countryCode ?? sender.value.country ?? '');
|
||
senderEditForm.province = String(sender.value.province ?? '');
|
||
senderEditForm.city = String(sender.value.city ?? '');
|
||
senderEditForm.zipCode = String(sender.value.zipCode ?? '');
|
||
senderEditForm.streetAddress1 = String(sender.value.streetAddress1 ?? '');
|
||
senderEditForm.streetAddress2 = String(sender.value.streetAddress2 ?? '');
|
||
senderEditForm.streetAddress3 = String(sender.value.streetAddress3 ?? '');
|
||
senderEditForm.houseNumber = String(sender.value.houseNumber ?? '');
|
||
senderEditForm.phone = String(sender.value.phone ?? '');
|
||
senderEditForm.telePhone = String(sender.value.telePhone ?? '');
|
||
senderEditForm.email = String(sender.value.email ?? '');
|
||
|
||
receiverEditForm.recipientsName = String(recipient.value.recipientsName ?? o.consignee ?? '');
|
||
receiverEditForm.companyName = String(recipient.value.companyName ?? '');
|
||
receiverEditForm.countryCode = String(
|
||
recipient.value.countryCode ?? recipient.value.country ?? o.consigneeCountry ?? ''
|
||
);
|
||
receiverEditForm.phone = String(recipient.value.phone ?? o.consigneePhone ?? '');
|
||
receiverEditForm.telePhone = String(recipient.value.telePhone ?? '');
|
||
receiverEditForm.zipCode = String(recipient.value.zipCode ?? o.consigneePostcode ?? '');
|
||
receiverEditForm.province = String(recipient.value.province ?? o.consigneeState ?? '');
|
||
receiverEditForm.city = String(recipient.value.city ?? o.consigneeCity ?? '');
|
||
receiverEditForm.streetNumber = String(recipient.value.streetNumber ?? '');
|
||
receiverEditForm.streetAddress1 = String(recipient.value.streetAddress1 ?? o.consigneeAddress ?? '');
|
||
receiverEditForm.streetAddress2 = String(recipient.value.streetAddress2 ?? '');
|
||
receiverEditForm.streetAddress3 = String(recipient.value.streetAddress3 ?? '');
|
||
receiverEditForm.email = String(recipient.value.email ?? '');
|
||
receiverEditForm.taxType = recipient.value.taxType != null ? String(recipient.value.taxType) : '';
|
||
receiverEditForm.taxNo = String(recipient.value.taxNo ?? '');
|
||
receiverEditForm.certificateType =
|
||
recipient.value.certificateType != null ? String(recipient.value.certificateType) : '';
|
||
receiverEditForm.certificateNo = String(recipient.value.certificateNo ?? '');
|
||
receiverEditForm.eori = String(recipient.value.eori ?? o.eori ?? '');
|
||
receiverEditForm.vat = String(recipient.value.vat ?? '');
|
||
|
||
declarationEditList.value = cloneDeclarationRows(declarationList.value);
|
||
}
|
||
|
||
function startBasicEdit() {
|
||
fillBasicEditForm();
|
||
basicEditing.value = true;
|
||
}
|
||
|
||
function cancelBasicEdit() {
|
||
basicEditing.value = false;
|
||
declarationEditList.value = [];
|
||
loadDetailData();
|
||
}
|
||
|
||
function startWeightEdit() {
|
||
weightEditForm.value = (weightList.value || []).map((row: any) => ({
|
||
type: row.type,
|
||
length: row.length != null ? Number(row.length) : undefined,
|
||
width: row.width != null ? Number(row.width) : undefined,
|
||
height: row.height != null ? Number(row.height) : undefined,
|
||
actualWeight: row.actualWeight != null ? Number(row.actualWeight) : undefined,
|
||
}));
|
||
weightEditing.value = true;
|
||
}
|
||
|
||
function cancelWeightEdit() {
|
||
weightEditing.value = false;
|
||
loadDetailData();
|
||
}
|
||
|
||
async function handleBasicEditSave() {
|
||
const id = orderId.value;
|
||
if (id == null) return;
|
||
basicSaving.value = true;
|
||
try {
|
||
const result = await postOrderUpdate({
|
||
id,
|
||
clientNo: basicEditForm.clientNo,
|
||
trackNo: basicEditForm.trackNo,
|
||
channelCode: basicEditForm.channelCode,
|
||
cargoAttribute: basicEditForm.cargoAttribute,
|
||
isCod: basicEditForm.isCod,
|
||
codSum: basicEditForm.codSum,
|
||
codCurrency: basicEditForm.codCurrency,
|
||
sender: hasSender.value
|
||
? {
|
||
senderName: senderEditForm.senderName,
|
||
company: senderEditForm.company,
|
||
countryCode: senderEditForm.countryCode,
|
||
province: senderEditForm.province,
|
||
city: senderEditForm.city,
|
||
zipCode: senderEditForm.zipCode,
|
||
streetAddress1: senderEditForm.streetAddress1,
|
||
streetAddress2: senderEditForm.streetAddress2,
|
||
streetAddress3: senderEditForm.streetAddress3,
|
||
houseNumber: senderEditForm.houseNumber,
|
||
phone: senderEditForm.phone,
|
||
telePhone: senderEditForm.telePhone,
|
||
email: senderEditForm.email,
|
||
}
|
||
: undefined,
|
||
receiver: {
|
||
recipientsName: receiverEditForm.recipientsName,
|
||
companyName: receiverEditForm.companyName,
|
||
countryCode: receiverEditForm.countryCode,
|
||
phone: receiverEditForm.phone,
|
||
telePhone: receiverEditForm.telePhone,
|
||
zipCode: receiverEditForm.zipCode,
|
||
province: receiverEditForm.province,
|
||
city: receiverEditForm.city,
|
||
streetNumber: receiverEditForm.streetNumber,
|
||
streetAddress1: receiverEditForm.streetAddress1,
|
||
streetAddress2: receiverEditForm.streetAddress2,
|
||
streetAddress3: receiverEditForm.streetAddress3,
|
||
email: receiverEditForm.email,
|
||
taxType: parseOptionalInt(receiverEditForm.taxType),
|
||
taxNo: receiverEditForm.taxNo,
|
||
certificateType: parseOptionalInt(receiverEditForm.certificateType),
|
||
certificateNo: receiverEditForm.certificateNo,
|
||
eori: receiverEditForm.eori,
|
||
vat: receiverEditForm.vat,
|
||
},
|
||
items: buildDeclarationPayload(),
|
||
});
|
||
applyDetailFromApi(result);
|
||
basicEditing.value = false;
|
||
Message.success(t('order.myOrders.drawer.message.saveSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '保存失败');
|
||
} finally {
|
||
basicSaving.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleWeightEditSave() {
|
||
const id = orderId.value;
|
||
if (id == null) return;
|
||
weightSaving.value = true;
|
||
try {
|
||
const result = await postOrderWeightUpdate({
|
||
id,
|
||
weights: weightEditForm.value.map((row) => ({
|
||
type: row.type,
|
||
length: row.length,
|
||
width: row.width,
|
||
height: row.height,
|
||
actualWeight: row.actualWeight,
|
||
})),
|
||
});
|
||
applyDetailFromApi(result);
|
||
weightEditing.value = false;
|
||
Message.success(t('order.myOrders.drawer.message.saveSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '保存失败');
|
||
} finally {
|
||
weightSaving.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleRecalculateReceivable() {
|
||
const id = orderId.value;
|
||
if (id == null) return;
|
||
recalcReceivableLoading.value = true;
|
||
try {
|
||
const result = await postOrderRecalculateReceivable({ id });
|
||
applyDetailFromApi(result);
|
||
Message.success(t('order.myOrders.drawer.message.recalculateSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '重算应收失败');
|
||
} finally {
|
||
recalcReceivableLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleRecalculatePayable() {
|
||
const id = orderId.value;
|
||
if (id == null) return;
|
||
recalcPayableLoading.value = true;
|
||
try {
|
||
const result = await postOrderRecalculatePayable({ id });
|
||
applyDetailFromApi(result);
|
||
Message.success(t('order.myOrders.drawer.message.recalculateSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '重算应付失败');
|
||
} finally {
|
||
recalcPayableLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleTabChange(tabKey: string) {
|
||
resetEditState();
|
||
if (tabKey === 'basic') {
|
||
basicViewMode.value = 'all';
|
||
}
|
||
if (tabKey === 'weight') {
|
||
weightViewMode.value = 'all';
|
||
}
|
||
if (tabKey === 'fee') {
|
||
feeViewMode.value = 'all';
|
||
void ensureFeeItemDictLoaded();
|
||
void refreshProductAdditionalServicesCapability();
|
||
}
|
||
if (tabKey === 'track') {
|
||
loadTrackTimeline();
|
||
}
|
||
}
|
||
|
||
function handleCancel() {
|
||
resetEditState();
|
||
emit('cancel');
|
||
}
|
||
|
||
watch(
|
||
() => props.order,
|
||
(newOrder) => {
|
||
detail.value = newOrder ? { ...newOrder } : null;
|
||
resetEditState();
|
||
if (newOrder) {
|
||
void ensureAdditionalServiceDictLoaded();
|
||
loadDetailData();
|
||
if (activeTab.value === 'fee') {
|
||
void ensureFeeItemDictLoaded();
|
||
void refreshProductAdditionalServicesCapability();
|
||
}
|
||
if (activeTab.value === 'track') {
|
||
loadTrackTimeline();
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true, deep: true }
|
||
);
|
||
|
||
defineExpose({
|
||
openDrawer: (opts?: { tab?: 'basic' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'issueHandling' }) => {
|
||
activeTab.value = opts?.tab || 'basic';
|
||
basicViewMode.value = 'all';
|
||
feeViewMode.value = 'all';
|
||
weightViewMode.value = 'all';
|
||
resetEditState();
|
||
setDrawerProps({
|
||
content: '',
|
||
placement: 'right',
|
||
width: 1000,
|
||
title: '',
|
||
});
|
||
innerOpenDrawer();
|
||
if (opts?.tab === 'track') {
|
||
void loadTrackTimeline();
|
||
} else if (opts?.tab === 'fee') {
|
||
void ensureFeeItemDictLoaded();
|
||
void refreshProductAdditionalServicesCapability();
|
||
}
|
||
},
|
||
startBasicEdit,
|
||
setDrawerProps,
|
||
closeDrawer,
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
:deep(.arco-drawer-header) {
|
||
display: none;
|
||
}
|
||
|
||
.order-detail-drawer {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.drawer-tabs {
|
||
margin-bottom: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.drawer-tab-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
flex-shrink: 0;
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-meta {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.toolbar-meta-label {
|
||
font-size: 13px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.toolbar-meta-value {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
.drawer-tab-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
min-height: 0;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.tab-panel {
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.inline-edit-form {
|
||
:deep(.arco-form-item) {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
:deep(.arco-form-item-label) {
|
||
font-size: 13px;
|
||
color: var(--color-text-3);
|
||
}
|
||
}
|
||
|
||
.edit-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
margin-bottom: 12px;
|
||
|
||
.edit-field-label {
|
||
font-size: 13px;
|
||
color: var(--color-text-3);
|
||
line-height: 20px;
|
||
}
|
||
|
||
:deep(.arco-input),
|
||
:deep(.arco-select),
|
||
:deep(.arco-input-number) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.readonly-field {
|
||
.readonly-value {
|
||
min-height: 32px;
|
||
padding: 4px 0;
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.form-grid-empty {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.declaration-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.readonly-inline {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.edit-form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0 16px;
|
||
}
|
||
|
||
.edit-form-span-2 {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.basic-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.predeclare-fail-guide {
|
||
margin-top: 12px;
|
||
padding: 12px 14px;
|
||
border-radius: 8px;
|
||
background: #fff7f7;
|
||
border-left: 3px solid #f53f3f;
|
||
border-top: 1px solid #ffd8d2;
|
||
border-right: 1px solid #ffd8d2;
|
||
border-bottom: 1px solid #ffd8d2;
|
||
|
||
&__reason {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
line-height: 1.45;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
&__tip {
|
||
margin: 8px 0 0;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--color-text-2);
|
||
}
|
||
|
||
&__tip-label {
|
||
margin-right: 4px;
|
||
color: var(--app-primary-color, #2d8c8c);
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__details {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
|
||
summary {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
}
|
||
|
||
&__raw {
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
word-break: break-word;
|
||
}
|
||
|
||
&__actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
}
|
||
|
||
.detail-field-text--with-action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.alloc-eye-btn {
|
||
color: var(--color-text-3);
|
||
padding: 0 2px;
|
||
|
||
&:hover {
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.basic-view-switch {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.detail-section {
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: var(--app-card-bg);
|
||
|
||
&.basic .detail-section-dot {
|
||
background: var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
&.sender .detail-section-dot {
|
||
background: #165dff;
|
||
}
|
||
|
||
&.recipient .detail-section-dot {
|
||
background: #00b42a;
|
||
}
|
||
|
||
&.declaration .detail-section-dot {
|
||
background: #ff7d00;
|
||
}
|
||
}
|
||
|
||
.detail-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
background: var(--color-fill-2);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
.detail-section-title-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.detail-section-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.detail-section-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.detail-field-group {
|
||
padding: 12px 12px 0;
|
||
|
||
&:last-child {
|
||
padding-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
.detail-field-group-title {
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--color-text-3);
|
||
line-height: 20px;
|
||
}
|
||
|
||
.detail-fields-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 10px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.detail-field-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
padding: 8px 10px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
background: var(--color-fill-2);
|
||
}
|
||
|
||
.detail-field-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
line-height: 18px;
|
||
}
|
||
|
||
.detail-field-value {
|
||
min-height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
:deep(.arco-input),
|
||
:deep(.arco-select),
|
||
:deep(.arco-input-number) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.detail-field-text {
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
font-weight: 500;
|
||
word-break: break-all;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.detail-section-empty {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.declaration-summary {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.declaration-table-wrap {
|
||
max-height: 280px;
|
||
overflow: auto;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #c9cdd4;
|
||
border-radius: 3px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.detail-fields-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.declaration-table {
|
||
min-width: max-content;
|
||
|
||
.table-header {
|
||
display: flex;
|
||
background: var(--color-fill-2);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--color-text-1);
|
||
|
||
.header-cell {
|
||
padding: 8px 12px;
|
||
border-right: 1px solid var(--color-border-2);
|
||
flex: 1;
|
||
flex-shrink: 0;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.table-row {
|
||
display: flex;
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
border-top: 1px solid var(--color-border-2);
|
||
|
||
&:hover {
|
||
background: var(--color-fill-2);
|
||
}
|
||
|
||
.table-cell {
|
||
padding: 8px 12px;
|
||
border-right: 1px solid var(--color-border-2);
|
||
flex: 1;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
&.action-cell {
|
||
flex: 0 0 72px;
|
||
min-width: 72px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-cell.action-cell {
|
||
flex: 0 0 72px;
|
||
min-width: 72px;
|
||
}
|
||
}
|
||
|
||
.weight-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.weight-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.weight-summary-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
background: var(--app-card-bg);
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
min-width: 0;
|
||
|
||
&:hover {
|
||
border-color: var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
&.active {
|
||
border-color: var(--app-primary-color, #2d8c8c);
|
||
box-shadow: 0 0 0 1px var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
&.forecast .weight-summary-value {
|
||
color: #165dff;
|
||
}
|
||
|
||
&.receive .weight-summary-value {
|
||
color: #ff7d00;
|
||
}
|
||
|
||
&.ship .weight-summary-value {
|
||
color: #00b42a;
|
||
}
|
||
|
||
&.downstream .weight-summary-value {
|
||
color: #722ed1;
|
||
}
|
||
}
|
||
|
||
.weight-summary-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
line-height: 20px;
|
||
}
|
||
|
||
.weight-summary-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 24px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.weight-summary-meta {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
line-height: 20px;
|
||
}
|
||
|
||
.weight-view-switch {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.weight-section {
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: var(--app-card-bg);
|
||
|
||
&.forecast .weight-section-dot {
|
||
background: #165dff;
|
||
}
|
||
|
||
&.receive .weight-section-dot {
|
||
background: #ff7d00;
|
||
}
|
||
|
||
&.ship .weight-section-dot {
|
||
background: #00b42a;
|
||
}
|
||
|
||
&.downstream .weight-section-dot {
|
||
background: #722ed1;
|
||
}
|
||
}
|
||
|
||
.weight-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
background: var(--color-fill-2);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
.weight-section-title-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.weight-section-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.weight-section-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.weight-section-total {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.weight-metrics-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 12px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.weight-metric-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
padding: 8px 10px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
background: var(--color-fill-2);
|
||
|
||
&.highlight {
|
||
border-color: #b9f0c2;
|
||
background: #f6ffed;
|
||
}
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
line-height: 18px;
|
||
}
|
||
|
||
.metric-value-wrap {
|
||
min-height: 24px;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
font-weight: 500;
|
||
font-variant-numeric: tabular-nums;
|
||
word-break: break-all;
|
||
|
||
&.metric-value-highlight {
|
||
color: #00b42a;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.weight-section-empty {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.weight-empty-all {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.weight-summary {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.weight-metrics-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.fee-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.fee-summary {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.fee-summary-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 12px 16px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
background: var(--app-card-bg);
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
border-color: var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
&.active {
|
||
border-color: var(--app-primary-color, #2d8c8c);
|
||
box-shadow: 0 0 0 1px var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
&.receivable .fee-summary-amount {
|
||
color: #00b42a;
|
||
}
|
||
|
||
&.payable .fee-summary-amount {
|
||
color: #f53f3f;
|
||
}
|
||
|
||
&.gross-profit {
|
||
cursor: default;
|
||
|
||
&:hover {
|
||
border-color: var(--color-border-2);
|
||
}
|
||
|
||
.fee-summary-amount,
|
||
.fee-summary-meta {
|
||
color: #ff7d00;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fee-profit-formula {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px dashed var(--color-border-2);
|
||
border-radius: 6px;
|
||
background: var(--color-fill-2);
|
||
font-size: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.fee-profit-formula-label {
|
||
color: var(--color-text-3);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.fee-profit-formula-value {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
font-weight: 500;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.fee-profit-operator {
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.fee-profit-result {
|
||
font-weight: 600;
|
||
color: #ff7d00;
|
||
}
|
||
|
||
.fee-forecast-diff {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
background: var(--color-fill-2);
|
||
}
|
||
|
||
.fee-forecast-diff-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--color-text-2);
|
||
}
|
||
|
||
.fee-forecast-diff-rows {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px 12px;
|
||
}
|
||
|
||
.fee-forecast-diff-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.fee-forecast-diff-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.fee-diff-up {
|
||
color: #f53f3f;
|
||
font-weight: 600;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.fee-forecast-diff-rows {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.fee-summary-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
line-height: 20px;
|
||
}
|
||
|
||
.fee-summary-amount {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
line-height: 26px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.fee-summary-meta {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
line-height: 20px;
|
||
}
|
||
|
||
.fee-view-switch {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.fee-section {
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: var(--app-card-bg);
|
||
|
||
&.receivable {
|
||
.fee-section-dot {
|
||
background: #00b42a;
|
||
}
|
||
}
|
||
|
||
&.payable {
|
||
.fee-section-dot {
|
||
background: #f53f3f;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fee-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
background: var(--color-fill-2);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
.fee-section-title-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.fee-section-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.fee-section-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.fee-section-total {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.fee-table-wrap {
|
||
max-height: 220px;
|
||
overflow: auto;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #c9cdd4;
|
||
border-radius: 3px;
|
||
}
|
||
}
|
||
|
||
.fee-table {
|
||
min-width: 100%;
|
||
|
||
.table-header,
|
||
.table-row {
|
||
display: grid;
|
||
grid-template-columns: 1.2fr 0.9fr 0.6fr 0.8fr 1fr 1fr 1.3fr;
|
||
align-items: center;
|
||
}
|
||
|
||
.table-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
background: var(--color-fill-2);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--color-text-3);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
.table-row {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--color-fill-2);
|
||
}
|
||
}
|
||
|
||
.header-cell,
|
||
.table-cell {
|
||
padding: 8px 12px;
|
||
min-width: 0;
|
||
word-break: break-all;
|
||
|
||
&.amount {
|
||
text-align: right;
|
||
}
|
||
|
||
&.priceEffectiveTime {
|
||
min-width: 152px;
|
||
white-space: nowrap;
|
||
word-break: normal;
|
||
}
|
||
}
|
||
|
||
.header-cell.amount {
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.fee-amount {
|
||
font-weight: 600;
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
&.amount-receivable {
|
||
color: #00b42a;
|
||
}
|
||
|
||
&.amount-payable {
|
||
color: #f53f3f;
|
||
}
|
||
}
|
||
|
||
.fee-section-empty {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.fee-empty-all {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.track-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.track-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 6px;
|
||
background: var(--color-fill-2);
|
||
}
|
||
|
||
.track-toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.track-toolbar-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.track-toolbar-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.track-toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.track-spin {
|
||
width: 100%;
|
||
min-height: 160px;
|
||
}
|
||
|
||
.track-empty {
|
||
padding: 24px 0;
|
||
}
|
||
|
||
.track-timeline {
|
||
padding: 4px 0 8px;
|
||
|
||
.track-item {
|
||
display: flex;
|
||
margin-bottom: 0;
|
||
|
||
&.active {
|
||
.track-status {
|
||
color: var(--app-primary-color, #2d8c8c);
|
||
}
|
||
|
||
.track-content {
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
.track-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 16px;
|
||
margin-right: 14px;
|
||
flex-shrink: 0;
|
||
padding-top: 5px;
|
||
|
||
.track-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: var(--color-fill-3);
|
||
border: 2px solid var(--color-border-2);
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
|
||
&.active {
|
||
width: 12px;
|
||
height: 12px;
|
||
background: var(--app-primary-color, #2d8c8c);
|
||
border-color: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 35%, #fff);
|
||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-primary-color, #2d8c8c) 18%, transparent);
|
||
}
|
||
}
|
||
|
||
.track-line {
|
||
width: 2px;
|
||
flex: 1;
|
||
min-height: 18px;
|
||
background: var(--color-fill-3);
|
||
margin: 4px 0 0;
|
||
|
||
&.active {
|
||
background: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 45%, var(--color-fill-3));
|
||
}
|
||
}
|
||
}
|
||
|
||
.track-right {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding-bottom: 18px;
|
||
}
|
||
|
||
.track-row-head {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.track-status {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--color-text-2);
|
||
line-height: 22px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.track-content {
|
||
margin-top: 4px;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: var(--color-text-2);
|
||
word-break: break-word;
|
||
|
||
&--en {
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: var(--color-text-3);
|
||
}
|
||
}
|
||
|
||
.track-meta-row {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 4px 10px;
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.track-meta-place {
|
||
color: var(--color-text-2);
|
||
}
|
||
|
||
.track-meta-code {
|
||
padding: 0 6px;
|
||
border-radius: 3px;
|
||
background: var(--color-fill-2);
|
||
color: var(--color-text-3);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
font-size: 11px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.track-remark {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: var(--color-text-3);
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
|
||
.track-item:last-child .track-right {
|
||
padding-bottom: 0;
|
||
}
|
||
}
|
||
</style>
|