feat: 查价页优化,销售产品可改代码,分区导入跳过重复

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-06 15:37:02 +08:00
parent 0d5ebdb591
commit 63c7e4ab6f
10 changed files with 231 additions and 146 deletions

View File

@ -117,6 +117,7 @@ export async function postForewarningServiceKpiAreaGetKpiAreaInfos(
export type KpiAreaInfoImportResult = { export type KpiAreaInfoImportResult = {
successCount: number successCount: number
failCount: number failCount: number
skipCount: number
failures: Array<{ rowNumber: number; reason: string }> failures: Array<{ rowNumber: number; reason: string }>
} }
@ -261,6 +262,7 @@ export async function importKpiAreaInfo(
return { return {
successCount: Number(d.successCount ?? d.SuccessCount ?? 0), successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
failCount: Number(d.failCount ?? d.FailCount ?? 0), failCount: Number(d.failCount ?? d.FailCount ?? 0),
skipCount: Number(d.skipCount ?? d.SkipCount ?? 0),
failures, failures,
} }
} }

View File

@ -62,6 +62,11 @@ export async function updateSalesProduct(body: Record<string, any> & { id: numbe
assertBusinessOk(res.data) assertBusinessOk(res.data)
} }
export async function updateSalesProductCode(body: { id: number | string; productCode: string }) {
const res = await axiosInstance.post(omsUrl('/channel/sales-product/product-code/update'), body)
assertBusinessOk(res.data)
}
export async function deleteSalesProduct(id: number | string) { export async function deleteSalesProduct(id: number | string) {
const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id }) const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id })
assertBusinessOk(res.data) assertBusinessOk(res.data)

View File

@ -656,6 +656,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'AddProduct', name: '新增产品' }, { code: 'AddProduct', name: '新增产品' },
{ code: 'Create', name: '新增' }, { code: 'Create', name: '新增' },
{ code: 'Update', name: '修改' }, { code: 'Update', name: '修改' },
{ code: 'UpdateCode', name: '修改产品代码' },
{ code: 'Delete', name: '删除' }, { code: 'Delete', name: '删除' },
{ code: 'EditPrice', name: '编辑' }, { code: 'EditPrice', name: '编辑' },
{ code: 'Save', name: '保存' }, { code: 'Save', name: '保存' },

View File

