This commit is contained in:
2026-08-07 10:45:15 +08:00
parent ac762ed302
commit 54fe05d864
4 changed files with 29 additions and 7 deletions
@@ -109,6 +109,14 @@ test('uses only fields present on C02 rows before loading full detail', () => {
assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/) assert.match(source, /createdFrom: range\[0\] \? `\$\{range\[0\]\}T00:00:00`/)
}) })
test('shows and searches the inbound code after a draft is submitted', () => {
assert.match(source, /placeholder: '订单号、入仓码、渠道或仓库'/)
assert.match(source, /order\.businessSummary\.inboundCode/)
assert.match(source, /v-if="\(row as FirstLegOrder\)\.businessSummary\.inboundCode"[^>]*>入仓码/)
assert.match(source, /v-if="order\.businessSummary\.inboundCode"[^>]*>入仓码/)
assert.match(source, /v-if="detailOrder\.businessSummary\.inboundCode"><dt>入仓码<\/dt>/)
})
test('keeps the seven-stage progress and business summary usable on mobile', () => { test('keeps the seven-stage progress and business summary usable on mobile', () => {
assert.match(progressSource, /grid-template-columns: repeat\(7, 82px\)/) assert.match(progressSource, /grid-template-columns: repeat\(7, 82px\)/)
assert.match(progressSource, /overflow-x: auto/) assert.match(progressSource, /overflow-x: auto/)
@@ -44,7 +44,7 @@ const availableDestinationWarehouses = computed(() => store.warehouses.filter((w
const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] }) const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] })
const appliedFilters = ref<QueryModel>({ ...filters.value }) const appliedFilters = ref<QueryModel>({ ...filters.value })
const queryFields: QueryField[] = [ const queryFields: QueryField[] = [
{ key: 'keyword', type: 'input', placeholder: '订单号、渠道或仓库', props: { 'aria-label': '订单关键词' } }, { 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: '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': '订单创建日期' } }, { key: 'createdRange', type: 'daterange', placeholder: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
] ]
@@ -264,7 +264,7 @@ const filteredOrders = computed(() => store.orders.filter((order) => {
const keyword = typeof keywordValue === 'string' ? keywordValue.trim().toLowerCase() : '' const keyword = typeof keywordValue === 'string' ? keywordValue.trim().toLowerCase() : ''
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : '' const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : [] const createdRange = Array.isArray(createdRangeValue) ? createdRangeValue.filter((value): value is string => typeof value === 'string') : []
const matchesKeyword = !keyword || [order.orderNo, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')] const matchesKeyword = !keyword || [order.orderNo, order.businessSummary.inboundCode, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
.filter(Boolean) .filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword)) .some((value) => String(value).toLowerCase().includes(keyword))
const matchesStatus = !status || order.status === status const matchesStatus = !status || order.status === status
@@ -874,6 +874,7 @@ async function searchOrders(model: QueryModel) {
</template> </template>
<template #order="{ row }"> <template #order="{ row }">
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button> <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 || '无客户参考号' }}</span> <span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无客户参考号' }}</span>
</template> </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 #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>
@@ -891,6 +892,7 @@ async function searchOrders(model: QueryModel) {
<div v-if="filteredOrders.length" class="mobile-order-list"> <div v-if="filteredOrders.length" class="mobile-order-list">
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card"> <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> <header><button type="button" @click="openDetail(order)">{{ order.orderNo || '未提交草稿' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
<span v-if="order.businessSummary.inboundCode" class="order-reference">入仓码 {{ order.businessSummary.inboundCode }}</span>
<span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span> <span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span>
<dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '待更新' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输渠道</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : order.channelName || '待更新' }}</dd></div></dl> <dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '待更新' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输渠道</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : order.channelName || '待更新' }}</dd></div></dl>
<footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer> <footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer>
@@ -1008,6 +1010,7 @@ async function searchOrders(model: QueryModel) {
<section class="detail-section"> <section class="detail-section">
<h3>订单与运输</h3> <h3>订单与运输</h3>
<dl class="order-detail-grid"> <dl class="order-detail-grid">
<div v-if="detailOrder.businessSummary.inboundCode"><dt>入仓码</dt><dd>{{ detailOrder.businessSummary.inboundCode }}</dd></div>
<div><dt>客户参考号</dt><dd>{{ displayValue(detailOrder.referenceNo) }}</dd></div> <div><dt>客户参考号</dt><dd>{{ displayValue(detailOrder.referenceNo) }}</dd></div>
<div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div> <div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div> <div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
+9 -5
View File
@@ -1,4 +1,4 @@
import { computed, onBeforeUnmount, ref, watchEffect, type Ref } from 'vue' import { computed, onBeforeUnmount, ref, watch, type Ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export interface UnsavedWorkEntry { export interface UnsavedWorkEntry {
@@ -23,7 +23,11 @@ export const useUnsavedWorkStore = defineStore('client-unsaved-work', () => {
if (owner && owners.get(entry.key) !== owner) return if (owner && owners.get(entry.key) !== owner) return
const index = entries.value.findIndex((item) => item.key === entry.key) const index = entries.value.findIndex((item) => item.key === entry.key)
if (index === -1) entries.value.push(entry) if (index === -1) entries.value.push(entry)
else entries.value[index] = entry else {
const current = entries.value[index]
if (current.routeKey === entry.routeKey && current.label === entry.label && current.dirty === entry.dirty) return
entries.value[index] = entry
}
} }
function remove(key: string, owner?: symbol) { function remove(key: string, owner?: symbol) {
@@ -62,13 +66,13 @@ export function useUnsavedWorkRegistration(options: {
const store = useUnsavedWorkStore() const store = useUnsavedWorkStore()
const owner = Symbol(options.key) const owner = Symbol(options.key)
store.claim(options.key, owner) store.claim(options.key, owner)
watchEffect(() => { watch(options.dirty, (dirty) => {
store.upsert({ store.upsert({
key: options.key, key: options.key,
routeKey: options.routeKey, routeKey: options.routeKey,
label: options.label, label: options.label,
dirty: options.dirty.value, dirty,
}, owner) }, owner)
}) }, { immediate: true })
onBeforeUnmount(() => store.remove(options.key, owner)) onBeforeUnmount(() => store.remove(options.key, owner))
} }
+7
View File
@@ -65,6 +65,13 @@ test('unsaved registrations use instance ownership so stale cached views cannot
assert.match(unsavedSource, /store\.remove\(options\.key, owner\)/) assert.match(unsavedSource, /store\.remove\(options\.key, owner\)/)
}) })
test('unsaved registration only tracks the dirty ref and cannot subscribe to its own store writes', () => {
assert.match(unsavedSource, /watch\(options\.dirty, \(dirty\) =>/)
assert.match(unsavedSource, /\}, \{ immediate: true \}\)/)
assert.doesNotMatch(unsavedSource, /watchEffect/)
assert.match(unsavedSource, /current\.routeKey === entry\.routeKey[\s\S]*current\.dirty === entry\.dirty\) return/)
})
test('the tab strip keeps touch targets and horizontal navigation usable on narrow screens', () => { test('the tab strip keeps touch targets and horizontal navigation usable on narrow screens', () => {
assert.match(tabStyles, /overflow-x: auto/) assert.match(tabStyles, /overflow-x: auto/)
assert.match(tabStyles, /scrollbar-width: none/) assert.match(tabStyles, /scrollbar-width: none/)