优化
This commit is contained in:
@@ -4,6 +4,8 @@ import type {
|
||||
TruckAppointmentPage,
|
||||
TruckAppointmentQuery,
|
||||
TruckAppointmentResource,
|
||||
TruckAppointmentStatusHistory,
|
||||
TruckAttachmentReference,
|
||||
} from '../types'
|
||||
|
||||
const basePath = '/platform/v1/truck-appointments'
|
||||
@@ -52,6 +54,27 @@ export async function getTruckAppointment(id: string | number) {
|
||||
return normalizeResource(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listTruckAppointmentHistory(id: string | number) {
|
||||
const response = await http.get<ApiEnvelope<TruckAppointmentStatusHistory[] | { records?: TruckAppointmentStatusHistory[] }>, ApiEnvelope<TruckAppointmentStatusHistory[] | { records?: TruckAppointmentStatusHistory[] }>>(`${basePath}/${id}/history`)
|
||||
const payload = unwrapApiResponse(response)
|
||||
const records = Array.isArray(payload) ? payload : payload?.records || []
|
||||
return records.map((item) => ({ ...item, id: item.id == null ? item.id : String(item.id), operatorId: item.operatorId == null ? item.operatorId : String(item.operatorId) }))
|
||||
}
|
||||
|
||||
export async function listTruckAppointmentAttachments(id: string | number) {
|
||||
const response = await http.get<ApiEnvelope<TruckAttachmentReference[]>, ApiEnvelope<TruckAttachmentReference[]>>(`${basePath}/${id}/attachments`)
|
||||
return unwrapApiResponse(response).map((item) => ({ ...item, id: item.id == null ? item.id : String(item.id), fileId: String(item.fileId), uploadedBy: item.uploadedBy == null ? item.uploadedBy : String(item.uploadedBy) }))
|
||||
}
|
||||
|
||||
export async function referenceTruckAppointmentAttachment(id: string | number, payload: { fileId: string; attachmentType?: string | null; fileName?: string | null; fileHash?: string | null }) {
|
||||
const response = await http.post<ApiEnvelope<TruckAttachmentReference>, ApiEnvelope<TruckAttachmentReference>>(`${basePath}/${id}/attachments`, { ...payload, fileId: Number(payload.fileId) })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function deleteTruckAppointmentAttachment(id: string | number, attachmentId: string | number) {
|
||||
await http.delete<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${basePath}/${id}/attachments/${attachmentId}`)
|
||||
}
|
||||
|
||||
export async function createTruckAppointment(payload: SaveTruckAppointmentRequest) {
|
||||
const response = await http.post<ApiEnvelope<TruckAppointmentResource>, ApiEnvelope<TruckAppointmentResource>>(basePath, dtoPayload(payload))
|
||||
return normalizeResource(unwrapApiResponse(response))
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { deleteTruckAppointmentAttachment, referenceTruckAppointmentAttachment } from '../api/platformTrucking'
|
||||
import type { TruckAttachmentReference } from '../types'
|
||||
const props = defineProps<{ modelValue: boolean; attachments: TruckAttachmentReference[] }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [boolean] }>()
|
||||
const props = defineProps<{ modelValue: boolean; appointmentId?: string; attachments: TruckAttachmentReference[]; canManage?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [boolean]; changed: [] }>()
|
||||
const fileId = ref(''); const fileName = ref(''); const busy = ref(false)
|
||||
async function add() { if (!props.appointmentId || !/^\d+$/.test(fileId.value)) return; busy.value = true; try { await referenceTruckAppointmentAttachment(props.appointmentId, { fileId: fileId.value, fileName: fileName.value || null }); fileId.value = ''; fileName.value = ''; emit('changed'); ElMessage.success('附件引用已添加') } catch { ElMessage.error('附件引用失败') } finally { busy.value = false } }
|
||||
async function remove(item: TruckAttachmentReference) { if (!props.appointmentId || !item.id) return; await ElMessageBox.confirm('确认删除该附件引用?', '删除确认', { type: 'warning' }); busy.value = true; try { await deleteTruckAppointmentAttachment(props.appointmentId, item.id); emit('changed'); ElMessage.success('附件引用已删除') } catch { ElMessage.error('删除失败') } finally { busy.value = false } }
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog :model-value="props.modelValue" title="附件引用" width="min(640px, 94vw)" @update:model-value="emit('update:modelValue', $event)">
|
||||
<el-alert type="info" :closable="false" title="仅展示后端返回的真实文件引用;当前接口未提供上传或删除动作。" />
|
||||
<el-alert type="info" :closable="false" title="仅允许引用文件服务中属于当前预约和客户范围的文件,后端会校验归属与业务类型。" />
|
||||
<el-form v-if="props.appointmentId && props.canManage" inline @submit.prevent="add"><el-form-item label="文件 ID"><el-input v-model="fileId" placeholder="输入文件服务 ID" /></el-form-item><el-form-item label="文件名"><el-input v-model="fileName" placeholder="可选" /></el-form-item><el-button type="primary" :loading="busy" @click="add">添加引用</el-button></el-form>
|
||||
<el-empty v-if="attachments.length === 0" description="暂无真实附件引用" :image-size="72" />
|
||||
<div v-for="item in attachments" :key="item.fileId" class="attachment-row">
|
||||
<div><strong>{{ item.fileName }}</strong><small>文件 ID:{{ item.fileId }}</small></div>
|
||||
<div><strong>{{ item.fileName || '未命名文件' }}</strong><small>文件 ID:{{ item.fileId }} · {{ item.attachmentType || '业务附件' }}</small></div>
|
||||
<code v-if="item.objectKey">{{ item.objectKey }}</code>
|
||||
<el-button v-if="props.appointmentId && props.canManage && item.id" text type="danger" :loading="busy" @click="remove(item)">删除</el-button>
|
||||
</div>
|
||||
<template #footer><el-button type="primary" @click="emit('update:modelValue', false)">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -7,13 +7,15 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getTruckAppointment, performTruckAppointmentAction, rescheduleTruckAppointment, updateTruckAppointment, type TruckAppointmentAction } from '../api/platformTrucking'
|
||||
import { getTruckAppointment, listTruckAppointmentAttachments, listTruckAppointmentHistory, performTruckAppointmentAction, rescheduleTruckAppointment, updateTruckAppointment, type TruckAppointmentAction } from '../api/platformTrucking'
|
||||
import TruckAppointmentForm from '../components/TruckAppointmentForm.vue'
|
||||
import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue'
|
||||
import { truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus } from '../types'
|
||||
import { truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAppointmentStatusHistory } from '../types'
|
||||
const route = useRoute(); const router = useRouter(); const open = ref(true); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref<TruckAppointmentResource | null>(null); const error = ref<ApiErrorDetails | null>(null); const form = reactive<SaveTruckAppointmentRequest>({ expectedVersion: 0 })
|
||||
const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && !['COMPLETED','CANCELLED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate'))
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0 }) } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } }
|
||||
const history = ref<TruckAppointmentStatusHistory[]>([])
|
||||
const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && !['COMPLETED','CANCELLED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate')); const canManageAttachments = computed(() => access.can('platform:firstleg:truck-appointment:edit'))
|
||||
async function loadRelated() { if (!resource.value) return; const [attachments, histories] = await Promise.all([listTruckAppointmentAttachments(id.value), listTruckAppointmentHistory(id.value)]); resource.value.attachments = attachments; history.value = histories }
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0 }); await loadRelated() } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } }
|
||||
async function submit() { saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } }
|
||||
const actionByTarget: Partial<Record<TruckAppointmentStatus, TruckAppointmentAction>> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' }
|
||||
const primaryTargetByStatus: Partial<Record<TruckAppointmentStatus, TruckAppointmentStatus>> = { DRAFT: 'PENDING_DISPATCH', PENDING_DISPATCH: 'DISPATCHED', RESCHEDULED: 'DISPATCHED', DISPATCHED: 'PICKUP_IN_PROGRESS', PICKUP_IN_PROGRESS: 'ARRIVED_WAREHOUSE', ARRIVED_WAREHOUSE: 'LOADING', LOADING: 'DEPARTED', DEPARTED: 'COMPLETED' }
|
||||
@@ -28,10 +30,12 @@ onMounted(load)
|
||||
<template>
|
||||
<AppDrawer v-model="open" :title="resource?.appointmentNo ? `拖车预约 ${resource.appointmentNo}` : '拖车预约详情'" :loading="loading" :show-footer="false" @cancel="router.push({ name: 'trucking' })">
|
||||
<LoadErrorState v-if="error && !resource" title="详情加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-else-if="resource"><div class="status-bar"><span>当前状态:<strong>{{ truckAppointmentStatusLabels[resource.status] }}</strong></span><div v-if="canChangeStatus" class="status-actions"><el-button v-for="status in availableTargets" :key="status" :type="status === 'CANCELLED' ? 'danger' : status === 'EXCEPTION' ? 'warning' : 'primary'" plain :loading="statusSaving" @click="changeStatus(status)">{{ truckAppointmentStatusLabels[status] }}</el-button></div></div><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><TruckAttachmentDialog v-model="attachmentOpen" :attachments="resource.attachments || []" /></template>
|
||||
<template v-else-if="resource"><div class="status-bar"><span>当前状态:<strong>{{ truckAppointmentStatusLabels[resource.status] }}</strong></span><div v-if="canChangeStatus" class="status-actions"><el-button v-for="status in availableTargets" :key="status" :type="status === 'CANCELLED' ? 'danger' : status === 'EXCEPTION' ? 'warning' : 'primary'" plain :loading="statusSaving" @click="changeStatus(status)">{{ truckAppointmentStatusLabels[status] }}</el-button></div></div><ApiErrorMeta v-if="error" :details="error" /><TruckAppointmentForm v-model="form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" /><section class="history-section"><h3>状态历史</h3><el-empty v-if="!history.length" description="暂无状态历史" :image-size="48" /><ol v-else><li v-for="(item, index) in history" :key="item.id || `${item.createdAt}-${index}`"><strong>{{ item.action }}</strong><span>{{ item.fromStatus || '—' }} → {{ item.toStatus || '—' }}</span><small>{{ item.operatorName || item.operatorId || '系统' }} · {{ item.createdAt || item.occurredAt || '—' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></section><TruckAttachmentDialog v-model="attachmentOpen" :appointment-id="id" :attachments="resource.attachments || []" :can-manage="canManageAttachments" @changed="loadRelated" /></template>
|
||||
</AppDrawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.status-bar { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:16px; padding:12px; background:var(--el-fill-color-light); border-radius:6px; }
|
||||
.status-actions { display:flex; flex-wrap:wrap; gap:8px; }.status-actions :deep(.el-button) { margin-left:0; }
|
||||
.history-section { margin-top:20px; padding-top:16px; border-top:1px solid var(--cc-border); }.history-section h3 { margin:0 0 12px; font-size:15px; }.history-section ol { display:grid; gap:12px; margin:0; padding:0; list-style:none; }.history-section li { display:grid; grid-template-columns:120px minmax(120px, 1fr) minmax(200px, 2fr); gap:12px; padding:10px 12px; border-radius:6px; background:var(--el-fill-color-lighter); }.history-section small { color:var(--cc-muted); overflow-wrap:anywhere; }
|
||||
@media (max-width: 640px) { .history-section li { grid-template-columns:1fr; gap:4px; } }
|
||||
</style>
|
||||
|
||||
@@ -29,11 +29,31 @@ export const truckAppointmentStatusLabels: Record<TruckAppointmentStatus, string
|
||||
}
|
||||
|
||||
export interface TruckAttachmentReference {
|
||||
id?: string
|
||||
fileId: string
|
||||
fileName: string
|
||||
attachmentType?: string | null
|
||||
fileHash?: string | null
|
||||
visibility?: string | null
|
||||
uploadedBy?: string | null
|
||||
uploadedAt?: string | null
|
||||
objectKey?: string | null
|
||||
}
|
||||
|
||||
export interface TruckAppointmentStatusHistory {
|
||||
id?: string
|
||||
revision?: number | null
|
||||
action: string
|
||||
fromStatus?: TruckAppointmentStatus | string | null
|
||||
toStatus: TruckAppointmentStatus | string | null
|
||||
reason?: string | null
|
||||
operatorId?: string | null
|
||||
operatorName?: string | null
|
||||
requestId?: string | null
|
||||
createdAt?: string | null
|
||||
occurredAt?: string | null
|
||||
}
|
||||
|
||||
export interface TruckAppointmentResource {
|
||||
id: string
|
||||
appointmentNo?: string | null
|
||||
@@ -79,6 +99,7 @@ export interface TruckAppointmentResource {
|
||||
relatedContainerType?: string | null
|
||||
entrustingPartyName?: string | null
|
||||
attachments?: TruckAttachmentReference[] | null
|
||||
statusHistory?: TruckAppointmentStatusHistory[] | null
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user