优化
This commit is contained in:
@@ -11,7 +11,7 @@ test('routes the confirmed first-leg path to the implemented order view', () =>
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
|
||||
})
|
||||
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a drawer', () => {
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a large dialog', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
|
||||
assert.match(routerSource, /name: 'first-leg-order-create'[\s\S]*?props: \{ createPage: true \}/)
|
||||
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
|
||||
@@ -19,7 +19,10 @@ test('opens order creation as a dedicated protected page while keeping draft edi
|
||||
assert.match(source, /const isCreatePage = computed\(\(\) => props\.createPage\)/)
|
||||
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
|
||||
assert.match(editorShellSource, /<section v-if="page" class="first-leg-editor-page">/)
|
||||
assert.match(editorShellSource, /<el-drawer[\s\S]*v-else/)
|
||||
assert.match(editorShellSource, /<el-dialog[\s\S]*v-else/)
|
||||
assert.match(editorShellSource, /class="first-leg-editor-dialog"/)
|
||||
assert.match(editorShellSource, /width="94vw"/)
|
||||
assert.doesNotMatch(editorShellSource, /<el-drawer/)
|
||||
assert.match(editorShellSource, /返回订单列表/)
|
||||
})
|
||||
|
||||
@@ -56,15 +59,24 @@ test('defers approval checks to the server when the member cannot read the compa
|
||||
assert.match(source, /store\.submit\([^\n]*knownCompanyAuditStatus\)/)
|
||||
})
|
||||
|
||||
test('renders dynamic cargo lines and responsive drawers', () => {
|
||||
test('renders dynamic cargo lines and a responsive editor dialog', () => {
|
||||
assert.match(source, /v-for="\(item, index\) in form\.items"/)
|
||||
assert.match(source, /function addCargoItem\(\)/)
|
||||
assert.match(source, /function removeCargoItem\(index: number\)/)
|
||||
assert.match(source, /:global\(\.first-leg-editor\).*width: 100% !important/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
|
||||
assert.match(editorShellSource, /width: 100vw !important; height: 100vh;/)
|
||||
assert.match(source, /class="mobile-order-list"/)
|
||||
assert.match(source, /\.desktop-order-table \{ display: none; \}/)
|
||||
})
|
||||
|
||||
test('uses the full editor dialog width for cargo details and cost estimates', () => {
|
||||
assert.match(source, /:class="\{ 'is-dialog-order-form': !isCreatePage \}"/)
|
||||
assert.match(source, /\.order-form\.is-dialog-order-form \{ width: 100%; max-width: none; \}/)
|
||||
assert.match(source, /\.is-dialog-order-form > \.cargo-section, \.is-dialog-order-form > \.cost-estimate-section \{ width: 100%; max-width: none; \}/)
|
||||
assert.match(source, /\.cost-estimate-cards \{ display: grid; grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);/)
|
||||
assert.match(source, /\.cargo-table-wrap \{ width: 100%;/)
|
||||
})
|
||||
|
||||
test('keeps primary mobile order actions touch sized', () => {
|
||||
assert.match(source, /\.editor-actions :deep\(\.el-button\) \{ min-height: 44px;/)
|
||||
})
|
||||
@@ -73,15 +85,106 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
assert.match(source, /import QueryToolbar, \{ type QueryField, type QueryModel \}/)
|
||||
assert.match(source, /import DataTable, \{ type DataTableColumn \}/)
|
||||
assert.match(source, /const filters = ref<QueryModel>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" \/>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders">/)
|
||||
assert.match(source, /async function searchOrders\(model: QueryModel\)/)
|
||||
assert.match(source, /await store\.load\(\{/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' \}/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 128, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' \}/)
|
||||
assert.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line, C02 business summaries, and optional customer-visible nodes', () => {
|
||||
test('adds prototype download actions to the order query panel', () => {
|
||||
assert.match(source, /下载唛头/)
|
||||
assert.match(source, /导出订单信息/)
|
||||
assert.match(source, /function downloadShippingMarks\(\)/)
|
||||
assert.match(source, /downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
|
||||
assert.match(source, /error\.traceId \? `,追踪号 \$\{error\.traceId\}`/)
|
||||
assert.doesNotMatch(source, /order\.businessSummary\.inboundId/)
|
||||
assert.match(source, /批量箱唛-\$\{orders\.length\}单\.pdf/)
|
||||
assert.match(source, /function exportOrderInformation\(\)/)
|
||||
assert.match(source, /selectedOrders\.value\.map/)
|
||||
assert.match(source, /订单信息\.csv/)
|
||||
assert.doesNotMatch(source, /exportOrderInformation[\s\S]{0,1200}loadDetail/)
|
||||
})
|
||||
|
||||
test('selects orders before downloading marks or exporting last-mile data', () => {
|
||||
assert.match(source, /\{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true \}/)
|
||||
assert.match(source, /const selectedOrders = ref<FirstLegOrder\[\]>\(\[\]\)/)
|
||||
assert.match(source, /const selectedOrderCount = computed\(\(\) => selectedOrders\.value\.length\)/)
|
||||
assert.match(source, /@selection-change="handleOrderSelectionChange"/)
|
||||
assert.match(source, /:disabled="selectedOrderCount === 0" @click="downloadShippingMarks"/)
|
||||
assert.match(source, /:disabled="selectedOrderCount === 0" @click="exportOrderInformation"/)
|
||||
assert.match(source, /const orders = selectedOrders\.value\s*$/m)
|
||||
assert.match(source, /const rows = selectedOrders\.value\.map/)
|
||||
assert.doesNotMatch(source, /const orders = filteredOrders\.value\.filter/)
|
||||
assert.doesNotMatch(source, /const rows = filteredOrders\.value\.map/)
|
||||
})
|
||||
|
||||
test('downloads selected carton labels with one array request', () => {
|
||||
const downloadStart = source.indexOf('async function downloadShippingMarks()')
|
||||
const downloadEnd = source.indexOf('function formatExportDateTime', downloadStart)
|
||||
const downloadSource = source.slice(downloadStart, downloadEnd)
|
||||
|
||||
assert.match(downloadSource, /await downloadMerchantOrderCartonLabelsBatch\(orders\.map\(\(order\) => order\.id\)\)/)
|
||||
assert.match(downloadSource, /saveBlob\(result\.blob/)
|
||||
assert.match(downloadSource, /result\.failureCount/)
|
||||
assert.doesNotMatch(downloadSource, /for \(const order of orders\)/)
|
||||
assert.doesNotMatch(downloadSource, /Promise\.all\(/)
|
||||
assert.doesNotMatch(downloadSource, /businessSummary\.inboundId\)/)
|
||||
assert.doesNotMatch(downloadSource, /没有已生成的入库单|未生成入库单/)
|
||||
})
|
||||
|
||||
test('exports every visible order field with normalized dates', () => {
|
||||
assert.match(source, /function formatExportDateTime\(value: string \| null \| undefined\)/)
|
||||
assert.match(source, /return `\$\{date\.getFullYear\(\)\}-\$\{pad\(date\.getMonth\(\) \+ 1\)\}-\$\{pad\(date\.getDate\(\)\)\} \$\{pad\(date\.getHours\(\)\)\}:\$\{pad\(date\.getMinutes\(\)\)\}:\$\{pad\(date\.getSeconds\(\)\)\}`/)
|
||||
for (const label of ['订单号', '入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '业务日期', '创建时间']) {
|
||||
assert.match(source, new RegExp(`'${label}'`))
|
||||
}
|
||||
assert.match(source, /formatExportDateTime\(order\.inboundedAt\)/)
|
||||
assert.match(source, /formatExportDateTime\(order\.businessDate\)/)
|
||||
assert.match(source, /formatExportDateTime\(order\.createdAt\)/)
|
||||
})
|
||||
|
||||
test('matches the prototype with selectable dense order-list columns', () => {
|
||||
assert.match(source, /aria-label="设置订单列表显示列"/)
|
||||
assert.match(source, /<SlidersHorizontal :size="18"/)
|
||||
assert.match(source, /<el-checkbox-group v-model="visibleColumnKeys"/)
|
||||
assert.match(source, /draggable="true"/)
|
||||
assert.match(source, /@drop\.prevent="dropOrderColumn/)
|
||||
assert.match(source, /function moveOrderColumn/)
|
||||
assert.match(source, /function resetOrderColumns/)
|
||||
assert.match(source, />恢复默认<\/el-button>/)
|
||||
assert.match(source, /const visibleOrderColumns = computed/)
|
||||
assert.match(source, /:columns="visibleOrderColumns"/)
|
||||
for (const label of ['入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS', '总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注', '货件编号', '跟踪编号', '国内仓入库时间', '业务日期']) {
|
||||
assert.match(source, new RegExp(`label: '${label}'`))
|
||||
}
|
||||
assert.match(source, /order\.businessSummary\.totalPackageCount/)
|
||||
assert.match(source, /summarizeFirstLegCargo\(order\.items\)/)
|
||||
assert.match(source, /order\.items\.reduce/)
|
||||
assert.match(source, /<template #chargeableWeight>-<\/template>/)
|
||||
assert.match(source, /<template #billingUnitPrice>-<\/template>/)
|
||||
assert.match(source, /<template #chargeableVolume>-<\/template>/)
|
||||
assert.match(source, /inboundedAt/)
|
||||
assert.match(source, /businessDate/)
|
||||
assert.match(source, /<template #businessDate>-<\/template>/)
|
||||
assert.match(source, /<template #createdAt="\{ row \}">\{\{ formatDateTime\(\(row as FirstLegOrder\)\.createdAt\) \}\}<\/template>/)
|
||||
assert.match(source, /order\.skuNos\.length/)
|
||||
assert.doesNotMatch(source, /pagedOrders[\s\S]{0,100}loadDetail/)
|
||||
})
|
||||
|
||||
test('keeps dense order identifiers on one line and aligns columns by content type', () => {
|
||||
assert.match(source, /key: 'order'[\s\S]{0,160}showOverflowTooltip: true/)
|
||||
assert.match(source, /key: 'status'[\s\S]{0,100}align: 'center'[\s\S]{0,100}headerAlign: 'center'/)
|
||||
assert.match(source, /key: 'grossWeight'[\s\S]{0,100}align: 'right'[\s\S]{0,100}headerAlign: 'right'/)
|
||||
assert.match(source, /class="order-number-link table-code"/)
|
||||
assert.doesNotMatch(source, /<template #order="\{ row \}">[\s\S]{0,400}class="order-reference"/)
|
||||
assert.match(source, /\.table-ellipsis, \.table-multivalue \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
|
||||
assert.match(source, /\.order-number-link \{[^}]*text-overflow: ellipsis;[^}]*white-space: nowrap;/)
|
||||
assert.match(source, /\.prototype-order-table :deep\(\.el-table__body-wrapper td\.el-table__cell\) \{ height: 50px;/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line and C02 business summaries without detail history', () => {
|
||||
assert.match(source, /import OrderProgressPanel/)
|
||||
assert.match(source, /<OrderProgressPanel :order="detailOrder" \/>/)
|
||||
assert.match(source, /<h3>关键业务摘要<\/h3>/)
|
||||
@@ -91,9 +194,7 @@ test('shows one unified progress line, C02 business summaries, and optional cust
|
||||
assert.match(source, /summary\.deliveryTrackingNo/)
|
||||
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
|
||||
assert.match(progressSource, /国内装柜进度及后续履约同步/)
|
||||
assert.match(progressSource, /item\.kind === 'CUSTOM_NODE'/)
|
||||
assert.match(progressSource, /业务节点/)
|
||||
assert.match(progressSource, /暂无进度记录/)
|
||||
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
|
||||
assert.match(source, /报关回执/)
|
||||
assert.match(source, /船期摘要/)
|
||||
assert.match(source, /柜次进度/)
|
||||
@@ -103,8 +204,9 @@ test('shows one unified progress line, C02 business summaries, and optional cust
|
||||
|
||||
test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
assert.match(source, /getOrderCargoSummary/)
|
||||
assert.match(source, /目的地 \/ 渠道/)
|
||||
assert.match(source, /运输方式待更新|渠道待更新/)
|
||||
assert.match(source, /label: '渠道'/)
|
||||
assert.match(source, /label: '目的仓'/)
|
||||
assert.match(source, /displayListValue/)
|
||||
assert.doesNotMatch(source, /key: 'operator'/)
|
||||
assert.doesNotMatch(source, /key: 'transportMode', type: 'select'/)
|
||||
assert.match(source, /store\.draftLoadError/)
|
||||
@@ -114,9 +216,9 @@ test('uses only fields present on C02 rows before loading full detail', () => {
|
||||
})
|
||||
|
||||
test('shows and searches the inbound code after a draft is submitted', () => {
|
||||
assert.match(source, /placeholder: '订单号、入仓码、渠道或仓库'/)
|
||||
assert.match(source, /label: '入仓码 \/ SKU'/)
|
||||
assert.match(source, /order\.businessSummary\.inboundCode/)
|
||||
assert.match(source, /v-if="\(row as FirstLegOrder\)\.businessSummary\.inboundCode"[^>]*>入仓码/)
|
||||
assert.match(source, /<template #inboundCode="\{ row \}"><span class="table-ellipsis table-code"[^>]*>\{\{ displayListValue\(\(row as FirstLegOrder\)\.businessSummary\.inboundCode\) \}\}<\/span><\/template>/)
|
||||
assert.match(source, /v-if="order\.businessSummary\.inboundCode"[^>]*>入仓码/)
|
||||
assert.match(source, /v-if="detailOrder\.businessSummary\.inboundCode"><dt>入仓码<\/dt>/)
|
||||
})
|
||||
@@ -135,10 +237,10 @@ test('keeps the first viewport compact and moves create into the table toolbar',
|
||||
assert.match(source, /client-main:has\(> \.first-leg-page:not\(\.is-create-route\)\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
|
||||
})
|
||||
|
||||
test('keeps desktop query actions on the first row and only shows the qualification notice before approval', () => {
|
||||
assert.match(source, /\.first-leg-query-toolbar \{ flex-wrap: nowrap; \}/)
|
||||
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; flex: 1 1 auto;/)
|
||||
assert.match(source, /query-toolbar__actions\) \{ align-self: flex-start; flex-wrap: nowrap; margin-left: auto;/)
|
||||
test('uses the prototype query grid and only shows the qualification notice before approval', () => {
|
||||
assert.match(source, /\.first-leg-query-toolbar \{ display: block;/)
|
||||
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; grid-template-columns: repeat\(4,/)
|
||||
assert.match(source, /query-toolbar__actions\) \{ justify-content: flex-start; margin-top: 14px;/)
|
||||
assert.match(source, /v-if="companySubmissionBlocked && !supportingDataErrors\.some/)
|
||||
assert.doesNotMatch(source, /企业资料已审核,可正常提交订单/)
|
||||
assert.doesNotMatch(source, /order-eligibility\.is-approved/)
|
||||
@@ -161,9 +263,9 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
|
||||
assert.match(source, /v-model="form\.receivingWarehouseId"/)
|
||||
assert.doesNotMatch(source, /selectedWarehouse\.businessHours/)
|
||||
assert.doesNotMatch(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__header\) \{[^}]*margin-bottom: 0;[^}]*border-bottom: 1px solid var\(--border\)/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__body\) \{[^}]*min-height: 0;[^}]*padding: 18px 22px 32px;[^}]*overscroll-behavior: contain/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__header\) \{[^}]*border-bottom: 1px solid var\(--border\)/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__body\) \{[^}]*min-height: 0;[^}]*overflow-y: auto;[^}]*overscroll-behavior: contain/)
|
||||
assert.match(editorShellSource, /first-leg-editor-dialog \.el-dialog__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\?\.name/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
|
||||
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
|
||||
@@ -297,7 +399,7 @@ test('shows fulfillment courier information as read-only client detail', () => {
|
||||
})
|
||||
|
||||
test('shows the full customer-safe order detail without platform-only controls', () => {
|
||||
const detailSource = source.slice(source.indexOf('<el-drawer v-model="detailOpen"'), source.indexOf('</el-drawer>', source.indexOf('<el-drawer v-model="detailOpen"')))
|
||||
const detailSource = source.slice(source.indexOf('<el-dialog v-model="detailOpen"'), source.indexOf('</el-dialog>', source.indexOf('<el-dialog v-model="detailOpen"')))
|
||||
for (const heading of ['订单与运输', '收发货信息', '关键业务摘要', '履约快递信息', '货物明细']) {
|
||||
assert.match(source, new RegExp(`<h3>${heading}<\\/h3>`))
|
||||
}
|
||||
@@ -314,6 +416,21 @@ test('shows the full customer-safe order detail without platform-only controls',
|
||||
assert.doesNotMatch(detailSource, /内部备注|审核通过|添加备注/)
|
||||
})
|
||||
|
||||
test('opens order detail in a large dialog and paginates cargoes as a read-only table', () => {
|
||||
assert.match(source, /<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh"/)
|
||||
assert.doesNotMatch(source, /<el-drawer v-model="detailOpen"/)
|
||||
assert.match(source, /const detailCargoPage = ref\(1\)/)
|
||||
assert.match(source, /const detailCargoPageSize = ref\(5\)/)
|
||||
assert.match(source, /const pagedDetailCargoItems = computed/)
|
||||
assert.match(source, /items\.slice\(start, start \+ detailCargoPageSize\.value\)/)
|
||||
assert.match(source, /<el-table :data="pagedDetailCargoItems"[^>]*class="cargo-detail-table"/)
|
||||
assert.match(source, /v-model:current-page="detailCargoPage"/)
|
||||
assert.match(source, /v-model:page-size="detailCargoPageSize"/)
|
||||
assert.match(source, /:total="detailOrder\.items\.length"/)
|
||||
assert.doesNotMatch(source, /class="cargo-detail-list"/)
|
||||
assert.match(source, /first-leg-detail-dialog\.el-dialog\) \{[^}]*height: 94vh;/)
|
||||
})
|
||||
|
||||
test('loads merchant inbound billing as an isolated read-only detail section', () => {
|
||||
assert.match(source, /import InboundBillingPanel/)
|
||||
assert.match(source, /import \{ getMerchantInboundBilling \}/)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules, type UploadRequestOptions } from 'element-plus'
|
||||
import { AlertTriangle, Box, Download, Eye, FilePlus2, ImagePlus, MapPin, PackageCheck, Pencil, Plus, Send, Trash2, Upload } from '@lucide/vue'
|
||||
import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, Eye, FilePlus2, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||||
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
@@ -14,6 +14,7 @@ import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBillin
|
||||
import { getMerchantCustomsByOrder, isMerchantCustomsUnavailable, type MerchantCustomsCaseDetail } from '@/modules/first-leg/api/customs'
|
||||
import {
|
||||
estimateMerchantFirstLegPrice,
|
||||
downloadMerchantOrderCartonLabelsBatch,
|
||||
downloadMerchantProductImage,
|
||||
getMerchantPricingChannels,
|
||||
uploadMerchantProductImage,
|
||||
@@ -54,13 +55,18 @@ const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create
|
||||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||||
const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC'))
|
||||
const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType))
|
||||
const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] })
|
||||
const filters = ref<QueryModel>({ orderNo: '', receivingWarehouseId: '', channelName: '', cargoKeyword: '', createdRange: [], status: '', destinationType: '', trackingNumber: '' })
|
||||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', placeholder: '订单号、入仓码、渠道或仓库', props: { 'aria-label': '订单关键词' } },
|
||||
{ key: 'status', type: 'select', placeholder: '全部状态', options: FIRST_LEG_ORDER_STATUSES.map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '订单状态' } },
|
||||
{ key: 'createdRange', type: 'daterange', placeholder: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
|
||||
]
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号', props: { 'aria-label': '订单号' } },
|
||||
{ key: 'receivingWarehouseId', label: '收货仓', type: 'select', placeholder: '全部', options: availableWarehouses.value.map((item) => ({ label: item.name, value: item.id })), props: { 'aria-label': '收货仓' } },
|
||||
{ key: 'channelName', label: '渠道名称', type: 'select', placeholder: '全部', options: [...new Set(store.orders.map((item) => item.channelName).filter(Boolean))].map((name) => ({ label: name, value: name })), props: { 'aria-label': '渠道名称' } },
|
||||
{ key: 'cargoKeyword', label: '入仓码 / SKU', type: 'input', placeholder: '请输入入仓码或 SKU', props: { 'aria-label': '入仓码或 SKU' } },
|
||||
{ key: 'createdRange', label: '创建时间', type: 'daterange', placeholder: '请选择创建时间', props: { 'aria-label': '创建时间' } },
|
||||
{ key: 'status', label: '当前状态', type: 'select', placeholder: '全部', options: FIRST_LEG_ORDER_STATUSES.map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '当前状态' } },
|
||||
{ key: 'destinationType', label: '目的仓类型', type: 'select', placeholder: '全部', options: [{ label: 'FBA 仓', value: 'FBA' }, { label: '海外仓', value: 'OVERSEAS' }, { label: '私人地址', value: 'PRIVATE' }], props: { 'aria-label': '目的仓类型' } },
|
||||
{ key: 'trackingNumber', label: '快递单号', type: 'input', placeholder: '请输入快递单号', props: { 'aria-label': '快递单号' } },
|
||||
])
|
||||
const channelOptions = ref<MerchantPricingChannel[]>([])
|
||||
const channelLoading = ref(false)
|
||||
const channelError = ref('')
|
||||
@@ -80,17 +86,76 @@ const cargoTemplateColumns = [
|
||||
] as const
|
||||
|
||||
const cargoNumberKeys = new Set<string>(['lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'packageCount', 'grossWeightKg', 'volumeCbm', 'pieces', 'netWeightKg', 'unitDeclaredValue', 'declaredValue', 'productLengthCm', 'productWidthCm', 'productHeightCm'])
|
||||
const orderColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单', minWidth: 190, slotName: 'order' },
|
||||
{ key: 'route', label: '目的地 / 渠道', minWidth: 190, slotName: 'route' },
|
||||
{ key: 'warehouse', label: '收货仓', prop: 'receivingWarehouseName', minWidth: 120 },
|
||||
{ key: 'cargo', label: '货物摘要', minWidth: 150, slotName: 'cargo' },
|
||||
{ key: 'status', label: '状态', width: 120, slotName: 'status' },
|
||||
{ key: 'updatedAt', label: '更新时间', width: 155, slotName: 'updatedAt' },
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 128, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 190, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'order' },
|
||||
]
|
||||
const selectableOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'inboundCode', label: '入仓码', width: 176, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'inboundCode' },
|
||||
{ key: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' },
|
||||
{ key: 'destinationWarehouse', label: '目的仓', width: 120, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'destinationWarehouse' },
|
||||
{ key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' },
|
||||
{ key: 'packageCount', label: '箱数', width: 82, align: 'center', headerAlign: 'center', slotName: 'packageCount' },
|
||||
{ key: 'expressCompany', label: '快递公司', width: 128, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'expressCompany' },
|
||||
{ key: 'trackingNumber', label: '快递单号', width: 158, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'trackingNumber' },
|
||||
{ key: 'skus', label: 'SKUS', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'skus' },
|
||||
{ key: 'grossWeight', label: '总毛重', width: 108, align: 'right', headerAlign: 'right', slotName: 'grossWeight' },
|
||||
{ key: 'netWeight', label: '总净重', width: 108, align: 'right', headerAlign: 'right', slotName: 'netWeight' },
|
||||
{ key: 'volume', label: '总体积', width: 106, align: 'right', headerAlign: 'right', slotName: 'volume' },
|
||||
{ key: 'pieces', label: '总数量', width: 92, align: 'right', headerAlign: 'right', slotName: 'pieces' },
|
||||
{ key: 'billingUnitPrice', label: '计费单价', width: 108, align: 'right', headerAlign: 'right', slotName: 'billingUnitPrice' },
|
||||
{ key: 'chargeableVolume', label: '计费体积', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableVolume' },
|
||||
{ key: 'chargeableWeight', label: '计费重量', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableWeight' },
|
||||
{ key: 'remark', label: '备注', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'remark' },
|
||||
{ key: 'shipmentId', label: '货件编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'shipmentId' },
|
||||
{ key: 'referenceId', label: '跟踪编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'referenceId' },
|
||||
{ key: 'receivingTime', label: '国内仓入库时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'receivingTime' },
|
||||
{ key: 'businessDate', label: '业务日期', width: 148, align: 'center', headerAlign: 'center', slotName: 'businessDate' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'createdAt' },
|
||||
]
|
||||
const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key))
|
||||
const visibleColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||||
const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||||
const draggedColumnKey = ref('')
|
||||
const orderedSelectableColumns = computed(() => orderedColumnKeys.value
|
||||
.map((key) => selectableOrderColumns.find((column) => String(column.key) === key))
|
||||
.filter((column): column is DataTableColumn => Boolean(column)))
|
||||
const visibleOrderColumns = computed(() => [
|
||||
...fixedOrderColumns,
|
||||
...orderedSelectableColumns.value.filter((column) => visibleColumnKeys.value.includes(String(column.key))),
|
||||
])
|
||||
|
||||
function moveOrderColumn(key: string, offset: -1 | 1) {
|
||||
const index = orderedColumnKeys.value.indexOf(key)
|
||||
const targetIndex = index + offset
|
||||
if (index < 0 || targetIndex < 0 || targetIndex >= orderedColumnKeys.value.length) return
|
||||
const nextKeys = [...orderedColumnKeys.value]
|
||||
;[nextKeys[index], nextKeys[targetIndex]] = [nextKeys[targetIndex], nextKeys[index]]
|
||||
orderedColumnKeys.value = nextKeys
|
||||
}
|
||||
|
||||
function dropOrderColumn(targetKey: string) {
|
||||
const sourceKey = draggedColumnKey.value
|
||||
draggedColumnKey.value = ''
|
||||
if (!sourceKey || sourceKey === targetKey) return
|
||||
const nextKeys = orderedColumnKeys.value.filter((key) => key !== sourceKey)
|
||||
const targetIndex = nextKeys.indexOf(targetKey)
|
||||
nextKeys.splice(targetIndex, 0, sourceKey)
|
||||
orderedColumnKeys.value = nextKeys
|
||||
}
|
||||
|
||||
function resetOrderColumns() {
|
||||
orderedColumnKeys.value = [...allSelectableColumnKeys]
|
||||
visibleColumnKeys.value = [...allSelectableColumnKeys]
|
||||
}
|
||||
const tablePage = ref(1)
|
||||
const tablePageSize = ref(10)
|
||||
const downloadingMarks = ref(false)
|
||||
const exportingOrders = ref(false)
|
||||
const selectedOrders = ref<FirstLegOrder[]>([])
|
||||
const selectedOrderCount = computed(() => selectedOrders.value.length)
|
||||
const orderTableVersion = ref(0)
|
||||
const editorOpen = ref(false)
|
||||
const orderFormRef = ref<FormInstance>()
|
||||
const detailOpen = ref(false)
|
||||
@@ -100,6 +165,8 @@ const detailId = ref<string>()
|
||||
const saving = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const detailCargoPage = ref(1)
|
||||
const detailCargoPageSize = ref(5)
|
||||
const inboundBilling = ref<MerchantInboundBilling | null>(null)
|
||||
const inboundBillingLoading = ref(false)
|
||||
const inboundBillingUnavailable = ref(false)
|
||||
@@ -219,6 +286,14 @@ function formatDateTime(value: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function formatExportDateTime(value: string | null | undefined) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const pad = (part: number) => String(part).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const pickupMethodLabels: Record<FirstLegOrder['pickupMethod'], string> = {
|
||||
SELF_EXPRESS: '自行快递到仓',
|
||||
PICKUP: '平台上门提货',
|
||||
@@ -342,18 +417,27 @@ useUnsavedWorkRegistration({
|
||||
})
|
||||
|
||||
const filteredOrders = computed(() => store.orders.filter((order) => {
|
||||
const keywordValue = appliedFilters.value.keyword
|
||||
const statusValue = appliedFilters.value.status
|
||||
const createdRangeValue = appliedFilters.value.createdRange
|
||||
const keyword = typeof keywordValue === 'string' ? keywordValue.trim().toLowerCase() : ''
|
||||
const textValue = (key: string) => typeof appliedFilters.value[key] === 'string' ? String(appliedFilters.value[key]).trim().toLowerCase() : ''
|
||||
const orderNo = textValue('orderNo')
|
||||
const receivingWarehouseId = textValue('receivingWarehouseId')
|
||||
const channelName = textValue('channelName')
|
||||
const cargoKeyword = textValue('cargoKeyword')
|
||||
const destinationType = textValue('destinationType')
|
||||
const trackingNumber = textValue('trackingNumber')
|
||||
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
|
||||
const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : []
|
||||
const matchesKeyword = !keyword || [order.orderNo, order.businessSummary.inboundCode, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(keyword))
|
||||
const matchesOrderNo = !orderNo || String(order.orderNo ?? '').toLowerCase().includes(orderNo)
|
||||
const matchesWarehouse = !receivingWarehouseId || order.receivingWarehouseId.toLowerCase() === receivingWarehouseId
|
||||
const matchesChannel = !channelName || order.channelName.toLowerCase() === channelName
|
||||
const matchesCargo = !cargoKeyword || [order.businessSummary.inboundCode, ...order.skuNos, ...order.items.map((item) => item.sku)]
|
||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(cargoKeyword))
|
||||
const matchesDestinationType = !destinationType || order.destinationType.toLowerCase() === destinationType
|
||||
const matchesTrackingNumber = !trackingNumber || order.trackingNumber.toLowerCase().includes(trackingNumber)
|
||||
const matchesStatus = !status || order.status === status
|
||||
const matchesDate = createdRange.length !== 2 || (order.createdAt.slice(0, 10) >= createdRange[0] && order.createdAt.slice(0, 10) <= createdRange[1])
|
||||
return matchesKeyword && matchesStatus && matchesDate
|
||||
return matchesOrderNo && matchesWarehouse && matchesChannel && matchesCargo && matchesDestinationType && matchesTrackingNumber && matchesStatus && matchesDate
|
||||
}))
|
||||
const pagedOrders = computed(() => {
|
||||
const start = (tablePage.value - 1) * tablePageSize.value
|
||||
@@ -362,6 +446,11 @@ const pagedOrders = computed(() => {
|
||||
|
||||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||||
const pagedDetailCargoItems = computed(() => {
|
||||
const items = detailOrder.value?.items ?? []
|
||||
const start = (detailCargoPage.value - 1) * detailCargoPageSize.value
|
||||
return items.slice(start, start + detailCargoPageSize.value)
|
||||
})
|
||||
const progressSummaryItems = computed(() => {
|
||||
const order = detailOrder.value
|
||||
if (!order) return []
|
||||
@@ -397,11 +486,7 @@ const selectedPricingChannel = computed(() => channelOptions.value.find((item) =
|
||||
const availablePricingTaxModes = computed(() => selectedPricingChannel.value?.pricingTaxModes ?? [])
|
||||
const hasActiveFilters = computed(() => {
|
||||
const values = appliedFilters.value
|
||||
return Boolean(
|
||||
(typeof values.keyword === 'string' && values.keyword.trim())
|
||||
|| values.status
|
||||
|| (Array.isArray(values.createdRange) && values.createdRange.length),
|
||||
)
|
||||
return Object.values(values).some((value) => Array.isArray(value) ? value.length > 0 : Boolean(typeof value === 'string' ? value.trim() : value))
|
||||
})
|
||||
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的订单' : '还没有订单')
|
||||
|
||||
@@ -927,6 +1012,7 @@ async function openEdit(order: FirstLegOrder) {
|
||||
|
||||
async function openDetail(order: FirstLegOrder) {
|
||||
detailId.value = order.id
|
||||
detailCargoPage.value = 1
|
||||
detailOpen.value = true
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
@@ -1059,6 +1145,128 @@ function removeCargoItem(index: number) {
|
||||
resetEditorValidation()
|
||||
}
|
||||
|
||||
function displayListValue(value: string | number | null | undefined) {
|
||||
return value === '' || value == null ? '-' : String(value)
|
||||
}
|
||||
|
||||
function uniqueItemValues(order: FirstLegOrder, key: 'sku' | 'shipmentId' | 'referenceId') {
|
||||
if (key === 'sku' && order.skuNos.length) return [...new Set(order.skuNos)].join('、')
|
||||
const values = order.items.map((item) => item[key]?.trim()).filter((value): value is string => Boolean(value))
|
||||
return [...new Set(values)].join('、') || '-'
|
||||
}
|
||||
|
||||
function orderPackageCount(order: FirstLegOrder) {
|
||||
return order.businessSummary.totalPackageCount ?? (order.items.length ? summarizeFirstLegCargo(order.items).packageCount : null)
|
||||
}
|
||||
|
||||
function orderGrossWeight(order: FirstLegOrder) {
|
||||
return order.businessSummary.actualGrossWeightKg ?? (order.items.length ? summarizeFirstLegCargo(order.items).grossWeightKg : null)
|
||||
}
|
||||
|
||||
function orderVolume(order: FirstLegOrder) {
|
||||
return order.businessSummary.actualVolumeCbm ?? (order.items.length ? summarizeFirstLegCargo(order.items).volumeCbm : null)
|
||||
}
|
||||
|
||||
function orderItemTotal(order: FirstLegOrder, key: 'netWeightKg' | 'pieces') {
|
||||
if (key === 'netWeightKg' && order.totalNetWeightKg != null) return order.totalNetWeightKg
|
||||
if (key === 'pieces' && order.totalQuantity != null) return order.totalQuantity
|
||||
if (!order.items.length) return null
|
||||
return order.items.reduce((total, item) => total + (Number(item[key]) || 0), 0)
|
||||
}
|
||||
|
||||
function formatListMetric(value: number | null | undefined, unit = '') {
|
||||
if (value == null) return '-'
|
||||
return `${value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })}${unit ? ` ${unit}` : ''}`
|
||||
}
|
||||
|
||||
function csvCell(value: unknown) {
|
||||
return `"${String(value ?? '').replaceAll('"', '""')}"`
|
||||
}
|
||||
|
||||
function downloadCsv(fileName: string, rows: unknown[][]) {
|
||||
const content = `\uFEFF${rows.map((row) => row.map(csvCell).join(',')).join('\r\n')}`
|
||||
const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }))
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function saveBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function handleOrderSelectionChange(rows: FirstLegOrder[]) {
|
||||
selectedOrders.value = rows
|
||||
}
|
||||
|
||||
async function downloadShippingMarks() {
|
||||
if (downloadingMarks.value) return
|
||||
const orders = selectedOrders.value
|
||||
if (!orders.length) return
|
||||
downloadingMarks.value = true
|
||||
try {
|
||||
const result = await downloadMerchantOrderCartonLabelsBatch(orders.map((order) => order.id))
|
||||
saveBlob(result.blob, `批量箱唛-${orders.length}单.pdf`)
|
||||
if (result.failureCount > 0) {
|
||||
ElMessage.warning(`箱唛已下载,${result.failureCount} 份订单生成失败:${result.failures}`)
|
||||
} else {
|
||||
ElMessage.success(`已下载 ${orders.length} 份订单箱唛`)
|
||||
}
|
||||
} catch (error) {
|
||||
const reason = error instanceof ApiError
|
||||
? `${error.message}${error.code ? ` [${error.code}]` : ''}${error.traceId ? `,追踪号 ${error.traceId}` : ''}`
|
||||
: (error instanceof Error ? error.message : '箱唛生成失败')
|
||||
ElMessage.error(reason)
|
||||
} finally {
|
||||
downloadingMarks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function exportOrderInformation() {
|
||||
exportingOrders.value = true
|
||||
try {
|
||||
const rows = selectedOrders.value.map((order) => [
|
||||
displayListValue(order.orderNo),
|
||||
displayListValue(order.businessSummary.inboundCode),
|
||||
displayListValue(order.channelName),
|
||||
displayListValue(order.destinationWarehouseName),
|
||||
getFirstLegStatusLabel(order.status),
|
||||
formatListMetric(orderPackageCount(order)),
|
||||
displayListValue(order.expressCompanyName),
|
||||
displayListValue(order.trackingNumber),
|
||||
uniqueItemValues(order, 'sku'),
|
||||
formatListMetric(orderGrossWeight(order), 'kg'),
|
||||
formatListMetric(orderItemTotal(order, 'netWeightKg'), 'kg'),
|
||||
formatListMetric(orderVolume(order), 'm³'),
|
||||
formatListMetric(orderItemTotal(order, 'pieces')),
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
displayListValue(order.customerRemark),
|
||||
displayListValue(order.shipmentId || uniqueItemValues(order, 'shipmentId')),
|
||||
displayListValue(order.referenceNo || uniqueItemValues(order, 'referenceId')),
|
||||
formatExportDateTime(order.inboundedAt),
|
||||
formatExportDateTime(order.businessDate),
|
||||
formatExportDateTime(order.createdAt),
|
||||
])
|
||||
downloadCsv('订单信息.csv', [[
|
||||
'订单号', '入仓码', '渠道', '目的仓', '当前订单状态', '箱数', '快递公司', '快递单号', 'SKUS',
|
||||
'总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注',
|
||||
'货件编号', '跟踪编号', '国内仓入库时间', '业务日期', '创建时间',
|
||||
], ...rows])
|
||||
ElMessage.success(`已导出 ${rows.length} 条订单信息`)
|
||||
} finally {
|
||||
exportingOrders.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const allCargoSelected = computed(() => form.items.length > 0 && selectedCargoIds.value.length === form.items.length)
|
||||
const someCargoSelected = computed(() => selectedCargoIds.value.length > 0 && !allCargoSelected.value)
|
||||
|
||||
@@ -1240,16 +1448,21 @@ async function searchOrders(model: QueryModel) {
|
||||
tablePage.value = 1
|
||||
try {
|
||||
const statusValue = typeof model.status === 'string' ? model.status as FirstLegOrderStatus | '' : ''
|
||||
const keyword = typeof model.keyword === 'string' ? model.keyword.trim() : ''
|
||||
const orderNo = typeof model.orderNo === 'string' ? model.orderNo.trim() : ''
|
||||
const cargoKeyword = typeof model.cargoKeyword === 'string' ? model.cargoKeyword.trim() : ''
|
||||
const range = Array.isArray(createdRange) ? createdRange.filter((value): value is string => typeof value === 'string') : []
|
||||
await store.load({
|
||||
keyword: keyword || undefined,
|
||||
keyword: cargoKeyword || undefined,
|
||||
orderNo: orderNo || undefined,
|
||||
receivingWarehouseId: typeof model.receivingWarehouseId === 'string' && model.receivingWarehouseId ? model.receivingWarehouseId : undefined,
|
||||
mainStatus: statusValue && statusValue !== 'DRAFT' ? statusValue : undefined,
|
||||
draftOnly: statusValue === 'DRAFT',
|
||||
includeDrafts: !statusValue,
|
||||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||||
createdTo: range[1] ? `${range[1]}T23:59:59` : undefined,
|
||||
})
|
||||
selectedOrders.value = []
|
||||
orderTableVersion.value += 1
|
||||
} catch {
|
||||
// Store keeps the recoverable error state rendered below the query toolbar.
|
||||
}
|
||||
@@ -1259,12 +1472,45 @@ async function searchOrders(model: QueryModel) {
|
||||
<template>
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||||
<template v-if="!isCreatePage">
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders">
|
||||
<template #actions>
|
||||
<el-button class="query-export-button" type="primary" :loading="downloadingMarks" :disabled="selectedOrderCount === 0" @click="downloadShippingMarks"><Download :size="16" />下载唛头</el-button>
|
||||
<el-button class="query-export-button" type="primary" :loading="exportingOrders" :disabled="selectedOrderCount === 0" @click="exportOrderInformation"><Download :size="16" />导出订单信息</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
<div class="order-table-toolbar-actions">
|
||||
<el-popover placement="bottom-end" :width="320" trigger="click" popper-class="order-column-popper">
|
||||
<template #reference>
|
||||
<el-button class="column-settings-trigger" aria-label="设置订单列表显示列" title="设置显示列"><SlidersHorizontal :size="18" /></el-button>
|
||||
</template>
|
||||
<div class="column-settings-panel">
|
||||
<p>拖动调整顺序,也可使用上下移动按钮。</p>
|
||||
<el-checkbox-group v-model="visibleColumnKeys" aria-label="订单列表显示列">
|
||||
<div
|
||||
v-for="(column, index) in orderedSelectableColumns"
|
||||
:key="String(column.key)"
|
||||
class="column-settings-item"
|
||||
draggable="true"
|
||||
@dragstart="draggedColumnKey = String(column.key)"
|
||||
@dragend="draggedColumnKey = ''"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropOrderColumn(String(column.key))"
|
||||
>
|
||||
<GripVertical :size="17" aria-hidden="true" />
|
||||
<el-checkbox :value="String(column.key)">{{ column.label }}</el-checkbox>
|
||||
<el-button link :disabled="index === 0" :aria-label="`上移${column.label}`" :title="`上移${column.label}`" @click="moveOrderColumn(String(column.key), -1)"><ChevronUp :size="17" /></el-button>
|
||||
<el-button link :disabled="index === orderedSelectableColumns.length - 1" :aria-label="`下移${column.label}`" :title="`下移${column.label}`" @click="moveOrderColumn(String(column.key), 1)"><ChevronDown :size="17" /></el-button>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-button link class="column-settings-reset" @click="resetOrderColumns"><RotateCcw :size="17" />恢复默认</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||||
<AlertTriangle :size="20" />
|
||||
@@ -1283,7 +1529,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="desktop-order-table">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单">
|
||||
<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="filteredOrders.length" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" @selection-change="handleOrderSelectionChange">
|
||||
<template #actions="{ row }">
|
||||
<div class="order-row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||||
@@ -1291,14 +1537,29 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
</template>
|
||||
<template #order="{ row }">
|
||||
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||||
<span v-if="(row as FirstLegOrder).businessSummary.inboundCode" class="order-reference">入仓码 {{ (row as FirstLegOrder).businessSummary.inboundCode }}</span>
|
||||
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无 Reference ID' }}</span>
|
||||
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '未提交草稿'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||||
</template>
|
||||
<template #route="{ row }"><strong>{{ (row as FirstLegOrder).destination || '目的地待更新' }}</strong><span class="order-reference">{{ (row as FirstLegOrder).transportMode ? getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) : (row as FirstLegOrder).channelName || '渠道待更新' }}</span></template>
|
||||
<template #cargo="{ row }">{{ getOrderCargoSummary(row as FirstLegOrder) }}</template>
|
||||
<template #inboundCode="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).businessSummary.inboundCode)">{{ displayListValue((row as FirstLegOrder).businessSummary.inboundCode) }}</span></template>
|
||||
<template #channel="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).channelName)">{{ displayListValue((row as FirstLegOrder).channelName) }}</span></template>
|
||||
<template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template>
|
||||
<template #status="{ row }"><el-tag :type="getFirstLegStatusTone((row as FirstLegOrder).status)" effect="light">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</el-tag></template>
|
||||
<template #updatedAt="{ row }">{{ formatDateTime((row as FirstLegOrder).updatedAt) }}</template>
|
||||
<template #packageCount="{ row }">{{ formatListMetric(orderPackageCount(row as FirstLegOrder)) }}</template>
|
||||
<template #expressCompany="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).expressCompanyName)">{{ displayListValue((row as FirstLegOrder).expressCompanyName) }}</span></template>
|
||||
<template #trackingNumber="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).trackingNumber)">{{ displayListValue((row as FirstLegOrder).trackingNumber) }}</span></template>
|
||||
<template #skus="{ row }"><span class="table-ellipsis table-code" :title="uniqueItemValues(row as FirstLegOrder, 'sku')">{{ uniqueItemValues(row as FirstLegOrder, 'sku') }}</span></template>
|
||||
<template #grossWeight="{ row }">{{ formatListMetric(orderGrossWeight(row as FirstLegOrder), 'kg') }}</template>
|
||||
<template #netWeight="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'netWeightKg'), 'kg') }}</template>
|
||||
<template #volume="{ row }">{{ formatListMetric(orderVolume(row as FirstLegOrder), 'm³') }}</template>
|
||||
<template #pieces="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'pieces')) }}</template>
|
||||
<template #billingUnitPrice>-</template>
|
||||
<template #chargeableVolume>-</template>
|
||||
<template #chargeableWeight>-</template>
|
||||
<template #remark="{ row }"><span class="table-ellipsis" :title="(row as FirstLegOrder).customerRemark">{{ displayListValue((row as FirstLegOrder).customerRemark) }}</span></template>
|
||||
<template #shipmentId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId'))">{{ displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId')) }}</span></template>
|
||||
<template #referenceId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId'))">{{ displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId')) }}</span></template>
|
||||
<template #receivingTime="{ row }">{{ (row as FirstLegOrder).inboundedAt ? formatDateTime((row as FirstLegOrder).inboundedAt) : '-' }}</template>
|
||||
<template #businessDate>-</template>
|
||||
<template #createdAt="{ row }">{{ formatDateTime((row as FirstLegOrder).createdAt) }}</template>
|
||||
<template #empty>
|
||||
<el-empty :description="emptyDescription">
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ pageDataLoading ? '加载收货仓中' : (availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓') }}</el-button>
|
||||
@@ -1332,7 +1593,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
|
||||
|
||||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" :class="{ 'is-dialog-order-form': !isCreatePage }" scroll-to-error>
|
||||
<section class="order-form-section prototype-section">
|
||||
<div class="order-section-heading"><div><h3>客户下单</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
|
||||
<div class="prototype-form-flow shipping-form-flow">
|
||||
@@ -1448,7 +1709,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||||
</FirstLegOrderEditorShell>
|
||||
|
||||
<el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="820px">
|
||||
<el-dialog v-model="detailOpen" class="first-leg-detail-dialog" title="订单详情" width="94vw" top="3vh" destroy-on-close append-to-body>
|
||||
<div v-loading="detailLoading" class="order-detail-content">
|
||||
<div v-if="detailError" class="order-load-state is-compact" role="alert">
|
||||
<AlertTriangle :size="24" />
|
||||
@@ -1527,24 +1788,44 @@ async function searchOrders(model: QueryModel) {
|
||||
<section class="detail-section">
|
||||
<h3>货物明细</h3>
|
||||
<div v-if="detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div>
|
||||
<div v-if="detailOrder.items.length" class="cargo-detail-list">
|
||||
<article v-for="(item, index) in detailOrder.items" :key="item.id">
|
||||
<header><span>货物 {{ index + 1 }}</span><strong>{{ item.cargoNameCn || '未命名货物' }}</strong><small>{{ item.cargoNameEn || '' }}</small></header>
|
||||
<div v-if="item.productImageFileId" class="cargo-detail-image"><img v-if="item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><span v-else-if="item.productImageUploading">图片加载中...</span><span v-else>{{ item.productImageError || '产品图片暂不可用' }}</span></div>
|
||||
<dl>
|
||||
<div><dt>SKU NO.</dt><dd>{{ item.sku || '-' }}</dd></div><div><dt>UPC NO.</dt><dd>{{ item.upc || '-' }}</dd></div>
|
||||
<div><dt>报关 HS CODE</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>清关 HS CODE</dt><dd>{{ item.customsHsCode || '-' }}</dd></div>
|
||||
<div><dt>箱规(长×宽×高)</dt><dd>{{ item.lengthCm || '-' }} × {{ item.widthCm || '-' }} × {{ item.heightCm || '-' }} cm</dd></div>
|
||||
<div><dt>每箱数量</dt><dd>{{ item.unitsPerPackage }} 件</dd></div><div><dt>箱数</dt><dd>{{ item.packageCount }} 箱</dd></div><div><dt>总数量</dt><dd>{{ item.pieces }} 件</dd></div>
|
||||
<div><dt>总毛重</dt><dd>{{ item.grossWeightKg }} kg</dd></div><div><dt>总净重</dt><dd>{{ item.netWeightKg ? `${item.netWeightKg} kg` : '-' }}</dd></div><div><dt>总体积</dt><dd>{{ item.volumeCbm }} m³</dd></div>
|
||||
<div><dt>清关申报单价</dt><dd>{{ item.unitDeclaredValue ? `USD ${item.unitDeclaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>清关申报金额</dt><dd>{{ item.declaredValue ? `USD ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div>
|
||||
<div><dt>中英文材质</dt><dd>{{ item.material || '-' }}</dd></div><div><dt>中英文用途</dt><dd>{{ item.usage || '-' }}</dd></div><div><dt>型号</dt><dd>{{ item.model || '-' }}</dd></div><div><dt>品牌</dt><dd>{{ item.brand || '-' }}</dd></div>
|
||||
<div><dt>产品规格(长×宽×高)</dt><dd>{{ item.productLengthCm || '-' }} × {{ item.productWidthCm || '-' }} × {{ item.productHeightCm || '-' }} cm</dd></div>
|
||||
<div><dt>Shipment ID</dt><dd>{{ item.shipmentId || '-' }}</dd></div><div><dt>Reference ID</dt><dd>{{ item.referenceId || '-' }}</dd></div>
|
||||
<div><dt>包装类型</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }}</dd></div><div><dt>危险品</dt><dd>{{ item.dangerousGoods ? '是' : '否' }}</dd></div>
|
||||
</dl>
|
||||
<p v-if="item.remark">{{ item.remark }}</p>
|
||||
</article>
|
||||
<div v-if="detailOrder.items.length" class="cargo-detail-table-wrap">
|
||||
<el-table :data="pagedDetailCargoItems" border stripe class="cargo-detail-table" row-key="id">
|
||||
<el-table-column type="index" label="#" width="54" align="center" :index="(index: number) => (detailCargoPage - 1) * detailCargoPageSize + index + 1" />
|
||||
<el-table-column label="SKU NO." width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.sku || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="中文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameCn || '-' }}</template></el-table-column>
|
||||
<el-table-column label="图片" width="66" align="center"><template #default="{ row }"><div class="cargo-detail-thumbnail"><img v-if="row.productImagePreviewUrl" :src="row.productImagePreviewUrl" :alt="`${row.cargoNameCn || '货物'}产品图片`" /><span v-else>-</span></div></template></el-table-column>
|
||||
<el-table-column label="英文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameEn || '-' }}</template></el-table-column>
|
||||
<el-table-column label="包装类型" width="100" align="center"><template #default="{ row }">{{ packageTypeLabels[row.packageType] || row.packageType || '-' }}</template></el-table-column>
|
||||
<el-table-column label="箱规 长×宽×高(cm)" width="170" align="center"><template #default="{ row }">{{ row.lengthCm || '-' }} × {{ row.widthCm || '-' }} × {{ row.heightCm || '-' }}</template></el-table-column>
|
||||
<el-table-column prop="unitsPerPackage" label="每箱数量" width="88" align="right" />
|
||||
<el-table-column prop="packageCount" label="箱数" width="72" align="right" />
|
||||
<el-table-column prop="pieces" label="总数量" width="82" align="right" />
|
||||
<el-table-column label="总毛重" width="94" align="right"><template #default="{ row }">{{ row.grossWeightKg }} kg</template></el-table-column>
|
||||
<el-table-column label="总净重" width="94" align="right"><template #default="{ row }">{{ row.netWeightKg ? `${row.netWeightKg} kg` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="总体积" width="98" align="right"><template #default="{ row }">{{ row.volumeCbm }} m³</template></el-table-column>
|
||||
<el-table-column label="报关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.hsCode || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="清关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.customsHsCode || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="申报单价" width="104" align="right"><template #default="{ row }">{{ row.unitDeclaredValue ? `USD ${row.unitDeclaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="申报金额" width="104" align="right"><template #default="{ row }">{{ row.declaredValue ? `USD ${row.declaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||||
<el-table-column label="Shipment ID" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.shipmentId || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="Reference ID" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.referenceId || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="UPC NO." width="130" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.upc || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="中英文材质" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.material || '-' }}</template></el-table-column>
|
||||
<el-table-column label="中英文用途" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.usage || '-' }}</template></el-table-column>
|
||||
<el-table-column label="型号" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.model || '-' }}</template></el-table-column>
|
||||
<el-table-column label="品牌" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.brand || '-' }}</template></el-table-column>
|
||||
<el-table-column label="产品规格 长×宽×高(cm)" width="180" align="center"><template #default="{ row }">{{ row.productLengthCm || '-' }} × {{ row.productWidthCm || '-' }} × {{ row.productHeightCm || '-' }}</template></el-table-column>
|
||||
<el-table-column label="危险品" width="82" align="center"><template #default="{ row }">{{ row.dangerousGoods ? '是' : '否' }}</template></el-table-column>
|
||||
<el-table-column label="备注" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '-' }}</template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="detailCargoPage"
|
||||
v-model:page-size="detailCargoPageSize"
|
||||
class="cargo-detail-pagination"
|
||||
:page-sizes="[5, 10, 20]"
|
||||
:total="detailOrder.items.length"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="暂无货物明细" :image-size="56" />
|
||||
</section>
|
||||
@@ -1560,7 +1841,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</template>
|
||||
</div>
|
||||
<template #footer><div v-if="detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button></div></template>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1568,10 +1849,14 @@ async function searchOrders(model: QueryModel) {
|
||||
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 0; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
.first-leg-query-toolbar { flex-wrap: nowrap; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; flex: 1 1 auto; grid-template-columns: minmax(180px, 1.4fr) minmax(120px, .72fr) minmax(210px, 1.15fr); }
|
||||
.first-leg-query-toolbar { display: block; padding: 16px 18px 14px; border: 0; border-radius: 10px; background: #fff; box-shadow: 0 3px 14px rgba(31, 48, 78, .05); }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 18px; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__field) { min-width: 0; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__field .el-input), .first-leg-query-toolbar :deep(.query-toolbar__field .el-select), .first-leg-query-toolbar :deep(.query-toolbar__field .el-date-editor) { flex: 1 1 auto; width: 100%; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters .el-input), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-select), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-date-editor) { width: 100%; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions) { align-self: flex-start; flex-wrap: nowrap; margin-left: auto; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions) { justify-content: flex-start; margin-top: 14px; padding-top: 12px; border-top: 1px solid #edf0f5; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions .el-button) { min-height: 36px; margin-left: 0; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions .query-export-button) { order: -1; }
|
||||
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
|
||||
.order-eligibility svg { flex: 0 0 20px; margin-top: 1px; }
|
||||
.order-eligibility div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
|
||||
@@ -1588,6 +1873,35 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-table-toolbar > div { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
|
||||
.order-table-toolbar strong { color: var(--navy); font-size: 14px; }
|
||||
.order-table-toolbar span { color: var(--muted); font-size: 11px; }
|
||||
.order-table-toolbar > .order-table-toolbar-actions { align-items: center; gap: 8px; }
|
||||
.order-table-toolbar-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.column-settings-trigger { width: 40px; min-width: 40px; height: 36px; padding: 0; color: #53627b; }
|
||||
:global(.order-column-popper.el-popover) { padding: 0; border-color: #e3e8f0; border-radius: 4px; box-shadow: 0 8px 24px rgba(29, 45, 74, .12); }
|
||||
:global(.order-column-popper .column-settings-panel) { padding: 14px 10px 10px; }
|
||||
:global(.order-column-popper .column-settings-panel > p) { margin: 0 8px 10px; color: #778398; font-size: 12px; line-height: 1.5; }
|
||||
:global(.order-column-popper .el-checkbox-group) { max-height: 430px; overflow-y: auto; padding: 0 2px; scrollbar-width: thin; }
|
||||
:global(.order-column-popper .column-settings-item) { display: grid; min-height: 42px; grid-template-columns: 24px minmax(0, 1fr) 28px 28px; align-items: center; padding: 0 4px; color: #596982; }
|
||||
:global(.order-column-popper .column-settings-item:hover) { background: #f7f8fb; }
|
||||
:global(.order-column-popper .column-settings-item > svg) { cursor: grab; color: #71809a; }
|
||||
:global(.order-column-popper .column-settings-item > svg:active) { cursor: grabbing; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox) { height: 42px; margin-right: 0; overflow: hidden; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox__label) { overflow: hidden; color: #4f5f78; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
:global(.order-column-popper .column-settings-item .el-checkbox.is-checked .el-checkbox__label) { color: var(--primary); }
|
||||
:global(.order-column-popper .column-settings-item .el-button) { width: 28px; height: 28px; padding: 0; margin: 0; color: #45556e; }
|
||||
:global(.order-column-popper .column-settings-reset) { min-height: 36px; margin: 8px 6px 0; color: #40516b; }
|
||||
.prototype-order-table { font-size: 11px; }
|
||||
.prototype-order-table :deep(.el-table) { color: #626b78; font-size: 11px; }
|
||||
.prototype-order-table :deep(.el-table__header-wrapper th.el-table__cell) { height: 44px; padding: 0; background: #fafbfc; color: #687386; font-size: 11px; font-weight: 650; vertical-align: middle; }
|
||||
.prototype-order-table :deep(.el-table__body-wrapper td.el-table__cell) { height: 50px; padding: 0; vertical-align: middle; }
|
||||
.prototype-order-table :deep(.el-table .cell) { min-width: 0; padding: 0 12px; line-height: 18px; white-space: nowrap; }
|
||||
.prototype-order-table :deep(.el-table__header-wrapper .cell) { overflow: visible; line-height: 16px; text-overflow: clip; }
|
||||
.prototype-order-table :deep(.el-table__body-wrapper .cell) { overflow: hidden; }
|
||||
.prototype-order-table :deep(.el-tag) { height: 24px; padding: 0 9px; line-height: 22px; white-space: nowrap; }
|
||||
.prototype-order-table :deep(.el-table__row:nth-child(even) td.el-table__cell) { background: #fcfcfd; }
|
||||
.prototype-order-table :deep(.el-table__row:hover > td.el-table__cell) { background: #f6f8fc; }
|
||||
.prototype-order-table :deep(.data-table__pagination) { min-height: 54px; padding: 10px 16px; }
|
||||
.table-ellipsis, .table-multivalue { display: block; width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.table-code { font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||||
.order-table-card > .order-eligibility, .order-table-card > .supporting-data-error { border-width: 0 0 1px; }
|
||||
.order-load-state { display: grid; min-height: 220px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 28px; color: #9a5a09; }
|
||||
.order-load-state > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||
@@ -1595,20 +1909,27 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-load-state span { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||||
.order-load-state.is-compact { min-height: 180px; }
|
||||
.order-detail-content { min-height: 180px; }
|
||||
:global(.first-leg-detail-dialog.el-dialog) { display: flex; max-width: 1500px; height: 94vh; margin-bottom: 0; flex-direction: column; overflow: hidden; border-radius: 6px; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__header) { flex: 0 0 auto; margin-right: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-detail-dialog .el-dialog__title) { color: var(--navy); font-size: 16px; font-weight: 700; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__headerbtn) { top: 8px; right: 10px; width: 44px; height: 44px; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 22px 24px 28px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
.mobile-order-surface { display: none; min-height: 220px; }
|
||||
.mobile-order-list { display: none; }
|
||||
.order-number-link { display: block; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-weight: 700; text-align: left; }
|
||||
.order-number-link { display: block; width: 100%; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-size: 11px; font-weight: 700; line-height: 18px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||||
.order-reference { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
|
||||
:global(.first-leg-editor .el-drawer__header) { flex: 0 0 auto; min-height: 58px; margin-bottom: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-editor .el-drawer__body) { min-height: 0; padding: 18px 22px 32px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
.order-row-actions { align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
|
||||
.order-row-actions :deep(.el-button) { margin-left: 0; padding: 3px 1px; font-size: 11px; }
|
||||
.is-create-route { gap: 0; }
|
||||
.is-create-route .first-leg-editor-page { width: 100%; }
|
||||
.is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; }
|
||||
.is-create-route .prototype-order-form { width: 100%; margin: 0; padding: 0 0 16px; }
|
||||
.is-create-route .prototype-section:not(.cargo-section):not(.cost-estimate-section):not(.destination-section) { width: min(100%, 900px); }
|
||||
.order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; }
|
||||
.order-form.is-dialog-order-form { width: 100%; max-width: none; }
|
||||
.is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; }
|
||||
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
|
||||
.order-form-section + .order-form-section { margin-top: 30px; padding-top: 28px; border-top: 1px solid #edf0f5; }
|
||||
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
|
||||
@@ -1794,20 +2115,19 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-summary div { display: flex; min-height: 74px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border-right: 1px solid var(--border); }
|
||||
.cargo-summary div:last-child { border-right: 0; }
|
||||
.cargo-summary span { color: var(--muted); font-size: 11px; }
|
||||
.cargo-detail-list { display: grid; gap: 10px; margin-top: 10px; }
|
||||
.cargo-detail-list article { padding: 14px 15px; border: 1px solid var(--border); }
|
||||
.cargo-detail-list header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: 10px; }
|
||||
.cargo-detail-list header span, .cargo-detail-list header small { color: var(--muted); font-size: 10px; }
|
||||
.cargo-detail-list header strong { overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
|
||||
.cargo-detail-list header small { overflow-wrap: anywhere; text-align: right; }
|
||||
.cargo-detail-list dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px 16px; margin: 13px 0 0; }
|
||||
.cargo-detail-list dt { color: var(--muted); font-size: 10px; }
|
||||
.cargo-detail-list dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 600; }
|
||||
.cargo-detail-list article > p { margin: 11px 0 0; padding-top: 10px; border-top: 1px solid var(--border); color: var(--muted); font-size: 11px; }
|
||||
.cargo-detail-table-wrap { overflow: hidden; margin-top: 10px; border: 1px solid var(--border); border-radius: 5px; background: #fff; }
|
||||
.cargo-detail-table { width: 100%; font-size: 11px; }
|
||||
.cargo-detail-table :deep(.el-table__header th.el-table__cell) { height: 42px; padding: 0; background: #f7f8fb; color: #657187; font-size: 11px; font-weight: 650; white-space: nowrap; }
|
||||
.cargo-detail-table :deep(.el-table__body td.el-table__cell) { height: 48px; padding: 0; color: #4f5b6d; vertical-align: middle; }
|
||||
.cargo-detail-table :deep(.cell) { min-width: 0; padding: 0 10px; line-height: 18px; white-space: nowrap; }
|
||||
.cargo-detail-thumbnail { display: flex; width: 34px; height: 34px; align-items: center; justify-content: center; margin: 0 auto; overflow: hidden; border: 1px solid #e1e6ee; border-radius: 3px; color: var(--muted); }
|
||||
.cargo-detail-thumbnail img { width: 100%; height: 100%; object-fit: contain; }
|
||||
.cargo-detail-pagination { min-height: 52px; justify-content: flex-end; box-sizing: border-box; padding: 9px 14px; border-top: 1px solid var(--border); }
|
||||
:deep(.el-timeline-item__content p), :deep(.el-timeline-item__content > span) { color: var(--muted); }
|
||||
:deep(.el-timeline-item__content p) { margin: 5px 0; font-size: 12px; }
|
||||
:deep(.el-timeline-item__content > span) { font-size: 11px; }
|
||||
@media (max-width: 1280px) {
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.desktop-order-table { display: none; }
|
||||
.mobile-order-surface { display: block; }
|
||||
.mobile-order-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 10px; }
|
||||
@@ -1824,12 +2144,11 @@ async function searchOrders(model: QueryModel) {
|
||||
.mobile-order-card footer > div { display: flex; }
|
||||
}
|
||||
@media (min-width: 721px) and (max-width: 1050px) {
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
|
||||
.cargo-form-grid, .cargo-form-grid--basic { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.product-image-field { grid-row: auto; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.first-leg-query-toolbar { flex-wrap: wrap; }
|
||||
.first-leg-query-toolbar { padding: 12px; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: flex; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 10px; }
|
||||
.order-table-toolbar { align-items: stretch; flex-direction: column; gap: 10px; }
|
||||
@@ -1858,9 +2177,6 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-import-actions, .cargo-table-actions { flex-wrap: wrap; }
|
||||
.cost-estimate-cards { grid-template-columns: 1fr; }
|
||||
.cost-estimate-card { min-height: 140px; }
|
||||
:global(.first-leg-editor .el-drawer__header) { min-height: 56px; padding: 15px 20px; }
|
||||
:global(.first-leg-editor .el-drawer__body) { padding: 16px 20px 28px; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { padding: 10px 20px; }
|
||||
.form-span-full { grid-column: auto; }
|
||||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||||
.desktop-order-table { display: none; }
|
||||
@@ -1886,16 +2202,15 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cargo-summary div:nth-child(2) { border-right: 0; }
|
||||
.cargo-summary div:nth-child(-n+2) { border-bottom: 1px solid var(--border); }
|
||||
.cargo-detail-list header { grid-template-columns: 1fr; gap: 4px; }
|
||||
.cargo-detail-list header small { text-align: left; }
|
||||
.cargo-detail-list dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.cabinet-progress-card dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
:global(.first-leg-editor), :global(.first-leg-detail) { width: 100% !important; }
|
||||
:global(.first-leg-detail-dialog.el-dialog) { width: calc(100vw - 24px) !important; height: 96vh; margin-top: 2vh; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.cargo-detail-list dl { grid-template-columns: 1fr; }
|
||||
.cargo-detail-pagination { justify-content: flex-start; overflow-x: auto; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
:global(.first-leg-detail-dialog.el-dialog) { width: 100vw !important; height: 100vh; margin: 0; border-radius: 0; }
|
||||
:global(.first-leg-detail-dialog .el-dialog__body) { padding: 16px 12px 22px; }
|
||||
.editor-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.editor-actions :deep(.el-button) { width: 100%; min-width: 0; margin-left: 0; }
|
||||
.editor-actions :deep(.el-button:nth-child(3)) { grid-column: 1 / -1; }
|
||||
|
||||
Reference in New Issue
Block a user