Files
admin-web/src/modules/loading-lists/components/LoadingListsView.vue
T
2026-09-30 14:01:58 +08:00

248 lines
12 KiB
Vue

<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ContainerSimulationCreate } from '@/modules/container-loading'
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 DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { loadingListStatusLabel } from '../status'
import {
listConfirmedLoadingLists,
listPreloadPlans,
type ConfirmedSummary,
type LoadingListMode,
type LoadingListQuery,
type LoadingListSummary,
type PreloadSummary,
} from '../api/platformLoadingLists'
const props = defineProps<{ mode: LoadingListMode }>()
const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const rows = ref<LoadingListSummary[]>([])
const loading = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const query = reactive<QueryModel>({ customerId: '', keyword: '', createdRange: [] })
const handledDeepLink = ref('')
const isPreload = computed(() => props.mode === 'preload')
const title = computed(() => isPreload.value ? '预装清单' : '装柜清单')
const listPermission = computed(() => isPreload.value ? 'platform:firstleg:preload:list' : 'platform:firstleg:loading-list:list')
const canList = computed(() => access.can(listPermission.value))
const canCreate = computed(() => isPreload.value && canList.value && access.can('platform:firstleg:container:create'))
const canView = computed(() => canList.value)
const fields = computed<QueryField[]>(() => [
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
{ key: 'keyword', type: 'input', label: '关键词', placeholder: isPreload.value ? '预装单号/柜次号/集装箱号' : '装柜清单号/柜次号/集装箱号', width: 270 },
{ key: 'createdRange', type: 'daterange', label: '创建日期', width: 260 },
])
const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
{ key: 'actions', label: '操作', fixed: 'left', width: 88, slotName: 'actions', hideable: false, movable: false },
{ key: 'preloadNo', prop: 'preloadNo', label: '预装单号', minWidth: 120 },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 120 },
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 140 },
// { key: 'customers', label: '客户', minWidth: 190, slotName: 'customers' },
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 80 },
{ key: 'vesselVoyage', label: '船名 / 航次', minWidth: 175, slotName: 'vesselVoyage' },
{ key: 'containerType', prop: 'containerType', label: '箱型', width: 80 },
{ key: 'status', label: '状态', width: 108, slotName: 'status' },
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' },
{ key: 'skuCount', prop: 'skuCount', label: 'SKU 数', width: 90, align: 'right' },
{ key: 'totalVolumeCbm', label: '总体积(m³)', width: 90, align: 'right', slotName: 'volume' },
{ key: 'totalGrossWeightKg', label: '总毛重(kg)', width: 90, align: 'right', slotName: 'grossWeight' },
{ key: 'createdAt', label: '创建时间', minWidth: 140, slotName: 'createdAt' },
] : [
{ key: 'actions', label: '操作', fixed: 'left', width: 110, slotName: 'actions', hideable: false, movable: false },
{ key: 'loadingListNo', prop: 'loadingListNo', label: '装柜清单号', minWidth: 175 },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145 },
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 150 },
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 140 },
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' },
{ key: 'totalGrossWeightKg', label: '总毛重(kg)', width: 125, align: 'right', slotName: 'grossWeight' },
{ key: 'totalNetWeightKg', label: '总净重(kg)', width: 125, align: 'right', slotName: 'netWeight' },
{ key: 'totalVolumeCbm', label: '总体积(m³)', width: 120, align: 'right', slotName: 'volume' },
{ key: 'totalQuantity', prop: 'totalQuantity', label: '产品总数', width: 105, align: 'right' },
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
{ key: 'createdByName', prop: 'createdByName', label: '创建人', minWidth: 120 },
{ key: 'createdAt', label: '创建时间', minWidth: 155, slotName: 'createdAt' },
])
function formatDateTime(value?: string | null) {
return value ? value.replace('T', ' ').slice(0, 16) : '—'
}
function formatNumber(value?: number | null, digits = 3) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '—'
return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: digits })
}
function queryId(value: unknown) {
return typeof value === 'string' && /^[1-9]\d*$/.test(value) ? value : ''
}
function toQuery(): LoadingListQuery {
const range = Array.isArray(query.createdRange) ? query.createdRange.map(String) : []
return {
page: page.value,
size: pageSize.value,
customerId: String(query.customerId || '').trim() || undefined,
keyword: String(query.keyword || '').trim() || undefined,
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
createdTo: range[1] ? `${range[1]}T23:59:59` : undefined,
}
}
async function load() {
if (!canList.value) return
const request = toQuery()
loading.value = true
error.value = null
try {
const result = isPreload.value ? await listPreloadPlans(request) : await listConfirmedLoadingLists(request)
rows.value = result.records
total.value = result.total
await openDeepLinkedDetail()
} catch (cause) {
error.value = getApiErrorDetails(cause, `${title.value}加载失败,请稍后重试`)
} finally {
loading.value = false
}
}
async function openDeepLinkedDetail() {
const planId = queryId(route.query.planId)
const warehouseId = queryId(route.query.warehouseId)
if (!planId || !warehouseId || !canView.value) return
const key = `${props.mode}:${planId}:${warehouseId}`
if (handledDeepLink.value === key) return
handledDeepLink.value = key
if (isPreload.value) {
await router.replace({ name: 'preload-plan-detail', params: { planId }, query: { warehouseId } })
return
}
await router.replace({ name: 'loading-list-detail', params: { planId }, query: { warehouseId } })
}
function onSimulationCreated() {
Object.assign(query, { customerId: '', keyword: '', createdRange: [] })
search()
}
function search() {
if (page.value !== 1) page.value = 1
else void load()
}
function openDetail(rowValue: object) {
const row = rowValue as LoadingListSummary
if (isPreload.value) {
void router.push({ name: 'preload-plan-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
return
}
void router.push({ name: 'loading-list-detail', params: { planId: String(row.planId) }, query: { warehouseId: String(row.warehouseId || '') } })
}
async function loadCustomers() {
customersLoading.value = true
try {
const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })
customers.value = result.records.filter((customer) => customer.status === 'ACTIVE')
} catch {
customers.value = []
} finally {
customersLoading.value = false
}
}
watch([page, pageSize], () => void load())
watch(() => [route.query.planId, route.query.warehouseId], () => void openDeepLinkedDetail())
onMounted(() => {
void load()
void loadCustomers()
})
</script>
<template>
<AdminPage>
<div class="business-page loading-lists-page admin-density">
<ListPageLayout :ariaLabel="title">
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="canList && !error"
v-model:page="page"
v-model:page-size="pageSize"
:data="rows"
:columns="columns"
:loading="loading"
:total="total"
:preference-key="`loading-lists-${mode}`"
row-key="planId"
:empty-text="`暂无符合筛选条件的${title}`"
:aria-label="title"
>
<template #toolbar-start><h1>{{ title }}</h1></template>
<template #filters>
<QueryToolbar
v-model="query"
:ariaLabel="`${title}筛选`"
:fields="fields"
:available-fields="fields"
:preference-key="`loading-lists-${mode}-query`"
@search="search"
/>
</template>
<template #notices><template v-if="!canList || error">
<LoadErrorState v-if="!canList" :title="`无权查看${title}`" message="当前账号缺少该清单的列表权限。" :show-retry="false" />
<LoadErrorState v-else-if="error" :title="`${title}加载失败`" :message="error.message" :loading="loading" @retry="load">
<template #details><ApiErrorMeta :details="error" /></template>
</LoadErrorState>
</template></template>
<template #toolbar-actions>
<ContainerSimulationCreate :dense="isPreload" v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
</template>
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions" :class="{ 'density-actions': isPreload }">
<el-button v-if="canView" :class="{ 'density-action': isPreload }" size="small" :type="isPreload ? undefined : 'primary'" :plain="!isPreload" @click="openDetail(row)">查看</el-button>
</div></template>
<template #customers="{ row }"><LineClampText :content="(row as PreloadSummary).customers.map((item) => item.customerName || '客户信息缺失').join('、')" /></template>
<template #vesselVoyage="{ row }">{{ (row as PreloadSummary).vesselName || '—' }} / {{ (row as PreloadSummary).voyageNo || '—' }}</template>
<template #status="{ row }"><StatusBadge :status="loadingListStatusLabel(row.status)" /></template>
<template #grossWeight="{ row }">{{ formatNumber(row.totalGrossWeightKg) }}</template>
<template #netWeight="{ row }">{{ formatNumber((row as ConfirmedSummary).totalNetWeightKg) }}</template>
<template #volume="{ row }">{{ formatNumber(row.totalVolumeCbm) }}</template>
<template #createdAt="{ row }">{{ formatDateTime(row.createdAt) }}</template>
</DataTable></section>
</ListPageLayout>
</div>
</AdminPage>
</template>
<style scoped>
.loading-lists-page { display: grid; min-width: 0; gap: var(--admin-space-6, 16px); }
.table-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.row-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-2, 6px); }
.row-actions :deep(.el-button) { margin-left: 0; }
@media (max-width: 720px) { .row-actions { gap: 4px; } }
</style>