feat: 查价州省城市收进更多条件,并优化 PC/H5 报价卡片与详情

- PC:目的州省/城市移入更多条件;上方已指定货属性/仓库时线路卡片锁定,未指定可下拉重算
- PC:推荐标签贴线路名右侧,产品分组/贸易条款夹在名称与切换项之间
- H5:尺寸改为更多折叠区;报价卡去掉详情文案与 DDU 税提示,顾问改图标
- H5:线路详情底栏对齐订单详情;线路说明改 MarkdownViewer 排版;店铺订单空态对齐我的订单

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-06 10:43:28 +08:00
parent 2a4251f9db
commit d771462a30
8 changed files with 644 additions and 185 deletions

View File

@ -2511,6 +2511,8 @@ export default {
'order.priceCheck.card.showFee': 'Details',
'order.priceCheck.card.hideFee': 'Hide details',
'order.priceCheck.card.switchRecalcHint': 'Changing this will recalculate this product quote',
'order.priceCheck.card.switchLockedHint':
'Set in More filters — clear or change it above to switch here',
'order.priceCheck.card.missingProductDescription': 'Product description missing',
'order.priceCheck.tradeTerms.ddp': 'DDP (duties included)',
'order.priceCheck.tradeTerms.ddu': 'DDU (duties excluded)',

View File

@ -2390,6 +2390,7 @@ export default {
'order.priceCheck.card.showFee': '明细',
'order.priceCheck.card.hideFee': '收起明细',
'order.priceCheck.card.switchRecalcHint': '切换后将重算该产品报价',
'order.priceCheck.card.switchLockedHint': '已在更多条件中指定,请到上方修改或清除后再切换',
'order.priceCheck.card.missingProductDescription': '缺少产品说明',
'order.priceCheck.tradeTerms.ddp': 'DDP(包税)',
'order.priceCheck.tradeTerms.ddu': 'DDU(不包税)',

View File

@ -50,8 +50,17 @@ export default {
'h5.price.tag.priceOptimal': 'Best price',
'h5.price.tag.timeOptimal': 'Fastest',
'h5.price.tag.best': 'Best',
'h5.price.dimensionsToggle': 'Size (optional)',
'h5.price.dimensionsHint': 'L×W×H for volumetric weight',
'h5.price.dimensionsToggle': 'More',
'h5.price.moreOptional': 'Optional',
'h5.price.moreActive': '{n} filled',
'h5.price.moreDimsLabel': 'Size (cm)',
'h5.price.moreGeoLabel': 'State / City',
'h5.price.dimensionsHint': 'Size for volumetric weight; state/city helps zoning without postal code',
'h5.price.lengthPh': 'L',
'h5.price.widthPh': 'W',
'h5.price.heightPh': 'H',
'h5.price.provincePh': 'State',
'h5.price.cityPh': 'City',
'h5.price.accuracy.postalCta': 'Estimate only — add a postal code for better accuracy',
'h5.price.accuracy.fillPostal': 'Add postal code',
'h5.price.accuracy.dismiss': 'Dismiss',

View File

@ -50,8 +50,17 @@ export default {
'h5.price.tag.priceOptimal': '价格最优',
'h5.price.tag.timeOptimal': '时效最优',
'h5.price.tag.best': '最优',
'h5.price.dimensionsToggle': '尺寸(选填)',
'h5.price.dimensionsHint': '填齐长宽高可算体积重',
'h5.price.dimensionsToggle': '更多',
'h5.price.moreOptional': '选填',
'h5.price.moreActive': '已填 {n} 项',
'h5.price.moreDimsLabel': '尺寸 (cm)',
'h5.price.moreGeoLabel': '州省 / 城市',
'h5.price.dimensionsHint': '尺寸可算体积重;无邮编时可用州省/城市辅助分区',
'h5.price.lengthPh': '长',
'h5.price.widthPh': '宽',
'h5.price.heightPh': '高',
'h5.price.provincePh': '州/省',
'h5.price.cityPh': '城市',
'h5.price.accuracy.postalCta': '当前为估算报价,补上邮编可提高准确性',
'h5.price.accuracy.fillPostal': '去填邮编',
'h5.price.accuracy.dismiss': '关闭',

View File

@ -69,24 +69,6 @@
autocomplete="postal-code"
/>
</div>
<div class="line-field">
<span class="line-field__label">{{ t('order.priceCheck.form.destinationProvince') }}</span>
<input
v-model="form.destinationProvince"
class="line-field__input"
type="text"
:placeholder="t('order.priceCheck.form.destinationProvince.placeholder')"
/>
</div>
<div class="line-field">
<span class="line-field__label">{{ t('order.priceCheck.form.destinationCity') }}</span>
<input
v-model="form.destinationCity"
class="line-field__input"
type="text"
:placeholder="t('order.priceCheck.form.destinationCity.placeholder')"
/>
</div>
<div class="pkg-row">
<label class="pkg-cell">
@ -113,25 +95,84 @@
</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 class="more-block" :class="{ 'is-open': showMore }">
<button
type="button"
class="more-toggle"
:aria-expanded="showMore"
@click="showMore = !showMore"
>
<span class="more-toggle__left">
<span class="more-toggle__title">{{ t('h5.price.dimensionsToggle') }}</span>
<span v-if="moreActiveCount" class="more-toggle__badge">
{{ t('h5.price.moreActive', { n: moreActiveCount }) }}
</span>
<span v-else class="more-toggle__hint">{{ t('h5.price.moreOptional') }}</span>
</span>
<icon-down class="more-toggle__chevron" :class="{ 'is-open': showMore }" />
</button>
<div v-show="showMore" class="more-panel">
<div class="more-group">
<p class="more-group__label">{{ t('h5.price.moreDimsLabel') }}</p>
<div class="dim-row">
<label class="dim-cell">
<input
v-model="form.length"
type="text"
inputmode="decimal"
:placeholder="t('h5.price.lengthPh')"
aria-label="length"
/>
</label>
<span class="dim-sep" aria-hidden="true">×</span>
<label class="dim-cell">
<input
v-model="form.width"
type="text"
inputmode="decimal"
:placeholder="t('h5.price.widthPh')"
aria-label="width"
/>
</label>
<span class="dim-sep" aria-hidden="true">×</span>
<label class="dim-cell">
<input
v-model="form.height"
type="text"
inputmode="decimal"
:placeholder="t('h5.price.heightPh')"
aria-label="height"
/>
</label>
</div>
</div>
<div class="more-group">
<p class="more-group__label">{{ t('h5.price.moreGeoLabel') }}</p>
<div class="dim-row dim-row--geo">
<label class="dim-cell">
<input
v-model="form.destinationProvince"
type="text"
:placeholder="t('h5.price.provincePh')"
:aria-label="t('order.priceCheck.form.destinationProvince')"
/>
</label>
<label class="dim-cell">
<input
v-model="form.destinationCity"
type="text"
:placeholder="t('h5.price.cityPh')"
:aria-label="t('order.priceCheck.form.destinationCity')"
/>
</label>
</div>
</div>
<p class="dim-hint">{{ t('h5.price.dimensionsHint') }}</p>
</div>
</div>
<p v-if="showDimensions" class="dim-hint">{{ t('h5.price.dimensionsHint') }}</p>
<button
type="button"
@ -232,9 +273,6 @@
<span class="ref-label">
{{ tradeTermsLabel(item.tradeTerms) || t('h5.price.referencePrice') }}
</span>
<span v-if="item.tradeTerms === 'DDU'" class="ref-label ref-label--warn">
{{ t('h5.price.hint.dduExtra') }}
</span>
</div>
</div>
@ -254,11 +292,13 @@
</span>
</div>
<div class="h5-quote__action-btns">
<button type="button" class="link-btn" @click="openDetail(item)">
{{ t('h5.price.detailShort') }}
</button>
<button type="button" class="link-btn" @click="askAdvisor(item)">
{{ t('h5.price.askAdvisor') }}
<button
type="button"
class="advisor-icon-btn"
:aria-label="t('h5.price.askAdvisor')"
@click="askAdvisor(item)"
>
<IconTabService class="advisor-icon-btn__icon" />
</button>
<button type="button" class="order-btn" @click="goOrder(item)">
{{ t('h5.price.goOrder') }}
@ -490,19 +530,28 @@
</section>
<section class="h5-detail__guide">
<h4>{{ t('h5.price.routeGuide') }}</h4>
<div v-if="guideHtml" class="h5-md" v-html="guideHtml" />
<header class="h5-detail__guide-head">
<h4>{{ t('h5.price.routeGuide') }}</h4>
</header>
<div v-if="guideContent" class="h5-detail__guide-body">
<MarkdownViewer :content="guideContent" />
</div>
<a-empty v-else-if="!detailLoading" :description="t('h5.price.guideEmpty')" />
</section>
<div class="h5-detail__footer">
<a-button long @click="askAdvisor(detailItem)">
{{ t('h5.price.askAdvisor') }}
</a-button>
<a-button type="primary" long @click="goOrder(detailItem)">
<footer class="h5-detail__footer">
<button
type="button"
class="h5-detail__icon-btn"
:aria-label="t('h5.price.askAdvisor')"
@click="askAdvisor(detailItem)"
>
<IconTabService class="h5-detail__icon" />
</button>
<button type="button" class="h5-detail__order-btn" @click="goOrder(detailItem)">
{{ t('h5.price.goOrder') }}
</a-button>
</div>
</button>
</footer>
</div>
</a-spin>
</a-drawer>
@ -521,12 +570,14 @@
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, reactive, ref, type ComponentPublicInstance } from 'vue';
import { computed, nextTick, onMounted, reactive, ref, watch, 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 IconRoutePackage from '@/assets/icons/h5/route-package.svg';
import IconTabService from '@/assets/icons/h5/tab-service.svg';
import MarkdownViewer from '@/components/MarkdownViewer/index.vue';
import type { CustomerProfileCompleteness } from '@/api/customer';
import { postOrderPriceCheck, type PriceCheckQueryHint } from '@/api/order';
import { getCountryList } from '@/api/system/country';
@ -574,8 +625,29 @@ const form = reactive({
height: '',
});
const showDimensions = ref(false);
const showMore = ref(false);
const formCollapsed = ref(false);
const moreActiveCount = computed(() => {
let n = 0;
if (String(form.length || '').trim()) n += 1;
if (String(form.width || '').trim()) n += 1;
if (String(form.height || '').trim()) n += 1;
if (String(form.destinationProvince || '').trim()) n += 1;
if (String(form.destinationCity || '').trim()) n += 1;
return n;
});
watch(
moreActiveCount,
(count, prev) => {
// 首次有值或从无到有时自动展开;已展开后允许用户手动收起
if (count > 0 && (prev == null || prev === 0)) {
showMore.value = true;
}
},
{ immediate: true },
);
const aiExpanded = ref(false);
const quoteElMap = new Map<string, HTMLElement>();
@ -610,7 +682,7 @@ const pickerType = ref<'country' | 'warehouse' | 'cargo'>('country');
const detailVisible = ref(false);
const detailLoading = ref(false);
const detailItem = ref<any>(null);
const guideHtml = ref('');
const guideContent = ref('');
function parseOptionalDimension(value: unknown) {
const n = parseFloat(String(value ?? '').trim());
@ -799,26 +871,6 @@ function parseTotalCost(cost: unknown) {
return Number.isNaN(num) ? Infinity : num;
}
function escapeHtml(text: string) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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+)/);
@ -1120,7 +1172,7 @@ function selectAiAlternative(rowKey: string) {
async function openDetail(item: any) {
detailItem.value = item;
detailVisible.value = true;
guideHtml.value = '';
guideContent.value = '';
const productId = item?.productId ?? item?.id;
if (productId == null || productId === '') {
Message.warning(t('h5.price.guideMissing'));
@ -1129,10 +1181,9 @@ async function openDetail(item: any) {
detailLoading.value = true;
try {
const detail = await getSalesProductDetail(productId);
const md = String(
guideContent.value = String(
detail?.customerGuideMarkdown ?? detail?.CustomerGuideMarkdown ?? '',
).trim();
guideHtml.value = await markdownToHtml(md);
} catch (e: any) {
Message.error(e?.message || t('h5.price.guideFailed'));
} finally {
@ -1487,67 +1538,148 @@ onMounted(() => {
display: block;
}
.dim-toggle {
.more-block {
margin: 4px 0 10px;
border-radius: 12px;
transition: background 0.2s ease;
&.is-open {
background: #f7f8fa;
padding: 0 10px 10px;
}
}
.more-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 0 0 8px;
padding: 8px 0 0;
min-height: 40px;
margin: 0;
padding: 8px 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
.more-block.is-open & {
border-bottom: 1px solid #eef0f3;
margin-bottom: 10px;
}
}
.more-toggle__left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.more-toggle__title {
font-size: 13px;
font-weight: 600;
color: #4e5969;
cursor: pointer;
}
em {
font-style: normal;
font-size: 16px;
.more-toggle__hint {
font-size: 12px;
font-weight: 500;
color: #c9cdd4;
}
.more-toggle__badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 8px;
border-radius: 10px;
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
color: var(--app-primary-color);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.more-toggle__chevron {
flex-shrink: 0;
color: #c9cdd4;
font-size: 12px;
transition: transform 0.2s ease;
&.is-open {
transform: rotate(180deg);
color: #86909c;
}
}
.more-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.more-group__label {
margin: 0 0 6px;
font-size: 11px;
font-weight: 500;
color: #86909c;
line-height: 1.2;
}
.dim-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 4px;
grid-template-columns: 1fr auto 1fr auto 1fr;
align-items: center;
gap: 6px;
&--geo {
grid-template-columns: 1fr 1fr;
gap: 8px;
}
}
.dim-sep {
color: #c9cdd4;
font-size: 14px;
font-weight: 500;
line-height: 1;
user-select: none;
}
.dim-cell {
display: flex;
flex-direction: column;
gap: 4px;
display: block;
min-width: 0;
span {
font-size: 11px;
color: #86909c;
}
input {
width: 100%;
height: 36px;
height: 38px;
border: 1px solid #e5e6eb;
border-radius: 8px;
padding: 0 8px;
font-size: 15px;
font-weight: 600;
color: #1d2129;
border-radius: 10px;
padding: 0 10px;
background: #fff;
font-size: 14px;
font-weight: 500;
color: #4e5969;
box-sizing: border-box;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
&::placeholder {
color: #c9cdd4;
font-weight: 400;
}
&:focus {
border-color: color-mix(in srgb, var(--app-primary-color) 50%, #e5e6eb);
border-color: color-mix(in srgb, var(--app-primary-color) 45%, #e5e6eb);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
}
}
}
.dim-hint {
margin: 0 0 12px;
margin: 0;
font-size: 11px;
color: #86909c;
color: #c9cdd4;
line-height: 1.4;
}
@ -2217,11 +2349,6 @@ onMounted(() => {
margin-top: 2px;
font-size: 10px;
color: #c9cdd4;
&--warn {
color: #f53f3f;
opacity: 0.85;
}
}
.h5-quote__price {
@ -2266,13 +2393,30 @@ onMounted(() => {
margin-left: auto;
}
.link-btn {
.advisor-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 28px;
margin: 0;
padding: 0;
border: none;
border-radius: 14px;
background: transparent;
padding: 0 4px;
font-size: 12px;
color: #86909c;
color: var(--app-primary-color);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
&:active {
opacity: 0.72;
}
}
.advisor-icon-btn__icon {
width: 22px;
height: 22px;
display: block;
}
.order-btn {
@ -2328,7 +2472,7 @@ onMounted(() => {
}
.h5-detail {
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
padding-bottom: calc(86px + env(safe-area-inset-bottom, 0px));
text-align: left;
}
@ -2382,41 +2526,183 @@ onMounted(() => {
}
.h5-detail__guide {
padding: 16px 0;
margin: 16px 0 8px;
padding: 14px 14px 4px;
border-radius: 14px;
background: #f7f8fa;
border: 1px solid #eef0f3;
}
.h5-detail__guide-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #e5e6eb;
h4 {
margin: 0 0 10px;
margin: 0;
font-size: 15px;
font-weight: 650;
color: #1d2129;
letter-spacing: -0.01em;
}
&::before {
content: '';
width: 3px;
height: 14px;
border-radius: 2px;
background: var(--app-primary-color);
flex-shrink: 0;
}
}
.h5-md {
font-size: 14px;
line-height: 1.65;
color: #4e5969;
word-break: break-word;
.h5-detail__guide-body {
margin: 0 -4px;
:deep(h1),
:deep(h2),
:deep(h3) {
color: #1d2129;
margin: 12px 0 8px;
}
:deep(.markdown-viewer) {
max-width: none;
margin: 0;
padding: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: #4e5969;
:deep(p) {
margin: 0 0 8px;
}
h1 {
margin: 0 0 12px;
padding-bottom: 8px;
font-size: 18px;
font-weight: 650;
color: #1d2129;
border-bottom: 1px solid #e5e6eb;
}
:deep(ul),
:deep(ol) {
padding-left: 1.2em;
margin: 0 0 8px;
}
h2 {
margin: 18px 0 8px;
padding: 0 0 0 10px;
font-size: 15px;
font-weight: 650;
color: #1d2129;
border-left: 3px solid var(--app-primary-color);
line-height: 1.35;
}
:deep(img) {
max-width: 100%;
border-radius: 8px;
h2:first-child {
margin-top: 0;
}
h3 {
margin: 14px 0 6px;
font-size: 14px;
font-weight: 650;
color: #1d2129;
}
h4 {
margin: 12px 0 6px;
font-size: 13px;
font-weight: 650;
color: #1d2129;
}
p {
margin: 0 0 10px;
}
ul,
ol {
margin: 0 0 10px;
padding-left: 1.25em;
}
li {
margin-bottom: 4px;
}
li + li {
margin-top: 2px;
}
strong {
font-weight: 650;
color: #1d2129;
}
blockquote {
margin: 10px 0;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border-left: 3px solid var(--app-primary-color);
color: #86909c;
}
table {
display: block;
width: 100%;
overflow-x: auto;
margin: 10px 0 12px;
border-collapse: collapse;
font-size: 12px;
-webkit-overflow-scrolling: touch;
}
th,
td {
padding: 8px 10px;
border: 1px solid #e5e6eb;
text-align: left;
white-space: nowrap;
background: #fff;
}
th {
background: #f2f3f5;
font-weight: 600;
color: #1d2129;
}
a {
color: var(--app-primary-color);
text-decoration: none;
word-break: break-all;
}
hr {
margin: 14px 0;
border: none;
border-top: 1px dashed #e5e6eb;
}
img {
max-width: 100%;
border-radius: 10px;
margin: 4px 0 10px;
}
code {
padding: 1px 5px;
border-radius: 4px;
background: #fff;
font-size: 12px;
color: #1d2129;
}
pre {
margin: 10px 0;
padding: 12px;
border-radius: 10px;
overflow-x: auto;
background: #1e1e1e;
code {
padding: 0;
background: transparent;
color: #d4d4d4;
}
}
}
}
@ -2426,11 +2712,60 @@ onMounted(() => {
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));
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
background: #fff;
border-top: 1px solid #e5e6eb;
border-top: 1px solid #eef0f3;
}
.h5-detail__icon-btn {
flex: 0 0 48px;
width: 48px;
height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
border: 1px solid #e5e6eb;
border-radius: 12px;
background: #fff;
color: #4e5969;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
&:active {
background: #f7f8fa;
transform: scale(0.96);
}
}
.h5-detail__icon {
width: 22px;
height: 22px;
display: block;
}
.h5-detail__order-btn {
flex: 1 1 auto;
min-width: 0;
height: 48px;
margin: 0;
padding: 0 16px;
border: none;
border-radius: 12px;
background: var(--app-primary-color);
color: #fff;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
-webkit-tap-highlight-color: transparent;
&:active {
opacity: 0.92;
}
}
</style>

View File

@ -98,10 +98,10 @@
<a-spin :loading="loading && rows.length > 0" class="h5-so-spin">
<H5EmptyState
v-if="!loading && !rows.length"
:icon="IconList"
:icon="IconTabPackage"
:text="t('h5.storeOrders.empty')"
>
<a-button type="outline" size="small" @click="pullVisible = true">
<a-button class="app-btn-primary-soft" @click="pullVisible = true">
{{ t('h5.storeOrders.pull.entry') }}
</a-button>
</H5EmptyState>
@ -248,7 +248,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Message, Modal } from '@arco-design/web-vue';
import { IconCheck, IconCopy, IconDown, IconLeft, IconList, IconSearch } from '@arco-design/web-vue/es/icon';
import { IconCheck, IconCopy, IconDown, IconLeft, IconSearch } from '@arco-design/web-vue/es/icon';
import {
getPlatformStoreOrderList,
getPlatformStoreOrderSummary,
@ -263,6 +263,7 @@ import {
} from '@/utils/warehouseListMapper';
import { h5Navigate } from '@/utils/device';
import { getStoreOrderStatusTagStyle } from '@/constants/storeOrderStatus';
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
import H5EmptyState from '../components/H5EmptyState.vue';
import H5StoreOrderPullSheet from '../components/H5StoreOrderPullSheet.vue';
import H5StoreOrderCreateSheet, {

View File

@ -10,6 +10,10 @@
selected?: boolean
recommended?: boolean
recalcLoading?: boolean
/** 更多条件已指定货物属性时锁定,卡片内不可切换 */
lockCargoType?: boolean
/** 更多条件已指定仓库时锁定,卡片内不可切换 */
lockWarehouse?: boolean
cargoOptions?: Array<{ value: string; label: string }>
warehouseOptions?: Array<{ value: number; label: string }>
}>()
@ -49,6 +53,12 @@
return byLabel?.value || code
})
const selectedCargoLabel = computed(() => {
const code = selectedCargoType.value
const hit = cargoOptions.value.find((o) => o.value === code)
return hit?.label || String(props.record.cargoTypeName || code || '-').trim() || '-'
})
const selectedWarehouseId = computed(() => {
const id = Number(props.record.warehouseId)
if (Number.isFinite(id) && id > 0) return id
@ -57,6 +67,12 @@
return hit?.value
})
const selectedWarehouseLabel = computed(() => {
const id = selectedWarehouseId.value
const hit = id != null ? warehouseOptions.value.find((o) => o.value === id) : undefined
return hit?.label || String(props.record.warehouse || '-').trim() || '-'
})
const tradeTermsLabel = computed(() => {
const tradeTerms = String(props.record.tradeTerms || '').trim().toUpperCase()
if (tradeTerms === 'DDP') return t('order.priceCheck.tradeTerms.ddp')
@ -106,17 +122,28 @@
emit('select')
}
function onCargoChange(value: string | number) {
function resolveRecalcWarehouseId() {
const fromSelect = Number(selectedWarehouseId.value)
if (Number.isFinite(fromSelect) && fromSelect > 0) return fromSelect
const fromRecord = Number(props.record.warehouseId ?? props.record.WarehouseId)
return Number.isFinite(fromRecord) && fromRecord > 0 ? fromRecord : undefined
}
function resolveRecalcCargoType() {
return String(selectedCargoType.value || props.record.cargoType || '').trim()
}
function onCargoChange(value: string | number | boolean | Record<string, unknown> | undefined) {
const cargoType = String(value ?? '').trim()
const warehouseId = selectedWarehouseId.value
const warehouseId = resolveRecalcWarehouseId()
if (!cargoType || warehouseId == null) return
if (cargoType === selectedCargoType.value) return
emit('recalc', { cargoType, warehouseId })
}
function onWarehouseChange(value: string | number) {
function onWarehouseChange(value: string | number | boolean | Record<string, unknown> | undefined) {
const warehouseId = Number(value)
const cargoType = selectedCargoType.value
const cargoType = resolveRecalcCargoType()
if (!cargoType || !Number.isFinite(warehouseId) || warehouseId <= 0) return
if (warehouseId === selectedWarehouseId.value) return
emit('recalc', { cargoType, warehouseId })
@ -138,22 +165,32 @@
<div class="quote-card__info">
<div class="quote-card__title-row">
<h3 class="quote-card__name">{{ record.shippingMethod || '-' }}</h3>
<p v-if="metaLine" class="quote-card__meta">{{ metaLine }}</p>
<span v-if="recommended" class="quote-card__badge">
{{ t('order.priceCheck.card.recommendTag') }}
</span>
</div>
<p v-if="metaLine" class="quote-card__meta">{{ metaLine }}</p>
<div class="quote-card__switches" @click.stop>
<label class="quote-card__switch">
<div
class="quote-card__switch"
:class="{ 'is-locked': lockCargoType }"
:title="
lockCargoType
? t('order.priceCheck.card.switchLockedHint')
: t('order.priceCheck.card.switchRecalcHint')
"
>
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.cargoType') }}</span>
<span v-if="lockCargoType" class="quote-card__switch-value">{{ selectedCargoLabel }}</span>
<a-select
v-else
:model-value="selectedCargoType"
size="mini"
class="quote-card__select"
popup-container="body"
:placeholder="t('order.priceCheck.card.cargoType')"
:disabled="!!recalcLoading || !cargoOptions.length"
:title="t('order.priceCheck.card.switchRecalcHint')"
@change="onCargoChange"
>
<a-option
@ -164,16 +201,26 @@
{{ option.label }}
</a-option>
</a-select>
</label>
<label class="quote-card__switch">
</div>
<div
class="quote-card__switch"
:class="{ 'is-locked': lockWarehouse }"
:title="
lockWarehouse
? t('order.priceCheck.card.switchLockedHint')
: t('order.priceCheck.card.switchRecalcHint')
"
>
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.warehouse') }}</span>
<span v-if="lockWarehouse" class="quote-card__switch-value">{{ selectedWarehouseLabel }}</span>
<a-select
v-else
:model-value="selectedWarehouseId"
size="mini"
class="quote-card__select"
popup-container="body"
:placeholder="t('order.priceCheck.card.warehouse')"
:disabled="!!recalcLoading || !warehouseOptions.length"
:title="t('order.priceCheck.card.switchRecalcHint')"
@change="onWarehouseChange"
>
<a-option
@ -184,7 +231,7 @@
{{ option.label }}
</a-option>
</a-select>
</label>
</div>
</div>
</div>
@ -398,7 +445,6 @@
.quote-card__meta {
margin: 0;
min-width: 0;
flex: 1 1 auto;
color: var(--color-text-3);
font-size: 12px;
line-height: 1.3;
@ -448,6 +494,10 @@
min-width: 0;
margin: 0;
cursor: pointer;
&.is-locked {
cursor: default;
}
}
.quote-card__switch-label {
@ -458,6 +508,17 @@
white-space: nowrap;
}
.quote-card__switch-value {
max-width: 110px;
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quote-card__select {
width: auto;
min-width: 72px;

View File

@ -98,6 +98,8 @@
if (formData.cargoType) n += 1;
if (formData.warehouseId != null) n += 1;
if (hasFullDimensions() || hasPartialDimensions()) n += 1;
if (String(formData.destinationProvince || '').trim()) n += 1;
if (String(formData.destinationCity || '').trim()) n += 1;
if (couponInstanceId.value) n += 1;
return n;
});
@ -117,7 +119,14 @@
type AdvancedConditionChip = {
key: string;
label: string;
kind: 'dimensions' | 'cargoType' | 'warehouse' | 'shipping' | 'coupon';
kind:
| 'dimensions'
| 'destinationProvince'
| 'destinationCity'
| 'cargoType'
| 'warehouse'
| 'shipping'
| 'coupon';
value?: string | number;
};
@ -132,6 +141,26 @@
});
}
const province = String(formData.destinationProvince || '').trim();
if (province) {
chips.push({
key: 'destinationProvince',
kind: 'destinationProvince',
value: province,
label: `${t('order.priceCheck.form.destinationProvince')}: ${province}`,
});
}
const city = String(formData.destinationCity || '').trim();
if (city) {
chips.push({
key: 'destinationCity',
kind: 'destinationCity',
value: city,
label: `${t('order.priceCheck.form.destinationCity')}: ${city}`,
});
}
if (formData.cargoType) {
const hit = cargoTypeOptions.value.find((c) => c.value === formData.cargoType);
chips.push({
@ -184,6 +213,14 @@
formData.height = '';
return;
}
if (chip.kind === 'destinationProvince') {
formData.destinationProvince = '';
return;
}
if (chip.kind === 'destinationCity') {
formData.destinationCity = '';
return;
}
if (chip.kind === 'cargoType') {
formData.cargoType = '';
return;
@ -205,6 +242,8 @@
formData.length = '';
formData.width = '';
formData.height = '';
formData.destinationProvince = '';
formData.destinationCity = '';
formData.cargoType = '';
formData.warehouseId = undefined;
formData.shippingMethod = '';
@ -1016,26 +1055,6 @@
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.destinationProvince') }}</label>
<a-input
v-model="formData.destinationProvince"
:placeholder="$t('order.priceCheck.form.destinationProvince.placeholder')"
class="form-input"
@press-enter="handleQuery"
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.destinationCity') }}</label>
<a-input
v-model="formData.destinationCity"
:placeholder="$t('order.priceCheck.form.destinationCity.placeholder')"
class="form-input"
@press-enter="handleQuery"
/>
</div>
<div class="form-item button-group">
<label class="form-label form-label-spacer" aria-hidden="true">&nbsp;</label>
<div class="button-group-row">
@ -1124,6 +1143,26 @@
</p>
</div>
<div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.destinationProvince') }}</label>
<a-input
v-model="formData.destinationProvince"
:placeholder="$t('order.priceCheck.form.destinationProvince.placeholder')"
class="form-input"
@press-enter="handleQuery"
/>
</div>
<div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.destinationCity') }}</label>
<a-input
v-model="formData.destinationCity"
:placeholder="$t('order.priceCheck.form.destinationCity.placeholder')"
class="form-input"
@press-enter="handleQuery"
/>
</div>
<div v-if="availableCoupons.length" class="form-item form-item-coupon">
<label class="form-label">{{ $t('marketing.priceCheck.couponLabel') }}</label>
<a-select
@ -1266,6 +1305,8 @@
:recommended="isRecommendStamp(record)"
:cargo-options="cargoTypeOptions"
:warehouse-options="warehouseOptions"
:lock-cargo-type="!!formData.cargoType"
:lock-warehouse="formData.warehouseId != null"
:recalc-loading="!!recalcLoadingKeys[record.cardKey || record.rowKey]"
@select="handleSelectGuide(record)"
@order="handleOrder(record)"