2368 lines
61 KiB
Vue
2368 lines
61 KiB
Vue
<template>
|
||
<div class="h5-price-page">
|
||
<!-- 查价卡片:手机原生交互 -->
|
||
<section class="quote-form h5-enter">
|
||
<div class="route-row">
|
||
<button type="button" class="route-cell" @click="openPicker('warehouse')">
|
||
<span class="route-cell__label">{{ t('h5.price.origin') }}</span>
|
||
<span class="route-cell__value">
|
||
<span>{{ warehouseLabel || t('h5.price.originAny') }}</span>
|
||
<icon-down />
|
||
</span>
|
||
</button>
|
||
<div class="route-mid" aria-hidden="true">
|
||
<span class="route-mid__icon">
|
||
<img :src="iconRoutePlane" alt="" class="route-mid__plane" draggable="false" />
|
||
</span>
|
||
</div>
|
||
<button type="button" class="route-cell" @click="openPicker('country')">
|
||
<span class="route-cell__label">{{ t('h5.price.destination') }}</span>
|
||
<span class="route-cell__value" :class="{ 'is-placeholder': !destinationCode }">
|
||
<span class="route-cell__dest">
|
||
<template v-if="destinationCode">
|
||
<b>{{ destinationName || destinationCode }}</b>
|
||
<em v-if="destinationName">{{ destinationCode }}</em>
|
||
</template>
|
||
<template v-else>{{ t('h5.price.countryPlaceholder') }}</template>
|
||
</span>
|
||
<icon-down />
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div ref="postalFieldRef" class="line-field">
|
||
<span class="line-field__label">{{ t('h5.price.postalCode') }}</span>
|
||
<input
|
||
v-model="form.postalCode"
|
||
class="line-field__input"
|
||
type="text"
|
||
inputmode="text"
|
||
:placeholder="t('h5.price.postalCodePh')"
|
||
autocomplete="postal-code"
|
||
/>
|
||
</div>
|
||
|
||
<div class="pkg-row">
|
||
<label class="pkg-cell">
|
||
<span class="pkg-cell__label">{{ t('h5.price.packageWeight') }}</span>
|
||
<span class="pkg-cell__value">
|
||
<input
|
||
v-model="form.cargoWeight"
|
||
type="text"
|
||
inputmode="decimal"
|
||
:placeholder="t('h5.price.weightPlaceholder')"
|
||
@keyup.enter="handleQuery"
|
||
/>
|
||
<em>KG</em>
|
||
</span>
|
||
</label>
|
||
<button type="button" class="pkg-cell pkg-cell--tap" @click="openPicker('cargo')">
|
||
<span class="pkg-cell__label">{{ t('h5.price.cargoAttr') }}</span>
|
||
<span class="pkg-cell__value">
|
||
<strong :class="{ 'is-placeholder': !cargoLabel }">
|
||
{{ cargoLabel || t('h5.price.cargoPick') }}
|
||
</strong>
|
||
<icon-down />
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<button type="button" class="dim-toggle" @click="showDimensions = !showDimensions">
|
||
<span>{{ t('h5.price.dimensionsToggle') }}</span>
|
||
<em>{{ showDimensions ? '−' : '+' }}</em>
|
||
</button>
|
||
<div v-if="showDimensions" class="dim-row">
|
||
<label class="dim-cell">
|
||
<span>{{ t('h5.price.length') }}</span>
|
||
<input v-model="form.length" type="text" inputmode="decimal" placeholder="cm" />
|
||
</label>
|
||
<label class="dim-cell">
|
||
<span>{{ t('h5.price.width') }}</span>
|
||
<input v-model="form.width" type="text" inputmode="decimal" placeholder="cm" />
|
||
</label>
|
||
<label class="dim-cell">
|
||
<span>{{ t('h5.price.height') }}</span>
|
||
<input v-model="form.height" type="text" inputmode="decimal" placeholder="cm" />
|
||
</label>
|
||
</div>
|
||
<p v-if="showDimensions" class="dim-hint">{{ t('h5.price.dimensionsHint') }}</p>
|
||
|
||
<button
|
||
type="button"
|
||
class="quote-cta"
|
||
:disabled="queryLoading"
|
||
@click="handleQuery"
|
||
>
|
||
<span v-if="queryLoading" class="quote-cta__loading" aria-hidden="true" />
|
||
<template v-else>{{ t('h5.price.query') }}</template>
|
||
<template v-if="queryLoading">{{ t('h5.price.querying') }}</template>
|
||
</button>
|
||
|
||
<div class="quote-foot">
|
||
<span v-if="summaryText" class="quote-summary">{{ summaryText }}</span>
|
||
<p class="quote-ref">{{ t('h5.price.referenceTip') }}</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="queried" class="h5-price-results">
|
||
<div v-if="sortedResults.length" class="h5-ai-bar">
|
||
<div class="h5-ai-bar__head">
|
||
<strong>{{ t('h5.price.ai.title') }}</strong>
|
||
</div>
|
||
<div class="h5-ai-bar__row">
|
||
<input
|
||
v-model="aiProductName"
|
||
class="h5-ai-bar__input"
|
||
type="text"
|
||
:placeholder="t('h5.price.ai.placeholder')"
|
||
maxlength="80"
|
||
@keyup.enter="handleAiRecommend"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="h5-ai-bar__btn"
|
||
:disabled="aiRecommendLoading || !aiProductName.trim()"
|
||
@click="handleAiRecommend"
|
||
>
|
||
{{ aiRecommendLoading ? '…' : t('h5.price.ai.action') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="recommendVerdict"
|
||
class="h5-ai-insight"
|
||
:class="`is-${recommendVerdict}`"
|
||
>
|
||
<div class="h5-ai-insight__top">
|
||
<span class="h5-ai-insight__verdict">
|
||
{{ t(`order.priceCheck.ai.verdict.${recommendVerdict}`) }}
|
||
</span>
|
||
<span v-if="recommendCargoName" class="h5-ai-insight__cargo">
|
||
{{ t('order.priceCheck.ai.cargoLabel', { name: recommendCargoName }) }}
|
||
</span>
|
||
</div>
|
||
<p v-if="recommendReason" class="h5-ai-insight__reason">{{ recommendReason }}</p>
|
||
<ul v-if="recommendEvidence.length" class="h5-ai-insight__evidence">
|
||
<li v-for="(line, idx) in recommendEvidence" :key="idx">{{ line }}</li>
|
||
</ul>
|
||
<div v-if="recommendAlternatives.length" class="h5-ai-insight__alts">
|
||
<button
|
||
v-for="alt in recommendAlternatives"
|
||
:key="alt.rowKey"
|
||
type="button"
|
||
class="h5-ai-alt"
|
||
:class="`is-${alt.verdict}`"
|
||
@click="selectAiAlternative(alt.rowKey)"
|
||
>
|
||
{{ t(`order.priceCheck.ai.verdict.${alt.verdict}`) }} · {{ alt.blurb }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="needPostalHint && !hintBannerDismissed"
|
||
class="h5-accuracy"
|
||
>
|
||
<span>{{ t('h5.price.accuracy.postalCta') }}</span>
|
||
<div class="h5-accuracy__actions">
|
||
<button type="button" class="h5-accuracy__fill" @click="focusPostal">
|
||
{{ t('h5.price.accuracy.fillPostal') }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="h5-accuracy__dismiss"
|
||
:aria-label="t('h5.price.accuracy.dismiss')"
|
||
@click="hintBannerDismissed = true"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="sortedResults.length" class="h5-results-bar">
|
||
<p v-if="showQueryHintBanner && !needPostalHint" class="h5-disclaimer">
|
||
{{ queryHintSummary }}
|
||
</p>
|
||
<p v-else-if="!needPostalHint" class="h5-disclaimer">
|
||
{{ t('h5.price.referenceBanner') }}
|
||
</p>
|
||
<div class="h5-sort">
|
||
<button
|
||
v-for="mode in sortModes"
|
||
:key="mode"
|
||
type="button"
|
||
class="h5-sort__btn"
|
||
:class="{ 'is-active': sortMode === mode }"
|
||
@click="sortMode = mode"
|
||
>
|
||
{{ t(`h5.price.sort.${mode}`) }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<a-spin :loading="queryLoading" class="h5-price-spin">
|
||
<div v-if="!sortedResults.length && !queryLoading" class="h5-empty">
|
||
<div class="h5-empty__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="28" cy="28" r="14" fill="none" stroke="currentColor" stroke-width="3" />
|
||
<path d="M38 38 50 50" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
||
<path d="M22 28h12M28 22v12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
<p>{{ queryFailureText || t('h5.price.noResult') }}</p>
|
||
<a-button type="outline" size="small" @click="askAdvisorEmpty">
|
||
{{ t('h5.price.askAdvisor') }}
|
||
</a-button>
|
||
</div>
|
||
|
||
<article
|
||
v-for="item in sortedResults"
|
||
:key="item.rowKey"
|
||
:ref="(el) => setQuoteRef(item.rowKey, el)"
|
||
class="h5-quote"
|
||
:class="{
|
||
'h5-quote--top': isRecommendStamp(item),
|
||
'h5-quote--ai': item.rowKey === recommendedRowKey,
|
||
}"
|
||
@click="openDetail(item)"
|
||
>
|
||
<span
|
||
v-if="isRecommendStamp(item)"
|
||
class="h5-quote__stamp"
|
||
aria-hidden="true"
|
||
>
|
||
{{ t('h5.price.recommendTag') }}
|
||
</span>
|
||
<div class="h5-quote__main">
|
||
<div class="h5-quote__head">
|
||
<div class="h5-quote__title">
|
||
<h3>{{ item.shippingMethod || item.productCode || '-' }}</h3>
|
||
<p class="h5-quote__sub">
|
||
<span
|
||
v-if="item.productGroupName"
|
||
class="h5-quote__chip h5-quote__chip--group"
|
||
>
|
||
{{ item.productGroupName }}
|
||
</span>
|
||
<span class="h5-quote__chip h5-quote__chip--cargo">
|
||
{{ item.cargoTypeName || item.cargoType || '-' }}
|
||
</span>
|
||
<span
|
||
v-if="item.tradeTerms === 'DDP' || item.tradeTerms === 'DDU'"
|
||
class="h5-quote__chip h5-quote__chip--trade"
|
||
>
|
||
{{ item.tradeTerms }}
|
||
</span>
|
||
<span v-if="item.isPriceOptimal" class="h5-quote__tag is-price">
|
||
{{ t('h5.price.tag.priceOptimal') }}
|
||
</span>
|
||
<span v-if="item.isTimeOptimal" class="h5-quote__tag is-time">
|
||
{{ t('h5.price.tag.timeOptimal') }}
|
||
</span>
|
||
<span v-if="item.chargeableWeight" class="h5-quote__weight">
|
||
{{ item.chargeableWeight }}kg
|
||
</span>
|
||
</p>
|
||
</div>
|
||
<div class="h5-quote__eta">
|
||
<strong class="h5-quote__eta-value">{{ item.deliveryTime || '-' }}</strong>
|
||
<span class="h5-quote__eta-label">{{ t('h5.price.workingDays') }}</span>
|
||
</div>
|
||
<div class="h5-quote__price-block">
|
||
<div class="h5-quote__price">
|
||
<small>¥</small>{{ formatMoney(item.totalCost) }}
|
||
</div>
|
||
<span class="ref-label">{{ t('h5.price.referencePrice') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h5-quote__actions" @click.stop>
|
||
<span class="h5-quote__chip h5-quote__chip--wh">
|
||
{{ item.warehouse || '-' }}
|
||
</span>
|
||
<div class="h5-quote__action-btns">
|
||
<button type="button" class="link-btn" @click="openDetail(item)">
|
||
{{ t('h5.price.viewDetail') }}
|
||
</button>
|
||
<a-dropdown trigger="click" position="top">
|
||
<button type="button" class="link-btn">
|
||
{{ t('h5.price.askAdvisor') }}
|
||
</button>
|
||
<template #content>
|
||
<a-doption @click="askAdvisor(item, 'canShip')">
|
||
{{ t('h5.price.askAiCanShip') }}
|
||
</a-doption>
|
||
<a-doption @click="askAdvisor(item, 'cheaper')">
|
||
{{ t('h5.price.askAiCheaper') }}
|
||
</a-doption>
|
||
</template>
|
||
</a-dropdown>
|
||
<button type="button" class="order-btn" @click="goOrder(item)">
|
||
{{ t('h5.price.goOrder') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</a-spin>
|
||
</section>
|
||
|
||
<div v-else class="h5-empty h5-price-empty">
|
||
<div class="h5-empty__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="10" y="22" width="44" height="28" rx="6" fill="currentColor" opacity="0.12" />
|
||
<path
|
||
d="M16 30h20M16 38h12"
|
||
stroke="currentColor"
|
||
stroke-width="2.5"
|
||
stroke-linecap="round"
|
||
/>
|
||
<path
|
||
fill="currentColor"
|
||
d="M46 18.5 52 21l-6 2.5V18.5Zm-18 4.5 20-6.5 1.5 4.5-20 6.5-1.5-4.5Z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<p>{{ t('h5.price.empty') }}</p>
|
||
</div>
|
||
|
||
<!-- 选择器:国家 / 仓库 / 属性 -->
|
||
<a-drawer
|
||
v-model:visible="pickerVisible"
|
||
:title="pickerTitle"
|
||
placement="bottom"
|
||
:height="pickerHeight"
|
||
unmount-on-close
|
||
:footer="false"
|
||
>
|
||
<div v-if="pickerType === 'country'" class="picker">
|
||
<input
|
||
v-model="countryKeyword"
|
||
class="picker-search"
|
||
type="search"
|
||
:placeholder="t('h5.price.countrySearchPh')"
|
||
/>
|
||
<div class="picker-shortcuts">
|
||
<button
|
||
v-for="item in shortcutCountries"
|
||
:key="item.value"
|
||
type="button"
|
||
class="quick-chip"
|
||
:class="{ 'is-active': destinationCode === item.value }"
|
||
@click="onPickCountry(item.value)"
|
||
>
|
||
<span>{{ item.name }}</span>
|
||
<em>{{ item.value }}</em>
|
||
</button>
|
||
</div>
|
||
<button
|
||
v-for="opt in filteredCountries"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="picker-item"
|
||
:class="{ 'is-active': destinationCode === opt.value }"
|
||
@click="onPickCountry(opt.value)"
|
||
>
|
||
<span class="picker-item__name">{{ opt.name || opt.value }}</span>
|
||
<span class="picker-item__code">{{ opt.value }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else-if="pickerType === 'warehouse'" class="picker">
|
||
<button
|
||
type="button"
|
||
class="picker-item"
|
||
:class="{ 'is-active': !form.warehouseId }"
|
||
@click="onPickWarehouse(undefined)"
|
||
>
|
||
{{ t('h5.price.originAny') }}
|
||
</button>
|
||
<button
|
||
v-for="opt in warehouseOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="picker-item"
|
||
:class="{ 'is-active': form.warehouseId === opt.value }"
|
||
@click="onPickWarehouse(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else class="picker">
|
||
<button
|
||
type="button"
|
||
class="picker-item"
|
||
:class="{ 'is-active': !form.cargoType }"
|
||
@click="onPickCargo('')"
|
||
>
|
||
{{ t('h5.price.cargoAny') }}
|
||
</button>
|
||
<button
|
||
v-for="opt in cargoOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="picker-item"
|
||
:class="{ 'is-active': form.cargoType === opt.value }"
|
||
@click="onPickCargo(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
</a-drawer>
|
||
|
||
<!-- 线路详情 -->
|
||
<a-drawer
|
||
v-model:visible="detailVisible"
|
||
:title="detailTitle"
|
||
placement="bottom"
|
||
:height="'78%'"
|
||
unmount-on-close
|
||
:footer="false"
|
||
>
|
||
<a-spin :loading="detailLoading" class="h5-detail-spin">
|
||
<div v-if="detailItem" class="h5-detail">
|
||
<section class="h5-detail__summary">
|
||
<div class="h5-detail__price">
|
||
<span>{{ t('h5.price.referencePrice') }}</span>
|
||
<strong>¥{{ formatMoney(detailItem.totalCost) }}</strong>
|
||
</div>
|
||
<p class="h5-detail__ref">{{ t('h5.price.referenceTip') }}</p>
|
||
<div class="h5-detail__grid">
|
||
<div v-if="detailItem.productGroupName">
|
||
<label>{{ t('h5.price.productGroup') }}</label>
|
||
<p>{{ detailItem.productGroupName }}</p>
|
||
</div>
|
||
<div>
|
||
<label>{{ t('h5.price.cargo') }}</label>
|
||
<p>{{ detailItem.cargoTypeName || detailItem.cargoType || '-' }}</p>
|
||
</div>
|
||
<div>
|
||
<label>{{ t('h5.price.warehouse') }}</label>
|
||
<p>{{ detailItem.warehouse || '-' }}</p>
|
||
</div>
|
||
<div v-if="detailItem.tradeTerms === 'DDP' || detailItem.tradeTerms === 'DDU'">
|
||
<label>{{ t('h5.price.tradeTerms') }}</label>
|
||
<p>{{ detailItem.tradeTerms }}</p>
|
||
</div>
|
||
<div>
|
||
<label>{{ t('h5.price.delivery') }}</label>
|
||
<p>{{ detailItem.deliveryTime || '-' }}</p>
|
||
</div>
|
||
<div>
|
||
<label>{{ t('h5.price.chargeableWeight') }}</label>
|
||
<p>{{ detailItem.chargeableWeight || '-' }}</p>
|
||
</div>
|
||
<div v-if="detailItem.freight !== '' && detailItem.freight != null">
|
||
<label>{{ t('h5.price.freight') }}</label>
|
||
<p>¥{{ formatMoney(detailItem.freight) }}</p>
|
||
</div>
|
||
<div v-if="detailItem.otherFees !== '' && detailItem.otherFees != null">
|
||
<label>{{ t('h5.price.otherFees') }}</label>
|
||
<p>¥{{ formatMoney(detailItem.otherFees) }}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="h5-detail__guide">
|
||
<h4>{{ t('h5.price.routeGuide') }}</h4>
|
||
<div v-if="guideHtml" class="h5-md" v-html="guideHtml" />
|
||
<a-empty v-else-if="!detailLoading" :description="t('h5.price.guideEmpty')" />
|
||
</section>
|
||
|
||
<div class="h5-detail__footer">
|
||
<a-dropdown trigger="click" position="top" class="h5-detail__ask-drop">
|
||
<a-button long>{{ t('h5.price.askAdvisor') }}</a-button>
|
||
<template #content>
|
||
<a-doption @click="askAdvisor(detailItem, 'canShip')">
|
||
{{ t('h5.price.askAiCanShip') }}
|
||
</a-doption>
|
||
<a-doption @click="askAdvisor(detailItem, 'cheaper')">
|
||
{{ t('h5.price.askAiCheaper') }}
|
||
</a-doption>
|
||
</template>
|
||
</a-dropdown>
|
||
<a-button type="primary" long @click="goOrder(detailItem)">
|
||
{{ t('h5.price.goOrder') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</a-drawer>
|
||
|
||
<H5PlaceOrderSheet
|
||
v-model:visible="placeOrderVisible"
|
||
:draft="placeOrderDraft"
|
||
/>
|
||
<H5CompleteContactSheet
|
||
v-model:visible="contactVisible"
|
||
:profile="contactProfile"
|
||
@success="onContactSuccess"
|
||
@cancel="onContactCancel"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, nextTick, onMounted, reactive, ref, type ComponentPublicInstance } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { Message } from '@arco-design/web-vue';
|
||
import { IconDown } from '@arco-design/web-vue/es/icon';
|
||
import type { CustomerProfileCompleteness } from '@/api/customer';
|
||
import { postOrderPriceCheck, type PriceCheckQueryHint } from '@/api/order';
|
||
import { getCountryList } from '@/api/system/country';
|
||
import { getWarehouseList } from '@/api/system/warehouse';
|
||
import { getDictListAll } from '@/api/system/dict';
|
||
import { getSalesProductDetail } from '@/api/channel/salesProduct';
|
||
import { h5Navigate } from '@/utils/device';
|
||
import { checkCustomerContactRequired } from '@/utils/customerContactGate';
|
||
import iconRoutePlane from '@/assets/icons/h5/route-plane.svg?url';
|
||
import {
|
||
getPriceAskLocation,
|
||
type PriceAskIntent,
|
||
} from '@/composables/priceAskAdvisor';
|
||
import {
|
||
recommendWithAi,
|
||
type QuoteCandidate,
|
||
type RecommendAlternative,
|
||
type ShipVerdict,
|
||
} from '@/views/order/priceCheck/recommendChannel';
|
||
import H5PlaceOrderSheet from '../components/H5PlaceOrderSheet.vue';
|
||
import H5CompleteContactSheet from '../components/H5CompleteContactSheet.vue';
|
||
|
||
defineOptions({ name: 'H5Price' });
|
||
|
||
const PRICE_CHECK_ORDER_DRAFT_KEY = 'oms_price_check_order_draft';
|
||
const countryShortcutCodes = ['US', 'GB', 'DE', 'CA', 'FR', 'ES'];
|
||
|
||
const { t } = useI18n();
|
||
const router = useRouter();
|
||
const placeOrderVisible = ref(false);
|
||
const placeOrderDraft = ref<Record<string, any> | null>(null);
|
||
const contactVisible = ref(false);
|
||
const contactProfile = ref<CustomerProfileCompleteness | null>(null);
|
||
let pendingOpenOrder = false;
|
||
|
||
const form = reactive({
|
||
destinationCountry: '',
|
||
cargoWeight: '1',
|
||
postalCode: '',
|
||
cargoType: '',
|
||
warehouseId: undefined as number | undefined,
|
||
length: '',
|
||
width: '',
|
||
height: '',
|
||
});
|
||
|
||
const showDimensions = ref(false);
|
||
const postalFieldRef = ref<HTMLElement | null>(null);
|
||
const quoteElMap = new Map<string, HTMLElement>();
|
||
|
||
type Opt = { value: string; label: string; name?: string };
|
||
type WhOpt = { value: number; label: string };
|
||
|
||
const countryOptions = ref<Opt[]>([]);
|
||
const warehouseOptions = ref<WhOpt[]>([]);
|
||
const cargoOptions = ref<Opt[]>([]);
|
||
const countryKeyword = ref('');
|
||
const optionsLoading = ref(false);
|
||
const queryLoading = ref(false);
|
||
const queried = ref(false);
|
||
const queryFailureText = ref('');
|
||
const results = ref<any[]>([]);
|
||
const queryHint = ref<PriceCheckQueryHint | null>(null);
|
||
const hintBannerDismissed = ref(false);
|
||
const sortMode = ref<'recommend' | 'price' | 'time'>('recommend');
|
||
const sortModes = ['recommend', 'price', 'time'] as const;
|
||
|
||
const aiProductName = ref('');
|
||
const aiRecommendLoading = ref(false);
|
||
const recommendedRowKey = ref('');
|
||
const recommendReason = ref('');
|
||
const recommendVerdict = ref<ShipVerdict | ''>('');
|
||
const recommendEvidence = ref<string[]>([]);
|
||
const recommendCargoName = ref('');
|
||
const recommendAlternatives = ref<RecommendAlternative[]>([]);
|
||
|
||
const pickerVisible = ref(false);
|
||
const pickerType = ref<'country' | 'warehouse' | 'cargo'>('country');
|
||
|
||
const detailVisible = ref(false);
|
||
const detailLoading = ref(false);
|
||
const detailItem = ref<any>(null);
|
||
const guideHtml = ref('');
|
||
|
||
function parseOptionalDimension(value: unknown) {
|
||
const n = parseFloat(String(value ?? '').trim());
|
||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||
}
|
||
|
||
function clearRecommendInsight() {
|
||
recommendedRowKey.value = '';
|
||
recommendReason.value = '';
|
||
recommendVerdict.value = '';
|
||
recommendEvidence.value = [];
|
||
recommendCargoName.value = '';
|
||
recommendAlternatives.value = [];
|
||
}
|
||
|
||
function applyRecommendInsight(result: {
|
||
rowKey: string;
|
||
reason: string;
|
||
verdict: ShipVerdict;
|
||
evidence: string[];
|
||
cargoName: string;
|
||
alternatives: RecommendAlternative[];
|
||
}) {
|
||
recommendedRowKey.value = result.rowKey;
|
||
recommendReason.value = result.reason;
|
||
recommendVerdict.value = result.verdict;
|
||
recommendEvidence.value = result.evidence || [];
|
||
recommendCargoName.value = result.cargoName || '';
|
||
recommendAlternatives.value = result.alternatives || [];
|
||
}
|
||
|
||
function setQuoteRef(rowKey: string, el: Element | ComponentPublicInstance | null) {
|
||
if (!el) {
|
||
quoteElMap.delete(rowKey);
|
||
return;
|
||
}
|
||
const node = (el as ComponentPublicInstance).$el
|
||
? ((el as ComponentPublicInstance).$el as HTMLElement)
|
||
: (el as HTMLElement);
|
||
if (node) quoteElMap.set(rowKey, node);
|
||
}
|
||
|
||
function scrollToQuote(rowKey: string) {
|
||
nextTick(() => {
|
||
quoteElMap.get(rowKey)?.scrollIntoView?.({ behavior: 'smooth', block: 'center' });
|
||
});
|
||
}
|
||
|
||
function isRecommendStamp(item: any) {
|
||
if (recommendedRowKey.value) return item.rowKey === recommendedRowKey.value;
|
||
return sortMode.value === 'recommend' && !!item.isPriceOptimal;
|
||
}
|
||
|
||
function resolveCountryName(code: string, opt?: Opt | null) {
|
||
const iso = String(code || opt?.value || '').trim().toUpperCase();
|
||
const fromOpt = String(opt?.name || '').trim();
|
||
// 忽略「名称=二字码」的无效值,继续走中文兜底
|
||
if (fromOpt && fromOpt.toUpperCase() !== iso) return fromOpt;
|
||
const key = `h5.price.shortcut.${iso}`;
|
||
const fromLocale = t(key);
|
||
if (fromLocale && fromLocale !== key && fromLocale.toUpperCase() !== iso) return fromLocale;
|
||
return '';
|
||
}
|
||
|
||
const selectedCountry = computed(() => {
|
||
const code = String(form.destinationCountry || '').trim().toUpperCase();
|
||
if (!code) return undefined;
|
||
return countryOptions.value.find((c) => c.value.toUpperCase() === code);
|
||
});
|
||
|
||
const destinationCode = computed(() =>
|
||
String(form.destinationCountry || selectedCountry.value?.value || '')
|
||
.trim()
|
||
.toUpperCase(),
|
||
);
|
||
|
||
const destinationName = computed(() =>
|
||
resolveCountryName(destinationCode.value, selectedCountry.value),
|
||
);
|
||
|
||
const destinationDisplay = computed(() => {
|
||
if (!destinationCode.value) return '';
|
||
if (destinationName.value) return `${destinationName.value} ${destinationCode.value}`;
|
||
return destinationCode.value;
|
||
});
|
||
|
||
const warehouseLabel = computed(() => {
|
||
if (!form.warehouseId) return '';
|
||
return warehouseOptions.value.find((w) => w.value === form.warehouseId)?.label || '';
|
||
});
|
||
|
||
const cargoLabel = computed(() => {
|
||
if (!form.cargoType) return '';
|
||
return cargoOptions.value.find((c) => c.value === form.cargoType)?.label || form.cargoType;
|
||
});
|
||
|
||
const summaryText = computed(() => {
|
||
const dest = destinationDisplay.value;
|
||
if (!dest && !form.cargoWeight) return '';
|
||
const origin = warehouseLabel.value || t('h5.price.originAny');
|
||
const zip = form.postalCode.trim();
|
||
const destPart = zip ? `${dest}(${zip})` : dest || '-';
|
||
const weight = form.cargoWeight ? `${form.cargoWeight}KG` : '-';
|
||
const cargo = cargoLabel.value || t('h5.price.cargoAny');
|
||
return `${origin}-${destPart} | ${weight} | ${cargo}`;
|
||
});
|
||
|
||
const shortcutCountries = computed(() =>
|
||
countryShortcutCodes
|
||
.map((code) => {
|
||
const hit = countryOptions.value.find((c) => c.value.toUpperCase() === code);
|
||
return {
|
||
value: code,
|
||
name: resolveCountryName(code, hit) || code,
|
||
};
|
||
})
|
||
.filter((item) => item.value),
|
||
);
|
||
|
||
const filteredCountries = computed(() => {
|
||
const q = countryKeyword.value.trim().toLowerCase();
|
||
if (!q) return countryOptions.value;
|
||
return countryOptions.value.filter(
|
||
(c) =>
|
||
c.value.toLowerCase().includes(q) ||
|
||
(c.name || '').toLowerCase().includes(q) ||
|
||
c.label.toLowerCase().includes(q),
|
||
);
|
||
});
|
||
|
||
const pickerTitle = computed(() => {
|
||
if (pickerType.value === 'warehouse') return t('h5.price.origin');
|
||
if (pickerType.value === 'cargo') return t('h5.price.cargoAttr');
|
||
return t('h5.price.destination');
|
||
});
|
||
|
||
const pickerHeight = computed(() => (pickerType.value === 'country' ? '72%' : '50%'));
|
||
|
||
const detailTitle = computed(() => {
|
||
const name = detailItem.value?.shippingMethod || detailItem.value?.productCode || '';
|
||
return name ? t('h5.price.detailTitleNamed', { name }) : t('h5.price.detailTitle');
|
||
});
|
||
|
||
const needPostalHint = computed(() => {
|
||
if (!queried.value) return false;
|
||
if (String(form.postalCode || '').trim()) return false;
|
||
const missing = queryHint.value?.missingFieldCodes ?? [];
|
||
return (
|
||
queryHint.value?.quoteLevel === 'estimated' ||
|
||
missing.includes('postalCode') ||
|
||
missing.includes('PostalCode')
|
||
);
|
||
});
|
||
|
||
const showQueryHintBanner = computed(
|
||
() =>
|
||
Boolean(queryHint.value?.disclaimers?.length) ||
|
||
queryHint.value?.quoteLevel === 'estimated',
|
||
);
|
||
|
||
const queryHintSummary = computed(() => {
|
||
if (!showQueryHintBanner.value) return '';
|
||
return t('h5.price.hint.estimated');
|
||
});
|
||
|
||
function formatMoney(val: unknown) {
|
||
const n = Number(String(val ?? '').replace(/[^\d.]/g, ''));
|
||
if (!Number.isFinite(n)) return String(val ?? '-');
|
||
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
function parseTotalCost(cost: unknown) {
|
||
if (cost == null || cost === '') return Infinity;
|
||
if (typeof cost === 'number') return cost;
|
||
const num = parseFloat(String(cost).replace(/[^\d.]/g, ''));
|
||
return Number.isNaN(num) ? Infinity : num;
|
||
}
|
||
|
||
function escapeHtml(text: string) {
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
async function markdownToHtml(md: string) {
|
||
const text = String(md || '').trim();
|
||
if (!text) return '';
|
||
try {
|
||
const { marked } = await import('marked');
|
||
marked.setOptions({ gfm: true, breaks: true });
|
||
return String(marked.parse(text));
|
||
} catch {
|
||
return `<pre>${escapeHtml(text)}</pre>`;
|
||
}
|
||
}
|
||
|
||
function normalizeRow(raw: any, index: number) {
|
||
const deliveryTime = String(raw.deliveryTime ?? raw.DeliveryTime ?? '');
|
||
const match = deliveryTime.match(/(\d+)-(\d+)/);
|
||
const maxDeliveryDays = match ? parseInt(match[2], 10) : 0;
|
||
const totalCost = raw.totalCost ?? raw.TotalCost ?? '';
|
||
const cargoType = raw.cargoType ?? raw.CargoType ?? '';
|
||
const cargoTypeName = raw.cargoTypeName ?? raw.CargoTypeName ?? cargoType ?? '';
|
||
return {
|
||
...raw,
|
||
productId: raw.id ?? raw.Id ?? raw.productId ?? raw.ProductId,
|
||
productCode: raw.productCode ?? raw.ProductCode ?? '',
|
||
shippingMethod: raw.shippingMethod ?? raw.ShippingMethod ?? '',
|
||
cargoType,
|
||
cargoTypeName: String(cargoTypeName || '').trim(),
|
||
warehouseId: raw.warehouseId ?? raw.WarehouseId,
|
||
warehouse: String(raw.warehouse ?? raw.Warehouse ?? '').trim(),
|
||
productGroup: String(raw.productGroup ?? raw.ProductGroup ?? '').trim(),
|
||
productGroupName: String(
|
||
raw.productGroupName ?? raw.ProductGroupName ?? raw.productGroup ?? raw.ProductGroup ?? '',
|
||
).trim(),
|
||
tradeTerms: (() => {
|
||
const rawTerms = String(raw.tradeTerms ?? raw.TradeTerms ?? '')
|
||
.trim()
|
||
.toUpperCase();
|
||
return rawTerms === 'DDP' || rawTerms === 'DDU' ? rawTerms : '';
|
||
})(),
|
||
deliveryTime,
|
||
chargeableWeight: raw.chargeableWeight ?? raw.ChargeableWeight ?? '',
|
||
freight: raw.freight ?? raw.Freight ?? '',
|
||
otherFees: raw.otherFees ?? raw.OtherFees ?? '',
|
||
totalCost,
|
||
totalCostNum: parseTotalCost(totalCost),
|
||
maxDeliveryDays,
|
||
quoteLevel: String(raw.quoteLevel ?? raw.QuoteLevel ?? '').toLowerCase(),
|
||
rowKey: [
|
||
raw.id ?? raw.productCode ?? 'p',
|
||
cargoType || 'c',
|
||
raw.warehouseId ?? raw.warehouse ?? 'w',
|
||
index,
|
||
].join('-'),
|
||
};
|
||
}
|
||
|
||
function processQueryResults(items: any[]) {
|
||
if (!items?.length) return [];
|
||
const processed = items.map((item, index) => normalizeRow(item, index));
|
||
const minCost = Math.min(...processed.map((item) => item.totalCostNum));
|
||
const minDays = Math.min(...processed.map((item) => item.maxDeliveryDays));
|
||
processed.forEach((item) => {
|
||
item.isPriceOptimal = item.totalCostNum === minCost;
|
||
item.isTimeOptimal = item.maxDeliveryDays === minDays;
|
||
});
|
||
return processed.sort((a, b) => {
|
||
if (a.isPriceOptimal && !b.isPriceOptimal) return -1;
|
||
if (!a.isPriceOptimal && b.isPriceOptimal) return 1;
|
||
if (a.isTimeOptimal && !b.isTimeOptimal) return -1;
|
||
if (!a.isTimeOptimal && b.isTimeOptimal) return 1;
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
const sortedResults = computed(() => {
|
||
const list = [...results.value];
|
||
if (sortMode.value === 'price') {
|
||
return list.sort((a, b) => a.totalCostNum - b.totalCostNum);
|
||
}
|
||
if (sortMode.value === 'time') {
|
||
return list.sort(
|
||
(a, b) => a.maxDeliveryDays - b.maxDeliveryDays || a.totalCostNum - b.totalCostNum,
|
||
);
|
||
}
|
||
// recommend:保持 processQueryResults 的综合序
|
||
return list;
|
||
});
|
||
|
||
function openPicker(type: 'country' | 'warehouse' | 'cargo') {
|
||
pickerType.value = type;
|
||
if (type === 'country') countryKeyword.value = '';
|
||
pickerVisible.value = true;
|
||
}
|
||
|
||
function pickCountry(code: string) {
|
||
form.destinationCountry = String(code || '').trim().toUpperCase();
|
||
}
|
||
|
||
function pickWarehouse(id?: number) {
|
||
form.warehouseId = id;
|
||
}
|
||
|
||
function pickCargo(code: string) {
|
||
form.cargoType = code;
|
||
}
|
||
|
||
function onPickCountry(code: string) {
|
||
pickCountry(code);
|
||
pickerVisible.value = false;
|
||
}
|
||
|
||
function onPickWarehouse(id?: number) {
|
||
pickWarehouse(id);
|
||
pickerVisible.value = false;
|
||
}
|
||
|
||
function onPickCargo(code: string) {
|
||
pickCargo(code);
|
||
pickerVisible.value = false;
|
||
}
|
||
|
||
function focusPostal() {
|
||
hintBannerDismissed.value = false;
|
||
postalFieldRef.value?.scrollIntoView?.({ behavior: 'smooth', block: 'center' });
|
||
const input = postalFieldRef.value?.querySelector?.('input') as HTMLInputElement | null;
|
||
input?.focus?.();
|
||
}
|
||
|
||
async function loadOptions() {
|
||
optionsLoading.value = true;
|
||
try {
|
||
const [countryRes, warehouseRes, dictRows] = await Promise.all([
|
||
getCountryList(),
|
||
getWarehouseList({ page: 1, pageSize: 500 }),
|
||
getDictListAll({ dictType: 'cargo_attribute' }),
|
||
]);
|
||
|
||
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?.countryNameCn ??
|
||
row?.countryName ??
|
||
row?.CountryName ??
|
||
row?.name ??
|
||
'',
|
||
).trim();
|
||
return {
|
||
value: code,
|
||
name,
|
||
label: name ? `${name} ${code}` : code,
|
||
};
|
||
})
|
||
.filter((c) => c.value);
|
||
|
||
warehouseOptions.value = (warehouseRes.items ?? [])
|
||
.map((row: any) => {
|
||
const id = Number(row?.id ?? row?.Id);
|
||
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||
return { value: id, label: name || String(id) };
|
||
})
|
||
.filter((w) => Number.isFinite(w.value) && w.value > 0);
|
||
|
||
if (!form.warehouseId && warehouseOptions.value.length) {
|
||
form.warehouseId = warehouseOptions.value[0].value;
|
||
}
|
||
|
||
cargoOptions.value = (dictRows ?? [])
|
||
.map((row: any) => {
|
||
const code = String(row?.dictCode ?? row?.DictCode ?? row?.code ?? '').trim();
|
||
const name = String(row?.dictName ?? row?.DictName ?? row?.name ?? '').trim();
|
||
return { value: code, label: name || code };
|
||
})
|
||
.filter((c) => c.value);
|
||
|
||
// 默认普货:名称含「普」或首项
|
||
if (!form.cargoType && cargoOptions.value.length) {
|
||
const general = cargoOptions.value.find((c) => /普货|general/i.test(c.label));
|
||
form.cargoType = (general || cargoOptions.value[0]).value;
|
||
}
|
||
} catch {
|
||
countryOptions.value = [];
|
||
warehouseOptions.value = [];
|
||
cargoOptions.value = [];
|
||
} finally {
|
||
optionsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleQuery() {
|
||
if (!form.destinationCountry || !String(form.cargoWeight).trim()) {
|
||
Message.warning(t('h5.price.required'));
|
||
return;
|
||
}
|
||
queryLoading.value = true;
|
||
queried.value = true;
|
||
queryFailureText.value = '';
|
||
hintBannerDismissed.value = false;
|
||
clearRecommendInsight();
|
||
try {
|
||
const length = parseOptionalDimension(form.length);
|
||
const width = parseOptionalDimension(form.width);
|
||
const height = parseOptionalDimension(form.height);
|
||
const { items, failures, queryHint: hint } = await postOrderPriceCheck({
|
||
destinationCountry: form.destinationCountry,
|
||
shippingMethod: '',
|
||
cargoWeight: String(form.cargoWeight).trim(),
|
||
...(length != null && width != null && height != null
|
||
? { length: String(length), width: String(width), height: String(height) }
|
||
: {}),
|
||
destinationProvince: '',
|
||
postalCode: form.postalCode.trim() || '',
|
||
cargoTypes: form.cargoType ? [form.cargoType] : undefined,
|
||
warehouseIds: form.warehouseId ? [form.warehouseId] : undefined,
|
||
});
|
||
queryHint.value = hint;
|
||
results.value = processQueryResults(items || []);
|
||
if (!results.value.length) {
|
||
queryFailureText.value =
|
||
(failures || [])
|
||
.map((f: any) => f.reason || f.Reason || '')
|
||
.filter(Boolean)
|
||
.join(';') || t('h5.price.noResult');
|
||
}
|
||
sortMode.value = 'recommend';
|
||
} catch (e: any) {
|
||
results.value = [];
|
||
queryHint.value = null;
|
||
queryFailureText.value = e?.message || t('h5.price.noResult');
|
||
} finally {
|
||
queryLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleAiRecommend() {
|
||
const name = aiProductName.value.trim();
|
||
if (!name) {
|
||
Message.warning(t('h5.price.ai.needName'));
|
||
return;
|
||
}
|
||
if (!results.value.length) {
|
||
Message.warning(t('h5.price.ai.needQuotes'));
|
||
return;
|
||
}
|
||
aiRecommendLoading.value = true;
|
||
try {
|
||
const candidates: QuoteCandidate[] = results.value.map((item) => ({
|
||
rowKey: String(item.rowKey),
|
||
productId: item.productId,
|
||
productCode: String(item.productCode || ''),
|
||
shippingMethod: String(item.shippingMethod || ''),
|
||
cargoTypeName: String(item.cargoTypeName || ''),
|
||
warehouse: String(item.warehouse || ''),
|
||
deliveryTime: String(item.deliveryTime || ''),
|
||
totalCostNum: Number(item.totalCostNum),
|
||
maxDeliveryDays: Number(item.maxDeliveryDays),
|
||
isPriceOptimal: !!item.isPriceOptimal,
|
||
isTimeOptimal: !!item.isTimeOptimal,
|
||
}));
|
||
const result = await recommendWithAi(name, candidates);
|
||
if (!result) {
|
||
clearRecommendInsight();
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name }));
|
||
return;
|
||
}
|
||
const hit = results.value.find((item) => item.rowKey === result.rowKey);
|
||
if (!hit) {
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name }));
|
||
return;
|
||
}
|
||
applyRecommendInsight(result);
|
||
sortMode.value = 'recommend';
|
||
scrollToQuote(result.rowKey);
|
||
Message.success(
|
||
result.source === 'ai'
|
||
? t('order.priceCheck.ai.successAi')
|
||
: result.source === 'mcp'
|
||
? t('order.priceCheck.ai.successMcp')
|
||
: t('order.priceCheck.ai.successLocal'),
|
||
);
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('order.priceCheck.ai.failed'));
|
||
} finally {
|
||
aiRecommendLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function selectAiAlternative(rowKey: string) {
|
||
const hit = results.value.find((item) => item.rowKey === rowKey);
|
||
if (!hit) {
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name: recommendCargoName.value || '' }));
|
||
return;
|
||
}
|
||
const alt = recommendAlternatives.value.find((a) => a.rowKey === rowKey);
|
||
recommendedRowKey.value = rowKey;
|
||
recommendVerdict.value = alt?.verdict || recommendVerdict.value || 'unknown';
|
||
if (alt?.blurb) recommendReason.value = alt.blurb;
|
||
scrollToQuote(rowKey);
|
||
}
|
||
|
||
async function openDetail(item: any) {
|
||
detailItem.value = item;
|
||
detailVisible.value = true;
|
||
guideHtml.value = '';
|
||
const productId = item?.productId ?? item?.id;
|
||
if (productId == null || productId === '') {
|
||
Message.warning(t('h5.price.guideMissing'));
|
||
return;
|
||
}
|
||
detailLoading.value = true;
|
||
try {
|
||
const detail = await getSalesProductDetail(productId);
|
||
const md = String(
|
||
detail?.customerGuideMarkdown ?? detail?.CustomerGuideMarkdown ?? '',
|
||
).trim();
|
||
guideHtml.value = await markdownToHtml(md);
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('h5.price.guideFailed'));
|
||
} finally {
|
||
detailLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function askAdvisor(item: any, intent: PriceAskIntent = 'general') {
|
||
const ctx = {
|
||
destinationCountry: String(form.destinationCountry || '').trim(),
|
||
cargoWeight: form.cargoWeight,
|
||
postalCode: form.postalCode,
|
||
productCode: String(item?.productCode ?? '').trim(),
|
||
shippingMethod: String(item?.shippingMethod ?? '').trim(),
|
||
cargoType: String(item?.cargoType ?? form.cargoType ?? '').trim(),
|
||
cargoTypeName: String(item?.cargoTypeName ?? item?.cargoType ?? '').trim(),
|
||
warehouse: String(item?.warehouse ?? '').trim(),
|
||
totalCost: item?.totalCost,
|
||
deliveryTime: item?.deliveryTime,
|
||
quoteLevel: String(item?.quoteLevel ?? item?.QuoteLevel ?? queryHint.value?.quoteLevel ?? '').trim(),
|
||
length: form.length,
|
||
width: form.width,
|
||
height: form.height,
|
||
};
|
||
detailVisible.value = false;
|
||
h5Navigate(router, getPriceAskLocation(intent, ctx, { h5: true }));
|
||
}
|
||
|
||
function askAdvisorEmpty() {
|
||
h5Navigate(router, '/h5/assistant');
|
||
}
|
||
|
||
function buildOrderDraft(item: any) {
|
||
return {
|
||
productCode: String(item?.productCode ?? '').trim(),
|
||
productId: item?.productId ?? item?.id ?? null,
|
||
shippingMethod: String(item?.shippingMethod ?? '').trim(),
|
||
cargoAttribute: String(item?.cargoType ?? form.cargoType ?? '').trim(),
|
||
warehouseId:
|
||
item?.warehouseId != null && item?.warehouseId !== ''
|
||
? String(item.warehouseId)
|
||
: form.warehouseId
|
||
? String(form.warehouseId)
|
||
: '',
|
||
countryCode: String(form.destinationCountry || '').trim(),
|
||
destinationCountry: String(form.destinationCountry || '').trim(),
|
||
actualWeight: String(form.cargoWeight || '').trim(),
|
||
cargoWeight: String(form.cargoWeight || '').trim(),
|
||
zipCode: form.postalCode.trim(),
|
||
length: String(form.length || '').trim(),
|
||
width: String(form.width || '').trim(),
|
||
height: String(form.height || '').trim(),
|
||
totalCost: item?.totalCost ?? null,
|
||
quoteLevel: String(item?.quoteLevel ?? queryHint.value?.quoteLevel ?? '').trim() || undefined,
|
||
};
|
||
}
|
||
|
||
function openPlaceOrderWithDraft(draft: Record<string, any>) {
|
||
try {
|
||
sessionStorage.setItem(PRICE_CHECK_ORDER_DRAFT_KEY, JSON.stringify(draft));
|
||
localStorage.setItem(PRICE_CHECK_ORDER_DRAFT_KEY, JSON.stringify(draft));
|
||
} catch {
|
||
// ignore
|
||
}
|
||
detailVisible.value = false;
|
||
placeOrderDraft.value = draft;
|
||
placeOrderVisible.value = true;
|
||
}
|
||
|
||
async function goOrder(item: any) {
|
||
const draft = buildOrderDraft(item);
|
||
const { needContact, profile } = await checkCustomerContactRequired();
|
||
if (needContact) {
|
||
placeOrderDraft.value = draft;
|
||
pendingOpenOrder = true;
|
||
contactProfile.value = profile;
|
||
contactVisible.value = true;
|
||
return;
|
||
}
|
||
openPlaceOrderWithDraft(draft);
|
||
}
|
||
|
||
function onContactSuccess() {
|
||
if (!pendingOpenOrder) return;
|
||
pendingOpenOrder = false;
|
||
const draft = placeOrderDraft.value || {};
|
||
openPlaceOrderWithDraft(draft);
|
||
}
|
||
|
||
function onContactCancel() {
|
||
pendingOpenOrder = false;
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadOptions();
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.h5-price-page {
|
||
padding: 12px 16px 24px;
|
||
min-height: 100%;
|
||
box-sizing: border-box;
|
||
background: linear-gradient(
|
||
180deg,
|
||
color-mix(in srgb, var(--app-primary-color) 10%, #f5f7f8) 0%,
|
||
#f5f7f8 140px
|
||
);
|
||
|
||
}
|
||
|
||
|
||
.quote-form {
|
||
background: #fff;
|
||
border-radius: 18px;
|
||
padding: 18px 16px 14px;
|
||
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.06);
|
||
text-align: left;
|
||
}
|
||
|
||
.route-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 48px minmax(0, 1fr);
|
||
align-items: center;
|
||
column-gap: 4px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.route-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
padding: 2px 4px;
|
||
border: none;
|
||
background: transparent;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
|
||
&:active .route-cell__value {
|
||
opacity: 0.72;
|
||
}
|
||
}
|
||
|
||
.route-cell__label {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
line-height: 1.2;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.route-cell__value {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
max-width: 100%;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #1d2129;
|
||
line-height: 1.25;
|
||
min-height: 26px;
|
||
|
||
> span {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&.is-placeholder > span {
|
||
color: #c9cdd4;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
:deep(.arco-icon) {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: #c9cdd4;
|
||
}
|
||
}
|
||
|
||
.route-cell__dest {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
|
||
b {
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
color: #1d2129;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
em {
|
||
flex-shrink: 0;
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
border-radius: 4px;
|
||
padding: 1px 5px;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
.route-mid {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 40px;
|
||
}
|
||
|
||
.route-mid__icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: none;
|
||
box-shadow: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.route-mid__plane {
|
||
width: 39px;
|
||
height: 39px;
|
||
display: block;
|
||
object-fit: contain;
|
||
pointer-events: none;
|
||
-webkit-user-drag: none;
|
||
}
|
||
|
||
.dim-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
margin: 0 0 8px;
|
||
padding: 8px 0 0;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #4e5969;
|
||
cursor: pointer;
|
||
|
||
em {
|
||
font-style: normal;
|
||
font-size: 16px;
|
||
color: #86909c;
|
||
}
|
||
}
|
||
|
||
.dim-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.dim-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
|
||
span {
|
||
font-size: 11px;
|
||
color: #86909c;
|
||
}
|
||
|
||
input {
|
||
width: 100%;
|
||
height: 36px;
|
||
border: 1px solid #e5e6eb;
|
||
border-radius: 8px;
|
||
padding: 0 8px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
box-sizing: border-box;
|
||
outline: none;
|
||
|
||
&:focus {
|
||
border-color: color-mix(in srgb, var(--app-primary-color) 50%, #e5e6eb);
|
||
}
|
||
}
|
||
}
|
||
|
||
.dim-hint {
|
||
margin: 0 0 12px;
|
||
font-size: 11px;
|
||
color: #86909c;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.h5-ai-bar {
|
||
margin-bottom: 10px;
|
||
padding: 12px;
|
||
border-radius: 14px;
|
||
background: #fff;
|
||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.h5-ai-bar__head {
|
||
margin-bottom: 8px;
|
||
|
||
strong {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #1d2129;
|
||
}
|
||
}
|
||
|
||
.h5-ai-bar__row {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.h5-ai-bar__input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 40px;
|
||
border: 1px solid #e5e6eb;
|
||
border-radius: 10px;
|
||
padding: 0 12px;
|
||
font-size: 14px;
|
||
outline: none;
|
||
background: #fafbfc;
|
||
|
||
&:focus {
|
||
border-color: color-mix(in srgb, var(--app-primary-color) 45%, #e5e6eb);
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
.h5-ai-bar__btn {
|
||
flex-shrink: 0;
|
||
height: 40px;
|
||
padding: 0 14px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: var(--app-primary-color);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
|
||
&:disabled {
|
||
opacity: 0.55;
|
||
}
|
||
}
|
||
|
||
.h5-ai-insight {
|
||
margin-bottom: 10px;
|
||
padding: 12px;
|
||
border-radius: 14px;
|
||
border: 1px solid #e8edf2;
|
||
background: #fafbfc;
|
||
text-align: left;
|
||
|
||
&.is-ok {
|
||
border-color: color-mix(in srgb, #00b42a 28%, #e8edf2);
|
||
background: color-mix(in srgb, #00b42a 6%, #fff);
|
||
}
|
||
&.is-caution {
|
||
border-color: color-mix(in srgb, #ff7d00 28%, #e8edf2);
|
||
background: color-mix(in srgb, #ff7d00 6%, #fff);
|
||
}
|
||
&.is-forbid {
|
||
border-color: color-mix(in srgb, #f53f3f 28%, #e8edf2);
|
||
background: color-mix(in srgb, #f53f3f 6%, #fff);
|
||
}
|
||
}
|
||
|
||
.h5-ai-insight__top {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.h5-ai-insight__verdict {
|
||
display: inline-flex;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #4e5969;
|
||
background: #fff;
|
||
|
||
.is-ok & {
|
||
color: #00b42a;
|
||
}
|
||
.is-caution & {
|
||
color: #ff7d00;
|
||
}
|
||
.is-forbid & {
|
||
color: #f53f3f;
|
||
}
|
||
}
|
||
|
||
.h5-ai-insight__cargo {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
}
|
||
|
||
.h5-ai-insight__reason {
|
||
margin: 0 0 6px;
|
||
font-size: 13px;
|
||
color: #1d2129;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.h5-ai-insight__evidence {
|
||
margin: 0 0 8px;
|
||
padding-left: 16px;
|
||
font-size: 12px;
|
||
color: #4e5969;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.h5-ai-insight__alts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.h5-ai-alt {
|
||
border: 1px solid #e5e6eb;
|
||
background: #fff;
|
||
border-radius: 999px;
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
color: #4e5969;
|
||
cursor: pointer;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.h5-accuracy {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
background: #fff7e8;
|
||
color: #ad6800;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
text-align: left;
|
||
}
|
||
|
||
.h5-accuracy__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.h5-accuracy__fill {
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 6px 10px;
|
||
background: var(--app-primary-color);
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.h5-accuracy__dismiss {
|
||
border: none;
|
||
background: transparent;
|
||
color: #86909c;
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.picker-shortcuts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.quick-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
border: 1px solid #e5e6eb;
|
||
background: #fff;
|
||
border-radius: 999px;
|
||
padding: 6px 10px;
|
||
font-size: 13px;
|
||
color: #1d2129;
|
||
cursor: pointer;
|
||
|
||
em {
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #86909c;
|
||
}
|
||
|
||
&.is-active {
|
||
border-color: color-mix(in srgb, var(--app-primary-color) 40%, #e5e6eb);
|
||
color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||
font-weight: 600;
|
||
|
||
em {
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
}
|
||
|
||
.line-field {
|
||
padding: 12px 0 10px;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.line-field__label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.line-field__input {
|
||
width: 100%;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
padding: 0;
|
||
line-height: 1.4;
|
||
|
||
&::placeholder {
|
||
color: #c9cdd4;
|
||
font-weight: 500;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
.pkg-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
margin: 0;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.pkg-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
padding: 12px 0 14px;
|
||
border: none;
|
||
background: transparent;
|
||
text-align: left;
|
||
font: inherit;
|
||
color: inherit;
|
||
|
||
&:first-child {
|
||
padding-right: 14px;
|
||
border-right: 1px solid #eef0f3;
|
||
}
|
||
|
||
&:last-child {
|
||
padding-left: 14px;
|
||
}
|
||
|
||
&--tap {
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
|
||
&:active .pkg-cell__value {
|
||
opacity: 0.72;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pkg-cell__label {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.pkg-cell__value {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
min-height: 32px;
|
||
|
||
input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
width: 100%;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #1d2129;
|
||
padding: 0;
|
||
line-height: 1.2;
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
&::placeholder {
|
||
color: #c9cdd4;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
em {
|
||
flex-shrink: 0;
|
||
font-style: normal;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #86909c;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
strong {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #1d2129;
|
||
line-height: 1.2;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&.is-placeholder {
|
||
color: #c9cdd4;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
:deep(.arco-icon) {
|
||
flex-shrink: 0;
|
||
align-self: center;
|
||
font-size: 12px;
|
||
color: #c9cdd4;
|
||
}
|
||
}
|
||
|
||
.quote-cta {
|
||
width: 100%;
|
||
height: 48px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: var(--app-primary-color);
|
||
color: #fff;
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
box-shadow: 0 8px 20px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
||
|
||
&:active {
|
||
transform: scale(0.985);
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.quote-cta__loading {
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin 0.7s linear infinite;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.quote-foot {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.quote-summary {
|
||
display: inline-flex;
|
||
max-width: 100%;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
background: #f2f3f5;
|
||
color: #86909c;
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
word-break: break-all;
|
||
text-align: center;
|
||
}
|
||
|
||
.quote-ref {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
text-align: center;
|
||
}
|
||
|
||
.picker {
|
||
padding: 0 4px 16px;
|
||
}
|
||
|
||
.picker-search {
|
||
width: 100%;
|
||
height: 40px;
|
||
margin-bottom: 12px;
|
||
padding: 0 12px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: #f2f3f5;
|
||
font-size: 14px;
|
||
outline: none;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.picker-item {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 14px 4px;
|
||
border: none;
|
||
border-bottom: 1px solid #f2f3f5;
|
||
background: transparent;
|
||
font-size: 16px;
|
||
color: #1d2129;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
|
||
&__name {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__code {
|
||
flex-shrink: 0;
|
||
min-width: 28px;
|
||
padding: 2px 6px;
|
||
border-radius: 6px;
|
||
background: #f2f3f5;
|
||
color: #4e5969;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
text-align: center;
|
||
}
|
||
|
||
&.is-active {
|
||
color: var(--app-primary-color);
|
||
font-weight: 600;
|
||
|
||
.picker-item__code {
|
||
color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
}
|
||
}
|
||
}
|
||
|
||
.h5-price-results {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.h5-results-bar {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.h5-disclaimer {
|
||
margin: 0 0 8px;
|
||
padding: 6px 10px;
|
||
border-radius: 8px;
|
||
background: #fff7e8;
|
||
color: #ff7d00;
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
text-align: left;
|
||
}
|
||
|
||
.h5-sort {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding: 2px;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
}
|
||
|
||
.h5-sort__btn {
|
||
flex: 1;
|
||
height: 26px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #86909c;
|
||
cursor: pointer;
|
||
|
||
&.is-active {
|
||
color: #4e5969;
|
||
background: rgba(15, 23, 42, 0.04);
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.h5-price-spin {
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.h5-quote {
|
||
position: relative;
|
||
z-index: 0;
|
||
overflow: hidden;
|
||
margin-bottom: 10px;
|
||
padding: 14px 14px 10px;
|
||
border-radius: 16px;
|
||
border: none;
|
||
background: #fff;
|
||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||
|
||
/* 推荐卡:允许盖章探出,整块内容统一左缩进对齐 */
|
||
&--top {
|
||
overflow: visible;
|
||
z-index: 1;
|
||
margin-top: 4px;
|
||
|
||
.h5-quote__main {
|
||
padding-left: 22px;
|
||
}
|
||
}
|
||
|
||
&--ai {
|
||
box-shadow:
|
||
0 0 0 1.5px color-mix(in srgb, var(--app-primary-color) 45%, transparent),
|
||
0 4px 14px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.992);
|
||
}
|
||
}
|
||
|
||
/* 推荐盖章:倾斜压在左上角 */
|
||
.h5-quote__stamp {
|
||
position: absolute;
|
||
z-index: 3;
|
||
top: -6px;
|
||
left: -8px;
|
||
padding: 5px 10px;
|
||
border: 2px solid var(--app-primary-color);
|
||
border-radius: 4px;
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
color: var(--app-primary-color);
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
letter-spacing: 0.12em;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
transform: rotate(-18deg);
|
||
transform-origin: center center;
|
||
box-shadow:
|
||
0 0 0 1px color-mix(in srgb, var(--app-primary-color) 35%, transparent) inset,
|
||
0 3px 8px color-mix(in srgb, var(--app-primary-color) 18%, transparent);
|
||
pointer-events: none;
|
||
opacity: 0.92;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 2px;
|
||
border: 1px dashed color-mix(in srgb, var(--app-primary-color) 55%, transparent);
|
||
border-radius: 2px;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.h5-quote__main {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
|
||
.h5-quote__head {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.h5-quote__title {
|
||
min-width: 0;
|
||
justify-self: start;
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #1d2129;
|
||
line-height: 1.35;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
.h5-quote__sub {
|
||
margin: 6px 0 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 5px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.h5-quote__chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
max-width: 100%;
|
||
height: 20px;
|
||
padding: 0 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
|
||
&--cargo {
|
||
color: var(--app-primary-color);
|
||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||
}
|
||
|
||
&--wh {
|
||
color: #ff7d00;
|
||
background: rgba(255, 125, 0, 0.1);
|
||
}
|
||
}
|
||
|
||
.h5-quote__weight {
|
||
font-size: 11px;
|
||
color: #86909c;
|
||
line-height: 1.3;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.h5-quote__tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 18px;
|
||
padding: 0 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
|
||
&.is-price {
|
||
color: #f53f3f;
|
||
background: rgba(245, 63, 63, 0.1);
|
||
}
|
||
|
||
&.is-time {
|
||
color: #165dff;
|
||
background: rgba(22, 93, 255, 0.1);
|
||
}
|
||
}
|
||
|
||
.h5-quote__eta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
min-width: 56px;
|
||
padding: 0 6px;
|
||
text-align: center;
|
||
justify-self: center;
|
||
}
|
||
|
||
.h5-quote__eta-value {
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: var(--app-primary-color);
|
||
line-height: 1.1;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.h5-quote__eta-label {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: #86909c;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.h5-quote__price-block {
|
||
text-align: right;
|
||
justify-self: end;
|
||
min-width: 0;
|
||
}
|
||
|
||
.ref-label {
|
||
display: block;
|
||
margin-top: 2px;
|
||
font-size: 10px;
|
||
color: #c9cdd4;
|
||
}
|
||
|
||
.h5-quote__price {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #f53f3f;
|
||
white-space: nowrap;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1.1;
|
||
|
||
small {
|
||
font-size: 12px;
|
||
margin-right: 1px;
|
||
}
|
||
}
|
||
|
||
.h5-quote__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid rgba(242, 243, 245, 0.85);
|
||
|
||
.h5-quote__chip--wh {
|
||
flex-shrink: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
.h5-quote__action-btns {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.link-btn {
|
||
border: none;
|
||
background: transparent;
|
||
padding: 0 4px;
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.order-btn {
|
||
border: none;
|
||
height: 28px;
|
||
padding: 0 12px;
|
||
border-radius: 14px;
|
||
background: var(--app-primary-color);
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 10px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
||
}
|
||
|
||
.h5-empty {
|
||
padding: 20px 12px 4px;
|
||
text-align: center;
|
||
|
||
p {
|
||
margin: 0 0 10px;
|
||
color: #86909c;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.h5-empty__icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
margin: 0 auto 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #c9cdd4;
|
||
background: transparent;
|
||
|
||
svg {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.h5-price-empty {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.h5-detail-spin {
|
||
width: 100%;
|
||
min-height: 200px;
|
||
display: block;
|
||
}
|
||
|
||
.h5-detail {
|
||
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
|
||
text-align: left;
|
||
}
|
||
|
||
.h5-detail__summary {
|
||
padding: 4px 0 16px;
|
||
border-bottom: 1px solid #f2f3f5;
|
||
}
|
||
|
||
.h5-detail__price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
|
||
span {
|
||
font-size: 13px;
|
||
color: #86909c;
|
||
}
|
||
|
||
strong {
|
||
font-size: 26px;
|
||
color: #f53f3f;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
}
|
||
|
||
.h5-detail__ref {
|
||
margin: 6px 0 12px;
|
||
font-size: 12px;
|
||
color: #ff7d00;
|
||
}
|
||
|
||
.h5-detail__grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px 12px;
|
||
|
||
label {
|
||
display: block;
|
||
font-size: 11px;
|
||
color: #86909c;
|
||
}
|
||
|
||
p {
|
||
margin: 2px 0 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.h5-detail__guide {
|
||
padding: 16px 0;
|
||
|
||
h4 {
|
||
margin: 0 0 10px;
|
||
font-size: 15px;
|
||
color: #1d2129;
|
||
}
|
||
}
|
||
|
||
.h5-md {
|
||
font-size: 14px;
|
||
line-height: 1.65;
|
||
color: #4e5969;
|
||
word-break: break-word;
|
||
|
||
:deep(h1),
|
||
:deep(h2),
|
||
:deep(h3) {
|
||
color: #1d2129;
|
||
margin: 12px 0 8px;
|
||
}
|
||
|
||
:deep(p) {
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
:deep(ul),
|
||
:deep(ol) {
|
||
padding-left: 1.2em;
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
:deep(img) {
|
||
max-width: 100%;
|
||
border-radius: 8px;
|
||
}
|
||
}
|
||
|
||
.h5-detail__footer {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
display: grid;
|
||
grid-template-columns: 0.9fr 1.3fr;
|
||
gap: 8px;
|
||
padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
|
||
background: #fff;
|
||
border-top: 1px solid #e5e6eb;
|
||
}
|
||
</style>
|