统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
1462 lines
42 KiB
Vue
1462 lines
42 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,
|
||
batchCreatePlatformStoreLogistics,
|
||
getPlatformStoreOrderDetail,
|
||
} from '@/api/order/platformStore'
|
||
import {
|
||
postOrderStockBindInventoryCandidates,
|
||
postOrderStockBindParcelCandidates,
|
||
} from '@/api/order'
|
||
import { resolveOmsAssetUrl } from '@/api/oms/common'
|
||
import { $baseMessage } from '@/utils'
|
||
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||
|
||
export type CreateLogisticsMode = 'logistics' | 'fulfill'
|
||
|
||
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
|
||
platformOrderNo: 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
|
||
isShipExcluded: boolean
|
||
}>
|
||
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 keyword = ref('')
|
||
const inventoryCandidates = ref<any[]>([])
|
||
const parcelCandidates = ref<any[]>([])
|
||
const candidatesLoading = ref(false)
|
||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
const modalVisible = computed({
|
||
get: () => props.visible,
|
||
set: (v: boolean) => emit('update:visible', v),
|
||
})
|
||
|
||
const isFulfill = computed(() => props.mode === 'fulfill')
|
||
const title = computed(() =>
|
||
isFulfill.value
|
||
? t('order.platformStoreOrder.createFulfill.title')
|
||
: t('order.platformStoreOrder.createLogistics.title'),
|
||
)
|
||
|
||
const activeDraft = computed(() => drafts.value.find((d) => d.storeOrderId === activeId.value))
|
||
|
||
const activeShipLines = computed(() =>
|
||
(activeDraft.value?.lines || []).filter((x) => !x.isShipExcluded),
|
||
)
|
||
|
||
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 readyCount = computed(() => {
|
||
if (!isFulfill.value) {
|
||
return drafts.value.filter((d) => !d.receiverMissing && !d.linesMissing).length
|
||
}
|
||
return drafts.value.filter((d) => !d.receiverMissing && !d.linesMissing && d.bindItems.length > 0).length
|
||
})
|
||
|
||
function draftOrderQty(d: OrderDraft) {
|
||
return d.lines.filter((x) => !x.isShipExcluded).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 canSubmit = computed(() => {
|
||
if (!productCode.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 (!warehouseId.value) return false
|
||
if (drafts.value.some((d) => d.bindItems.length === 0)) return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
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') ?? '-'),
|
||
isShipExcluded: Boolean(pickField(r, 'isShipExcluded', 'IsShipExcluded')),
|
||
}
|
||
})
|
||
const shipLines = lines.filter((x) => !x.isShipExcluded)
|
||
return {
|
||
storeOrderId: Number(pickField(row, 'id', 'Id')),
|
||
platformOrderNo: String(pickField(row, 'platformOrderNo', '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: [],
|
||
}
|
||
}
|
||
|
||
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
|
||
} 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
|
||
}
|
||
candidatesLoading.value = true
|
||
try {
|
||
const body = {
|
||
orderId: 0,
|
||
warehouseId: warehouseId.value,
|
||
customerId: props.customerId,
|
||
keyword: keyword.value || undefined,
|
||
page: 1,
|
||
pageSize: 30,
|
||
}
|
||
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 {
|
||
inventoryCandidates.value = []
|
||
parcelCandidates.value = []
|
||
} finally {
|
||
candidatesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function scheduleSearch() {
|
||
if (searchTimer) clearTimeout(searchTimer)
|
||
searchTimer = setTimeout(() => void loadCandidates(), 300)
|
||
}
|
||
|
||
function selectOrder(id: number) {
|
||
activeId.value = id
|
||
}
|
||
|
||
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 addInventory(row: any) {
|
||
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) {
|
||
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 (!productCode.value) {
|
||
$baseMessage(t('order.platformStoreOrder.createLogistics.productRequired'), 'warning')
|
||
return false
|
||
}
|
||
if (isFulfill.value && !warehouseId.value) {
|
||
$baseMessage(t('order.platformStoreOrder.message.warehouseRequired'), '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 = 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,
|
||
bindItems: isFulfill.value ? buildBindPayload(d.bindItems) : undefined,
|
||
}))
|
||
const body = {
|
||
productCode: productCode.value,
|
||
warehouseId: warehouseId.value,
|
||
customerId: props.customerId,
|
||
items,
|
||
}
|
||
const result = 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(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
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.visible,
|
||
async (open) => {
|
||
if (!open) return
|
||
productCode.value = props.defaultProductCode
|
||
warehouseId.value = props.defaultWarehouseId
|
||
leftTab.value = 'inventory'
|
||
keyword.value = ''
|
||
await loadDrafts()
|
||
if (isFulfill.value) await loadCandidates()
|
||
},
|
||
)
|
||
|
||
watch(warehouseId, () => {
|
||
if (!props.visible || !isFulfill.value) return
|
||
clearBindsOnWarehouseChange()
|
||
void loadCandidates()
|
||
})
|
||
|
||
watch(leftTab, () => {
|
||
if (!props.visible || !isFulfill.value) return
|
||
void loadCandidates()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<a-modal
|
||
v-model:visible="modalVisible"
|
||
:title="title"
|
||
:width="isFulfill ? '1360px' : 960"
|
||
:top="isFulfill ? '20px' : '10vh'"
|
||
:ok-loading="submitting"
|
||
:ok-text="
|
||
isFulfill
|
||
? t('order.platformStoreOrder.createFulfill.submit')
|
||
: t('order.platformStoreOrder.createLogistics.submit')
|
||
"
|
||
:ok-button-props="{ disabled: !canSubmit || loading }"
|
||
unmount-on-close
|
||
:body-style="isFulfill ? { padding: '12px 16px', maxHeight: 'calc(100vh - 140px)', overflow: 'hidden' } : undefined"
|
||
: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">
|
||
{{
|
||
isFulfill
|
||
? t('order.platformStoreOrder.createFulfill.tip')
|
||
: t('order.platformStoreOrder.createLogistics.tip')
|
||
}}
|
||
</p>
|
||
|
||
<div class="psc-config">
|
||
<div class="psc-config__item">
|
||
<span class="psc-config__label">{{ t('order.platformStoreOrder.createLogistics.product') }}</span>
|
||
<a-select
|
||
v-model="productCode"
|
||
allow-search
|
||
:options="productOptions"
|
||
:placeholder="t('order.platformStoreOrder.form.product')"
|
||
style="width: 260px"
|
||
/>
|
||
</div>
|
||
<div class="psc-config__item">
|
||
<span class="psc-config__label">
|
||
{{ t('warehouse.common.warehouse') }}
|
||
<em v-if="isFulfill">*</em>
|
||
</span>
|
||
<a-select
|
||
v-model="warehouseId"
|
||
allow-clear
|
||
:options="warehouseOptions"
|
||
:placeholder="t('warehouse.common.warehouse')"
|
||
style="width: 200px"
|
||
/>
|
||
</div>
|
||
<div class="psc-config__meta">
|
||
{{
|
||
t('order.platformStoreOrder.createLogistics.readyMeta', {
|
||
total: drafts.length,
|
||
ready: readyCount,
|
||
})
|
||
}}
|
||
</div>
|
||
</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.country') }}</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>
|
||
<a-radio-group v-model="leftTab" type="button" size="small" :disabled="!warehouseId">
|
||
<a-radio value="inventory">{{ t('order.myOrders.stockBind.tabInventory') }}</a-radio>
|
||
<a-radio value="parcel">{{ t('order.myOrders.stockBind.tabParcel') }}</a-radio>
|
||
</a-radio-group>
|
||
<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">
|
||
{{ parcel.trackingNo || parcel.TrackingNo || `#${parcel.parcelId || parcel.id}` }}
|
||
</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>
|
||
</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-config {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px 16px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 10px;
|
||
background: var(--color-fill-2);
|
||
border-radius: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.psc-config__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.psc-config__label {
|
||
font-size: 13px;
|
||
color: var(--color-text-2);
|
||
white-space: nowrap;
|
||
|
||
em {
|
||
color: #f53f3f;
|
||
font-style: normal;
|
||
margin-left: 2px;
|
||
}
|
||
}
|
||
|
||
.psc-config__meta {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.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__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;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--color-text-2);
|
||
padding: 2px 4px 6px;
|
||
}
|
||
|
||
.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>
|