fix: 创建运单基本信息卡片对齐下单页样式与交互
Some checks failed
Build and Deploy Front / deploy (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-20 11:44:46 +08:00
parent a917216c3b
commit 5f6496c661

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
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 } from '@arco-design/web-vue/es/icon'
import { IconPlus, IconDelete, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
import {
batchCreatePlatformStoreFulfill,
batchCreatePlatformStoreFulfillWms,
@ -18,6 +19,7 @@ import { getExportQualificationList } from '@/api/customer/exportQualification'
import { getDictListAll } from '@/api/system/dict'
import { resolveOmsAssetUrl } from '@/api/oms/common'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
import ExportCustoms9610GuideModal from '@/views/order/placeOrder/components/ExportCustoms9610GuideModal.vue'
import { $baseMessage } from '@/utils'
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute'
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
@ -102,6 +104,7 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const router = useRouter()
const loading = ref(false)
const submitting = ref(false)
const productCode = ref<string>()
@ -125,21 +128,22 @@ const batchMatchDeclaration = ref(false)
const batchQtyPerOrder = ref(1)
let searchTimer: ReturnType<typeof setTimeout> | null = null
/** 运单基本信息(对齐下单页) */
/** 运单基本信息(对齐下单页:字符串输入 + liquid-card 表单交互) */
const cargoAttribute = ref('general')
const actualWeight = ref<number | undefined>(undefined)
const quantity = ref<number>(1)
const lengthCm = ref<number | undefined>(10)
const widthCm = ref<number | undefined>(10)
const heightCm = ref<number | undefined>(10)
const actualWeight = ref('')
const quantity = ref('1')
const lengthCm = ref('10')
const widthCm = ref('10')
const heightCm = ref('10')
const exportCustomsMode = ref(SAMPLE_EXPORT_CUSTOMS_MODE_CODE)
const exportQualificationId = ref<number | undefined>()
const exportQualificationId = ref<string>()
const platformCode = ref<string>()
const inboundMethod = ref<'selfShip' | 'doorPickup'>('selfShip')
const senderAddressId = ref<number | undefined>()
const expectPickupTime = ref<string>()
const agreementChecked = ref(false)
const agreementModalVisible = ref(false)
const exportCustomsGuideVisible = ref(false)
const dictLoading = ref(false)
const cargoAttributeOptions = ref<SelectOption[]>([])
const exportCustomsModeOptions = ref<SelectOption[]>([])
@ -147,6 +151,10 @@ const ecommercePlatformOptions = ref<SelectOption[]>([])
const exportQualificationOptions = ref<QualOption[]>([])
const pickupAddressOptions = ref<Array<{ label: string; value: number }>>([])
function goToExportQualificationManage() {
router.push({ path: '/user/exportQualification' })
}
const modalVisible = computed({
get: () => props.visible,
set: (v: boolean) => emit('update:visible', v),
@ -238,6 +246,11 @@ const basicInfoReady = computed(() => {
return true
})
function parsePositiveNumber(raw: unknown) {
const n = Number(String(raw ?? '').trim())
return Number.isFinite(n) && n > 0 ? n : undefined
}
const canSubmit = computed(() => {
if (!basicInfoReady.value) return false
if (!agreementChecked.value) return false
@ -325,7 +338,7 @@ async function loadExportQualifications() {
if (!exportQualificationId.value) {
const def =
exportQualificationOptions.value.find((x) => x.isDefault) || exportQualificationOptions.value[0]
exportQualificationId.value = def?.id
exportQualificationId.value = def ? String(def.id) : undefined
}
} catch {
exportQualificationOptions.value = []
@ -363,13 +376,15 @@ function openAgreementModal() {
function buildParcelBasicPayload() {
return {
cargoAttribute: normalizeCargoAttributeCode(cargoAttribute.value),
actualWeight: Number(actualWeight.value),
quantity: Number(quantity.value),
length: Number(lengthCm.value),
width: Number(widthCm.value),
height: Number(heightCm.value),
actualWeight: parsePositiveNumber(actualWeight.value),
quantity: parsePositiveNumber(quantity.value),
length: parsePositiveNumber(lengthCm.value),
width: parsePositiveNumber(widthCm.value),
height: parsePositiveNumber(heightCm.value),
exportCustomsMode: String(exportCustomsMode.value || '').trim() || resolveExportCustomsMode9610Simple(),
exportQualificationId: needExportQualification.value ? exportQualificationId.value : undefined,
exportQualificationId: needExportQualification.value
? parsePositiveNumber(exportQualificationId.value)
: undefined,
platformCode: needExportQualification.value ? platformCode.value : undefined,
inboundMethod: inboundMethod.value,
senderAddressId: inboundMethod.value === 'doorPickup' ? senderAddressId.value : undefined,
@ -848,11 +863,11 @@ watch(
batchMatchDeclaration.value = false
batchQtyPerOrder.value = 1
cargoAttribute.value = 'general'
actualWeight.value = undefined
quantity.value = 1
lengthCm.value = 10
widthCm.value = 10
heightCm.value = 10
actualWeight.value = ''
quantity.value = '1'
lengthCm.value = '10'
widthCm.value = '10'
heightCm.value = '10'
exportCustomsMode.value = SAMPLE_EXPORT_CUSTOMS_MODE_CODE
exportQualificationId.value = undefined
platformCode.value = undefined
@ -866,7 +881,7 @@ watch(
const first = drafts.value[0]
if (first && !(Number(actualWeight.value) > 0)) {
const est = first.lines.reduce((s, x) => s + Math.max(1, Number(x.quantity) || 0) * 0.01, 0)
actualWeight.value = Number(est.toFixed(3)) || 0.01
actualWeight.value = String(Number(est.toFixed(3)) || 0.01)
}
await loadExportQualifications()
await loadPickupAddresses()
@ -933,10 +948,11 @@ watch(inboundMethod, (method) => {
}}
</p>
<section class="psc-basic">
<header class="psc-basic__head">
<strong>{{ t('order.placeOrder.single.stepBasic') }}</strong>
<span class="psc-config__meta">
<section class="single-info-card liquid-card psc-basic-card">
<header class="single-info-card__header">
<span class="single-section-index">1</span>
<span class="single-section-title">{{ t('order.placeOrder.single.stepBasic') }}</span>
<span class="psc-basic-meta">
{{
t('order.platformStoreOrder.createLogistics.readyMeta', {
total: drafts.length,
@ -945,134 +961,217 @@ watch(inboundMethod, (method) => {
}}
</span>
</header>
<div class="psc-basic__grid">
<div class="psc-basic__item">
<span class="psc-basic__label">
{{
<a-form layout="vertical" class="single-form-grid single-form-grid--compact">
<a-form-item
:label="
isFulfillWms
? t('order.platformStoreOrder.createFulfillWms.product')
: t('order.platformStoreOrder.createLogistics.product')
}}
<em>*</em>
</span>
"
required
>
<a-select
v-model="productCode"
allow-search
:options="productOptions"
placeholder="请选择"
:loading="dictLoading"
:placeholder="t('order.platformStoreOrder.form.product')"
/>
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">客户单号 <em>*</em></span>
>
<a-option
v-for="opt in productOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="客户单号" required>
<a-input
v-if="activeDraft"
v-model="activeDraft.clientNo"
:placeholder="activeDraft.platformOrderNo || '客户单号'"
/>
<a-input v-else disabled placeholder="—" />
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">货物属性 <em>*</em></span>
</a-form-item>
<a-form-item label="货物属性" required>
<a-select
v-model="cargoAttribute"
allow-search
:options="cargoAttributeOptions"
:loading="dictLoading"
placeholder="请选择"
/>
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">仓库 <em>*</em></span>
:loading="dictLoading"
>
<a-option
v-for="opt in cargoAttributeOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="仓库" required>
<a-select
v-model="warehouseId"
allow-search
:options="warehouseOptions"
:placeholder="t('warehouse.common.warehouse')"
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="实重(kg)" required>
<a-input v-model="actualWeight" placeholder="请输入" />
</a-form-item>
<a-form-item label="包裹数量" required>
<a-input v-model="quantity" placeholder="请输入" />
</a-form-item>
<a-form-item class="export-customs-mode-item">
<template #label>
<span class="export-customs-mode-label">
出口报关方式
<IconQuestionCircle
class="export-customs-help-icon"
@click.stop.prevent="exportCustomsGuideVisible = true"
/>
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">实重(kg) <em>*</em></span>
<a-input-number v-model="actualWeight" :min="0.01" :precision="3" hide-button style="width: 100%" />
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">包裹数量 <em>*</em></span>
<a-input-number v-model="quantity" :min="1" :precision="0" hide-button style="width: 100%" />
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">出口报关方式</span>
</span>
</template>
<a-select
v-model="exportCustomsMode"
allow-clear
allow-search
:options="exportCustomsModeOptions"
:placeholder="t('order.placeOrder.single.exportCustomsPlaceholder')"
:loading="dictLoading"
placeholder="请选择"
/>
</div>
<div v-if="needExportQualification" class="psc-basic__item">
<span class="psc-basic__label">出口资质备案 <em>*</em></span>
>
<a-option
v-for="opt in exportCustomsModeOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="needExportQualification"
:label="t('order.placeOrder.single.exportQualification')"
required
>
<div class="export-qualification-field">
<a-select
v-model="exportQualificationId"
allow-search
:options="exportQualificationOptions.map((x) => ({ label: x.label, value: x.id }))"
placeholder="请选择"
/>
:placeholder="t('order.placeOrder.single.exportQualificationPlaceholder')"
>
<a-option
v-for="opt in exportQualificationOptions"
:key="opt.id"
:value="String(opt.id)"
>
{{ opt.label }}
</a-option>
</a-select>
<a-button type="text" size="mini" @click="goToExportQualificationManage">
{{ t('order.placeOrder.single.exportQualificationManage') }}
</a-button>
</div>
<div v-if="needExportQualification" class="psc-basic__item">
<span class="psc-basic__label">电商平台 <em>*</em></span>
</a-form-item>
<a-form-item
v-if="needExportQualification"
:label="t('order.placeOrder.single.platformCode')"
required
>
<a-select
v-model="platformCode"
allow-clear
allow-search
:options="ecommercePlatformOptions"
placeholder="请选择"
/>
:placeholder="t('order.placeOrder.single.platformCodePlaceholder')"
:loading="dictLoading"
>
<a-option
v-for="opt in ecommercePlatformOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item class="dims-form-item" label="长宽高(cm)" required>
<div class="dims-group">
<a-input v-model="lengthCm" placeholder="长" />
<span class="dims-sep">×</span>
<a-input v-model="widthCm" placeholder="宽" />
<span class="dims-sep">×</span>
<a-input v-model="heightCm" placeholder="高" />
</div>
<div class="psc-basic__item psc-basic__item--dims">
<span class="psc-basic__label">长宽高(cm) <em>*</em></span>
<div class="psc-dims">
<a-input-number v-model="lengthCm" :min="0.1" :precision="1" hide-button placeholder="长" />
<span>×</span>
<a-input-number v-model="widthCm" :min="0.1" :precision="1" hide-button placeholder="宽" />
<span>×</span>
<a-input-number v-model="heightCm" :min="0.1" :precision="1" hide-button placeholder="高" />
</div>
</div>
<div class="psc-basic__item psc-basic__item--inbound">
<span class="psc-basic__label">到仓方式 <em>*</em></span>
<a-radio-group v-model="inboundMethod" type="button" size="small">
</a-form-item>
<a-form-item label="到仓方式" required class="inbound-method-item">
<a-radio-group
v-model="inboundMethod"
type="button"
size="small"
class="inbound-method-radios"
>
<a-radio value="selfShip">自寄到仓</a-radio>
<a-radio value="doorPickup">上门揽收</a-radio>
<a-radio value="doorPickup">
<span class="inbound-method-pickup-label">
上门揽收
<a-tooltip content="上门揽收为付费服务,费用通常低于市场价。" position="top">
<IconQuestionCircle class="inbound-method-help" @click.stop.prevent />
</a-tooltip>
</span>
</a-radio>
</a-radio-group>
</div>
</a-form-item>
<template v-if="inboundMethod === 'doorPickup'">
<div class="psc-basic__item">
<span class="psc-basic__label">揽收地址 <em>*</em></span>
<a-form-item label="揽收地址" required>
<a-select
v-model="senderAddressId"
allow-search
:options="pickupAddressOptions"
placeholder="请选择揽收地址"
/>
</div>
<div class="psc-basic__item">
<span class="psc-basic__label">期望揽收时间 <em>*</em></span>
allow-clear
placeholder="请选择国内揽收地址"
@focus="loadPickupAddresses"
>
<a-option
v-for="opt in pickupAddressOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="期望揽收时间" required>
<a-date-picker
v-model="expectPickupTime"
show-time
format="YYYY-MM-DD HH:mm"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</div>
</a-form-item>
</template>
</div>
<div class="psc-agreement">
</a-form>
</section>
<div class="psc-submit-bar liquid-card">
<div class="order-agreement-bar">
<a-checkbox v-model="agreementChecked" />
<span class="psc-agreement__link" @click="openAgreementModal">
<span class="agreement-link" @click="openAgreementModal">
{{ t('order.placeOrder.agreement.label') }}
</span>
</div>
</section>
<span class="psc-submit-bar__hint">
{{ productLabel }}
<template v-if="warehouseId"> · {{ warehouseLabel }}</template>
· {{ drafts.length }} {{ t('order.platformStoreOrder.createLogistics.unitOrders') }}
</span>
</div>
<div v-if="showBatchBar" class="psc-batch-bar">
<div class="psc-batch-bar__left">
@ -1404,11 +1503,6 @@ watch(inboundMethod, (method) => {
</section>
</div>
<div class="psc-footer-summary">
{{ productLabel }}
<template v-if="warehouseId"> · {{ warehouseLabel }}</template>
· {{ drafts.length }} {{ t('order.platformStoreOrder.createLogistics.unitOrders') }}
</div>
</div>
</a-modal>
@ -1421,6 +1515,8 @@ watch(inboundMethod, (method) => {
>
<pre class="psc-agreement-body">{{ AGREEMENT_TEXT }}</pre>
</a-modal>
<ExportCustoms9610GuideModal v-model:visible="exportCustomsGuideVisible" />
</template>
<style scoped lang="less">
@ -1448,79 +1544,276 @@ watch(inboundMethod, (method) => {
flex-shrink: 0;
}
.psc-basic {
.liquid-card {
position: relative;
z-index: 0;
overflow: hidden;
isolation: isolate;
border: 1px solid var(--app-card-border);
border-radius: 16px !important;
background: var(--app-card-bg);
box-shadow:
0 8px 24px rgba(15, 23, 42, 0.05),
0 1px 2px rgba(15, 23, 42, 0.03);
> * {
position: relative;
z-index: 1;
}
}
.psc-basic-card {
margin-bottom: 10px;
padding: 12px;
background: var(--color-fill-2);
border-radius: 4px;
padding: 14px 16px 12px;
box-sizing: border-box;
flex-shrink: 0;
}
.psc-basic__head {
.single-info-card__header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
gap: 8px;
margin-bottom: 12px;
}
strong {
.single-section-index {
width: 22px;
height: 22px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: var(--color-text-2);
background: var(--color-fill-2);
flex-shrink: 0;
}
.single-section-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
}
line-height: 1.4;
}
.psc-basic__grid {
.psc-basic-meta {
margin-left: auto;
font-size: 12px;
color: var(--color-text-3);
}
.single-form-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px 12px;
}
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px 16px;
.psc-basic__item {
&--compact {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
:deep(.arco-form-item) {
margin-bottom: 0;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
align-items: stretch;
}
.psc-basic__label {
font-size: 12px;
:deep(.arco-form-item-label-col) {
width: 100%;
flex: none;
margin-bottom: 6px;
text-align: left !important;
}
:deep(.arco-form-item-wrapper-col) {
width: 100%;
flex: none;
}
:deep(.arco-form-item-label) {
line-height: 1.4;
color: var(--color-text-2);
font-size: 13px;
font-weight: 500;
justify-content: flex-start !important;
text-align: left !important;
width: 100%;
}
em {
color: #f53f3f;
font-style: normal;
margin-left: 2px;
:deep(.arco-form-item-label-required-symbol) {
margin-right: 2px;
margin-left: 0;
line-height: 1;
vertical-align: middle;
}
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple),
:deep(.arco-picker) {
min-height: 32px;
border-radius: 4px;
background-color: var(--app-card-bg);
border: 1px solid var(--color-border-2);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
:deep(.arco-input-wrapper:hover),
:deep(.arco-select-view-single:hover),
:deep(.arco-select-view-multiple:hover),
:deep(.arco-picker:hover) {
border-color: rgba(var(--primary-6), 0.45);
}
:deep(.arco-input-wrapper.arco-input-focus),
:deep(.arco-select-view-single.arco-select-view-focus),
:deep(.arco-select-view-multiple.arco-select-view-focus),
:deep(.arco-picker-focused) {
border-color: var(--app-primary-color);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.12);
}
:deep(.arco-input),
:deep(.arco-select-view-value) {
font-size: 13px;
}
:deep(.arco-select) {
width: 100%;
}
}
.psc-basic__item--dims,
.psc-basic__item--inbound {
grid-column: span 1;
}
.psc-dims {
.dims-group {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
min-width: 0;
.arco-input-number {
:deep(.arco-input-wrapper) {
flex: 1;
min-width: 0;
}
}
.psc-agreement {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--color-border-2);
.dims-sep {
flex-shrink: 0;
color: var(--color-text-3);
font-size: 13px;
line-height: 1;
}
.psc-agreement__link {
.inbound-method-item {
grid-column: 1 / -1;
}
.inbound-method-radios {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
:deep(.arco-radio-group-button) {
background: var(--color-fill-2);
}
:deep(.arco-radio-button) {
color: var(--color-text-2);
&:hover {
color: var(--app-primary-color, rgb(var(--primary-6)));
}
&.arco-radio-checked {
color: #fff;
background-color: var(--app-primary-color, rgb(var(--primary-6)));
border-color: var(--app-primary-color, rgb(var(--primary-6)));
&:hover {
color: #fff;
background-color: var(--app-primary-color-hover, var(--app-primary-color));
border-color: var(--app-primary-color-hover, var(--app-primary-color));
}
}
}
:deep(.arco-radio-checked .arco-radio-button-content) {
color: #fff;
}
}
.inbound-method-pickup-label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.inbound-method-help {
color: inherit;
opacity: 0.75;
font-size: 13px;
cursor: help;
vertical-align: middle;
&:hover {
opacity: 1;
color: inherit;
}
}
.export-customs-mode-label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.export-qualification-field {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
:deep(.arco-select) {
flex: 1;
min-width: 0;
}
}
.export-customs-help-icon {
color: var(--color-text-3);
font-size: 14px;
cursor: pointer;
flex-shrink: 0;
&:hover {
color: var(--app-primary-color);
}
}
.psc-submit-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
padding: 12px 16px;
flex-shrink: 0;
}
.order-agreement-bar {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--color-text-2);
flex-shrink: 0;
}
.agreement-link {
color: var(--app-primary-color);
cursor: pointer;
font-size: 13px;
user-select: none;
&:hover {
@ -1528,6 +1821,12 @@ watch(inboundMethod, (method) => {
}
}
.psc-submit-bar__hint {
margin-left: auto;
font-size: 12px;
color: var(--color-text-3);
}
.psc-agreement-body {
margin: 0;
white-space: pre-wrap;
@ -1540,12 +1839,6 @@ watch(inboundMethod, (method) => {
overflow: auto;
}
.psc-config__meta {
margin-left: auto;
font-size: 12px;
color: var(--color-text-3);
}
.psc-batch-bar {
display: flex;
flex-wrap: wrap;
@ -2180,12 +2473,4 @@ watch(inboundMethod, (method) => {
}
}
.psc-footer-summary {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--color-border-2);
font-size: 13px;
color: var(--color-text-2);
flex-shrink: 0;
}
</style>