feat: edit collaboration remarks across order loading
This commit is contained in:
@@ -3,7 +3,6 @@ import { http } from '@/shared/api/http'
|
||||
import {
|
||||
getPlatformTenantOrderStatistics,
|
||||
acceptPlatformFirstLegOrder,
|
||||
addPlatformFirstLegOrderNote,
|
||||
approvePlatformFirstLegOrder,
|
||||
cancelPlatformFirstLegOrder,
|
||||
completePlatformFirstLegOrder,
|
||||
@@ -85,7 +84,6 @@ describe('platform first-leg order API', () => {
|
||||
await startPlatformFirstLegOrder('10001', '90001')
|
||||
await completePlatformFirstLegOrder('10001', '90001')
|
||||
await cancelPlatformFirstLegOrder('10001', '90001', '客户取消')
|
||||
await addPlatformFirstLegOrderNote('10001', '90001', '已联系客户确认')
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/90001', { params: { tenantId: '10001' } })
|
||||
expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/orders/first-leg/90001/approve', undefined, { params: { tenantId: '10001' } })
|
||||
@@ -93,7 +91,6 @@ describe('platform first-leg order API', () => {
|
||||
expect(http.post).toHaveBeenNthCalledWith(3, '/platform/v1/orders/first-leg/90001/start', undefined, { params: { tenantId: '10001' } })
|
||||
expect(http.post).toHaveBeenNthCalledWith(4, '/platform/v1/orders/first-leg/90001/complete', undefined, { params: { tenantId: '10001' } })
|
||||
expect(http.post).toHaveBeenNthCalledWith(5, '/platform/v1/orders/first-leg/90001/cancel', { reason: '客户取消' }, { params: { tenantId: '10001' } })
|
||||
expect(http.post).toHaveBeenNthCalledWith(6, '/platform/v1/orders/first-leg/90001/internal-notes', { content: '已联系客户确认' }, { params: { tenantId: '10001' } })
|
||||
})
|
||||
|
||||
it('writes the backend express contract as names, not a guessed master-data ID', async () => {
|
||||
|
||||
@@ -192,10 +192,6 @@ export function cancelPlatformFirstLegOrder(tenantId: string, orderId: string, r
|
||||
return postData<void>(`/platform/v1/orders/first-leg/${orderId}/cancel`, { reason }, { tenantId })
|
||||
}
|
||||
|
||||
export function addPlatformFirstLegOrderNote(tenantId: string, orderId: string, content: string) {
|
||||
return postData<number | string>(`/platform/v1/orders/first-leg/${orderId}/internal-notes`, { content }, { tenantId })
|
||||
}
|
||||
|
||||
export function updatePlatformFirstLegOrderExpress(
|
||||
tenantId: string,
|
||||
orderId: string,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
preparePalletLabels,
|
||||
printPalletLabels,
|
||||
updatePreloadTransport,
|
||||
updateLoadingItemWarehouseRemark,
|
||||
updateLoadingItemRemarks,
|
||||
} from './platformLoadingLists'
|
||||
|
||||
const preloadSummary = {
|
||||
@@ -53,7 +53,7 @@ const detail = {
|
||||
customerName: '测试客户', customsTaxMode: 'TAX_REFUND', taxRefundFlag: true, inboundCode: 'RK001', skuCode: 'SKU001', destinationWarehouse: 'ONT8', chineseName: '产品', englishName: null,
|
||||
goodsImageUrl: null, unitType: 'SEALED_PALLET', palletId: 9007199254740993n, palletNo: 'PLT-001', indivisible: true, packageCount: 2, palletCount: 1, grossWeightKg: 10, netWeightKg: 9, volumeCbm: 1,
|
||||
dimensionalWeightKg: 2, unitsPerPackage: 10, fbaTrackingNo: null, shipmentNo: null, shippingMark: null,
|
||||
internalRemark: null, customerRemark: null, warehouseRemark: null, loadSequence: 1, frozenAt: null,
|
||||
internalRemark: null, customerRemark: null, warehouseRemark: null, customerServiceRemark: null, loadSequence: 1, frozenAt: null,
|
||||
}],
|
||||
histories: [{ historyId: 21, itemId: 11, operationId: 'transport-21', actionType: 'UPDATE_TRANSPORT', fromStatus: 'LOADING', toStatus: 'LOADING', beforeSnapshot: null, afterSnapshot: null, operatorId: 22, operatorName: '操作员', operationReason: '调整', occurredAt: '2026-08-10T11:00:00' }],
|
||||
}
|
||||
@@ -108,18 +108,20 @@ describe('platform loading lists API', () => {
|
||||
expect(loaded.summary.status).toBe('COMPLETED')
|
||||
})
|
||||
|
||||
it('updates one item warehouse remark with both optimistic-lock versions and operation id', async () => {
|
||||
const patch = vi.spyOn(http, 'patch').mockResolvedValue({ code: '200', message: 'ok', data: { id: 100, version: 4, items: [{ id: 11, warehouseRemark: '装柜时注意外箱', version: 5 }] } })
|
||||
const result = await updateLoadingItemWarehouseRemark('100', '11', {
|
||||
it('updates one item editable remarks with both optimistic-lock versions and operation id', async () => {
|
||||
const patch = vi.spyOn(http, 'patch').mockResolvedValue({ code: '200', message: 'ok', data: { id: 100, version: 4, items: [{ id: 11, internalRemark: '内部', warehouseRemark: '装柜时注意外箱', customerServiceRemark: '客服', version: 5 }] } })
|
||||
const result = await updateLoadingItemRemarks('100', '11', {
|
||||
internalRemark: '内部',
|
||||
warehouseRemark: '装柜时注意外箱',
|
||||
customerServiceRemark: '客服',
|
||||
expectedPlanVersion: 3,
|
||||
expectedItemVersion: 4,
|
||||
operationId: 'warehouse-remark-1',
|
||||
})
|
||||
|
||||
expect(patch).toHaveBeenCalledWith(
|
||||
'/platform/v1/first-leg/smart-loading/plans/100/items/11/warehouse-remark',
|
||||
{ warehouseRemark: '装柜时注意外箱', expectedPlanVersion: 3, expectedItemVersion: 4, operationId: 'warehouse-remark-1' },
|
||||
'/platform/v1/first-leg/smart-loading/plans/100/items/11/remarks',
|
||||
{ internalRemark: '内部', warehouseRemark: '装柜时注意外箱', customerServiceRemark: '客服', expectedPlanVersion: 3, expectedItemVersion: 4, operationId: 'warehouse-remark-1' },
|
||||
)
|
||||
expect(result).toMatchObject({ id: '100', version: 4, items: [{ id: '11', version: 5, warehouseRemark: '装柜时注意外箱' }] })
|
||||
})
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface LoadingItem {
|
||||
internalRemark: string | null
|
||||
customerRemark: string | null
|
||||
warehouseRemark: string | null
|
||||
customerServiceRemark: string | null
|
||||
loadSequence: number
|
||||
frozenAt: string | null
|
||||
}
|
||||
@@ -161,17 +162,19 @@ export interface TransportUpdateRequest {
|
||||
operationReason: string | null
|
||||
}
|
||||
|
||||
export interface WarehouseRemarkUpdateRequest {
|
||||
export interface LoadingItemRemarksUpdateRequest {
|
||||
internalRemark: string | null
|
||||
warehouseRemark: string | null
|
||||
customerServiceRemark: string | null
|
||||
expectedPlanVersion: number
|
||||
expectedItemVersion: number
|
||||
operationId: string
|
||||
}
|
||||
|
||||
export interface WarehouseRemarkUpdateResponse {
|
||||
export interface LoadingItemRemarksUpdateResponse {
|
||||
id: string
|
||||
version: number
|
||||
items: Array<{ id: string; version: number; warehouseRemark: string | null }>
|
||||
items: Array<{ id: string; version: number; internalRemark: string | null; warehouseRemark: string | null; customerServiceRemark: string | null }>
|
||||
}
|
||||
|
||||
export interface PreloadConfirmationResponse {
|
||||
@@ -342,9 +345,9 @@ export async function updatePreloadTransport(planId: string, warehouseId: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLoadingItemWarehouseRemark(planId: string, itemId: string, request: WarehouseRemarkUpdateRequest) {
|
||||
const response = await http.patch<ApiEnvelope<WarehouseRemarkUpdateResponse>, ApiEnvelope<WarehouseRemarkUpdateResponse>>(
|
||||
`${basePath}/smart-loading/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(itemId)}/warehouse-remark`,
|
||||
export async function updateLoadingItemRemarks(planId: string, itemId: string, request: LoadingItemRemarksUpdateRequest) {
|
||||
const response = await http.patch<ApiEnvelope<LoadingItemRemarksUpdateResponse>, ApiEnvelope<LoadingItemRemarksUpdateResponse>>(
|
||||
`${basePath}/smart-loading/plans/${encodeURIComponent(planId)}/items/${encodeURIComponent(itemId)}/remarks`,
|
||||
request,
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
|
||||
@@ -24,7 +24,7 @@ const props = defineProps<{
|
||||
confirmSaving?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ back: []; retry: []; edit: []; editWarehouseRemark: [item: LoadingItem]; confirmPreload: [] }>()
|
||||
const emit = defineEmits<{ back: []; retry: []; edit: []; editRemarks: [item: LoadingItem]; confirmPreload: [] }>()
|
||||
|
||||
const actionColumn: DataTableColumn = { key: 'actions', label: '操作', fixed: 'left', width: 132, slotName: 'actions', hideable: false, movable: false }
|
||||
const baseItemColumns: DataTableColumn[] = [
|
||||
@@ -54,6 +54,7 @@ const baseItemColumns: DataTableColumn[] = [
|
||||
{ key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 190, slotName: 'customerRemark' },
|
||||
{ key: 'internalRemark', prop: 'internalRemark', label: '备注(内部)', minWidth: 190, slotName: 'internalRemark' },
|
||||
{ key: 'warehouseRemark', prop: 'warehouseRemark', label: '备注(仓库)', minWidth: 210, slotName: 'warehouseRemark' },
|
||||
{ key: 'customerServiceRemark', prop: 'customerServiceRemark', label: '备注(客服)', minWidth: 210, slotName: 'customerServiceRemark' },
|
||||
{ key: 'frozenAt', label: '冻结时间', minWidth: 150, slotName: 'frozenAt' },
|
||||
]
|
||||
|
||||
@@ -203,7 +204,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
>
|
||||
<template #toolbar-start><div class="detail-section__heading"><div><h2>装柜货物明细 <span>{{ detail.items.length }} 条</span></h2><p>仓库备注将随装柜完成资料下发海外仓</p></div></div></template>
|
||||
<template #actions="{ row }">
|
||||
<el-button size="small" type="primary" plain :aria-label="`编辑 ${row.inboundCode || row.orderNo} 的仓库备注`" @click="emit('editWarehouseRemark', row as LoadingItem)"><Pencil :size="14" />编辑备注</el-button>
|
||||
<el-button size="small" type="primary" plain :aria-label="`编辑 ${row.inboundCode || row.orderNo} 的备注`" @click="emit('editRemarks', row as LoadingItem)"><Pencil :size="14" />编辑备注</el-button>
|
||||
</template>
|
||||
<template #unitType="{ row }"><el-tag size="small" :type="row.unitType === 'SEALED_PALLET' ? 'success' : 'info'">{{ row.unitType === 'SEALED_PALLET' ? '封托托盘' : '箱件' }}</el-tag><small v-if="row.indivisible">整托不可拆</small></template>
|
||||
<template #grossWeight="{ row }">{{ formatNumber(row.grossWeightKg) }}</template>
|
||||
@@ -214,6 +215,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
<template #customerRemark="{ row }"><div class="remark-cell"><span>{{ row.customerRemark || '—' }}</span><small>来源:客户下单</small></div></template>
|
||||
<template #internalRemark="{ row }"><div class="remark-cell"><span>{{ row.internalRemark || '—' }}</span><small>来源:订单内部备注</small></div></template>
|
||||
<template #warehouseRemark="{ row }"><div class="remark-cell"><span>{{ row.warehouseRemark || '—' }}</span><small>{{ mode === 'preload' ? '由仓库维护' : '装柜完成时冻结' }}</small></div></template>
|
||||
<template #customerServiceRemark="{ row }"><div class="remark-cell"><span>{{ row.customerServiceRemark || '—' }}</span><small>{{ mode === 'preload' ? '由客服维护' : '装柜完成时冻结' }}</small></div></template>
|
||||
<template #frozenAt="{ row }">{{ formatDateTime(row.frozenAt) }}</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
|
||||
@@ -169,6 +169,7 @@ describe('LoadingListsView', () => {
|
||||
'customerRemark',
|
||||
'internalRemark',
|
||||
'warehouseRemark',
|
||||
'customerServiceRemark',
|
||||
]
|
||||
let previous = detailSource.indexOf("key: 'customerName'")
|
||||
for (const field of fields) {
|
||||
@@ -180,14 +181,14 @@ describe('LoadingListsView', () => {
|
||||
expect(detailSource).not.toContain("label: '退税资料'")
|
||||
})
|
||||
|
||||
it('keeps customer and internal remarks read-only while exposing per-item warehouse remark editing', () => {
|
||||
it('keeps customer remarks read-only while exposing three editable collaboration remarks', () => {
|
||||
expect(detailSource).toContain('来源:客户下单')
|
||||
expect(detailSource).toContain('来源:订单内部备注')
|
||||
expect(detailSource).toContain('仓库备注将随装柜完成资料下发海外仓')
|
||||
expect(detailSource).toContain("emit('editWarehouseRemark', row as LoadingItem)")
|
||||
expect(detailSource).toContain("emit('editRemarks', row as LoadingItem)")
|
||||
expect(detailSource).toContain("props.mode === 'preload' && props.canEdit && ['EDITING', 'LOADING'].includes(summary.value?.status || '')")
|
||||
expect(detailSource).not.toContain("emit('editCustomerRemark'")
|
||||
expect(detailSource).not.toContain("emit('editInternalRemark'")
|
||||
expect(detailSource).toContain("key: 'customerServiceRemark'")
|
||||
})
|
||||
|
||||
it('uses one white detail surface, a readable four-column summary and dense item table', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('../api/platformLoadingLists', () => ({
|
||||
confirmPreloadPlan: vi.fn(),
|
||||
getConfirmedLoadingList: vi.fn(),
|
||||
getPreloadPlan: vi.fn(),
|
||||
updateLoadingItemWarehouseRemark: vi.fn(),
|
||||
updateLoadingItemRemarks: vi.fn(),
|
||||
updatePreloadTransport: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -39,20 +39,22 @@ describe('PreloadPlanDetailView prototype contract', () => {
|
||||
expect(source).not.toContain('saveMfNote')
|
||||
})
|
||||
|
||||
it('edits only the selected item warehouse remark with optimistic locking', () => {
|
||||
expect(source).toContain('@edit-warehouse-remark="openWarehouseRemark"')
|
||||
expect(source).toContain('updateLoadingItemWarehouseRemark(current.summary.planId, target.itemId')
|
||||
it('edits the selected item internal, warehouse and customer-service remarks with optimistic locking', () => {
|
||||
expect(source).toContain('@edit-remarks="openRemarks"')
|
||||
expect(source).toContain('updateLoadingItemRemarks(current.summary.planId, target.itemId')
|
||||
expect(source).toContain('expectedPlanVersion: current.summary.version')
|
||||
expect(source).toContain('expectedItemVersion: target.itemVersion')
|
||||
expect(source).toContain('operationId: remarkOperationId.value')
|
||||
expect(source).toContain('maxlength="1000"')
|
||||
expect(source).toContain("warehouseRemark.value.trim() || null")
|
||||
expect(source).toContain("remarkForm.internalRemark.trim() || null")
|
||||
expect(source).toContain("remarkForm.warehouseRemark.trim() || null")
|
||||
expect(source).toContain("remarkForm.customerServiceRemark.trim() || null")
|
||||
expect(source).toContain('summary: { ...current.summary, version: result.version }')
|
||||
expect(source).toContain('updatedItem?.version')
|
||||
expect(source).not.toContain('plan.remark')
|
||||
})
|
||||
|
||||
it('preserves warehouse remark input and refreshes current data on conflicts', () => {
|
||||
it('preserves remark input and refreshes current data on conflicts', () => {
|
||||
expect(source).toContain("details.code === 'A0409'")
|
||||
expect(source).toContain('await loadDetail()')
|
||||
expect(source).toContain("detail.value?.items.find((item) => item.itemId === target.itemId) || target")
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getApiErrorDetails, http, unwrapApiResponse, type ApiEnvelope, type Api
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useWorkspaceTabsStore } from '@/core/workspace-tabs/workspaceTabs'
|
||||
import LoadingListDetailDrawer from '../components/LoadingListDetailDrawer.vue'
|
||||
import { confirmPreloadPlan, getConfirmedLoadingList, getPreloadPlan, updateLoadingItemWarehouseRemark, updatePreloadTransport, type LoadingDetail, type LoadingItem, type PreloadSummary } from '../api/platformLoadingLists'
|
||||
import { confirmPreloadPlan, getConfirmedLoadingList, getPreloadPlan, updateLoadingItemRemarks, updatePreloadTransport, type LoadingDetail, type LoadingItem, type PreloadSummary } from '../api/platformLoadingLists'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -34,7 +34,7 @@ const remarkOpen = ref(false)
|
||||
const remarkSaving = ref(false)
|
||||
const remarkError = ref<ApiErrorDetails | null>(null)
|
||||
const remarkTarget = ref<LoadingItem | null>(null)
|
||||
const warehouseRemark = ref('')
|
||||
const remarkForm = reactive({ internalRemark: '', warehouseRemark: '', customerServiceRemark: '' })
|
||||
const remarkOperationId = ref('')
|
||||
let loadSequence = 0
|
||||
const planId = computed(() => String(route.params.planId || ''))
|
||||
@@ -163,28 +163,32 @@ async function saveTransport() {
|
||||
}
|
||||
}
|
||||
|
||||
function openWarehouseRemark(item: LoadingItem) {
|
||||
function openRemarks(item: LoadingItem) {
|
||||
if (!canEdit.value || !detail.value || !['EDITING', 'LOADING'].includes(detail.value.summary.status)) return
|
||||
remarkTarget.value = item
|
||||
warehouseRemark.value = item.warehouseRemark || ''
|
||||
remarkForm.internalRemark = item.internalRemark || ''
|
||||
remarkForm.warehouseRemark = item.warehouseRemark || ''
|
||||
remarkForm.customerServiceRemark = item.customerServiceRemark || ''
|
||||
remarkError.value = null
|
||||
remarkOperationId.value = `admin-preload-warehouse-remark-${detail.value.summary.planId}-${item.itemId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
remarkOperationId.value = `admin-preload-remarks-${detail.value.summary.planId}-${item.itemId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
remarkOpen.value = true
|
||||
}
|
||||
|
||||
async function saveWarehouseRemark() {
|
||||
async function saveRemarks() {
|
||||
const target = remarkTarget.value
|
||||
const current = detail.value
|
||||
if (!target || !current || !canEdit.value || remarkSaving.value) return
|
||||
if (warehouseRemark.value.length > 1000) {
|
||||
remarkError.value = { code: 'WAREHOUSE_REMARK_TOO_LONG', message: '仓库备注不能超过 1000 个字符' }
|
||||
if (Object.values(remarkForm).some((value) => value.length > 1000)) {
|
||||
remarkError.value = { code: 'REMARK_TOO_LONG', message: '每项备注不能超过 1000 个字符' }
|
||||
return
|
||||
}
|
||||
remarkSaving.value = true
|
||||
remarkError.value = null
|
||||
try {
|
||||
const result = await updateLoadingItemWarehouseRemark(current.summary.planId, target.itemId, {
|
||||
warehouseRemark: warehouseRemark.value.trim() || null,
|
||||
const result = await updateLoadingItemRemarks(current.summary.planId, target.itemId, {
|
||||
internalRemark: remarkForm.internalRemark.trim() || null,
|
||||
warehouseRemark: remarkForm.warehouseRemark.trim() || null,
|
||||
customerServiceRemark: remarkForm.customerServiceRemark.trim() || null,
|
||||
expectedPlanVersion: current.summary.version,
|
||||
expectedItemVersion: target.itemVersion,
|
||||
operationId: remarkOperationId.value,
|
||||
@@ -194,15 +198,18 @@ async function saveWarehouseRemark() {
|
||||
...current,
|
||||
summary: { ...current.summary, version: result.version },
|
||||
items: current.items.map((item) => item.itemId === target.itemId
|
||||
? { ...item, itemVersion: updatedItem?.version ?? item.itemVersion, warehouseRemark: updatedItem?.warehouseRemark ?? null }
|
||||
? { ...item, itemVersion: updatedItem?.version ?? item.itemVersion,
|
||||
internalRemark: updatedItem?.internalRemark ?? null,
|
||||
warehouseRemark: updatedItem?.warehouseRemark ?? null,
|
||||
customerServiceRemark: updatedItem?.customerServiceRemark ?? null }
|
||||
: item),
|
||||
}
|
||||
remarkOpen.value = false
|
||||
remarkTarget.value = null
|
||||
remarkOperationId.value = ''
|
||||
ElMessage.success('仓库备注已更新')
|
||||
ElMessage.success('备注已更新')
|
||||
} catch (cause) {
|
||||
const details = getApiErrorDetails(cause, '仓库备注保存失败,请稍后重试')
|
||||
const details = getApiErrorDetails(cause, '备注保存失败,请稍后重试')
|
||||
if (details.code === 'A0409' || /版本|并发|状态.*变化/.test(details.message)) {
|
||||
await loadDetail()
|
||||
remarkTarget.value = detail.value?.items.find((item) => item.itemId === target.itemId) || target
|
||||
@@ -247,7 +254,7 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
<AdminPage>
|
||||
<div class="preload-detail-page admin-density">
|
||||
<OperationErrorToast :details="remarkError || editError || confirmError" />
|
||||
<LoadingListDetailDrawer :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :can-edit-transport="canCorrectTransport" :transport-note="transportCorrectionNote" :confirm-saving="confirmSaving" @back="goBack" @retry="loadDetail" @edit="openEdit" @edit-warehouse-remark="openWarehouseRemark" @confirm-preload="openConfirmPreload" />
|
||||
<LoadingListDetailDrawer :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :can-edit-transport="canCorrectTransport" :transport-note="transportCorrectionNote" :confirm-saving="confirmSaving" @back="goBack" @retry="loadDetail" @edit="openEdit" @edit-remarks="openRemarks" @confirm-preload="openConfirmPreload" />
|
||||
</div>
|
||||
|
||||
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
||||
@@ -258,14 +265,15 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog admin-density-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
|
||||
<el-alert type="info" :closable="false" show-icon title="仓库备注将随装柜完成资料下发海外仓" />
|
||||
<el-form label-position="top" class="warehouse-remark-form">
|
||||
<AppDialog v-model="remarkOpen" class="remarks-dialog admin-density-dialog" title="编辑备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveRemarks">
|
||||
<el-alert type="info" :closable="false" show-icon title="客户备注来自客户下单且不可修改;其余备注可维护" />
|
||||
<el-form label-position="top" class="remarks-form">
|
||||
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
|
||||
<el-form-item label="内部备注(只读)"><el-input :model-value="remarkTarget?.internalRemark || '—'" disabled /><small>来源:订单内部备注</small></el-form-item>
|
||||
<el-form-item label="仓库备注"><el-input v-model="warehouseRemark" type="textarea" :rows="5" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
|
||||
<el-form-item label="内部备注"><el-input v-model="remarkForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item>
|
||||
<el-form-item label="仓库备注"><el-input v-model="remarkForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
|
||||
<el-form-item label="客服备注"><el-input v-model="remarkForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></el-form-item>
|
||||
</el-form>
|
||||
<LoadErrorState v-if="remarkError" compact title="仓库备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="remarkError" compact title="备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
|
||||
@@ -286,8 +294,8 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
|
||||
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.confirm-content { display: grid; gap: 12px; }
|
||||
.warehouse-remark-form { margin-top: 14px; }
|
||||
.warehouse-remark-form small { display: block; margin-top: 4px; color: var(--cc-muted); }
|
||||
.remarks-form { margin-top: 14px; }
|
||||
.remarks-form small { display: block; margin-top: 4px; color: var(--cc-muted); }
|
||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
||||
@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: 12px; } }
|
||||
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
approvePlatformUnifiedOrder,
|
||||
batchApprovePlatformUnifiedOrders,
|
||||
getPlatformUnifiedOrderBatchApprovalPreview,
|
||||
addPlatformUnifiedOrderNote,
|
||||
updatePlatformUnifiedOrderRemarks,
|
||||
getPlatformUnifiedOrder,
|
||||
downloadPlatformProductImage,
|
||||
listPlatformUnifiedOrders,
|
||||
@@ -172,10 +172,11 @@ describe('unified orders API contract', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('writes platform internal notes through the unified order boundary', async () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ data: '91001' })
|
||||
await expect(addPlatformUnifiedOrderNote('208', '90001', '已确认船期')).resolves.toBe('91001')
|
||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/90001/internal-notes', { content: '已确认船期' }, { params: { tenantId: '208' } })
|
||||
it('updates the three editable remarks while keeping customer remark outside the request', async () => {
|
||||
const request = { expectedVersion: 7, internalRemark: '内部', warehouseRemark: '仓库', customerServiceRemark: '客服' }
|
||||
vi.mocked(http.put).mockResolvedValue({ data: { ...request, version: 8, customerRemark: '客户只读' } })
|
||||
await expect(updatePlatformUnifiedOrderRemarks('208', '90001', request)).resolves.toMatchObject({ version: 8 })
|
||||
expect(http.put).toHaveBeenCalledWith('/platform/v1/orders/90001/remarks', request, { params: { tenantId: '208' } })
|
||||
})
|
||||
|
||||
it('updates customer declaration fields through the versioned tenant-scoped contract', async () => {
|
||||
|
||||
@@ -160,6 +160,8 @@ export interface UnifiedOrderResource {
|
||||
customerRemark?: string | null
|
||||
latestInternalNote?: string | null
|
||||
internalRemark?: string | null
|
||||
warehouseRemark?: string | null
|
||||
customerServiceRemark?: string | null
|
||||
createdBy?: string | null
|
||||
}
|
||||
|
||||
@@ -599,10 +601,25 @@ export function batchApprovePlatformUnifiedOrders(tenantId: string, items: Unifi
|
||||
).then(unwrapApiResponse)
|
||||
}
|
||||
|
||||
export function addPlatformUnifiedOrderNote(tenantId: string, orderId: string, content: string) {
|
||||
return http.post<ApiEnvelope<number | string>, ApiEnvelope<number | string>>(
|
||||
`${unifiedOrdersApiPath}/${orderId}/internal-notes`,
|
||||
{ content },
|
||||
export interface UnifiedOrderRemarksUpdateRequest {
|
||||
expectedVersion: number
|
||||
internalRemark: string | null
|
||||
warehouseRemark: string | null
|
||||
customerServiceRemark: string | null
|
||||
}
|
||||
|
||||
export interface UnifiedOrderRemarksUpdateResponse {
|
||||
version: number
|
||||
customerRemark: string | null
|
||||
internalRemark: string | null
|
||||
warehouseRemark: string | null
|
||||
customerServiceRemark: string | null
|
||||
}
|
||||
|
||||
export function updatePlatformUnifiedOrderRemarks(tenantId: string, orderId: string, request: UnifiedOrderRemarksUpdateRequest) {
|
||||
return http.put<ApiEnvelope<UnifiedOrderRemarksUpdateResponse>, ApiEnvelope<UnifiedOrderRemarksUpdateResponse>>(
|
||||
`${unifiedOrdersApiPath}/${orderId}/remarks`,
|
||||
request,
|
||||
{ params: { tenantId } },
|
||||
).then(unwrapApiResponse)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mock
|
||||
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
updatePlatformUnifiedOrderRemarks: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
|
||||
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
|
||||
}))
|
||||
|
||||
@@ -26,7 +26,7 @@ vi.mock('@/shared/api/inboundCodeRelations', () => ({
|
||||
getLoadingAssociationSummary: vi.fn(), getOverseasInboundCodeRelationship: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
updatePlatformUnifiedOrderRemarks: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
downloadPlatformProductImage: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
|
||||
unifiedOrderStatusOptions: [],
|
||||
|
||||
@@ -24,7 +24,7 @@ vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(),
|
||||
updatePlatformUnifiedOrderRemarks: vi.fn(),
|
||||
approvePlatformUnifiedOrder: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(),
|
||||
listPlatformUnifiedOrderFeeDetails: vi.fn(),
|
||||
|
||||
@@ -375,6 +375,16 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain("value.replaceAll('_', ' ')")
|
||||
})
|
||||
|
||||
it('keeps customer remarks read-only and edits the three collaboration remarks together', () => {
|
||||
expect(source).toContain('title="编辑订单备注"')
|
||||
expect(source).toContain('label="客户备注(只读)"')
|
||||
expect(source).toContain(':model-value="noteOrder?.customerRemark')
|
||||
expect(source).toContain('v-model="noteForm.internalRemark"')
|
||||
expect(source).toContain('v-model="noteForm.warehouseRemark"')
|
||||
expect(source).toContain('v-model="noteForm.customerServiceRemark"')
|
||||
expect(source).toContain('await updatePlatformUnifiedOrderRemarks')
|
||||
})
|
||||
|
||||
it('keeps the customer order package count independent from measured inbound totals', () => {
|
||||
expect(source).toContain("{ source: '客户下单数据', count: customerTotals.count")
|
||||
expect(source).not.toContain("{ source: '客户下单数据', count: summary?.totalPackageCount")
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
approvePlatformUnifiedOrder,
|
||||
batchApprovePlatformUnifiedOrders,
|
||||
getPlatformUnifiedOrderBatchApprovalPreview,
|
||||
addPlatformUnifiedOrderNote,
|
||||
updatePlatformUnifiedOrderRemarks,
|
||||
listPlatformUnifiedOrders,
|
||||
updatePlatformUnifiedOrderPlanning,
|
||||
unifiedOrderStatusOptions,
|
||||
@@ -179,7 +179,7 @@ const coLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const approvalOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const noteOpen = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
const noteContent = ref('')
|
||||
const noteForm = reactive({ internalRemark: '', warehouseRemark: '', customerServiceRemark: '' })
|
||||
const noteOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const holdDetail = ref<InboundHoldDetail | null>(null)
|
||||
const holdLoading = ref(false)
|
||||
@@ -1360,27 +1360,30 @@ function openNoteDialog(order?: UnifiedOrderResource) {
|
||||
const target = order || detail.value?.summary || null
|
||||
if (!target) return
|
||||
noteOrder.value = target
|
||||
noteContent.value = ''
|
||||
noteForm.internalRemark = target.latestInternalNote || target.internalRemark || ''
|
||||
noteForm.warehouseRemark = target.warehouseRemark || ''
|
||||
noteForm.customerServiceRemark = target.customerServiceRemark || ''
|
||||
noteOpen.value = true
|
||||
}
|
||||
|
||||
async function saveNote() {
|
||||
const content = noteContent.value.trim()
|
||||
if (!noteOrder.value || !content) {
|
||||
ElMessage.warning('请填写内部备注')
|
||||
return
|
||||
}
|
||||
if (!noteOrder.value) return
|
||||
noteSaving.value = true
|
||||
try {
|
||||
const target = noteOrder.value
|
||||
await addPlatformUnifiedOrderNote(String(target.tenantId), String(target.orderId), content)
|
||||
await updatePlatformUnifiedOrderRemarks(String(target.tenantId), String(target.orderId), {
|
||||
expectedVersion: Number(target.orderVersion || 0),
|
||||
internalRemark: noteForm.internalRemark.trim() || null,
|
||||
warehouseRemark: noteForm.warehouseRemark.trim() || null,
|
||||
customerServiceRemark: noteForm.customerServiceRemark.trim() || null,
|
||||
})
|
||||
noteOpen.value = false
|
||||
await loadOrders()
|
||||
if (detail.value?.summary.orderId === target.orderId) await loadDetail(true)
|
||||
noteOrder.value = null
|
||||
ElMessage.success('内部备注已保存')
|
||||
ElMessage.success('订单备注已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '内部备注保存失败,请稍后重试').message)
|
||||
ElMessage.error(getApiErrorDetails(error, '订单备注保存失败,请稍后重试').message)
|
||||
} finally {
|
||||
noteSaving.value = false
|
||||
}
|
||||
@@ -2725,8 +2728,13 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</AppDrawer>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
|
||||
<AppDialog class="admin-density-dialog" v-model="noteOpen" title="编辑订单备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<el-form label-position="top" class="order-remarks-form">
|
||||
<el-form-item label="客户备注(只读)"><el-input :model-value="noteOrder?.customerRemark || '—'" disabled /></el-form-item>
|
||||
<el-form-item label="内部备注"><el-input v-model="noteForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item>
|
||||
<el-form-item label="仓库备注"><el-input v-model="noteForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
|
||||
<el-form-item label="客服备注"><el-input v-model="noteForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
|
||||
|
||||
Reference in New Issue
Block a user