Merge remote-tracking branch 'origin/release/test' into dev
This commit is contained in:
@@ -44,6 +44,7 @@ export function createPaginationItems(page: number, pageCount: number): Paginati
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots, watch } from 'vue'
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/vue'
|
||||
import { splitTableActions, runTableAction, type TableAction } from './tableActions'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
@@ -54,6 +55,7 @@ const props = withDefaults(defineProps<{
|
||||
emptyText?: string
|
||||
total?: number
|
||||
pageSizes?: number[]
|
||||
rowActions?: (row: object) => TableAction[]
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: '暂无数据',
|
||||
@@ -140,8 +142,15 @@ function setPage(value: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<el-table v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :aria-busy="loading">
|
||||
<div class="data-table" :class="$attrs.class">
|
||||
<header v-if="$slots.toolbar || $slots['toolbar-actions']" class="data-table__toolbar">
|
||||
<div class="data-table__toolbar-heading"><slot name="toolbar" /></div>
|
||||
<div class="data-table__toolbar-actions"><slot name="toolbar-actions" /></div>
|
||||
</header>
|
||||
<slot name="before-table" />
|
||||
<slot v-if="$slots.error" name="error" />
|
||||
<template v-else>
|
||||
<el-table v-bind="{ ...$attrs, class: undefined }" v-loading="loading" :data="data" :empty-text="emptyText" :aria-busy="loading">
|
||||
<el-table-column
|
||||
v-for="(column, index) in columns"
|
||||
:key="columnRenderKey(column, index)"
|
||||
@@ -152,7 +161,26 @@ function setPage(value: number) {
|
||||
<template v-if="hasSlot(column.headerSlotName)" #header="scope">
|
||||
<slot :name="column.headerSlotName" v-bind="scope" />
|
||||
</template>
|
||||
<template v-if="hasSlot(column.slotName)" #default="scope">
|
||||
<template v-if="column.slotName === 'actions' && rowActions" #default="{ row }">
|
||||
<div class="data-table__row-actions">
|
||||
<el-button v-for="action in splitTableActions(rowActions(row)).visible" :key="action.key"
|
||||
:disabled="action.disabled" :loading="action.loading" @click="runTableAction(action)">
|
||||
{{ action.label }}
|
||||
</el-button>
|
||||
<el-dropdown v-if="splitTableActions(rowActions(row)).overflow.length" trigger="click">
|
||||
<el-button class="data-table__more" aria-label="更多操作" title="更多操作">…</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="action in splitTableActions(rowActions(row)).overflow" :key="action.key"
|
||||
:disabled="action.disabled || action.loading" @click="runTableAction(action)">
|
||||
{{ action.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="hasSlot(column.slotName)" #default="scope">
|
||||
<slot :name="column.slotName" v-bind="scope" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -192,15 +220,30 @@ function setPage(value: number) {
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.data-table { min-width: 0; }
|
||||
.data-table__toolbar { display: flex; min-height: 48px; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--client-gap); padding: var(--client-toolbar-padding); border-bottom: 1px solid var(--client-border); }
|
||||
.data-table__toolbar-heading, .data-table__toolbar-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--client-gap); }
|
||||
.data-table__toolbar-heading { color: var(--client-muted); font-size: 12px; }
|
||||
.data-table__toolbar-heading :slotted(strong) { color: var(--client-text); font-size: 14px; }
|
||||
.data-table__toolbar-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.data-table__row-actions { display: flex; align-items: center; gap: var(--client-gap); white-space: nowrap; }
|
||||
.data-table__row-actions :deep(.el-button) { height: var(--client-row-action-height); padding: 0 8px; margin: 0; }
|
||||
.data-table :deep(.data-table__system-cell .data-table__row-actions .el-button + .el-button), .data-table :deep(.data-table__system-cell .data-table__row-actions .el-button + .el-dropdown) { margin-left: 0; }
|
||||
.data-table__more { min-width: 28px; }
|
||||
:global(.client-ui .data-table .el-table) { color: var(--client-text); font-size: var(--client-font-size); --el-table-border-color: var(--client-border); --el-table-header-bg-color: var(--client-header-bg); --el-table-row-hover-bg-color: var(--client-hover-bg); }
|
||||
:global(.client-ui .data-table th.el-table__cell) { height: var(--client-table-header-height); padding: 0; color: var(--client-muted); font-weight: 600; }
|
||||
:global(.client-ui .data-table td.el-table__cell) { height: var(--client-table-row-height); padding: 0; }
|
||||
:global(.client-ui .data-table .el-table .cell) { padding: 0 12px; line-height: 20px; }
|
||||
:global(.client-ui .data-table .el-table__row:nth-child(even)) { --el-table-tr-bg-color: #fcfcfd; }
|
||||
.data-table :deep(.data-table__single-line-cell .cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .cell) { white-space: nowrap; }
|
||||
.data-table :deep(.data-table__system-cell .el-button + .el-button), .data-table :deep(.data-table__system-cell .el-button + .el-dropdown) { margin-left: 8px; }
|
||||
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
|
||||
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: var(--client-pagination-padding); border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
|
||||
.data-table__pagination-left { display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; }
|
||||
.data-table__pagination-controls, .data-table__page-size, .data-table__page-buttons { display: flex; align-items: center; gap: 8px; }
|
||||
.data-table__pagination-controls { min-width: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
|
||||
@@ -23,12 +23,14 @@ export interface QueryField {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
layout?: 'inline' | 'grid'
|
||||
ariaLabel: string
|
||||
fields: QueryField[]
|
||||
searchText?: string
|
||||
resetText?: string
|
||||
searchOnReset?: boolean
|
||||
}>(), {
|
||||
layout: 'inline',
|
||||
searchText: '查询',
|
||||
resetText: '重置',
|
||||
searchOnReset: true,
|
||||
@@ -77,6 +79,7 @@ defineExpose({ search, reset })
|
||||
<section
|
||||
v-bind="$attrs"
|
||||
class="query-toolbar"
|
||||
:class="{ 'query-toolbar--grid': layout === 'grid' }"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<div class="query-toolbar__filters">
|
||||
@@ -133,5 +136,11 @@ defineExpose({ search, reset })
|
||||
.query-toolbar__filters :deep(.el-input), .query-toolbar__filters :deep(.el-select), .query-toolbar__filters :deep(.el-date-editor) { width: 260px; max-width: 100%; min-width: 0; }
|
||||
.query-toolbar__actions { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-start; gap: 8px; }
|
||||
.query-toolbar__actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.query-toolbar--grid { display: block; padding: var(--client-query-padding); border: 1px solid var(--client-border); border-radius: var(--client-radius); }
|
||||
.query-toolbar--grid .query-toolbar__filters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 16px; }
|
||||
.query-toolbar--grid .query-toolbar__field { min-width: 0; }
|
||||
.query-toolbar--grid .query-toolbar__field :deep(.el-input), .query-toolbar--grid .query-toolbar__field :deep(.el-select), .query-toolbar--grid .query-toolbar__field :deep(.el-date-editor) { flex: 1; width: 100%; min-width: 0; }
|
||||
.query-toolbar--grid .query-toolbar__actions { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--client-border); }
|
||||
@media (max-width: 1200px) { .query-toolbar--grid .query-toolbar__filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .query-toolbar { align-items: stretch; flex-direction: column; } .query-toolbar__filters { width: 100%; flex-direction: column; } .query-toolbar__field { display: grid; gap: 6px; } .query-toolbar__filters :deep(.el-input), .query-toolbar__filters :deep(.el-select), .query-toolbar__filters :deep(.el-date-editor) { width: 100%; } .query-toolbar__actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); width: 100%; } .query-toolbar__actions :deep(.el-button) { min-height: 40px; margin-left: 0; } }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { splitTableActions, runTableAction } from './tableActions.ts'
|
||||
|
||||
test('shows up to three permitted actions and folds only when there are more', () => {
|
||||
const actions = Array.from({ length: 5 }, (_, index) => ({ key: String(index), label: String(index), run() {} }))
|
||||
for (const count of [0, 1, 2, 3]) {
|
||||
const result = splitTableActions(actions.slice(0, count))
|
||||
assert.equal(result.visible.length, count)
|
||||
assert.equal(result.overflow.length, 0)
|
||||
}
|
||||
for (const count of [4, 5]) {
|
||||
const result = splitTableActions(actions.slice(0, count))
|
||||
assert.deepEqual(result.visible, actions.slice(0, 2))
|
||||
assert.deepEqual(result.overflow, actions.slice(2, count))
|
||||
}
|
||||
})
|
||||
|
||||
test('disabled and loading actions cannot execute or disappear from the layout', () => {
|
||||
let count = 0
|
||||
const action = { key: 'edit', label: '编辑', run() { count++ } }
|
||||
runTableAction({ ...action, disabled: true })
|
||||
runTableAction({ ...action, loading: true })
|
||||
assert.equal(count, 0)
|
||||
runTableAction(action)
|
||||
assert.equal(count, 1)
|
||||
assert.equal(splitTableActions([{ ...action, disabled: true }]).visible.length, 1)
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface TableAction {
|
||||
key: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
run: () => unknown
|
||||
}
|
||||
|
||||
// Callers supply only permitted actions; disabled actions retain their place.
|
||||
export function splitTableActions(actions: readonly TableAction[]) {
|
||||
return actions.length > 3
|
||||
? { visible: actions.slice(0, 2), overflow: actions.slice(2) }
|
||||
: { visible: actions, overflow: [] }
|
||||
}
|
||||
|
||||
export function runTableAction(action: TableAction) {
|
||||
if (!action.disabled && !action.loading) return action.run()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import test from 'node:test'
|
||||
const config = readFileSync(new URL('../../nginx/default.conf.template', import.meta.url), 'utf8')
|
||||
const testCompose = readFileSync(new URL('../../docker-compose.test.yml', import.meta.url), 'utf8')
|
||||
const developmentCompose = readFileSync(new URL('../../docker-compose.development.yml', import.meta.url), 'utf8')
|
||||
const productionCompose = readFileSync(new URL('../../docker-compose.production.yml', import.meta.url), 'utf8')
|
||||
const jenkinsfile = readFileSync(new URL('../../Jenkinsfile', import.meta.url), 'utf8')
|
||||
const dockerfile = readFileSync(new URL('../../Dockerfile', import.meta.url), 'utf8')
|
||||
|
||||
@@ -21,8 +22,10 @@ test('does not cache the SPA entry while keeping hashed assets immutable', () =>
|
||||
})
|
||||
|
||||
test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => {
|
||||
assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/)
|
||||
assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/)
|
||||
assert.match(testCompose, /- "host\.docker\.internal:host-gateway"/)
|
||||
assert.match(testCompose, /API_UPSTREAM: "\$\{API_UPSTREAM:-http:\/\/host\.docker\.internal:9000\}"/)
|
||||
assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "\$\{OVERSEAS_API_UPSTREAM:-http:\/\/host\.docker\.internal:9300\}"/)
|
||||
assert.doesNotMatch(testCompose, /http:\/\/test-api-(?:cn|global)\.buggoglobal\.com/)
|
||||
assert.match(testCompose, /VITE_ADMIN_WEB_ORIGIN: "\$\{VITE_ADMIN_WEB_ORIGIN:-https:\/\/test-admin\.buggoglobal\.com\}"/)
|
||||
assert.match(config, /location \/overseas-api\/ \{[\s\S]*?rewrite \^\/overseas-api\/\(\.\*\)\$ \/api\/\$1 break;[\s\S]*?proxy_pass \$\{OVERSEAS_API_UPSTREAM\};/)
|
||||
assert.match(config, /proxy_set_header Host \$proxy_host;/)
|
||||
@@ -38,11 +41,16 @@ test('connects the development client proxy to both development gateways', () =>
|
||||
assert.doesNotMatch(developmentCompose, /test-api|prod(?:uction)?/i)
|
||||
})
|
||||
|
||||
test('runs each deployment on its target node without controller-local SSH files', () => {
|
||||
assert.match(jenkinsfile, /env\.JOB_BASE_NAME\?\.endsWith\('-dev'\) \? 'java-dev' : 'java-test'/)
|
||||
test('keeps local environments on target nodes and uses credential-managed SSH for production', () => {
|
||||
assert.match(jenkinsfile, /env\.JOB_BASE_NAME\?\.endsWith\('-prod'\) \? 'java-builder'/)
|
||||
assert.match(jenkinsfile, /DEVELOPMENT_COMPOSE_PROJECT = 'client-web'/)
|
||||
assert.match(jenkinsfile, /docker compose -p "\$DEVELOPMENT_COMPOSE_PROJECT" -f "\$DEVELOPMENT_COMPOSE_FILE" up -d/)
|
||||
assert.doesNotMatch(jenkinsfile, /\/var\/jenkins_home\/\.ssh|docker save|\bssh\b/)
|
||||
assert.doesNotMatch(jenkinsfile, /\/var\/jenkins_home\/\.ssh|docker save/)
|
||||
assert.match(jenkinsfile, /credentialsId: 'firstleg-production-ssh'/)
|
||||
assert.match(jenkinsfile, /git rev-parse origin\/main/)
|
||||
assert.match(productionCompose, /127\.0\.0\.1:\$\{HOST_PORT:-9002\}:80/)
|
||||
assert.match(productionCompose, /API_UPSTREAM: "http:\/\/gateway:9000"/)
|
||||
assert.match(productionCompose, /name: firstleg-prod-infra-network/)
|
||||
})
|
||||
|
||||
test('uses authenticated internal base images in CI', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type TransportDetail,
|
||||
type TransportSummary,
|
||||
} from '../api/businessOrders'
|
||||
import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '../containerSpecifications'
|
||||
|
||||
const props = defineProps<{ kind: CrossBorderBusinessKind }>()
|
||||
const auth = useAuthStore()
|
||||
@@ -155,7 +156,7 @@ function emptyForm(): BusinessForm {
|
||||
return {
|
||||
purchaseCategory: '', productName: '', productSpecification: '', productUrl: '', purchaseQuantity: 1,
|
||||
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
|
||||
destination: '', destinationAddress: '', containerType: '', palletCount: 1, packageCount: 0,
|
||||
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0,
|
||||
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
|
||||
receivingAddress: '', remark: '',
|
||||
}
|
||||
@@ -328,7 +329,7 @@ onMounted(() => void load())
|
||||
<el-form-item label="起运地址"><el-input v-model.trim="form.originAddress" maxlength="500" placeholder="请输入详细起运地址" /></el-form-item>
|
||||
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="请输入目的地" /></el-form-item>
|
||||
<el-form-item label="目的地址"><el-input v-model.trim="form.destinationAddress" maxlength="500" placeholder="请输入详细目的地址" /></el-form-item>
|
||||
<el-form-item label="柜型"><el-input v-model.trim="form.containerType" maxlength="32" placeholder="请输入柜型" /></el-form-item>
|
||||
<el-form-item label="柜型"><el-select v-model="form.containerType"><el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></el-select></el-form-item>
|
||||
<el-form-item v-if="kind === 'trucking'" label="托盘数" prop="palletCount"><el-input-number v-model="form.palletCount" :min="1" placeholder="请输入托盘数" /></el-form-item>
|
||||
<el-form-item label="件数"><el-input-number v-model="form.packageCount" :min="0" placeholder="请输入件数" /></el-form-item>
|
||||
<el-form-item label="总重量(kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" placeholder="请输入总重量" /></el-form-item>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const CONTAINER_SPECIFICATIONS = [
|
||||
{ type: '40GP', recommendedMinCbm: 56, capacityCbm: 58, maxWeightKg: 19500 },
|
||||
{ type: '40HQ', recommendedMinCbm: 65, capacityCbm: 68, maxWeightKg: 19500 },
|
||||
{ type: '45HQ', recommendedMinCbm: 82, capacityCbm: 86, maxWeightKg: 19500 },
|
||||
] as const
|
||||
|
||||
export const DEFAULT_CONTAINER_TYPE = '40HQ' as const
|
||||
@@ -9,6 +9,7 @@ export type ApiUnifiedOrderMainStatus =
|
||||
| 'LOADING'
|
||||
| 'LOADED'
|
||||
| 'DELIVERED'
|
||||
| 'COMPLETED'
|
||||
|
||||
export interface MerchantUnifiedMeasurementSummary {
|
||||
reviewStatus: string | null
|
||||
@@ -59,6 +60,7 @@ export interface MerchantUnifiedOrderRow {
|
||||
lxCode?: string | null
|
||||
mainStatus: ApiUnifiedOrderMainStatus
|
||||
nextAction: string | null
|
||||
customerFeeConfirmationRequired: boolean
|
||||
internalOrderStatus: ApiFirstLegStatus | null
|
||||
customerId: number | string | null
|
||||
customerName: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FIRST_LEG_PROGRESS_STAGES,
|
||||
getFirstLegProgressIndex,
|
||||
getFirstLegStatusLabel,
|
||||
getFirstLegStatusTone,
|
||||
} from '@/modules/first-leg/utils/firstLegOrder'
|
||||
|
||||
const props = defineProps<{ order: FirstLegOrder }>()
|
||||
@@ -25,7 +26,7 @@ function stageState(index: number) {
|
||||
<h3 id="order-progress-title">订单进度</h3>
|
||||
<span>国内入仓、组托、装柜及后续履约同步</span>
|
||||
</div>
|
||||
<el-tag :type="order.status === 'DELIVERED' ? 'success' : order.status === 'DRAFT' || order.status === 'PENDING_REVIEW' ? 'warning' : 'info'" effect="light">
|
||||
<el-tag :type="getFirstLegStatusTone(order.status)" effect="light">
|
||||
{{ getFirstLegStatusLabel(order.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
@@ -49,7 +50,7 @@ function stageState(index: number) {
|
||||
.progress-heading h3 { margin: 0; color: var(--navy); }
|
||||
.progress-heading h3 { font-size: 15px; }
|
||||
.progress-heading span { color: var(--muted); font-size: 11px; }
|
||||
.progress-stages { display: grid; grid-template-columns: repeat(7, minmax(76px, 1fr)); margin: 16px 0 0; padding: 0; overflow-x: auto; list-style: none; }
|
||||
.progress-stages { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(76px, 1fr); margin: 16px 0 0; padding: 0; overflow-x: auto; list-style: none; }
|
||||
.progress-stages li { position: relative; display: flex; min-width: 76px; align-items: center; flex-direction: column; gap: 7px; color: #96a0a6; font-size: 10px; text-align: center; }
|
||||
.progress-stages li::before { position: absolute; top: 10px; right: 50%; left: -50%; height: 2px; background: #e1e6e8; content: ''; }
|
||||
.progress-stages li:first-child::before { display: none; }
|
||||
@@ -60,7 +61,7 @@ function stageState(index: number) {
|
||||
.is-current .stage-label { color: var(--navy); font-weight: 700; }
|
||||
.draft-progress-note { margin: 12px 0 0; padding: 10px 12px; background: #f6f8f8; color: var(--muted); font-size: 11px; }
|
||||
@media (max-width: 720px) {
|
||||
.progress-stages { grid-template-columns: repeat(7, 82px); padding-bottom: 8px; }
|
||||
.progress-stages { grid-auto-columns: 82px; padding-bottom: 8px; }
|
||||
.progress-heading { align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@ test('maps transport and status fields instead of treating container mode as tra
|
||||
assert.match(storeSource, /status === 'SUBMITTED'\) return 'PENDING_REVIEW'/)
|
||||
assert.match(storeSource, /status === 'REJECTED'\) return 'PENDING_REVIEW'/)
|
||||
assert.match(storeSource, /status === 'APPROVED' \|\| status === 'ACCEPTED' \|\| status === 'PROCESSING'\) return 'WAIT_MEASURE'/)
|
||||
assert.match(storeSource, /status === 'COMPLETED'\) return 'LOADED'/)
|
||||
assert.match(storeSource, /status === 'COMPLETED'\) return 'COMPLETED'/)
|
||||
assert.doesNotMatch(storeSource, /status === 'COMPLETED'\) return 'DELIVERED'/)
|
||||
})
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function clientStatus(status: ApiFirstLegStatus): FirstLegOrder['status'] {
|
||||
// 后端联调任务曾出现 REJECTED,但一期已确认订单不设驳回状态;兼容旧数据时仍按待审核展示。
|
||||
if (status === 'REJECTED') return 'PENDING_REVIEW'
|
||||
if (status === 'APPROVED' || status === 'ACCEPTED' || status === 'PROCESSING') return 'WAIT_MEASURE'
|
||||
if (status === 'COMPLETED') return 'LOADED'
|
||||
if (status === 'COMPLETED') return 'COMPLETED'
|
||||
if (status === 'CANCELLED') return 'PENDING_REVIEW'
|
||||
return 'PENDING_REVIEW'
|
||||
}
|
||||
@@ -198,6 +198,7 @@ function timeline(detail: MerchantFirstLegOrderDetail): FirstLegOrderTimelineIte
|
||||
|
||||
function businessSummary(summary?: MerchantUnifiedOrderRow): FirstLegOrder['businessSummary'] {
|
||||
return {
|
||||
customerFeeConfirmationRequired: summary?.customerFeeConfirmationRequired ?? false,
|
||||
totalPackageCount: summary?.totalPackageCount ?? null,
|
||||
actualGrossWeightKg: summary?.actualGrossWeightKg ?? null,
|
||||
actualVolumeCbm: summary?.actualVolumeCbm ?? null,
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface FirstLegOrderTimelineItem {
|
||||
}
|
||||
|
||||
export interface FirstLegOrderBusinessSummary {
|
||||
customerFeeConfirmationRequired: boolean
|
||||
totalPackageCount: number | null
|
||||
actualGrossWeightKg: number | null
|
||||
actualVolumeCbm: number | null
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
FIRST_LEG_PROGRESS_STAGES,
|
||||
getFirstLegProgressIndex,
|
||||
getFirstLegStatusLabel,
|
||||
getFirstLegStatusTone,
|
||||
getFirstLegTransportModeLabel,
|
||||
summarizeFirstLegCargo,
|
||||
validateFirstLegOrderDraft,
|
||||
@@ -67,7 +68,7 @@ test('uses the confirmed status and transport labels', () => {
|
||||
assert.equal(getFirstLegStatusLabel('DELIVERED'), '派送完成')
|
||||
assert.equal(getFirstLegTransportModeLabel('FCL'), '海运整箱 FCL')
|
||||
assert.deepEqual(FIRST_LEG_PROGRESS_STAGES.map((item) => item.code), [
|
||||
'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED',
|
||||
'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED', 'COMPLETED',
|
||||
])
|
||||
assert.equal(getFirstLegProgressIndex('WAIT_PALLETIZE'), 2)
|
||||
})
|
||||
@@ -197,3 +198,17 @@ test('counts one physical carton series for SKU lines in the same mixed-carton g
|
||||
second.packageCount = 9
|
||||
assert.ok(validateFirstLegOrderDraft(draft).includes('同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致'))
|
||||
})
|
||||
|
||||
test('distinguishes first-leg draft, review, active handling and completion', () => {
|
||||
assert.equal(getFirstLegStatusTone('DRAFT'), 'info')
|
||||
assert.equal(getFirstLegStatusTone('PENDING_REVIEW'), 'warning')
|
||||
for (const status of ['WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING']) {
|
||||
assert.equal(getFirstLegStatusTone(status), 'primary')
|
||||
}
|
||||
assert.equal(getFirstLegStatusTone('LOADED'), 'success')
|
||||
assert.equal(getFirstLegStatusTone('DELIVERED'), 'success')
|
||||
assert.equal(getFirstLegStatusTone('COMPLETED'), 'success')
|
||||
assert.equal(getFirstLegStatusLabel('COMPLETED'), '已完成')
|
||||
assert.equal(getFirstLegStatusLabel('UNRECOGNIZED'), '未知状态')
|
||||
assert.equal(getFirstLegStatusTone('UNRECOGNIZED'), 'info')
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ const transportModeLabels = new Map<FirstLegTransportMode, string>(
|
||||
)
|
||||
|
||||
export function getFirstLegStatusLabel(status: FirstLegOrderStatus) {
|
||||
return statusLabels.get(status) ?? status
|
||||
return statusLabels.get(status) ?? '未知状态'
|
||||
}
|
||||
|
||||
export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode | null) {
|
||||
@@ -23,9 +23,11 @@ export function getFirstLegTransportModeLabel(mode: FirstLegTransportMode | null
|
||||
return transportModeLabels.get(mode) ?? mode
|
||||
}
|
||||
|
||||
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'danger' {
|
||||
if (status === 'DELIVERED') return 'success'
|
||||
if (status === 'DRAFT' || status === 'PENDING_REVIEW') return 'warning'
|
||||
export function getFirstLegStatusTone(status: FirstLegOrderStatus): 'success' | 'warning' | 'info' | 'primary' {
|
||||
if (status === 'DELIVERED' || status === 'LOADED' || status === 'COMPLETED') return 'success'
|
||||
if (status === 'DRAFT') return 'info'
|
||||
if (status === 'PENDING_REVIEW') return 'warning'
|
||||
if (['WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING'].includes(status)) return 'primary'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const tableSource = readFileSync(new URL('../../../components/common/DataTable.vue', import.meta.url), 'utf8')
|
||||
const querySource = readFileSync(new URL('../../../components/common/QueryToolbar.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
|
||||
const progressSource = readFileSync(new URL('../components/OrderProgressPanel.vue', import.meta.url), 'utf8')
|
||||
const customsDialogSource = readFileSync(new URL('../components/CustomsDocumentsDialog.vue', import.meta.url), 'utf8')
|
||||
const customsApiSource = readFileSync(new URL('../api/customs.ts', import.meta.url), 'utf8')
|
||||
const orderStoreSource = readFileSync(new URL('../store/firstLegOrders.ts', import.meta.url), 'utf8')
|
||||
|
||||
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'/)
|
||||
@@ -69,7 +72,7 @@ test('renders dynamic cargo lines and a responsive editor dialog', () => {
|
||||
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; \}/)
|
||||
assert.match(source, /\.prototype-order-table :deep\(> \.el-table\), \.prototype-order-table :deep\(> \.data-table__pagination\) \{ display: none; \}/)
|
||||
})
|
||||
|
||||
test('uses the full editor dialog width for cargo details and cost estimates', () => {
|
||||
@@ -88,15 +91,15 @@ 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" layout="grid" :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="visibleOrderColumns"/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' \}/)
|
||||
assert.match(source, /\{ key: 'actions', label: '操作', width: 284, fixed: 'left', align: 'left', headerAlign: 'left', slotName: 'actions' \}/)
|
||||
assert.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
|
||||
test('adds prototype download actions to the order query panel', () => {
|
||||
test('adds download actions to the order-list toolbar', () => {
|
||||
assert.match(source, /下载唛头/)
|
||||
assert.match(source, /导出订单信息/)
|
||||
assert.match(source, /function downloadShippingMarks\(\)/)
|
||||
@@ -187,7 +190,7 @@ test('keeps dense order identifiers on one line and aligns columns by content ty
|
||||
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;/)
|
||||
assert.match(tableSource, /height: var\(--client-table-row-height\)/)
|
||||
})
|
||||
|
||||
test('shows one unified progress line and C02 business summaries without detail history', () => {
|
||||
@@ -230,26 +233,26 @@ test('shows and searches the inbound code after a draft is submitted', () => {
|
||||
assert.match(source, /<span v-else>—<\/span>/)
|
||||
})
|
||||
|
||||
test('keeps the seven-stage progress and business summary usable on mobile', () => {
|
||||
assert.match(progressSource, /grid-template-columns: repeat\(7, 82px\)/)
|
||||
test('keeps all progress stages and business summary usable on mobile', () => {
|
||||
assert.match(progressSource, /grid-auto-columns: 82px/)
|
||||
assert.match(progressSource, /overflow-x: auto/)
|
||||
assert.match(source, /\.fulfillment-summary \{ grid-template-columns: 1fr; \}/)
|
||||
})
|
||||
|
||||
test('keeps the first viewport compact and moves create into the table toolbar', () => {
|
||||
const queryToolbarSource = source.slice(source.indexOf('<QueryToolbar'), source.indexOf('</QueryToolbar>') + '</QueryToolbar>'.length)
|
||||
assert.match(source, /class="order-table-toolbar"/)
|
||||
const tableToolbarSource = source.slice(source.indexOf('#toolbar-actions'), source.indexOf('class="order-eligibility is-pending"'))
|
||||
assert.match(source, /#toolbar-actions/)
|
||||
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/)
|
||||
assert.match(source, /class="order-table-toolbar"[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
|
||||
assert.match(queryToolbarSource, /<template #actions>[\s\S]*下载唛头[\s\S]*导出订单信息/)
|
||||
assert.doesNotMatch(queryToolbarSource, /order:first-leg:create|创建订单/)
|
||||
assert.match(source, /#toolbar-actions[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
|
||||
assert.match(tableToolbarSource, /下载唛头[\s\S]*导出订单信息[\s\S]*批量提交草稿[\s\S]*创建订单/)
|
||||
assert.doesNotMatch(source.slice(source.indexOf('<QueryToolbar'), source.indexOf('<div class="client-card order-table-card">')), /下载唛头|导出订单信息|创建订单/)
|
||||
assert.match(source, /client-main:has\(> \.first-leg-page:not\(\.is-create-route\)\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
|
||||
})
|
||||
|
||||
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(querySource, /\.query-toolbar--grid \{ display: block;/)
|
||||
assert.match(querySource, /query-toolbar__filters \{ display: grid; grid-template-columns: repeat\(4,/)
|
||||
assert.match(querySource, /query-toolbar__actions \{ margin-top: 12px;/)
|
||||
assert.match(source, /v-if="companySubmissionBlocked && !supportingDataErrors\.some/)
|
||||
assert.doesNotMatch(source, /企业资料已审核,可正常提交订单/)
|
||||
assert.doesNotMatch(source, /order-eligibility\.is-approved/)
|
||||
@@ -305,6 +308,7 @@ test('selects platform destinations and uses customer-scoped server address book
|
||||
test('lets the create-page instance own destination loading and exposes a recoverable error state', () => {
|
||||
assert.match(source, /await router\.push\(\{ name: 'first-leg-order-create' \}\)\s*\n\}/)
|
||||
assert.doesNotMatch(source, /await router\.push\(\{ name: 'first-leg-order-create' \}\)[\s\S]*?await loadDestinationWarehouses\(form\.destinationType\)/)
|
||||
assert.match(source, /watch\(isCreatePage, \(isCreate, wasCreate\) => \{[\s\S]*?if \(wasCreate === false\) void loadPageData\(\)/)
|
||||
assert.match(source, /if \(destinationResult\.status === 'rejected'\) supportingDataErrors\.value\.push\(`目的仓加载失败:\$\{errorMessage\(destinationResult\.reason\)\}`\)/)
|
||||
assert.match(source, /下单配套数据加载失败/)
|
||||
})
|
||||
@@ -397,6 +401,9 @@ test('loads only active server channels for the selected domestic warehouse', ()
|
||||
assert.match(source, /availablePricingTaxModes/)
|
||||
assert.match(source, /:loading="channelLoading"/)
|
||||
assert.match(source, /:disabled="!form\.receivingWarehouseId \|\| channelLoading"/)
|
||||
assert.match(source, /label="报价包税方式" prop="pricingTaxMode" required class="pricing-tax-mode-field"/)
|
||||
assert.match(source, /\.compact-transport-grid > :deep\(\.pricing-tax-mode-field\) \{ display: none; \}/)
|
||||
assert.doesNotMatch(source, /\.compact-transport-grid > :deep\(\.el-form-item:nth-child\(2\)\)/)
|
||||
assert.match(source, /当前收货仓暂无可用渠道/)
|
||||
assert.match(source, /渠道加载失败/)
|
||||
assert.match(source, /@click="retryPricingChannels"/)
|
||||
@@ -532,11 +539,20 @@ test('opens billing-weight confirmation independently from order details', () =>
|
||||
assert.match(source, /unavailable-message="未推送计费重"/)
|
||||
})
|
||||
|
||||
test('shows the standalone billing-weight entry at every order node', () => {
|
||||
test('shows billing-weight confirmation only when the backend marks it pending', () => {
|
||||
const templateSource = source.slice(source.indexOf('<template>'), source.lastIndexOf('</template>'))
|
||||
const entries = templateSource.match(/<el-button link type="primary" @click="openFeeConfirmation\([^)]*\)">确认计费重<\/el-button>/g) ?? []
|
||||
assert.equal(entries.length, 2)
|
||||
assert.doesNotMatch(templateSource, /v-if="[^"]*status === 'WAIT_SPLIT'[^"]*"[^>]*>确认计费重/)
|
||||
const entries = templateSource.match(/<el-button v-if="canConfirmInboundFee && [^"]*customerFeeConfirmationRequired" link type="primary" @click="openFeeConfirmation\([^)]*\)">确认计费重<\/el-button>/g) ?? []
|
||||
assert.equal(entries.length, 1)
|
||||
assert.match(source, /if \(canConfirmInboundFee\.value && order\.businessSummary\.customerFeeConfirmationRequired\) \{\s*actions\.push\(\{ key: 'fee'/)
|
||||
assert.match(source, /:row-actions="getOrderRowActions"/)
|
||||
assert.match(orderStoreSource, /customerFeeConfirmationRequired: summary\?\.customerFeeConfirmationRequired \?\? false/)
|
||||
})
|
||||
|
||||
test('submits only selected draft orders in one batch action', () => {
|
||||
assert.match(source, /selectedDraftOrders = computed\(\(\) => selectedOrders\.value\.filter\(\(order\) => order\.status === 'DRAFT'\)\)/)
|
||||
assert.match(source, /Promise\.allSettled\(drafts\.map\(\(order\) => submitMerchantFirstLegOrder/)
|
||||
assert.match(source, /createIdempotencyKey\(`first-leg-batch-submit-\$\{order\.id\}`\)/)
|
||||
assert.match(source, /v-if="canSubmitOrder"[\s\S]*批量提交草稿/)
|
||||
})
|
||||
|
||||
test('discards stale inbound billing responses when billing views close or the user changes orders', () => {
|
||||
@@ -586,7 +602,7 @@ test('opens customer customs documents only for server-confirmed loaded tax-refu
|
||||
assert.match(source, /order\.status !== 'LOADED'/)
|
||||
assert.match(source, /item\.orderId === order\.id && item\.taxMode === 'TAX_REFUND'/)
|
||||
assert.match(source, /getMerchantCustomsForOrder\(order\.id\)/)
|
||||
assert.match(source, /v-if="eligibleCustomsDetail\(row as FirstLegOrder\)"[\s\S]*?报关资料/)
|
||||
assert.match(source, /if \(eligibleCustomsDetail\(order\)\) \{\s*actions\.push\(\{ key: 'customs', label: '报关资料'/)
|
||||
assert.match(source, /class="mobile-order-card"[\s\S]*?v-if="eligibleCustomsDetail\(order\)"[\s\S]*?openCustomsDocuments\(order\)[\s\S]*?报关资料/)
|
||||
assert.match(source, /<CustomsDocumentsDialog[\s\S]*?:can-edit="auth\.hasPermission\('order:first-leg:edit'\)"/)
|
||||
assert.doesNotMatch(customsApiSource, /customs\/cases\//)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
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, ChevronDown, ChevronUp, Download, Eye, FilePlus2, FileText, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||||
import { AlertTriangle, Box, ChevronDown, ChevronUp, Download, FilePlus2, FileText, 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'
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { ApiError } from '@/api/http'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
getMerchantOrderAddresses,
|
||||
getMerchantPricingChannels,
|
||||
importMerchantFirstLegOrders,
|
||||
submitMerchantFirstLegOrder,
|
||||
updateMerchantOrderAddress,
|
||||
uploadMerchantProductImage,
|
||||
type CustomerOrderAddress,
|
||||
@@ -72,6 +74,7 @@ const companyAuditLoaded = ref(false)
|
||||
const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE')
|
||||
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
|
||||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||||
const canSubmitOrder = computed(() => auth.hasPermission('order:first-leg:submit'))
|
||||
const canConfirmInboundFee = 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))
|
||||
@@ -108,11 +111,12 @@ const batchImportResult = ref<MerchantBatchImportResponse | null>(null)
|
||||
const batchImportError = ref('')
|
||||
const fixedOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||||
{ key: 'actions', label: '操作', width: 216, fixed: 'left', align: 'center', headerAlign: 'center', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 190, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'order' },
|
||||
{ key: 'actions', label: '操作', width: 284, fixed: 'left', align: 'left', headerAlign: 'left', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单号', width: 240, 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: 'inboundCode', label: '入仓码', width: 230, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'inboundCode' },
|
||||
{ key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' },
|
||||
{ key: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' },
|
||||
{ key: 'transportMode', label: '运输方式', width: 104, align: 'center', headerAlign: 'center', slotName: 'transportMode' },
|
||||
{ key: 'pickupMethod', label: '交货方式', width: 112, align: 'center', headerAlign: 'center', slotName: 'pickupMethod' },
|
||||
@@ -120,7 +124,6 @@ const selectableOrderColumns: DataTableColumn[] = [
|
||||
{ key: 'receivingWarehouse', label: '国内收货仓', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'receivingWarehouse' },
|
||||
{ key: 'destinationWarehouse', label: '目的仓', width: 120, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'destinationWarehouse' },
|
||||
{ key: 'destinationType', label: '目的仓类型', width: 112, align: 'center', headerAlign: 'center', slotName: 'destinationType' },
|
||||
{ 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' },
|
||||
@@ -182,8 +185,10 @@ const tablePage = ref(1)
|
||||
const tablePageSize = ref(10)
|
||||
const downloadingMarks = ref(false)
|
||||
const exportingOrders = ref(false)
|
||||
const batchSubmitting = ref(false)
|
||||
const selectedOrders = ref<FirstLegOrder[]>([])
|
||||
const selectedOrderCount = computed(() => selectedOrders.value.length)
|
||||
const selectedDraftOrders = computed(() => selectedOrders.value.filter((order) => order.status === 'DRAFT'))
|
||||
const orderTableVersion = ref(0)
|
||||
const editorOpen = ref(false)
|
||||
const orderFormRef = ref<FormInstance>()
|
||||
@@ -650,6 +655,8 @@ watch(isCreatePage, (isCreate, wasCreate) => {
|
||||
editingId.value = undefined
|
||||
replaceForm(quickOrderDraft())
|
||||
resetEditorValidation()
|
||||
// The list and create routes reuse this component, so onMounted does not run again.
|
||||
if (wasCreate === false) void loadPageData()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(detailOpen, (open) => {
|
||||
@@ -1151,6 +1158,23 @@ async function autofillWarehouseSelections() {
|
||||
markEditorClean()
|
||||
}
|
||||
|
||||
function getOrderRowActions(row: object): TableAction[] {
|
||||
const order = row as FirstLegOrder
|
||||
const actions: TableAction[] = [
|
||||
{ key: 'detail', label: '详情', run: () => openDetail(order) },
|
||||
]
|
||||
if (canConfirmInboundFee.value && order.businessSummary.customerFeeConfirmationRequired) {
|
||||
actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) })
|
||||
}
|
||||
if (canEditOrder.value && canClientEditFirstLegOrder(order.status)) {
|
||||
actions.push({ key: 'edit', label: '编辑', loading: openingEditor.value, run: () => openEdit(order) })
|
||||
}
|
||||
if (eligibleCustomsDetail(order)) {
|
||||
actions.push({ key: 'customs', label: '报关资料', run: () => openCustomsDocuments(order) })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
async function openCreate() {
|
||||
if (availableWarehouses.value.length === 0) {
|
||||
ElMessage.warning(supportingDataLoading.value
|
||||
@@ -1485,6 +1509,46 @@ function handleOrderSelectionChange(rows: FirstLegOrder[]) {
|
||||
selectedOrders.value = rows
|
||||
}
|
||||
|
||||
async function submitSelectedDraftOrders() {
|
||||
const drafts = selectedDraftOrders.value
|
||||
if (batchSubmitting.value || !drafts.length) return
|
||||
if (companySubmissionBlocked.value) {
|
||||
ElMessage.warning('企业资料审核通过后才能提交订单')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认提交选中的 ${drafts.length} 条草稿订单吗?`, '批量提交订单', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认提交',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
throw error
|
||||
}
|
||||
|
||||
batchSubmitting.value = true
|
||||
try {
|
||||
const results = await Promise.allSettled(drafts.map((order) => submitMerchantFirstLegOrder(
|
||||
order.id,
|
||||
createIdempotencyKey(`first-leg-batch-submit-${order.id}`),
|
||||
)))
|
||||
const successCount = results.filter((result) => result.status === 'fulfilled').length
|
||||
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||||
await searchOrders(appliedFilters.value)
|
||||
if (!failures.length) {
|
||||
ElMessage.success(`已提交 ${successCount} 条草稿订单`)
|
||||
return
|
||||
}
|
||||
const firstReason = failures[0]?.reason
|
||||
const reason = firstReason instanceof Error ? firstReason.message : '提交失败'
|
||||
if (successCount > 0) ElMessage.warning(`${successCount} 条提交成功,${failures.length} 条失败:${reason}`)
|
||||
else ElMessage.error(`${failures.length} 条草稿订单提交失败:${reason}`)
|
||||
} finally {
|
||||
batchSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadShippingMarks() {
|
||||
if (downloadingMarks.value) return
|
||||
const orders = selectedOrders.value
|
||||
@@ -1814,19 +1878,14 @@ async function searchOrders(model: QueryModel) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage, 'client-ui': !isCreatePage }">
|
||||
<template v-if="!isCreatePage">
|
||||
<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>
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||||
<div class="order-table-toolbar-actions">
|
||||
<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" :row-actions="getOrderRowActions" @selection-change="handleOrderSelectionChange">
|
||||
<template #toolbar><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span><span v-if="selectedOrderCount">已选 {{ selectedOrderCount }} 条</span></template>
|
||||
<template #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>
|
||||
@@ -1853,10 +1912,14 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-button link class="column-settings-reset" @click="resetOrderColumns"><RotateCcw :size="17" />恢复默认</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-button :loading="downloadingMarks" :disabled="selectedOrderCount === 0" @click="downloadShippingMarks"><Download :size="16" />下载唛头</el-button>
|
||||
<el-button :loading="exportingOrders" :disabled="selectedOrderCount === 0" @click="exportOrderInformation"><Download :size="16" />导出订单信息</el-button>
|
||||
<el-button v-if="canSubmitOrder" :loading="batchSubmitting" :disabled="selectedDraftOrders.length === 0 || companySubmissionBlocked" @click="submitSelectedDraftOrders"><Send :size="17" />批量提交草稿</el-button>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" @click="openBatchImport"><Upload :size="17" />批量导入订单</el-button>
|
||||
<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>
|
||||
|
||||
</template>
|
||||
<template #before-table>
|
||||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||||
<AlertTriangle :size="20" />
|
||||
<div><strong>{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||||
@@ -1867,21 +1930,15 @@ async function searchOrders(model: QueryModel) {
|
||||
<div><strong>部分订单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||||
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
|
||||
</div>
|
||||
<div v-if="store.loadError" class="order-load-state" role="alert">
|
||||
|
||||
</template>
|
||||
<template v-if="store.loadError" #error>
|
||||
<div class="order-load-state" role="alert">
|
||||
<AlertTriangle :size="24" />
|
||||
<div><strong>订单数据加载失败</strong><span>{{ store.loadError }}</span></div>
|
||||
<el-button :loading="store.loading" @click="loadPageData">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="desktop-order-table">
|
||||
<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>
|
||||
<el-button link type="primary" @click="openFeeConfirmation(row as FirstLegOrder)">确认计费重</el-button>
|
||||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" />编辑</el-button>
|
||||
<el-button v-if="eligibleCustomsDetail(row as FirstLegOrder)" link type="primary" @click="openCustomsDocuments(row as FirstLegOrder)"><FileText :size="15" />报关资料</el-button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #order="{ row }">
|
||||
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '—'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '—' }}</button>
|
||||
@@ -1903,7 +1960,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<template #receivingWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).receivingWarehouseName)">{{ displayListValue((row as FirstLegOrder).receivingWarehouseName) }}</span></template>
|
||||
<template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template>
|
||||
<template #destinationType="{ row }">{{ destinationTypeLabels[(row as FirstLegOrder).destinationType] }}</template>
|
||||
<template #status="{ row }"><el-tag :type="getFirstLegStatusTone((row as FirstLegOrder).status)" effect="light">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</el-tag></template>
|
||||
<template #status="{ row }"><span class="client-status" :data-tone="getFirstLegStatusTone((row as FirstLegOrder).status)">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</span></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>
|
||||
@@ -1931,8 +1988,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</el-empty>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
<div v-loading="store.loading" class="mobile-order-surface">
|
||||
<div v-if="!store.loadError" v-loading="store.loading" class="mobile-order-surface">
|
||||
<div v-if="filteredOrders.length" class="mobile-order-list">
|
||||
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
|
||||
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '—' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
|
||||
@@ -1943,10 +1999,10 @@ async function searchOrders(model: QueryModel) {
|
||||
<footer>
|
||||
<span>更新于 {{ formatDateTime(order.updatedAt) }}</span>
|
||||
<div>
|
||||
<el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button>
|
||||
<el-button link type="primary" @click="openFeeConfirmation(order)">确认计费重</el-button>
|
||||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button>
|
||||
<el-button v-if="eligibleCustomsDetail(order)" link type="primary" @click="openCustomsDocuments(order)"><FileText :size="15" />报关资料</el-button>
|
||||
<el-button link type="primary" @click="openDetail(order)">详情</el-button>
|
||||
<el-button v-if="canConfirmInboundFee && order.businessSummary.customerFeeConfirmationRequired" link type="primary" @click="openFeeConfirmation(order)">确认计费重</el-button>
|
||||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)">编辑</el-button>
|
||||
<el-button v-if="eligibleCustomsDetail(order)" link type="primary" @click="openCustomsDocuments(order)">报关资料</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
@@ -1955,7 +2011,6 @@ async function searchOrders(model: QueryModel) {
|
||||
<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>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1996,7 +2051,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<small v-if="channelError" class="field-guidance field-guidance--error">渠道加载失败:{{ channelError }} <el-button link type="primary" @click="retryPricingChannels">重试</el-button></small>
|
||||
<small v-else-if="form.receivingWarehouseId && !channelLoading && channelOptions.length === 0" class="field-guidance">当前收货仓暂无可用渠道,请联系平台管理员。</small>
|
||||
</el-form-item>
|
||||
<el-form-item label="报价包税方式" prop="pricingTaxMode" required>
|
||||
<el-form-item label="报价包税方式" prop="pricingTaxMode" required class="pricing-tax-mode-field">
|
||||
<el-radio-group v-model="form.pricingTaxMode"><el-radio v-for="mode in availablePricingTaxModes" :key="mode" :value="mode">{{ pricingTaxModeLabel(mode) }}</el-radio></el-radio-group>
|
||||
<small v-if="form.channelCode && availablePricingTaxModes.length === 0" class="field-guidance field-guidance--error">当前渠道没有可用报价方式,请重新选择渠道。</small>
|
||||
</el-form-item>
|
||||
@@ -2295,17 +2350,9 @@ async function searchOrders(model: QueryModel) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
|
||||
.first-leg-page { display: flex; flex-direction: column; gap: var(--client-section-gap); }
|
||||
: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 { 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) { 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; }
|
||||
@@ -2317,13 +2364,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.supporting-data-error strong { color: var(--navy); font-size: 13px; }
|
||||
.supporting-data-error span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||||
.order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.order-table-card { padding: 0; overflow: hidden; }
|
||||
.order-table-toolbar { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
||||
.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; }
|
||||
.order-table-card { padding: 0; overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); box-shadow: none; }
|
||||
.batch-import-dialog { display: flex; flex-direction: column; gap: 18px; }
|
||||
.batch-import-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||
.batch-import-actions > span { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -2346,17 +2387,6 @@ async function searchOrders(model: QueryModel) {
|
||||
: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; }
|
||||
@@ -2374,8 +2404,8 @@ async function searchOrders(model: QueryModel) {
|
||||
: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; 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; }
|
||||
.inbound-code-link { max-width: 100%; overflow: hidden; padding: 0; border: 0; background: transparent; color: var(--primary); cursor: pointer; text-align: left; text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-number-link { display: block; width: 100%; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: var(--client-action); cursor: pointer; font-size: var(--client-font-size); font-weight: 700; line-height: 18px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.inbound-code-link { max-width: 100%; overflow: hidden; padding: 0; border: 0; background: transparent; color: var(--client-action); cursor: pointer; text-align: left; text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.inbound-code-link:hover, .inbound-code-link:focus-visible { text-decoration-color: currentColor; }
|
||||
.inbound-code-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||||
@@ -2465,7 +2495,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.shipping-form-flow .prototype-form-row--tabs { margin-bottom: 12px; }
|
||||
.compact-transport-grid { display: grid; width: 100%; grid-template-columns: 1fr; gap: 0; margin: 6px 0 0; }
|
||||
.compact-transport-grid > :deep(.el-form-item) { display: grid; width: 100%; grid-template-columns: 114px minmax(0, 560px); align-items: start; margin-bottom: 10px; }
|
||||
.compact-transport-grid > :deep(.el-form-item:nth-child(2)) { display: none; }
|
||||
.compact-transport-grid > :deep(.pricing-tax-mode-field) { display: none; }
|
||||
.compact-transport-grid > :deep(.el-form-item:nth-child(1)) { grid-template-columns: 114px minmax(0, 360px); }
|
||||
.compact-transport-grid > :deep(.el-form-item:nth-child(6)) { grid-template-columns: 114px minmax(0, 360px); }
|
||||
.compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 114px minmax(0, 620px); margin-top: 2px; margin-bottom: 0; }
|
||||
@@ -2599,7 +2629,7 @@ async function searchOrders(model: QueryModel) {
|
||||
: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; }
|
||||
.prototype-order-table :deep(> .el-table), .prototype-order-table :deep(> .data-table__pagination) { display: none; }
|
||||
.mobile-order-surface { display: block; }
|
||||
.mobile-order-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 10px; }
|
||||
.mobile-order-card { min-width: 0; padding: 14px; border: 1px solid var(--border); background: #fff; }
|
||||
@@ -2622,9 +2652,6 @@ async function searchOrders(model: QueryModel) {
|
||||
.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; }
|
||||
.order-table-toolbar > div { justify-content: space-between; }
|
||||
.order-table-toolbar .el-button { width: 100%; min-height: 40px; }
|
||||
.order-eligibility { flex-wrap: wrap; }
|
||||
.order-eligibility .el-button { width: 100%; min-height: 44px; }
|
||||
.order-form-grid, .order-detail-grid, .cargo-form-grid, .cargo-form-grid--basic, .party-detail-grid { grid-template-columns: 1fr; }
|
||||
@@ -2642,7 +2669,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||||
.warehouse-options { width: 100%; gap: 12px 18px; }
|
||||
.compact-transport-grid > :deep(.el-form-item), .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { display: block; margin-bottom: 14px; }
|
||||
.compact-transport-grid > :deep(.el-form-item:nth-child(2)) { display: none; }
|
||||
.compact-transport-grid > :deep(.pricing-tax-mode-field) { display: none; }
|
||||
.compact-transport-grid > :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
|
||||
.cargo-table-toolbar { align-items: stretch; flex-direction: column; }
|
||||
.cargo-import-actions, .cargo-table-actions { flex-wrap: wrap; }
|
||||
@@ -2653,7 +2680,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.price-estimate__state.is-error { align-items: flex-start; flex-direction: column; }
|
||||
.form-span-full { grid-column: auto; }
|
||||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||||
.desktop-order-table { display: none; }
|
||||
.prototype-order-table :deep(> .el-table), .prototype-order-table :deep(> .data-table__pagination) { display: none; }
|
||||
.order-load-state { min-height: 240px; grid-template-columns: 1fr; justify-items: start; }
|
||||
.supporting-data-error { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.supporting-data-error .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Client content standard. Sidebar and brand navigation are intentionally outside this scope. */
|
||||
:root {
|
||||
--client-action: #1777ff;
|
||||
--client-action-hover: #0958d9;
|
||||
--client-text: #344054;
|
||||
--client-muted: #667085;
|
||||
--client-border: #e4e7ec;
|
||||
--client-surface: #fff;
|
||||
--client-header-bg: #f8fafc;
|
||||
--client-hover-bg: #f5f8ff;
|
||||
--client-success: #067647;
|
||||
--client-success-bg: #ecfdf3;
|
||||
--client-warning: #b54708;
|
||||
--client-warning-bg: #fffaeb;
|
||||
--client-danger: #b42318;
|
||||
--client-danger-bg: #fef3f2;
|
||||
--client-info: #475467;
|
||||
--client-info-bg: #f2f4f7;
|
||||
--client-progress: #175cd3;
|
||||
--client-progress-bg: #eff8ff;
|
||||
--client-font-size: 13px;
|
||||
--client-control-height: 32px;
|
||||
--client-row-action-height: 28px;
|
||||
--client-table-header-height: 44px;
|
||||
--client-table-row-height: 48px;
|
||||
--client-toolbar-padding: 8px 16px;
|
||||
--client-pagination-padding: 12px 16px;
|
||||
--client-query-padding: 16px;
|
||||
--client-gap: 8px;
|
||||
--client-section-gap: 16px;
|
||||
--client-radius: 6px;
|
||||
}
|
||||
.client-ui { color: var(--client-text); font-size: var(--client-font-size); --el-color-primary: var(--client-action); --el-color-primary-light-3: #609fff; --el-color-primary-light-5: #8bbbff; --el-color-primary-light-7: #bad6ff; --el-color-primary-light-8: #d1e4ff; --el-color-primary-light-9: #e8f1ff; --el-color-primary-dark-2: var(--client-action-hover); }
|
||||
.client-ui .el-button {
|
||||
--el-button-text-color: var(--client-action);
|
||||
--el-button-border-color: var(--client-border);
|
||||
--el-button-hover-text-color: var(--client-action-hover);
|
||||
--el-button-hover-border-color: #b2ccff;
|
||||
--el-button-hover-bg-color: var(--client-hover-bg);
|
||||
height: var(--client-control-height); border-radius: var(--client-radius); font-size: var(--client-font-size);
|
||||
}
|
||||
.client-ui .el-button--primary { --el-button-bg-color: var(--client-action); --el-button-border-color: var(--client-action); --el-button-text-color: #fff; --el-button-hover-bg-color: var(--client-action-hover); --el-button-hover-border-color: var(--client-action-hover); --el-button-hover-text-color: #fff; --el-button-active-bg-color: var(--client-action-hover); --el-button-active-border-color: var(--client-action-hover); }
|
||||
.client-ui .el-button--success { --el-button-text-color: var(--client-success); --el-button-bg-color: var(--client-success-bg); --el-button-border-color: #abefc6; --el-button-hover-text-color: var(--client-success); --el-button-hover-bg-color: #dcfae6; }
|
||||
.client-ui .el-button--warning { --el-button-text-color: var(--client-warning); --el-button-bg-color: var(--client-warning-bg); --el-button-border-color: #fedf89; --el-button-hover-text-color: var(--client-warning); --el-button-hover-bg-color: #fef0c7; }
|
||||
.client-ui .el-button--danger { --el-button-text-color: var(--client-danger); --el-button-bg-color: var(--client-danger-bg); --el-button-border-color: #fecdca; --el-button-hover-text-color: var(--client-danger); --el-button-hover-bg-color: #fee4e2; }
|
||||
.client-ui .el-button.is-link, .client-ui .el-button.is-text { --el-button-text-color: var(--client-action); --el-button-hover-text-color: var(--client-action-hover); }
|
||||
.client-ui .el-button.is-disabled { color: #98a2b3; background: #f9fafb; border-color: var(--client-border); }
|
||||
.client-ui .el-button > span { gap: 4px; }
|
||||
.client-ui .el-input__wrapper, .client-ui .el-select__wrapper { min-height: var(--client-control-height); font-size: var(--client-font-size); }
|
||||
.client-ui .el-button:focus-visible { outline: 2px solid var(--client-action); outline-offset: 2px; }
|
||||
.client-status { display: inline-flex; align-items: center; gap: 6px; min-height: 26px; padding: 3px 10px; border-radius: 999px; color: var(--client-info); background: var(--client-info-bg); font-size: 12px; font-weight: 600; line-height: 20px; white-space: nowrap; }
|
||||
.client-status::before { content: ''; flex: 0 0 6px; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.client-status[data-tone="primary"] { color: var(--client-progress); background: var(--client-progress-bg); }
|
||||
.client-status[data-tone="success"] { color: var(--client-success); background: var(--client-success-bg); }
|
||||
.client-status[data-tone="warning"] { color: var(--client-warning); background: var(--client-warning-bg); }
|
||||
.client-status[data-tone="danger"] { color: var(--client-danger); background: var(--client-danger-bg); }
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './foundation.css';
|
||||
@import './client-ui.css';
|
||||
@import './shell.css';
|
||||
@import './workspace-tabs.css';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user