All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m33s
Co-authored-by: Cursor <cursoragent@cursor.com>
4389 lines
135 KiB
Vue
4389 lines
135 KiB
Vue
<template>
|
||
<a-drawer
|
||
:visible="visible"
|
||
placement="bottom"
|
||
height="auto"
|
||
:footer="false"
|
||
:header="false"
|
||
unmount-on-close
|
||
:mask-closable="!submitting"
|
||
:drawer-style="drawerStyle"
|
||
popup-container="body"
|
||
class="h5-po-drawer"
|
||
@cancel="close"
|
||
>
|
||
<div class="h5-po">
|
||
<div class="h5-po__handle" aria-hidden="true" />
|
||
|
||
<header class="h5-po__head">
|
||
<h3>{{ isEditMode ? t('h5.placeOrder.editTitle') : t('h5.placeOrder.title') }}</h3>
|
||
<button type="button" class="h5-po__close" :disabled="submitting" @click="close">
|
||
<icon-close />
|
||
</button>
|
||
</header>
|
||
|
||
<div class="h5-po__steps">
|
||
<button
|
||
v-for="(s, idx) in steps"
|
||
:key="s.key"
|
||
type="button"
|
||
class="h5-po__step"
|
||
:class="{ 'is-active': step === idx, 'is-done': idx < step }"
|
||
@click="goToStep(idx)"
|
||
>
|
||
{{ idx + 1 }}.{{ s.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<p v-if="handoffTip" class="h5-po__tip">
|
||
<span>{{ handoffTip }}</span>
|
||
<button type="button" @click="handoffTip = ''">×</button>
|
||
</p>
|
||
|
||
<a-spin :loading="optionsLoading" class="h5-po__spin">
|
||
<section v-show="step === 0" class="h5-po__card">
|
||
<div class="h5-po__sender-head">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.sender') }}</label>
|
||
<button type="button" class="h5-po__link" @click="showSenderForm = !showSenderForm">
|
||
{{ showSenderForm ? t('h5.placeOrder.cancelAddSender') : t('h5.placeOrder.addSender') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="showSenderForm" class="h5-po__sender-form">
|
||
<div class="h5-po__row">
|
||
<a-input v-model="senderForm.senderName" :placeholder="t('h5.placeOrder.senderNamePh')" />
|
||
<a-input v-model="senderForm.phone" :placeholder="t('h5.placeOrder.senderPhonePh')" />
|
||
</div>
|
||
<div class="h5-po__row">
|
||
<a-input v-model="senderForm.province" :placeholder="t('h5.placeOrder.senderProvincePh')" />
|
||
<a-input v-model="senderForm.city" :placeholder="t('h5.placeOrder.senderCityPh')" />
|
||
</div>
|
||
<a-textarea
|
||
v-model="senderForm.streetAddress1"
|
||
:placeholder="t('h5.placeOrder.senderAddressPh')"
|
||
:auto-size="{ minRows: 2, maxRows: 3 }"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="h5-po__btn is-primary h5-po__btn--block"
|
||
:disabled="savingSender"
|
||
@click="saveSender"
|
||
>
|
||
{{ savingSender ? t('h5.placeOrder.savingSender') : t('h5.placeOrder.saveSender') }}
|
||
</button>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<p v-if="!senderLoading && !senderOptions.length" class="h5-po__sender-empty">
|
||
{{ t('h5.placeOrder.senderEmpty') }}
|
||
</p>
|
||
<a-select
|
||
:model-value="selectedSenderId"
|
||
allow-clear
|
||
:loading="senderLoading"
|
||
:error="fieldHasError('sender')"
|
||
:placeholder="t('h5.placeOrder.senderPh')"
|
||
@change="onSenderChange"
|
||
>
|
||
<a-option v-for="opt in senderOptions" :key="opt.id" :value="opt.id">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('sender')" class="h5-po__field-err">{{ fieldError('sender') }}</p>
|
||
</template>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.clientNo') }}</label>
|
||
<a-input
|
||
v-model="form.clientNo"
|
||
allow-clear
|
||
:error="fieldHasError('clientNo')"
|
||
:placeholder="t('h5.placeOrder.clientNoPh')"
|
||
/>
|
||
<p v-if="fieldError('clientNo')" class="h5-po__field-err">{{ fieldError('clientNo') }}</p>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.product') }}</label>
|
||
<a-select
|
||
v-model="form.productCode"
|
||
allow-search
|
||
allow-clear
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('productCode')"
|
||
:placeholder="t('h5.placeOrder.productPh')"
|
||
>
|
||
<a-option v-for="opt in productOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('productCode')" class="h5-po__field-err">{{ fieldError('productCode') }}</p>
|
||
</div>
|
||
|
||
<div v-if="productAdditionalServiceOptions.length" class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.additionalServices') }}</label>
|
||
<a-checkbox-group
|
||
v-model="selectedAdditionalServices"
|
||
class="h5-po__services"
|
||
:disabled="isEditMode"
|
||
>
|
||
<a-checkbox
|
||
v-for="opt in productAdditionalServiceOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</a-checkbox>
|
||
</a-checkbox-group>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.cargo') }}</label>
|
||
<a-select v-model="form.cargoAttribute" allow-clear :error="fieldHasError('cargoAttribute')">
|
||
<a-option v-for="opt in cargoOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('cargoAttribute')" class="h5-po__field-err">
|
||
{{ fieldError('cargoAttribute') }}
|
||
</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.warehouse') }}</label>
|
||
<a-select
|
||
v-model="form.warehouseId"
|
||
allow-clear
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('warehouseId')"
|
||
>
|
||
<a-option v-for="opt in warehouseOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('warehouseId')" class="h5-po__field-err">
|
||
{{ fieldError('warehouseId') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">
|
||
{{ t('h5.placeOrder.weight') }} / {{ t('h5.placeOrder.dimensions') }}
|
||
</label>
|
||
<div class="h5-po__dims h5-po__dims--with-weight">
|
||
<a-input
|
||
v-model="form.actualWeight"
|
||
allow-clear
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('actualWeight')"
|
||
:placeholder="t('h5.placeOrder.weightPh')"
|
||
/>
|
||
<a-input
|
||
v-model="form.length"
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('length')"
|
||
:placeholder="t('h5.placeOrder.length')"
|
||
/>
|
||
<a-input
|
||
v-model="form.width"
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('width')"
|
||
:placeholder="t('h5.placeOrder.width')"
|
||
/>
|
||
<a-input
|
||
v-model="form.height"
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('height')"
|
||
:placeholder="t('h5.placeOrder.height')"
|
||
/>
|
||
</div>
|
||
<p
|
||
v-if="
|
||
fieldError('actualWeight') ||
|
||
fieldError('length') ||
|
||
fieldError('width') ||
|
||
fieldError('height')
|
||
"
|
||
class="h5-po__field-err"
|
||
>
|
||
{{
|
||
fieldError('actualWeight') ||
|
||
fieldError('length') ||
|
||
fieldError('width') ||
|
||
fieldError('height')
|
||
}}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.packageQty') }}</label>
|
||
<a-input
|
||
v-model="form.quantity"
|
||
allow-clear
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('quantity')"
|
||
:placeholder="t('h5.placeOrder.packageQtyPh')"
|
||
/>
|
||
<p v-if="fieldError('quantity')" class="h5-po__field-err">
|
||
{{ fieldError('quantity') }}
|
||
</p>
|
||
<p v-else class="h5-po__hint">{{ t('h5.placeOrder.packageQtyHint') }}</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label h5-po__label--with-help">
|
||
<span>{{ t('h5.placeOrder.exportCustomsMode') }}</span>
|
||
<button
|
||
type="button"
|
||
class="h5-po__help"
|
||
:aria-label="t('h5.placeOrder.customsGuideTitle')"
|
||
@click="exportCustomsGuideVisible = true"
|
||
>
|
||
<icon-question-circle />
|
||
</button>
|
||
</label>
|
||
<a-select
|
||
v-model="form.exportCustomsMode"
|
||
allow-search
|
||
allow-clear
|
||
:disabled="isEditMode"
|
||
:placeholder="t('h5.placeOrder.exportCustomsModePh')"
|
||
>
|
||
<a-option
|
||
v-for="opt in exportCustomsModeOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="needExportQualification">
|
||
<div class="h5-po__summary-banner">
|
||
<strong>{{ t('h5.placeOrder.summaryNeedTitle') }}</strong>
|
||
<p>{{ t('h5.placeOrder.summaryNeedDesc') }}</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.exportQualification') }}</label>
|
||
<a-select
|
||
v-model="form.exportQualificationId"
|
||
allow-search
|
||
allow-clear
|
||
:loading="exportQualificationLoading"
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('exportQualificationId')"
|
||
:placeholder="t('h5.placeOrder.exportQualificationPh')"
|
||
>
|
||
<a-option
|
||
v-for="opt in exportQualificationOptions"
|
||
:key="opt.id"
|
||
:value="String(opt.id)"
|
||
>
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('exportQualificationId')" class="h5-po__field-err">
|
||
{{ fieldError('exportQualificationId') }}
|
||
</p>
|
||
<p v-else-if="!exportQualificationOptions.length" class="h5-po__hint">
|
||
{{ t('h5.placeOrder.exportQualificationEmpty') }}
|
||
</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.platformCode') }}</label>
|
||
<a-input
|
||
v-model="form.platformCode"
|
||
allow-clear
|
||
:max-length="16"
|
||
:disabled="isEditMode"
|
||
:error="fieldHasError('platformCode')"
|
||
:placeholder="t('h5.placeOrder.platformCodePh')"
|
||
/>
|
||
<p v-if="fieldError('platformCode')" class="h5-po__field-err">
|
||
{{ fieldError('platformCode') }}
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.inboundMethod') }}</label>
|
||
<a-radio-group
|
||
v-model="form.inboundMethod"
|
||
type="button"
|
||
size="small"
|
||
class="h5-po__inbound"
|
||
:disabled="isEditMode"
|
||
>
|
||
<a-radio value="selfShip">{{ t('h5.placeOrder.inboundSelfShip') }}</a-radio>
|
||
<a-radio value="doorPickup">{{ t('h5.placeOrder.inboundDoorPickup') }}</a-radio>
|
||
</a-radio-group>
|
||
<p v-if="form.inboundMethod === 'doorPickup'" class="h5-po__hint">
|
||
{{ t('h5.placeOrder.inboundDoorPickupHint') }}
|
||
</p>
|
||
<div v-if="form.inboundMethod === 'selfShip'" class="h5-po__wh-card">
|
||
<div class="h5-po__wh-head">
|
||
<strong>{{ t('h5.placeOrder.selfShipAddressTitle') }}</strong>
|
||
</div>
|
||
<p v-if="!warehouseOptions.length" class="h5-po__hint">
|
||
{{ t('h5.placeOrder.selfShipNoWarehouse') }}
|
||
</p>
|
||
<p v-else-if="!warehousesWithShipAddress.length" class="h5-po__hint">
|
||
{{ t('h5.placeOrder.selfShipNoAddressConfigured') }}
|
||
</p>
|
||
<template v-else>
|
||
<div class="h5-po__wh-list">
|
||
<div
|
||
v-for="opt in warehouseOptions"
|
||
:key="opt.value"
|
||
class="h5-po__wh-item"
|
||
:class="{
|
||
'is-selected': String(opt.value) === String(form.warehouseId ?? ''),
|
||
'is-empty': !warehouseHasAddress(opt),
|
||
}"
|
||
>
|
||
<div class="h5-po__wh-item-head">
|
||
<strong>
|
||
{{ opt.label || opt.value }}
|
||
<span
|
||
v-if="String(opt.value) === String(form.warehouseId ?? '')"
|
||
class="h5-po__wh-badge"
|
||
>
|
||
{{ t('h5.placeOrder.selfShipCurrent') }}
|
||
</span>
|
||
</strong>
|
||
<button
|
||
v-if="warehouseHasAddress(opt)"
|
||
type="button"
|
||
class="h5-po__wh-copy"
|
||
@click="copyWarehouseShipAddress(opt)"
|
||
>
|
||
{{ t('h5.placeOrder.selfShipCopy') }}
|
||
</button>
|
||
</div>
|
||
<p v-if="!warehouseHasAddress(opt)" class="h5-po__hint">
|
||
{{ t('h5.placeOrder.selfShipAddressEmpty') }}
|
||
</p>
|
||
<template v-else>
|
||
<p>
|
||
<span>{{ t('h5.placeOrder.selfShipRecipient') }}</span>
|
||
{{ displayWarehouseRecipient(opt) }}
|
||
</p>
|
||
<p>
|
||
<span>{{ t('h5.placeOrder.selfShipPhone') }}</span>
|
||
{{ opt.contactPhone || '-' }}
|
||
</p>
|
||
<p class="is-addr">
|
||
<span>{{ t('h5.placeOrder.selfShipAddress') }}</span>
|
||
{{ displayWarehouseShippingAddress(opt) }}
|
||
</p>
|
||
<p v-if="opt.remark">
|
||
<span>{{ t('h5.placeOrder.selfShipRemark') }}</span>
|
||
{{ opt.remark }}
|
||
</p>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<p class="h5-po__hint">{{ t('h5.placeOrder.selfShipAddressHint') }}</p>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="form.inboundMethod === 'doorPickup'">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.pickupAddress') }}</label>
|
||
<a-select
|
||
:model-value="selectedPickupAddressId"
|
||
allow-clear
|
||
:loading="pickupAddressLoading"
|
||
:error="fieldHasError('sender')"
|
||
:placeholder="t('h5.placeOrder.pickupAddressPh')"
|
||
@change="onPickupAddressChange"
|
||
@focus="loadPickupAddresses"
|
||
>
|
||
<a-option v-for="opt in pickupAddressOptions" :key="opt.id" :value="opt.id">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('sender')" class="h5-po__field-err">{{ fieldError('sender') }}</p>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.expectPickupTime') }}</label>
|
||
<a-date-picker
|
||
v-model="form.expectPickupTime"
|
||
show-time
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
:error="fieldHasError('expectPickupTime')"
|
||
:placeholder="t('h5.placeOrder.expectPickupTimePh')"
|
||
/>
|
||
<p v-if="fieldError('expectPickupTime')" class="h5-po__field-err">
|
||
{{ fieldError('expectPickupTime') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.carrierService') }}</label>
|
||
<a-select
|
||
v-model="form.carrierService"
|
||
allow-search
|
||
allow-clear
|
||
:placeholder="t('h5.placeOrder.carrierServicePh')"
|
||
>
|
||
<a-option
|
||
v-for="opt in carrierServiceOptions"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<p class="h5-po__hint">{{ t('h5.placeOrder.doorPickupBatchHint') }}</p>
|
||
</template>
|
||
</section>
|
||
|
||
<section v-show="step === 1" class="h5-po__card">
|
||
<div class="h5-po__receiver-toolbar">
|
||
<div class="h5-po__field h5-po__field--grow">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.receiverAddressBook') }}</label>
|
||
<a-select
|
||
:model-value="selectedReceiverAddressId"
|
||
allow-clear
|
||
:loading="receiverAddressLoading"
|
||
:placeholder="t('h5.placeOrder.receiverAddressBookPh')"
|
||
@change="onReceiverAddressChange"
|
||
@focus="loadReceiverAddresses"
|
||
>
|
||
<a-option v-for="opt in receiverAddressOptions" :key="opt.id" :value="opt.id">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="h5-po__save-addr"
|
||
:disabled="savingReceiverAddress"
|
||
@click="saveReceiverAddress"
|
||
>
|
||
{{
|
||
savingReceiverAddress
|
||
? t('h5.placeOrder.savingReceiver')
|
||
: t('h5.placeOrder.saveReceiver')
|
||
}}
|
||
</button>
|
||
</div>
|
||
<p class="h5-po__hint h5-po__hint--tight">{{ t('h5.placeOrder.receiverAddressBookHint') }}</p>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.receiverName') }}</label>
|
||
<a-input
|
||
v-model="form.receiver.recipientsName"
|
||
allow-clear
|
||
:error="fieldHasError('receiver.recipientsName')"
|
||
/>
|
||
<p v-if="fieldError('receiver.recipientsName')" class="h5-po__field-err">
|
||
{{ fieldError('receiver.recipientsName') }}
|
||
</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.receiverPhone') }}</label>
|
||
<a-input
|
||
v-model="form.receiver.phone"
|
||
allow-clear
|
||
:error="fieldHasError('receiver.phone')"
|
||
/>
|
||
<p v-if="fieldError('receiver.phone')" class="h5-po__field-err">
|
||
{{ fieldError('receiver.phone') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.country') }}</label>
|
||
<a-select
|
||
v-model="form.receiver.countryCode"
|
||
allow-search
|
||
allow-clear
|
||
:error="fieldHasError('receiver.countryCode')"
|
||
>
|
||
<a-option v-for="opt in countryOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('receiver.countryCode')" class="h5-po__field-err">
|
||
{{ fieldError('receiver.countryCode') }}
|
||
</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.zip') }}</label>
|
||
<a-input
|
||
v-model="form.receiver.zipCode"
|
||
allow-clear
|
||
:error="fieldHasError('receiver.zipCode')"
|
||
/>
|
||
<p v-if="fieldError('receiver.zipCode')" class="h5-po__field-err">
|
||
{{ fieldError('receiver.zipCode') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.address') }}</label>
|
||
<a-textarea
|
||
v-model="form.receiver.streetAddress1"
|
||
:error="fieldHasError('receiver.streetAddress1')"
|
||
:auto-size="{ minRows: 2, maxRows: 3 }"
|
||
/>
|
||
<p v-if="fieldError('receiver.streetAddress1')" class="h5-po__field-err">
|
||
{{ fieldError('receiver.streetAddress1') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.city') }}</label>
|
||
<a-input v-model="form.receiver.city" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.province') }}</label>
|
||
<a-input v-model="form.receiver.province" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__field h5-po__field--street-number">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.streetNumber') }}</label>
|
||
<div class="h5-po__street-number-row">
|
||
<a-input v-model="form.receiver.streetNumber" allow-clear />
|
||
<button
|
||
type="button"
|
||
class="h5-po__more h5-po__more--inline"
|
||
@click="showReceiverMore = !showReceiverMore"
|
||
>
|
||
{{
|
||
showReceiverMore
|
||
? t('h5.placeOrder.hideReceiverOptional')
|
||
: t('h5.placeOrder.showReceiverOptional')
|
||
}}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-show="showReceiverMore" class="h5-po__more-panel">
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.company') }}</label>
|
||
<a-input v-model="form.receiver.companyName" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.telePhone') }}</label>
|
||
<a-input v-model="form.receiver.telePhone" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.email') }}</label>
|
||
<a-input v-model="form.receiver.email" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.address2') }}</label>
|
||
<a-input v-model="form.receiver.streetAddress2" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.address3') }}</label>
|
||
<a-input v-model="form.receiver.streetAddress3" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.taxType') }}</label>
|
||
<a-input v-model="form.receiver.taxType" allow-clear :placeholder="t('h5.placeOrder.typePh')" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.taxNo') }}</label>
|
||
<a-input v-model="form.receiver.taxNo" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.certificateType') }}</label>
|
||
<a-input
|
||
v-model="form.receiver.certificateType"
|
||
allow-clear
|
||
:placeholder="t('h5.placeOrder.typePh')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.certificateNo') }}</label>
|
||
<a-input v-model="form.receiver.certificateNo" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.eori') }}</label>
|
||
<a-input v-model="form.receiver.eori" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.vat') }}</label>
|
||
<a-input v-model="form.receiver.vat" allow-clear />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-show="step === 2" class="h5-po__declare">
|
||
<div class="h5-po__declare-bar">
|
||
<button type="button" class="h5-po__sku-btn" @click="openDeclarationSkuSheet">
|
||
{{ t('h5.placeOrder.pickSku') }}
|
||
</button>
|
||
<p class="h5-po__declare-bar-tip">{{ t('h5.placeOrder.pickSkuHint') }}</p>
|
||
</div>
|
||
|
||
<p v-if="declarationLiveHint" class="h5-po__field-err h5-po__field-err--block">
|
||
{{ declarationLiveHint }}
|
||
</p>
|
||
<div class="h5-po__card h5-po__card--currency">
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.declaredTotal') }}</label>
|
||
<a-input
|
||
v-model="declaredTotalInput"
|
||
allow-clear
|
||
:placeholder="t('h5.placeOrder.declaredTotalPh')"
|
||
@focus="declaredTotalInputFocused = true"
|
||
@blur="handleDeclaredTotalBlur"
|
||
@change="handleDeclaredTotalChange"
|
||
/>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.currency') }}</label>
|
||
<a-select
|
||
v-model="form.declaredCurrency"
|
||
allow-clear
|
||
:error="fieldHasError('declaredCurrency')"
|
||
>
|
||
<a-option v-for="opt in currencyOptions" :key="opt.value" :value="opt.value">
|
||
{{ opt.label }}
|
||
</a-option>
|
||
</a-select>
|
||
<p v-if="fieldError('declaredCurrency')" class="h5-po__field-err">
|
||
{{ fieldError('declaredCurrency') }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="fieldError('itemsEmpty')" class="h5-po__field-err h5-po__field-err--block">
|
||
{{ fieldError('itemsEmpty') }}
|
||
</p>
|
||
|
||
<div v-for="(row, idx) in items" :key="row._key" class="h5-po__card h5-po__item">
|
||
<div class="h5-po__item-head">
|
||
<div class="h5-po__item-head-main">
|
||
<strong>{{ t('h5.placeOrder.itemTitle', { n: idx + 1 }) }}</strong>
|
||
<span class="h5-po__item-derived">
|
||
{{ t('h5.placeOrder.amount') }} {{ row.amount || '-' }} ·
|
||
{{ t('h5.placeOrder.itemWeight') }} {{ row.weight || '-' }}
|
||
</span>
|
||
</div>
|
||
<button
|
||
v-if="items.length > 1"
|
||
type="button"
|
||
class="h5-po__item-remove"
|
||
@click="removeItem(idx)"
|
||
>
|
||
{{ t('h5.placeOrder.removeItem') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="row.skuImage" class="h5-po__sku-preview">
|
||
<img :src="skuImageSrc(row.skuImage)" alt="" referrerpolicy="no-referrer" />
|
||
<span>{{ row.sku || t('h5.placeOrder.sku') }}</span>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.enName') }}</label>
|
||
<a-input
|
||
v-model="row.enName"
|
||
allow-clear
|
||
:error="fieldHasError(`items.${idx}.enName`)"
|
||
/>
|
||
<p v-if="fieldError(`items.${idx}.enName`)" class="h5-po__field-err">
|
||
{{ fieldError(`items.${idx}.enName`) }}
|
||
</p>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.cnName') }}</label>
|
||
<a-input v-model="row.cnName" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__three">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.unitPrice') }}</label>
|
||
<a-input
|
||
v-model="row.unitPrice"
|
||
allow-clear
|
||
:error="fieldHasError(`items.${idx}.unitPrice`)"
|
||
@input="syncItemDerived(row)"
|
||
/>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.unitWeight') }}</label>
|
||
<a-input
|
||
v-model="row.unitWeight"
|
||
allow-clear
|
||
:error="fieldHasError(`items.${idx}.unitWeight`)"
|
||
@input="syncItemDerived(row)"
|
||
/>
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.qty') }}</label>
|
||
<a-input
|
||
v-model="row.quantity"
|
||
allow-clear
|
||
:error="fieldHasError(`items.${idx}.quantity`)"
|
||
@input="syncItemDerived(row)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<p
|
||
v-if="
|
||
fieldError(`items.${idx}.unitPrice`) ||
|
||
fieldError(`items.${idx}.unitWeight`) ||
|
||
fieldError(`items.${idx}.quantity`)
|
||
"
|
||
class="h5-po__field-err"
|
||
>
|
||
{{
|
||
fieldError(`items.${idx}.unitPrice`) ||
|
||
fieldError(`items.${idx}.unitWeight`) ||
|
||
fieldError(`items.${idx}.quantity`)
|
||
}}
|
||
</p>
|
||
|
||
<button
|
||
type="button"
|
||
class="h5-po__more"
|
||
@click="toggleItemMore(row._key)"
|
||
>
|
||
{{
|
||
isItemMoreOpen(row._key)
|
||
? t('h5.placeOrder.hideMore')
|
||
: t('h5.placeOrder.showMore')
|
||
}}
|
||
</button>
|
||
|
||
<div v-show="isItemMoreOpen(row._key)" class="h5-po__more-panel">
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.sku') }}</label>
|
||
<a-input v-model="row.sku" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.unit') }}</label>
|
||
<a-input v-model="row.unit" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__magnetic">
|
||
<span>{{ t('h5.placeOrder.isMagnetic') }}</span>
|
||
<a-switch v-model="row.isMagnetic" size="small" />
|
||
</div>
|
||
<p class="h5-po__hint h5-po__hint--tight">{{ t('h5.placeOrder.isMagneticHint') }}</p>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.skuImage') }}</label>
|
||
<a-input
|
||
v-model="row.skuImage"
|
||
allow-clear
|
||
:placeholder="t('h5.placeOrder.skuImagePh')"
|
||
/>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.imHsCode') }}</label>
|
||
<a-input v-model="row.imHsCode" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.exHsCode') }}</label>
|
||
<a-input v-model="row.exHsCode" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.declareElement') }}</label>
|
||
<a-textarea v-model="row.declareElement" :auto-size="{ minRows: 2, maxRows: 3 }" />
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.brand') }}</label>
|
||
<a-input v-model="row.brand" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.brandType') }}</label>
|
||
<a-input v-model="row.brandType" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.model') }}</label>
|
||
<a-input v-model="row.model" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.distribution') }}</label>
|
||
<a-input v-model="row.distribution" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.material') }}</label>
|
||
<a-input v-model="row.material" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.enMaterial') }}</label>
|
||
<a-input v-model="row.enMaterial" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.purpose') }}</label>
|
||
<a-input v-model="row.purpose" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.originCountry') }}</label>
|
||
<a-input v-model="row.originCountry" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__row">
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.category') }}</label>
|
||
<a-input v-model="row.category" allow-clear />
|
||
</div>
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.specifications') }}</label>
|
||
<a-input v-model="row.specifications" allow-clear />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.attributes') }}</label>
|
||
<a-input v-model="row.attributes" allow-clear />
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.salesLink') }}</label>
|
||
<a-input v-model="row.salesLink" allow-clear />
|
||
</div>
|
||
|
||
<div class="h5-po__field">
|
||
<label class="h5-po__label">{{ t('h5.placeOrder.comment') }}</label>
|
||
<a-textarea v-model="row.comment" :auto-size="{ minRows: 2, maxRows: 3 }" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="button" class="h5-po__add-item" @click="addItem">
|
||
+ {{ t('h5.placeOrder.addItem') }}
|
||
</button>
|
||
</section>
|
||
</a-spin>
|
||
|
||
<footer class="h5-po__foot">
|
||
<button
|
||
v-if="!isEditMode"
|
||
type="button"
|
||
class="h5-po__btn is-mock"
|
||
:disabled="submitting"
|
||
@click="fillMockOrder"
|
||
>
|
||
{{ t('h5.placeOrder.mock') }}
|
||
</button>
|
||
<div class="h5-po__foot-main">
|
||
<button
|
||
v-if="step > 0"
|
||
type="button"
|
||
class="h5-po__btn is-ghost"
|
||
:disabled="submitting"
|
||
@click="step -= 1"
|
||
>
|
||
{{ t('h5.placeOrder.prev') }}
|
||
</button>
|
||
<button
|
||
v-if="step === 1"
|
||
type="button"
|
||
class="h5-po__btn is-ai"
|
||
:disabled="submitting"
|
||
@click="openReceiverAiParse"
|
||
>
|
||
{{ t('h5.placeOrder.aiParse') }}
|
||
</button>
|
||
<button
|
||
v-if="step < 2"
|
||
type="button"
|
||
class="h5-po__btn is-primary"
|
||
@click="goNext"
|
||
>
|
||
{{ t('h5.placeOrder.next') }}
|
||
</button>
|
||
<button
|
||
v-else
|
||
type="button"
|
||
class="h5-po__btn is-primary"
|
||
:disabled="submitting"
|
||
@click="submit"
|
||
>
|
||
<span v-if="submitting" class="h5-po__loading" aria-hidden="true" />
|
||
{{
|
||
submitting
|
||
? isEditMode
|
||
? t('h5.placeOrder.saving')
|
||
: t('h5.placeOrder.submitting')
|
||
: isEditMode
|
||
? t('h5.placeOrder.save')
|
||
: t('h5.placeOrder.submit')
|
||
}}
|
||
</button>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
</a-drawer>
|
||
|
||
<a-modal
|
||
v-model:visible="receiverAiVisible"
|
||
:title="t('h5.placeOrder.aiParseTitle')"
|
||
:footer="false"
|
||
unmount-on-close
|
||
width="92%"
|
||
:mask-closable="!receiverParseLoading"
|
||
>
|
||
<div class="h5-po-ai">
|
||
<p class="h5-po-ai__hint">{{ t('h5.placeOrder.aiParseHint') }}</p>
|
||
<div class="h5-po-ai__config">
|
||
<a-select
|
||
v-model="receiverAiProviderKey"
|
||
class="h5-po-ai__select"
|
||
size="small"
|
||
:placeholder="t('h5.placeOrder.aiParseProvider')"
|
||
:loading="receiverAiProvidersLoading"
|
||
allow-search
|
||
:disabled="receiverParseLoading"
|
||
@change="onReceiverAiProviderChange"
|
||
>
|
||
<a-option
|
||
v-for="p in receiverAiEnabledProviders"
|
||
:key="p.providerKey"
|
||
:value="p.providerKey"
|
||
:label="p.displayName || p.providerKey"
|
||
/>
|
||
</a-select>
|
||
<a-select
|
||
v-model="receiverAiModelId"
|
||
class="h5-po-ai__select"
|
||
size="small"
|
||
:placeholder="t('h5.placeOrder.aiParseModel')"
|
||
allow-create
|
||
allow-search
|
||
:disabled="receiverParseLoading"
|
||
@change="saveReceiverAiPrefs"
|
||
>
|
||
<a-option
|
||
v-for="m in receiverAiModelOptions"
|
||
:key="m.modelId"
|
||
:value="m.modelId"
|
||
>
|
||
{{ m.modelId }}{{ m.role ? `(${m.role})` : '' }}
|
||
</a-option>
|
||
</a-select>
|
||
<a-select
|
||
v-model="receiverAiSkillKey"
|
||
class="h5-po-ai__select h5-po-ai__select--skill"
|
||
size="small"
|
||
:placeholder="t('h5.placeOrder.aiParseSkill')"
|
||
:loading="receiverAiSkillsLoading"
|
||
allow-search
|
||
:disabled="receiverParseLoading"
|
||
@change="saveReceiverAiPrefs"
|
||
>
|
||
<a-option
|
||
v-for="s in receiverAiSkills"
|
||
:key="s.skillKey"
|
||
:value="s.skillKey"
|
||
:label="s.name || s.skillKey"
|
||
/>
|
||
</a-select>
|
||
</div>
|
||
<a-textarea
|
||
v-model="receiverPasteText"
|
||
class="h5-po-ai__textarea"
|
||
:auto-size="{ minRows: 5, maxRows: 10 }"
|
||
:placeholder="t('h5.placeOrder.aiParsePlaceholder')"
|
||
:disabled="receiverParseLoading"
|
||
@paste="onReceiverAiPaste"
|
||
/>
|
||
<p
|
||
v-if="receiverParseTip"
|
||
class="h5-po-ai__tip"
|
||
:class="`is-${receiverParseTip.tone}`"
|
||
>
|
||
{{ receiverParseTip.text }}
|
||
</p>
|
||
<div class="h5-po-ai__actions">
|
||
<button
|
||
type="button"
|
||
class="h5-po__btn is-ghost"
|
||
:disabled="receiverParseLoading"
|
||
@click="receiverAiVisible = false"
|
||
>
|
||
{{ t('h5.placeOrder.aiParseCancel') }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="h5-po__btn is-primary"
|
||
:disabled="receiverParseLoading || !receiverPasteText.trim()"
|
||
@click="handleReceiverAiParse"
|
||
>
|
||
<span v-if="receiverParseLoading" class="h5-po__loading" aria-hidden="true" />
|
||
{{
|
||
receiverParseLoading
|
||
? t('h5.placeOrder.aiParseLoading')
|
||
: t('h5.placeOrder.aiParseConfirm')
|
||
}}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:visible="createFailVisible"
|
||
:title="
|
||
createFailInfo?.draftSaved
|
||
? t('h5.placeOrder.failTitleDraft')
|
||
: t('h5.placeOrder.failTitle')
|
||
"
|
||
:footer="false"
|
||
:mask-closable="!createFailCancelling && !createFailSavingDraft"
|
||
unmount-on-close
|
||
width="92%"
|
||
>
|
||
<div v-if="createFailInfo" class="h5-po-fail">
|
||
<div class="h5-po-fail__hero">
|
||
<p class="h5-po-fail__reason">{{ createFailInfo.reason }}</p>
|
||
<p v-if="createFailInfo.tips?.[0]" class="h5-po-fail__tip">
|
||
<span class="h5-po-fail__tip-label">{{ t('h5.placeOrder.failHow') }}</span>
|
||
{{ createFailInfo.tips[0] }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="createFailInfo.draftSaved" class="h5-po-fail__status">
|
||
<span>{{ t('h5.placeOrder.failDraftNote') }}</span>
|
||
<span v-if="createFailInfo.serviceNo" class="h5-po-fail__sno">
|
||
{{ createFailInfo.serviceNo }}
|
||
</span>
|
||
</div>
|
||
|
||
<details
|
||
v-if="createFailInfo.rawReason && createFailInfo.rawReason !== createFailInfo.reason"
|
||
class="h5-po-fail__details"
|
||
>
|
||
<summary>{{ t('h5.placeOrder.failRaw') }}</summary>
|
||
<p class="h5-po-fail__raw">{{ createFailInfo.rawReason }}</p>
|
||
</details>
|
||
|
||
<div class="h5-po-fail__actions">
|
||
<button
|
||
type="button"
|
||
class="h5-po__btn is-ghost"
|
||
:disabled="createFailCancelling || createFailSavingDraft"
|
||
@click="handleCreateFailCancel"
|
||
>
|
||
{{
|
||
createFailInfo.draftSaved
|
||
? t('h5.placeOrder.failCancelDraft')
|
||
: t('h5.placeOrder.failAbandon')
|
||
}}
|
||
</button>
|
||
<button
|
||
v-if="!createFailInfo.draftSaved"
|
||
type="button"
|
||
class="h5-po__btn is-ghost"
|
||
:disabled="createFailCancelling || createFailSavingDraft"
|
||
@click="handleCreateFailSaveDraft"
|
||
>
|
||
{{
|
||
createFailSavingDraft
|
||
? t('h5.placeOrder.submitting')
|
||
: t('h5.placeOrder.failSaveDraft')
|
||
}}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="h5-po__btn is-primary"
|
||
:disabled="createFailCancelling || createFailSavingDraft"
|
||
@click="handleCreateFailPrimary"
|
||
>
|
||
{{
|
||
createFailInfo.draftSaved
|
||
? t('h5.placeOrder.failGoDraft')
|
||
: t('h5.placeOrder.failContinue')
|
||
}}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<H5SelectDeclarationSkuSheet
|
||
v-model:visible="declarationSkuVisible"
|
||
@select="handleDeclarationSkuSelect"
|
||
/>
|
||
<H5ExportCustomsGuideSheet v-model:visible="exportCustomsGuideVisible" />
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { Message, Modal } from '@arco-design/web-vue';
|
||
import { IconClose, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
||
import {
|
||
getOrderDetail,
|
||
postOrderDelete,
|
||
postOrderParcelCreate,
|
||
postOrderReceiverParseAddress,
|
||
postOrderUpdate,
|
||
} from '@/api/order';
|
||
import { listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai';
|
||
import { createSenderAddress, getSenderAddressList } from '@/api/order/senderAddress';
|
||
import { getExportQualificationList } from '@/api/customer/exportQualification';
|
||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||
import { getDictListAll } from '@/api/system/dict';
|
||
import { getCountryList } from '@/api/system/country';
|
||
import { getWarehouseList } from '@/api/system/warehouse';
|
||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||
import {
|
||
mapWarehouseSkuToDeclarationFields,
|
||
normalizeSkuImageForStorage,
|
||
pickField,
|
||
} from '@/utils/warehouseListMapper';
|
||
import {
|
||
compareWarehouseSort,
|
||
formatWarehouseShipAddress,
|
||
formatWarehouseShipRecipient,
|
||
} from '@/utils/warehouseShipAddress';
|
||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||
import { h5Navigate } from '@/utils/device';
|
||
import { isCustomerContactRequiredError } from '@/utils/customerContactGate';
|
||
import {
|
||
buildParcelCreateFailGuide,
|
||
type ParcelCreateFailGuide,
|
||
} from '@/utils/parcelCreateFailGuide';
|
||
import {
|
||
loadDomesticPickupProductOptions,
|
||
type DomesticPickupProductOption,
|
||
} from '@/utils/domesticPickupProducts';
|
||
import {
|
||
providerModelTags,
|
||
type AiProviderItem,
|
||
} from '@/views/system/aiAssistant/providerCatalog';
|
||
import H5SelectDeclarationSkuSheet from './H5SelectDeclarationSkuSheet.vue';
|
||
import H5ExportCustomsGuideSheet from './H5ExportCustomsGuideSheet.vue';
|
||
|
||
function isSenderRequiredError(error: unknown): boolean {
|
||
const msg = String(
|
||
(error as any)?.response?.data?.message ||
|
||
(error as any)?.response?.data?.Message ||
|
||
(error as any)?.message ||
|
||
error ||
|
||
'',
|
||
);
|
||
return msg.includes('发件人姓名不能为空') || /sender\s*name/i.test(msg);
|
||
}
|
||
|
||
defineOptions({ name: 'H5PlaceOrderSheet' });
|
||
|
||
const PRICE_CHECK_ORDER_DRAFT_KEY = 'oms_price_check_order_draft';
|
||
|
||
const props = defineProps<{
|
||
visible: boolean;
|
||
/** 查价带入草稿;不传则尝试读本地缓存 */
|
||
draft?: Record<string, any> | null;
|
||
/** 传入则为编辑模式:加载详情并走 /order/update */
|
||
orderId?: number | string | null;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
'update:visible': [value: boolean];
|
||
success: [];
|
||
}>();
|
||
|
||
const { t } = useI18n();
|
||
const router = useRouter();
|
||
|
||
const isEditMode = computed(() => {
|
||
const id = Number(props.orderId);
|
||
return Number.isFinite(id) && id > 0;
|
||
});
|
||
/** 详情是否已有发件人(更新接口不能新建 sender) */
|
||
const editHasSender = ref(false);
|
||
const countryIdToCode = ref<Record<string, string>>({});
|
||
|
||
const step = ref(0);
|
||
const optionsLoading = ref(false);
|
||
const senderLoading = ref(false);
|
||
const showSenderForm = ref(false);
|
||
const savingSender = ref(false);
|
||
const showReceiverMore = ref(false);
|
||
const RECEIVER_AI_PREFS_KEY = 'oms.placeOrder.receiverAi';
|
||
const RECEIVER_AI_SCENE = 'receiverAddressParse';
|
||
const RECEIVER_AI_DEFAULT_SKILL_KEY = 'oms-receiver-address-parse';
|
||
const SAMPLE_EXPORT_CUSTOMS_MODE_CODE = '9610Simple';
|
||
const EXPORT_CUSTOMS_MODE_9610_SUMMARY = '9610Summary';
|
||
const receiverAiVisible = ref(false);
|
||
const receiverPasteText = ref('');
|
||
const receiverParseLoading = ref(false);
|
||
const receiverParseTip = ref<{ tone: 'ok' | 'warn' | 'danger'; text: string } | null>(null);
|
||
const receiverAiProviders = ref<AiProviderItem[]>([]);
|
||
const receiverAiSkills = ref<AiSkillItem[]>([]);
|
||
const receiverAiProviderKey = ref('');
|
||
const receiverAiModelId = ref('');
|
||
const receiverAiSkillKey = ref(RECEIVER_AI_DEFAULT_SKILL_KEY);
|
||
const receiverAiProvidersLoading = ref(false);
|
||
const receiverAiSkillsLoading = ref(false);
|
||
const exportCustomsGuideVisible = ref(false);
|
||
const declarationSkuVisible = ref(false);
|
||
const exportQualificationLoading = ref(false);
|
||
const exportQualificationOptions = ref<Array<{ id: number; label: string; isDefault: boolean }>>([]);
|
||
const exportCustomsModeOptions = ref<{ value: string; label: string }[]>([]);
|
||
const receiverAddressLoading = ref(false);
|
||
const receiverAddressOptions = ref<
|
||
Array<{ id: number; label: string; isDefault: boolean; data: Record<string, string> }>
|
||
>([]);
|
||
const selectedReceiverAddressId = ref<number | undefined>();
|
||
const savingReceiverAddress = ref(false);
|
||
|
||
const receiverAiEnabledProviders = computed(() =>
|
||
receiverAiProviders.value.filter((p) => p.enabled && Boolean(p.apiKey)),
|
||
);
|
||
const receiverAiSelectedProvider = computed(
|
||
() =>
|
||
receiverAiEnabledProviders.value.find(
|
||
(p) => p.providerKey === receiverAiProviderKey.value,
|
||
) ?? null,
|
||
);
|
||
const receiverAiModelOptions = computed(() => {
|
||
const p = receiverAiSelectedProvider.value;
|
||
if (!p) return [];
|
||
const tags = providerModelTags(p.providerType);
|
||
const extra = String(p.apiSecret || '').trim();
|
||
if (extra && extra !== '******' && !tags.some((m) => m.modelId === extra)) {
|
||
return [...tags, { modelId: extra, role: '自定义' }];
|
||
}
|
||
return tags;
|
||
});
|
||
/** 申报品「更多」展开状态,按 item._key */
|
||
const openItemMoreKeys = ref<Set<string>>(new Set());
|
||
const senderForm = reactive({
|
||
senderName: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
streetAddress1: '',
|
||
countryCode: 'CN',
|
||
});
|
||
const submitting = ref(false);
|
||
const handoffTip = ref('');
|
||
const selectedSenderId = ref<number | undefined>();
|
||
/** 当前步骤校验失败后标红;申报步骤始终即时标红 */
|
||
const showFieldErrors = ref(false);
|
||
|
||
const drawerStyle = {
|
||
borderRadius: '20px 20px 0 0',
|
||
overflow: 'hidden',
|
||
maxHeight: 'min(92vh, 780px)',
|
||
};
|
||
|
||
const steps = computed(() => [
|
||
{ key: 'basic', label: t('h5.placeOrder.stepBasic') },
|
||
{ key: 'receiver', label: t('h5.placeOrder.stepReceiver') },
|
||
{ key: 'declaration', label: t('h5.placeOrder.stepDeclaration') },
|
||
]);
|
||
|
||
const productOptions = ref<{ value: string; label: string; productId?: number }[]>([]);
|
||
const cargoOptions = ref<{ value: string; label: string }[]>([]);
|
||
const warehouseOptions = ref<
|
||
{
|
||
value: string;
|
||
label: string;
|
||
recipient?: string;
|
||
contactPhone?: string;
|
||
shippingAddress?: string;
|
||
sort?: number | null;
|
||
remark?: string;
|
||
}[]
|
||
>([]);
|
||
const countryOptions = ref<{ value: string; label: string }[]>([]);
|
||
const currencyOptions = ref<{ value: string; label: string }[]>([]);
|
||
const senderOptions = ref<{ id: number; label: string; data: Record<string, string> }[]>([]);
|
||
const pickupAddressOptions = ref<{ id: number; label: string }[]>([]);
|
||
const pickupAddressLoading = ref(false);
|
||
const selectedPickupAddressId = ref<number | undefined>();
|
||
const carrierServiceOptions = ref<DomesticPickupProductOption[]>([]);
|
||
const additionalServiceDictRows = ref<Array<{ dictCode: string; dictName: string }>>([]);
|
||
const productAdditionalServiceOptions = ref<{ value: string; label: string }[]>([]);
|
||
const selectedAdditionalServices = ref<string[]>([]);
|
||
|
||
function parseAdditionalServiceCodes(raw: unknown): string[] {
|
||
if (Array.isArray(raw)) {
|
||
return raw.map((x) => String(x ?? '').trim()).filter(Boolean);
|
||
}
|
||
if (raw == null || raw === '') return [];
|
||
const s = String(raw).trim();
|
||
if (!s) return [];
|
||
try {
|
||
const parsed = JSON.parse(s);
|
||
if (Array.isArray(parsed)) {
|
||
return parsed.map((x) => String(x ?? '').trim()).filter(Boolean);
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return s
|
||
.split(/[,,]/)
|
||
.map((x) => x.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function labelAdditionalService(code: string) {
|
||
const hit = additionalServiceDictRows.value.find((r) => r.dictCode === code);
|
||
return hit?.dictName || code;
|
||
}
|
||
|
||
async function loadProductAdditionalServices(productCode: string) {
|
||
const code = trim(productCode);
|
||
if (!code) {
|
||
productAdditionalServiceOptions.value = [];
|
||
selectedAdditionalServices.value = [];
|
||
return;
|
||
}
|
||
const selected = productOptions.value.find((opt) => opt.value === code);
|
||
const productId = selected?.productId;
|
||
if (!productId) {
|
||
productAdditionalServiceOptions.value = [];
|
||
selectedAdditionalServices.value = [];
|
||
return;
|
||
}
|
||
try {
|
||
const detail = await getSalesProductDetail(productId);
|
||
const codes = parseAdditionalServiceCodes(
|
||
detail?.additionalServices ?? detail?.AdditionalServices,
|
||
);
|
||
productAdditionalServiceOptions.value = codes.map((c) => ({
|
||
value: c,
|
||
label: labelAdditionalService(c),
|
||
}));
|
||
selectedAdditionalServices.value = selectedAdditionalServices.value.filter((c) =>
|
||
codes.includes(c),
|
||
);
|
||
} catch {
|
||
productAdditionalServiceOptions.value = [];
|
||
selectedAdditionalServices.value = [];
|
||
}
|
||
}
|
||
|
||
type DeclareItem = {
|
||
_key: string;
|
||
cnName: string;
|
||
enName: string;
|
||
unitPrice: string;
|
||
unitWeight: string;
|
||
quantity: string;
|
||
amount: string;
|
||
weight: string;
|
||
sku: string;
|
||
unit: string;
|
||
imHsCode: string;
|
||
exHsCode: string;
|
||
declareElement: string;
|
||
skuImage: string;
|
||
brand: string;
|
||
brandType: string;
|
||
model: string;
|
||
material: string;
|
||
enMaterial: string;
|
||
purpose: string;
|
||
originCountry: string;
|
||
category: string;
|
||
specifications: string;
|
||
salesLink: string;
|
||
comment: string;
|
||
isMagnetic: boolean;
|
||
distribution: string;
|
||
attributes: string;
|
||
};
|
||
|
||
let itemKeySeq = 0;
|
||
function createEmptyItem(): DeclareItem {
|
||
itemKeySeq += 1;
|
||
return {
|
||
_key: `item-${itemKeySeq}`,
|
||
cnName: '',
|
||
enName: '',
|
||
unitPrice: '',
|
||
unitWeight: '',
|
||
quantity: '1',
|
||
amount: '',
|
||
weight: '',
|
||
sku: '',
|
||
unit: 'pcs',
|
||
imHsCode: '',
|
||
exHsCode: '',
|
||
declareElement: '',
|
||
skuImage: '',
|
||
brand: '',
|
||
brandType: '',
|
||
model: '',
|
||
material: '',
|
||
enMaterial: '',
|
||
purpose: '',
|
||
originCountry: '',
|
||
category: '',
|
||
specifications: '',
|
||
salesLink: '',
|
||
comment: '',
|
||
isMagnetic: false,
|
||
distribution: '',
|
||
attributes: '',
|
||
};
|
||
}
|
||
|
||
const items = ref<DeclareItem[]>([createEmptyItem()]);
|
||
|
||
const form = reactive({
|
||
clientNo: '',
|
||
productCode: '',
|
||
cargoAttribute: '',
|
||
exportCustomsMode: '9610Simple',
|
||
exportQualificationId: '',
|
||
platformCode: '',
|
||
warehouseId: '',
|
||
inboundMethod: 'selfShip',
|
||
expectPickupTime: '',
|
||
carrierService: '',
|
||
length: '10',
|
||
width: '10',
|
||
height: '10',
|
||
actualWeight: '',
|
||
quantity: '1',
|
||
declaredCurrency: 'USD',
|
||
isCod: false,
|
||
receiver: {
|
||
recipientsName: '',
|
||
phone: '',
|
||
countryCode: '',
|
||
province: '',
|
||
city: '',
|
||
streetAddress1: '',
|
||
zipCode: '',
|
||
companyName: '',
|
||
telePhone: '',
|
||
email: '',
|
||
streetNumber: '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
taxType: '0',
|
||
taxNo: '',
|
||
certificateType: '0',
|
||
certificateNo: '',
|
||
eori: '',
|
||
vat: '',
|
||
},
|
||
sender: {
|
||
senderName: '',
|
||
company: '',
|
||
phone: '',
|
||
telePhone: '',
|
||
email: '',
|
||
countryCode: '',
|
||
province: '',
|
||
city: '',
|
||
zipCode: '',
|
||
streetAddress1: '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
houseNumber: '',
|
||
},
|
||
});
|
||
|
||
function resolveExportCustomsMode9610Simple() {
|
||
const opts = exportCustomsModeOptions.value;
|
||
const byCode = opts.find(
|
||
(o) =>
|
||
o.value === SAMPLE_EXPORT_CUSTOMS_MODE_CODE ||
|
||
o.value.toLowerCase() === '9610simple',
|
||
);
|
||
if (byCode) return byCode.value;
|
||
const byLabel = opts.find((o) => /9610.*简易|简易.*9610/i.test(o.label));
|
||
if (byLabel) return byLabel.value;
|
||
return SAMPLE_EXPORT_CUSTOMS_MODE_CODE;
|
||
}
|
||
|
||
function isExportCustomsMode9610Summary(mode: unknown) {
|
||
return String(mode ?? '').trim().toLowerCase() === EXPORT_CUSTOMS_MODE_9610_SUMMARY.toLowerCase();
|
||
}
|
||
|
||
const needExportQualification = computed(() =>
|
||
isExportCustomsMode9610Summary(form.exportCustomsMode),
|
||
);
|
||
|
||
function skuImageSrc(url?: string | null) {
|
||
return resolveOmsAssetUrl(url || '') || '';
|
||
}
|
||
|
||
type WarehouseOption = (typeof warehouseOptions.value)[number];
|
||
|
||
const rbacUserStore = useRbacUserStore();
|
||
const currentCustomerCode = ref('');
|
||
|
||
async function loadCurrentCustomerCode() {
|
||
try {
|
||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||
const data = await resolveCurrentCustomer({ mapId, account });
|
||
currentCustomerCode.value = String(data?.customerCode ?? '').trim();
|
||
} catch {
|
||
currentCustomerCode.value = '';
|
||
}
|
||
}
|
||
|
||
function displayWarehouseRecipient(opt?: WarehouseOption) {
|
||
return formatWarehouseShipRecipient(opt?.recipient, currentCustomerCode.value);
|
||
}
|
||
|
||
function displayWarehouseShippingAddress(opt?: WarehouseOption) {
|
||
return formatWarehouseShipAddress(opt?.shippingAddress, currentCustomerCode.value);
|
||
}
|
||
|
||
function warehouseHasAddress(opt?: WarehouseOption) {
|
||
if (!opt) return false;
|
||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress);
|
||
}
|
||
|
||
const warehousesWithShipAddress = computed(() =>
|
||
warehouseOptions.value.filter((opt) => warehouseHasAddress(opt)),
|
||
);
|
||
|
||
async function copyWarehouseShipAddress(opt?: WarehouseOption) {
|
||
if (!opt || !warehouseHasAddress(opt)) {
|
||
Message.warning(t('h5.placeOrder.selfShipAddressEmpty'));
|
||
return;
|
||
}
|
||
const text = [
|
||
opt.label || '',
|
||
`${t('h5.placeOrder.selfShipRecipient')}:${displayWarehouseRecipient(opt)}`,
|
||
`${t('h5.placeOrder.selfShipPhone')}:${opt.contactPhone || '-'}`,
|
||
`${t('h5.placeOrder.selfShipAddress')}:${displayWarehouseShippingAddress(opt)}`,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n');
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
Message.success(t('h5.placeOrder.selfShipCopySuccess'));
|
||
} catch {
|
||
Message.error(t('h5.orders.copyFailed'));
|
||
}
|
||
}
|
||
|
||
function trim(v: unknown) {
|
||
return String(v ?? '').trim();
|
||
}
|
||
|
||
function parseNum(v: unknown) {
|
||
const n = parseFloat(String(v ?? '').trim());
|
||
return Number.isFinite(n) ? n : undefined;
|
||
}
|
||
|
||
function syncItemDerived(row: DeclareItem) {
|
||
const unitPrice = parseNum(row.unitPrice);
|
||
const unitWeight = parseNum(row.unitWeight);
|
||
const quantity = parseNum(row.quantity);
|
||
row.amount =
|
||
unitPrice != null && quantity != null ? String(Number((unitPrice * quantity).toFixed(4))) : '';
|
||
row.weight =
|
||
unitWeight != null && quantity != null ? String(Number((unitWeight * quantity).toFixed(4))) : '';
|
||
}
|
||
|
||
function formatDerivedNum(value: number) {
|
||
if (Number.isInteger(value)) return String(value);
|
||
return value.toFixed(4).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
|
||
}
|
||
|
||
function calcDeclaredTotal() {
|
||
let total = 0;
|
||
let hasValue = false;
|
||
for (const row of items.value) {
|
||
const amount = parseNum(row.amount);
|
||
if (amount != null) {
|
||
total += amount;
|
||
hasValue = true;
|
||
}
|
||
}
|
||
return hasValue ? total : undefined;
|
||
}
|
||
|
||
const declaredTotalInput = ref('');
|
||
const declaredTotalInputFocused = ref(false);
|
||
|
||
function syncDeclaredTotalInputFromRows() {
|
||
if (declaredTotalInputFocused.value) return;
|
||
const total = calcDeclaredTotal();
|
||
declaredTotalInput.value = total == null ? '' : formatDerivedNum(total);
|
||
}
|
||
|
||
function applyDeclaredTotalReverse(raw: string) {
|
||
const total = parseNum(raw);
|
||
if (total == null || total < 0) {
|
||
syncDeclaredTotalInputFromRows();
|
||
return;
|
||
}
|
||
const list = items.value;
|
||
if (!list.length) return;
|
||
|
||
const othersSum = list.slice(1).reduce((sum, row) => sum + (parseNum(row.amount) ?? 0), 0);
|
||
let firstAmount = list.length === 1 ? total : total - othersSum;
|
||
if (firstAmount < 0) firstAmount = 0;
|
||
|
||
const row = list[0];
|
||
let qty = parseNum(row.quantity);
|
||
if (qty == null || qty <= 0) {
|
||
row.quantity = '1';
|
||
qty = 1;
|
||
}
|
||
row.amount = formatDerivedNum(firstAmount);
|
||
row.unitPrice = formatDerivedNum(firstAmount / qty);
|
||
declaredTotalInput.value = formatDerivedNum(calcDeclaredTotal() ?? firstAmount);
|
||
}
|
||
|
||
function handleDeclaredTotalBlur() {
|
||
declaredTotalInputFocused.value = false;
|
||
applyDeclaredTotalReverse(declaredTotalInput.value);
|
||
}
|
||
|
||
function handleDeclaredTotalChange(val: string | number) {
|
||
declaredTotalInput.value = String(val ?? '');
|
||
if (!declaredTotalInputFocused.value) {
|
||
applyDeclaredTotalReverse(declaredTotalInput.value);
|
||
}
|
||
}
|
||
|
||
function addItem() {
|
||
items.value.push(createEmptyItem());
|
||
}
|
||
|
||
function removeItem(idx: number) {
|
||
if (items.value.length <= 1) return;
|
||
const removed = items.value[idx];
|
||
items.value.splice(idx, 1);
|
||
if (removed?._key) {
|
||
const next = new Set(openItemMoreKeys.value);
|
||
next.delete(removed._key);
|
||
openItemMoreKeys.value = next;
|
||
}
|
||
}
|
||
|
||
function isItemMoreOpen(key: string) {
|
||
return openItemMoreKeys.value.has(key);
|
||
}
|
||
|
||
function toggleItemMore(key: string) {
|
||
const next = new Set(openItemMoreKeys.value);
|
||
if (next.has(key)) next.delete(key);
|
||
else next.add(key);
|
||
openItemMoreKeys.value = next;
|
||
}
|
||
|
||
function validateDeclareItem(row: DeclareItem): string | null {
|
||
if (!trim(row.enName)) return t('h5.placeOrder.err.enName');
|
||
if (parseNum(row.unitPrice) == null) return t('h5.placeOrder.err.unitPrice');
|
||
if (parseNum(row.unitWeight) == null) return t('h5.placeOrder.err.unitWeight');
|
||
if (parseNum(row.quantity) == null) return t('h5.placeOrder.err.qty');
|
||
return null;
|
||
}
|
||
|
||
function collectFieldErrors(idx: number): Record<string, string> {
|
||
const errors: Record<string, string> = {};
|
||
if (idx === 0) {
|
||
if (!trim(form.sender.senderName)) {
|
||
errors.sender = senderOptions.value.length
|
||
? t('h5.placeOrder.err.sender')
|
||
: t('h5.placeOrder.err.senderEmpty');
|
||
}
|
||
if (!trim(form.clientNo)) errors.clientNo = t('h5.placeOrder.err.clientNo');
|
||
if (!trim(form.productCode)) errors.productCode = t('h5.placeOrder.err.product');
|
||
if (!trim(form.cargoAttribute)) errors.cargoAttribute = t('h5.placeOrder.err.cargo');
|
||
if (!trim(form.warehouseId) || parseNum(form.warehouseId) == null) {
|
||
errors.warehouseId = t('h5.placeOrder.err.warehouse');
|
||
}
|
||
if (parseNum(form.actualWeight) == null) errors.actualWeight = t('h5.placeOrder.err.weight');
|
||
if ((parseNum(form.length) ?? 0) <= 0) errors.length = t('h5.placeOrder.err.length');
|
||
if ((parseNum(form.width) ?? 0) <= 0) errors.width = t('h5.placeOrder.err.width');
|
||
if ((parseNum(form.height) ?? 0) <= 0) errors.height = t('h5.placeOrder.err.height');
|
||
if (!trim(form.quantity) || (parseNum(form.quantity) ?? 0) <= 0) {
|
||
errors.quantity = t('h5.placeOrder.err.packageQty');
|
||
}
|
||
if (needExportQualification.value) {
|
||
if (!parseNum(form.exportQualificationId)) {
|
||
errors.exportQualificationId = t('h5.placeOrder.err.exportQualification');
|
||
}
|
||
if (!trim(form.platformCode)) {
|
||
errors.platformCode = t('h5.placeOrder.err.platformCode');
|
||
}
|
||
}
|
||
if (form.inboundMethod === 'doorPickup') {
|
||
if (!selectedPickupAddressId.value) {
|
||
errors.sender = t('h5.placeOrder.err.inboundPickupSender');
|
||
}
|
||
if (!trim(form.expectPickupTime)) {
|
||
errors.expectPickupTime = t('h5.placeOrder.err.expectPickupTime');
|
||
}
|
||
}
|
||
}
|
||
if (idx === 1) {
|
||
if (!trim(form.receiver.recipientsName)) {
|
||
errors['receiver.recipientsName'] = t('h5.placeOrder.err.receiverName');
|
||
}
|
||
if (!trim(form.receiver.phone)) errors['receiver.phone'] = t('h5.placeOrder.err.receiverPhone');
|
||
if (!trim(form.receiver.countryCode)) {
|
||
errors['receiver.countryCode'] = t('h5.placeOrder.err.country');
|
||
}
|
||
if (!trim(form.receiver.streetAddress1)) {
|
||
errors['receiver.streetAddress1'] = t('h5.placeOrder.err.address');
|
||
}
|
||
if (!trim(form.receiver.zipCode)) errors['receiver.zipCode'] = t('h5.placeOrder.err.zip');
|
||
}
|
||
if (idx === 2) {
|
||
if (!trim(form.declaredCurrency)) errors.declaredCurrency = t('h5.placeOrder.err.currency');
|
||
if (!items.value.length) errors.itemsEmpty = t('h5.placeOrder.err.itemsEmpty');
|
||
items.value.forEach((row, i) => {
|
||
if (!trim(row.enName)) errors[`items.${i}.enName`] = t('h5.placeOrder.err.enName');
|
||
if (parseNum(row.unitPrice) == null) {
|
||
errors[`items.${i}.unitPrice`] = t('h5.placeOrder.err.unitPrice');
|
||
}
|
||
if (parseNum(row.unitWeight) == null) {
|
||
errors[`items.${i}.unitWeight`] = t('h5.placeOrder.err.unitWeight');
|
||
}
|
||
if (parseNum(row.quantity) == null) errors[`items.${i}.quantity`] = t('h5.placeOrder.err.qty');
|
||
});
|
||
}
|
||
return errors;
|
||
}
|
||
|
||
function isDeclareRowStarted(row: DeclareItem) {
|
||
return Boolean(
|
||
trim(row.enName) ||
|
||
trim(row.cnName) ||
|
||
trim(row.unitPrice) ||
|
||
trim(row.unitWeight) ||
|
||
trim(row.sku) ||
|
||
trim(row.skuImage),
|
||
);
|
||
}
|
||
|
||
const activeFieldErrors = computed(() => {
|
||
if (step.value === 2) {
|
||
const all = collectFieldErrors(2);
|
||
if (showFieldErrors.value) return all;
|
||
const filtered: Record<string, string> = {};
|
||
Object.entries(all).forEach(([key, msg]) => {
|
||
if (key === 'declaredCurrency') {
|
||
filtered[key] = msg;
|
||
return;
|
||
}
|
||
const match = /^items\.(\d+)\./.exec(key);
|
||
if (!match) return;
|
||
const row = items.value[Number(match[1])];
|
||
if (row && isDeclareRowStarted(row)) filtered[key] = msg;
|
||
});
|
||
return filtered;
|
||
}
|
||
if (!showFieldErrors.value) return {} as Record<string, string>;
|
||
return collectFieldErrors(step.value);
|
||
});
|
||
|
||
const declarationLiveHint = computed(() => {
|
||
if (step.value !== 2 || showFieldErrors.value) return '';
|
||
return firstFieldErrorMessage(2) || '';
|
||
});
|
||
|
||
function fieldHasError(key: string) {
|
||
return Boolean(activeFieldErrors.value[key]);
|
||
}
|
||
|
||
function fieldError(key: string) {
|
||
return activeFieldErrors.value[key] || '';
|
||
}
|
||
|
||
function firstFieldErrorMessage(idx: number): string | null {
|
||
if (idx === 2) {
|
||
if (!trim(form.declaredCurrency)) return t('h5.placeOrder.err.currency');
|
||
if (!items.value.length) return t('h5.placeOrder.err.itemsEmpty');
|
||
for (let i = 0; i < items.value.length; i += 1) {
|
||
const msg = validateDeclareItem(items.value[i]);
|
||
if (msg) return t('h5.placeOrder.err.itemLine', { n: i + 1, msg });
|
||
}
|
||
return null;
|
||
}
|
||
const errors = collectFieldErrors(idx);
|
||
return Object.values(errors)[0] || null;
|
||
}
|
||
|
||
function resetForm() {
|
||
step.value = 0;
|
||
handoffTip.value = '';
|
||
showFieldErrors.value = false;
|
||
showReceiverMore.value = false;
|
||
openItemMoreKeys.value = new Set();
|
||
selectedSenderId.value = undefined;
|
||
items.value = [createEmptyItem()];
|
||
form.clientNo = '';
|
||
form.productCode = '';
|
||
form.cargoAttribute = '';
|
||
form.exportCustomsMode = resolveExportCustomsMode9610Simple();
|
||
form.exportQualificationId = '';
|
||
form.platformCode = '';
|
||
form.warehouseId = '';
|
||
form.inboundMethod = 'selfShip';
|
||
form.expectPickupTime = '';
|
||
form.carrierService = '';
|
||
form.length = '10';
|
||
form.width = '10';
|
||
form.height = '10';
|
||
form.actualWeight = '';
|
||
form.quantity = '1';
|
||
form.declaredCurrency = 'USD';
|
||
declaredTotalInput.value = '';
|
||
declaredTotalInputFocused.value = false;
|
||
selectedAdditionalServices.value = [];
|
||
productAdditionalServiceOptions.value = [];
|
||
selectedReceiverAddressId.value = undefined;
|
||
savingReceiverAddress.value = false;
|
||
declarationSkuVisible.value = false;
|
||
exportCustomsGuideVisible.value = false;
|
||
Object.keys(form.receiver).forEach((k) => {
|
||
(form.receiver as any)[k] = k === 'taxType' || k === 'certificateType' ? '0' : '';
|
||
});
|
||
Object.keys(form.sender).forEach((k) => {
|
||
(form.sender as any)[k] = '';
|
||
});
|
||
}
|
||
|
||
function ensureClientNo() {
|
||
if (trim(form.clientNo)) return;
|
||
const now = new Date();
|
||
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||
form.clientNo = `C${date}${Math.random().toString().slice(2, 8)}`;
|
||
}
|
||
|
||
function validateStep(idx: number): string | null {
|
||
return firstFieldErrorMessage(idx);
|
||
}
|
||
|
||
function goNext() {
|
||
const err = validateStep(step.value);
|
||
if (err) {
|
||
showFieldErrors.value = true;
|
||
if (!trim(form.sender.senderName) && !senderOptions.value.length) {
|
||
showSenderForm.value = true;
|
||
}
|
||
Message.warning(err);
|
||
return;
|
||
}
|
||
showFieldErrors.value = false;
|
||
step.value += 1;
|
||
}
|
||
|
||
function loadReceiverAiPrefs() {
|
||
try {
|
||
const raw = localStorage.getItem(RECEIVER_AI_PREFS_KEY);
|
||
if (!raw) return;
|
||
const data = JSON.parse(raw) as {
|
||
providerKey?: string;
|
||
modelId?: string;
|
||
skillKey?: string;
|
||
};
|
||
if (data.providerKey) receiverAiProviderKey.value = data.providerKey;
|
||
if (data.modelId) receiverAiModelId.value = data.modelId;
|
||
if (data.skillKey) receiverAiSkillKey.value = data.skillKey;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
function saveReceiverAiPrefs() {
|
||
try {
|
||
localStorage.setItem(
|
||
RECEIVER_AI_PREFS_KEY,
|
||
JSON.stringify({
|
||
providerKey: receiverAiProviderKey.value,
|
||
modelId: receiverAiModelId.value,
|
||
skillKey: receiverAiSkillKey.value,
|
||
}),
|
||
);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
function onReceiverAiProviderChange() {
|
||
const opts = receiverAiModelOptions.value;
|
||
if (!opts.some((m) => m.modelId === receiverAiModelId.value)) {
|
||
receiverAiModelId.value = opts[0]?.modelId ?? '';
|
||
}
|
||
saveReceiverAiPrefs();
|
||
}
|
||
|
||
async function loadReceiverAiProviders() {
|
||
receiverAiProvidersLoading.value = true;
|
||
try {
|
||
const items = await listAiProviders();
|
||
receiverAiProviders.value = (items ?? []) as AiProviderItem[];
|
||
if (!receiverAiProviderKey.value) {
|
||
const def =
|
||
receiverAiEnabledProviders.value.find((p) => p.isDefault) ??
|
||
receiverAiEnabledProviders.value[0];
|
||
if (def) receiverAiProviderKey.value = def.providerKey;
|
||
}
|
||
if (
|
||
receiverAiProviderKey.value &&
|
||
!receiverAiEnabledProviders.value.some(
|
||
(p) => p.providerKey === receiverAiProviderKey.value,
|
||
)
|
||
) {
|
||
receiverAiProviderKey.value =
|
||
receiverAiEnabledProviders.value[0]?.providerKey ?? '';
|
||
}
|
||
onReceiverAiProviderChange();
|
||
} catch (err: unknown) {
|
||
Message.error(err instanceof Error ? err.message : t('h5.placeOrder.aiParseFailed'));
|
||
} finally {
|
||
receiverAiProvidersLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadReceiverAiSkills() {
|
||
receiverAiSkillsLoading.value = true;
|
||
try {
|
||
receiverAiSkills.value = await listAiSkills({
|
||
scene: RECEIVER_AI_SCENE,
|
||
includeContent: false,
|
||
enabledOnly: true,
|
||
});
|
||
if (
|
||
!receiverAiSkills.value.some((s) => s.skillKey === receiverAiSkillKey.value) &&
|
||
receiverAiSkills.value[0]
|
||
) {
|
||
receiverAiSkillKey.value = receiverAiSkills.value[0].skillKey;
|
||
} else if (!receiverAiSkillKey.value) {
|
||
receiverAiSkillKey.value = RECEIVER_AI_DEFAULT_SKILL_KEY;
|
||
}
|
||
saveReceiverAiPrefs();
|
||
} catch {
|
||
receiverAiSkills.value = [];
|
||
} finally {
|
||
receiverAiSkillsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function ensureReceiverAiReady() {
|
||
if (!receiverAiProviderKey.value) {
|
||
Message.warning(t('h5.placeOrder.aiParseNeedProvider'));
|
||
return false;
|
||
}
|
||
if (!receiverAiModelId.value) {
|
||
Message.warning(t('h5.placeOrder.aiParseNeedModel'));
|
||
return false;
|
||
}
|
||
if (!receiverAiSkillKey.value) {
|
||
Message.warning(t('h5.placeOrder.aiParseNeedSkill'));
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function openReceiverAiParse() {
|
||
receiverParseTip.value = null;
|
||
loadReceiverAiPrefs();
|
||
void Promise.all([loadReceiverAiProviders(), loadReceiverAiSkills()]);
|
||
receiverAiVisible.value = true;
|
||
}
|
||
|
||
function onReceiverAiPaste() {
|
||
nextTick(() => {
|
||
if (trim(receiverPasteText.value)) {
|
||
void handleReceiverAiParse();
|
||
}
|
||
});
|
||
}
|
||
|
||
function applyParsedReceiver(parsed: Record<string, string | undefined>) {
|
||
const assignKeys = [
|
||
'recipientsName',
|
||
'companyName',
|
||
'phone',
|
||
'telePhone',
|
||
'email',
|
||
'countryCode',
|
||
'province',
|
||
'city',
|
||
'streetNumber',
|
||
'streetAddress1',
|
||
'streetAddress2',
|
||
'streetAddress3',
|
||
'zipCode',
|
||
] as const;
|
||
for (const key of assignKeys) {
|
||
const val = trim(parsed[key] || '');
|
||
if (val) {
|
||
(form.receiver as Record<string, string>)[key] = val;
|
||
}
|
||
}
|
||
const needMore = Boolean(
|
||
trim(parsed.companyName || '') ||
|
||
trim(parsed.telePhone || '') ||
|
||
trim(parsed.email || '') ||
|
||
trim(parsed.streetNumber || '') ||
|
||
trim(parsed.streetAddress2 || '') ||
|
||
trim(parsed.streetAddress3 || '') ||
|
||
trim(form.receiver.taxNo) ||
|
||
trim(form.receiver.certificateNo) ||
|
||
trim(form.receiver.eori) ||
|
||
trim(form.receiver.vat),
|
||
);
|
||
if (needMore) {
|
||
showReceiverMore.value = true;
|
||
}
|
||
selectedReceiverAddressId.value = undefined;
|
||
}
|
||
|
||
function buildReceiverParseTip(result: {
|
||
isComplete: boolean;
|
||
missingRequired: string[];
|
||
missingRecommended: string[];
|
||
warnings: string[];
|
||
}) {
|
||
const parts: string[] = [];
|
||
if (result.missingRequired.length) {
|
||
parts.push(`${t('h5.placeOrder.aiParseMissingRequired')}${result.missingRequired.join('、')}`);
|
||
}
|
||
if (result.missingRecommended.length) {
|
||
parts.push(`${t('h5.placeOrder.aiParseMissingRecommended')}${result.missingRecommended.join('、')}`);
|
||
}
|
||
if (result.warnings.length) {
|
||
parts.push(result.warnings.slice(0, 2).join(';'));
|
||
}
|
||
if (result.isComplete && !result.missingRecommended.length && !result.warnings.length) {
|
||
receiverParseTip.value = { tone: 'ok', text: t('h5.placeOrder.aiParseComplete') };
|
||
return;
|
||
}
|
||
if (result.isComplete) {
|
||
receiverParseTip.value = {
|
||
tone: 'warn',
|
||
text: parts.length ? `${t('h5.placeOrder.aiParseRequiredOk')}${parts.join('。')}` : t('h5.placeOrder.aiParseRequiredOkShort'),
|
||
};
|
||
return;
|
||
}
|
||
receiverParseTip.value = {
|
||
tone: 'danger',
|
||
text: parts.length ? parts.join('。') : t('h5.placeOrder.aiParseIncomplete'),
|
||
};
|
||
}
|
||
|
||
async function handleReceiverAiParse() {
|
||
const raw = receiverPasteText.value.trim();
|
||
if (!raw) {
|
||
Message.warning(t('h5.placeOrder.aiParseEmpty'));
|
||
return;
|
||
}
|
||
if (!ensureReceiverAiReady()) return;
|
||
if (receiverParseLoading.value) return;
|
||
saveReceiverAiPrefs();
|
||
receiverParseLoading.value = true;
|
||
receiverParseTip.value = null;
|
||
try {
|
||
const result = await postOrderReceiverParseAddress({
|
||
rawText: raw,
|
||
providerKey: receiverAiProviderKey.value,
|
||
model: receiverAiModelId.value,
|
||
skillKey: receiverAiSkillKey.value,
|
||
});
|
||
applyParsedReceiver(result.receiver);
|
||
buildReceiverParseTip(result);
|
||
Message.success(t('h5.placeOrder.aiParseSuccess'));
|
||
if (result.isComplete) {
|
||
receiverAiVisible.value = false;
|
||
}
|
||
} catch (e: any) {
|
||
receiverParseTip.value = {
|
||
tone: 'danger',
|
||
text: e?.message || t('h5.placeOrder.aiParseFailed'),
|
||
};
|
||
Message.error(e?.message || t('h5.placeOrder.aiParseFailed'));
|
||
} finally {
|
||
receiverParseLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function goToStep(idx: number) {
|
||
if (idx === step.value || submitting.value) return;
|
||
if (idx < step.value) {
|
||
showFieldErrors.value = false;
|
||
step.value = idx;
|
||
return;
|
||
}
|
||
for (let i = step.value; i < idx; i += 1) {
|
||
const err = validateStep(i);
|
||
if (err) {
|
||
showFieldErrors.value = true;
|
||
if (i === 0 && !trim(form.sender.senderName) && !senderOptions.value.length) {
|
||
showSenderForm.value = true;
|
||
}
|
||
step.value = i;
|
||
Message.warning(err);
|
||
return;
|
||
}
|
||
}
|
||
showFieldErrors.value = false;
|
||
step.value = idx;
|
||
}
|
||
|
||
function resetSenderForm() {
|
||
Object.assign(senderForm, {
|
||
senderName: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
streetAddress1: '',
|
||
countryCode: 'CN',
|
||
});
|
||
}
|
||
|
||
function emptyReceiverForm() {
|
||
return {
|
||
recipientsName: '',
|
||
phone: '',
|
||
countryCode: '',
|
||
province: '',
|
||
city: '',
|
||
streetAddress1: '',
|
||
zipCode: '',
|
||
companyName: '',
|
||
telePhone: '',
|
||
email: '',
|
||
streetNumber: '',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
taxType: '0',
|
||
taxNo: '',
|
||
certificateType: '0',
|
||
certificateNo: '',
|
||
eori: '',
|
||
vat: '',
|
||
};
|
||
}
|
||
|
||
function mapReceiverAddressOption(row: Record<string, unknown>) {
|
||
const id = Number(pickField(row, 'id', 'Id'));
|
||
if (!Number.isFinite(id) || id <= 0) return null;
|
||
const name = String(pickField(row, 'senderName', 'SenderName') || '');
|
||
const isDefault = Boolean(pickField(row, 'isDefault', 'IsDefault'));
|
||
return {
|
||
id,
|
||
label: `${name || `#${id}`}${isDefault ? ` (${t('h5.placeOrder.default')})` : ''}`,
|
||
isDefault,
|
||
data: {
|
||
recipientsName: name,
|
||
companyName: String(pickField(row, 'company', 'Company') || ''),
|
||
phone: String(pickField(row, 'phone', 'Phone') || ''),
|
||
telePhone: String(pickField(row, 'telePhone', 'TelePhone') || ''),
|
||
email: String(pickField(row, 'email', 'Email') || ''),
|
||
countryCode: String(pickField(row, 'countryCode', 'CountryCode') || ''),
|
||
province: String(pickField(row, 'province', 'Province') || ''),
|
||
city: String(pickField(row, 'city', 'City') || ''),
|
||
zipCode: String(pickField(row, 'zipCode', 'ZipCode') || ''),
|
||
streetAddress1: String(pickField(row, 'streetAddress1', 'StreetAddress1') || ''),
|
||
streetAddress2: String(pickField(row, 'streetAddress2', 'StreetAddress2') || ''),
|
||
streetAddress3: String(pickField(row, 'streetAddress3', 'StreetAddress3') || ''),
|
||
streetNumber: String(pickField(row, 'houseNumber', 'HouseNumber') || ''),
|
||
taxType: '0',
|
||
taxNo: '',
|
||
certificateType: '0',
|
||
certificateNo: '',
|
||
eori: '',
|
||
vat: '',
|
||
},
|
||
};
|
||
}
|
||
|
||
function applyReceiverAddressById(id: number | undefined, { silentEmpty = false } = {}) {
|
||
if (id == null) {
|
||
if (!silentEmpty) Object.assign(form.receiver, emptyReceiverForm());
|
||
selectedReceiverAddressId.value = undefined;
|
||
return;
|
||
}
|
||
const hit = receiverAddressOptions.value.find((opt) => opt.id === id);
|
||
if (!hit) return;
|
||
Object.assign(form.receiver, {
|
||
...emptyReceiverForm(),
|
||
...hit.data,
|
||
});
|
||
selectedReceiverAddressId.value = id;
|
||
if (
|
||
trim(hit.data.companyName) ||
|
||
trim(hit.data.telePhone) ||
|
||
trim(hit.data.email) ||
|
||
trim(hit.data.streetAddress2) ||
|
||
trim(hit.data.streetAddress3)
|
||
) {
|
||
showReceiverMore.value = true;
|
||
}
|
||
}
|
||
|
||
async function loadReceiverAddresses({ applyDefault = false } = {}) {
|
||
receiverAddressLoading.value = true;
|
||
try {
|
||
const res = await getSenderAddressList({
|
||
page: 1,
|
||
pageSize: 200,
|
||
addressType: 'receiver',
|
||
});
|
||
receiverAddressOptions.value = (res.items || [])
|
||
.map((item: Record<string, unknown>) => mapReceiverAddressOption(item))
|
||
.filter(Boolean) as typeof receiverAddressOptions.value;
|
||
|
||
if (applyDefault && !trim(form.receiver.recipientsName)) {
|
||
const defaultAddr =
|
||
receiverAddressOptions.value.find((x) => x.isDefault) || receiverAddressOptions.value[0];
|
||
if (defaultAddr) applyReceiverAddressById(defaultAddr.id);
|
||
} else if (selectedReceiverAddressId.value) {
|
||
const still = receiverAddressOptions.value.some(
|
||
(x) => x.id === selectedReceiverAddressId.value,
|
||
);
|
||
if (!still) selectedReceiverAddressId.value = undefined;
|
||
}
|
||
} catch (e: any) {
|
||
receiverAddressOptions.value = [];
|
||
Message.warning(e?.message || t('h5.placeOrder.receiverLoadFailed'));
|
||
} finally {
|
||
receiverAddressLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function onReceiverAddressChange(id: number | undefined) {
|
||
applyReceiverAddressById(id);
|
||
}
|
||
|
||
async function saveReceiverAddress() {
|
||
const receiver = form.receiver;
|
||
if (!trim(receiver.recipientsName) || !trim(receiver.streetAddress1)) {
|
||
Message.warning(t('h5.placeOrder.saveReceiverRequired'));
|
||
return;
|
||
}
|
||
savingReceiverAddress.value = true;
|
||
try {
|
||
await createSenderAddress({
|
||
addressType: 'receiver',
|
||
addressName: trim(receiver.recipientsName),
|
||
senderName: trim(receiver.recipientsName),
|
||
company: trim(receiver.companyName) || undefined,
|
||
phone: trim(receiver.phone) || undefined,
|
||
telePhone: trim(receiver.telePhone) || undefined,
|
||
email: trim(receiver.email) || undefined,
|
||
countryCode: trim(receiver.countryCode) || undefined,
|
||
province: trim(receiver.province) || undefined,
|
||
city: trim(receiver.city) || undefined,
|
||
zipCode: trim(receiver.zipCode) || undefined,
|
||
streetAddress1: trim(receiver.streetAddress1),
|
||
streetAddress2: trim(receiver.streetAddress2) || undefined,
|
||
streetAddress3: trim(receiver.streetAddress3) || undefined,
|
||
houseNumber: trim(receiver.streetNumber) || undefined,
|
||
isDefault: false,
|
||
});
|
||
Message.success(t('h5.placeOrder.saveReceiverSuccess'));
|
||
await loadReceiverAddresses({ applyDefault: false });
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.saveReceiverFailed'));
|
||
} finally {
|
||
savingReceiverAddress.value = false;
|
||
}
|
||
}
|
||
|
||
function mapExportQualificationOption(item: Record<string, unknown>) {
|
||
const id = Number(pickField(item, 'id', 'Id') ?? 0);
|
||
if (!id) return null;
|
||
const companyName = String(pickField(item, 'companyName', 'CompanyName') ?? '').trim();
|
||
const creditCode = String(pickField(item, 'socialCreditCode', 'SocialCreditCode') ?? '').trim();
|
||
const isDefault = Boolean(pickField(item, 'isDefault', 'IsDefault'));
|
||
const base = companyName || creditCode || `#${id}`;
|
||
return {
|
||
id,
|
||
label: isDefault ? `${base}(${t('h5.placeOrder.default')})` : base,
|
||
isDefault,
|
||
};
|
||
}
|
||
|
||
async function loadExportQualificationOptions({ applyDefault = true } = {}) {
|
||
exportQualificationLoading.value = true;
|
||
try {
|
||
const res = await getExportQualificationList({ page: 1, pageSize: 200 });
|
||
exportQualificationOptions.value = (res.items || [])
|
||
.map((item: Record<string, unknown>) => mapExportQualificationOption(item))
|
||
.filter(Boolean) as typeof exportQualificationOptions.value;
|
||
|
||
const currentId = parseNum(form.exportQualificationId);
|
||
const still = currentId
|
||
? exportQualificationOptions.value.some((x) => x.id === currentId)
|
||
: false;
|
||
if (still) return;
|
||
if (applyDefault && needExportQualification.value) {
|
||
const defaultOpt =
|
||
exportQualificationOptions.value.find((x) => x.isDefault) ||
|
||
exportQualificationOptions.value[0];
|
||
form.exportQualificationId = defaultOpt ? String(defaultOpt.id) : '';
|
||
} else {
|
||
form.exportQualificationId = '';
|
||
}
|
||
} catch (e: any) {
|
||
exportQualificationOptions.value = [];
|
||
Message.warning(e?.message || t('h5.placeOrder.exportQualificationLoadFailed'));
|
||
} finally {
|
||
exportQualificationLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function resolveDeclarationSkuTargetIndex() {
|
||
const list = items.value || [];
|
||
const emptyIdx = list.findIndex((row) => !isDeclareRowStarted(row));
|
||
if (emptyIdx >= 0) return emptyIdx;
|
||
return -1;
|
||
}
|
||
|
||
function openDeclarationSkuSheet() {
|
||
declarationSkuVisible.value = true;
|
||
}
|
||
|
||
function handleDeclarationSkuSelect(raw: Record<string, unknown>) {
|
||
let targetIdx = resolveDeclarationSkuTargetIndex();
|
||
if (targetIdx < 0) {
|
||
addItem();
|
||
targetIdx = items.value.length - 1;
|
||
}
|
||
const mapped = mapWarehouseSkuToDeclarationFields(raw);
|
||
Object.assign(items.value[targetIdx], {
|
||
...items.value[targetIdx],
|
||
...mapped,
|
||
});
|
||
if (!trim(items.value[targetIdx].quantity)) {
|
||
items.value[targetIdx].quantity = '1';
|
||
}
|
||
syncItemDerived(items.value[targetIdx]);
|
||
if (
|
||
trim(mapped.exHsCode) ||
|
||
trim(mapped.imHsCode) ||
|
||
trim(mapped.declareElement) ||
|
||
trim(mapped.skuImage) ||
|
||
trim(mapped.comment)
|
||
) {
|
||
const next = new Set(openItemMoreKeys.value);
|
||
next.add(items.value[targetIdx]._key);
|
||
openItemMoreKeys.value = next;
|
||
}
|
||
Message.success(t('h5.placeOrder.skuPickFilled'));
|
||
}
|
||
|
||
async function saveSender() {
|
||
if (!trim(senderForm.senderName) || !trim(senderForm.streetAddress1)) {
|
||
Message.warning(t('h5.placeOrder.err.senderForm'));
|
||
return;
|
||
}
|
||
savingSender.value = true;
|
||
try {
|
||
await createSenderAddress({
|
||
addressType: 'sender',
|
||
addressName: trim(senderForm.senderName),
|
||
senderName: trim(senderForm.senderName),
|
||
phone: trim(senderForm.phone),
|
||
countryCode: trim(senderForm.countryCode) || 'CN',
|
||
province: trim(senderForm.province),
|
||
city: trim(senderForm.city),
|
||
streetAddress1: trim(senderForm.streetAddress1),
|
||
isDefault: senderOptions.value.length === 0,
|
||
});
|
||
Message.success(t('h5.placeOrder.senderSaved'));
|
||
showSenderForm.value = false;
|
||
resetSenderForm();
|
||
await loadSenders();
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.senderSaveFailed'));
|
||
} finally {
|
||
savingSender.value = false;
|
||
}
|
||
}
|
||
|
||
function onSenderChange(id: number | undefined) {
|
||
selectedSenderId.value = id;
|
||
const hit = senderOptions.value.find((x) => x.id === id);
|
||
if (!hit) {
|
||
Object.keys(form.sender).forEach((k) => {
|
||
(form.sender as any)[k] = '';
|
||
});
|
||
return;
|
||
}
|
||
Object.assign(form.sender, hit.data);
|
||
}
|
||
|
||
function onPickupAddressChange(id: number | undefined) {
|
||
selectedPickupAddressId.value = id;
|
||
}
|
||
|
||
async function loadPickupAddresses() {
|
||
pickupAddressLoading.value = true;
|
||
try {
|
||
const res = await getSenderAddressList({ page: 1, pageSize: 200, addressType: 'pickup' });
|
||
pickupAddressOptions.value = (res.items || [])
|
||
.map((row: Record<string, unknown>) => {
|
||
const id = Number(pickField(row, 'id', 'Id'));
|
||
if (!Number.isFinite(id) || id <= 0) return null;
|
||
const name = String(
|
||
pickField(row, 'addressName', 'AddressName') ||
|
||
pickField(row, 'senderName', 'SenderName') ||
|
||
`#${id}`,
|
||
);
|
||
const phone = String(pickField(row, 'phone', 'Phone') || '');
|
||
return { id, label: phone ? `${name} · ${phone}` : name };
|
||
})
|
||
.filter(Boolean) as { id: number; label: string }[];
|
||
} catch {
|
||
pickupAddressOptions.value = [];
|
||
} finally {
|
||
pickupAddressLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function randomOrderNo(prefix: string) {
|
||
const now = new Date();
|
||
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(
|
||
now.getDate(),
|
||
).padStart(2, '0')}`;
|
||
const suffix = Math.random().toString().slice(2, 8);
|
||
return `${prefix}${date}${suffix}`;
|
||
}
|
||
|
||
/** 开发/联调用:一键填入 Mock 下单数据 */
|
||
function fillMockOrder() {
|
||
form.clientNo = randomOrderNo('C');
|
||
form.productCode = productOptions.value[0]?.value || form.productCode || '';
|
||
form.cargoAttribute = cargoOptions.value[0]?.value || form.cargoAttribute || 'general';
|
||
form.warehouseId = warehouseOptions.value[0]?.value || form.warehouseId || '';
|
||
form.exportCustomsMode = resolveExportCustomsMode9610Simple();
|
||
form.exportQualificationId = '';
|
||
form.platformCode = '';
|
||
form.inboundMethod = 'selfShip';
|
||
form.expectPickupTime = '';
|
||
form.carrierService = '';
|
||
form.length = '10';
|
||
form.width = '10';
|
||
form.height = '10';
|
||
form.actualWeight = '1.2';
|
||
form.quantity = '1';
|
||
selectedReceiverAddressId.value = undefined;
|
||
form.declaredCurrency =
|
||
currencyOptions.value.find((c) => c.value === 'USD')?.value ||
|
||
currencyOptions.value[0]?.value ||
|
||
'USD';
|
||
|
||
if (senderOptions.value.length) {
|
||
onSenderChange(senderOptions.value[0].id);
|
||
} else {
|
||
selectedSenderId.value = undefined;
|
||
Object.assign(form.sender, {
|
||
senderName: 'Demo Sender',
|
||
company: 'Demo Sender Co.,Ltd',
|
||
phone: '13800138000',
|
||
telePhone: '',
|
||
email: 'sender@example.com',
|
||
countryCode: 'CN',
|
||
province: 'Guangdong',
|
||
city: 'Shenzhen',
|
||
zipCode: '518000',
|
||
streetAddress1: 'Nanshan District',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
houseNumber: '88',
|
||
});
|
||
}
|
||
|
||
Object.assign(form.receiver, {
|
||
recipientsName: 'Demo Receiver',
|
||
companyName: 'Demo Receiver LLC',
|
||
phone: '13900139000',
|
||
telePhone: '',
|
||
email: 'receiver@example.com',
|
||
countryCode: 'US',
|
||
province: 'California',
|
||
city: 'Los Angeles',
|
||
streetNumber: '1001',
|
||
streetAddress1: 'Sunset Blvd',
|
||
streetAddress2: '',
|
||
streetAddress3: '',
|
||
zipCode: '90001',
|
||
taxType: '0',
|
||
taxNo: '',
|
||
certificateType: '0',
|
||
certificateNo: '',
|
||
eori: '',
|
||
vat: '',
|
||
});
|
||
|
||
const demoItem = createEmptyItem();
|
||
Object.assign(demoItem, {
|
||
cnName: '手机壳',
|
||
enName: 'Phone Case',
|
||
sku: 'SKU001',
|
||
unit: 'pcs',
|
||
unitPrice: '12.5',
|
||
unitWeight: '0.3',
|
||
quantity: '1',
|
||
brand: 'DemoBrand',
|
||
model: 'Model-X',
|
||
material: 'plastic',
|
||
enMaterial: 'plastic',
|
||
purpose: 'protection',
|
||
originCountry: 'CN',
|
||
category: 'Accessories',
|
||
specifications: 'Standard',
|
||
comment: 'Mock order',
|
||
});
|
||
syncItemDerived(demoItem);
|
||
items.value = [demoItem];
|
||
syncDeclaredTotalInputFromRows();
|
||
selectedAdditionalServices.value = [];
|
||
if (form.productCode) {
|
||
void loadProductAdditionalServices(form.productCode);
|
||
}
|
||
showFieldErrors.value = false;
|
||
Message.success(t('h5.placeOrder.mockFilled'));
|
||
}
|
||
|
||
function pickDetail(raw: Record<string, any> | undefined | null, ...keys: string[]) {
|
||
if (!raw) return undefined;
|
||
for (const k of keys) {
|
||
const v = raw[k];
|
||
if (v !== undefined && v !== null && v !== '') return v;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
function resolveCountryCode(raw: unknown) {
|
||
const s = String(raw ?? '').trim();
|
||
if (!s) return '';
|
||
if (/^[A-Za-z]{2}$/.test(s)) return s.toUpperCase();
|
||
return countryIdToCode.value[s] || countryIdToCode.value[String(Number(s))] || s;
|
||
}
|
||
|
||
function mapDetailItems(rawItems: any[]): DeclareItem[] {
|
||
const list = Array.isArray(rawItems) ? rawItems : [];
|
||
const mapped = list.map((raw) => {
|
||
const row = createEmptyItem();
|
||
const hs =
|
||
pickDetail(raw, 'imHsCode', 'ImHsCode', 'hsCode', 'HsCode', 'exHsCode', 'ExHsCode') ?? '';
|
||
Object.assign(row, {
|
||
cnName: String(pickDetail(raw, 'cnName', 'CnName') ?? ''),
|
||
enName: String(pickDetail(raw, 'enName', 'EnName') ?? ''),
|
||
sku: String(pickDetail(raw, 'sku', 'Sku') ?? ''),
|
||
unit: String(pickDetail(raw, 'unit', 'Unit') ?? 'pcs') || 'pcs',
|
||
unitPrice: String(pickDetail(raw, 'unitPrice', 'UnitPrice') ?? ''),
|
||
unitWeight: String(pickDetail(raw, 'unitWeight', 'UnitWeight') ?? ''),
|
||
quantity: String(pickDetail(raw, 'quantity', 'Quantity') ?? '1') || '1',
|
||
amount: String(pickDetail(raw, 'amount', 'Amount') ?? ''),
|
||
weight: String(pickDetail(raw, 'weight', 'Weight') ?? ''),
|
||
imHsCode: String(pickDetail(raw, 'imHsCode', 'ImHsCode') ?? hs),
|
||
exHsCode: String(pickDetail(raw, 'exHsCode', 'ExHsCode') ?? hs),
|
||
declareElement: String(pickDetail(raw, 'declareElement', 'DeclareElement') ?? ''),
|
||
skuImage: normalizeSkuImageForStorage(
|
||
String(pickDetail(raw, 'skuImage', 'SkuImage') ?? ''),
|
||
),
|
||
brand: String(pickDetail(raw, 'brand', 'Brand') ?? ''),
|
||
brandType: String(pickDetail(raw, 'brandType', 'BrandType') ?? ''),
|
||
model: String(pickDetail(raw, 'model', 'Model') ?? ''),
|
||
material: String(pickDetail(raw, 'material', 'Material') ?? ''),
|
||
enMaterial: String(pickDetail(raw, 'enMaterial', 'EnMaterial') ?? ''),
|
||
purpose: String(pickDetail(raw, 'purpose', 'Purpose') ?? ''),
|
||
originCountry: String(pickDetail(raw, 'originCountry', 'OriginCountry') ?? ''),
|
||
category: String(pickDetail(raw, 'category', 'Category') ?? ''),
|
||
specifications: String(pickDetail(raw, 'specifications', 'Specifications') ?? ''),
|
||
salesLink: String(pickDetail(raw, 'salesLink', 'SalesLink') ?? ''),
|
||
comment: String(pickDetail(raw, 'comment', 'Comment') ?? ''),
|
||
isMagnetic: Boolean(pickDetail(raw, 'isMagnetic', 'IsMagnetic')),
|
||
distribution: String(pickDetail(raw, 'distribution', 'Distribution') ?? ''),
|
||
attributes: String(pickDetail(raw, 'attributes', 'Attributes') ?? ''),
|
||
});
|
||
syncItemDerived(row);
|
||
return row;
|
||
});
|
||
return mapped.length ? mapped : [createEmptyItem()];
|
||
}
|
||
|
||
function applyOrderDetail(detail: Record<string, any>) {
|
||
const sender = detail?.sender || detail?.Sender || {};
|
||
const receiver = detail?.receiver || detail?.Receiver || {};
|
||
const weights = detail?.weightList || detail?.WeightList || [];
|
||
const weight =
|
||
(Array.isArray(weights)
|
||
? weights.find((w: any) =>
|
||
String(pickDetail(w, 'type', 'Type') ?? '')
|
||
.toLowerCase()
|
||
.includes('forecast'),
|
||
)
|
||
: null) ||
|
||
(Array.isArray(weights) ? weights[0] : null) ||
|
||
{};
|
||
|
||
form.clientNo = String(pickDetail(detail, 'clientNo', 'ClientNo') ?? '');
|
||
form.productCode = String(pickDetail(detail, 'productCode', 'ProductCode') ?? '');
|
||
form.cargoAttribute = normalizeCargoAttributeCode(
|
||
pickDetail(detail, 'cargoAttribute', 'CargoAttribute') ?? form.cargoAttribute,
|
||
);
|
||
form.exportCustomsMode =
|
||
String(pickDetail(detail, 'exportCustomsMode', 'ExportCustomsMode') ?? '') ||
|
||
resolveExportCustomsMode9610Simple();
|
||
form.exportQualificationId = String(
|
||
pickDetail(detail, 'exportQualificationId', 'ExportQualificationId') ?? '',
|
||
);
|
||
form.platformCode = String(pickDetail(detail, 'platformCode', 'PlatformCode') ?? '');
|
||
form.warehouseId = String(pickDetail(detail, 'warehouseId', 'WarehouseId') ?? '') || form.warehouseId;
|
||
form.declaredCurrency = String(
|
||
pickDetail(detail, 'declaredCurrency', 'DeclaredCurrency') ?? form.declaredCurrency,
|
||
);
|
||
form.quantity = String(pickDetail(detail, 'quantity', 'Quantity') ?? (form.quantity || '1'));
|
||
form.inboundMethod =
|
||
String(pickDetail(detail, 'inboundMethod', 'InboundMethod') ?? 'selfShip') || 'selfShip';
|
||
form.length = String(pickDetail(weight, 'length', 'Length') ?? form.length ?? '');
|
||
form.width = String(pickDetail(weight, 'width', 'Width') ?? form.width ?? '');
|
||
form.height = String(pickDetail(weight, 'height', 'Height') ?? form.height ?? '');
|
||
form.actualWeight = String(pickDetail(weight, 'actualWeight', 'ActualWeight') ?? '');
|
||
|
||
const senderName = String(pickDetail(sender, 'senderName', 'SenderName') ?? '');
|
||
editHasSender.value = Boolean(senderName || pickDetail(sender, 'phone', 'Phone'));
|
||
Object.assign(form.sender, {
|
||
senderName,
|
||
company: String(pickDetail(sender, 'company', 'Company') ?? ''),
|
||
phone: String(pickDetail(sender, 'phone', 'Phone') ?? ''),
|
||
telePhone: String(pickDetail(sender, 'telePhone', 'TelePhone') ?? ''),
|
||
email: String(pickDetail(sender, 'email', 'Email') ?? ''),
|
||
countryCode: resolveCountryCode(pickDetail(sender, 'countryCode', 'CountryCode', 'country', 'Country')),
|
||
province: String(pickDetail(sender, 'province', 'Province') ?? ''),
|
||
city: String(pickDetail(sender, 'city', 'City') ?? ''),
|
||
zipCode: String(pickDetail(sender, 'zipCode', 'ZipCode') ?? ''),
|
||
streetAddress1: String(pickDetail(sender, 'streetAddress1', 'StreetAddress1') ?? ''),
|
||
streetAddress2: String(pickDetail(sender, 'streetAddress2', 'StreetAddress2') ?? ''),
|
||
streetAddress3: String(pickDetail(sender, 'streetAddress3', 'StreetAddress3') ?? ''),
|
||
houseNumber: String(pickDetail(sender, 'houseNumber', 'HouseNumber') ?? ''),
|
||
});
|
||
|
||
Object.assign(form.receiver, {
|
||
recipientsName: String(pickDetail(receiver, 'recipientsName', 'RecipientsName') ?? ''),
|
||
companyName: String(pickDetail(receiver, 'companyName', 'CompanyName') ?? ''),
|
||
phone: String(pickDetail(receiver, 'phone', 'Phone') ?? ''),
|
||
telePhone: String(pickDetail(receiver, 'telePhone', 'TelePhone') ?? ''),
|
||
email: String(pickDetail(receiver, 'email', 'Email') ?? ''),
|
||
countryCode: resolveCountryCode(
|
||
pickDetail(receiver, 'countryCode', 'CountryCode', 'country', 'Country') ??
|
||
pickDetail(detail, 'consigneeCountry', 'ConsigneeCountry'),
|
||
),
|
||
province: String(pickDetail(receiver, 'province', 'Province') ?? ''),
|
||
city: String(pickDetail(receiver, 'city', 'City') ?? ''),
|
||
streetNumber: String(pickDetail(receiver, 'streetNumber', 'StreetNumber') ?? ''),
|
||
streetAddress1: String(pickDetail(receiver, 'streetAddress1', 'StreetAddress1') ?? ''),
|
||
streetAddress2: String(pickDetail(receiver, 'streetAddress2', 'StreetAddress2') ?? ''),
|
||
streetAddress3: String(pickDetail(receiver, 'streetAddress3', 'StreetAddress3') ?? ''),
|
||
zipCode: String(pickDetail(receiver, 'zipCode', 'ZipCode') ?? ''),
|
||
taxType: String(pickDetail(receiver, 'taxType', 'TaxType') ?? '0') || '0',
|
||
taxNo: String(pickDetail(receiver, 'taxNo', 'TaxNo') ?? ''),
|
||
certificateType: String(pickDetail(receiver, 'certificateType', 'CertificateType') ?? '0') || '0',
|
||
certificateNo: String(pickDetail(receiver, 'certificateNo', 'CertificateNo') ?? ''),
|
||
eori: String(pickDetail(receiver, 'eori', 'Eori') ?? ''),
|
||
vat: String(pickDetail(receiver, 'vat', 'Vat') ?? ''),
|
||
});
|
||
|
||
const services = detail?.additionalServices ?? detail?.AdditionalServices ?? [];
|
||
selectedAdditionalServices.value = Array.isArray(services)
|
||
? services.map((x: any) => String(x ?? '').trim()).filter(Boolean)
|
||
: [];
|
||
|
||
items.value = mapDetailItems(detail?.items || detail?.Items || []);
|
||
syncDeclaredTotalInputFromRows();
|
||
selectedSenderId.value = undefined;
|
||
showSenderForm.value = !editHasSender.value;
|
||
step.value = 0;
|
||
}
|
||
|
||
async function loadOrderForEdit(orderId: number | string) {
|
||
optionsLoading.value = true;
|
||
try {
|
||
const detail = await getOrderDetail(orderId);
|
||
applyOrderDetail(detail || {});
|
||
await loadProductAdditionalServices(form.productCode);
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.editLoadFailed'));
|
||
emit('update:visible', false);
|
||
} finally {
|
||
optionsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function buildUpdatePayload() {
|
||
items.value.forEach((row) => syncItemDerived(row));
|
||
const payloadItems = items.value.map((row) => {
|
||
const { _key, ...rest } = row;
|
||
return {
|
||
...rest,
|
||
unitPrice: parseNum(row.unitPrice) ?? 0,
|
||
unitWeight: parseNum(row.unitWeight) ?? 0,
|
||
quantity: parseNum(row.quantity) ?? 0,
|
||
amount: parseNum(row.amount) ?? 0,
|
||
weight: parseNum(row.weight) ?? 0,
|
||
isMagnetic: Boolean(row.isMagnetic),
|
||
skuImage: normalizeSkuImageForStorage(row.skuImage),
|
||
};
|
||
});
|
||
const body: Record<string, any> = {
|
||
id: Number(props.orderId),
|
||
clientNo: trim(form.clientNo),
|
||
cargoAttribute: normalizeCargoAttributeCode(form.cargoAttribute),
|
||
quantity: trim(form.quantity) || '1',
|
||
declaredCurrency: trim(form.declaredCurrency),
|
||
isCod: false,
|
||
receiver: {
|
||
recipientsName: trim(form.receiver.recipientsName),
|
||
companyName: trim(form.receiver.companyName),
|
||
countryCode: resolveCountryCode(form.receiver.countryCode),
|
||
phone: trim(form.receiver.phone),
|
||
telePhone: trim(form.receiver.telePhone),
|
||
zipCode: trim(form.receiver.zipCode),
|
||
province: trim(form.receiver.province),
|
||
city: trim(form.receiver.city),
|
||
streetNumber: trim(form.receiver.streetNumber),
|
||
streetAddress1: trim(form.receiver.streetAddress1),
|
||
streetAddress2: trim(form.receiver.streetAddress2),
|
||
streetAddress3: trim(form.receiver.streetAddress3),
|
||
email: trim(form.receiver.email),
|
||
taxType: parseNum(form.receiver.taxType) ?? 0,
|
||
taxNo: trim(form.receiver.taxNo),
|
||
certificateType: parseNum(form.receiver.certificateType) ?? 0,
|
||
certificateNo: trim(form.receiver.certificateNo),
|
||
eori: trim(form.receiver.eori),
|
||
vat: trim(form.receiver.vat),
|
||
},
|
||
items: payloadItems,
|
||
};
|
||
if (editHasSender.value || trim(form.sender.senderName)) {
|
||
body.sender = {
|
||
senderName: trim(form.sender.senderName),
|
||
company: trim(form.sender.company),
|
||
countryCode: resolveCountryCode(form.sender.countryCode),
|
||
province: trim(form.sender.province),
|
||
city: trim(form.sender.city),
|
||
zipCode: trim(form.sender.zipCode),
|
||
streetAddress1: trim(form.sender.streetAddress1),
|
||
streetAddress2: trim(form.sender.streetAddress2),
|
||
streetAddress3: trim(form.sender.streetAddress3),
|
||
houseNumber: trim(form.sender.houseNumber),
|
||
phone: trim(form.sender.phone),
|
||
telePhone: trim(form.sender.telePhone),
|
||
email: trim(form.sender.email),
|
||
};
|
||
}
|
||
return body;
|
||
}
|
||
|
||
function buildPayload() {
|
||
items.value.forEach((row) => syncItemDerived(row));
|
||
const payloadItems = items.value.map((row) => {
|
||
const { _key, ...rest } = row;
|
||
return {
|
||
...rest,
|
||
unitPrice: parseNum(row.unitPrice) ?? 0,
|
||
unitWeight: parseNum(row.unitWeight) ?? 0,
|
||
quantity: parseNum(row.quantity) ?? 0,
|
||
amount: parseNum(row.amount) ?? 0,
|
||
weight: parseNum(row.weight) ?? 0,
|
||
isMagnetic: Boolean(row.isMagnetic),
|
||
skuImage: normalizeSkuImageForStorage(row.skuImage),
|
||
};
|
||
});
|
||
const declaredSum = payloadItems.reduce((sum, row) => sum + (Number(row.amount) || 0), 0);
|
||
const inbound = trim(form.inboundMethod) || 'selfShip';
|
||
const exportMode = trim(form.exportCustomsMode) || resolveExportCustomsMode9610Simple();
|
||
const payload: Record<string, any> = {
|
||
clientNo: trim(form.clientNo),
|
||
productCode: trim(form.productCode),
|
||
cargoAttribute: normalizeCargoAttributeCode(form.cargoAttribute),
|
||
exportCustomsMode: exportMode,
|
||
warehouseId: parseNum(form.warehouseId) ?? 0,
|
||
length: parseNum(form.length),
|
||
width: parseNum(form.width),
|
||
height: parseNum(form.height),
|
||
actualWeight: parseNum(form.actualWeight) ?? 0,
|
||
quantity: trim(form.quantity) || '1',
|
||
declaredCurrency: trim(form.declaredCurrency),
|
||
declaredSum,
|
||
inboundMethod: inbound,
|
||
isCod: false,
|
||
additionalServices: [...selectedAdditionalServices.value],
|
||
receiver: {
|
||
...form.receiver,
|
||
countryCode: trim(form.receiver.countryCode).toUpperCase(),
|
||
taxType: parseNum(form.receiver.taxType) ?? 0,
|
||
certificateType: parseNum(form.receiver.certificateType) ?? 0,
|
||
},
|
||
sender: {
|
||
...form.sender,
|
||
countryCode: trim(form.sender.countryCode).toUpperCase() || undefined,
|
||
},
|
||
items: payloadItems,
|
||
};
|
||
if (isExportCustomsMode9610Summary(exportMode)) {
|
||
payload.exportQualificationId = parseNum(form.exportQualificationId);
|
||
payload.platformCode = trim(form.platformCode);
|
||
}
|
||
if (inbound === 'doorPickup') {
|
||
if (selectedPickupAddressId.value) {
|
||
payload.senderAddressId = selectedPickupAddressId.value;
|
||
}
|
||
payload.expectPickupTime = trim(form.expectPickupTime) || undefined;
|
||
payload.carrierService = trim(form.carrierService) || undefined;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function clearDraftStorage() {
|
||
try {
|
||
sessionStorage.removeItem(PRICE_CHECK_ORDER_DRAFT_KEY);
|
||
localStorage.removeItem(PRICE_CHECK_ORDER_DRAFT_KEY);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function close() {
|
||
if (submitting.value) return;
|
||
emit('update:visible', false);
|
||
}
|
||
|
||
async function submit() {
|
||
for (let i = 0; i <= 2; i += 1) {
|
||
const err = validateStep(i);
|
||
if (err) {
|
||
step.value = i;
|
||
showFieldErrors.value = true;
|
||
if (i === 0 && !trim(form.sender.senderName) && !senderOptions.value.length) {
|
||
showSenderForm.value = true;
|
||
}
|
||
Message.warning(err);
|
||
return;
|
||
}
|
||
}
|
||
submitting.value = true;
|
||
try {
|
||
if (isEditMode.value) {
|
||
await postOrderUpdate(buildUpdatePayload());
|
||
Message.success(t('h5.placeOrder.editSaved'));
|
||
emit('success');
|
||
emit('update:visible', false);
|
||
return;
|
||
}
|
||
await postOrderParcelCreate(buildPayload());
|
||
clearDraftStorage();
|
||
emit('success');
|
||
Modal.confirm({
|
||
title: t('h5.placeOrder.successTitle'),
|
||
content: t('h5.placeOrder.successContent'),
|
||
okText: t('h5.placeOrder.goOrders'),
|
||
cancelText: t('h5.placeOrder.stay'),
|
||
onOk: () => {
|
||
emit('update:visible', false);
|
||
h5Navigate(router, '/h5/orders');
|
||
},
|
||
onCancel: () => {
|
||
// 继续下单:重置表单留在抽屉
|
||
resetForm();
|
||
ensureClientNo();
|
||
void loadOptions();
|
||
void loadSenders();
|
||
},
|
||
});
|
||
} catch (e: any) {
|
||
if (isEditMode.value) {
|
||
Message.error(e?.message || t('h5.placeOrder.editSaveFailed'));
|
||
} else if (isCustomerContactRequiredError(e)) {
|
||
Message.warning(t('h5.contact.requiredToast'));
|
||
} else if (isSenderRequiredError(e)) {
|
||
step.value = 0;
|
||
if (!senderOptions.value.length) showSenderForm.value = true;
|
||
Message.warning(
|
||
senderOptions.value.length
|
||
? t('h5.placeOrder.err.sender')
|
||
: t('h5.placeOrder.err.senderEmpty'),
|
||
);
|
||
} else {
|
||
openCreateFailModal(e);
|
||
}
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
const createFailVisible = ref(false);
|
||
const createFailCancelling = ref(false);
|
||
const createFailSavingDraft = ref(false);
|
||
const createFailInfo = ref<ParcelCreateFailGuide | null>(null);
|
||
|
||
function openCreateFailModal(error: unknown) {
|
||
createFailInfo.value = buildParcelCreateFailGuide(error);
|
||
createFailVisible.value = true;
|
||
}
|
||
|
||
function focusFailSection(section: string) {
|
||
if (section === 'recipient') step.value = 1;
|
||
else if (section === 'declaration') step.value = 2;
|
||
else {
|
||
step.value = 0;
|
||
if (section === 'sender' && !senderOptions.value.length) showSenderForm.value = true;
|
||
}
|
||
showFieldErrors.value = true;
|
||
}
|
||
|
||
function handleCreateFailPrimary() {
|
||
const info = createFailInfo.value;
|
||
createFailVisible.value = false;
|
||
if (!info) return;
|
||
if (info.draftSaved && info.orderId) {
|
||
emit('update:visible', false);
|
||
h5Navigate(router, {
|
||
path: '/h5/orders',
|
||
query: { orderId: String(info.orderId) },
|
||
});
|
||
return;
|
||
}
|
||
focusFailSection(info.section);
|
||
}
|
||
|
||
async function handleCreateFailSaveDraft() {
|
||
const info = createFailInfo.value;
|
||
if (!info || info.draftSaved || createFailSavingDraft.value) return;
|
||
|
||
createFailSavingDraft.value = true;
|
||
try {
|
||
const created = await postOrderParcelCreate({
|
||
...buildPayload(),
|
||
saveAsDraft: true,
|
||
});
|
||
const orderId = Number(created?.orderId ?? created?.OrderId ?? created?.id ?? created?.Id ?? 0) || undefined;
|
||
createFailVisible.value = false;
|
||
clearDraftStorage();
|
||
Modal.confirm({
|
||
title: t('h5.placeOrder.failSaveDraftOk'),
|
||
content: t('h5.placeOrder.failDraftNote'),
|
||
okText: t('h5.placeOrder.failSaveDraftGo'),
|
||
cancelText: t('h5.placeOrder.failSaveDraftStay'),
|
||
onOk: () => {
|
||
emit('update:visible', false);
|
||
h5Navigate(router, {
|
||
path: '/h5/orders',
|
||
query: orderId ? { orderId: String(orderId) } : {},
|
||
});
|
||
},
|
||
onCancel: () => {
|
||
resetForm();
|
||
ensureClientNo();
|
||
},
|
||
});
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.failSaveDraftFailed'));
|
||
} finally {
|
||
createFailSavingDraft.value = false;
|
||
}
|
||
}
|
||
|
||
function handleCreateFailCancel() {
|
||
const info = createFailInfo.value;
|
||
if (!info) {
|
||
createFailVisible.value = false;
|
||
return;
|
||
}
|
||
if (info.draftSaved && info.orderId) {
|
||
Modal.confirm({
|
||
title: t('h5.placeOrder.failCancelDraftTitle'),
|
||
content: t('h5.placeOrder.failCancelDraftContent'),
|
||
okText: t('h5.placeOrder.failCancelDraftOk'),
|
||
cancelText: t('h5.placeOrder.failKeep'),
|
||
okButtonProps: { status: 'danger' },
|
||
onOk: async () => {
|
||
createFailCancelling.value = true;
|
||
try {
|
||
await postOrderDelete({ id: info.orderId! });
|
||
Message.success(t('h5.placeOrder.failCancelDraftDone'));
|
||
createFailVisible.value = false;
|
||
resetForm();
|
||
ensureClientNo();
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.failCancelDraftFailed'));
|
||
} finally {
|
||
createFailCancelling.value = false;
|
||
}
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: t('h5.placeOrder.failAbandonTitle'),
|
||
content: t('h5.placeOrder.failAbandonContent'),
|
||
okText: t('h5.placeOrder.failAbandonOk'),
|
||
cancelText: t('h5.placeOrder.failKeep'),
|
||
okButtonProps: { status: 'danger' },
|
||
onOk: () => {
|
||
createFailVisible.value = false;
|
||
resetForm();
|
||
ensureClientNo();
|
||
Message.success(t('h5.placeOrder.failAbandonDone'));
|
||
},
|
||
});
|
||
}
|
||
|
||
function readStoredDraft(): Record<string, any> | null {
|
||
try {
|
||
const raw =
|
||
sessionStorage.getItem(PRICE_CHECK_ORDER_DRAFT_KEY) ||
|
||
localStorage.getItem(PRICE_CHECK_ORDER_DRAFT_KEY);
|
||
return raw ? JSON.parse(raw) : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function applyDraft(draft: Record<string, any> | null) {
|
||
if (!draft) return;
|
||
const filled: string[] = [];
|
||
if (draft.productCode || draft.shippingMethod) {
|
||
const code = trim(draft.productCode);
|
||
const byCode = productOptions.value.find((o) => o.value === code);
|
||
const byName = productOptions.value.find(
|
||
(o) => o.label.includes(trim(draft.shippingMethod)) || o.value === trim(draft.shippingMethod),
|
||
);
|
||
form.productCode = byCode?.value || byName?.value || code;
|
||
if (form.productCode) filled.push(t('h5.placeOrder.product'));
|
||
}
|
||
const draftServices = parseAdditionalServiceCodes(draft.additionalServices);
|
||
if (draftServices.length) {
|
||
selectedAdditionalServices.value = draftServices;
|
||
filled.push(t('h5.placeOrder.additionalServices'));
|
||
}
|
||
if (draft.cargoAttribute || draft.cargoType) {
|
||
form.cargoAttribute = normalizeCargoAttributeCode(draft.cargoAttribute || draft.cargoType);
|
||
filled.push(t('h5.placeOrder.cargo'));
|
||
}
|
||
if (draft.warehouseId != null && draft.warehouseId !== '') {
|
||
form.warehouseId = String(draft.warehouseId);
|
||
filled.push(t('h5.placeOrder.warehouse'));
|
||
}
|
||
const weight = draft.actualWeight || draft.cargoWeight;
|
||
if (weight) {
|
||
form.actualWeight = String(weight);
|
||
filled.push(t('h5.placeOrder.weight'));
|
||
}
|
||
if (draft.length && draft.width && draft.height) {
|
||
form.length = String(draft.length);
|
||
form.width = String(draft.width);
|
||
form.height = String(draft.height);
|
||
filled.push(t('h5.placeOrder.dimensions'));
|
||
}
|
||
const country = draft.countryCode || draft.destinationCountry;
|
||
if (country) {
|
||
form.receiver.countryCode = String(country).toUpperCase();
|
||
filled.push(t('h5.placeOrder.country'));
|
||
}
|
||
if (draft.zipCode || draft.postalCode) {
|
||
form.receiver.zipCode = String(draft.zipCode || draft.postalCode);
|
||
}
|
||
|
||
handoffTip.value = t('h5.placeOrder.handoffFilled', {
|
||
items: filled.join('、') || '-',
|
||
});
|
||
// 始终从基础信息开始,不因查价带入字段跳到收件
|
||
step.value = 0;
|
||
}
|
||
|
||
async function loadOptions(
|
||
draft: Record<string, any> | null,
|
||
opts?: { skipDefaults?: boolean; skipEnsureClientNo?: boolean },
|
||
) {
|
||
optionsLoading.value = true;
|
||
try {
|
||
const [
|
||
productRes,
|
||
cargoRows,
|
||
warehouseRes,
|
||
currencyRows,
|
||
countryRes,
|
||
salesProductGroupDictRows,
|
||
additionalServiceRows,
|
||
exportCustomsModeRows,
|
||
] = await Promise.all([
|
||
getSalesProductList({ page: 1, pageSize: 500, isActive: 2 }),
|
||
getDictListAll({ dictType: 'cargo_attribute' }),
|
||
getWarehouseList({ page: 1, pageSize: 500 }),
|
||
getDictListAll({ dictType: 'currency' }),
|
||
getCountryList(),
|
||
getDictListAll({ dictType: 'sales_product_group' }),
|
||
getDictListAll({ dictType: 'additional_service' }),
|
||
getDictListAll({ dictType: 'export_customs_mode' }),
|
||
]);
|
||
additionalServiceDictRows.value = (additionalServiceRows ?? [])
|
||
.map((r: any) => ({
|
||
dictCode: String(r?.dictCode ?? r?.DictCode ?? '').trim(),
|
||
dictName: String(r?.dictName ?? r?.DictName ?? '').trim(),
|
||
}))
|
||
.filter((r) => r.dictCode);
|
||
|
||
exportCustomsModeOptions.value = (exportCustomsModeRows ?? [])
|
||
.map((row: any) => ({
|
||
value: String(row?.dictCode ?? row?.DictCode ?? '').trim(),
|
||
label: String(row?.dictName ?? row?.DictName ?? '').trim(),
|
||
}))
|
||
.filter((x) => x.value);
|
||
if (!opts?.skipDefaults || !trim(form.exportCustomsMode)) {
|
||
form.exportCustomsMode = resolveExportCustomsMode9610Simple();
|
||
}
|
||
|
||
const domesticPickupGroupCodes = new Set(
|
||
(salesProductGroupDictRows ?? [])
|
||
.filter((d: any) => String(d?.dictName ?? d?.DictName ?? '').includes('国内揽收'))
|
||
.map((d: any) => String(d?.dictCode ?? d?.DictCode ?? '').trim())
|
||
.filter((code: string) => Boolean(code)),
|
||
);
|
||
|
||
productOptions.value = (productRes.items ?? [])
|
||
.filter((row: any) => {
|
||
const groupCode = String(row?.productGroup ?? row?.ProductGroup ?? '').trim();
|
||
return domesticPickupGroupCodes.size ? !domesticPickupGroupCodes.has(groupCode) : true;
|
||
})
|
||
.map((row: any) => {
|
||
const code = String(row?.productCode ?? row?.ProductCode ?? '').trim();
|
||
const name = String(row?.productName ?? row?.ProductName ?? '').trim();
|
||
const id = Number(row?.id ?? row?.Id ?? 0);
|
||
const routeSortRaw = row?.routeSort ?? row?.RouteSort;
|
||
const routeSortNum = Number(routeSortRaw);
|
||
const minTimelinessRaw = row?.minTimeliness ?? row?.MinTimeliness;
|
||
const minTimelinessNum =
|
||
minTimelinessRaw == null || minTimelinessRaw === ''
|
||
? null
|
||
: Number(minTimelinessRaw);
|
||
return {
|
||
value: code,
|
||
label: name ? `${name} ${code}` : code,
|
||
productId: Number.isFinite(id) && id > 0 ? id : undefined,
|
||
routeSort: Number.isFinite(routeSortNum) ? routeSortNum : 0,
|
||
minTimeliness:
|
||
minTimelinessNum != null && Number.isFinite(minTimelinessNum)
|
||
? minTimelinessNum
|
||
: null,
|
||
name,
|
||
};
|
||
})
|
||
.filter((x) => x.value)
|
||
.sort((a, b) => {
|
||
if (a.routeSort !== b.routeSort) return a.routeSort - b.routeSort;
|
||
const aTl = a.minTimeliness ?? Number.MAX_SAFE_INTEGER;
|
||
const bTl = b.minTimeliness ?? Number.MAX_SAFE_INTEGER;
|
||
if (aTl !== bTl) return aTl - bTl;
|
||
const byName = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||
if (byName) return byName;
|
||
return a.value.localeCompare(b.value);
|
||
})
|
||
.map(({ value, label, productId }) => ({ value, label, productId }));
|
||
|
||
cargoOptions.value = (cargoRows ?? [])
|
||
.map((row: any) => ({
|
||
value: String(row?.dictCode ?? row?.DictCode ?? '').trim(),
|
||
label: String(row?.dictName ?? row?.DictName ?? '').trim(),
|
||
}))
|
||
.filter((x) => x.value);
|
||
if (!opts?.skipDefaults && cargoOptions.value.length && !form.cargoAttribute) {
|
||
form.cargoAttribute = cargoOptions.value[0].value;
|
||
}
|
||
|
||
warehouseOptions.value = (warehouseRes.items ?? [])
|
||
.map((row: any) => {
|
||
const id = row?.id ?? row?.Id;
|
||
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||
const pick = (...keys: string[]) => {
|
||
for (const k of keys) {
|
||
const v = row?.[k];
|
||
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
|
||
}
|
||
return '';
|
||
};
|
||
const sortRaw = row?.sort ?? row?.Sort;
|
||
const sortNum = sortRaw == null || sortRaw === '' ? null : Number(sortRaw);
|
||
return {
|
||
value: String(id ?? ''),
|
||
label: name || String(id),
|
||
recipient: pick('recipient', 'Recipient'),
|
||
contactPhone: pick('contactPhone', 'ContactPhone'),
|
||
shippingAddress: pick('shippingAddress', 'ShippingAddress'),
|
||
sort: sortNum != null && Number.isFinite(sortNum) ? sortNum : null,
|
||
remark: pick('remark', 'Remark'),
|
||
};
|
||
})
|
||
.filter((x) => x.value)
|
||
.sort(compareWarehouseSort);
|
||
if (!opts?.skipDefaults && warehouseOptions.value.length && !form.warehouseId) {
|
||
form.warehouseId = warehouseOptions.value[0].value;
|
||
}
|
||
|
||
currencyOptions.value = (currencyRows ?? [])
|
||
.map((row: any) => ({
|
||
value: String(row?.dictCode ?? row?.DictCode ?? '').trim(),
|
||
label: String(row?.dictName ?? row?.DictName ?? row?.dictCode ?? '').trim(),
|
||
}))
|
||
.filter((x) => x.value);
|
||
if (!opts?.skipDefaults) {
|
||
const usd = currencyOptions.value.find((x) => x.value.toUpperCase() === 'USD');
|
||
form.declaredCurrency = usd?.value || currencyOptions.value[0]?.value || 'USD';
|
||
}
|
||
|
||
const idMap: Record<string, string> = {};
|
||
countryOptions.value = (countryRes.items ?? [])
|
||
.map((row: any) => {
|
||
const code = String(row?.countryCode ?? row?.CountryCode ?? row?.code ?? '')
|
||
.trim()
|
||
.toUpperCase();
|
||
const name = String(
|
||
row?.chineseName ??
|
||
row?.ChineseName ??
|
||
row?.countryName ??
|
||
row?.CountryName ??
|
||
row?.name ??
|
||
'',
|
||
).trim();
|
||
const id = String(row?.id ?? row?.Id ?? '').trim();
|
||
if (id && code) idMap[id] = code;
|
||
return { value: code, label: name ? `${name} ${code}` : code };
|
||
})
|
||
.filter((x) => x.value);
|
||
countryIdToCode.value = idMap;
|
||
|
||
if (!opts?.skipDefaults) {
|
||
applyDraft(draft);
|
||
}
|
||
await Promise.all([
|
||
loadProductAdditionalServices(form.productCode),
|
||
loadDomesticPickupProductOptions()
|
||
.then((opts) => {
|
||
carrierServiceOptions.value = opts;
|
||
})
|
||
.catch(() => {
|
||
carrierServiceOptions.value = [];
|
||
}),
|
||
loadExportQualificationOptions({
|
||
applyDefault: !opts?.skipDefaults && needExportQualification.value,
|
||
}),
|
||
loadReceiverAddresses({ applyDefault: !opts?.skipDefaults }),
|
||
]);
|
||
if (!opts?.skipEnsureClientNo) {
|
||
ensureClientNo();
|
||
}
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.placeOrder.optionsFailed'));
|
||
} finally {
|
||
optionsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadSenders(opts?: { skipAutoSelect?: boolean }) {
|
||
senderLoading.value = true;
|
||
try {
|
||
const res = await getSenderAddressList({ page: 1, pageSize: 200, addressType: 'sender' });
|
||
senderOptions.value = (res.items || [])
|
||
.map((row: Record<string, unknown>) => {
|
||
const id = Number(pickField(row, 'id', 'Id'));
|
||
if (!Number.isFinite(id) || id <= 0) return null;
|
||
const name = String(pickField(row, 'senderName', 'SenderName') || '');
|
||
const isDefault = Boolean(pickField(row, 'isDefault', 'IsDefault'));
|
||
return {
|
||
id,
|
||
label: `${name || `#${id}`}${isDefault ? ` (${t('h5.placeOrder.default')})` : ''}`,
|
||
isDefault,
|
||
data: {
|
||
senderName: String(pickField(row, 'senderName', 'SenderName') || ''),
|
||
company: String(pickField(row, 'company', 'Company') || ''),
|
||
phone: String(pickField(row, 'phone', 'Phone') || ''),
|
||
telePhone: String(pickField(row, 'telePhone', 'TelePhone') || ''),
|
||
email: String(pickField(row, 'email', 'Email') || ''),
|
||
countryCode: String(pickField(row, 'countryCode', 'CountryCode') || ''),
|
||
province: String(pickField(row, 'province', 'Province') || ''),
|
||
city: String(pickField(row, 'city', 'City') || ''),
|
||
zipCode: String(pickField(row, 'zipCode', 'ZipCode') || ''),
|
||
streetAddress1: String(pickField(row, 'streetAddress1', 'StreetAddress1') || ''),
|
||
streetAddress2: String(pickField(row, 'streetAddress2', 'StreetAddress2') || ''),
|
||
streetAddress3: String(pickField(row, 'streetAddress3', 'StreetAddress3') || ''),
|
||
houseNumber: String(pickField(row, 'houseNumber', 'HouseNumber') || ''),
|
||
},
|
||
};
|
||
})
|
||
.filter(Boolean) as typeof senderOptions.value;
|
||
|
||
if (opts?.skipAutoSelect) return;
|
||
|
||
const def = (senderOptions.value as any[]).find((x) => x.isDefault) || senderOptions.value[0];
|
||
if (def) {
|
||
onSenderChange(def.id);
|
||
showSenderForm.value = false;
|
||
} else {
|
||
onSenderChange(undefined);
|
||
showSenderForm.value = true;
|
||
}
|
||
} catch {
|
||
senderOptions.value = [];
|
||
if (!opts?.skipAutoSelect) showSenderForm.value = true;
|
||
} finally {
|
||
senderLoading.value = false;
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => form.productCode,
|
||
(code) => {
|
||
void loadProductAdditionalServices(String(code ?? ''));
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => form.exportCustomsMode,
|
||
(mode) => {
|
||
if (isExportCustomsMode9610Summary(mode)) {
|
||
void loadExportQualificationOptions({ applyDefault: true });
|
||
} else {
|
||
form.exportQualificationId = '';
|
||
form.platformCode = '';
|
||
}
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() =>
|
||
items.value.map(
|
||
(row) => `${row.unitPrice ?? ''}|${row.unitWeight ?? ''}|${row.quantity ?? ''}|${row.amount ?? ''}`,
|
||
),
|
||
() => {
|
||
items.value.forEach((row) => syncItemDerived(row));
|
||
syncDeclaredTotalInputFromRows();
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => props.visible,
|
||
async (visible) => {
|
||
if (!visible) {
|
||
submitting.value = false;
|
||
showSenderForm.value = false;
|
||
editHasSender.value = false;
|
||
resetSenderForm();
|
||
receiverAiVisible.value = false;
|
||
receiverPasteText.value = '';
|
||
receiverParseTip.value = null;
|
||
receiverParseLoading.value = false;
|
||
declarationSkuVisible.value = false;
|
||
exportCustomsGuideVisible.value = false;
|
||
return;
|
||
}
|
||
resetForm();
|
||
void loadCurrentCustomerCode();
|
||
if (isEditMode.value) {
|
||
await loadOptions(null, { skipDefaults: true, skipEnsureClientNo: true });
|
||
await loadSenders({ skipAutoSelect: true });
|
||
await loadPickupAddresses();
|
||
await loadOrderForEdit(props.orderId as number | string);
|
||
if (needExportQualification.value) {
|
||
await loadExportQualificationOptions({ applyDefault: false });
|
||
}
|
||
return;
|
||
}
|
||
const draft = props.draft ?? readStoredDraft();
|
||
void loadOptions(draft);
|
||
void loadSenders();
|
||
void loadPickupAddresses();
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
</script>
|
||
|
||
<style lang="less">
|
||
.h5-po-drawer {
|
||
.arco-drawer-content {
|
||
border-radius: 20px 20px 0 0;
|
||
overflow: hidden;
|
||
max-height: min(92vh, 780px);
|
||
}
|
||
|
||
.arco-drawer-body {
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: min(92vh, 780px);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="less" scoped>
|
||
.h5-po {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: min(92vh, 780px);
|
||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||
background: #f5f7f8;
|
||
}
|
||
|
||
.h5-po__handle {
|
||
width: 36px;
|
||
height: 4px;
|
||
margin: 10px auto 0;
|
||
border-radius: 999px;
|
||
background: #c9cdd4;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.h5-po__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
height: 48px;
|
||
flex-shrink: 0;
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
}
|
||
}
|
||
|
||
.h5-po__close {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 36px;
|
||
height: 36px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
color: #86909c;
|
||
font-size: 18px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
.h5-po__steps {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin: 0 16px 10px;
|
||
padding: 3px;
|
||
border-radius: 12px;
|
||
background: #eef0f3;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.h5-po__step {
|
||
flex: 1;
|
||
height: 32px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #86909c;
|
||
cursor: pointer;
|
||
|
||
&.is-active,
|
||
&.is-done {
|
||
background: #fff;
|
||
color: #1d2129;
|
||
font-weight: 600;
|
||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
||
}
|
||
}
|
||
|
||
.h5-po__tip {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin: 0 16px 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||
color: #4e5969;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
|
||
button {
|
||
border: none;
|
||
background: transparent;
|
||
color: #86909c;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
padding: 0 2px;
|
||
}
|
||
}
|
||
|
||
.h5-po__spin {
|
||
flex: 1;
|
||
min-height: 220px;
|
||
overflow: auto;
|
||
display: block;
|
||
width: 100%;
|
||
padding: 0 16px 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.h5-po__item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.h5-po__card--currency {
|
||
.h5-po__label {
|
||
margin-bottom: 6px;
|
||
}
|
||
}
|
||
|
||
.h5-po__declare {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.h5-po__card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
||
|
||
&--currency {
|
||
padding-bottom: 14px;
|
||
}
|
||
|
||
:deep(.arco-input-wrapper),
|
||
:deep(.arco-textarea-wrapper),
|
||
:deep(.arco-select-view-single),
|
||
:deep(.arco-select-view-multiple) {
|
||
background: #f7f8fa !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
border-radius: 12px;
|
||
min-height: 40px;
|
||
}
|
||
|
||
:deep(.arco-input-wrapper:hover),
|
||
:deep(.arco-textarea-wrapper:hover),
|
||
:deep(.arco-select-view-single:hover) {
|
||
background: #f2f3f5 !important;
|
||
}
|
||
|
||
:deep(.arco-input-wrapper.arco-input-focus),
|
||
:deep(.arco-textarea-wrapper.arco-textarea-focus),
|
||
:deep(.arco-select-view-single.arco-select-view-focus) {
|
||
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff) !important;
|
||
}
|
||
|
||
:deep(.arco-input),
|
||
:deep(.arco-textarea) {
|
||
font-size: 14px;
|
||
color: #1d2129;
|
||
}
|
||
|
||
:deep(.arco-select-view-value),
|
||
:deep(.arco-select-view-input) {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.h5-po__field {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.h5-po__row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.h5-po__services {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 16px;
|
||
}
|
||
|
||
.h5-po__item-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
|
||
strong {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.h5-po__item-head-main {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 6px 10px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.h5-po__item-derived {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #86909c;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.h5-po__more {
|
||
margin-top: 2px;
|
||
height: 36px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: #f2f3f5;
|
||
color: var(--app-primary-color);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
|
||
&:active {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
&--inline {
|
||
margin-top: 0;
|
||
flex-shrink: 0;
|
||
height: 32px;
|
||
padding: 0 10px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.h5-po__street-number-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
min-width: 0;
|
||
|
||
:deep(.arco-input-wrapper) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
.h5-po__inbound {
|
||
display: flex;
|
||
width: 100%;
|
||
|
||
:deep(.arco-radio-button) {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.arco-radio-checked) {
|
||
background: var(--app-primary-color);
|
||
border-color: var(--app-primary-color);
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.h5-po__hint {
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #86909c;
|
||
|
||
&--tight {
|
||
margin-top: 0;
|
||
margin-bottom: 4px;
|
||
}
|
||
}
|
||
|
||
.h5-po__label--with-help {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.h5-po__help {
|
||
width: 22px;
|
||
height: 22px;
|
||
border: none;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
color: #86909c;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
|
||
&:active {
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.h5-po__summary-banner {
|
||
padding: 12px;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, #f77234 10%, #fff);
|
||
border: 1px solid color-mix(in srgb, #f77234 20%, #e5e6eb);
|
||
|
||
strong {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 650;
|
||
color: #d4380d;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: #4e5969;
|
||
}
|
||
}
|
||
|
||
.h5-po__receiver-toolbar {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.h5-po__field--grow {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.h5-po__save-addr {
|
||
flex-shrink: 0;
|
||
height: 40px;
|
||
padding: 0 12px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
color: var(--app-primary-color);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
|
||
&:disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.h5-po__declare-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
background: #fff;
|
||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.h5-po__sku-btn {
|
||
height: 40px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
color: var(--app-primary-color);
|
||
font-size: 14px;
|
||
font-weight: 650;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.h5-po__declare-bar-tip {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #86909c;
|
||
}
|
||
|
||
.h5-po__sku-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 12px;
|
||
background: #f7f8fa;
|
||
|
||
img {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 8px;
|
||
object-fit: cover;
|
||
display: block;
|
||
background: #fff;
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
color: #4e5969;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.h5-po__magnetic {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
min-height: 36px;
|
||
padding: 0 2px;
|
||
font-size: 13px;
|
||
color: #1d2129;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.h5-po__wh-card {
|
||
margin-top: 8px;
|
||
padding: 12px;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 16%, #e5e6eb);
|
||
}
|
||
|
||
.h5-po__wh-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
strong {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.h5-po__wh-list {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.h5-po__wh-item {
|
||
padding: 10px;
|
||
border-radius: 10px;
|
||
border: 1px solid #e5e6eb;
|
||
background: #f7f8fa;
|
||
|
||
&.is-selected {
|
||
border-color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
|
||
}
|
||
|
||
&.is-empty {
|
||
opacity: 0.72;
|
||
}
|
||
}
|
||
|
||
.h5-po__wh-item-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
|
||
strong {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
line-height: 20px;
|
||
}
|
||
}
|
||
|
||
.h5-po__wh-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 18px;
|
||
padding: 0 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 14%, #fff);
|
||
}
|
||
|
||
.h5-po__wh-copy {
|
||
border: none;
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
color: var(--app-primary-color);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
height: 28px;
|
||
padding: 0 10px;
|
||
border-radius: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.h5-po__wh-card p,
|
||
.h5-po__wh-item p {
|
||
margin: 8px 0 0;
|
||
font-size: 13px;
|
||
color: #1d2129;
|
||
line-height: 1.45;
|
||
span {
|
||
color: #86909c;
|
||
margin-right: 6px;
|
||
}
|
||
&.is-addr {
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.h5-po__wh-item .h5-po__hint {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.h5-po__more-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.h5-po__item-remove {
|
||
border: none;
|
||
background: var(--color-fill-2, #f2f3f5);
|
||
color: #86909c;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 4px 10px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.h5-po__add-item {
|
||
height: 44px;
|
||
border: none;
|
||
border-radius: 14px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||
color: var(--app-primary-color);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
|
||
&:active {
|
||
opacity: 0.88;
|
||
}
|
||
}
|
||
|
||
.h5-po__label {
|
||
display: block;
|
||
margin: 0 0 6px;
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
color: #4e5969;
|
||
}
|
||
|
||
.h5-po__sender-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
|
||
.h5-po__label {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.h5-po__link {
|
||
border: none;
|
||
background: transparent;
|
||
padding: 0;
|
||
color: var(--app-primary-color);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.h5-po__sender-empty {
|
||
margin: 0;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
background: #fff7e8;
|
||
color: #ad6800;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.h5-po__field-err {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #f53f3f;
|
||
|
||
&--block {
|
||
margin: 0 0 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: #ffece8;
|
||
}
|
||
}
|
||
|
||
.h5-po__sender-form {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.h5-po__btn--block {
|
||
width: 100%;
|
||
}
|
||
|
||
.h5-po__dims,
|
||
.h5-po__three {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.h5-po__dims--with-weight {
|
||
grid-template-columns: 1.15fr 1fr 1fr 1fr;
|
||
}
|
||
|
||
.h5-po__foot {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||
background: #fff;
|
||
box-shadow: 0 -6px 20px rgba(15, 23, 42, 0.04);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.h5-po__foot-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
gap: 10px;
|
||
|
||
.h5-po__btn {
|
||
flex: 1;
|
||
}
|
||
|
||
.h5-po__btn.is-ghost,
|
||
.h5-po__btn.is-ai {
|
||
flex: 0 0 auto;
|
||
}
|
||
}
|
||
|
||
.h5-po-fail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.h5-po-fail__hero {
|
||
padding: 12px 14px;
|
||
border-radius: 8px;
|
||
border-left: 3px solid #f53f3f;
|
||
background: #fff7f7;
|
||
}
|
||
|
||
.h5-po-fail__reason {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 1.45;
|
||
color: #1d2129;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.h5-po-fail__tip {
|
||
margin: 8px 0 0;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: #4e5969;
|
||
}
|
||
|
||
.h5-po-fail__tip-label {
|
||
margin-right: 4px;
|
||
color: var(--app-primary-color, #2d8c8c);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.h5-po-fail__status {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: #0fc6c2;
|
||
background: rgba(15, 198, 194, 0.08);
|
||
}
|
||
|
||
.h5-po-fail__sno {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.h5-po-fail__details {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
|
||
summary {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
}
|
||
|
||
.h5-po-fail__raw {
|
||
margin: 6px 0 0;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: #f7f8fa;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: #86909c;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.h5-po-fail__actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 4px;
|
||
|
||
.h5-po__btn {
|
||
flex: 1;
|
||
height: 44px;
|
||
}
|
||
}
|
||
|
||
.h5-po__btn {
|
||
flex: 1;
|
||
height: 48px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
|
||
&.is-mock {
|
||
flex: 0 0 auto;
|
||
min-width: 88px;
|
||
padding: 0 12px;
|
||
background: #fff7e8;
|
||
color: #d25f00;
|
||
border: 1px solid #ffcf8b;
|
||
font-size: 13px;
|
||
|
||
&:active:not(:disabled) {
|
||
background: #ffefd6;
|
||
}
|
||
}
|
||
|
||
&.is-ghost {
|
||
flex: 0 0 88px;
|
||
background: #f2f3f5;
|
||
color: #4e5969;
|
||
|
||
&:active:not(:disabled) {
|
||
background: #e5e6eb;
|
||
}
|
||
}
|
||
|
||
&.is-ai {
|
||
flex: 0 0 auto;
|
||
min-width: 88px;
|
||
padding: 0 12px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
color: var(--app-primary-color);
|
||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 35%, #e5e6eb);
|
||
box-shadow: none;
|
||
|
||
&:active:not(:disabled) {
|
||
background: color-mix(in srgb, var(--app-primary-color) 18%, #fff);
|
||
}
|
||
}
|
||
|
||
&.is-primary {
|
||
background: var(--app-primary-color);
|
||
color: #fff;
|
||
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
||
|
||
&:active:not(:disabled) {
|
||
opacity: 0.92;
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.h5-po-ai {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.h5-po-ai__hint {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
color: #86909c;
|
||
}
|
||
|
||
.h5-po-ai__config {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.h5-po-ai__select {
|
||
width: 100%;
|
||
min-width: 0;
|
||
|
||
&--skill {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.h5-po-ai__textarea {
|
||
:deep(textarea) {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.h5-po-ai__tip {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
|
||
&.is-ok {
|
||
color: #00a870;
|
||
}
|
||
|
||
&.is-warn {
|
||
color: #d48806;
|
||
}
|
||
|
||
&.is-danger {
|
||
color: #f53f3f;
|
||
}
|
||
}
|
||
|
||
.h5-po-ai__actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 4px;
|
||
|
||
.h5-po__btn {
|
||
height: 44px;
|
||
}
|
||
|
||
.is-ghost {
|
||
flex: 0 0 88px;
|
||
}
|
||
}
|
||
|
||
.h5-po__loading {
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: h5-po-spin 0.7s linear infinite;
|
||
}
|
||
|
||
@keyframes h5-po-spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style>
|