优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Plus } from '@lucide/vue'
|
||||
import { Plus, PackagePlus } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -14,7 +14,7 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { changeRelabelOrderStatus, createRelabelOrder, listRelabelOrders, listRelabelWarehouseOptions, type RelabelWarehouseOptionDto } from '../api/relabelOrders'
|
||||
import { changeRelabelOrderStatus, createRelabelOrder, createReturnPackingPlan, listRelabelOrders, listRelabelWarehouseOptions, type RelabelWarehouseOptionDto, type ReturnPackingPlanBox } from '../api/relabelOrders'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { formatOverseasDateTime, relabelBusinessStatusLabel } from '../lastMilePresentation'
|
||||
import type { RelabelOrderListItem } from '../types'
|
||||
@@ -42,6 +42,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||||
{ key: 'orderNo', label: '换标单号', minWidth: 170 },
|
||||
{ key: 'sku', label: '商品SKU', minWidth: 150 },
|
||||
{ key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
@@ -63,6 +64,12 @@ const submitting = ref(false)
|
||||
const editSourceStatus = ref('')
|
||||
const form = reactive({ orderNo: '', customerId: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined as number | undefined, budgetTotal: undefined as number | undefined, status: 'PENDING_PROCESSING', labelRequirement: '', relabelCost: undefined as number | undefined, remark: '', warehouseId: '', labelDeadline: '', contactName: '', contactPhone: '', shipmentNo: '', newSkuCode: '' })
|
||||
const createIdempotencyKey = ref('')
|
||||
const selectedRows = ref<RelabelOrderListItem[]>([])
|
||||
const packingDialogOpen = ref(false)
|
||||
const packingSubmitting = ref(false)
|
||||
const packingMode = ref<'CUSTOMER_INSTRUCTION' | 'WAREHOUSE_RECOMMENDATION'>('CUSTOMER_INSTRUCTION')
|
||||
const packingRemark = ref('')
|
||||
const packingBoxes = ref<ReturnPackingPlanBox[]>([])
|
||||
const allowedStatusOptions = computed(() => {
|
||||
if (editSourceStatus.value === 'PENDING_REVIEW') return ['PENDING_PROCESSING', 'REJECTED']
|
||||
if (editSourceStatus.value === 'PENDING_PROCESSING') return ['PROCESSING', 'REJECTED', 'CANCELLED']
|
||||
@@ -71,6 +78,39 @@ const allowedStatusOptions = computed(() => {
|
||||
})
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function selectedSkus() {
|
||||
const values = new Map<string, number>()
|
||||
for (const row of selectedRows.value) {
|
||||
const skus = String(row.sku || '').split(',').map((value) => value.trim()).filter(Boolean)
|
||||
for (const sku of skus.length ? skus : ['未维护 SKU']) values.set(sku, (values.get(sku) || 0) + Math.max(0, Number(row.cartons || 0)))
|
||||
}
|
||||
return [...values.entries()].map(([skuCode, quantity]) => ({ skuCode, quantity }))
|
||||
}
|
||||
function openPackingPlan() {
|
||||
if (!selectedRows.value.length) { ElMessage.warning('请先勾选已入库退货订单'); return }
|
||||
packingMode.value = 'CUSTOMER_INSTRUCTION'
|
||||
packingRemark.value = ''
|
||||
packingBoxes.value = selectedSkus().map((item, index) => ({ boxNo: index + 1, items: [{ ...item }] }))
|
||||
packingDialogOpen.value = true
|
||||
}
|
||||
function addPackingBox() { packingBoxes.value.push({ boxNo: packingBoxes.value.length + 1, items: [{ skuCode: '', quantity: 0 }] }) }
|
||||
function removePackingBox(index: number) { if (packingBoxes.value.length > 1) packingBoxes.value.splice(index, 1) }
|
||||
function addBoxItem(box: ReturnPackingPlanBox) { box.items.push({ skuCode: '', quantity: 0 }) }
|
||||
function removeBoxItem(box: ReturnPackingPlanBox, index: number) { if (box.items.length > 1) box.items.splice(index, 1) }
|
||||
async function submitPackingPlan() {
|
||||
if (packingSubmitting.value) return
|
||||
const boxes = packingBoxes.value.map((box, index) => ({ ...box, boxNo: index + 1, items: box.items.filter((item) => item.skuCode.trim() && Number(item.quantity) > 0).map((item) => ({ skuCode: item.skuCode.trim(), quantity: Number(item.quantity) })) })).filter((box) => box.items.length)
|
||||
if (!boxes.length) { ElMessage.warning('至少配置一个箱和一条 SKU 数量'); return }
|
||||
packingSubmitting.value = true
|
||||
try {
|
||||
const result = await createReturnPackingPlan({ returnNos: selectedRows.value.map((row) => row.relatedOrderNo).filter((value) => value && value !== '—'), mode: packingMode.value, boxes, remark: packingRemark.value.trim() || undefined, idempotencyKey: globalThis.crypto?.randomUUID?.() || `packing-${Date.now()}` })
|
||||
ElMessage.success(`合箱计划 ${result.planNo} 已推送 PDA`)
|
||||
packingDialogOpen.value = false
|
||||
selectedRows.value = []
|
||||
await load()
|
||||
} catch (error) { ElMessage.error(getApiErrorDetails(error, '合箱计划推送失败,请重试').message) }
|
||||
finally { packingSubmitting.value = false }
|
||||
}
|
||||
function openForm(mode: 'create' | 'edit', source?: object) {
|
||||
const row = source as RelabelOrderListItem | undefined
|
||||
dialogMode.value = mode
|
||||
@@ -201,12 +241,21 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
<AdminPage class="admin-density"><ListPageLayout aria-label="换标单列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="换标单筛选" preference-key="overseas-relabel-orders-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="换标单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-relabel-orders" row-key="orderNo" empty-text="暂无换标单" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.relabelOrderCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增换标单</el-button></template>
|
||||
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-relabel-orders" row-key="orderNo" empty-text="暂无换标单" @selection-change="selectedRows = $event" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="selectedRows.length" type="warning" @click="openPackingPlan"><PackagePlus :size="16" />创建合箱计划({{ selectedRows.length }})</el-button><el-button v-if="access.can(permission.relabelOrderCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增换标单</el-button></template>
|
||||
<template #actions="{ row }"><div class="density-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="warning" v-if="access.can(permission.relabelOrderStatus) && canAudit(row)" size="small" @click="openForm('edit', row)">审核</el-button></div></template>
|
||||
<template #pagination-left>共 {{ total }} 条换标单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AppDialog class="admin-density-dialog" v-model="packingDialogOpen" title="配置退货合箱计划" width="min(980px, calc(100vw - 28px))" confirm-text="推送 PDA 合箱计划" :confirm-loading="packingSubmitting" @confirm="submitPackingPlan">
|
||||
<div class="packing-plan-dialog">
|
||||
<div class="packing-plan-summary"><span>已选订单:{{ selectedRows.length }} 个</span><span>SKU:{{ selectedSkus().length }} 种</span><span>默认:单 SKU 一箱,可调整为混箱</span></div>
|
||||
<el-radio-group v-model="packingMode" class="packing-mode"><el-radio-button label="CUSTOMER_INSTRUCTION">客户合箱指令</el-radio-button><el-radio-button label="WAREHOUSE_RECOMMENDATION">仓库自主合箱</el-radio-button></el-radio-group>
|
||||
<div v-for="(box, boxIndex) in packingBoxes" :key="boxIndex" class="packing-box"><div class="packing-box-head"><strong>第 {{ boxIndex + 1 }} 箱</strong><el-button text type="danger" :disabled="packingBoxes.length <= 1" @click="removePackingBox(boxIndex)">删除箱</el-button></div><div v-for="(item, itemIndex) in box.items" :key="itemIndex" class="packing-item"><el-input v-model="item.skuCode" placeholder="SKU" /><el-input-number v-model="item.quantity" :min="0" :precision="0" controls-position="right" placeholder="数量" /><el-button text type="danger" :disabled="box.items.length <= 1" @click="removeBoxItem(box, itemIndex)">移除</el-button></div><el-button text type="primary" @click="addBoxItem(box)">+ 添加 SKU</el-button><div class="packing-dimensions"><el-input-number v-model="box.length" :min="0" :precision="2" placeholder="长 cm" /><el-input-number v-model="box.width" :min="0" :precision="2" placeholder="宽 cm" /><el-input-number v-model="box.height" :min="0" :precision="2" placeholder="高 cm" /><el-input-number v-model="box.weight" :min="0" :precision="2" placeholder="重量 kg" /></div></div>
|
||||
<el-button plain type="primary" @click="addPackingBox">+ 添加箱</el-button>
|
||||
<el-input v-model="packingRemark" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="合箱备注(选填)" />
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="dialogOpen" :title="dialogMode === 'create' ? '新增换标单' : '审核换标单'" width="min(760px, calc(100vw - 28px))" :confirm-text="dialogMode === 'create' ? '保存换标单' : '提交审核'" :confirm-loading="submitting" @confirm="submitForm">
|
||||
<el-form class="density-form" label-position="top"><h3>订单信息</h3><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="换标单号"><el-input :model-value="dialogMode === 'create' ? '系统自动生成' : form.orderNo || '—'" readonly /></el-form-item></el-col>
|
||||
@@ -236,4 +285,13 @@ h3 { margin: 0 0 var(--admin-space-5); color: var(--cc-text); font-size: var(--a
|
||||
h3:not(:first-child) { margin-top: var(--admin-space-1); padding-top: var(--admin-space-5); border-top: 1px solid var(--cc-border); }
|
||||
.el-form :deep(.el-row) { margin-left: 0 !important; margin-right: 0 !important; }
|
||||
.el-form :deep(.el-input), .el-form :deep(.el-select), .el-form :deep(.el-input-number), .el-form :deep(.el-date-editor) { width: 100%; }
|
||||
.packing-plan-dialog { display: grid; gap: 14px; }
|
||||
.packing-plan-summary { display: flex; gap: 24px; color: var(--cc-text-secondary); font-size: 13px; }
|
||||
.packing-mode { margin-bottom: 2px; }
|
||||
.packing-box { border: 1px solid var(--cc-border); border-radius: 6px; padding: 14px; display: grid; gap: 10px; }
|
||||
.packing-box-head, .packing-item, .packing-dimensions { display: flex; align-items: center; gap: 10px; }
|
||||
.packing-box-head { justify-content: space-between; }
|
||||
.packing-item .el-input { flex: 1; }
|
||||
.packing-item .el-input-number { width: 160px; }
|
||||
.packing-dimensions .el-input-number { width: 150px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user