|
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
|
|
|
import { Eye, Pencil, Plus } from '@lucide/vue'
|
|
|
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
|
|
|
|
import DataTable from '@/shared/components/DataTable.vue'
|
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
|
|
|
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
import { createPortSchedule, getPortSchedule, listPortSchedules, updatePortSchedule } from '../api/platformPortSchedules'
|
|
|
|
|
import type { PortScheduleData, PortScheduleView } from '../types'
|
|
|
|
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
const rows = ref<PortScheduleView[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
|
|
|
|
const query = reactive<QueryModel>({ keyword: '', expired: '' })
|
|
|
|
|
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '船期号/船名/航次/港口', width: 230 }, { key: 'expired', type: 'select', label: '有效性', options: [{ label: '有效', value: 'false' }, { label: '已过期', value: 'true' }] }]
|
|
|
|
|
const columns = [
|
|
|
|
|
{ key: 'actions', label: '操作', fixed: 'left', width: 165, slotName: 'actions', hideable: false, movable: false },
|
|
|
|
|
{ prop: 'schedule.scheduleNo', label: '船期编号', minWidth: 145 }, { prop: 'schedule.sourceName', label: '信息来源', minWidth: 130 },
|
|
|
|
|
{ prop: 'schedule.vesselName', label: '船名/航次', minWidth: 155, slotName: 'vessel' }, { prop: 'schedule.departurePortName', label: '起运港', minWidth: 120 },
|
|
|
|
|
{ prop: 'schedule.destinationPortName', label: '目的港', minWidth: 120 }, { prop: 'schedule.etdAt', label: '预计开船', minWidth: 160 },
|
|
|
|
|
{ prop: 'schedule.observedAt', label: '查询时间', minWidth: 160 }, { prop: 'expired', label: '有效性', width: 95, slotName: 'expired' },
|
|
|
|
|
]
|
|
|
|
|
function blank(): PortScheduleData { const now = new Date().toISOString().slice(0, 19); return { customerId: '', oceanBookingId: '', containerBookingId: null, sourceType: 'MANUAL', sourceName: '', sourceRef: '', vesselName: '', voyageNo: '', departurePortCode: '', departurePortName: '', destinationPortCode: '', destinationPortName: '', etdAt: null, etaAt: null, bookingCutoffAt: null, customsCutoffAt: null, portCutoffAt: null, observedAt: now, expiresAt: now, remark: '' } }
|
|
|
|
|
const dialogOpen = ref(false); const saving = ref(false); const editingId = ref(''); const expectedVersion = ref(0); const formRef = ref<FormInstance>(); const form = reactive<PortScheduleData>(blank()); const actionError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
const rules: FormRules = { customerId: [{ required: true, message: '请输入客户 ID', trigger: 'blur' }], oceanBookingId: [{ required: true, message: '请输入订舱 ID', trigger: 'blur' }], sourceName: [{ required: true, message: '请输入信息来源', trigger: 'blur' }], sourceRef: [{ required: true, message: '请输入来源链接或参考号', trigger: 'blur' }], vesselName: [{ required: true, message: '请输入船名', trigger: 'blur' }], voyageNo: [{ required: true, message: '请输入航次', trigger: 'blur' }], departurePortCode: [{ required: true, message: '请输入起运港代码', trigger: 'blur' }], destinationPortCode: [{ required: true, message: '请输入目的港代码', trigger: 'blur' }], observedAt: [{ required: true, message: '请选择查询时间', trigger: 'change' }], expiresAt: [{ required: true, message: '请选择过期时间', trigger: 'change' }] }
|
|
|
|
|
const canCreate = computed(() => access.can('platform:firstleg:port-schedule:create'))
|
|
|
|
|
async function load() { loading.value = true; error.value = null; try { const result = await listPortSchedules({ page: page.value, size: pageSize.value, keyword: String(query.keyword || '') || undefined, expired: query.expired === '' ? undefined : query.expired === 'true' }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
|
|
|
|
|
function search() { if (page.value === 1) void load(); else page.value = 1 }
|
|
|
|
|
function openCreate() { editingId.value = ''; expectedVersion.value = 0; Object.assign(form, blank()); actionError.value = null; dialogOpen.value = true }
|
|
|
|
|
async function openEdit(id: string) { actionError.value = null; try { const result = await getPortSchedule(id); editingId.value = id; expectedVersion.value = result.portSchedule.schedule.version; Object.assign(form, result.portSchedule.schedule); dialogOpen.value = true } catch (cause) { error.value = getApiErrorDetails(cause) } }
|
|
|
|
|
async function save() { if (!await formRef.value?.validate().catch(() => false)) return; saving.value = true; actionError.value = null; try { if (editingId.value) await updatePortSchedule(editingId.value, { expectedVersion: expectedVersion.value, data: { ...form }, reason: '后台维护船期' }); else await createPortSchedule({ ...form }); ElMessage.success(editingId.value ? '船期已更新' : '船期已新增'); dialogOpen.value = false; await load() } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409' && editingId.value) await openEdit(editingId.value) } finally { saving.value = false } }
|
|
|
|
|
watch([page, pageSize], () => void load()); onMounted(load)
|
|
|
|
|
</script>
|
|
|
|
|
<template>
|
|
|
|
|
<AdminPage as="div"><ListPageLayout aria-label="船期列表">
|
|
|
|
|
<template #toolbar><QueryToolbar v-model="query" class="surface-panel" ariaLabel="船期筛选" :fields="fields" @search="search" @reset="search" /></template>
|
|
|
|
|
<template v-if="error" #state><LoadErrorState title="船期列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template>
|
|
|
|
|
<section v-if="!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="schedule.id" preference-key="port-schedules" empty-text="暂无符合条件的船期">
|
|
|
|
|
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增船期</el-button></template>
|
|
|
|
|
<template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:port-schedule:view')" link type="primary" @click="openEdit(row.schedule.id)"><Eye :size="15" />查看</el-button><el-button v-if="access.can('platform:firstleg:port-schedule:edit')" link @click="openEdit(row.schedule.id)"><Pencil :size="15" />编辑</el-button></template>
|
|
|
|
|
<template #vessel="{ row }">{{ row.schedule.vesselName }} / {{ row.schedule.voyageNo }}</template><template #expired="{ row }"><el-tag :type="row.expired ? 'danger' : 'success'">{{ row.expired ? '已过期' : '有效' }}</el-tag></template>
|
|
|
|
|
</DataTable></section>
|
|
|
|
|
</ListPageLayout>
|
|
|
|
|
<AppDialog v-model="dialogOpen" :title="editingId ? '编辑船期' : '新增船期'" width="820px" :confirm-loading="saving" @confirm="save"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="schedule-form"><el-row :gutter="16">
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="客户 ID" prop="customerId"><el-input v-model="form.customerId" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="订舱 ID" prop="oceanBookingId"><el-input v-model="form.oceanBookingId" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="信息来源" prop="sourceName"><el-input v-model="form.sourceName" placeholder="如美森官网" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="来源参考" prop="sourceRef"><el-input v-model="form.sourceRef" placeholder="查询链接或参考号" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="船名" prop="vesselName"><el-input v-model="form.vesselName" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="航次" prop="voyageNo"><el-input v-model="form.voyageNo" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="起运港代码" prop="departurePortCode"><el-input v-model="form.departurePortCode" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="目的港代码" prop="destinationPortCode"><el-input v-model="form.destinationPortCode" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="预计开船"><el-date-picker v-model="form.etdAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="预计到港"><el-date-picker v-model="form.etaAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24" :sm="12"><el-form-item label="查询时间" prop="observedAt"><el-date-picker v-model="form.observedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="过期时间" prop="expiresAt"><el-date-picker v-model="form.expiresAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col>
|
|
|
|
|
<el-col :xs="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item></el-col>
|
|
|
|
|
</el-row></el-form></AppDialog></AdminPage>
|
|
|
|
|
</template>
|
|
|
|
|
<style scoped>.schedule-form{max-height:65vh;overflow:auto;padding-inline:2px}</style>
|