596 lines
26 KiB
Vue
596 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { MapPin, Plus, Send, Trash2 } from '@lucide/vue'
|
||
import { useAuthStore } from '@/modules/auth/store/auth'
|
||
import {
|
||
getMerchantInboundCodePolicy,
|
||
getMerchantOrderAddresses,
|
||
getMerchantPricingChannels,
|
||
type CustomerOrderAddress,
|
||
type InboundCodePolicy,
|
||
type MerchantPricingChannel,
|
||
} from '@/modules/first-leg/api/firstLeg'
|
||
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
|
||
import { useWorkspaceTabsStore } from '@/navigation/workspace-tabs/workspaceTabs'
|
||
import type { FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
|
||
import { createEmptyCargoItem } from '@/modules/first-leg/utils/firstLegOrder'
|
||
|
||
type DestinationType = 'FBA' | 'OVERSEAS' | 'PRIVATE'
|
||
|
||
interface QuickOrderProduct {
|
||
id: string
|
||
sku: string
|
||
packageCount: number
|
||
}
|
||
|
||
interface QuickOrderRow {
|
||
id: string
|
||
inboundCode: string
|
||
channelKey: string
|
||
destinationType: DestinationType
|
||
destinationWarehouseId: string
|
||
destinationWarehouseName: string
|
||
destinationCountryCode: string
|
||
amazonWarehouseCode: string
|
||
privateDestinationAddressId: string
|
||
consigneeContactName: string
|
||
consigneePhone: string
|
||
consigneeCity: string
|
||
consigneeAddress: string
|
||
consigneePostalCode: string
|
||
products: QuickOrderProduct[]
|
||
}
|
||
|
||
interface QuickChannelOption extends MerchantPricingChannel {
|
||
warehouseId: string
|
||
warehouseName: string
|
||
}
|
||
|
||
interface QuickDraftResult {
|
||
rowId: string
|
||
sku: string
|
||
draftId: string
|
||
inboundCode: string
|
||
}
|
||
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const store = useFirstLegOrdersStore()
|
||
const workspace = useWorkspaceTabsStore()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const loadError = ref('')
|
||
const inboundCodePolicy = ref<InboundCodePolicy | null>(null)
|
||
const channelOptions = ref<QuickChannelOption[]>([])
|
||
const destinationDialogOpen = ref(false)
|
||
const destinationLoading = ref(false)
|
||
const privateAddresses = ref<CustomerOrderAddress[]>([])
|
||
const privateAddressesError = ref('')
|
||
const editingDestinationRow = ref<QuickOrderRow>()
|
||
const draftResults = reactive<QuickDraftResult[]>([])
|
||
const resultDialogOpen = ref(false)
|
||
const activeBatchKey = ref('')
|
||
const destinationForm = reactive({
|
||
type: 'FBA' as DestinationType,
|
||
warehouseId: '',
|
||
privateAddressId: '',
|
||
})
|
||
let rowSequence = 0
|
||
let productSequence = 0
|
||
|
||
function createBatchKey() {
|
||
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||
return `first-leg-quick-batch-${randomPart}`
|
||
}
|
||
|
||
function createProduct(): QuickOrderProduct {
|
||
return reactive({ id: `quick-product-${++productSequence}`, sku: '', packageCount: 1 })
|
||
}
|
||
|
||
function createRow(): QuickOrderRow {
|
||
return reactive({
|
||
id: `quick-order-${++rowSequence}`,
|
||
inboundCode: '',
|
||
channelKey: '',
|
||
destinationType: 'FBA',
|
||
destinationWarehouseId: '',
|
||
destinationWarehouseName: '',
|
||
destinationCountryCode: '',
|
||
amazonWarehouseCode: '',
|
||
privateDestinationAddressId: '',
|
||
consigneeContactName: '',
|
||
consigneePhone: '',
|
||
consigneeCity: '',
|
||
consigneeAddress: '',
|
||
consigneePostalCode: '',
|
||
products: [createProduct()],
|
||
})
|
||
}
|
||
|
||
const rows = reactive<QuickOrderRow[]>([createRow()])
|
||
const hasChannels = computed(() => channelOptions.value.length > 0)
|
||
const manualInboundCodeRequired = computed(() => inboundCodePolicy.value?.mode === 'LEGACY_MANUAL')
|
||
const availableDestinationWarehouses = computed(() => destinationForm.type === 'PRIVATE'
|
||
? []
|
||
: store.warehouses.filter((warehouse) => warehouse.warehouseType === destinationForm.type))
|
||
|
||
function addOrderRow() {
|
||
rows.push(createRow())
|
||
}
|
||
|
||
function addProduct(row: QuickOrderRow) {
|
||
row.products.push(createProduct())
|
||
}
|
||
|
||
function removeProduct(row: QuickOrderRow, productIndex: number) {
|
||
if (row.products.length === 1) {
|
||
row.products[0] = createProduct()
|
||
return
|
||
}
|
||
row.products.splice(productIndex, 1)
|
||
}
|
||
|
||
function removeRow(rowIndex: number) {
|
||
if (rows.length === 1) {
|
||
rows[0] = createRow()
|
||
return
|
||
}
|
||
rows.splice(rowIndex, 1)
|
||
}
|
||
|
||
function channelKey(warehouseId: string, channelCode: string) {
|
||
return `${warehouseId}:${channelCode}`
|
||
}
|
||
|
||
function selectedChannel(row: QuickOrderRow) {
|
||
return channelOptions.value.find((channel) => channelKey(channel.warehouseId, channel.channelCode) === row.channelKey)
|
||
}
|
||
|
||
function addressLabel(address: CustomerOrderAddress) {
|
||
return `${address.addressName} · ${address.contactName} · ${address.countryCode} ${address.city} ${address.addressLine}`
|
||
}
|
||
|
||
function createConsigneeSnapshot(row: QuickOrderRow) {
|
||
return {
|
||
id: '',
|
||
code: '',
|
||
name: row.destinationWarehouseName,
|
||
role: 'CONSIGNEE' as const,
|
||
contactName: row.consigneeContactName,
|
||
phone: row.consigneePhone,
|
||
email: '',
|
||
countryRegion: row.destinationCountryCode,
|
||
province: '',
|
||
city: row.consigneeCity,
|
||
postalCode: row.consigneePostalCode,
|
||
address: row.consigneeAddress,
|
||
}
|
||
}
|
||
|
||
function hasCompletePrivateContact(row: QuickOrderRow) {
|
||
return [row.consigneeContactName, row.consigneePhone, row.consigneeCity, row.consigneeAddress]
|
||
.every((value) => value.trim())
|
||
}
|
||
|
||
function buildOrderDraft(row: QuickOrderRow, channel: QuickChannelOption): FirstLegOrderDraft {
|
||
const pricingTaxMode = channel.pricingTaxModes[0] ?? 'TAX_INCLUDED'
|
||
return {
|
||
inboundCode: row.inboundCode.trim() || undefined,
|
||
referenceNo: '',
|
||
transportMode: 'FCL',
|
||
businessType: 'EXPORT',
|
||
origin: '',
|
||
destination: row.destinationWarehouseName,
|
||
receivingWarehouseId: channel.warehouseId,
|
||
expectedDepartureDate: '',
|
||
shipperPartyId: '',
|
||
senderAddressId: '',
|
||
shipperPartySnapshot: null,
|
||
shipper: '',
|
||
consigneePartyId: '',
|
||
privateDestinationAddressId: row.destinationType === 'PRIVATE' ? row.privateDestinationAddressId : '',
|
||
consigneePartySnapshot: row.destinationType === 'PRIVATE' && hasCompletePrivateContact(row)
|
||
? createConsigneeSnapshot(row) : null,
|
||
consignee: row.destinationType === 'PRIVATE' && hasCompletePrivateContact(row)
|
||
? row.destinationWarehouseName : '',
|
||
priority: 'NORMAL',
|
||
customerRemark: '',
|
||
tradeTerm: 'FOB',
|
||
destinationCountryCode: row.destinationCountryCode,
|
||
destinationWarehouseId: row.destinationWarehouseId,
|
||
destinationWarehouseName: row.destinationWarehouseName,
|
||
pickupMethod: 'SELF_EXPRESS',
|
||
channelCode: channel.channelCode,
|
||
channelName: channel.channelName,
|
||
pricingTaxMode,
|
||
taxMode: 'BUY_DECLARATION',
|
||
customsInspection: null,
|
||
destinationType: row.destinationType,
|
||
amazonWarehouseCode: row.amazonWarehouseCode,
|
||
shipmentId: '',
|
||
items: row.products.map((product) => {
|
||
return {
|
||
...createEmptyCargoItem(`quick-cargo-${product.id}`),
|
||
sku: product.sku.trim(),
|
||
packageCount: product.packageCount,
|
||
pieces: product.packageCount,
|
||
}
|
||
}),
|
||
}
|
||
}
|
||
|
||
function destinationLabel(row: QuickOrderRow) {
|
||
if (!row.destinationWarehouseName) return '尚未绑定目的仓'
|
||
const code = row.amazonWarehouseCode || row.destinationCountryCode
|
||
return [row.destinationWarehouseName, code].filter(Boolean).join(' · ')
|
||
}
|
||
|
||
async function openDestinationDialog(row: QuickOrderRow, typeValue: string | number | boolean | undefined) {
|
||
const type = String(typeValue) as DestinationType
|
||
if (!['FBA', 'OVERSEAS', 'PRIVATE'].includes(type)) return
|
||
editingDestinationRow.value = row
|
||
Object.assign(destinationForm, {
|
||
type,
|
||
warehouseId: row.destinationType === type ? row.destinationWarehouseId : '',
|
||
privateAddressId: row.destinationType === type ? row.privateDestinationAddressId : '',
|
||
})
|
||
destinationDialogOpen.value = true
|
||
destinationLoading.value = true
|
||
try {
|
||
if (type === 'PRIVATE') {
|
||
privateAddressesError.value = ''
|
||
privateAddresses.value = await getMerchantOrderAddresses('PRIVATE_DESTINATION')
|
||
} else {
|
||
await store.loadWarehouseType(type)
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '目的仓加载失败,请稍后重试'
|
||
if (type === 'PRIVATE') {
|
||
privateAddresses.value = []
|
||
privateAddressesError.value = message
|
||
}
|
||
ElMessage.error(message)
|
||
} finally {
|
||
destinationLoading.value = false
|
||
}
|
||
}
|
||
|
||
function bindDestination() {
|
||
const row = editingDestinationRow.value
|
||
if (!row) return
|
||
if (destinationForm.type !== 'PRIVATE') {
|
||
const warehouse = availableDestinationWarehouses.value.find((item) => item.id === destinationForm.warehouseId)
|
||
if (!warehouse) {
|
||
ElMessage.warning('请选择一个目的仓')
|
||
return
|
||
}
|
||
Object.assign(row, {
|
||
destinationType: destinationForm.type,
|
||
destinationWarehouseId: warehouse.id,
|
||
destinationWarehouseName: warehouse.name,
|
||
destinationCountryCode: warehouse.countryCode,
|
||
amazonWarehouseCode: destinationForm.type === 'FBA' ? warehouse.code : '',
|
||
privateDestinationAddressId: '',
|
||
consigneeContactName: '',
|
||
consigneePhone: '',
|
||
consigneeCity: '',
|
||
consigneeAddress: '',
|
||
consigneePostalCode: '',
|
||
})
|
||
} else {
|
||
const address = privateAddresses.value.find((item) => String(item.id) === destinationForm.privateAddressId)
|
||
if (!address) {
|
||
ElMessage.warning('请选择企业已保存的私人仓地址')
|
||
return
|
||
}
|
||
Object.assign(row, {
|
||
destinationType: 'PRIVATE',
|
||
destinationWarehouseId: '',
|
||
destinationWarehouseName: address.addressName,
|
||
destinationCountryCode: address.countryCode,
|
||
amazonWarehouseCode: '',
|
||
privateDestinationAddressId: String(address.id),
|
||
consigneeContactName: address.contactName,
|
||
consigneePhone: address.phone,
|
||
consigneeCity: address.city,
|
||
consigneeAddress: address.addressLine,
|
||
consigneePostalCode: address.postalCode ?? '',
|
||
})
|
||
}
|
||
destinationDialogOpen.value = false
|
||
}
|
||
|
||
async function loadChannels() {
|
||
loading.value = true
|
||
loadError.value = ''
|
||
try {
|
||
const [, policy] = await Promise.all([
|
||
store.loadWarehouseType('DOMESTIC', true),
|
||
getMerchantInboundCodePolicy(),
|
||
])
|
||
inboundCodePolicy.value = policy
|
||
const results = await Promise.allSettled(store.warehouses
|
||
.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC')
|
||
.map(async (warehouse) => {
|
||
const channels = await getMerchantPricingChannels(warehouse.id)
|
||
return channels.map((channel) => ({ ...channel, warehouseId: warehouse.id, warehouseName: warehouse.name }))
|
||
}))
|
||
channelOptions.value = results.flatMap((result) => result.status === 'fulfilled' ? result.value : [])
|
||
if (!channelOptions.value.length) loadError.value = '当前国内收货仓暂无可用渠道,请联系平台管理员维护渠道。'
|
||
} catch (error) {
|
||
loadError.value = error instanceof Error ? error.message : '渠道加载失败,请稍后重试。'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function refreshInboundCodePolicyForWrite() {
|
||
try {
|
||
const policy = await getMerchantInboundCodePolicy()
|
||
inboundCodePolicy.value = policy
|
||
if (policy.mode === 'AUTO') {
|
||
rows.forEach((row) => { row.inboundCode = '' })
|
||
}
|
||
return true
|
||
} catch (error) {
|
||
inboundCodePolicy.value = null
|
||
ElMessage.warning(error instanceof Error ? error.message : '入仓码规则加载失败,请稍后重试')
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function submitQuickOrder() {
|
||
if (!await refreshInboundCodePolicyForWrite()) return
|
||
const missingInboundCodeIndex = manualInboundCodeRequired.value
|
||
? rows.findIndex((row) => !row.inboundCode.trim())
|
||
: -1
|
||
if (missingInboundCodeIndex >= 0) {
|
||
ElMessage.warning(`请填写第 ${missingInboundCodeIndex + 1} 行的旧系统入仓码`)
|
||
return
|
||
}
|
||
const incompleteIndex = rows.findIndex((row) => !row.channelKey || !row.destinationWarehouseName
|
||
|| (row.destinationType === 'PRIVATE' && !row.privateDestinationAddressId)
|
||
|| row.products.some((product) => !product.sku.trim() || product.packageCount < 1))
|
||
if (incompleteIndex >= 0) {
|
||
ElMessage.warning(`请完善第 ${incompleteIndex + 1} 行的渠道、目的仓、产品 SKU 和箱数`)
|
||
return
|
||
}
|
||
|
||
const unavailableIndex = rows.findIndex((row) => !selectedChannel(row))
|
||
if (unavailableIndex >= 0) {
|
||
ElMessage.warning(`第 ${unavailableIndex + 1} 行所选渠道已不可用,请重新选择`)
|
||
return
|
||
}
|
||
|
||
const drafts = rows.flatMap((row) => {
|
||
const channel = selectedChannel(row)
|
||
return channel ? [{ clientRowId: row.id, draft: buildOrderDraft(row, channel) }] : []
|
||
})
|
||
|
||
submitting.value = true
|
||
try {
|
||
const customerName = auth.user?.tenantName?.trim() || auth.profile.tenantName.trim()
|
||
if (!customerName) throw new Error('当前登录会话缺少企业名称,请重新登录后重试')
|
||
if (!activeBatchKey.value) activeBatchKey.value = createBatchKey()
|
||
const response = await store.saveDraftBatch(drafts, customerName, activeBatchKey.value)
|
||
const successes = response.items.flatMap((item): QuickDraftResult[] => {
|
||
if (!item.success || item.orderId == null) return []
|
||
const row = rows.find((candidate) => candidate.id === item.clientRowId)
|
||
if (!row) return []
|
||
return [{
|
||
rowId: row.id,
|
||
sku: row.products.map((product) => product.sku.trim()).join('、'),
|
||
draftId: String(item.orderId),
|
||
inboundCode: item.inboundCode?.trim() ?? '',
|
||
}]
|
||
})
|
||
const failures = response.items.filter((item) => !item.success || item.orderId == null)
|
||
draftResults.splice(0, draftResults.length, ...successes)
|
||
const successfulIds = new Set(successes.map((item) => item.rowId))
|
||
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
||
if (successfulIds.has(rows[index].id)) rows.splice(index, 1)
|
||
}
|
||
if (rows.length === 0) rows.push(createRow())
|
||
resultDialogOpen.value = successes.length > 0 && failures.length > 0
|
||
if (failures.length) ElMessage.error(`${failures.length} 张草稿生成失败:${failures[0].errorMessage || '草稿单生成失败'}`)
|
||
else {
|
||
activeBatchKey.value = ''
|
||
await Promise.all(successes.map((item) => store.loadDraftDetail(item.draftId)))
|
||
workspace.close(['first-leg-quick-order', 'first-leg-order-create'])
|
||
await router.push({ name: 'first-leg-orders' })
|
||
ElMessage.success(`已生成 ${successes.length} 张草稿单,提交后将生成入仓码`)
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '批量草稿生成失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function editDraft(result: QuickDraftResult) {
|
||
resultDialogOpen.value = false
|
||
await router.push({ name: 'first-leg-order-edit', params: { draftId: result.draftId } })
|
||
}
|
||
|
||
onMounted(loadChannels)
|
||
</script>
|
||
|
||
<template>
|
||
<section class="quick-order-page" aria-labelledby="quick-order-heading">
|
||
<header class="quick-order-intro">
|
||
<h2 id="quick-order-heading">客户下单</h2>
|
||
<p>只需输入渠道、目的仓、产品 SKU 和箱数,即可快速填写订单。</p>
|
||
<small v-if="manualInboundCodeRequired">当前全局使用旧系统入仓码,每张草稿都需要填写旧系统生成的入仓码。</small>
|
||
<small>每一行会直接生成一张独立草稿单,其他明细可进入草稿后继续编辑并提交。</small>
|
||
</header>
|
||
|
||
<el-alert v-if="loadError" :title="loadError" type="warning" show-icon :closable="false">
|
||
<template #default><el-button text type="primary" @click="loadChannels">重新加载</el-button></template>
|
||
</el-alert>
|
||
|
||
<div v-loading="loading" class="quick-order-table-wrap">
|
||
<table class="quick-order-table">
|
||
<thead>
|
||
<tr>
|
||
<th v-if="manualInboundCodeRequired" class="inbound-code-heading">旧系统入仓码</th>
|
||
<th class="channel-heading">选择渠道</th>
|
||
<th class="destination-heading">目的仓</th>
|
||
<th class="operation-heading">操作</th>
|
||
<th>产品信息:产品 SKU / 箱数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, rowIndex) in rows" :key="row.id">
|
||
<td v-if="manualInboundCodeRequired" class="inbound-code-cell">
|
||
<el-input
|
||
v-model="row.inboundCode"
|
||
maxlength="40"
|
||
placeholder="请输入旧系统入仓码"
|
||
@input="row.inboundCode = String($event).trim().toUpperCase()"
|
||
/>
|
||
</td>
|
||
<td class="channel-cell">
|
||
<el-select v-model="row.channelKey" :disabled="!hasChannels" placeholder="请选择">
|
||
<el-option
|
||
v-for="channel in channelOptions"
|
||
:key="channelKey(channel.warehouseId, channel.channelCode)"
|
||
:label="channel.channelName"
|
||
:value="channelKey(channel.warehouseId, channel.channelCode)"
|
||
>
|
||
<span>{{ channel.channelName }}</span>
|
||
<small>{{ channel.warehouseName }}</small>
|
||
</el-option>
|
||
</el-select>
|
||
</td>
|
||
<td>
|
||
<el-radio-group :model-value="row.destinationType" class="destination-options" @change="(value) => openDestinationDialog(row, value)">
|
||
<el-radio value="FBA">FBA</el-radio>
|
||
<el-radio value="OVERSEAS">海外仓</el-radio>
|
||
<el-radio value="PRIVATE">私人仓</el-radio>
|
||
</el-radio-group>
|
||
<button type="button" class="destination-binding" @click="openDestinationDialog(row, row.destinationType)">
|
||
<MapPin :size="14" />
|
||
<span>{{ destinationLabel(row) }}</span>
|
||
</button>
|
||
</td>
|
||
<td class="operation-cell">
|
||
<el-button text type="primary" @click="addProduct(row)"><Plus :size="15" />添加产品行</el-button>
|
||
<el-button text type="danger" @click="removeRow(rowIndex)"><Trash2 :size="15" />删除本行</el-button>
|
||
</td>
|
||
<td class="products-cell">
|
||
<div v-for="(product, productIndex) in row.products" :key="product.id" class="product-fields">
|
||
<el-input v-model="product.sku" maxlength="64" placeholder="产品 SKU" />
|
||
<el-input-number v-model="product.packageCount" :min="1" :max="99999" :precision="0" controls-position="right" aria-label="箱数" placeholder="请输入箱数" />
|
||
<el-button v-if="row.products.length > 1" text type="danger" aria-label="删除产品" @click="removeProduct(row, productIndex)"><Trash2 :size="15" /></el-button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="quick-order-actions">
|
||
<el-button plain @click="addOrderRow"><Plus :size="16" />新增一行</el-button>
|
||
<el-button type="primary" :loading="submitting" :disabled="loading || !hasChannels" @click="submitQuickOrder">
|
||
<Send :size="16" />生成草稿单
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-dialog v-model="destinationDialogOpen" class="destination-dialog" :title="`绑定${destinationForm.type === 'FBA' ? ' FBA 仓' : destinationForm.type === 'OVERSEAS' ? '海外仓' : '私人仓'}`" width="min(560px, 92vw)">
|
||
<el-form label-position="top" class="destination-dialog-form">
|
||
<el-form-item label="目的仓类型" required>
|
||
<el-radio-group v-model="destinationForm.type" class="destination-type-switch" @change="(value) => openDestinationDialog(editingDestinationRow!, value)">
|
||
<el-radio-button value="FBA">FBA 仓</el-radio-button>
|
||
<el-radio-button value="OVERSEAS">海外仓</el-radio-button>
|
||
<el-radio-button value="PRIVATE">私人仓</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item v-if="destinationForm.type !== 'PRIVATE'" :label="destinationForm.type === 'FBA' ? 'FBA 仓' : '海外仓'" required>
|
||
<el-select v-model="destinationForm.warehouseId" filterable :loading="destinationLoading" placeholder="请选择当前可送达的目的仓">
|
||
<el-option v-for="warehouse in availableDestinationWarehouses" :key="warehouse.id" :label="`${warehouse.name} · ${warehouse.code}`" :value="warehouse.id" />
|
||
</el-select>
|
||
<small v-if="!destinationLoading && availableDestinationWarehouses.length === 0">平台暂未维护可用目的仓,请联系平台管理员。</small>
|
||
</el-form-item>
|
||
<template v-else>
|
||
<el-form-item label="私人仓地址" required>
|
||
<el-select v-model="destinationForm.privateAddressId" filterable :loading="destinationLoading" placeholder="请选择企业已保存的私人仓地址">
|
||
<el-option v-for="address in privateAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" />
|
||
</el-select>
|
||
<small v-if="privateAddressesError">私人仓地址加载失败,请关闭后重试。</small>
|
||
<small v-else-if="!destinationLoading && privateAddresses.length === 0">暂无已保存的私人仓地址,请先在普通下单页面维护。</small>
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="destinationDialogOpen = false">取消</el-button>
|
||
<el-button type="primary" @click="bindDestination">确认绑定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="resultDialogOpen" class="quick-result-dialog" title="草稿单生成结果" width="min(700px, 94vw)">
|
||
<div class="quick-draft-results">
|
||
<article v-for="result in draftResults" :key="result.rowId">
|
||
<div><strong>{{ result.inboundCode || '入仓码提交后生成' }}</strong><span>SKU {{ result.sku }}</span></div>
|
||
<el-button type="primary" plain @click="editDraft(result)">编辑明细</el-button>
|
||
</article>
|
||
</div>
|
||
<template #footer><el-button type="primary" @click="resultDialogOpen = false">完成</el-button></template>
|
||
</el-dialog>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.quick-order-page { min-width: 0; min-height: 330px; padding: 18px 16px 24px; border: 1px solid #e8edf4; border-radius: 8px; background: #fff; box-shadow: 0 10px 28px rgba(15, 31, 58, .05); }
|
||
.quick-order-intro { margin-bottom: 16px; }
|
||
.quick-order-intro h2 { margin: 0 0 6px; color: #1d2939; font-size: 16px; }
|
||
.quick-order-intro p, .quick-order-intro small { display: block; margin: 0; color: #8a94a6; font-size: 12px; line-height: 1.65; }
|
||
.quick-order-page > :deep(.el-alert) { margin-bottom: 14px; }
|
||
.quick-order-table-wrap { min-height: 126px; overflow-x: auto; border: 1px solid #e5eaf2; border-radius: 8px; }
|
||
.quick-order-table { width: 100%; min-width: 900px; border-collapse: collapse; table-layout: fixed; }
|
||
.quick-order-table th { height: 42px; padding: 0 14px; background: #f4f5fb; color: #637fec; font-size: 12px; font-weight: 650; text-align: left; }
|
||
.quick-order-table .inbound-code-heading, .quick-order-table .channel-heading { width: 220px; }
|
||
.quick-order-table .destination-heading { width: 145px; }
|
||
.quick-order-table .operation-heading { width: 210px; }
|
||
.quick-order-table td { padding: 12px 14px; border-top: 1px solid #edf0f5; vertical-align: middle; }
|
||
.channel-cell :deep(.el-select) { width: 100%; }
|
||
.inbound-code-cell :deep(.el-input) { width: 100%; }
|
||
.destination-options { display: flex; align-items: flex-start; flex-direction: column; gap: 4px; }
|
||
.destination-options :deep(.el-radio) { height: 20px; margin-right: 0; }
|
||
.destination-binding { display: flex; width: 100%; min-width: 0; align-items: center; gap: 5px; margin-top: 7px; padding: 0; border: 0; background: transparent; color: #637fec; cursor: pointer; font-size: 11px; text-align: left; }
|
||
.destination-binding span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.destination-binding:focus-visible { outline: 2px solid #637fec; outline-offset: 3px; }
|
||
.operation-cell { white-space: normal; }
|
||
.operation-cell :deep(.el-button) { margin: 0 8px 4px 0; padding: 4px 0; }
|
||
.products-cell { padding-top: 7px !important; padding-bottom: 7px !important; }
|
||
.product-fields { display: grid; grid-template-columns: minmax(160px, 260px) 112px 32px; align-items: center; gap: 10px; min-height: 44px; }
|
||
.product-fields + .product-fields { padding-top: 7px; border-top: 1px dashed #e5eaf2; }
|
||
.product-fields :deep(.el-input-number) { width: 112px; }
|
||
.product-fields :deep(.el-button) { width: 30px; height: 30px; margin: 0; padding: 0; }
|
||
.quick-order-actions { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 12px; padding-bottom: 44px; }
|
||
.quick-order-actions > :deep(.el-button:last-child) { position: absolute; left: 50%; bottom: 0; min-width: 112px; transform: translateX(-50%); }
|
||
.quick-order-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||
:deep(.el-select-dropdown__item small) { float: right; margin-left: 12px; color: #98a2b3; }
|
||
:deep(.destination-dialog .el-dialog__body) { padding-top: 12px; }
|
||
.destination-dialog-form :deep(.el-select) { width: 100%; }
|
||
.destination-dialog-form :deep(.el-form-item) { margin-bottom: 16px; }
|
||
.destination-dialog-form small { display: block; margin-top: 6px; color: #8a94a6; font-size: 11px; }
|
||
.destination-type-switch { display: flex; width: 100%; }
|
||
.destination-type-switch :deep(.el-radio-button) { flex: 1; }
|
||
.destination-type-switch :deep(.el-radio-button__inner) { width: 100%; }
|
||
.destination-private-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
|
||
.quick-draft-results { display: grid; gap: 10px; }
|
||
.quick-draft-results article { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 14px; border: 1px solid #e5eaf2; border-radius: 6px; }
|
||
.quick-draft-results article div { display: grid; min-width: 0; gap: 3px; }
|
||
.quick-draft-results article strong { color: #1d2939; font-size: 14px; }
|
||
.quick-draft-results article span { color: #667085; font-size: 11px; }
|
||
@media (max-width: 720px) {
|
||
.quick-order-page { padding: 14px 12px 20px; }
|
||
.quick-order-table { min-width: 760px; }
|
||
.quick-order-actions { align-items: stretch; flex-direction: column; padding-bottom: 0; }
|
||
.quick-order-actions > :deep(.el-button), .quick-order-actions > :deep(.el-button:last-child) { position: static; width: 100%; min-height: 42px; transform: none; }
|
||
.destination-private-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|