All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m50s
Co-authored-by: Cursor <cursoragent@cursor.com>
2192 lines
67 KiB
Vue
2192 lines
67 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { Modal } from '@arco-design/web-vue'
|
||
import { IconPlus, IconDelete } from '@arco-design/web-vue/es/icon'
|
||
import {
|
||
batchCreatePlatformStoreFulfill,
|
||
batchCreatePlatformStoreFulfillWms,
|
||
batchCreatePlatformStoreLogistics,
|
||
getPlatformStoreOrderDetail,
|
||
} from '@/api/order/platformStore'
|
||
import {
|
||
postOrderStockBindInventoryCandidates,
|
||
postOrderStockBindParcelCandidates,
|
||
} from '@/api/order'
|
||
import { getSenderAddressList } from '@/api/order/senderAddress'
|
||
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 { $baseMessage } from '@/utils'
|
||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute'
|
||
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||
|
||
import type { CreateLogisticsMode } from './constants'
|
||
|
||
type SelectOption = { label: string; value: string }
|
||
type QualOption = { id: number; label: string; isDefault: boolean }
|
||
|
||
const SAMPLE_EXPORT_CUSTOMS_MODE_CODE = '9610Simple'
|
||
const EXPORT_CUSTOMS_MODE_9610_SUMMARY = '9610Summary'
|
||
const AGREEMENT_TEXT = `《物流下单用户协议》及《物流服务赔偿协议》
|
||
|
||
一、服务说明
|
||
您确认通过本系统创建的运单/代发订单,将按所选运输方式、仓库与件重尺计费并履约。
|
||
|
||
二、责任与理赔
|
||
请以正式协议文本为准;勾选即视为已阅读并同意全部条款。
|
||
|
||
三、勾选确认
|
||
本人已仔细阅读并充分理解全部条款,自愿接受本协议所有约定。`
|
||
|
||
export type { CreateLogisticsMode }
|
||
|
||
export type BindDraftItem = {
|
||
key: string
|
||
bindType: 'inventory' | 'parcelItem'
|
||
skuId?: number
|
||
inventoryId?: number
|
||
parcelId?: number
|
||
parcelItemId?: number
|
||
bindQty: number
|
||
skuCode?: string
|
||
skuName?: string
|
||
productName?: string
|
||
trackingNo?: string
|
||
availableQty?: number
|
||
imageUrl?: string
|
||
}
|
||
|
||
type OrderDraft = {
|
||
storeOrderId: number
|
||
customerId?: number
|
||
warehouseId?: number
|
||
platformOrderNo: string
|
||
/** 可编辑客户单号,默认平台订单号 */
|
||
clientNo: string
|
||
orderCountry: string
|
||
recipientName: string
|
||
recipientPhone: string
|
||
recipientCountry: string
|
||
recipientProvince: string
|
||
recipientCity: string
|
||
recipientZipCode: string
|
||
recipientAddress1: string
|
||
recipientAddress2: string
|
||
lines: Array<{
|
||
platformSku: string
|
||
productName: string
|
||
quantity: number
|
||
unitPrice: string
|
||
}>
|
||
receiverMissing: boolean
|
||
linesMissing: boolean
|
||
bindItems: BindDraftItem[]
|
||
}
|
||
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
mode: CreateLogisticsMode
|
||
storeOrderIds: number[]
|
||
productOptions: Array<{ label: string; value: string }>
|
||
warehouseOptions: Array<{ label: string; value: number }>
|
||
defaultProductCode?: string
|
||
defaultWarehouseId?: number
|
||
customerId?: number
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:visible': [value: boolean]
|
||
success: []
|
||
}>()
|
||
|
||
const { t } = useI18n()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const productCode = ref<string>()
|
||
const warehouseId = ref<number>()
|
||
const drafts = ref<OrderDraft[]>([])
|
||
const activeId = ref<number>()
|
||
const leftTab = ref<'inventory' | 'parcel'>('inventory')
|
||
const pickSourceTabs = computed(() => {
|
||
const disabled = !warehouseId.value
|
||
return [
|
||
{ key: 'inventory', label: t('order.myOrders.stockBind.tabInventory'), disabled },
|
||
{ key: 'parcel', label: t('order.myOrders.stockBind.tabParcel'), disabled },
|
||
]
|
||
})
|
||
const keyword = ref('')
|
||
const inventoryCandidates = ref<any[]>([])
|
||
const parcelCandidates = ref<any[]>([])
|
||
const candidatesLoading = ref(false)
|
||
const batchMode = ref(true)
|
||
const batchMatchDeclaration = ref(false)
|
||
const batchQtyPerOrder = ref(1)
|
||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
/** 运单基本信息(对齐下单页) */
|
||
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 exportCustomsMode = ref(SAMPLE_EXPORT_CUSTOMS_MODE_CODE)
|
||
const exportQualificationId = ref<number | undefined>()
|
||
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 dictLoading = ref(false)
|
||
const cargoAttributeOptions = ref<SelectOption[]>([])
|
||
const exportCustomsModeOptions = ref<SelectOption[]>([])
|
||
const ecommercePlatformOptions = ref<SelectOption[]>([])
|
||
const exportQualificationOptions = ref<QualOption[]>([])
|
||
const pickupAddressOptions = ref<Array<{ label: string; value: number }>>([])
|
||
|
||
const modalVisible = computed({
|
||
get: () => props.visible,
|
||
set: (v: boolean) => emit('update:visible', v),
|
||
})
|
||
|
||
const isFulfill = computed(() => props.mode === 'fulfill' || props.mode === 'fulfillWms')
|
||
const isFulfillWms = computed(() => props.mode === 'fulfillWms')
|
||
const title = computed(() => {
|
||
if (isFulfillWms.value) return t('order.platformStoreOrder.createFulfillWms.title')
|
||
if (isFulfill.value) return t('order.platformStoreOrder.createFulfill.title')
|
||
return t('order.platformStoreOrder.createLogistics.title')
|
||
})
|
||
|
||
const activeDraft = computed(() => drafts.value.find((d) => d.storeOrderId === activeId.value))
|
||
|
||
const activeShipLines = computed(() => activeDraft.value?.lines || [])
|
||
|
||
const orderQtyTotal = computed(() =>
|
||
activeShipLines.value.reduce((sum, x) => sum + (Number(x.quantity) || 0), 0),
|
||
)
|
||
|
||
const bindQtyTotal = computed(() =>
|
||
(activeDraft.value?.bindItems || []).reduce((sum, x) => sum + (Number(x.bindQty) || 0), 0),
|
||
)
|
||
|
||
/** 数量不一致仅提示校对,不强制拦截(业务允许申报与绑定不对齐) */
|
||
const qtyMismatch = computed(() => {
|
||
if (!isFulfill.value || !activeDraft.value?.bindItems.length) return false
|
||
return orderQtyTotal.value !== bindQtyTotal.value
|
||
})
|
||
|
||
const assignableDrafts = computed(() =>
|
||
drafts.value.filter((d) => !d.receiverMissing && !d.linesMissing),
|
||
)
|
||
|
||
const showBatchBar = computed(
|
||
() => isFulfill.value && drafts.value.length > 1 && assignableDrafts.value.length > 0,
|
||
)
|
||
|
||
const readyCount = computed(() => {
|
||
if (!isFulfill.value) {
|
||
return assignableDrafts.value.length
|
||
}
|
||
return assignableDrafts.value.filter((d) => d.bindItems.length > 0).length
|
||
})
|
||
|
||
function draftOrderQty(d: OrderDraft) {
|
||
return d.lines.reduce((s, x) => s + (Number(x.quantity) || 0), 0)
|
||
}
|
||
|
||
function draftBindQty(d: OrderDraft) {
|
||
return d.bindItems.reduce((s, x) => s + (Number(x.bindQty) || 0), 0)
|
||
}
|
||
|
||
const productLabel = computed(() => {
|
||
const opt = props.productOptions.find((x) => x.value === productCode.value)
|
||
return opt?.label || productCode.value || '-'
|
||
})
|
||
|
||
const warehouseLabel = computed(() => {
|
||
const opt = props.warehouseOptions.find((x) => x.value === warehouseId.value)
|
||
return opt?.label || '-'
|
||
})
|
||
|
||
const needExportQualification = computed(
|
||
() =>
|
||
String(exportCustomsMode.value ?? '').trim().toLowerCase() ===
|
||
EXPORT_CUSTOMS_MODE_9610_SUMMARY.toLowerCase(),
|
||
)
|
||
|
||
const basicInfoReady = computed(() => {
|
||
if (!productCode.value) return false
|
||
if (!warehouseId.value) return false
|
||
if (!String(cargoAttribute.value || '').trim()) return false
|
||
if (!(Number(actualWeight.value) > 0)) return false
|
||
if (!(Number(quantity.value) > 0)) return false
|
||
if (!(Number(lengthCm.value) > 0) || !(Number(widthCm.value) > 0) || !(Number(heightCm.value) > 0)) {
|
||
return false
|
||
}
|
||
if (needExportQualification.value) {
|
||
if (!(Number(exportQualificationId.value) > 0)) return false
|
||
if (!String(platformCode.value || '').trim()) return false
|
||
}
|
||
if (inboundMethod.value === 'doorPickup') {
|
||
if (!(Number(senderAddressId.value) > 0)) return false
|
||
if (!expectPickupTime.value) return false
|
||
}
|
||
if (drafts.value.some((d) => !String(d.clientNo || '').trim())) return false
|
||
return true
|
||
})
|
||
|
||
const canSubmit = computed(() => {
|
||
if (!basicInfoReady.value) return false
|
||
if (!agreementChecked.value) return false
|
||
if (drafts.value.length === 0) return false
|
||
if (drafts.value.some((d) => d.receiverMissing || d.linesMissing)) return false
|
||
if (isFulfill.value) {
|
||
if (drafts.value.some((d) => d.bindItems.length === 0)) return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
function resolveExportCustomsMode9610Simple() {
|
||
const opts = exportCustomsModeOptions.value
|
||
const byCode = opts.find(
|
||
(o) => o.value === SAMPLE_EXPORT_CUSTOMS_MODE_CODE || o.value.toLowerCase() === '9610simple',
|
||
)
|
||
if (byCode) return byCode.value
|
||
const byLabel = opts.find((o) => /9610.*简易|简易.*9610/i.test(o.label))
|
||
if (byLabel) return byLabel.value
|
||
return SAMPLE_EXPORT_CUSTOMS_MODE_CODE
|
||
}
|
||
|
||
function mapDictOptions(rows: any[]): SelectOption[] {
|
||
return (rows || [])
|
||
.map((row) => {
|
||
const value = String(row?.dictCode ?? row?.DictCode ?? '').trim()
|
||
const label = String(row?.dictName ?? row?.DictName ?? value).trim()
|
||
if (!value) return null
|
||
return { label: label || value, value }
|
||
})
|
||
.filter((x): x is SelectOption => !!x)
|
||
}
|
||
|
||
async function loadBasicDicts() {
|
||
dictLoading.value = true
|
||
try {
|
||
const [cargoRows, exportRows, platformRows] = await Promise.all([
|
||
getDictListAll({ dictType: 'cargo_attribute' }),
|
||
getDictListAll({ dictType: 'export_customs_mode' }),
|
||
getDictListAll({ dictType: 'e-commerce_platform' }),
|
||
])
|
||
cargoAttributeOptions.value = mapDictOptions(cargoRows as any[])
|
||
exportCustomsModeOptions.value = mapDictOptions(exportRows as any[])
|
||
ecommercePlatformOptions.value = mapDictOptions(platformRows as any[])
|
||
if (!cargoAttribute.value && cargoAttributeOptions.value[0]) {
|
||
cargoAttribute.value = normalizeCargoAttributeCode(cargoAttributeOptions.value[0].value)
|
||
} else {
|
||
cargoAttribute.value = normalizeCargoAttributeCode(cargoAttribute.value)
|
||
}
|
||
exportCustomsMode.value = resolveExportCustomsMode9610Simple()
|
||
} catch {
|
||
cargoAttributeOptions.value = [
|
||
{ label: '普货', value: 'general' },
|
||
{ label: '带电', value: 'electronics' },
|
||
{ label: '敏感', value: 'sensitive' },
|
||
]
|
||
exportCustomsModeOptions.value = [
|
||
{ label: '9610 简易申报', value: SAMPLE_EXPORT_CUSTOMS_MODE_CODE },
|
||
{ label: '9610 汇总申报', value: EXPORT_CUSTOMS_MODE_9610_SUMMARY },
|
||
]
|
||
} finally {
|
||
dictLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadExportQualifications() {
|
||
const customerId = resolveCandidatesCustomerId()
|
||
try {
|
||
const res = await getExportQualificationList({
|
||
page: 1,
|
||
pageSize: 100,
|
||
customerId,
|
||
})
|
||
exportQualificationOptions.value = (res.items || [])
|
||
.map((item: Record<string, unknown>) => {
|
||
const id = Number(pickField(item, 'id', 'Id') ?? 0)
|
||
if (!id) return null
|
||
const companyName = String(pickField(item, 'companyName', 'CompanyName') ?? '').trim()
|
||
const creditCode = String(pickField(item, 'socialCreditCode', 'SocialCreditCode') ?? '').trim()
|
||
const isDefault = Boolean(pickField(item, 'isDefault', 'IsDefault'))
|
||
const base = companyName || creditCode || `#${id}`
|
||
return { id, label: isDefault ? `${base}(默认)` : base, isDefault }
|
||
})
|
||
.filter((x): x is QualOption => !!x)
|
||
if (!exportQualificationId.value) {
|
||
const def =
|
||
exportQualificationOptions.value.find((x) => x.isDefault) || exportQualificationOptions.value[0]
|
||
exportQualificationId.value = def?.id
|
||
}
|
||
} catch {
|
||
exportQualificationOptions.value = []
|
||
}
|
||
}
|
||
|
||
async function loadPickupAddresses() {
|
||
const customerId = resolveCandidatesCustomerId()
|
||
try {
|
||
const res = await getSenderAddressList({
|
||
page: 1,
|
||
pageSize: 200,
|
||
addressType: 'pickup',
|
||
customerId,
|
||
})
|
||
pickupAddressOptions.value = (res.items || [])
|
||
.map((item: Record<string, unknown>) => {
|
||
const id = Number(pickField(item, 'id', 'Id') ?? 0)
|
||
if (!id) return null
|
||
const name = String(pickField(item, 'contactName', 'ContactName', 'name', 'Name') ?? '').trim()
|
||
const phone = String(pickField(item, 'phone', 'Phone') ?? '').trim()
|
||
const label = [name, phone].filter(Boolean).join(' · ') || `#${id}`
|
||
return { label, value: id }
|
||
})
|
||
.filter((x): x is { label: string; value: number } => !!x)
|
||
} catch {
|
||
pickupAddressOptions.value = []
|
||
}
|
||
}
|
||
|
||
function openAgreementModal() {
|
||
agreementModalVisible.value = true
|
||
}
|
||
|
||
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),
|
||
exportCustomsMode: String(exportCustomsMode.value || '').trim() || resolveExportCustomsMode9610Simple(),
|
||
exportQualificationId: needExportQualification.value ? exportQualificationId.value : undefined,
|
||
platformCode: needExportQualification.value ? platformCode.value : undefined,
|
||
inboundMethod: inboundMethod.value,
|
||
senderAddressId: inboundMethod.value === 'doorPickup' ? senderAddressId.value : undefined,
|
||
expectPickupTime: inboundMethod.value === 'doorPickup' ? expectPickupTime.value : undefined,
|
||
}
|
||
}
|
||
|
||
function close() {
|
||
emit('update:visible', false)
|
||
}
|
||
|
||
function imgSrc(url?: string | null) {
|
||
if (!url) return ''
|
||
return resolveOmsAssetUrl(url) || url
|
||
}
|
||
|
||
function mapDetail(raw: Record<string, unknown>): OrderDraft {
|
||
const row = normalizeListRow(raw)
|
||
const recipientName = String(pickField(row, 'recipientName', 'RecipientName') ?? '')
|
||
const recipientCountry = String(
|
||
pickField(row, 'recipientCountry', 'RecipientCountry') ?? pickField(row, 'orderCountry', 'OrderCountry') ?? '',
|
||
)
|
||
const recipientZipCode = String(pickField(row, 'recipientZipCode', 'RecipientZipCode') ?? '')
|
||
const recipientAddress1 = String(pickField(row, 'recipientAddress1', 'RecipientAddress1') ?? '')
|
||
const linesRaw = (pickField(row, 'lines', 'Lines') as any[]) || []
|
||
const lines = linesRaw.map((line) => {
|
||
const r = normalizeListRow(line as Record<string, unknown>)
|
||
return {
|
||
platformSku: String(pickField(r, 'platformSku', 'PlatformSku') ?? '-'),
|
||
productName: String(pickField(r, 'productName', 'ProductName') ?? '-'),
|
||
quantity: Number(pickField(r, 'quantity', 'Quantity') ?? 0),
|
||
unitPrice: String(pickField(r, 'unitPrice', 'UnitPrice') ?? '-'),
|
||
}
|
||
})
|
||
const shipLines = lines
|
||
const customerIdRaw = Number(pickField(row, 'customerId', 'CustomerId') ?? 0)
|
||
const warehouseIdRaw = Number(pickField(row, 'warehouseId', 'WarehouseId') ?? 0)
|
||
const platformOrderNo = String(pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? '')
|
||
return {
|
||
storeOrderId: Number(pickField(row, 'id', 'Id')),
|
||
customerId: Number.isFinite(customerIdRaw) && customerIdRaw > 0 ? customerIdRaw : undefined,
|
||
warehouseId: Number.isFinite(warehouseIdRaw) && warehouseIdRaw > 0 ? warehouseIdRaw : undefined,
|
||
platformOrderNo,
|
||
clientNo: platformOrderNo,
|
||
orderCountry: String(pickField(row, 'orderCountry', 'OrderCountry') ?? ''),
|
||
recipientName,
|
||
recipientPhone: String(pickField(row, 'recipientPhone', 'RecipientPhone') ?? ''),
|
||
recipientCountry,
|
||
recipientProvince: String(pickField(row, 'recipientProvince', 'RecipientProvince') ?? ''),
|
||
recipientCity: String(pickField(row, 'recipientCity', 'RecipientCity') ?? ''),
|
||
recipientZipCode,
|
||
recipientAddress1,
|
||
recipientAddress2: String(pickField(row, 'recipientAddress2', 'RecipientAddress2') ?? ''),
|
||
lines,
|
||
receiverMissing: !recipientName || !recipientCountry || !recipientZipCode || !recipientAddress1,
|
||
linesMissing: shipLines.length === 0,
|
||
bindItems: [],
|
||
}
|
||
}
|
||
|
||
/** 草稿模式 orderId=0,必须带上店铺订单所属客户,才能查到该客户库存(对齐运单绑定用订单客户)。 */
|
||
function resolveCandidatesCustomerId(): number | undefined {
|
||
const fromActive = activeDraft.value?.customerId
|
||
if (fromActive && fromActive > 0) return fromActive
|
||
const fromDrafts = drafts.value.find((d) => d.customerId && d.customerId > 0)?.customerId
|
||
if (fromDrafts && fromDrafts > 0) return fromDrafts
|
||
const fromProp = Number(props.customerId ?? 0)
|
||
return Number.isFinite(fromProp) && fromProp > 0 ? fromProp : undefined
|
||
}
|
||
|
||
async function loadDrafts() {
|
||
loading.value = true
|
||
drafts.value = []
|
||
activeId.value = undefined
|
||
try {
|
||
const list: OrderDraft[] = []
|
||
for (const id of props.storeOrderIds) {
|
||
const detail = await getPlatformStoreOrderDetail(id, props.customerId)
|
||
list.push(mapDetail(detail as Record<string, unknown>))
|
||
}
|
||
drafts.value = list
|
||
activeId.value = list[0]?.storeOrderId
|
||
// 优先用店铺订单已绑定仓库,避免默认仓与库存仓不一致导致空列表
|
||
const orderWarehouseId = list.find((d) => d.warehouseId)?.warehouseId
|
||
if (orderWarehouseId && props.warehouseOptions.some((w) => w.value === orderWarehouseId)) {
|
||
warehouseId.value = orderWarehouseId
|
||
}
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || t('order.platformStoreOrder.message.detailFailed'), 'error')
|
||
close()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadCandidates() {
|
||
if (!isFulfill.value || !warehouseId.value) {
|
||
inventoryCandidates.value = []
|
||
parcelCandidates.value = []
|
||
return
|
||
}
|
||
const customerId = resolveCandidatesCustomerId()
|
||
candidatesLoading.value = true
|
||
try {
|
||
const body = {
|
||
orderId: 0,
|
||
warehouseId: warehouseId.value,
|
||
customerId,
|
||
keyword: keyword.value || undefined,
|
||
page: 1,
|
||
pageSize: 50,
|
||
}
|
||
if (leftTab.value === 'inventory') {
|
||
const res = await postOrderStockBindInventoryCandidates(body)
|
||
inventoryCandidates.value = res.items || []
|
||
} else {
|
||
const res = await postOrderStockBindParcelCandidates({ ...body, lifecycleStatus: 'all' })
|
||
parcelCandidates.value = res.items || []
|
||
}
|
||
} catch (e: any) {
|
||
inventoryCandidates.value = []
|
||
parcelCandidates.value = []
|
||
$baseMessage(e?.message || t('order.myOrders.stockBind.loadFailed'), 'error')
|
||
} finally {
|
||
candidatesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function scheduleSearch() {
|
||
if (searchTimer) clearTimeout(searchTimer)
|
||
searchTimer = setTimeout(() => void loadCandidates(), 300)
|
||
}
|
||
|
||
function selectOrder(id: number) {
|
||
activeId.value = id
|
||
if (isFulfill.value) void loadCandidates()
|
||
}
|
||
|
||
function bindKey(item: {
|
||
bindType: string
|
||
skuId?: number
|
||
inventoryId?: number
|
||
parcelItemId?: number
|
||
}) {
|
||
if (item.bindType === 'parcelItem') return `parcelItem:${item.parcelItemId}`
|
||
return `inventory:${item.skuId}:${item.inventoryId ?? 0}`
|
||
}
|
||
|
||
function resolveBatchQty(draft: OrderDraft) {
|
||
if (batchMatchDeclaration.value) {
|
||
const qty = draftOrderQty(draft)
|
||
return qty > 0 ? qty : 1
|
||
}
|
||
return Math.max(1, Number(batchQtyPerOrder.value) || 1)
|
||
}
|
||
|
||
function cloneBindItems(items: BindDraftItem[]): BindDraftItem[] {
|
||
return items.map((item) => ({ ...item }))
|
||
}
|
||
|
||
function setInventoryOnDraft(draft: OrderDraft, row: any, qty: number) {
|
||
const skuId = Number(row.skuId ?? row.SkuId)
|
||
const key = bindKey({ bindType: 'inventory', skuId })
|
||
const available = Number(row.availableQty ?? row.AvailableQty ?? 0)
|
||
const exists = draft.bindItems.find((x) => x.key === key)
|
||
const line: BindDraftItem = {
|
||
key,
|
||
bindType: 'inventory',
|
||
skuId,
|
||
bindQty: qty,
|
||
skuCode: String(row.skuCode ?? row.SkuCode ?? ''),
|
||
skuName: String(row.skuName ?? row.SkuName ?? ''),
|
||
availableQty: available,
|
||
imageUrl: row.skuImageUrl ?? row.SkuImageUrl,
|
||
}
|
||
if (exists) {
|
||
Object.assign(exists, line)
|
||
} else {
|
||
draft.bindItems.push(line)
|
||
}
|
||
}
|
||
|
||
function setParcelItemOnDraft(draft: OrderDraft, parcel: any, item: any, qty: number) {
|
||
const parcelItemId = Number(item.parcelItemId ?? item.ParcelItemId)
|
||
const key = bindKey({ bindType: 'parcelItem', parcelItemId })
|
||
const available = Number(item.availableQty ?? item.AvailableQty ?? 0)
|
||
draft.bindItems = draft.bindItems.filter((x) => x.bindType !== 'parcelItem')
|
||
draft.bindItems.push({
|
||
key,
|
||
bindType: 'parcelItem',
|
||
parcelId: Number(parcel.parcelId ?? parcel.ParcelId ?? parcel.id ?? parcel.Id),
|
||
parcelItemId,
|
||
skuId: item.skuId ?? item.SkuId,
|
||
bindQty: qty,
|
||
skuCode: String(item.skuCode ?? item.SkuCode ?? ''),
|
||
productName: String(item.productName ?? item.ProductName ?? ''),
|
||
trackingNo: String(parcel.trackingNo ?? parcel.TrackingNo ?? ''),
|
||
availableQty: available,
|
||
imageUrl: item.imageUrl ?? item.ImageUrl,
|
||
})
|
||
}
|
||
|
||
function copyActiveToAll() {
|
||
const source = activeDraft.value
|
||
if (!source || source.receiverMissing || source.linesMissing) return
|
||
if (!source.bindItems.length) {
|
||
$baseMessage(t('order.myOrders.stockBind.copyEmpty'), 'warning')
|
||
return
|
||
}
|
||
const targets = assignableDrafts.value.filter((d) => d.storeOrderId !== source.storeOrderId)
|
||
if (!targets.length) {
|
||
$baseMessage(t('order.myOrders.stockBind.copyNoTarget'), 'warning')
|
||
return
|
||
}
|
||
Modal.confirm({
|
||
title: t('order.myOrders.stockBind.copyToAllTitle'),
|
||
content: t('order.myOrders.stockBind.copyConfirm', { count: targets.length }),
|
||
onOk: () => {
|
||
for (const draft of targets) {
|
||
draft.bindItems = cloneBindItems(source.bindItems)
|
||
}
|
||
$baseMessage(t('order.myOrders.stockBind.copySuccess', { count: targets.length }), 'success')
|
||
},
|
||
})
|
||
}
|
||
|
||
function distributeParcelToOrders(parcel: any) {
|
||
const editable = assignableDrafts.value
|
||
const items = (parcel.items || parcel.Items || []).filter(
|
||
(x: any) => Number(x.availableQty ?? x.AvailableQty ?? 0) > 0,
|
||
)
|
||
if (!editable.length) return
|
||
if (!items.length) {
|
||
$baseMessage(t('order.myOrders.stockBind.parcelDistributeEmpty'), 'warning')
|
||
return
|
||
}
|
||
const count = Math.min(editable.length, items.length)
|
||
if (items.length < editable.length) {
|
||
$baseMessage(
|
||
t('order.myOrders.stockBind.parcelDistributePartial', {
|
||
orders: editable.length,
|
||
items: items.length,
|
||
}),
|
||
'warning',
|
||
)
|
||
}
|
||
for (let i = 0; i < count; i += 1) {
|
||
setParcelItemOnDraft(editable[i], parcel, items[i], 1)
|
||
}
|
||
$baseMessage(t('order.myOrders.stockBind.parcelDistributeSuccess', { count }), 'success')
|
||
}
|
||
|
||
function addInventory(row: any) {
|
||
const editable = assignableDrafts.value
|
||
if (batchMode.value && isFulfill.value && editable.length > 1) {
|
||
const totalQty = editable.reduce((sum, draft) => sum + resolveBatchQty(draft), 0)
|
||
const available = Number(row.availableQty ?? row.AvailableQty ?? 0)
|
||
if (totalQty > available) {
|
||
$baseMessage(
|
||
t('order.myOrders.stockBind.batchInsufficient', { need: totalQty, available }),
|
||
'warning',
|
||
)
|
||
return
|
||
}
|
||
for (const draft of editable) {
|
||
setInventoryOnDraft(draft, row, resolveBatchQty(draft))
|
||
}
|
||
$baseMessage(t('order.myOrders.stockBind.batchAssignSuccess', { count: editable.length }), 'success')
|
||
return
|
||
}
|
||
|
||
const draft = activeDraft.value
|
||
if (!draft) return
|
||
const skuId = Number(row.skuId ?? row.SkuId)
|
||
const key = bindKey({ bindType: 'inventory', skuId })
|
||
const exist = draft.bindItems.find((x) => x.key === key)
|
||
const available = Number(row.availableQty ?? row.AvailableQty ?? 0)
|
||
if (exist) {
|
||
if (exist.bindQty < available) exist.bindQty += 1
|
||
return
|
||
}
|
||
draft.bindItems.push({
|
||
key,
|
||
bindType: 'inventory',
|
||
skuId,
|
||
bindQty: 1,
|
||
skuCode: String(row.skuCode ?? row.SkuCode ?? ''),
|
||
skuName: String(row.skuName ?? row.SkuName ?? ''),
|
||
availableQty: available,
|
||
imageUrl: row.skuImageUrl ?? row.SkuImageUrl,
|
||
})
|
||
}
|
||
|
||
function addParcelItem(parcel: any, item: any) {
|
||
if (batchMode.value && isFulfill.value && assignableDrafts.value.length > 1) {
|
||
$baseMessage(t('order.myOrders.stockBind.parcelBatchHint'), 'info')
|
||
return
|
||
}
|
||
|
||
const draft = activeDraft.value
|
||
if (!draft) return
|
||
const parcelItemId = Number(item.parcelItemId ?? item.ParcelItemId)
|
||
const key = bindKey({ bindType: 'parcelItem', parcelItemId })
|
||
const exist = draft.bindItems.find((x) => x.key === key)
|
||
const available = Number(item.availableQty ?? item.AvailableQty ?? 0)
|
||
if (exist) {
|
||
if (exist.bindQty < available) exist.bindQty += 1
|
||
return
|
||
}
|
||
draft.bindItems.push({
|
||
key,
|
||
bindType: 'parcelItem',
|
||
parcelId: Number(parcel.parcelId ?? parcel.ParcelId ?? parcel.id ?? parcel.Id),
|
||
parcelItemId,
|
||
skuId: item.skuId ?? item.SkuId,
|
||
bindQty: 1,
|
||
skuCode: String(item.skuCode ?? item.SkuCode ?? ''),
|
||
productName: String(item.productName ?? item.ProductName ?? ''),
|
||
trackingNo: String(parcel.trackingNo ?? parcel.TrackingNo ?? ''),
|
||
availableQty: available,
|
||
imageUrl: item.imageUrl ?? item.ImageUrl,
|
||
})
|
||
}
|
||
|
||
function changeQty(item: BindDraftItem, delta: number) {
|
||
const next = item.bindQty + delta
|
||
if (next < 1) return
|
||
if (item.availableQty != null && next > item.availableQty) return
|
||
item.bindQty = next
|
||
}
|
||
|
||
function removeBind(key: string) {
|
||
const draft = activeDraft.value
|
||
if (!draft) return
|
||
draft.bindItems = draft.bindItems.filter((x) => x.key !== key)
|
||
}
|
||
|
||
function clearBindsOnWarehouseChange() {
|
||
drafts.value.forEach((d) => {
|
||
d.bindItems = []
|
||
})
|
||
}
|
||
|
||
function buildBindPayload(items: BindDraftItem[]) {
|
||
return items.map((x) => ({
|
||
bindType: x.bindType,
|
||
skuId: x.skuId,
|
||
inventoryId: x.inventoryId,
|
||
parcelId: x.parcelId,
|
||
parcelItemId: x.parcelItemId,
|
||
bindQty: x.bindQty,
|
||
}))
|
||
}
|
||
|
||
async function handleSubmit(): Promise<boolean> {
|
||
if (!canSubmit.value) {
|
||
if (!agreementChecked.value) {
|
||
$baseMessage(t('order.placeOrder.agreement.required'), 'warning')
|
||
return false
|
||
}
|
||
if (!productCode.value) {
|
||
$baseMessage(
|
||
isFulfillWms.value
|
||
? t('order.platformStoreOrder.createFulfillWms.productRequired')
|
||
: t('order.platformStoreOrder.createLogistics.productRequired'),
|
||
'warning',
|
||
)
|
||
return false
|
||
}
|
||
if (!warehouseId.value) {
|
||
$baseMessage(t('order.platformStoreOrder.message.warehouseRequired'), 'warning')
|
||
return false
|
||
}
|
||
if (!basicInfoReady.value) {
|
||
$baseMessage(t('order.platformStoreOrder.createLogistics.basicRequired'), 'warning')
|
||
return false
|
||
}
|
||
const bad = drafts.value.find(
|
||
(d) => d.receiverMissing || d.linesMissing || (isFulfill.value && d.bindItems.length === 0),
|
||
)
|
||
if (bad) {
|
||
activeId.value = bad.storeOrderId
|
||
$baseMessage(t('order.platformStoreOrder.createLogistics.fixIncomplete'), 'warning')
|
||
}
|
||
return false
|
||
}
|
||
|
||
const confirmContent = isFulfillWms.value
|
||
? t('order.platformStoreOrder.createFulfillWms.confirm', {
|
||
count: drafts.value.length,
|
||
product: productLabel.value,
|
||
warehouse: warehouseLabel.value,
|
||
})
|
||
: isFulfill.value
|
||
? t('order.platformStoreOrder.createFulfill.confirm', {
|
||
count: drafts.value.length,
|
||
product: productLabel.value,
|
||
warehouse: warehouseLabel.value,
|
||
})
|
||
: t('order.platformStoreOrder.createLogistics.confirm', {
|
||
count: drafts.value.length,
|
||
product: productLabel.value,
|
||
})
|
||
|
||
const confirmed = await new Promise<boolean>((resolve) => {
|
||
Modal.confirm({
|
||
title: title.value,
|
||
content: confirmContent,
|
||
onOk: () => resolve(true),
|
||
onCancel: () => resolve(false),
|
||
})
|
||
})
|
||
if (!confirmed) return false
|
||
|
||
submitting.value = true
|
||
try {
|
||
const items = drafts.value.map((d) => ({
|
||
storeOrderId: d.storeOrderId,
|
||
clientNo: String(d.clientNo || '').trim() || undefined,
|
||
bindItems: isFulfill.value ? buildBindPayload(d.bindItems) : undefined,
|
||
}))
|
||
const body = {
|
||
productCode: productCode.value,
|
||
warehouseId: warehouseId.value,
|
||
customerId: props.customerId,
|
||
parcelBasic: buildParcelBasicPayload(),
|
||
items,
|
||
}
|
||
const result = isFulfillWms.value
|
||
? await batchCreatePlatformStoreFulfillWms(body)
|
||
: isFulfill.value
|
||
? await batchCreatePlatformStoreFulfill(body)
|
||
: await batchCreatePlatformStoreLogistics(body)
|
||
const success = Number(result?.successCount ?? result?.SuccessCount ?? 0)
|
||
const fail = Number(result?.failCount ?? result?.FailCount ?? 0)
|
||
if (fail > 0) {
|
||
const failItems = (result?.items ?? result?.Items ?? []).filter(
|
||
(x: any) => !(x.success ?? x.Success),
|
||
)
|
||
const msg = failItems
|
||
.slice(0, 3)
|
||
.map((x: any) => `${x.platformOrderNo || x.PlatformOrderNo || x.id}: ${x.message || x.Message}`)
|
||
.join('')
|
||
$baseMessage(
|
||
t('order.platformStoreOrder.createLogistics.partial', { success, fail, detail: msg }),
|
||
'warning',
|
||
)
|
||
} else {
|
||
$baseMessage(
|
||
isFulfillWms.value
|
||
? t('order.platformStoreOrder.createFulfillWms.success', { count: success })
|
||
: t('order.platformStoreOrder.createLogistics.success', { count: success }),
|
||
'success',
|
||
)
|
||
}
|
||
emit('success')
|
||
return true
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || t('order.platformStoreOrder.createLogistics.failed'), 'error')
|
||
return false
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
/** v-if 挂载时 visible 已是 true,必须 immediate,否则不会拉草稿/库存(对齐 PullOrdersModal / StockBindModal) */
|
||
watch(
|
||
() => props.visible,
|
||
async (open) => {
|
||
if (!open) return
|
||
productCode.value = props.defaultProductCode
|
||
warehouseId.value = props.defaultWarehouseId
|
||
leftTab.value = 'inventory'
|
||
keyword.value = ''
|
||
batchMode.value = props.storeOrderIds.length > 1
|
||
batchMatchDeclaration.value = false
|
||
batchQtyPerOrder.value = 1
|
||
cargoAttribute.value = 'general'
|
||
actualWeight.value = undefined
|
||
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
|
||
inboundMethod.value = 'selfShip'
|
||
senderAddressId.value = undefined
|
||
expectPickupTime.value = undefined
|
||
agreementChecked.value = false
|
||
await loadBasicDicts()
|
||
await loadDrafts()
|
||
// 实重默认按订单申报行估算(取首单)
|
||
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
|
||
}
|
||
await loadExportQualifications()
|
||
await loadPickupAddresses()
|
||
if (isFulfill.value) await loadCandidates()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
watch(warehouseId, () => {
|
||
if (!props.visible || !isFulfill.value) return
|
||
clearBindsOnWarehouseChange()
|
||
void loadCandidates()
|
||
})
|
||
|
||
watch(leftTab, () => {
|
||
if (!props.visible || !isFulfill.value) return
|
||
void loadCandidates()
|
||
})
|
||
|
||
watch(needExportQualification, (need) => {
|
||
if (!props.visible || !need) return
|
||
void loadExportQualifications()
|
||
})
|
||
|
||
watch(inboundMethod, (method) => {
|
||
if (!props.visible || method !== 'doorPickup') return
|
||
void loadPickupAddresses()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<a-modal
|
||
v-model:visible="modalVisible"
|
||
:title="title"
|
||
:width="isFulfill ? '1360px' : '1080px'"
|
||
:top="isFulfill ? '20px' : '6vh'"
|
||
:ok-loading="submitting"
|
||
:ok-text="
|
||
isFulfillWms
|
||
? t('order.platformStoreOrder.createFulfillWms.submit')
|
||
: isFulfill
|
||
? t('order.platformStoreOrder.createFulfill.submit')
|
||
: t('order.platformStoreOrder.createLogistics.submit')
|
||
"
|
||
:ok-button-props="{ disabled: !canSubmit || loading }"
|
||
unmount-on-close
|
||
:body-style="{
|
||
padding: '12px 16px',
|
||
maxHeight: isFulfill ? 'calc(100vh - 140px)' : 'calc(100vh - 160px)',
|
||
overflow: isFulfill ? 'hidden' : 'auto',
|
||
}"
|
||
:on-before-ok="handleSubmit"
|
||
@cancel="close"
|
||
>
|
||
<div v-if="loading" class="psc-loading">{{ t('common.loading') }}</div>
|
||
<div v-else class="psc-root" :class="{ 'is-fulfill': isFulfill }">
|
||
<p class="psc-tip">
|
||
{{
|
||
isFulfillWms
|
||
? t('order.platformStoreOrder.createFulfillWms.tip')
|
||
: isFulfill
|
||
? t('order.platformStoreOrder.createFulfill.tip')
|
||
: t('order.platformStoreOrder.createLogistics.tip')
|
||
}}
|
||
</p>
|
||
|
||
<section class="psc-basic">
|
||
<header class="psc-basic__head">
|
||
<strong>{{ t('order.placeOrder.single.stepBasic') }}</strong>
|
||
<span class="psc-config__meta">
|
||
{{
|
||
t('order.platformStoreOrder.createLogistics.readyMeta', {
|
||
total: drafts.length,
|
||
ready: readyCount,
|
||
})
|
||
}}
|
||
</span>
|
||
</header>
|
||
<div class="psc-basic__grid">
|
||
<div class="psc-basic__item">
|
||
<span class="psc-basic__label">
|
||
{{
|
||
isFulfillWms
|
||
? t('order.platformStoreOrder.createFulfillWms.product')
|
||
: t('order.platformStoreOrder.createLogistics.product')
|
||
}}
|
||
<em>*</em>
|
||
</span>
|
||
<a-select
|
||
v-model="productCode"
|
||
allow-search
|
||
:options="productOptions"
|
||
:loading="dictLoading"
|
||
:placeholder="t('order.platformStoreOrder.form.product')"
|
||
/>
|
||
</div>
|
||
<div class="psc-basic__item">
|
||
<span class="psc-basic__label">客户单号 <em>*</em></span>
|
||
<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-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>
|
||
<a-select
|
||
v-model="warehouseId"
|
||
allow-search
|
||
:options="warehouseOptions"
|
||
:placeholder="t('warehouse.common.warehouse')"
|
||
/>
|
||
</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>
|
||
<a-select
|
||
v-model="exportCustomsMode"
|
||
allow-search
|
||
:options="exportCustomsModeOptions"
|
||
:loading="dictLoading"
|
||
placeholder="请选择"
|
||
/>
|
||
</div>
|
||
<div v-if="needExportQualification" class="psc-basic__item">
|
||
<span class="psc-basic__label">出口资质备案 <em>*</em></span>
|
||
<a-select
|
||
v-model="exportQualificationId"
|
||
allow-search
|
||
:options="exportQualificationOptions.map((x) => ({ label: x.label, value: x.id }))"
|
||
placeholder="请选择"
|
||
/>
|
||
</div>
|
||
<div v-if="needExportQualification" class="psc-basic__item">
|
||
<span class="psc-basic__label">电商平台 <em>*</em></span>
|
||
<a-select
|
||
v-model="platformCode"
|
||
allow-search
|
||
:options="ecommercePlatformOptions"
|
||
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-radio value="selfShip">自寄到仓</a-radio>
|
||
<a-radio value="doorPickup">上门揽收</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<template v-if="inboundMethod === 'doorPickup'">
|
||
<div class="psc-basic__item">
|
||
<span class="psc-basic__label">揽收地址 <em>*</em></span>
|
||
<a-select
|
||
v-model="senderAddressId"
|
||
allow-search
|
||
:options="pickupAddressOptions"
|
||
placeholder="请选择揽收地址"
|
||
/>
|
||
</div>
|
||
<div class="psc-basic__item">
|
||
<span class="psc-basic__label">期望揽收时间 <em>*</em></span>
|
||
<a-date-picker
|
||
v-model="expectPickupTime"
|
||
show-time
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
style="width: 100%"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="psc-agreement">
|
||
<a-checkbox v-model="agreementChecked" />
|
||
<span class="psc-agreement__link" @click="openAgreementModal">
|
||
{{ t('order.placeOrder.agreement.label') }}
|
||
</span>
|
||
</div>
|
||
</section>
|
||
|
||
<div v-if="showBatchBar" class="psc-batch-bar">
|
||
<div class="psc-batch-bar__left">
|
||
<a-switch v-model="batchMode" size="small" />
|
||
<span class="psc-batch-bar__label">{{ t('order.myOrders.stockBind.batchMode') }}</span>
|
||
<span class="psc-batch-bar__hint">{{ t('order.myOrders.stockBind.batchModeHint') }}</span>
|
||
</div>
|
||
<div class="psc-batch-bar__opts">
|
||
<span class="psc-batch-bar__opt-label">{{ t('order.myOrders.stockBind.batchQtyPerOrder') }}</span>
|
||
<a-input-number
|
||
v-model="batchQtyPerOrder"
|
||
:min="1"
|
||
:max="9999"
|
||
size="small"
|
||
:disabled="batchMatchDeclaration"
|
||
style="width: 88px"
|
||
/>
|
||
<a-checkbox v-model="batchMatchDeclaration">
|
||
{{ t('order.platformStoreOrder.createFulfill.batchMatchOrderQty') }}
|
||
</a-checkbox>
|
||
</div>
|
||
<a-button
|
||
size="small"
|
||
type="outline"
|
||
:disabled="!activeDraft?.bindItems.length"
|
||
@click="copyActiveToAll"
|
||
>
|
||
{{
|
||
t('order.myOrders.stockBind.copyToAll', {
|
||
count: Math.max(assignableDrafts.length - 1, 0),
|
||
})
|
||
}}
|
||
</a-button>
|
||
</div>
|
||
|
||
<div class="psc-layout" :class="{ 'is-fulfill': isFulfill }">
|
||
<aside class="psc-list">
|
||
<div class="psc-list__head">{{ t('order.platformStoreOrder.createLogistics.orderList') }}</div>
|
||
<button
|
||
v-for="d in drafts"
|
||
:key="d.storeOrderId"
|
||
type="button"
|
||
class="psc-list__item"
|
||
:class="{
|
||
active: d.storeOrderId === activeId,
|
||
danger: d.receiverMissing || d.linesMissing || (isFulfill && !d.bindItems.length),
|
||
}"
|
||
@click="selectOrder(d.storeOrderId)"
|
||
>
|
||
<div class="psc-list__no">{{ d.platformOrderNo }}</div>
|
||
<div class="psc-list__sub">
|
||
<span>{{ d.recipientCountry || d.orderCountry || '-' }}</span>
|
||
<span>{{ d.recipientName || '-' }}</span>
|
||
</div>
|
||
<div v-if="isFulfill" class="psc-list__qty">
|
||
<span>{{ t('order.platformStoreOrder.createFulfill.orderQtyShort', { n: draftOrderQty(d) }) }}</span>
|
||
<span class="psc-list__qty-sep">/</span>
|
||
<span :class="{ warn: d.bindItems.length > 0 && draftOrderQty(d) !== draftBindQty(d) }">
|
||
{{ t('order.platformStoreOrder.createFulfill.bindQtyShort', { n: draftBindQty(d) }) }}
|
||
</span>
|
||
</div>
|
||
<div class="psc-list__tags">
|
||
<a-tag v-if="d.receiverMissing" size="small" color="red">
|
||
{{ t('order.platformStoreOrder.createLogistics.tagReceiver') }}
|
||
</a-tag>
|
||
<a-tag v-else-if="d.linesMissing" size="small" color="red">
|
||
{{ t('order.platformStoreOrder.createLogistics.tagLines') }}
|
||
</a-tag>
|
||
<a-tag v-else-if="isFulfill && !d.bindItems.length" size="small" color="orangered">
|
||
{{ t('order.platformStoreOrder.createLogistics.tagUnbound') }}
|
||
</a-tag>
|
||
<a-tag
|
||
v-else-if="isFulfill && draftOrderQty(d) !== draftBindQty(d)"
|
||
size="small"
|
||
color="orangered"
|
||
>
|
||
{{ t('order.platformStoreOrder.createFulfill.tagQtyDiff') }}
|
||
</a-tag>
|
||
<a-tag v-else-if="isFulfill" size="small" color="green">
|
||
{{ t('order.platformStoreOrder.createFulfill.tagQtyOk') }}
|
||
</a-tag>
|
||
<a-tag v-else size="small" color="green">
|
||
{{ t('order.platformStoreOrder.createLogistics.tagOk') }}
|
||
</a-tag>
|
||
</div>
|
||
</button>
|
||
</aside>
|
||
|
||
<section v-if="activeDraft" class="psc-detail">
|
||
<!-- 仅物流:收件?+ 商品纵向校对 -->
|
||
<template v-if="!isFulfill">
|
||
<div class="psc-section">
|
||
<h4>{{ t('order.platformStoreOrder.createLogistics.receiverTitle') }}</h4>
|
||
<div class="psc-receiver" :class="{ missing: activeDraft.receiverMissing }">
|
||
<div><label>{{ t('order.platformStoreOrder.recipient.name') }}</label>{{ activeDraft.recipientName || '-' }}</div>
|
||
<div><label>{{ t('order.platformStoreOrder.recipient.phone') }}</label>{{ activeDraft.recipientPhone || '-' }}</div>
|
||
<div><label>{{ t('order.platformStoreOrder.recipient.destCountry') }}</label>{{ activeDraft.recipientCountry || '-' }}</div>
|
||
<div><label>{{ t('order.platformStoreOrder.recipient.zipCode') }}</label>{{ activeDraft.recipientZipCode || '-' }}</div>
|
||
<div class="full">
|
||
<label>{{ t('order.platformStoreOrder.recipient.address1') }}</label>
|
||
{{
|
||
[activeDraft.recipientProvince, activeDraft.recipientCity, activeDraft.recipientAddress1, activeDraft.recipientAddress2]
|
||
.filter(Boolean)
|
||
.join(' ') || '-'
|
||
}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="psc-section">
|
||
<h4>{{ t('order.platformStoreOrder.createLogistics.linesTitle') }}</h4>
|
||
<a-table
|
||
:data="activeShipLines"
|
||
:pagination="false"
|
||
size="small"
|
||
row-key="platformSku"
|
||
>
|
||
<template #columns>
|
||
<a-table-column :title="t('order.platformStoreOrder.line.sku')" data-index="platformSku" :width="120" />
|
||
<a-table-column :title="t('order.platformStoreOrder.line.name')" data-index="productName" />
|
||
<a-table-column :title="t('order.platformStoreOrder.line.qty')" data-index="quantity" :width="70" />
|
||
<a-table-column :title="t('order.platformStoreOrder.line.price')" data-index="unitPrice" :width="90" />
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 物流&代发:订单商已绑资源 对照校对 -->
|
||
<template v-else>
|
||
<div class="psc-ship-strip" :class="{ missing: activeDraft.receiverMissing }">
|
||
<strong>{{ activeDraft.platformOrderNo }}</strong>
|
||
<span>{{ activeDraft.recipientName || '-' }}</span>
|
||
<span>{{ activeDraft.recipientPhone || '-' }}</span>
|
||
<span class="psc-ship-strip__addr">
|
||
{{
|
||
[activeDraft.recipientCountry, activeDraft.recipientProvince, activeDraft.recipientCity, activeDraft.recipientAddress1]
|
||
.filter(Boolean)
|
||
.join(' ') || '-'
|
||
}}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="psc-compare-bar" :class="{ mismatch: qtyMismatch, empty: !activeDraft.bindItems.length }">
|
||
<div class="psc-compare-bar__side">
|
||
<span class="psc-compare-bar__label">{{ t('order.platformStoreOrder.createFulfill.orderSide') }}</span>
|
||
<strong class="psc-compare-bar__num">{{ orderQtyTotal }}</strong>
|
||
<span class="psc-compare-bar__unit">{{ t('order.platformStoreOrder.createFulfill.pcs') }}</span>
|
||
<em>{{ t('order.platformStoreOrder.createFulfill.lineCount', { n: activeShipLines.length }) }}</em>
|
||
</div>
|
||
<div class="psc-compare-bar__vs">vs</div>
|
||
<div class="psc-compare-bar__side">
|
||
<span class="psc-compare-bar__label">{{ t('order.platformStoreOrder.createFulfill.bindSide') }}</span>
|
||
<strong class="psc-compare-bar__num" :class="{ warn: qtyMismatch }">{{ bindQtyTotal }}</strong>
|
||
<span class="psc-compare-bar__unit">{{ t('order.platformStoreOrder.createFulfill.pcs') }}</span>
|
||
<em>{{ t('order.platformStoreOrder.createFulfill.bindCount', { n: activeDraft.bindItems.length }) }}</em>
|
||
</div>
|
||
<div v-if="!activeDraft.bindItems.length" class="psc-compare-bar__msg">
|
||
{{ t('order.platformStoreOrder.createFulfill.compareNeedBind') }}
|
||
</div>
|
||
<div v-else-if="qtyMismatch" class="psc-compare-bar__msg warn">
|
||
{{ t('order.platformStoreOrder.createFulfill.compareQtyDiff', { order: orderQtyTotal, bind: bindQtyTotal }) }}
|
||
</div>
|
||
<div v-else class="psc-compare-bar__msg ok">
|
||
{{ t('order.platformStoreOrder.createFulfill.compareQtyOk') }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="psc-compare">
|
||
<div class="psc-compare__col psc-compare__col--order">
|
||
<div class="psc-compare__head">
|
||
<h4>{{ t('order.platformStoreOrder.createFulfill.orderGoodsTitle') }}</h4>
|
||
<span>{{ t('order.platformStoreOrder.createFulfill.orderGoodsHint') }}</span>
|
||
</div>
|
||
<div class="psc-compare__scroll">
|
||
<div
|
||
v-for="(line, idx) in activeShipLines"
|
||
:key="`${line.platformSku}-${idx}`"
|
||
class="psc-goods-row"
|
||
>
|
||
<div class="psc-goods-row__main">
|
||
<strong>{{ line.productName || '-' }}</strong>
|
||
<span class="psc-goods-row__sku">{{ line.platformSku || '-' }}</span>
|
||
</div>
|
||
<div class="psc-goods-row__qty">
|
||
<em>{{ t('order.platformStoreOrder.createFulfill.needQty') }}</em>
|
||
<b>{{ line.quantity }}</b>
|
||
</div>
|
||
</div>
|
||
<div v-if="!activeShipLines.length" class="psc-bind__empty">
|
||
{{ t('order.platformStoreOrder.createLogistics.tagLines') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="psc-compare__col psc-compare__col--bound">
|
||
<div class="psc-compare__head">
|
||
<h4>{{ t('order.platformStoreOrder.createFulfill.boundTitle') }}</h4>
|
||
<span>{{ t('order.platformStoreOrder.createFulfill.boundHint') }}</span>
|
||
</div>
|
||
<div class="psc-compare__scroll">
|
||
<div
|
||
v-for="item in activeDraft.bindItems"
|
||
:key="item.key"
|
||
class="psc-bound-row"
|
||
>
|
||
<a-tag size="small" :color="item.bindType === 'inventory' ? 'arcoblue' : 'purple'">
|
||
{{
|
||
item.bindType === 'inventory'
|
||
? t('order.myOrders.stockBind.tabInventory')
|
||
: t('order.myOrders.stockBind.tabParcel')
|
||
}}
|
||
</a-tag>
|
||
<div class="psc-bound-row__main">
|
||
<strong>{{ item.skuCode || item.productName || item.trackingNo || '-' }}</strong>
|
||
<span>{{ item.skuName || item.productName || item.trackingNo }}</span>
|
||
</div>
|
||
<div class="psc-bound-row__ops">
|
||
<div class="psc-stepper">
|
||
<button type="button" class="psc-stepper__btn" @click="changeQty(item, -1)">-</button>
|
||
<span class="psc-stepper__val">{{ item.bindQty }}</span>
|
||
<button type="button" class="psc-stepper__btn" @click="changeQty(item, 1)">+</button>
|
||
</div>
|
||
<button type="button" class="psc-cart-row__remove" @click="removeBind(item.key)">
|
||
<IconDelete />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div v-if="!activeDraft.bindItems.length" class="psc-bind__empty">
|
||
{{ t('order.platformStoreOrder.createFulfill.bindEmpty') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="psc-pick">
|
||
<div class="psc-pick__head">
|
||
<h4>{{ t('order.platformStoreOrder.createFulfill.pickTitle') }}</h4>
|
||
<SlidingTabs
|
||
v-model="leftTab"
|
||
size="mini"
|
||
class="psc-pick__tabs"
|
||
:items="pickSourceTabs"
|
||
:aria-label="t('order.platformStoreOrder.createFulfill.pickTitle')"
|
||
/>
|
||
<a-input
|
||
v-model="keyword"
|
||
allow-clear
|
||
size="small"
|
||
class="psc-pick__search"
|
||
:disabled="!warehouseId"
|
||
:placeholder="t('order.myOrders.stockBind.searchPlaceholder')"
|
||
@input="scheduleSearch"
|
||
@press-enter="loadCandidates"
|
||
@clear="loadCandidates"
|
||
/>
|
||
</div>
|
||
<div v-if="!warehouseId" class="psc-bind__empty">
|
||
{{ t('order.platformStoreOrder.message.warehouseRequired') }}
|
||
</div>
|
||
<div v-else class="psc-pick__scroll">
|
||
<div v-if="candidatesLoading" class="psc-bind__empty">{{ t('common.loading') }}</div>
|
||
<template v-else-if="leftTab === 'inventory'">
|
||
<button
|
||
v-for="row in inventoryCandidates"
|
||
:key="row.skuId || row.SkuId"
|
||
type="button"
|
||
class="psc-cand"
|
||
@click="addInventory(row)"
|
||
>
|
||
<img v-if="imgSrc(row.skuImageUrl || row.SkuImageUrl)" :src="imgSrc(row.skuImageUrl || row.SkuImageUrl)" alt="" />
|
||
<div class="psc-cand__main">
|
||
<strong>{{ row.skuCode || row.SkuCode }}</strong>
|
||
<span>{{ row.skuName || row.SkuName }}</span>
|
||
</div>
|
||
<div class="psc-cand__meta">
|
||
<em>{{ t('order.myOrders.stockBind.available') }} {{ row.availableQty ?? row.AvailableQty ?? 0 }}</em>
|
||
<IconPlus />
|
||
</div>
|
||
</button>
|
||
<div v-if="!inventoryCandidates.length" class="psc-bind__empty">
|
||
{{ t('order.myOrders.stockBind.emptyInventory') }}
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div
|
||
v-for="parcel in parcelCandidates"
|
||
:key="parcel.parcelId || parcel.ParcelId || parcel.id"
|
||
class="psc-parcel"
|
||
>
|
||
<div class="psc-parcel__head">
|
||
<span>{{ parcel.trackingNo || parcel.TrackingNo || `#${parcel.parcelId || parcel.id}` }}</span>
|
||
<a-button
|
||
v-if="batchMode && assignableDrafts.length > 1"
|
||
size="mini"
|
||
type="outline"
|
||
class="psc-parcel__batch"
|
||
@click.stop="distributeParcelToOrders(parcel)"
|
||
>
|
||
{{
|
||
t('order.myOrders.stockBind.parcelDistributeBtn', {
|
||
count: assignableDrafts.length,
|
||
})
|
||
}}
|
||
</a-button>
|
||
</div>
|
||
<button
|
||
v-for="item in (parcel.items || parcel.Items || [])"
|
||
:key="item.parcelItemId || item.ParcelItemId"
|
||
type="button"
|
||
class="psc-cand"
|
||
@click="addParcelItem(parcel, item)"
|
||
>
|
||
<div class="psc-cand__main">
|
||
<strong>{{ item.productName || item.ProductName || item.skuCode }}</strong>
|
||
<span>{{ item.skuCode || item.SkuCode || item.attributeText || '' }}</span>
|
||
</div>
|
||
<div class="psc-cand__meta">
|
||
<em>{{ t('order.myOrders.stockBind.available') }} {{ item.availableQty ?? item.AvailableQty ?? 0 }}</em>
|
||
<IconPlus />
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<div v-if="!parcelCandidates.length" class="psc-bind__empty">
|
||
{{ t('order.myOrders.stockBind.emptyParcel') }}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</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>
|
||
|
||
<a-modal
|
||
v-model:visible="agreementModalVisible"
|
||
:title="t('order.placeOrder.agreement.label')"
|
||
:footer="false"
|
||
:width="560"
|
||
unmount-on-close
|
||
>
|
||
<pre class="psc-agreement-body">{{ AGREEMENT_TEXT }}</pre>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<style scoped lang="less">
|
||
@primary: var(--app-primary-color);
|
||
|
||
.psc-loading {
|
||
padding: 48px;
|
||
text-align: center;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.psc-root.is-fulfill {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 160px);
|
||
min-height: 620px;
|
||
max-height: calc(100vh - 160px);
|
||
}
|
||
|
||
.psc-tip {
|
||
margin: 0 0 10px;
|
||
color: var(--color-text-2);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.psc-basic {
|
||
margin-bottom: 10px;
|
||
padding: 12px;
|
||
background: var(--color-fill-2);
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.psc-basic__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
|
||
strong {
|
||
font-size: 14px;
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
.psc-basic__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 10px 12px;
|
||
}
|
||
|
||
.psc-basic__item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.psc-basic__label {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
|
||
em {
|
||
color: #f53f3f;
|
||
font-style: normal;
|
||
margin-left: 2px;
|
||
}
|
||
}
|
||
|
||
.psc-basic__item--dims,
|
||
.psc-basic__item--inbound {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.psc-dims {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
.arco-input-number {
|
||
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);
|
||
}
|
||
|
||
.psc-agreement__link {
|
||
color: var(--app-primary-color);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
user-select: none;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.psc-agreement-body {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
color: var(--color-text-2);
|
||
max-height: 50vh;
|
||
overflow: auto;
|
||
}
|
||
|
||
.psc-config__meta {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.psc-batch-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10px 16px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid color-mix(in srgb, @primary 22%, var(--color-border-2));
|
||
background: color-mix(in srgb, @primary 6%, var(--app-card-bg));
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.psc-batch-bar__left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.psc-batch-bar__label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.psc-batch-bar__hint {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.psc-batch-bar__opts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 12px;
|
||
flex: 1;
|
||
}
|
||
|
||
.psc-batch-bar__opt-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.psc-layout {
|
||
display: grid;
|
||
grid-template-columns: 240px 1fr;
|
||
gap: 12px;
|
||
min-height: 420px;
|
||
max-height: 62vh;
|
||
}
|
||
|
||
.psc-layout.is-fulfill {
|
||
flex: 1;
|
||
min-height: 0;
|
||
max-height: none;
|
||
height: auto;
|
||
grid-template-columns: 220px 1fr;
|
||
}
|
||
|
||
.psc-list {
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
overflow: auto;
|
||
background: var(--app-card-bg);
|
||
min-height: 0;
|
||
}
|
||
|
||
.psc-list__head {
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
background: var(--color-fill-1);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
.psc-list__item {
|
||
display: block;
|
||
width: 100%;
|
||
text-align: left;
|
||
border: none;
|
||
border-left: 3px solid transparent;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
background: var(--app-card-bg);
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: var(--color-fill-2);
|
||
}
|
||
|
||
&.active {
|
||
border-left-color: @primary;
|
||
background: color-mix(in srgb, @primary 8%, var(--app-card-bg));
|
||
}
|
||
|
||
&.danger:not(.active) {
|
||
background: #fff7f7;
|
||
}
|
||
}
|
||
|
||
.psc-list__no {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.psc-list__sub {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.psc-list__tags {
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.psc-list__qty {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
.warn {
|
||
color: #f77234;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.psc-list__qty-sep {
|
||
margin: 0 4px;
|
||
color: #c9cdd4;
|
||
}
|
||
|
||
.psc-detail {
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
overflow: auto;
|
||
background: var(--app-card-bg);
|
||
min-height: 0;
|
||
}
|
||
|
||
.psc-layout.is-fulfill .psc-detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
}
|
||
|
||
.psc-section {
|
||
& + & {
|
||
margin-top: 16px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--color-border-2);
|
||
}
|
||
|
||
h4 {
|
||
margin: 0 0 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
.psc-receiver {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px 12px;
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
|
||
&.missing {
|
||
background: #fff7f7;
|
||
border: 1px solid #ffece8;
|
||
border-radius: 4px;
|
||
padding: 8px;
|
||
}
|
||
|
||
label {
|
||
display: inline-block;
|
||
min-width: 48px;
|
||
color: var(--color-text-3);
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
/* ── 代发:收件摘?── */
|
||
.psc-ship-strip {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 14px;
|
||
padding: 8px 12px;
|
||
background: var(--color-fill-1);
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
|
||
strong {
|
||
color: var(--color-text-1);
|
||
font-size: 13px;
|
||
}
|
||
|
||
&.missing {
|
||
background: #fff7f7;
|
||
}
|
||
|
||
&__addr {
|
||
flex: 1;
|
||
min-width: 180px;
|
||
color: var(--color-text-3);
|
||
}
|
||
}
|
||
|
||
/* ── 代发:数量对照条 ── */
|
||
.psc-compare-bar {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 16px;
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
background: #f2f9f9;
|
||
|
||
&.mismatch {
|
||
background: #fff7e8;
|
||
}
|
||
|
||
&.empty {
|
||
background: #fff7f7;
|
||
}
|
||
|
||
&__side {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
}
|
||
|
||
&__label {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
&__num {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: var(--color-text-1);
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1;
|
||
|
||
&.warn {
|
||
color: #f77234;
|
||
}
|
||
}
|
||
|
||
&__unit {
|
||
font-size: 12px;
|
||
color: var(--color-text-2);
|
||
}
|
||
|
||
em {
|
||
font-style: normal;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
&__vs {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #c9cdd4;
|
||
}
|
||
|
||
&__msg {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: #00b42a;
|
||
|
||
&.warn {
|
||
color: #f77234;
|
||
}
|
||
|
||
&.ok {
|
||
color: #00b42a;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ── 代发:订单商已绑 对照 ── */
|
||
.psc-compare {
|
||
flex: 1.2;
|
||
min-height: 220px;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.psc-compare__col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
|
||
&--order {
|
||
border-right: 1px solid var(--color-border-2);
|
||
background: var(--app-card-bg);
|
||
}
|
||
|
||
&--bound {
|
||
background: #fcfcfd;
|
||
}
|
||
}
|
||
|
||
.psc-compare__head {
|
||
flex-shrink: 0;
|
||
padding: 10px 12px 8px;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
|
||
h4 {
|
||
margin: 0 0 2px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
}
|
||
|
||
.psc-compare__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
padding: 6px 8px;
|
||
}
|
||
|
||
.psc-goods-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--color-border-2);
|
||
margin-bottom: 6px;
|
||
background: var(--app-card-bg);
|
||
}
|
||
|
||
.psc-goods-row__main {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
strong {
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.psc-goods-row__sku {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
}
|
||
|
||
.psc-goods-row__qty {
|
||
flex-shrink: 0;
|
||
text-align: right;
|
||
|
||
em {
|
||
display: block;
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
b {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: @primary;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
|
||
.psc-bound-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--color-border-2);
|
||
margin-bottom: 6px;
|
||
background: var(--app-card-bg);
|
||
}
|
||
|
||
.psc-bound-row__main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
strong {
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.psc-bound-row__ops {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ── 代发:底部挑选资?── */
|
||
.psc-pick {
|
||
flex: 0.9;
|
||
min-height: 180px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--color-fill-2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.psc-pick__head {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 12px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
background: var(--app-card-bg);
|
||
|
||
h4 {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.psc-pick__tabs {
|
||
width: fit-content;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.psc-pick__search {
|
||
width: 220px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.psc-pick__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
padding: 8px;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 8px;
|
||
align-content: start;
|
||
}
|
||
|
||
.psc-bind__empty {
|
||
padding: 28px 12px;
|
||
text-align: center;
|
||
color: var(--color-text-3);
|
||
font-size: 13px;
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.psc-cand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--color-border-2);
|
||
background: var(--app-card-bg);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
width: 100%;
|
||
|
||
&:hover {
|
||
border-color: color-mix(in srgb, @primary 45%, var(--color-border-2));
|
||
background: color-mix(in srgb, @primary 6%, var(--app-card-bg));
|
||
}
|
||
|
||
img {
|
||
width: 40px;
|
||
height: 40px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
background: var(--color-fill-2);
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.psc-cand__main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
strong {
|
||
font-size: 13px;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.psc-cand__meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
color: @primary;
|
||
|
||
em {
|
||
font-style: normal;
|
||
font-size: 11px;
|
||
color: var(--color-text-3);
|
||
}
|
||
}
|
||
|
||
.psc-parcel {
|
||
grid-column: 1 / -1;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
background: var(--app-card-bg);
|
||
padding: 8px;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||
gap: 6px;
|
||
|
||
.psc-cand {
|
||
border-color: var(--color-border-2);
|
||
}
|
||
}
|
||
|
||
.psc-parcel__head {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--color-text-2);
|
||
padding: 2px 4px 6px;
|
||
}
|
||
|
||
.psc-parcel__batch {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.psc-stepper {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border: 1px solid var(--color-border-2);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.psc-stepper__btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: none;
|
||
background: var(--color-fill-2);
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: color-mix(in srgb, @primary 10%, var(--color-fill-2));
|
||
color: @primary;
|
||
}
|
||
}
|
||
|
||
.psc-stepper__val {
|
||
min-width: 32px;
|
||
text-align: center;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.psc-cart-row__remove {
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--color-text-3);
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
|
||
&:hover {
|
||
color: #f53f3f;
|
||
}
|
||
}
|
||
|
||
.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>
|