feat: edit collaboration remarks across order loading
This commit is contained in:
@@ -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; } }
|
||||
|
||||
Reference in New Issue
Block a user