feat: 官网海洋蓝波浪首页与指南分受众,并同步业务页优化 #9

Merged
zzh merged 6 commits from test into master 2026-10-21 18:18:27 +08:00
2 changed files with 404 additions and 9 deletions
Showing only changes of commit 209a6e1c2a - Show all commits

View File

@ -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>

View File

@ -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
})