diff --git a/src/api/channel/kpiArea.ts b/src/api/channel/kpiArea.ts index a05871b..95ae68d 100644 --- a/src/api/channel/kpiArea.ts +++ b/src/api/channel/kpiArea.ts @@ -117,6 +117,7 @@ export async function postForewarningServiceKpiAreaGetKpiAreaInfos( export type KpiAreaInfoImportResult = { successCount: number failCount: number + skipCount: number failures: Array<{ rowNumber: number; reason: string }> } @@ -261,6 +262,7 @@ export async function importKpiAreaInfo( return { successCount: Number(d.successCount ?? d.SuccessCount ?? 0), failCount: Number(d.failCount ?? d.FailCount ?? 0), + skipCount: Number(d.skipCount ?? d.SkipCount ?? 0), failures, } } diff --git a/src/api/channel/salesProduct.ts b/src/api/channel/salesProduct.ts index 14da776..09d9ebe 100644 --- a/src/api/channel/salesProduct.ts +++ b/src/api/channel/salesProduct.ts @@ -62,6 +62,11 @@ export async function updateSalesProduct(body: Record & { id: numbe 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) { const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id }) assertBusinessOk(res.data) diff --git a/src/constants/pageButtonCatalog.ts b/src/constants/pageButtonCatalog.ts index 43a9f77..85b1607 100644 --- a/src/constants/pageButtonCatalog.ts +++ b/src/constants/pageButtonCatalog.ts @@ -656,6 +656,7 @@ const CORE_GROUPS: PageButtonGroup[] = [ { code: 'AddProduct', name: '新增产品' }, { code: 'Create', name: '新增' }, { code: 'Update', name: '修改' }, + { code: 'UpdateCode', name: '修改产品代码' }, { code: 'Delete', name: '删除' }, { code: 'EditPrice', name: '编辑' }, { code: 'Save', name: '保存' }, diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 757deb5..84c1d66 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -2368,7 +2368,7 @@ export default { 'order.priceCheck.form.destinationCountry.placeholder': 'Please select', 'order.priceCheck.form.shippingMethod': 'Shipping method', '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.cargoWeight': 'Cargo weight', '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.tip': 'more accurate', '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.clearFilters': 'Clear', 'order.priceCheck.form.removeFilter': 'Remove filter', @@ -2465,9 +2465,9 @@ export default { 'order.priceCheck.table.viewDetail': 'Details', 'order.priceCheck.table.viewGuide': 'Channel guide', 'order.priceCheck.table.viewGuideHint': 'Click to view route guide', - 'order.priceCheck.table.customsOrderNow': 'Place order', - 'order.priceCheck.table.orderNow': 'Place order', - 'order.priceCheck.table.askAi': 'Ask AI', + 'order.priceCheck.table.customsOrderNow': 'Go to order', + 'order.priceCheck.table.orderNow': 'Go to order', + 'order.priceCheck.table.askAi': 'Ask advisor', 'order.priceCheck.table.askAiCanShip': 'Can this ship?', 'order.priceCheck.table.askAiCheaper': 'Anything cheaper?', 'order.priceCheck.table.empty': 'No matching quotes', @@ -5171,7 +5171,13 @@ export default { 'channel.salesProducts.placeholder.productDescription': 'Customer-facing description (optional)', 'channel.salesProducts.placeholder.preStorageScript': 'Script before order persistence (optional)', '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': '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', @@ -5558,7 +5564,7 @@ export default { 'channel.area.message.deleteFailed': 'Delete failed. Please try again later.', 'channel.area.import.modalTitle': 'Import area details', '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.submit': 'Import', 'channel.area.import.downloadSuccess': 'Template downloaded', @@ -5569,7 +5575,7 @@ export default { 'channel.area.import.failed': 'Import failed', 'channel.area.import.partialSuccess': 'Some rows failed. See details below.', '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.export.success': 'Exported {count} row(s)', 'channel.area.export.failed': 'Export failed', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 184de88..e9b7537 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -2254,7 +2254,7 @@ export default { 'order.priceCheck.form.destinationCountry.placeholder': '请选择', 'order.priceCheck.form.shippingMethod': '运输方式', 'order.priceCheck.form.shippingMethod.placeholder': '请选择', - 'order.priceCheck.form.channel': '渠道', + 'order.priceCheck.form.channel': '线路', 'order.priceCheck.form.channel.placeholder': '全部', 'order.priceCheck.form.cargoWeight': '货物重量', 'order.priceCheck.form.cargoWeight.placeholder': '请输入重量(KG)', @@ -2275,7 +2275,7 @@ export default { 'order.priceCheck.form.postalCode': '邮编', 'order.priceCheck.form.postalCode.tip': '更准', 'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确', - 'order.priceCheck.form.moreFilters': '更多条件', + 'order.priceCheck.form.moreFilters': '更多', 'order.priceCheck.form.activeFilters': '已选 {count} 项', 'order.priceCheck.form.clearFilters': '清空', 'order.priceCheck.form.removeFilter': '移除条件', @@ -2351,9 +2351,9 @@ export default { 'order.priceCheck.table.viewDetail': '详情', 'order.priceCheck.table.viewGuide': '渠道说明', 'order.priceCheck.table.viewGuideHint': '点击查看线路说明', - 'order.priceCheck.table.customsOrderNow': '立刻下单', - 'order.priceCheck.table.orderNow': '立刻下单', - 'order.priceCheck.table.askAi': '问 AI', + 'order.priceCheck.table.customsOrderNow': '去下单', + 'order.priceCheck.table.orderNow': '去下单', + 'order.priceCheck.table.askAi': '问顾问', 'order.priceCheck.table.askAiCanShip': '这个能不能发?', 'order.priceCheck.table.askAiCheaper': '有没有更便宜?', 'order.priceCheck.table.empty': '暂无匹配报价', @@ -5206,7 +5206,13 @@ export default { 'channel.salesProducts.placeholder.productDescription': '面向客户的产品说明(选填)', 'channel.salesProducts.placeholder.preStorageScript': '订单落库前执行的脚本(选填)', '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': '命名规则:地区/国家 + 产品类型描述 + 其他。\n示例:美国空运小包-5日达', 'channel.salesProducts.tip.group': '用于官网/后台分组展示,如专线小包', @@ -5569,7 +5575,7 @@ export default { 'channel.area.search.button.query': '查询', 'channel.area.search.button.reset': '重置', 'channel.area.import.modalTitle': '导入分区明细', - 'channel.area.import.tip': '请先下载模板。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。', + 'channel.area.import.tip': '请先下载模板。导入为增量写入:与本分区已有规则完全相同的行会跳过,不会覆盖。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。', 'channel.area.import.downloadTemplate': '下载模板', 'channel.area.import.submit': '开始导入', 'channel.area.import.downloadSuccess': '模板下载成功', @@ -5580,7 +5586,7 @@ export default { 'channel.area.import.failed': '导入失败', 'channel.area.import.partialSuccess': '部分行导入失败,请查看失败明细', '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.export.success': '已导出 {count} 条', 'channel.area.export.failed': '导出失败', diff --git a/src/views/channel/components/KpiAreaImportModal.vue b/src/views/channel/components/KpiAreaImportModal.vue index c7308fe..4ac427d 100644 --- a/src/views/channel/components/KpiAreaImportModal.vue +++ b/src/views/channel/components/KpiAreaImportModal.vue @@ -34,6 +34,7 @@ {{ t('channel.area.import.resultSummary', { success: importResult.successCount, + skip: importResult.skipCount, fail: importResult.failCount, }) }} @@ -117,7 +118,7 @@ try { const result = await importKpiAreaInfo(areaId.value, selectedFile.value); importResult.value = result; - if (result.successCount > 0) { + if (result.successCount > 0 || result.skipCount > 0) { emit('success'); } if (result.failCount === 0) { @@ -125,7 +126,7 @@ visible.value = false; return true; } - if (result.successCount > 0) { + if (result.successCount > 0 || result.skipCount > 0) { Message.warning(t('channel.area.import.partialSuccess')); } else { Message.error(t('channel.area.import.allFailed')); diff --git a/src/views/channel/salesProducts/index.vue b/src/views/channel/salesProducts/index.vue index da231c7..910b81a 100644 --- a/src/views/channel/salesProducts/index.vue +++ b/src/views/channel/salesProducts/index.vue @@ -18,6 +18,7 @@ saveSalesProductCustomerGuide, toggleSalesProductPriceStatus, updateSalesProduct, + updateSalesProductCode, updateSalesProductPrice, } from '@/api/channel/salesProduct'; import { getLabelTemplateList } from '@/api/channel/labelTemplate'; @@ -154,6 +155,9 @@ const customerGuideSaving = ref(false); const customerGuidePanelRef = ref | null>(null); const savingConfig = ref(false); + const productCodeModalVisible = ref(false); + const productCodeDraft = ref(''); + const savingProductCode = ref(false); function parseAdditionalServiceCodes(raw: unknown): string[] { if (Array.isArray(raw)) { @@ -252,6 +256,42 @@ .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 pid = selectedProduct.value; if (pid == null) return null; @@ -4014,10 +4054,18 @@ + @@ -5487,6 +5535,25 @@ + +

{{ t('channel.salesProducts.modal.editProductCodeHint') }}

+ +
+ @@ -5691,6 +5758,31 @@ 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-select, .form-textarea { diff --git a/src/views/order/priceCheck/components/QuoteResultCard.vue b/src/views/order/priceCheck/components/QuoteResultCard.vue index 31db020..f2637b9 100644 --- a/src/views/order/priceCheck/components/QuoteResultCard.vue +++ b/src/views/order/priceCheck/components/QuoteResultCard.vue @@ -1,7 +1,7 @@