feat: 官网海洋蓝波浪首页与指南分受众,并同步业务页优化 #9
@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus, IconDelete, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
|
||||
import { IconPlus, IconDelete, IconQuestionCircle, IconCopy } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
batchCreatePlatformStoreFulfill,
|
||||
batchCreatePlatformStoreFulfillWms,
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from '@/api/order'
|
||||
import { getSenderAddressList } from '@/api/order/senderAddress'
|
||||
import { getExportQualificationList } from '@/api/customer/exportQualification'
|
||||
import { getSalesProductList } from '@/api/channel/salesProduct'
|
||||
import { getDictListAll } from '@/api/system/dict'
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common'
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||
@ -23,6 +24,19 @@ import ExportCustoms9610GuideModal from '@/views/order/placeOrder/components/Exp
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute'
|
||||
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||
import {
|
||||
formatWarehouseShipAddress,
|
||||
formatWarehouseShipRecipient,
|
||||
} from '@/utils/warehouseShipAddress'
|
||||
|
||||
type WarehouseOption = {
|
||||
label: string
|
||||
value: number
|
||||
recipient?: string
|
||||
contactPhone?: string
|
||||
shippingAddress?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
import type { CreateLogisticsMode } from './constants'
|
||||
|
||||
@ -92,7 +106,7 @@ const props = defineProps<{
|
||||
mode: CreateLogisticsMode
|
||||
storeOrderIds: number[]
|
||||
productOptions: Array<{ label: string; value: string }>
|
||||
warehouseOptions: Array<{ label: string; value: number }>
|
||||
warehouseOptions: WarehouseOption[]
|
||||
defaultProductCode?: string
|
||||
defaultWarehouseId?: number
|
||||
customerId?: number
|
||||
@ -141,6 +155,7 @@ const platformCode = ref<string>()
|
||||
const inboundMethod = ref<'selfShip' | 'doorPickup'>('selfShip')
|
||||
const senderAddressId = ref<number | undefined>()
|
||||
const expectPickupTime = ref<string>()
|
||||
const carrierService = ref<string>()
|
||||
const agreementChecked = ref(false)
|
||||
const agreementModalVisible = ref(false)
|
||||
const exportCustomsGuideVisible = ref(false)
|
||||
@ -150,11 +165,70 @@ const exportCustomsModeOptions = ref<SelectOption[]>([])
|
||||
const ecommercePlatformOptions = ref<SelectOption[]>([])
|
||||
const exportQualificationOptions = ref<QualOption[]>([])
|
||||
const pickupAddressOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const domesticPickupProductOptions = ref<SelectOption[]>([])
|
||||
const pickupAddressLoading = ref(false)
|
||||
|
||||
function goToExportQualificationManage() {
|
||||
router.push({ path: '/user/exportQualification' })
|
||||
}
|
||||
|
||||
const warehouseShipAddressList = computed(() => props.warehouseOptions || [])
|
||||
const warehousesWithShipAddress = computed(() =>
|
||||
warehouseShipAddressList.value.filter((opt) => warehouseHasAddress(opt)),
|
||||
)
|
||||
|
||||
function warehouseHasAddress(opt?: WarehouseOption) {
|
||||
if (!opt) return false
|
||||
return Boolean(opt.recipient || opt.contactPhone || opt.shippingAddress)
|
||||
}
|
||||
|
||||
function displayWarehouseRecipient(opt?: WarehouseOption) {
|
||||
return formatWarehouseShipRecipient(opt?.recipient, '')
|
||||
}
|
||||
|
||||
function displayWarehouseShippingAddress(opt?: WarehouseOption) {
|
||||
return formatWarehouseShipAddress(opt?.shippingAddress, '')
|
||||
}
|
||||
|
||||
function buildWarehouseShipCopyText(opt?: WarehouseOption) {
|
||||
if (!opt) return ''
|
||||
const lines = [
|
||||
opt.label || '',
|
||||
`收件人:${displayWarehouseRecipient(opt)}`,
|
||||
`电话:${opt.contactPhone || '-'}`,
|
||||
`地址:${displayWarehouseShippingAddress(opt)}`,
|
||||
]
|
||||
const clientNo = String(activeDraft.value?.clientNo ?? '').trim()
|
||||
if (clientNo) lines.push(`客户单号:${clientNo}`)
|
||||
return lines.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
async function copyWarehouseShipAddress(opt?: WarehouseOption) {
|
||||
const text = buildWarehouseShipCopyText(opt)
|
||||
if (!text.trim()) {
|
||||
$baseMessage('该仓库暂未配置收货地址', 'warning')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
$baseMessage('仓库寄件信息已复制', 'success')
|
||||
} catch {
|
||||
const area = document.createElement('textarea')
|
||||
area.value = text
|
||||
area.style.position = 'fixed'
|
||||
area.style.left = '-9999px'
|
||||
document.body.appendChild(area)
|
||||
area.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
$baseMessage('仓库寄件信息已复制', 'success')
|
||||
} catch {
|
||||
$baseMessage('复制失败', 'error')
|
||||
}
|
||||
document.body.removeChild(area)
|
||||
}
|
||||
}
|
||||
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (v: boolean) => emit('update:visible', v),
|
||||
@ -347,6 +421,7 @@ async function loadExportQualifications() {
|
||||
|
||||
async function loadPickupAddresses() {
|
||||
const customerId = resolveCandidatesCustomerId()
|
||||
pickupAddressLoading.value = true
|
||||
try {
|
||||
const res = await getSenderAddressList({
|
||||
page: 1,
|
||||
@ -366,6 +441,38 @@ async function loadPickupAddresses() {
|
||||
.filter((x): x is { label: string; value: number } => !!x)
|
||||
} catch {
|
||||
pickupAddressOptions.value = []
|
||||
} finally {
|
||||
pickupAddressLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDomesticPickupProducts() {
|
||||
try {
|
||||
const [groupRows, productRes] = await Promise.all([
|
||||
getDictListAll({ dictType: 'sales_product_group' }),
|
||||
getSalesProductList({ page: 1, pageSize: 200, isActive: 2 }),
|
||||
])
|
||||
const domesticPickupGroupCodes = new Set(
|
||||
(groupRows || [])
|
||||
.filter((d: any) => String(d?.dictName ?? d?.DictName ?? '').includes('国内揽收'))
|
||||
.map((d: any) => String(d?.dictCode ?? d?.DictCode ?? '').trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
const items = (productRes as any)?.items || []
|
||||
domesticPickupProductOptions.value = items
|
||||
.filter((row: any) => {
|
||||
const groupCode = String(row?.productGroup ?? row?.ProductGroup ?? '').trim()
|
||||
return domesticPickupGroupCodes.size > 0 && domesticPickupGroupCodes.has(groupCode)
|
||||
})
|
||||
.map((row: any) => {
|
||||
const code = String(row?.productCode ?? row?.ProductCode ?? '').trim()
|
||||
const name = String(row?.productName ?? row?.ProductName ?? '').trim()
|
||||
if (!code) return null
|
||||
return { value: code, label: name ? `${name} (${code})` : code }
|
||||
})
|
||||
.filter((x: SelectOption | null): x is SelectOption => !!x)
|
||||
} catch {
|
||||
domesticPickupProductOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,6 +496,10 @@ function buildParcelBasicPayload() {
|
||||
inboundMethod: inboundMethod.value,
|
||||
senderAddressId: inboundMethod.value === 'doorPickup' ? senderAddressId.value : undefined,
|
||||
expectPickupTime: inboundMethod.value === 'doorPickup' ? expectPickupTime.value : undefined,
|
||||
carrierService:
|
||||
inboundMethod.value === 'doorPickup' && String(carrierService.value || '').trim()
|
||||
? String(carrierService.value).trim()
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -874,6 +985,7 @@ watch(
|
||||
inboundMethod.value = 'selfShip'
|
||||
senderAddressId.value = undefined
|
||||
expectPickupTime.value = undefined
|
||||
carrierService.value = undefined
|
||||
agreementChecked.value = false
|
||||
await loadBasicDicts()
|
||||
await loadDrafts()
|
||||
@ -883,8 +995,11 @@ watch(
|
||||
const est = first.lines.reduce((s, x) => s + Math.max(1, Number(x.quantity) || 0) * 0.01, 0)
|
||||
actualWeight.value = String(Number(est.toFixed(3)) || 0.01)
|
||||
}
|
||||
await loadExportQualifications()
|
||||
await loadPickupAddresses()
|
||||
await Promise.all([
|
||||
loadExportQualifications(),
|
||||
loadPickupAddresses(),
|
||||
loadDomesticPickupProducts(),
|
||||
])
|
||||
if (isFulfill.value) await loadCandidates()
|
||||
},
|
||||
{ immediate: true },
|
||||
@ -909,6 +1024,7 @@ watch(needExportQualification, (need) => {
|
||||
watch(inboundMethod, (method) => {
|
||||
if (!props.visible || method !== 'doorPickup') return
|
||||
void loadPickupAddresses()
|
||||
if (!domesticPickupProductOptions.value.length) void loadDomesticPickupProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1117,11 +1233,106 @@ watch(inboundMethod, (method) => {
|
||||
size="small"
|
||||
class="inbound-method-radios"
|
||||
>
|
||||
<a-radio value="selfShip">自寄到仓</a-radio>
|
||||
<a-radio value="selfShip">
|
||||
<span class="inbound-method-pickup-label">
|
||||
自寄到仓
|
||||
<a-popover
|
||||
position="bl"
|
||||
trigger="hover"
|
||||
content-class="warehouse-ship-popover"
|
||||
>
|
||||
<IconQuestionCircle class="inbound-method-help" @click.stop />
|
||||
<template #content>
|
||||
<div class="warehouse-ship-popover__body">
|
||||
<div class="warehouse-ship-popover__head">
|
||||
<strong>寄往仓库</strong>
|
||||
</div>
|
||||
<p
|
||||
v-if="!warehouseShipAddressList.length"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
暂无仓库信息,请稍后重试或联系客服
|
||||
</p>
|
||||
<p
|
||||
v-else-if="!warehousesWithShipAddress.length"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
暂无已配置收货地址的仓库,请联系客服
|
||||
</p>
|
||||
<template v-else>
|
||||
<div class="warehouse-ship-popover__list">
|
||||
<div
|
||||
v-for="opt in warehouseShipAddressList"
|
||||
:key="opt.value"
|
||||
class="warehouse-ship-popover__item"
|
||||
:class="{
|
||||
'is-selected': String(opt.value) === String(warehouseId ?? ''),
|
||||
'is-empty': !warehouseHasAddress(opt),
|
||||
}"
|
||||
>
|
||||
<div class="warehouse-ship-popover__item-head">
|
||||
<strong>
|
||||
{{ opt.label || opt.value }}
|
||||
<span
|
||||
v-if="String(opt.value) === String(warehouseId ?? '')"
|
||||
class="warehouse-ship-popover__badge"
|
||||
>
|
||||
当前
|
||||
</span>
|
||||
</strong>
|
||||
<a-button
|
||||
v-if="warehouseHasAddress(opt)"
|
||||
type="text"
|
||||
size="mini"
|
||||
class="warehouse-ship-popover__copy"
|
||||
@click="copyWarehouseShipAddress(opt)"
|
||||
>
|
||||
<template #icon><IconCopy /></template>
|
||||
复制
|
||||
</a-button>
|
||||
</div>
|
||||
<p
|
||||
v-if="!warehouseHasAddress(opt)"
|
||||
class="warehouse-ship-popover__empty"
|
||||
>
|
||||
暂未配置收货地址
|
||||
</p>
|
||||
<dl v-else class="warehouse-ship-popover__rows">
|
||||
<div>
|
||||
<dt>收件人</dt>
|
||||
<dd>{{ displayWarehouseRecipient(opt) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>电话</dt>
|
||||
<dd>{{ opt.contactPhone || '-' }}</dd>
|
||||
</div>
|
||||
<div class="is-addr">
|
||||
<dt>地址</dt>
|
||||
<dd>{{ displayWarehouseShippingAddress(opt) }}</dd>
|
||||
</div>
|
||||
<div v-if="opt.remark">
|
||||
<dt>备注</dt>
|
||||
<dd>{{ opt.remark }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<p class="warehouse-ship-popover__hint">
|
||||
按所选仓库地址自行寄出。快递面单建议备注客户单号,方便仓库认领。
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</span>
|
||||
</a-radio>
|
||||
<a-radio value="doorPickup">
|
||||
<span class="inbound-method-pickup-label">
|
||||
上门揽收
|
||||
<a-tooltip content="上门揽收为付费服务,费用通常低于市场价。" position="top">
|
||||
<a-tooltip
|
||||
content="上门揽收为付费服务,费用通常低于市场价。"
|
||||
position="top"
|
||||
>
|
||||
<IconQuestionCircle class="inbound-method-help" @click.stop.prevent />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
@ -1129,12 +1340,24 @@ watch(inboundMethod, (method) => {
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inboundMethod === 'doorPickup'">
|
||||
<a-form-item label="仓库地址" required>
|
||||
<a-select v-model="warehouseId" allow-search placeholder="请选择仓库">
|
||||
<a-option
|
||||
v-for="opt in warehouseOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="揽收地址" required>
|
||||
<a-select
|
||||
v-model="senderAddressId"
|
||||
allow-search
|
||||
allow-clear
|
||||
placeholder="请选择国内揽收地址"
|
||||
:loading="pickupAddressLoading"
|
||||
@focus="loadPickupAddresses"
|
||||
>
|
||||
<a-option
|
||||
@ -1155,6 +1378,25 @@ watch(inboundMethod, (method) => {
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="承运服务">
|
||||
<a-select
|
||||
v-model="carrierService"
|
||||
allow-search
|
||||
allow-clear
|
||||
placeholder="选择国内揽收销售产品(可后补)"
|
||||
>
|
||||
<a-option
|
||||
v-for="opt in domesticPickupProductOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<p class="inbound-pickup-batch-hint">
|
||||
{{ t('order.placeOrder.single.doorPickupBatchHint') }}
|
||||
</p>
|
||||
</template>
|
||||
</a-form>
|
||||
</section>
|
||||
@ -1708,6 +1950,14 @@ watch(inboundMethod, (method) => {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.inbound-pickup-batch-hint {
|
||||
margin: -4px 0 8px;
|
||||
grid-column: 1 / -1;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-3, #86909c);
|
||||
}
|
||||
|
||||
.inbound-method-radios {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
@ -2474,3 +2724,136 @@ watch(inboundMethod, (method) => {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* Popover 挂载到 body,需非 scoped(与下单页一致) */
|
||||
.warehouse-ship-popover {
|
||||
max-width: 400px;
|
||||
padding: 0;
|
||||
|
||||
.arco-popover-content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__body {
|
||||
min-width: 260px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__copy {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
color: var(--app-primary-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__list {
|
||||
margin-top: 8px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 4px;
|
||||
background: var(--color-fill-1);
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
|
||||
}
|
||||
|
||||
&.is-empty {
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 14%, #fff);
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__rows {
|
||||
margin: 6px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
> div {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__hint,
|
||||
.warehouse-ship-popover__empty {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.warehouse-ship-popover__item .warehouse-ship-popover__empty {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -82,7 +82,15 @@ const fulfillStatus = ref<string>(resolveFulfillStatusFromQuery())
|
||||
const platformCode = ref<string>(resolvePlatformFromQuery())
|
||||
const keyword = ref('')
|
||||
const summary = ref<Record<string, number>>({})
|
||||
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
type WarehouseOption = {
|
||||
label: string
|
||||
value: number
|
||||
recipient?: string
|
||||
contactPhone?: string
|
||||
shippingAddress?: string
|
||||
remark?: string
|
||||
}
|
||||
const warehouseOptions = ref<WarehouseOption[]>([])
|
||||
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
|
||||
const productOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const wmsProductOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
@ -728,8 +736,12 @@ onMounted(async () => {
|
||||
const warehouseTask = getWarehouseList({ page: 1, pageSize: 200 })
|
||||
.then(({ items }) => {
|
||||
warehouseOptions.value = (items || []).map((w: any) => ({
|
||||
label: w.warehouseName || `#${w.id}`,
|
||||
value: Number(w.id),
|
||||
label: w.warehouseName || w.WarehouseName || `#${w.id ?? w.Id}`,
|
||||
value: Number(w.id ?? w.Id),
|
||||
recipient: String(w.recipient ?? w.Recipient ?? '').trim() || undefined,
|
||||
contactPhone: String(w.contactPhone ?? w.ContactPhone ?? '').trim() || undefined,
|
||||
shippingAddress: String(w.shippingAddress ?? w.ShippingAddress ?? '').trim() || undefined,
|
||||
remark: String(w.remark ?? w.Remark ?? '').trim() || undefined,
|
||||
}))
|
||||
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user