diff --git a/src/api/order/index.ts b/src/api/order/index.ts index 390bacf..f71469f 100644 --- a/src/api/order/index.ts +++ b/src/api/order/index.ts @@ -1267,6 +1267,7 @@ export interface PriceCheckRequest { /** 仓库主键(多选;为空则遍历全部仓库) */ warehouseIds?: number[] destinationProvince?: string + destinationCity?: string postalCode?: string /** 选用的优惠券实例 Id */ couponInstanceId?: number diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index dadcde7..2cf0d2c 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -312,6 +312,8 @@ export default { 'warehouse.parcel.drawer.frozenQty': 'Frozen', 'warehouse.parcel.drawer.itemCount': '{n} line(s)', 'warehouse.parcel.tip': 'Available = ready to bind; Frozen = reserved by orders. Use status tabs to filter.', + 'warehouse.parcel.pluginHint': 'Don’t want to fill parcel details? Use the one-click add-parcel plugin.', + 'warehouse.parcel.pluginHintDownload': 'Download it from the top-right icon', 'warehouse.parcel.drawer.edit': 'Edit parcel', 'warehouse.parcel.drawer.empty': 'No items', 'warehouse.parcel.table.arrivalTime': 'Arrival Time', @@ -1439,43 +1441,49 @@ export default { 'nanaAssistant.tabAi': 'AI Nana', 'nanaAssistant.tabAdvisor': 'Exclusive Advisor', 'nanaAssistant.badge': 'Logistics Advisor', - 'nanaAssistant.welcomeTitle': 'What do you need help shipping?', - 'nanaAssistant.welcomeSub': 'Price, cargo eligibility, tracking — pick a capability below.', + 'nanaAssistant.welcomeTitle': 'What do you want to check?', + 'nanaAssistant.welcomeHello': '{greet}, Xiao Ai at your service', + 'nanaAssistant.welcomeSub': 'What do you want to check? Tap below, or just type.', + 'nanaAssistant.greet.morning': 'Good morning', + 'nanaAssistant.greet.noon': 'Good afternoon', + 'nanaAssistant.greet.afternoon': 'Good afternoon', + 'nanaAssistant.greet.evening': 'Good evening', + 'nanaAssistant.greet.night': 'Still up', 'nanaAssistant.quickLabel': 'Common questions', - 'nanaAssistant.welcomeMsg': "Hi — I'm your IDE logistics advisor. Pick a question below or type your own.", + 'nanaAssistant.welcomeMsg': "Hi — I'm your IDE logistics advisor. Tap below or type your own question.", 'nanaAssistant.historyTitle': 'Chat history', 'nanaAssistant.newChat': 'New chat', 'nanaAssistant.emptyHistory': 'No conversations yet', 'nanaAssistant.searchHistory': 'Search chats or waybill no.', 'nanaAssistant.noSearchResults': 'No matching conversations', 'nanaAssistant.deleteThread': 'Delete conversation', - 'nanaAssistant.hint': 'Type a question, or pick a capability above.', - 'nanaAssistant.hintLive': 'Smart advisor is live — price, channel guide, tracking.', + 'nanaAssistant.hint': 'Type a question, or tap an option above.', + 'nanaAssistant.hintLive': 'Ask about rates, whether it can ship, or where a parcel is.', 'nanaAssistant.inputPlaceholder': 'Ask a question…', 'nanaAssistant.inputPlaceholderLive': 'Ask about price, channels, or tracking…', 'nanaAssistant.inputPlaceholderTrack': 'Enter waybill / tracking no.…', - 'nanaAssistant.mcp.sectionLabel': 'Capabilities', + 'nanaAssistant.mcp.exampleHint': 'e.g. {sample}', 'nanaAssistant.mcp.required': 'Required', 'nanaAssistant.mcp.optional': 'Optional', 'nanaAssistant.mcp.missingAsk': 'Please also provide: {fields}. Then I can look it up.', - 'nanaAssistant.mcp.priceTitle': 'Price check', - 'nanaAssistant.mcp.priceDesc': 'Estimate freight by destCountry & weight', + 'nanaAssistant.mcp.priceTitle': 'Check rates', + 'nanaAssistant.mcp.priceDesc': 'Estimate freight by country and weight', 'nanaAssistant.mcp.priceCoach': - 'To quote I need:\n1) Destination destCountry (e.g. US)\n2) Actual weight in kg\n\nOptional: postal code, L×W×H (cm), cargo type.\nExample: US 0.5kg zip 10001', - 'nanaAssistant.mcp.priceExample': 'e.g. US 0.5kg zip 10001', + 'To quote I need destination country and actual weight (kg). Postal code and size help accuracy.\nExample: US 0.5kg', + 'nanaAssistant.mcp.priceExample': 'Check US 0.5kg rates for me', 'nanaAssistant.mcp.priceDraft': 'Country:\nWeight (kg):\nPostal code (optional):\nDims LWH cm (optional):', 'nanaAssistant.mcp.guideTitle': 'Can this cargo ship?', - 'nanaAssistant.mcp.guideDesc': 'Check against the channel customer guide', + 'nanaAssistant.mcp.guideDesc': 'See if this item can go on this service', 'nanaAssistant.mcp.guideCoach': - 'I need:\n1) Sales product code or name\n2) Cargo / product name\n\nExample: product EX_ASP_HKDHL, cargo lipstick — can it ship?', - 'nanaAssistant.mcp.guideExample': 'e.g. product XX, cargo lipstick OK?', + 'I need the sales product (service) and the cargo name.\nExample: product XX, cargo lipstick — can it ship?', + 'nanaAssistant.mcp.guideExample': 'Can lipstick ship?', 'nanaAssistant.mcp.guideDraft': 'Sales product code/name:\nCargo name:', - 'nanaAssistant.mcp.trackTitle': 'Track shipment', - 'nanaAssistant.mcp.trackDesc': 'Timeline by waybill number', + 'nanaAssistant.mcp.trackTitle': 'Where is my parcel?', + 'nanaAssistant.mcp.trackDesc': 'See where the parcel is now', 'nanaAssistant.mcp.trackCoach': - 'Please provide a waybill / tracking / customer order number.\nIf a waybill is already loaded, tap “Track”.', - 'nanaAssistant.mcp.trackExample': 'e.g. Track waybill NFXXXX', + 'Send a waybill number. If one is already loaded, tap “Track this shipment”.', + 'nanaAssistant.mcp.trackExample': 'Where is waybill NFXXXX', 'nanaAssistant.mcp.trackDraft': 'Waybill No.:', 'nanaAssistant.mcp.field.destCountry': 'Destination destCountry', 'nanaAssistant.mcp.field.weight': 'Weight (kg)', @@ -1528,6 +1536,10 @@ export default { 'nanaAssistant.viewWaybill': 'View waybill →', 'nanaAssistant.queryTrack': 'Track shipment', 'nanaAssistant.endWaybillConsult': 'End waybill consult', + 'nanaAssistant.waybillWelcomeTitle': 'How can I help with this shipment?', + 'nanaAssistant.waybillWelcomeSub': 'Waybill {no} is loaded — track it or ask about fees.', + 'nanaAssistant.waybillAskTrack': 'Track this shipment', + 'nanaAssistant.waybillAskFee': 'Ask about fees', 'nanaAssistant.waybillAutoAsk': 'Please track waybill {no} for me', 'nanaAssistant.trackWaybillDemo': 'Got your request for waybill {no}. When the smart service is configured, we prefer the live API; this is a reference reply: milestones usually include pickup, export, line-haul, customs, and delivery. Your advisor will follow up on exceptions.', 'nanaAssistant.copyWechat': 'Copy WeChat ID', @@ -2255,7 +2267,9 @@ export default { 'order.priceCheck.form.warehouse': 'Warehouse', 'order.priceCheck.form.warehouse.placeholder': 'All', 'order.priceCheck.form.destinationProvince': 'State / Province', - 'order.priceCheck.form.destinationProvince.placeholder': 'Please select', + 'order.priceCheck.form.destinationProvince.placeholder': 'Used when postal code is empty', + 'order.priceCheck.form.destinationCity': 'City', + 'order.priceCheck.form.destinationCity.placeholder': 'Used when postal code is empty', 'order.priceCheck.form.postalCode': 'Postal code', 'order.priceCheck.form.postalCode.tip': 'more accurate', 'order.priceCheck.form.postalCode.placeholder': 'Optional — improves zoning', @@ -2568,7 +2582,7 @@ export default { 'order.placeOrder.single.exportQualificationManage': 'Manage', 'order.placeOrder.single.exportQualificationLoadFailed': 'Failed to load export qualifications', 'order.placeOrder.single.platformCode': 'E-commerce platform', - 'order.placeOrder.single.platformCodePlaceholder': 'e.g. Amazon / eBay / Shopify', + 'order.placeOrder.single.platformCodePlaceholder': 'Select ecommerce platform', 'order.placeOrder.single.showExtraCols': 'More fields', 'order.placeOrder.single.hideExtraCols': 'Less', 'order.placeOrder.single.showRecipientOptional': 'More fields', @@ -5330,6 +5344,15 @@ export default { 'channel.area.form.city': 'City', 'channel.area.form.leftPostCode': 'Start postcode', 'channel.area.form.rightPostCode': 'End postcode', + 'channel.area.form.matchDim': 'Match dimension', + 'channel.area.form.matchMode': 'Match mode', + 'channel.area.matchDim.country': 'Country only', + 'channel.area.matchDim.zip': 'Postcode', + 'channel.area.matchDim.state': 'State/Province', + 'channel.area.matchDim.city': 'City', + 'channel.area.matchMode.prefix': 'Prefix', + 'channel.area.matchMode.exact': 'Exact', + 'channel.area.matchMode.range': 'Range', 'channel.area.form.placeholder.keyword': 'Enter keyword', 'channel.area.form.placeholder.areaName': 'Enter area name', 'channel.area.form.placeholder.input': 'Please enter', @@ -5337,17 +5360,28 @@ export default { 'channel.area.form.rules.countryRequired': 'Please select country/region', 'channel.area.form.rules.destCountryRequired': 'Please select country/region', 'channel.area.section.match': 'Match scope', - 'channel.area.section.postcode': 'Postcode range', - 'channel.area.tip.country': 'Required; country only matches all postcodes in that country', - 'channel.area.tip.province': 'Optional; blank means any state/province', - 'channel.area.tip.city': 'Optional; blank means any city', - 'channel.area.tip.leftPostCode': 'Pair with end postcode; if one side is filled the other is required', - 'channel.area.tip.rightPostCode': 'Must be greater than or equal to start postcode', + 'channel.area.section.postcode': 'Postcode match', + 'channel.area.tip.country': 'Required; Country-only is the lowest-priority fallback for that country', + 'channel.area.tip.province': 'Required for state or city dimension', + 'channel.area.tip.city': 'Required for city dimension; state is also required', + 'channel.area.tip.leftPostCode': 'Prefix: leading digits; Exact: full postcode; Range: start postcode', + 'channel.area.tip.rightPostCode': 'Range end postcode, closed interval (inclusive)', + 'channel.area.tip.matchDim': 'One row, one dimension. Postcode beats state/city; country-only is last fallback', + 'channel.area.tip.matchMode': 'Prefix matches leading chars; exact is equality; range is inclusive', 'channel.area.form.rules.maxLength128': 'Cannot exceed 128 characters', 'channel.area.form.rules.maxLength30': 'Cannot exceed 30 characters', 'channel.area.form.rules.noChinese': 'Cannot contain Chinese characters', 'channel.area.form.rules.noSpacesOnly': 'Start postcode cannot be spaces only', 'channel.area.form.rules.noSpacesOnlyEnd': 'End postcode cannot be spaces only', + 'channel.area.form.rules.matchDimRequired': 'Select a match dimension', + 'channel.area.form.rules.matchModeRequired': 'Select a match mode', + 'channel.area.form.rules.provinceRequired': 'Enter state/province', + 'channel.area.form.rules.cityRequired': 'Enter city', + 'channel.area.form.rules.cityRequiresState': 'City match requires state/province', + 'channel.area.form.rules.postcodeRequired': 'Enter postcode', + 'channel.area.form.rules.postcodeRangeRequired': 'Range match requires end postcode', + 'channel.area.form.placeholder.provinceRequired': 'Enter state/province', + 'channel.area.form.placeholder.cityRequired': 'Enter city', 'channel.area.modal.addArea': 'Add area', 'channel.area.modal.editArea': 'Edit area', 'channel.area.modal.addDetail': 'Add area detail', @@ -5376,12 +5410,12 @@ export default { 'channel.config.detailCount': '{count} detail record(s)', 'channel.area.tip.addArea': 'Areas group detail records. Use a clear name such as "US West Coast" or "EU Remote".', 'channel.area.tip.editArea': 'Renaming an area does not change existing details. Avoid duplicate names.', - 'channel.area.tip.addDetail': 'Country/region is required; state/province and city are optional. When using postcodes, start must not exceed end. Country only matches all postcodes in that destCountry.', - 'channel.area.tip.editDetail': 'Changes apply immediately to channel matching. Verify destCountry and postcode range.', + 'channel.area.tip.addDetail': 'One row, one dimension. Postcode (prefix/exact/closed range) wins; then state; then city (needs state); country-only is last fallback.', + 'channel.area.tip.editDetail': 'Changes apply immediately to quote/billing matching. Verify dimension and postcode rules.', 'channel.area.form.placeholder.provinceOptional': 'Optional; leave blank for any state/province', 'channel.area.form.placeholder.cityOptional': 'Optional; leave blank for any city', - 'channel.area.form.placeholder.leftPostCode': 'Optional; pair with end postcode', - 'channel.area.form.placeholder.rightPostCode': 'Optional; must be greater than or equal to start', + 'channel.area.form.placeholder.leftPostCode': 'Prefix, exact postcode, or range start', + 'channel.area.form.placeholder.rightPostCode': 'Range end (inclusive)', 'channel.area.form.rules.maxLength50': 'Cannot exceed 50 characters', 'channel.area.form.rules.postcodeRangeInvalid': 'Start postcode cannot be greater than end postcode', 'channel.area.modal.deleteConfirmWithName': 'Delete area "{name}"? All details under it will be removed.', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 943eae2..b09f722 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -313,6 +313,8 @@ export default { 'warehouse.parcel.drawer.frozenQty': '冻结', 'warehouse.parcel.drawer.itemCount': '共 {n} 项', 'warehouse.parcel.tip': '可用=可绑定发货;冻结=已被订单占用。点状态 Tab 快速筛选在途/到仓/已上架。', + 'warehouse.parcel.pluginHint': '懒得填写包裹信息?那就使用一键添加包裹插件吧。', + 'warehouse.parcel.pluginHintDownload': '去右上角下载插件', 'warehouse.parcel.drawer.edit': '编辑包裹', 'warehouse.parcel.drawer.empty': '暂无商品明细', 'warehouse.parcel.table.arrivalTime': '到仓时间', @@ -1222,42 +1224,48 @@ export default { 'nanaAssistant.tabAi': 'AI Nana', 'nanaAssistant.tabAdvisor': '专属顾问', 'nanaAssistant.badge': '物流顾问', - 'nanaAssistant.welcomeTitle': '这次发货,需要什么帮助?', - 'nanaAssistant.welcomeSub': '查价格、品名能否发、查轨迹 — 点选下方能力后补充信息即可。', + 'nanaAssistant.welcomeTitle': '今天想查点什么?', + 'nanaAssistant.welcomeHello': '{greet},小艾为您服务', + 'nanaAssistant.welcomeSub': '今天想查点什么?点下面,或自己打字问我。', + 'nanaAssistant.greet.morning': '早上好', + 'nanaAssistant.greet.noon': '中午好', + 'nanaAssistant.greet.afternoon': '下午好', + 'nanaAssistant.greet.evening': '晚上好', + 'nanaAssistant.greet.night': '夜深了', 'nanaAssistant.quickLabel': '常见问题', - 'nanaAssistant.welcomeMsg': '你好,我是 IDE 物流顾问。可以点选下方问题,或直接输入。', + 'nanaAssistant.welcomeMsg': '你好,我是 IDE 物流顾问。可以直接问,也可以点下面的入口。', 'nanaAssistant.historyTitle': '历史记录', 'nanaAssistant.newChat': '新会话', 'nanaAssistant.emptyHistory': '暂无会话', 'nanaAssistant.searchHistory': '搜索会话或运单号', 'nanaAssistant.noSearchResults': '未找到匹配的会话', 'nanaAssistant.deleteThread': '删除会话', - 'nanaAssistant.hint': '直接提问,或点选上方能力。', - 'nanaAssistant.hintLive': '已接入智能顾问,可查价格、对客说明、轨迹。', + 'nanaAssistant.hint': '直接提问,或点上面的入口。', + 'nanaAssistant.hintLive': '可以问运费、能不能寄、包裹到哪了。', 'nanaAssistant.inputPlaceholder': '输入您的问题…', 'nanaAssistant.inputPlaceholderLive': '', 'nanaAssistant.inputPlaceholderTrack': '', - 'nanaAssistant.mcp.sectionLabel': '可查能力', + 'nanaAssistant.mcp.exampleHint': '例:{sample}', 'nanaAssistant.mcp.required': '必填', 'nanaAssistant.mcp.optional': '选填', 'nanaAssistant.mcp.missingAsk': '还需要补充:{fields}。补全后我再帮你查。', - 'nanaAssistant.mcp.priceTitle': '查价格', - 'nanaAssistant.mcp.priceDesc': '按目的国与重量试算运费', + 'nanaAssistant.mcp.priceTitle': '查运费', + 'nanaAssistant.mcp.priceDesc': '按目的国和重量估算运费', 'nanaAssistant.mcp.priceCoach': - '查价格需要你提供:\n① 目的国家(如美国 / US)\n② 实重 kg\n\n选填:邮编、长宽高(cm)、货物属性(普货/敏感)。\n补全后发送即可,例如:美国 0.5kg 邮编10001', - 'nanaAssistant.mcp.priceExample': '例:美国 0.5kg 邮编10001', + '查运费需要:目的国家和实重(kg)。有邮编、尺寸会更准。\n补全后发送即可,例如:美国 0.5kg', + 'nanaAssistant.mcp.priceExample': '帮我查下美国0.5kg价格', 'nanaAssistant.mcp.priceDraft': '目的国:\n实重(kg):\n邮编(选填):\n尺寸长宽高cm(选填):', - 'nanaAssistant.mcp.guideTitle': '品名能否发', - 'nanaAssistant.mcp.guideDesc': '对照渠道对客说明判断可否承运', + 'nanaAssistant.mcp.guideTitle': '这个货能不能寄', + 'nanaAssistant.mcp.guideDesc': '看这个品名能不能走这条渠道', 'nanaAssistant.mcp.guideCoach': - '判断品名能否承运需要:\n① 销售产品代码或名称(运输方式)\n② 客户品名(英文/中文均可)\n\n例如:产品 EX_ASP_HKDHL,品名 lipstick,能不能发?', - 'nanaAssistant.mcp.guideExample': '例:产品XX,品名 lipstick 能发吗', + '判断能不能寄需要:销售产品(运输方式)和品名。\n例如:产品 XX,品名 lipstick,能发吗?', + 'nanaAssistant.mcp.guideExample': 'lipstick能寄吗', 'nanaAssistant.mcp.guideDraft': '销售产品代码/名称:\n客户品名:', - 'nanaAssistant.mcp.trackTitle': '查轨迹', - 'nanaAssistant.mcp.trackDesc': '按运单号查看物流节点', + 'nanaAssistant.mcp.trackTitle': '包裹到哪了', + 'nanaAssistant.mcp.trackDesc': '看包裹现在到哪了', 'nanaAssistant.mcp.trackCoach': - '查轨迹请提供运单号 / 跟踪号 / 客户单号(任选其一)。\n若已从订单带入运单,可直接点「查轨迹」。', - 'nanaAssistant.mcp.trackExample': '例:请查运单 NFXXXX 的轨迹', + '查包裹位置请发运单号。如果上面已经带入运单,直接点「查这票轨迹」即可。', + 'nanaAssistant.mcp.trackExample': '帮我查下运单NFXXXX到哪了', 'nanaAssistant.mcp.trackDraft': '运单号:', 'nanaAssistant.mcp.field.destCountry': '目的国家', 'nanaAssistant.mcp.field.weight': '实重(kg)', @@ -1310,6 +1318,10 @@ export default { 'nanaAssistant.viewWaybill': '查看运单 →', 'nanaAssistant.queryTrack': '查轨迹', 'nanaAssistant.endWaybillConsult': '结束运单咨询', + 'nanaAssistant.waybillWelcomeTitle': '这票需要什么帮助?', + 'nanaAssistant.waybillWelcomeSub': '已带入运单 {no},可以直接查轨迹或问费用。', + 'nanaAssistant.waybillAskTrack': '查这票轨迹', + 'nanaAssistant.waybillAskFee': '问这票费用', 'nanaAssistant.waybillAutoAsk': '请帮我查询运单 {no} 的物流轨迹', 'nanaAssistant.trackWaybillDemo': '已收到运单 {no} 的查询。若已配置智能服务,将优先走真实接口;当前为参考回复:一般可在揽收、出口、干线、清关、妥投等节点看到更新。如有异常,专属顾问会主动跟进。', 'nanaAssistant.copyWechat': '复制微信号', @@ -2141,7 +2153,9 @@ export default { 'order.priceCheck.form.warehouse': '仓库', 'order.priceCheck.form.warehouse.placeholder': '全部', 'order.priceCheck.form.destinationProvince': '目的州省', - 'order.priceCheck.form.destinationProvince.placeholder': '请选择', + 'order.priceCheck.form.destinationProvince.placeholder': '无邮编时按州匹配', + 'order.priceCheck.form.destinationCity': '目的城市', + 'order.priceCheck.form.destinationCity.placeholder': '无邮编时按城市匹配', 'order.priceCheck.form.postalCode': '邮编', 'order.priceCheck.form.postalCode.tip': '更准', 'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确', @@ -2451,7 +2465,7 @@ export default { 'order.placeOrder.single.exportQualificationManage': '管理备案', 'order.placeOrder.single.exportQualificationLoadFailed': '加载出口资质备案失败', 'order.placeOrder.single.platformCode': '电商平台', - 'order.placeOrder.single.platformCodePlaceholder': '如 Amazon / eBay / Shopify', + 'order.placeOrder.single.platformCodePlaceholder': '请选择电商平台', 'order.placeOrder.single.showExtraCols': '更多字段', 'order.placeOrder.single.hideExtraCols': '收起', 'order.placeOrder.single.declarationPickSku': '从 SKU 选择', @@ -5367,6 +5381,15 @@ export default { 'channel.area.form.city': '城市', 'channel.area.form.leftPostCode': '起始邮编', 'channel.area.form.rightPostCode': '截止邮编', + 'channel.area.form.matchDim': '匹配维度', + 'channel.area.form.matchMode': '匹配方式', + 'channel.area.matchDim.country': '仅国家', + 'channel.area.matchDim.zip': '邮编', + 'channel.area.matchDim.state': '州/省', + 'channel.area.matchDim.city': '城市', + 'channel.area.matchMode.prefix': '前缀', + 'channel.area.matchMode.exact': '精确', + 'channel.area.matchMode.range': '区间', 'channel.area.form.placeholder.keyword': '请输入关键字', 'channel.area.form.placeholder.areaName': '请输入分区名称', 'channel.area.form.placeholder.input': '请输入', @@ -5374,17 +5397,28 @@ export default { 'channel.area.form.rules.countryRequired': '请选择国家/地区', 'channel.area.form.rules.destCountryRequired': '请选择国家/地区', 'channel.area.section.match': '匹配范围', - 'channel.area.section.postcode': '邮编区间', - 'channel.area.tip.country': '必填;仅填国家表示匹配该国全部邮编', - 'channel.area.tip.province': '选填,留空表示不限州/省', - 'channel.area.tip.city': '选填,留空表示不限城市', - 'channel.area.tip.leftPostCode': '与截止邮编成对填写;可只填一侧时另一侧必填', - 'channel.area.tip.rightPostCode': '须大于等于起始邮编', + 'channel.area.section.postcode': '邮编匹配', + 'channel.area.tip.country': '必填;选择「仅国家」表示该国最低优先级兜底', + 'channel.area.tip.province': '州/省或城市维度时必填', + 'channel.area.tip.city': '城市维度时必填,且必须同时填写州/省', + 'channel.area.tip.leftPostCode': '前缀填邮编前几位;精确填完整邮编;区间填起始邮编', + 'channel.area.tip.rightPostCode': '区间截止邮编,闭区间(含起止)', + 'channel.area.tip.matchDim': '一行只表达一个维度:邮编优先于州/省、城市;仅国家为最低兜底', + 'channel.area.tip.matchMode': '前缀按左起位数匹配;精确全等;区间为闭区间', 'channel.area.form.rules.maxLength128': '限制不可超过128个字符', 'channel.area.form.rules.maxLength30': '限制不可超过30个字符', 'channel.area.form.rules.noChinese': '不能存在中文字符', 'channel.area.form.rules.noSpacesOnly': '起始邮编不可只输入空格', 'channel.area.form.rules.noSpacesOnlyEnd': '截止邮编不可只输入空格', + 'channel.area.form.rules.matchDimRequired': '请选择匹配维度', + 'channel.area.form.rules.matchModeRequired': '请选择匹配方式', + 'channel.area.form.rules.provinceRequired': '请填写州/省', + 'channel.area.form.rules.cityRequired': '请填写城市', + 'channel.area.form.rules.cityRequiresState': '城市匹配必须同时填写州/省', + 'channel.area.form.rules.postcodeRequired': '请填写邮编', + 'channel.area.form.rules.postcodeRangeRequired': '区间匹配请填写截止邮编', + 'channel.area.form.placeholder.provinceRequired': '请输入州/省', + 'channel.area.form.placeholder.cityRequired': '请输入城市', 'channel.area.modal.addArea': '新增分区', 'channel.area.modal.editArea': '修改分区', 'channel.area.modal.addDetail': '新增分区明细', @@ -5405,7 +5439,7 @@ export default { 'channel.area.search.button.query': '查询', 'channel.area.search.button.reset': '重置', 'channel.area.import.modalTitle': '导入分区明细', - 'channel.area.import.tip': '请先下载模板,按列填写国家/邮编等信息后上传 Excel(.xlsx/.xls)。国家代码必填;仅填国家代码时将自动匹配国家名称。', + 'channel.area.import.tip': '请先下载模板。国家代码必填。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。', 'channel.area.import.downloadTemplate': '下载模板', 'channel.area.import.submit': '开始导入', 'channel.area.import.downloadSuccess': '模板下载成功', @@ -5428,12 +5462,12 @@ export default { 'channel.config.detailCount': '共 {count} 条明细', 'channel.area.tip.addArea': '分区用于归类管理明细。名称建议简洁明确,例如「美国西海岸」或「欧盟偏远」。', 'channel.area.tip.editArea': '修改分区名称不会影响已有明细,但请避免与现有分区重名造成混淆。', - 'channel.area.tip.addDetail': '国家/地区必填;州/省、城市可选。填写邮编区间时,起始邮编不能大于截止邮编;仅填国家表示匹配该国全部邮编。', - 'channel.area.tip.editDetail': '修改后将立即影响渠道匹配逻辑,请确认国家与邮编区间准确无误。', + 'channel.area.tip.addDetail': '一行只选一个维度。有邮编时只按邮编匹配(前缀/精确/闭区间);无邮编才按州、再按城市;仅国家为最低兜底。', + 'channel.area.tip.editDetail': '修改后立即影响查价/计费匹配,请确认维度与邮编规则。', 'channel.area.form.placeholder.provinceOptional': '选填,留空表示不限州/省', 'channel.area.form.placeholder.cityOptional': '选填,留空表示不限城市', - 'channel.area.form.placeholder.leftPostCode': '选填,与截止邮编成对填写', - 'channel.area.form.placeholder.rightPostCode': '选填,需大于等于起始邮编', + 'channel.area.form.placeholder.leftPostCode': '前缀/精确邮编,或区间起始', + 'channel.area.form.placeholder.rightPostCode': '区间截止(含)', 'channel.area.form.rules.maxLength50': '限制不可超过50个字符', 'channel.area.form.rules.postcodeRangeInvalid': '起始邮编不能大于截止邮编', 'channel.area.modal.deleteConfirmWithName': '确定删除分区「{name}」吗?其下明细将一并删除。', diff --git a/src/views/channel/composables/useKpiAreaPage.ts b/src/views/channel/composables/useKpiAreaPage.ts index 5baf412..05466b4 100644 --- a/src/views/channel/composables/useKpiAreaPage.ts +++ b/src/views/channel/composables/useKpiAreaPage.ts @@ -43,6 +43,74 @@ function trimStr(v: unknown): string { return String(v ?? '').trim(); } +function inferGeoMatch(data: Record) { + const dim = String(data.matchDim || '').trim(); + const mode = String(data.matchMode || '').trim(); + if (dim) { + return { + matchDim: dim, + matchMode: mode || (dim === 'zip' ? 'range' : dim === 'country' ? 'all' : 'exact'), + }; + } + const left = trimStr(data.leftPostCode || data.leftPostcode); + const right = trimStr(data.rightPostCode || data.rightPostcode); + if (left || right) { + let matchMode = 'range'; + if (left && !right) matchMode = 'prefix'; + else if (left && right && left === right) matchMode = 'exact'; + return { matchDim: 'zip', matchMode }; + } + if (trimStr(data.city)) return { matchDim: 'city', matchMode: 'exact' }; + if (trimStr(data.province)) return { matchDim: 'state', matchMode: 'exact' }; + return { matchDim: 'country', matchMode: 'all' }; +} + +function applyMatchDimension(data: Record) { + const rest = normalizePostcodeFields({ ...data }); + const { matchDim, matchMode } = inferGeoMatch(rest); + rest.matchDim = matchDim; + rest.matchMode = matchMode; + + if (matchDim === 'country') { + rest.province = undefined; + rest.city = undefined; + rest.leftPostCode = undefined; + rest.rightPostCode = undefined; + rest.leftPostcode = undefined; + rest.rightPostcode = undefined; + } else if (matchDim === 'state') { + rest.city = undefined; + rest.leftPostCode = undefined; + rest.rightPostCode = undefined; + rest.leftPostcode = undefined; + rest.rightPostcode = undefined; + } else if (matchDim === 'city') { + rest.leftPostCode = undefined; + rest.rightPostCode = undefined; + rest.leftPostcode = undefined; + rest.rightPostcode = undefined; + } else if (matchDim === 'zip') { + rest.province = undefined; + rest.city = undefined; + const left = trimStr(rest.leftPostCode); + if (matchMode === 'prefix') { + rest.rightPostCode = undefined; + rest.rightPostcode = undefined; + rest.leftPostCode = left || undefined; + rest.leftPostcode = left || undefined; + } else if (matchMode === 'exact') { + rest.leftPostCode = left || undefined; + rest.leftPostcode = left || undefined; + rest.rightPostCode = left || undefined; + rest.rightPostcode = left || undefined; + } else { + rest.leftPostcode = rest.leftPostCode; + rest.rightPostcode = rest.rightPostCode; + } + } + return rest; +} + function normalizePostcodeFields(data: Record) { const leftPostCode = trimStr(data.leftPostCode); const rightPostCode = trimStr(data.rightPostCode); @@ -91,20 +159,48 @@ export function useKpiAreaPage(areaType: KpiAreaType) { } function beforeSubmitRightAreaInfo(data: Record) { - const rest = normalizePostcodeFields({ ...data }); + const rest = applyMatchDimension({ ...data }); delete rest._countryQuickTags; + delete rest.sectionMatch; + delete rest.sectionPostcode; - const left = rest.leftPostCode; - const right = rest.rightPostCode; - if (isPostcodeRangeInvalid(left, right)) { - $baseMessage(t('channel.area.form.rules.postcodeRangeInvalid'), 'warning'); - throw new Error('postcode-range-invalid'); + const { matchDim, matchMode } = inferGeoMatch(rest); + if (matchDim === 'state' && !trimStr(rest.province)) { + $baseMessage(t('channel.area.form.rules.provinceRequired'), 'warning'); + throw new Error('province-required'); + } + if (matchDim === 'city') { + if (!trimStr(rest.province) || !trimStr(rest.city)) { + $baseMessage(t('channel.area.form.rules.cityRequiresState'), 'warning'); + throw new Error('city-requires-state'); + } + } + if (matchDim === 'zip') { + const left = trimStr(rest.leftPostCode); + const right = trimStr(rest.rightPostCode); + if (!left) { + $baseMessage(t('channel.area.form.rules.postcodeRequired'), 'warning'); + throw new Error('postcode-required'); + } + if (matchMode === 'range') { + if (!right) { + $baseMessage(t('channel.area.form.rules.postcodeRangeRequired'), 'warning'); + throw new Error('postcode-range-required'); + } + if (isPostcodeRangeInvalid(left, right)) { + $baseMessage(t('channel.area.form.rules.postcodeRangeInvalid'), 'warning'); + throw new Error('postcode-range-invalid'); + } + } } const code = rest?.countryCode; const hit = countryRows.value.find((r) => r.countryCode === code); return { ...rest, + matchDim, + matchMode, + prefixLen: matchDim === 'zip' && matchMode === 'prefix' ? trimStr(rest.leftPostCode).length : rest.prefixLen, countryName: hit?.chineseName ?? rest?.countryName ?? '', }; } @@ -219,17 +315,60 @@ export function useKpiAreaPage(areaType: KpiAreaType) { label: '', span: 24, }, + { + label: t('channel.area.form.matchDim'), + field: 'matchDim', + comp: 'a-select', + tooltip: t('channel.area.tip.matchDim'), + compProps: { + options: [ + { label: t('channel.area.matchDim.country'), value: 'country' }, + { label: t('channel.area.matchDim.zip'), value: 'zip' }, + { label: t('channel.area.matchDim.state'), value: 'state' }, + { label: t('channel.area.matchDim.city'), value: 'city' }, + ], + placeholder: t('common.selectPlaceholder'), + allowClear: false, + }, + rules: [{ required: true, message: t('channel.area.form.rules.matchDimRequired') }], + span: 12, + }, + { + label: t('channel.area.form.matchMode'), + field: 'matchMode', + comp: 'a-select', + tooltip: t('channel.area.tip.matchMode'), + ifShow: (model: Record) => model.matchDim === 'zip', + compProps: { + options: [ + { label: t('channel.area.matchMode.prefix'), value: 'prefix' }, + { label: t('channel.area.matchMode.exact'), value: 'exact' }, + { label: t('channel.area.matchMode.range'), value: 'range' }, + ], + placeholder: t('common.selectPlaceholder'), + allowClear: false, + }, + rules: [{ required: true, message: t('channel.area.form.rules.matchModeRequired') }], + span: 12, + }, { label: t('channel.area.form.province'), field: 'province', comp: 'a-input', tooltip: t('channel.area.tip.province'), + ifShow: (model: Record) => + model.matchDim === 'state' || model.matchDim === 'city', compProps: { - placeholder: t('channel.area.form.placeholder.provinceOptional'), + placeholder: t('channel.area.form.placeholder.provinceRequired'), clearable: true, allowClear: true, }, rules: [ + { + required: true, + message: t('channel.area.form.rules.provinceRequired'), + trigger: 'change', + }, { max: 128, message: t('channel.area.form.rules.maxLength128'), @@ -243,12 +382,18 @@ export function useKpiAreaPage(areaType: KpiAreaType) { field: 'city', comp: 'a-input', tooltip: t('channel.area.tip.city'), + ifShow: (model: Record) => model.matchDim === 'city', compProps: { - placeholder: t('channel.area.form.placeholder.cityOptional'), + placeholder: t('channel.area.form.placeholder.cityRequired'), clearable: true, allowClear: true, }, rules: [ + { + required: true, + message: t('channel.area.form.rules.cityRequired'), + trigger: 'change', + }, { max: 128, message: t('channel.area.form.rules.maxLength128'), @@ -264,12 +409,14 @@ export function useKpiAreaPage(areaType: KpiAreaType) { hideColon: true, comp: 'slot', span: 24, + ifShow: (model: Record) => model.matchDim === 'zip', }, { label: t('channel.area.form.leftPostCode'), field: 'leftPostCode', comp: 'a-input', tooltip: t('channel.area.tip.leftPostCode'), + ifShow: (model: Record) => model.matchDim === 'zip', compProps: { placeholder: t('channel.area.form.placeholder.leftPostCode'), clearable: true, @@ -277,21 +424,11 @@ export function useKpiAreaPage(areaType: KpiAreaType) { }, dynamicRules: ({ model }: { model: Record }) => [ { - required: !!model.rightPostCode, + required: model.matchDim === 'zip', pattern: /^((?![\u4E00-\u9FA5]).)*$/, message: t('channel.area.form.rules.noChinese'), trigger: 'change', }, - { - validator: (_rule: any, value: any, callback: any) => { - if (value && !String(value).trim()) { - callback(new Error(t('channel.area.form.rules.noSpacesOnly'))); - } else { - callback(); - } - }, - trigger: 'change', - }, { max: 30, message: t('channel.area.form.rules.maxLength30'), @@ -305,6 +442,8 @@ export function useKpiAreaPage(areaType: KpiAreaType) { field: 'rightPostCode', comp: 'a-input', tooltip: t('channel.area.tip.rightPostCode'), + ifShow: (model: Record) => + model.matchDim === 'zip' && model.matchMode === 'range', compProps: { placeholder: t('channel.area.form.placeholder.rightPostCode'), clearable: true, @@ -312,16 +451,15 @@ export function useKpiAreaPage(areaType: KpiAreaType) { }, dynamicRules: ({ model }: { model: Record }) => [ { - required: !!model.leftPostCode, + required: model.matchDim === 'zip' && model.matchMode === 'range', pattern: /^((?![\u4E00-\u9FA5]).)*$/, message: t('channel.area.form.rules.noChinese'), trigger: 'change', }, { validator: (_rule: any, value: any, callback: any) => { - if (value && !String(value).trim()) { - callback(new Error(t('channel.area.form.rules.noSpacesOnlyEnd'))); - } else if ( + if ( + model.matchMode === 'range' && model.leftPostCode && value && isPostcodeRangeInvalid(String(model.leftPostCode).trim(), String(value).trim()) @@ -415,6 +553,8 @@ export function useKpiAreaPage(areaType: KpiAreaType) { const hotColumns = computed(() => [ { key: 'countryName', label: t('channel.area.form.country'), width: 120 }, + { key: 'matchDim', label: t('channel.area.form.matchDim'), width: 100 }, + { key: 'matchMode', label: t('channel.area.form.matchMode'), width: 100 }, { key: 'province', label: t('channel.area.form.province'), width: 120 }, { key: 'city', label: t('channel.area.form.city'), width: 120 }, { key: 'leftPostcode', label: t('channel.area.table.leftPostcode'), width: 120 }, @@ -505,6 +645,11 @@ export function useKpiAreaPage(areaType: KpiAreaType) { (r) => r.chineseName === row.countryName || r.countryCode === row.countryName )?.countryCode || '', + ...inferGeoMatch({ + ...row, + leftPostCode: row.leftPostcode ?? row.leftPostCode, + rightPostCode: row.rightPostcode ?? row.rightPostCode, + }), }); return; } @@ -513,6 +658,8 @@ export function useKpiAreaPage(areaType: KpiAreaType) { title: titleObj[type], formData: { areaId: currentId.value?.id, + matchDim: 'zip', + matchMode: 'range', }, }); } diff --git a/src/views/h5/customer-center/price/index.vue b/src/views/h5/customer-center/price/index.vue index b18218b..682a4f0 100644 --- a/src/views/h5/customer-center/price/index.vue +++ b/src/views/h5/customer-center/price/index.vue @@ -23,7 +23,11 @@