优化提交
This commit is contained in:
@@ -1,66 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Plus } from '@lucide/vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Eye, Pencil, Plus, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import ContractPendingNotice from '../components/ContractPendingNotice.vue'
|
||||
import ContractPendingFormDialog from '../components/ContractPendingFormDialog.vue'
|
||||
import { createPallet, deletePallet, getPallet, listCustomerWarehouses, listPallets, updatePallet, type CustomerWarehouseDto, type PalletDto } from '../api/pallets'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import type { PalletListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const warehouses = ref<CustomerWarehouseDto[]>([])
|
||||
const rows = ref<PalletListItem[]>([])
|
||||
const loading = ref(false)
|
||||
const contextLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const deletingId = ref('')
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const filters = ref<QueryModel>({ code: '', type: '', warehouse: '', status: '' })
|
||||
const fields: QueryField[] = [
|
||||
const total = ref(0)
|
||||
const filters = ref<QueryModel>({ customerId: '', warehouseId: '', code: '', type: '', status: '' })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户名称', width: 240, placeholder: '请选择客户', options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })), props: { filterable: true, clearable: true } },
|
||||
{ key: 'warehouseId', type: 'select', label: '仓库', width: 240, placeholder: contextLoading.value ? '仓库加载中' : '请选择仓库', options: warehouses.value.map((item) => ({ label: `${item.warehouseName}(${item.warehouseCode})`, value: String(item.id) })), props: { filterable: true, clearable: true, disabled: !filters.value.customerId || contextLoading.value } },
|
||||
{ key: 'code', type: 'input', label: '托盘编码', placeholder: '请输入托盘编码', width: 180 },
|
||||
{ key: 'type', type: 'select', label: '托盘类型', placeholder: '全部类型', width: 160, options: ['木托盘', '塑料托盘', '金属托盘', '纸托盘', '免熏蒸'].map((value) => ({ label: value, value })) },
|
||||
{ key: 'warehouse', type: 'select', label: '仓库', placeholder: '全部仓库', width: 190, options: [] },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 140, options: ['闲置', '占用'].map((value) => ({ label: value, value })) },
|
||||
]
|
||||
{ key: 'type', type: 'select', label: '托盘类型', placeholder: '全部类型', width: 160, options: ['WOOD', 'PLASTIC', 'METAL', 'PAPER', 'FUMIGATION_FREE'].map((value) => ({ label: palletTypeLabel(value), value })) },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 140, options: [{ label: '闲置', value: 'IDLE' }, { label: '占用', value: 'OCCUPIED' }, { label: '已报废', value: 'SCRAPPED' }] },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'code', label: '托盘编码', minWidth: 160 }, { key: 'type', label: '托盘类型', minWidth: 130 },
|
||||
{ key: 'warehouse', label: '仓库', minWidth: 180 }, { key: 'volume', label: '容积(m³)', width: 110, align: 'right' },
|
||||
{ key: 'purchasePrice', label: '采购价格', width: 120, align: 'right' }, { key: 'fbaWarehouse', label: '所属FBA仓', minWidth: 140 },
|
||||
{ key: 'status', label: '状态', width: 100 }, { key: 'remark', label: '备注', minWidth: 180 },
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'code', prop: 'code', label: '托盘编码', minWidth: 160 }, { key: 'type', prop: 'type', label: '托盘类型', minWidth: 130 },
|
||||
{ key: 'warehouse', prop: 'warehouse', label: '仓库', minWidth: 180 }, { key: 'volume', prop: 'volume', label: '容积(m³)', width: 110, align: 'right' },
|
||||
{ key: 'purchasePrice', prop: 'purchasePrice', label: '采购价格', width: 120, align: 'right' }, { key: 'fbaWarehouse', prop: 'fbaWarehouse', label: '所属FBA仓', minWidth: 140 },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
const form = reactive({ code: '', type: '木托盘', volume: undefined as number | undefined, purchasePrice: undefined as number | undefined, warehouse: '', fbaWarehouse: '', status: '闲置', remark: '' })
|
||||
const form = reactive({ id: '', customerId: '', warehouseId: '', code: '', type: 'WOOD', volume: 2, purchasePrice: 0, status: 'IDLE', remark: '' })
|
||||
|
||||
function optionalNumber(value: unknown) { const number = Number(value); return value === '' || value == null || !Number.isFinite(number) ? undefined : number }
|
||||
function openForm(mode: 'create' | 'edit', source?: object) { const row = source as PalletListItem | undefined; dialogMode.value = mode; Object.assign(form, { code: '', type: '木托盘', volume: undefined, purchasePrice: undefined, warehouse: '', fbaWarehouse: '', status: '闲置', remark: '' }); if (mode === 'edit' && row) Object.assign(form, { code: String(row.code || ''), type: String(row.type || '木托盘'), volume: optionalNumber(row.volume), purchasePrice: optionalNumber(row.purchasePrice), warehouse: String(row.warehouse || ''), fbaWarehouse: String(row.fbaWarehouse || ''), status: String(row.status || '闲置'), remark: String(row.remark || '') }); dialogOpen.value = true }
|
||||
function openDetail(source: object) { const row = source as PalletListItem; void router.push({ name: 'overseas-pallet-detail', params: { palletId: String(row.code) } }) }
|
||||
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
|
||||
function number(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : 0 }
|
||||
function palletTypeLabel(value: string) { return ({ WOOD: '木托盘', PLASTIC: '塑料托盘', METAL: '金属托盘', PAPER: '纸托盘', FUMIGATION_FREE: '免熏蒸托盘' } as Record<string, string>)[value] || value }
|
||||
function statusLabel(value: string) { return ({ IDLE: '闲置', OCCUPIED: '占用', SCRAPPED: '已报废' } as Record<string, string>)[value] || value }
|
||||
function warehouseName(id: string) { const warehouse = warehouses.value.find((item) => String(item.id) === id); return warehouse ? `${warehouse.warehouseName}(${warehouse.warehouseCode})` : '—' }
|
||||
function toRow(dto: PalletDto): PalletListItem { return { id: String(dto.id), customerId: String(dto.customerId), warehouseId: String(dto.warehouseId), locationId: text(dto.locationId), code: text(dto.palletCode), type: palletTypeLabel(dto.palletType), warehouse: warehouseName(String(dto.warehouseId)), volume: number(dto.capacityCbm), purchasePrice: number(dto.unitPrice), fbaWarehouse: '—', status: statusLabel(dto.status), remark: text(dto.remark) } }
|
||||
|
||||
async function loadCustomers() {
|
||||
contextLoading.value = true; loadError.value = null
|
||||
try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records }
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') }
|
||||
finally { contextLoading.value = false }
|
||||
}
|
||||
|
||||
async function loadWarehouses(customerId: string) {
|
||||
warehouses.value = []; filters.value.warehouseId = ''; rows.value = []; total.value = 0
|
||||
if (!customerId) return
|
||||
contextLoading.value = true; loadError.value = null
|
||||
try { warehouses.value = (await listCustomerWarehouses(customerId)).records }
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') }
|
||||
finally { contextLoading.value = false }
|
||||
}
|
||||
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
const customerId = String(filters.value.customerId || '')
|
||||
const warehouseId = String(filters.value.warehouseId || '')
|
||||
if (!customerId || !warehouseId) { rows.value = []; total.value = 0; return }
|
||||
loading.value = true; loadError.value = null
|
||||
try {
|
||||
const result = await listPallets(customerId, warehouseId, { page: page.value, size: pageSize.value, keyword: String(filters.value.code || '').trim() || undefined, palletType: String(filters.value.type || '') || undefined, status: String(filters.value.status || '') || undefined })
|
||||
rows.value = result.records.map(toRow); total.value = result.total
|
||||
} catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '托盘列表加载失败,请重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (!filters.value.customerId || !filters.value.warehouseId) { ElMessage.warning('请先选择客户和仓库'); return }
|
||||
dialogMode.value = 'create'; Object.assign(form, { id: '', customerId: String(filters.value.customerId), warehouseId: String(filters.value.warehouseId), code: '', type: 'WOOD', volume: 2, purchasePrice: 0, status: 'IDLE', remark: '' }); mutationError.value = null; dialogOpen.value = true
|
||||
}
|
||||
|
||||
async function openEdit(source: object) {
|
||||
const row = source as PalletListItem; saving.value = true; mutationError.value = null
|
||||
try {
|
||||
const detail = await getPallet(row.customerId, row.warehouseId, row.id)
|
||||
dialogMode.value = 'edit'; Object.assign(form, { id: String(detail.id), customerId: String(detail.customerId), warehouseId: String(detail.warehouseId), code: detail.palletCode, type: detail.palletType, volume: number(detail.capacityCbm), purchasePrice: number(detail.unitPrice), status: detail.status, remark: detail.remark || '' }); dialogOpen.value = true
|
||||
} catch (error) { mutationError.value = getApiErrorDetails(error, '托盘详情加载失败,请重试') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.code.trim() || !form.type || form.volume <= 0 || form.purchasePrice < 0 || !form.status) { ElMessage.warning('请完整填写托盘编码、类型、容积、采购价格和状态'); return }
|
||||
saving.value = true; mutationError.value = null
|
||||
const payload = { palletCode: form.code.trim(), palletType: form.type, capacityCbm: form.volume, unitPrice: form.purchasePrice, status: form.status, locationId: null, remark: form.remark.trim() || null }
|
||||
try {
|
||||
if (dialogMode.value === 'edit') await updatePallet(form.customerId, form.warehouseId, form.id, payload)
|
||||
else await createPallet(form.customerId, form.warehouseId, payload)
|
||||
dialogOpen.value = false; ElMessage.success(dialogMode.value === 'edit' ? '托盘已更新' : '托盘已新增'); await load()
|
||||
} catch (error) { mutationError.value = getApiErrorDetails(error, '托盘保存失败,请重试') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function remove(source: object) {
|
||||
const row = source as PalletListItem
|
||||
try { await ElMessageBox.confirm(`确认删除托盘 ${row.code}?`, '删除托盘', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }) }
|
||||
catch { return }
|
||||
deletingId.value = row.id; mutationError.value = null
|
||||
try { await deletePallet(row.customerId, row.warehouseId, row.id); ElMessage.success('托盘已删除'); await load() }
|
||||
catch (error) { mutationError.value = getApiErrorDetails(error, '托盘删除失败,请重试') }
|
||||
finally { deletingId.value = '' }
|
||||
}
|
||||
|
||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||
function openDetail(source: object) { const row = source as PalletListItem; void router.push({ name: 'overseas-pallet-detail', params: { palletId: row.id }, query: { customerId: row.customerId, warehouseId: row.warehouseId } }) }
|
||||
watch(() => String(filters.value.customerId || ''), (value, oldValue) => { if (value !== oldValue) void loadWarehouses(value) })
|
||||
onMounted(loadCustomers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage><ListPageLayout aria-label="托盘管理列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" ariaLabel="托盘筛选" preference-key="overseas-pallets-query" @search="page = 1" /></template>
|
||||
<template #state><ContractPendingNotice domain="托盘管理" /></template>
|
||||
<section class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :total="0" preference-key="overseas-pallets" row-key="code" empty-text="海外区域托盘查询契约待接入">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.palletCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增托盘</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.palletEdit)" size="small" plain @click="openForm('edit', row)"><Pencil :size="15" />编辑</el-button></template>
|
||||
<template #pagination-left>共 0 条托盘</template>
|
||||
<AdminPage><OperationErrorToast :details="mutationError" /><ListPageLayout aria-label="托盘管理列表">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading || contextLoading" ariaLabel="托盘筛选" preference-key="overseas-pallets-query" @search="search" @reset="page = 1; rows = []; total = 0" /></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="托盘数据加载失败" :message="loadError.message" :loading="loading || contextLoading" @retry="filters.customerId ? loadWarehouses(String(filters.customerId)) : loadCustomers()"><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-pallets" row-key="id" :empty-text="filters.customerId && filters.warehouseId ? '暂无托盘' : '请先选择客户和仓库后查询'" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><el-button v-if="access.can(permission.palletCreate)" type="primary" @click="openCreate"><Plus :size="16" />新增托盘</el-button></template>
|
||||
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button><el-button v-if="access.can(permission.palletEdit)" size="small" plain @click="openEdit(row)"><Pencil :size="15" />编辑</el-button><el-button v-if="access.can(permission.palletDelete)" size="small" plain type="danger" :loading="deletingId === (row as PalletListItem).id" @click="remove(row)"><Trash2 :size="15" />删除</el-button></template>
|
||||
<template #pagination-left>共 {{ total }} 条托盘</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<ContractPendingFormDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增托盘' : '编辑托盘'" width="min(760px, calc(100vw - 28px))" domain="托盘">
|
||||
<AppDialog v-model="dialogOpen" :title="dialogMode === 'create' ? '新增托盘' : '编辑托盘'" width="min(760px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="saving" @confirm="save">
|
||||
<el-form label-position="top"><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="托盘编码" required><el-input v-model="form.code" :readonly="dialogMode === 'edit'" placeholder="请输入托盘编码" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="托盘类型" required><el-select v-model="form.type"><el-option v-for="value in ['木托盘', '塑料托盘', '金属托盘', '纸托盘', '免熏蒸']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="容积(m³)" required><el-input-number v-model="form.volume" :min="0.01" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="托盘编码" required><el-input v-model="form.code" :readonly="dialogMode === 'edit'" maxlength="128" placeholder="请输入托盘编码" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="托盘类型" required><el-select v-model="form.type"><el-option v-for="value in ['WOOD', 'PLASTIC', 'METAL', 'PAPER', 'FUMIGATION_FREE']" :key="value" :label="palletTypeLabel(value)" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="容积(m³)" required><el-input-number v-model="form.volume" :min="0.0001" :precision="4" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="采购价格(USD)" required><el-input-number v-model="form.purchasePrice" :min="0" :precision="2" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="仓库" required><el-select v-model="form.warehouse" placeholder="海外仓候选接口待接入" disabled /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="所属 FBA 仓" required><el-select v-model="form.fbaWarehouse" placeholder="FBA仓候选接口待接入" disabled /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="状态" required><el-select v-model="form.status"><el-option label="闲置" value="闲置" /><el-option label="占用" value="占用" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="仓库"><el-input :model-value="warehouseName(form.warehouseId)" readonly /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="所属 FBA 仓"><el-input model-value="—" readonly placeholder="当前托盘不绑定 FBA 仓" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="状态" required><el-select v-model="form.status"><el-option label="闲置" value="IDLE" /><el-option label="占用" value="OCCUPIED" /><el-option label="已报废" value="SCRAPPED" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" /></el-form-item></el-col>
|
||||
</el-row></el-form>
|
||||
</ContractPendingFormDialog>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user