@ -2368,7 +2368,7 @@ export default {
'order.priceCheck.form.destinationCountry.placeholder': 'Please select', 'order.priceCheck.form.destinationCountry.placeholder': 'Please select',
'order.priceCheck.form.shippingMethod': 'Shipping method', 'order.priceCheck.form.shippingMethod': 'Shipping method',
'order.priceCheck.form.shippingMethod.placeholder': 'Please select', 'order.priceCheck.form.shippingMethod.placeholder': 'Please select',
'order.priceCheck.form.channel': 'Channel', 'order.priceCheck.form.channel': 'Route',
'order.priceCheck.form.channel.placeholder': 'All', 'order.priceCheck.form.channel.placeholder': 'All',
'order.priceCheck.form.cargoWeight': 'Cargo weight', 'order.priceCheck.form.cargoWeight': 'Cargo weight',
'order.priceCheck.form.cargoWeight.placeholder': 'Please enter weight (KG)', 'order.priceCheck.form.cargoWeight.placeholder': 'Please enter weight (KG)',
@ -2389,7 +2389,7 @@ export default {
'order.priceCheck.form.postalCode': 'Postal code', 'order.priceCheck.form.postalCode': 'Postal code',
'order.priceCheck.form.postalCode.tip': 'more accurate', 'order.priceCheck.form.postalCode.tip': 'more accurate',
'order.priceCheck.form.postalCode.placeholder': 'Optional — improves zoning', 'order.priceCheck.form.postalCode.placeholder': 'Optional — improves zoning',
'order.priceCheck.form.moreFilters': 'More filters', 'order.priceCheck.form.moreFilters': 'More',
'order.priceCheck.form.activeFilters': '{count} selected', 'order.priceCheck.form.activeFilters': '{count} selected',
'order.priceCheck.form.clearFilters': 'Clear', 'order.priceCheck.form.clearFilters': 'Clear',
'order.priceCheck.form.removeFilter': 'Remove filter', 'order.priceCheck.form.removeFilter': 'Remove filter',
@ -2465,9 +2465,9 @@ export default {
'order.priceCheck.table.viewDetail': 'Details', 'order.priceCheck.table.viewDetail': 'Details',
'order.priceCheck.table.viewGuide': 'Channel guide', 'order.priceCheck.table.viewGuide': 'Channel guide',
'order.priceCheck.table.viewGuideHint': 'Click to view route guide', 'order.priceCheck.table.viewGuideHint': 'Click to view route guide',
'order.priceCheck.table.customsOrderNow': 'Place order', 'order.priceCheck.table.customsOrderNow': 'Go to order',
'order.priceCheck.table.orderNow': 'Place order', 'order.priceCheck.table.orderNow': 'Go to order',
'order.priceCheck.table.askAi': 'Ask AI', 'order.priceCheck.table.askAi': 'Ask advisor',
'order.priceCheck.table.askAiCanShip': 'Can this ship?', 'order.priceCheck.table.askAiCanShip': 'Can this ship?',
'order.priceCheck.table.askAiCheaper': 'Anything cheaper?', 'order.priceCheck.table.askAiCheaper': 'Anything cheaper?',
'order.priceCheck.table.empty': 'No matching quotes', 'order.priceCheck.table.empty': 'No matching quotes',
@ -5171,7 +5171,13 @@ export default {
'channel.salesProducts.placeholder.productDescription': 'Customer-facing description (optional)', 'channel.salesProducts.placeholder.productDescription': 'Customer-facing description (optional)',
'channel.salesProducts.placeholder.preStorageScript': 'Script before order persistence (optional)', 'channel.salesProducts.placeholder.preStorageScript': 'Script before order persistence (optional)',
'channel.salesProducts.tip.productCode': 'channel.salesProducts.tip.productCode':
'Rule: region/country code + 01 line/02 express/03 postal/04 packet + other.\nExample: US04-5DAY\nNot editable after create; uppercase letters, digits, and - only', 'Rule: region/country code + 01 line/02 express/03 postal/04 packet + other.\nExample: US04-5DAY\nUppercase letters, digits, and - only; can be changed later via the edit action',
'channel.salesProducts.form.editProductCode': 'Edit',
'channel.salesProducts.modal.editProductCodeTitle': 'Edit product code',
'channel.salesProducts.modal.editProductCodeHint':
'Updates the sales-product master code only. Historical orders keep the old code; quoting and place-order use the new one.',
'channel.salesProducts.messages.productCodeRequired': 'Enter a product code',
'channel.salesProducts.messages.productCodeUpdated': 'Product code updated',
'channel.salesProducts.tip.productName': 'channel.salesProducts.tip.productName':
'Rule: region/country + product type description + other.\nExample: US air packet-5 days', 'Rule: region/country + product type description + other.\nExample: US air packet-5 days',
'channel.salesProducts.tip.group': 'Used for website/admin grouping, e.g. express parcel', 'channel.salesProducts.tip.group': 'Used for website/admin grouping, e.g. express parcel',
@ -5558,7 +5564,7 @@ export default {
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.', 'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
'channel.area.import.modalTitle': 'Import area details', 'channel.area.import.modalTitle': 'Import area details',
'channel.area.import.tip': 'channel.area.import.tip':
'Download the template first. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.', 'Download the template first. Import is incremental: exact duplicate rules in this area are skipped, not overwritten. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
'channel.area.import.downloadTemplate': 'Download template', 'channel.area.import.downloadTemplate': 'Download template',
'channel.area.import.submit': 'Import', 'channel.area.import.submit': 'Import',
'channel.area.import.downloadSuccess': 'Template downloaded', 'channel.area.import.downloadSuccess': 'Template downloaded',
@ -5569,7 +5575,7 @@ export default {
'channel.area.import.failed': 'Import failed', 'channel.area.import.failed': 'Import failed',
'channel.area.import.partialSuccess': 'Some rows failed. See details below.', 'channel.area.import.partialSuccess': 'Some rows failed. See details below.',
'channel.area.import.allFailed': 'Import failed. Please check the file.', 'channel.area.import.allFailed': 'Import failed. Please check the file.',
'channel.area.import.resultSummary': '{success} succeeded, {fail} failed', 'channel.area.import.resultSummary': '{success} succeeded, {skip} duplicate skipped, {fail} failed',
'channel.area.import.failRow': 'Row {row}: {reason}', 'channel.area.import.failRow': 'Row {row}: {reason}',
'channel.area.export.success': 'Exported {count} row(s)', 'channel.area.export.success': 'Exported {count} row(s)',
'channel.area.export.failed': 'Export failed', 'channel.area.export.failed': 'Export failed',

View File

@ -2254,7 +2254,7 @@ export default {
'order.priceCheck.form.destinationCountry.placeholder': '请选择', 'order.priceCheck.form.destinationCountry.placeholder': '请选择',
'order.priceCheck.form.shippingMethod': '运输方式', 'order.priceCheck.form.shippingMethod': '运输方式',
'order.priceCheck.form.shippingMethod.placeholder': '请选择', 'order.priceCheck.form.shippingMethod.placeholder': '请选择',
'order.priceCheck.form.channel': '渠道', 'order.priceCheck.form.channel': '线路',
'order.priceCheck.form.channel.placeholder': '全部', 'order.priceCheck.form.channel.placeholder': '全部',
'order.priceCheck.form.cargoWeight': '货物重量', 'order.priceCheck.form.cargoWeight': '货物重量',
'order.priceCheck.form.cargoWeight.placeholder': '请输入重量(KG)', 'order.priceCheck.form.cargoWeight.placeholder': '请输入重量(KG)',
@ -2275,7 +2275,7 @@ export default {
'order.priceCheck.form.postalCode': '邮编', 'order.priceCheck.form.postalCode': '邮编',
'order.priceCheck.form.postalCode.tip': '更准', 'order.priceCheck.form.postalCode.tip': '更准',
'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确', 'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确',
'order.priceCheck.form.moreFilters': '更多条件', 'order.priceCheck.form.moreFilters': '更多',
'order.priceCheck.form.activeFilters': '已选 {count} 项', 'order.priceCheck.form.activeFilters': '已选 {count} 项',
'order.priceCheck.form.clearFilters': '清空', 'order.priceCheck.form.clearFilters': '清空',
'order.priceCheck.form.removeFilter': '移除条件', 'order.priceCheck.form.removeFilter': '移除条件',
@ -2351,9 +2351,9 @@ export default {
'order.priceCheck.table.viewDetail': '详情', 'order.priceCheck.table.viewDetail': '详情',
'order.priceCheck.table.viewGuide': '渠道说明', 'order.priceCheck.table.viewGuide': '渠道说明',
'order.priceCheck.table.viewGuideHint': '点击查看线路说明', 'order.priceCheck.table.viewGuideHint': '点击查看线路说明',
'order.priceCheck.table.customsOrderNow': '立刻下单', 'order.priceCheck.table.customsOrderNow': '去下单',
'order.priceCheck.table.orderNow': '立刻下单', 'order.priceCheck.table.orderNow': '去下单',
'order.priceCheck.table.askAi': '问 AI', 'order.priceCheck.table.askAi': '问顾问',
'order.priceCheck.table.askAiCanShip': '这个能不能发?', 'order.priceCheck.table.askAiCanShip': '这个能不能发?',
'order.priceCheck.table.askAiCheaper': '有没有更便宜?', 'order.priceCheck.table.askAiCheaper': '有没有更便宜?',
'order.priceCheck.table.empty': '暂无匹配报价', 'order.priceCheck.table.empty': '暂无匹配报价',
@ -5206,7 +5206,13 @@ export default {
'channel.salesProducts.placeholder.productDescription': '面向客户的产品说明(选填)', 'channel.salesProducts.placeholder.productDescription': '面向客户的产品说明(选填)',
'channel.salesProducts.placeholder.preStorageScript': '订单落库前执行的脚本(选填)', 'channel.salesProducts.placeholder.preStorageScript': '订单落库前执行的脚本(选填)',
'channel.salesProducts.tip.productCode': 'channel.salesProducts.tip.productCode':
'命名规则:地区/国家代码 + 01专线/02快递/03邮政/04小包 + 其他。\n示例:US04-5DAY\n创建后不可修改;仅支持大写字母、数字和 -', '命名规则:地区/国家代码 + 01专线/02快递/03邮政/04小包 + 其他。\n示例:US04-5DAY\n仅支持大写字母、数字和 -;创建后可通过修改入口单独改代码',
'channel.salesProducts.form.editProductCode': '修改',
'channel.salesProducts.modal.editProductCodeTitle': '修改产品代码',
'channel.salesProducts.modal.editProductCodeHint':
'仅改销售产品主数据代码。历史订单仍保留原代码;下单、查价将使用新代码。',
'channel.salesProducts.messages.productCodeRequired': '请输入产品代码',
'channel.salesProducts.messages.productCodeUpdated': '产品代码已更新',
'channel.salesProducts.tip.productName': 'channel.salesProducts.tip.productName':
'命名规则:地区/国家 + 产品类型描述 + 其他。\n示例:美国空运小包-5日达', '命名规则:地区/国家 + 产品类型描述 + 其他。\n示例:美国空运小包-5日达',
'channel.salesProducts.tip.group': '用于官网/后台分组展示,如专线小包', 'channel.salesProducts.tip.group': '用于官网/后台分组展示,如专线小包',
@ -5569,7 +5575,7 @@ export default {
'channel.area.search.button.query': '查询', 'channel.area.search.button.query': '查询',
'channel.area.search.button.reset': '重置', 'channel.area.search.button.reset': '重置',
'channel.area.import.modalTitle': '导入分区明细', 'channel.area.import.modalTitle': '导入分区明细',
'channel.area.import.tip': '请先下载模板。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。', 'channel.area.import.tip': '请先下载模板。导入为增量写入:与本分区已有规则完全相同的行会跳过,不会覆盖。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
'channel.area.import.downloadTemplate': '下载模板', 'channel.area.import.downloadTemplate': '下载模板',
'channel.area.import.submit': '开始导入', 'channel.area.import.submit': '开始导入',
'channel.area.import.downloadSuccess': '模板下载成功', 'channel.area.import.downloadSuccess': '模板下载成功',
@ -5580,7 +5586,7 @@ export default {
'channel.area.import.failed': '导入失败', 'channel.area.import.failed': '导入失败',
'channel.area.import.partialSuccess': '部分行导入失败,请查看失败明细', 'channel.area.import.partialSuccess': '部分行导入失败,请查看失败明细',
'channel.area.import.allFailed': '导入失败,请检查文件内容', 'channel.area.import.allFailed': '导入失败,请检查文件内容',
'channel.area.import.resultSummary': '成功 {success} 条,失败 {fail} 条', 'channel.area.import.resultSummary': '成功 {success} 条,跳过重复 {skip} 条,失败 {fail} 条',
'channel.area.import.failRow': '第 {row} 行:{reason}', 'channel.area.import.failRow': '第 {row} 行:{reason}',
'channel.area.export.success': '已导出 {count} 条', 'channel.area.export.success': '已导出 {count} 条',
'channel.area.export.failed': '导出失败', 'channel.area.export.failed': '导出失败',

View File

@ -34,6 +34,7 @@
{{ {{
t('channel.area.import.resultSummary', { t('channel.area.import.resultSummary', {
success: importResult.successCount, success: importResult.successCount,
skip: importResult.skipCount,
fail: importResult.failCount, fail: importResult.failCount,
}) })
}} }}
@ -117,7 +118,7 @@
try { try {
const result = await importKpiAreaInfo(areaId.value, selectedFile.value); const result = await importKpiAreaInfo(areaId.value, selectedFile.value);
importResult.value = result; importResult.value = result;
if (result.successCount > 0) { if (result.successCount > 0 || result.skipCount > 0) {
emit('success'); emit('success');
} }
if (result.failCount === 0) { if (result.failCount === 0) {
@ -125,7 +126,7 @@
visible.value = false; visible.value = false;
return true; return true;
} }
if (result.successCount > 0) { if (result.successCount > 0 || result.skipCount > 0) {
Message.warning(t('channel.area.import.partialSuccess')); Message.warning(t('channel.area.import.partialSuccess'));
} else { } else {
Message.error(t('channel.area.import.allFailed')); Message.error(t('channel.area.import.allFailed'));

View File

@ -18,6 +18,7 @@
saveSalesProductCustomerGuide, saveSalesProductCustomerGuide,
toggleSalesProductPriceStatus, toggleSalesProductPriceStatus,
updateSalesProduct, updateSalesProduct,
updateSalesProductCode,
updateSalesProductPrice, updateSalesProductPrice,
} from '@/api/channel/salesProduct'; } from '@/api/channel/salesProduct';
import { getLabelTemplateList } from '@/api/channel/labelTemplate'; import { getLabelTemplateList } from '@/api/channel/labelTemplate';
@ -154,6 +155,9 @@
const customerGuideSaving = ref(false); const customerGuideSaving = ref(false);
const customerGuidePanelRef = ref<InstanceType<typeof MarkdownDocPanel> | null>(null); const customerGuidePanelRef = ref<InstanceType<typeof MarkdownDocPanel> | null>(null);
const savingConfig = ref(false); const savingConfig = ref(false);
const productCodeModalVisible = ref(false);
const productCodeDraft = ref('');
const savingProductCode = ref(false);
function parseAdditionalServiceCodes(raw: unknown): string[] { function parseAdditionalServiceCodes(raw: unknown): string[] {
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
@ -252,6 +256,42 @@
.replace(/[^A-Z0-9-]/g, ''); .replace(/[^A-Z0-9-]/g, '');
} }
function openProductCodeModal() {
if (selectedProduct.value == null) return;
productCodeDraft.value = normalizeBizCode(formData.productCode);
productCodeModalVisible.value = true;
}
async function handleSaveProductCode() {
const pid = selectedProduct.value;
if (pid == null) return;
const next = normalizeBizCode(productCodeDraft.value);
if (!next) {
Message.warning(t('channel.salesProducts.messages.productCodeRequired'));
return Promise.reject();
}
savingProductCode.value = true;
try {
await updateSalesProductCode({ id: pid, productCode: next });
formData.productCode = next;
const hit = productList.value.find((p: any) => p.id === pid);
if (hit?.raw) {
hit.raw.productCode = next;
hit.raw.ProductCode = next;
}
if (configFormBaseline.value) {
configFormBaseline.value = { ...configFormBaseline.value, productCode: next };
}
productCodeModalVisible.value = false;
Message.success(t('channel.salesProducts.messages.productCodeUpdated'));
} catch (e: any) {
Message.error(e?.message || '修改产品代码失败');
return Promise.reject(e);
} finally {
savingProductCode.value = false;
}
}
const selectedProductContext = computed(() => { const selectedProductContext = computed(() => {
const pid = selectedProduct.value; const pid = selectedProduct.value;
if (pid == null) return null; if (pid == null) return null;
@ -4014,10 +4054,18 @@
</template> </template>
<icon-question-circle class="tip-icon" /> <icon-question-circle class="tip-icon" />
</a-tooltip> </a-tooltip>
<button
type="button"
class="product-code-edit-icon"
:aria-label="t('channel.salesProducts.form.editProductCode')"
@click.stop="openProductCodeModal"
>
<icon-edit />
</button>
</label> </label>
<a-input <a-input
v-model="formData.productCode" v-model="formData.productCode"
:placeholder="t('channel.salesProducts.form.placeholder.input')" :placeholder="t('channel.salesProducts.placeholder.productCode')"
class="form-input" class="form-input"
disabled disabled
/> />
@ -5487,6 +5535,25 @@
</div> </div>
</a-modal> </a-modal>
<a-modal
v-model:visible="productCodeModalVisible"
:title="t('channel.salesProducts.modal.editProductCodeTitle')"
width="420px"
:mask-closable="false"
:ok-loading="savingProductCode"
@ok="handleSaveProductCode"
@cancel="productCodeModalVisible = false"
>
<p class="product-code-edit-hint">{{ t('channel.salesProducts.modal.editProductCodeHint') }}</p>
<a-input
:model-value="productCodeDraft"
:placeholder="t('channel.salesProducts.placeholder.productCode')"
allow-clear
@update:model-value="(v) => (productCodeDraft = normalizeBizCode(v))"
@press-enter="handleSaveProductCode"
/>
</a-modal>
<ChannelReferencePreviewModal ref="referencePreviewRef" :dict-label-fn="dictLabelByType" /> <ChannelReferencePreviewModal ref="referencePreviewRef" :dict-label-fn="dictLabelByType" />
</div> </div>
</template> </template>
@ -5691,6 +5758,31 @@
font-size: 14px; font-size: 14px;
} }
.product-code-edit-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
border: none;
background: transparent;
color: var(--color-text-3);
font-size: 14px;
line-height: 1;
cursor: pointer;
&:hover {
color: var(--app-primary-color, #007979);
}
}
.product-code-edit-hint {
margin: 0 0 12px;
color: var(--color-text-3);
font-size: 12px;
line-height: 1.5;
}
.form-input, .form-input,
.form-select, .form-select,
.form-textarea { .form-textarea {

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { IconDown } from '@arco-design/web-vue/es/icon' import IconTabService from '@/assets/icons/h5/tab-service.svg'
defineOptions({ name: 'PriceCheckQuoteResultCard' }) defineOptions({ name: 'PriceCheckQuoteResultCard' })
@ -328,13 +328,13 @@
</div> </div>
<div class="quote-card__actions" @click.stop> <div class="quote-card__actions" @click.stop>
<button type="button" class="ops-btn ops-btn--primary" @click="emit('order')"> <a-dropdown trigger="click" position="bl">
{{ t('order.priceCheck.table.orderNow') }} <button
</button> type="button"
<a-dropdown trigger="click" position="br"> class="advisor-icon-btn"
<button type="button" class="ops-btn ops-btn--ghost"> :aria-label="t('order.priceCheck.table.askAi')"
{{ t('order.priceCheck.table.askAi') }} >
<icon-down style="margin-left: 2px" /> <IconTabService class="advisor-icon-btn__icon" />
</button> </button>
<template #content> <template #content>
<a-doption @click="emit('ask', 'canShip')"> <a-doption @click="emit('ask', 'canShip')">
@ -345,6 +345,9 @@
</a-doption> </a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
<button type="button" class="ops-btn ops-btn--primary" @click="emit('order')">
{{ t('order.priceCheck.table.orderNow') }}
</button>
</div> </div>
</div> </div>
</a-spin> </a-spin>
@ -666,25 +669,18 @@
.quote-card__actions { .quote-card__actions {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: stretch; align-items: center;
justify-content: center; justify-content: center;
gap: 6px; gap: 6px;
flex-shrink: 0; flex-shrink: 0;
min-width: 88px;
:deep(.arco-dropdown-open), :deep(.arco-dropdown-open),
:deep(.arco-trigger), :deep(.arco-trigger),
> :deep(.arco-dropdown-trigger), > :deep(.arco-dropdown-trigger),
> .arco-dropdown-open { > .arco-dropdown-open {
width: 100%; display: inline-flex;
display: block; flex-shrink: 0;
}
> :deep(span),
> :deep(div) {
width: 100%;
display: block;
} }
} }
@ -692,36 +688,54 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%;
height: 28px;
padding: 0 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer; cursor: pointer;
border: 1px solid transparent; border: 1px solid transparent;
&--ghost {
background: transparent;
border-color: var(--color-border-2);
color: var(--color-text-2);
&:hover {
color: var(--app-primary-color, #007979);
border-color: color-mix(in srgb, var(--app-primary-color, #007979) 35%, var(--color-border-2));
background: color-mix(in srgb, var(--app-primary-color, #007979) 4%, transparent);
}
}
&--primary { &--primary {
min-width: 88px;
height: 32px;
padding: 0 16px;
border-radius: 6px;
background: var(--app-primary-color, #007979); background: var(--app-primary-color, #007979);
color: #fff; color: #fff;
font-size: 13px;
font-weight: 600;
line-height: 1;
text-align: center;
box-shadow: 0 1px 2px color-mix(in srgb, var(--app-primary-color, #007979) 28%, transparent);
&:hover { &:hover {
filter: brightness(1.04); filter: brightness(1.06);
} }
} }
} }
.advisor-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin: 0;
padding: 0;
border: none;
border-radius: 16px;
background: transparent;
color: var(--app-primary-color, #007979);
cursor: pointer;
flex-shrink: 0;
&:hover {
background: color-mix(in srgb, var(--app-primary-color, #007979) 8%, transparent);
}
}
.advisor-icon-btn__icon {
width: 22px;
height: 22px;
display: block;
}
.quote-card__fee-pop { .quote-card__fee-pop {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -799,7 +813,7 @@
.quote-card__actions { .quote-card__actions {
width: 100%; width: 100%;
justify-content: flex-end; justify-content: center;
} }
} }
@ -810,13 +824,10 @@
} }
.quote-card__actions { .quote-card__actions {
flex-direction: row;
width: 100%; width: 100%;
min-width: 0;
} }
.ops-btn--primary { .ops-btn--primary {
flex: 1;
min-width: 120px; min-width: 120px;
} }
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { IconDown, IconSearch } from '@arco-design/web-vue/es/icon' import { IconClose, IconDown, IconSearch } from '@arco-design/web-vue/es/icon'
import SlidingTabs from '@/components/SlidingTabs/index.vue' import SlidingTabs from '@/components/SlidingTabs/index.vue'
import type { PriceCheckShippingGroup } from '@/api/order' import type { PriceCheckShippingGroup } from '@/api/order'
@ -132,7 +132,7 @@ function clearSelection(e: Event) {
aria-label="clear" aria-label="clear"
@click="clearSelection" @click="clearSelection"
> >
× <icon-close />
</button> </button>
<icon-down class="trigger-icon" /> <icon-down class="trigger-icon" />
</div> </div>
@ -233,20 +233,23 @@ function clearSelection(e: Event) {
} }
.trigger-clear { .trigger-clear {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0; flex-shrink: 0;
width: 18px; width: 16px;
height: 18px; height: 16px;
margin-left: 4px; margin-left: 4px;
padding: 0;
border: none; border: none;
border-radius: 50%; background: transparent;
background: var(--color-fill-4); color: var(--color-text-3);
color: #fff;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 1;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: var(--color-text-3); color: var(--color-text-1);
} }
} }

View File

@ -40,7 +40,7 @@
const formData = reactive({ const formData = reactive({
destinationCountry: '', destinationCountry: '',
shippingMethod: '', shippingMethod: '',
cargoWeight: '', cargoWeight: '1',
length: '', length: '',
width: '', width: '',
height: '', height: '',
@ -92,18 +92,6 @@
const couponInstanceId = ref<number | undefined>(undefined); const couponInstanceId = ref<number | undefined>(undefined);
const availableCoupons = ref<CouponAvailableOutput[]>([]); const availableCoupons = ref<CouponAvailableOutput[]>([]);
const advancedActiveCount = computed(() => {
let n = 0;
if (formData.shippingMethod) n += 1;
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;
});
const needPostalHint = computed(() => { const needPostalHint = computed(() => {
if (!hasQueried.value) return false; if (!hasQueried.value) return false;
if (String(formData.postalCode || '').trim()) return false; if (String(formData.postalCode || '').trim()) return false;
@ -125,7 +113,6 @@
| 'destinationCity' | 'destinationCity'
| 'cargoType' | 'cargoType'
| 'warehouse' | 'warehouse'
| 'shipping'
| 'coupon'; | 'coupon';
value?: string | number; value?: string | number;
}; };
@ -181,17 +168,6 @@
}); });
} }
if (formData.shippingMethod) {
const flat = (shippingMethodGroups.value || []).flatMap((g) => g.products || []);
const hit = flat.find((it) => it.productCode === formData.shippingMethod);
chips.push({
key: 'shipping',
kind: 'shipping',
value: formData.shippingMethod,
label: hit?.label || formData.shippingMethod,
});
}
if (couponInstanceId.value) { if (couponInstanceId.value) {
const c = availableCoupons.value.find((x) => (x.id ?? x.Id) === couponInstanceId.value); const c = availableCoupons.value.find((x) => (x.id ?? x.Id) === couponInstanceId.value);
const name = String(c?.templateName ?? c?.TemplateName ?? '').trim(); const name = String(c?.templateName ?? c?.TemplateName ?? '').trim();
@ -229,10 +205,6 @@
formData.warehouseId = undefined; formData.warehouseId = undefined;
return; return;
} }
if (chip.kind === 'shipping') {
formData.shippingMethod = '';
return;
}
if (chip.kind === 'coupon') { if (chip.kind === 'coupon') {
couponInstanceId.value = undefined; couponInstanceId.value = undefined;
} }
@ -246,7 +218,6 @@
formData.destinationCity = ''; formData.destinationCity = '';
formData.cargoType = ''; formData.cargoType = '';
formData.warehouseId = undefined; formData.warehouseId = undefined;
formData.shippingMethod = '';
couponInstanceId.value = undefined; couponInstanceId.value = undefined;
} }
@ -621,14 +592,15 @@
availableCoupons.value = coupons ?? []; availableCoupons.value = coupons ?? [];
hintBannerDismissed.value = false; hintBannerDismissed.value = false;
queryResults.value = processQueryResults(items || []); queryResults.value = processQueryResults(items || []);
selectedGuideRecord.value = null;
resetRecommendState(); resetRecommendState();
sortMode.value = 'recommend'; sortMode.value = 'recommend';
currentPage.value = 1; currentPage.value = 1;
recalcLoadingKeys.value = {}; recalcLoadingKeys.value = {};
if (!queryResults.value.length) { if (!queryResults.value.length) {
selectedGuideRecord.value = null;
applyPriceCheckFailures(failures ?? []); applyPriceCheckFailures(failures ?? []);
} else { } else {
selectedGuideRecord.value = sortedResults.value[0] ?? queryResults.value[0];
clearPriceCheckFailures(); clearPriceCheckFailures();
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length })); Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
} }
@ -648,7 +620,7 @@
Object.assign(formData, { Object.assign(formData, {
destinationCountry: '', destinationCountry: '',
shippingMethod: '', shippingMethod: '',
cargoWeight: '', cargoWeight: '1',
length: '', length: '',
width: '', width: '',
height: '', height: '',
@ -1001,6 +973,17 @@
</div> </div>
<div class="form-row form-row-primary"> <div class="form-row form-row-primary">
<div class="form-item form-item-channel">
<label class="form-label">{{ $t('order.priceCheck.form.channel') }}</label>
<ShippingMethodTabSelect
v-model="formData.shippingMethod"
:groups="shippingMethodGroups"
:loading="optionsLoading"
:placeholder="$t('order.priceCheck.form.channel.placeholder')"
class="form-select"
/>
</div>
<div class="form-item form-item-country"> <div class="form-item form-item-country">
<label class="form-label required">{{ $t('order.priceCheck.form.destinationCountry') }}</label> <label class="form-label required">{{ $t('order.priceCheck.form.destinationCountry') }}</label>
<a-select <a-select
@ -1035,21 +1018,21 @@
</a-select> </a-select>
</div> </div>
<div class="form-item"> <div ref="postalInputRef" class="form-item form-item-compact">
<label class="form-label required">{{ $t('order.priceCheck.form.cargoWeight') }}</label> <label class="form-label">{{ $t('order.priceCheck.form.postalCode') }}</label>
<a-input <a-input
v-model="formData.cargoWeight" v-model="formData.postalCode"
:placeholder="$t('order.priceCheck.form.cargoWeight.placeholder')" :placeholder="$t('order.priceCheck.form.postalCode.placeholder')"
class="form-input" class="form-input"
@press-enter="handleQuery" @press-enter="handleQuery"
/> />
</div> </div>
<div ref="postalInputRef" class="form-item"> <div class="form-item form-item-compact">
<label class="form-label">{{ $t('order.priceCheck.form.postalCode') }}</label> <label class="form-label required">{{ $t('order.priceCheck.form.cargoWeight') }}</label>
<a-input <a-input
v-model="formData.postalCode" v-model="formData.cargoWeight"
:placeholder="$t('order.priceCheck.form.postalCode.placeholder')" :placeholder="$t('order.priceCheck.form.cargoWeight.placeholder')"
class="form-input" class="form-input"
@press-enter="handleQuery" @press-enter="handleQuery"
/> />
@ -1071,10 +1054,7 @@
{{ $t('order.priceCheck.actions.reset') }} {{ $t('order.priceCheck.actions.reset') }}
</a-button> </a-button>
<button type="button" class="advanced-toggle-btn" @click="showAdvanced = !showAdvanced"> <button type="button" class="advanced-toggle-btn" @click="showAdvanced = !showAdvanced">
<span> <span>{{ $t('order.priceCheck.form.moreFilters') }}</span>
{{ $t('order.priceCheck.form.moreFilters') }}
<span v-if="advancedActiveCount" class="advanced-count">{{ advancedActiveCount }}</span>
</span>
<icon-up v-if="showAdvanced" /> <icon-up v-if="showAdvanced" />
<icon-down v-else /> <icon-down v-else />
</button> </button>
@ -1110,10 +1090,10 @@
<icon-close /> <icon-close />
</span> </span>
</button> </button>
<button type="button" class="filter-summary-bar__clear" @click="clearAdvancedConditions">
{{ $t('order.priceCheck.form.clearFilters') }}
</button>
</div> </div>
<button type="button" class="filter-summary-bar__clear" @click="clearAdvancedConditions">
{{ $t('order.priceCheck.form.clearFilters') }}
</button>
</div> </div>
<div v-show="showAdvanced" class="form-row form-row-advanced"> <div v-show="showAdvanced" class="form-row form-row-advanced">
@ -1183,17 +1163,6 @@
</a-select> </a-select>
</div> </div>
<div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.channel') }}</label>
<ShippingMethodTabSelect
v-model="formData.shippingMethod"
:groups="shippingMethodGroups"
:loading="optionsLoading"
:placeholder="$t('order.priceCheck.form.channel.placeholder')"
class="form-select"
/>
</div>
<div class="form-item"> <div class="form-item">
<label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label> <label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label>
<a-select <a-select
@ -1365,8 +1334,8 @@
background-color: var(--app-page-bg); background-color: var(--app-page-bg);
} }
.price-check-layout { display: flex; gap: 12px; flex: 1; min-height: 0; min-width: 0; } .price-check-layout { display: flex; gap: 12px; flex: 1; min-height: 0; min-width: 0; }
.price-check-main { flex: 7 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; } .price-check-main { flex: 5 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
.price-check-aside { flex: 3 1 0; min-width: 300px; min-height: 0; display: flex; flex-direction: column; > * { flex: 1; min-height: 0; } } .price-check-aside { flex: 3 1 0; min-width: 340px; min-height: 0; display: flex; flex-direction: column; > * { flex: 1; min-height: 0; } }
.liquid-panel { .liquid-panel {
position: relative; position: relative;
z-index: 0; z-index: 0;
@ -1439,8 +1408,9 @@
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
} }
.form-row { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; } .form-row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; } .form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
.form-row-primary { flex-wrap: nowrap; }
.form-row-advanced { .form-row-advanced {
margin-top: 12px; margin-top: 12px;
padding-top: 16px; padding-top: 16px;
@ -1543,23 +1513,9 @@
white-space: nowrap; white-space: nowrap;
&:hover { color: var(--app-primary-color, #007979); } &:hover { color: var(--app-primary-color, #007979); }
} }
.advanced-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
margin-left: 4px;
padding: 0 4px;
border-radius: 8px;
background: rgba(0, 121, 121, 0.1);
color: var(--app-primary-color, #007979);
font-size: 11px;
line-height: 16px;
}
.form-hint { margin: 0; color: var(--color-text-3); font-size: 12px; line-height: 1.3; } .form-hint { margin: 0; color: var(--color-text-3); font-size: 12px; line-height: 1.3; }
.form-item { .form-item {
display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; width: 168px; min-width: 140px; display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; width: 148px; min-width: 132px;
&.dimensions-group { width: 260px; min-width: 220px; } &.dimensions-group { width: 260px; min-width: 220px; }
&.button-group { width: auto; min-width: 0; flex-shrink: 0; } &.button-group { width: auto; min-width: 0; flex-shrink: 0; }
.button-group-row { .button-group-row {
@ -1570,7 +1526,9 @@
justify-content: flex-start; justify-content: flex-start;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
&.form-item-country { width: 280px; min-width: 240px; flex-shrink: 0; } &.form-item-country { width: 240px; min-width: 200px; flex-shrink: 0; }
&.form-item-channel { width: 220px; min-width: 180px; flex-shrink: 0; }
&.form-item-compact { width: 112px; min-width: 100px; }
} }
.country-shortcuts { .country-shortcuts {
display: flex; display: flex;