Files
client-web/src/modules/first-leg/views/QuickOrderView.vue
T
2026-09-19 17:02:11 +08:00

596 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>