2026-08-06 13:56:04 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
2026-08-11 18:06:35 +08:00
|
|
|
|
import { ElMessageBox } from 'element-plus'
|
2026-08-11 11:15:44 +08:00
|
|
|
|
import { RefreshCw, Save, Check, Play, Camera, Eye, Plus, Pencil, Trash2 } from '@lucide/vue'
|
2026-08-06 13:56:04 +08:00
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
2026-08-10 15:36:53 +08:00
|
|
|
|
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
|
|
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import SmartLoadingWorkbench from '@/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue'
|
2026-08-11 18:43:56 +08:00
|
|
|
|
import { oceanBookingStatusLabel } from '@/modules/ocean-bookings'
|
|
|
|
|
|
import { truckAppointmentStatusLabel } from '@/modules/trucking'
|
2026-08-10 15:36:53 +08:00
|
|
|
|
import { sanitizePreloadItem, validatePreloadItem, validatePreloadItems } from '@/modules/unified-orders/workflows/preloadForm'
|
2026-08-06 13:56:04 +08:00
|
|
|
|
import {
|
|
|
|
|
|
completeLoading,
|
|
|
|
|
|
completePicking,
|
|
|
|
|
|
confirmingPreload,
|
|
|
|
|
|
confirmPreload,
|
2026-08-11 11:15:44 +08:00
|
|
|
|
downloadEvidenceFile,
|
2026-08-06 13:56:04 +08:00
|
|
|
|
getEvidence,
|
2026-08-10 12:05:20 +08:00
|
|
|
|
getOutboundDispatch,
|
2026-08-06 13:56:04 +08:00
|
|
|
|
getPicking,
|
|
|
|
|
|
getPreload,
|
|
|
|
|
|
smartSplit,
|
|
|
|
|
|
startPicking,
|
|
|
|
|
|
upsertEvidence,
|
2026-08-10 12:05:20 +08:00
|
|
|
|
uploadEvidenceFile,
|
|
|
|
|
|
voidEvidence,
|
2026-08-06 13:56:04 +08:00
|
|
|
|
type Evidence,
|
|
|
|
|
|
type EvidenceType,
|
|
|
|
|
|
type PickingTask,
|
|
|
|
|
|
type PreloadItem,
|
|
|
|
|
|
type PreloadPlan,
|
2026-08-10 12:05:20 +08:00
|
|
|
|
type OutboundDispatchAggregate,
|
2026-08-06 13:56:04 +08:00
|
|
|
|
} from '@/modules/unified-orders/workflows/api'
|
|
|
|
|
|
import type { UnifiedOrderMainStatus } from '@/modules/unified-orders/api/unifiedOrders'
|
|
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<{
|
|
|
|
|
|
tenantId: string
|
|
|
|
|
|
inboundId: string | number
|
2026-08-10 12:05:20 +08:00
|
|
|
|
orderId?: string | number | null
|
2026-08-06 13:56:04 +08:00
|
|
|
|
mainStatus: UnifiedOrderMainStatus
|
|
|
|
|
|
inboundVersion?: number | null
|
|
|
|
|
|
pickingVersion?: number | null
|
2026-08-10 09:12:06 +08:00
|
|
|
|
readOnly?: boolean
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}>(), {
|
|
|
|
|
|
inboundVersion: null,
|
|
|
|
|
|
pickingVersion: null,
|
2026-08-10 09:12:06 +08:00
|
|
|
|
readOnly: false,
|
2026-08-06 13:56:04 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{ updated: [] }>()
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
|
const preload = ref<PreloadPlan | null>(null)
|
|
|
|
|
|
const preloadItems = ref<PreloadItem[]>([])
|
|
|
|
|
|
const picking = ref<PickingTask | null>(null)
|
|
|
|
|
|
const evidences = ref<Evidence[]>([])
|
2026-08-10 12:05:20 +08:00
|
|
|
|
const dispatch = ref<OutboundDispatchAggregate | null>(null)
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const loadingRemark = ref('')
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const sealNoDraft = ref('')
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const splitRemark = ref('')
|
|
|
|
|
|
const busy = ref('')
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const error = ref('')
|
2026-08-10 15:36:53 +08:00
|
|
|
|
const errorDetails = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const preloadLoadError = ref<ApiErrorDetails | null>(null)
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const notice = ref('')
|
2026-08-10 15:36:53 +08:00
|
|
|
|
const preloadDialogOpen = ref(false)
|
|
|
|
|
|
const preloadDialogMode = ref<'create' | 'edit'>('create')
|
|
|
|
|
|
const editingPreloadIndex = ref<number | null>(null)
|
|
|
|
|
|
const preloadDraft = ref<PreloadItem>(itemTemplate())
|
|
|
|
|
|
const preloadDraftError = ref('')
|
2026-08-06 13:56:04 +08:00
|
|
|
|
|
|
|
|
|
|
const evidenceTypes: Array<{ type: EvidenceType; label: string }> = [
|
2026-08-10 12:05:20 +08:00
|
|
|
|
{ type: 'PHOTO_STEP_1', label: '空仓照' },
|
|
|
|
|
|
{ type: 'PHOTO_STEP_2', label: '半仓照' },
|
|
|
|
|
|
{ type: 'PHOTO_STEP_3', label: '满仓照' },
|
|
|
|
|
|
{ type: 'PHOTO_STEP_4', label: '锁仓照' },
|
|
|
|
|
|
{ type: 'SEAL', label: '带锁码照' },
|
2026-08-06 13:56:04 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const isSplit = computed(() => props.mainStatus === 'WAIT_SPLIT')
|
|
|
|
|
|
const isLoading = computed(() => props.mainStatus === 'LOADING')
|
|
|
|
|
|
const isLoaded = computed(() => props.mainStatus === 'LOADED')
|
|
|
|
|
|
const preloadVersion = computed(() => preload.value?.version ?? 0)
|
|
|
|
|
|
const pickingExpectedVersion = computed(() => picking.value?.version ?? props.pickingVersion ?? null)
|
|
|
|
|
|
const inboundExpectedVersion = computed(() => props.inboundVersion)
|
2026-08-10 09:12:06 +08:00
|
|
|
|
const canSplit = computed(() => !props.readOnly && access.can('firstleg:inbound:split'))
|
|
|
|
|
|
const canPreload = computed(() => !props.readOnly && access.can('firstleg:inbound:preload'))
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const canPreloadView = computed(() => access.can('firstleg:inbound:preload-view'))
|
2026-08-10 09:12:06 +08:00
|
|
|
|
const canPicking = computed(() => !props.readOnly && access.can('platform:firstleg:inbound:picking:edit'))
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const canPickingView = computed(() => access.can('platform:firstleg:inbound:picking:view'))
|
2026-08-10 12:05:20 +08:00
|
|
|
|
// 操作员、理货员由 IAM 授予 loading:edit;拣货员仅能在 PDA 现场采集,不授予该权限。
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const canEvidence = computed(() => !props.readOnly && isLoading.value && access.can('platform:firstleg:inbound:loading:edit'))
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const canEvidenceView = computed(() => access.can('platform:firstleg:inbound:loading:view'))
|
2026-08-10 12:05:20 +08:00
|
|
|
|
const canLoadingComplete = computed(() => !props.readOnly && !isLoaded.value && access.can('platform:firstleg:inbound:loading:complete'))
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const evidenceReady = computed(() => evidenceTypes.every(({ type }) => activeEvidences(type).some((item) => (
|
|
|
|
|
|
Boolean(item.fileId) && (type !== 'SEAL' || Boolean(item.sealNo?.trim()))
|
|
|
|
|
|
))))
|
2026-08-06 13:56:04 +08:00
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
|
const preloadColumns: DataTableColumn[] = [
|
|
|
|
|
|
{ key: 'actions', label: '操作', width: 132, fixed: 'left', slotName: 'actions' },
|
|
|
|
|
|
{ key: 'containerCode', prop: 'containerCode', label: '柜号', minWidth: 150 },
|
|
|
|
|
|
{ key: 'cargoReference', prop: 'cargoReference', label: '货物引用', minWidth: 180 },
|
|
|
|
|
|
{ key: 'packageCount', prop: 'packageCount', label: '箱数', width: 90 },
|
|
|
|
|
|
{ key: 'volumeCbm', label: '体积(m³)', width: 110, slotName: 'volume' },
|
|
|
|
|
|
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-08-06 13:56:04 +08:00
|
|
|
|
function itemTemplate(): PreloadItem {
|
|
|
|
|
|
return { containerCode: '', cargoReference: '', packageCount: null, volumeCbm: 0, remark: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addItem() {
|
2026-08-10 15:36:53 +08:00
|
|
|
|
preloadDialogMode.value = 'create'
|
|
|
|
|
|
editingPreloadIndex.value = null
|
|
|
|
|
|
preloadDraft.value = itemTemplate()
|
|
|
|
|
|
preloadDraftError.value = ''
|
|
|
|
|
|
preloadDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function editItem(index: number) {
|
|
|
|
|
|
preloadDialogMode.value = 'edit'
|
|
|
|
|
|
editingPreloadIndex.value = index
|
|
|
|
|
|
preloadDraft.value = { ...preloadItems.value[index] }
|
|
|
|
|
|
preloadDraftError.value = ''
|
|
|
|
|
|
preloadDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveItem() {
|
|
|
|
|
|
const validation = validatePreloadItem(preloadDraft.value)
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
|
preloadDraftError.value = validation.message
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const next = sanitizePreloadItem(preloadDraft.value)
|
|
|
|
|
|
if (preloadDialogMode.value === 'edit' && editingPreloadIndex.value !== null) {
|
|
|
|
|
|
preloadItems.value.splice(editingPreloadIndex.value, 1, next)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
preloadItems.value.push(next)
|
|
|
|
|
|
}
|
|
|
|
|
|
preloadDialogOpen.value = false
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeItem(index: number) {
|
|
|
|
|
|
preloadItems.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
|
function preloadStatusLabel(status?: string | null) {
|
2026-08-11 18:43:56 +08:00
|
|
|
|
return workflowStatusLabel(status, { PENDING: '待提交', CONFIRMING: '确认中', CONFIRMED: '已确认' }, '尚未生成')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function workflowStatusLabel(value: string | null | undefined, labels: Record<string, string>, emptyLabel: string) {
|
|
|
|
|
|
if (!value) return emptyLabel
|
|
|
|
|
|
return labels[value] || '未知状态'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pickingStatusLabel(status?: string | null) {
|
|
|
|
|
|
return workflowStatusLabel(status, {
|
|
|
|
|
|
PENDING: '待拣货', PICKING: '拣货中', IN_PROGRESS: '拣货中', AWAITING_REVIEW: '待复核',
|
|
|
|
|
|
COMPLETED: '已完成', CANCELLED: '已取消',
|
|
|
|
|
|
}, '暂无数据')
|
2026-08-10 15:36:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatVolume(value: number | null) {
|
|
|
|
|
|
return value === null || !Number.isFinite(Number(value)) ? '—' : Number(value).toFixed(3)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
|
function activeEvidences(type: EvidenceType) {
|
|
|
|
|
|
return evidences.value.filter((item) => item.evidenceType === type && item.status === 'ACTIVE')
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function idempotentMessage(action: string) {
|
|
|
|
|
|
return busy.value === action ? '正在提交,请勿重复点击' : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 12:05:20 +08:00
|
|
|
|
function gateItemName(code: string, fallback: string) {
|
|
|
|
|
|
return code === 'LOADING_EVIDENCE_COMPLETE' ? '五类装柜证据与锁码完整' : fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
|
async function run<T>(action: string, operation: () => Promise<T>, message: string, restoreAfterConflict?: () => void) {
|
2026-08-06 13:56:04 +08:00
|
|
|
|
busy.value = action
|
|
|
|
|
|
error.value = ''
|
2026-08-10 15:36:53 +08:00
|
|
|
|
errorDetails.value = null
|
2026-08-06 13:56:04 +08:00
|
|
|
|
notice.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await operation()
|
|
|
|
|
|
notice.value = message
|
|
|
|
|
|
emit('updated')
|
|
|
|
|
|
return result
|
|
|
|
|
|
} catch (cause) {
|
2026-08-10 15:36:53 +08:00
|
|
|
|
const details = getApiErrorDetails(cause, '操作失败,请重试')
|
|
|
|
|
|
error.value = details.message
|
|
|
|
|
|
errorDetails.value = details
|
|
|
|
|
|
if (details.code === 'A0409' || /版本|并发|已被其他人修改/.test(details.message)) {
|
|
|
|
|
|
await refresh()
|
|
|
|
|
|
restoreAfterConflict?.()
|
|
|
|
|
|
notice.value = '系统已刷新最新数据,请核对后重新提交'
|
|
|
|
|
|
}
|
2026-08-06 13:56:04 +08:00
|
|
|
|
return null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
busy.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPreload() {
|
|
|
|
|
|
if (!canPreloadView.value || !props.inboundId) return
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
2026-08-10 15:36:53 +08:00
|
|
|
|
errorDetails.value = null
|
|
|
|
|
|
preloadLoadError.value = null
|
2026-08-06 13:56:04 +08:00
|
|
|
|
try {
|
|
|
|
|
|
preload.value = await getPreload(props.tenantId, props.inboundId)
|
|
|
|
|
|
preloadItems.value = preload.value.items?.map((item) => ({ ...item })) || []
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
preload.value = null
|
|
|
|
|
|
preloadItems.value = []
|
|
|
|
|
|
const missing = cause instanceof ApiResponseError
|
|
|
|
|
|
&& (cause.status === 404 || cause.code === 'A0404' || cause.code === 'NOT_FOUND')
|
2026-08-10 15:36:53 +08:00
|
|
|
|
if (!missing) {
|
|
|
|
|
|
preloadLoadError.value = getApiErrorDetails(cause, '预装清单加载失败,请重试')
|
|
|
|
|
|
}
|
2026-08-06 13:56:04 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLoading() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
2026-08-10 15:36:53 +08:00
|
|
|
|
errorDetails.value = null
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const messages: string[] = []
|
|
|
|
|
|
if (canPickingView.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
picking.value = await getPicking(props.tenantId, props.inboundId)
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
picking.value = null
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const details = getApiErrorDetails(cause, '拣货任务加载失败')
|
|
|
|
|
|
messages.push(details.message)
|
|
|
|
|
|
errorDetails.value ||= details
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (canEvidenceView.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
evidences.value = (await getEvidence(props.tenantId, props.inboundId)).evidences || []
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
evidences.value = []
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const details = getApiErrorDetails(cause, '装柜证据加载失败')
|
|
|
|
|
|
messages.push(details.message)
|
|
|
|
|
|
errorDetails.value ||= details
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-10 15:36:53 +08:00
|
|
|
|
error.value = messages.join(';')
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDispatch() {
|
2026-08-10 12:05:20 +08:00
|
|
|
|
try {
|
|
|
|
|
|
dispatch.value = await getOutboundDispatch(props.tenantId, props.orderId || undefined, props.inboundId)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
dispatch.value = null
|
|
|
|
|
|
}
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refresh() {
|
2026-08-10 15:36:53 +08:00
|
|
|
|
await loadDispatch()
|
2026-08-06 13:56:04 +08:00
|
|
|
|
if (isSplit.value) await loadPreload()
|
2026-08-11 11:15:44 +08:00
|
|
|
|
if (isLoading.value || isLoaded.value) await loadLoading()
|
2026-08-06 13:56:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runSmartSplit() {
|
2026-08-10 15:36:53 +08:00
|
|
|
|
const validation = validatePreloadItems(preloadItems.value)
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
|
error.value = validation.message
|
|
|
|
|
|
errorDetails.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const submittedItems = preloadItems.value.map((item) => sanitizePreloadItem(item))
|
|
|
|
|
|
const submittedRemark = splitRemark.value
|
2026-08-06 13:56:04 +08:00
|
|
|
|
const result = await run('smart-split', () => smartSplit(props.tenantId, props.inboundId, {
|
|
|
|
|
|
expectedVersion: preloadVersion.value,
|
2026-08-10 15:36:53 +08:00
|
|
|
|
items: submittedItems,
|
|
|
|
|
|
remark: submittedRemark.trim() || undefined,
|
|
|
|
|
|
}), '分柜清单已保存', () => {
|
|
|
|
|
|
preloadItems.value = submittedItems.map((item) => ({ ...item }))
|
|
|
|
|
|
splitRemark.value = submittedRemark
|
|
|
|
|
|
})
|
2026-08-06 13:56:04 +08:00
|
|
|
|
if (result) {
|
|
|
|
|
|
preload.value = result.preloadPlan
|
|
|
|
|
|
preloadItems.value = result.preloadPlan?.items?.map((item) => ({ ...item })) || []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function startPreloadConfirmation() {
|
|
|
|
|
|
const result = await run('preload-confirming', () => confirmingPreload(props.tenantId, props.inboundId, {
|
|
|
|
|
|
expectedVersion: preloadVersion.value,
|
|
|
|
|
|
}), '预装清单已进入确认中')
|
|
|
|
|
|
if (result?.preloadPlan) preload.value = result.preloadPlan
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function finishPreloadConfirmation() {
|
|
|
|
|
|
const result = await run('preload-confirm', () => confirmPreload(props.tenantId, props.inboundId, {
|
|
|
|
|
|
expectedVersion: preloadVersion.value,
|
|
|
|
|
|
}), '预装清单已确认,订单进入装柜中')
|
|
|
|
|
|
if (result?.preloadPlan) preload.value = result.preloadPlan
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pickingRequest(): { expectedVersion: number; expectedCount: number; pickedCount: number; remark?: string } | null {
|
|
|
|
|
|
if (pickingExpectedVersion.value === null) {
|
|
|
|
|
|
error.value = '拣货版本缺失,请刷新订单详情后重试'
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
expectedVersion: pickingExpectedVersion.value,
|
|
|
|
|
|
expectedCount: picking.value?.expectedCount ?? 0,
|
|
|
|
|
|
pickedCount: picking.value?.pickedCount ?? 0,
|
|
|
|
|
|
remark: loadingRemark.value || undefined,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function startPickingTask() {
|
|
|
|
|
|
const request = pickingRequest()
|
|
|
|
|
|
if (!request) return
|
|
|
|
|
|
const result = await run('picking-start', () => startPicking(props.tenantId, props.inboundId, request), '拣货任务已开始')
|
|
|
|
|
|
if (result) picking.value = result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function completePickingTask() {
|
|
|
|
|
|
const request = pickingRequest()
|
|
|
|
|
|
if (!request) return
|
|
|
|
|
|
const result = await run('picking-complete', () => completePicking(props.tenantId, props.inboundId, request), '拣货已完成')
|
|
|
|
|
|
if (result) picking.value = result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 12:05:20 +08:00
|
|
|
|
async function uploadAndSaveEvidence(type: EvidenceType, event: Event) {
|
|
|
|
|
|
const input = event.target as HTMLInputElement
|
|
|
|
|
|
const file = input.files?.[0]
|
|
|
|
|
|
input.value = ''
|
|
|
|
|
|
if (!file || !canEvidence.value) return
|
2026-08-11 11:15:44 +08:00
|
|
|
|
const sealNo = sealNoDraft.value.trim()
|
|
|
|
|
|
if (type === 'SEAL' && !sealNo) {
|
|
|
|
|
|
error.value = '上传带锁码照前必须填写锁码'
|
|
|
|
|
|
errorDetails.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-10 12:05:20 +08:00
|
|
|
|
const uploaded = await run(`evidence-upload-${type}`, () => uploadEvidenceFile(props.tenantId, props.inboundId, type, file), '文件已上传,正在登记证据')
|
|
|
|
|
|
if (!uploaded) return
|
|
|
|
|
|
const result = await run(`evidence-${type}`, () => upsertEvidence(props.tenantId, props.inboundId, {
|
2026-08-11 11:15:44 +08:00
|
|
|
|
expectedVersion: 0,
|
2026-08-10 12:05:20 +08:00
|
|
|
|
evidenceType: type,
|
|
|
|
|
|
fileId: uploaded.fileId,
|
2026-08-11 11:15:44 +08:00
|
|
|
|
sealNo: type === 'SEAL' ? sealNo : undefined,
|
2026-08-10 12:05:20 +08:00
|
|
|
|
}), `${type === 'SEAL' ? '带锁码照' : '证据'}已登记`)
|
2026-08-11 11:15:44 +08:00
|
|
|
|
if (result?.evidences) {
|
|
|
|
|
|
evidences.value = result.evidences.evidences || []
|
|
|
|
|
|
if (type === 'SEAL') sealNoDraft.value = ''
|
|
|
|
|
|
}
|
2026-08-10 12:05:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function markEvidenceVoided(item: Evidence) {
|
|
|
|
|
|
if (!canEvidence.value || !item.id || String(item.id).startsWith('draft-')) return
|
2026-08-11 18:06:35 +08:00
|
|
|
|
let reason = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await ElMessageBox.prompt('作废后照片仍会保留在审计记录中。', '作废证据照片', {
|
|
|
|
|
|
inputType: 'textarea',
|
|
|
|
|
|
inputPlaceholder: '请输入作废原因',
|
|
|
|
|
|
inputValidator: (value) => Boolean(value?.trim()) || '作废原因不能为空',
|
|
|
|
|
|
confirmButtonText: '确认作废',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
})
|
|
|
|
|
|
reason = result.value.trim()
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
if (cause === 'cancel' || cause === 'close') return
|
|
|
|
|
|
throw cause
|
|
|
|
|
|
}
|
2026-08-10 12:05:20 +08:00
|
|
|
|
const result = await run(`evidence-void-${item.id}`, () => voidEvidence(props.tenantId, props.inboundId, item.id, {
|
|
|
|
|
|
expectedVersion: item.version,
|
|
|
|
|
|
reason,
|
|
|
|
|
|
}), '证据已作废')
|
|
|
|
|
|
if (result?.evidences) evidences.value = result.evidences.evidences || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
|
async function previewEvidence(item: Evidence) {
|
|
|
|
|
|
if (!canEvidenceView.value || !item.fileId) return
|
|
|
|
|
|
busy.value = `evidence-preview-${item.id}`
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
errorDetails.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = await downloadEvidenceFile(props.tenantId, props.inboundId, item.fileId)
|
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
|
window.open(url, '_blank', 'noopener,noreferrer')
|
|
|
|
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 60_000)
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
const details = getApiErrorDetails(cause, '证据照片加载失败,请重试')
|
|
|
|
|
|
error.value = details.message
|
|
|
|
|
|
errorDetails.value = details
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
busy.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 13:56:04 +08:00
|
|
|
|
async function finishLoading() {
|
|
|
|
|
|
const expectedVersion = inboundExpectedVersion.value
|
|
|
|
|
|
if (expectedVersion === null) {
|
|
|
|
|
|
error.value = '入库单版本缺失,刷新订单详情后才能确认装柜完成'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await run('loading-complete', () => completeLoading(props.tenantId, props.inboundId, {
|
|
|
|
|
|
expectedVersion,
|
|
|
|
|
|
remark: loadingRemark.value || undefined,
|
|
|
|
|
|
}), '装柜完成已提交')
|
|
|
|
|
|
if (result) await refresh()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => props.mainStatus, () => { void refresh() })
|
|
|
|
|
|
onMounted(() => { void refresh() })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<section class="workflow-panel" aria-label="订单后半程作业">
|
2026-08-10 16:24:54 +08:00
|
|
|
|
<div class="workflow-heading" aria-label="订单作业上下文">
|
|
|
|
|
|
<div class="workflow-context"><span>入库作业</span><strong>#{{ props.inboundId }}</strong></div>
|
|
|
|
|
|
<el-tooltip content="刷新作业数据" placement="top"><el-button text :loading="loading" aria-label="刷新作业数据" @click="refresh"><RefreshCw :size="15" /></el-button></el-tooltip>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert v-if="error" class="workflow-alert" type="error" :closable="false" show-icon>
|
|
|
|
|
|
<template #title>{{ error }}</template>
|
2026-08-10 15:36:53 +08:00
|
|
|
|
<ApiErrorMeta v-if="errorDetails" :details="errorDetails" />
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<el-button link type="danger" @click="refresh">重试</el-button>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-alert v-if="notice" class="workflow-alert" type="success" :closable="false" show-icon>{{ notice }}</el-alert>
|
|
|
|
|
|
<el-alert v-if="busy" class="workflow-alert" type="info" :closable="false">{{ idempotentMessage(busy) }}</el-alert>
|
2026-08-11 11:15:44 +08:00
|
|
|
|
<el-alert v-if="props.readOnly" class="workflow-alert" type="info" :closable="false" show-icon>当前为只读查看,不能修改作业数据。</el-alert>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
|
2026-08-10 12:05:20 +08:00
|
|
|
|
<el-card v-if="dispatch" shadow="never" class="workflow-card">
|
|
|
|
|
|
<template #header><div class="card-title"><span>子流程与门禁摘要</span><el-tag :type="dispatch.gate?.passed ? 'success' : 'warning'">{{ dispatch.gate?.passed ? '已通过' : '待补齐' }}</el-tag></div></template>
|
|
|
|
|
|
<div class="gate-grid">
|
2026-08-11 18:43:56 +08:00
|
|
|
|
<div><span>订舱</span><strong>{{ oceanBookingStatusLabel(dispatch.oceanBookingStatus, '未关联') }}</strong></div>
|
|
|
|
|
|
<div><span>拖车</span><strong>{{ truckAppointmentStatusLabel(dispatch.truckAppointmentStatus, '未关联') }}</strong></div>
|
2026-08-10 12:05:20 +08:00
|
|
|
|
<div><span>柜号</span><strong>{{ dispatch.containerNo || '暂无数据' }}</strong></div>
|
|
|
|
|
|
<div v-for="item in dispatch.gate?.items || []" :key="item.code"><span>{{ gateItemName(item.code, item.name) }}</span><el-tag size="small" :type="item.passed ? 'success' : 'danger'">{{ item.passed ? '已满足' : '待补齐' }}</el-tag></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="workflow-hint">此处为订舱、拖车、PDA、报关、证据和船期的只读摘要;装柜完成仍以服务端 LOADED 门禁为准。</p>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<template v-if="isSplit">
|
2026-08-10 16:24:54 +08:00
|
|
|
|
<el-card shadow="never" class="workflow-card surface-panel table-panel">
|
2026-08-10 15:36:53 +08:00
|
|
|
|
<template #header><div class="card-title"><span>入库预装清单</span><el-tag>{{ preloadStatusLabel(preload?.status) }}</el-tag></div></template>
|
|
|
|
|
|
<el-alert v-if="!canPreloadView" type="warning" :closable="false" show-icon>当前账号无预装清单查询权限。</el-alert>
|
|
|
|
|
|
<LoadErrorState v-else-if="preloadLoadError" title="预装清单暂时无法加载" :message="preloadLoadError.message" :loading="loading" @retry="loadPreload">
|
|
|
|
|
|
<template #details><ApiErrorMeta :details="preloadLoadError" /></template>
|
|
|
|
|
|
</LoadErrorState>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<DataTable :data="preloadItems" :columns="preloadColumns" :loading="loading" empty-text="暂无预装明细,请先新增">
|
|
|
|
|
|
<template #actions="{ $index }">
|
2026-08-12 17:45:49 +08:00
|
|
|
|
<el-button size="small" plain type="primary" :disabled="!canSplit || Boolean(busy)" :aria-label="`编辑第 ${Number($index) + 1} 条预装明细`" @click="editItem(Number($index))"><Pencil :size="14" />编辑</el-button>
|
|
|
|
|
|
<el-button size="small" plain type="danger" :disabled="!canSplit || Boolean(busy)" :aria-label="`删除第 ${Number($index) + 1} 条预装明细`" @click="removeItem(Number($index))"><Trash2 :size="14" />删除</el-button>
|
2026-08-10 15:36:53 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template #volume="{ row }">{{ formatVolume((row as PreloadItem).volumeCbm) }}</template>
|
|
|
|
|
|
<template #toolbar-actions><el-button :disabled="!canSplit || Boolean(busy)" @click="addItem"><Plus :size="15" />新增明细</el-button></template>
|
|
|
|
|
|
</DataTable>
|
|
|
|
|
|
<el-input v-model="splitRemark" :disabled="!canSplit || Boolean(busy)" class="workflow-remark" type="textarea" :rows="2" maxlength="1000" show-word-limit aria-label="分柜备注" placeholder="分柜备注(可选)" />
|
|
|
|
|
|
</template>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<div class="workflow-actions">
|
2026-08-10 15:36:53 +08:00
|
|
|
|
<el-button :disabled="!canSplit || Boolean(busy) || Boolean(preloadLoadError)" :loading="busy === 'smart-split'" @click="runSmartSplit"><Save :size="15" />保存分柜清单</el-button>
|
|
|
|
|
|
<el-button :disabled="!canPreload || !preload || preload.status !== 'PENDING' || Boolean(busy)" :loading="busy === 'preload-confirming'" @click="startPreloadConfirmation"><Play :size="15" />提交确认</el-button>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<el-button type="primary" :disabled="!canPreload || !preload || preload?.status !== 'CONFIRMING' || Boolean(busy)" :loading="busy === 'preload-confirm'" @click="finishPreloadConfirmation"><Check :size="15" />确认预装清单</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
2026-08-10 15:36:53 +08:00
|
|
|
|
|
|
|
|
|
|
<el-card v-if="dispatch?.containerBookingId" shadow="never" class="workflow-card">
|
|
|
|
|
|
<SmartLoadingWorkbench
|
|
|
|
|
|
:booking-id="String(dispatch.containerBookingId)"
|
|
|
|
|
|
:highlight-inbound-id="props.inboundId"
|
|
|
|
|
|
:read-only="props.readOnly"
|
|
|
|
|
|
@updated="refresh"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-alert v-else type="info" :closable="false" show-icon>
|
|
|
|
|
|
当前订单尚未关联柜次装柜计划;请先在货柜智能排柜工作台选择柜次并分配货物。
|
|
|
|
|
|
</el-alert>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="isLoading">
|
|
|
|
|
|
<el-card shadow="never" class="workflow-card">
|
|
|
|
|
|
<template #header><div class="card-title"><span>拣货与装柜</span><el-tag type="warning">装柜中</el-tag></div></template>
|
|
|
|
|
|
<el-descriptions v-if="picking" :column="2" border>
|
2026-08-11 18:43:56 +08:00
|
|
|
|
<el-descriptions-item label="拣货状态">{{ pickingStatusLabel(picking.status) }}</el-descriptions-item>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<el-descriptions-item label="拣货版本">{{ picking.version }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="应拣件数">{{ picking.expectedCount ?? '—' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="已拣件数">{{ picking.pickedCount ?? '—' }}</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
<el-empty v-else-if="!loading" description="暂无拣货任务" />
|
|
|
|
|
|
<div class="workflow-actions">
|
|
|
|
|
|
<el-button :disabled="!canPicking || !picking || Boolean(busy)" :loading="busy === 'picking-start'" @click="startPickingTask"><Play :size="15" />开始拣货</el-button>
|
|
|
|
|
|
<el-button :disabled="!canPicking || !picking || Boolean(busy)" :loading="busy === 'picking-complete'" @click="completePickingTask"><Check :size="15" />完成拣货</el-button>
|
|
|
|
|
|
</div>
|
2026-08-10 09:12:06 +08:00
|
|
|
|
<el-input v-model="loadingRemark" :disabled="props.readOnly" class="workflow-remark" type="textarea" :rows="2" aria-label="装柜备注" placeholder="装柜备注(可选)" />
|
2026-08-06 13:56:04 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<el-card v-if="isLoading || isLoaded" v-loading="loading" shadow="never" class="workflow-card">
|
|
|
|
|
|
<template #header><div class="card-title"><span>装柜五类证据与锁码</span><el-tag :type="evidenceReady ? 'success' : 'info'">{{ evidenceReady ? '证据齐全' : '五类必传' }}</el-tag></div></template>
|
|
|
|
|
|
<el-alert v-if="!canEvidenceView" type="warning" :closable="false" show-icon>当前账号无装柜证据查询权限。</el-alert>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<div class="evidence-grid">
|
|
|
|
|
|
<div v-for="entry in evidenceTypes" :key="entry.type" class="evidence-item">
|
2026-08-11 11:15:44 +08:00
|
|
|
|
<strong><Camera :size="15" />{{ entry.label }}<el-tag size="small" :type="activeEvidences(entry.type).length ? 'success' : 'danger'">{{ activeEvidences(entry.type).length }} 张</el-tag></strong>
|
2026-08-10 12:05:20 +08:00
|
|
|
|
<div class="evidence-control">
|
2026-08-11 11:15:44 +08:00
|
|
|
|
<el-input v-if="entry.type === 'SEAL' && canEvidence" v-model="sealNoDraft" maxlength="64" show-word-limit aria-label="下一张照片的锁码" placeholder="先填写锁码,再上传带锁码照" />
|
|
|
|
|
|
<label v-if="canEvidence" class="file-picker"><Camera :size="14" />追加照片<input type="file" accept="image/*" @change="uploadAndSaveEvidence(entry.type, $event)" /></label>
|
2026-08-10 12:05:20 +08:00
|
|
|
|
</div>
|
2026-08-11 11:15:44 +08:00
|
|
|
|
<div v-if="canEvidenceView" class="evidence-files">
|
|
|
|
|
|
<div v-for="item in activeEvidences(entry.type)" :key="String(item.id)" class="evidence-file">
|
|
|
|
|
|
<span>{{ item.fileName || `证据 #${item.id}` }}<template v-if="item.sealNo"> · 锁码 {{ item.sealNo }}</template></span>
|
|
|
|
|
|
<div class="evidence-file__actions">
|
2026-08-12 17:45:49 +08:00
|
|
|
|
<el-button size="small" plain type="primary" :disabled="Boolean(busy)" @click="previewEvidence(item)"><Eye :size="14" />查看</el-button>
|
|
|
|
|
|
<el-button v-if="canEvidence" size="small" plain type="danger" :disabled="Boolean(busy)" @click="markEvidenceVoided(item)"><Trash2 :size="14" />作废</el-button>
|
2026-08-11 11:15:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-if="activeEvidences(entry.type).length === 0" class="workflow-hint">尚未上传有效照片</span>
|
2026-08-10 12:05:20 +08:00
|
|
|
|
</div>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-10 12:05:20 +08:00
|
|
|
|
<p class="workflow-hint">五类证据各至少一张有效照片;锁码文本与带锁码照同时必填。错误照片只能作废并保留审计记录。</p>
|
2026-08-11 11:15:44 +08:00
|
|
|
|
<div v-if="isLoading" class="workflow-actions">
|
|
|
|
|
|
<el-button type="primary" :disabled="!canLoadingComplete || !evidenceReady || inboundExpectedVersion === null || Boolean(busy)" :loading="busy === 'loading-complete'" @click="finishLoading"><Check :size="15" />完成装柜</el-button>
|
|
|
|
|
|
<span v-if="!evidenceReady" class="workflow-hint">五类有效照片及锁码齐全后可提交;其他门禁仍由服务端校验</span>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<span v-if="inboundExpectedVersion === null" class="workflow-hint">缺少入库版本,刷新订单详情后可提交</span>
|
|
|
|
|
|
</div>
|
2026-08-11 11:15:44 +08:00
|
|
|
|
</el-card>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
|
2026-08-10 12:05:20 +08:00
|
|
|
|
<el-alert v-if="isLoaded" class="workflow-alert" type="success" :closable="false" show-icon>
|
|
|
|
|
|
已装柜完成。国内端不提供在线冲正、回退或推进派送完成,后续状态仅接受海外/尾程可信回传。
|
|
|
|
|
|
</el-alert>
|
2026-08-10 15:36:53 +08:00
|
|
|
|
|
|
|
|
|
|
<AppDialog
|
|
|
|
|
|
v-model="preloadDialogOpen"
|
|
|
|
|
|
:title="preloadDialogMode === 'create' ? '新增预装明细' : '编辑预装明细'"
|
|
|
|
|
|
confirm-text="保存明细"
|
|
|
|
|
|
:confirm-disabled="Boolean(busy)"
|
|
|
|
|
|
@confirm="saveItem"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form label-position="top" @submit.prevent>
|
|
|
|
|
|
<el-form-item label="柜号" required :error="preloadDraftError === '柜号不能为空' ? preloadDraftError : ''">
|
|
|
|
|
|
<el-input v-model="preloadDraft.containerCode" maxlength="64" placeholder="请输入柜号" @input="preloadDraftError = ''" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="货物引用" required :error="preloadDraftError === '货物引用不能为空' ? preloadDraftError : ''">
|
2026-08-14 19:59:32 +08:00
|
|
|
|
<el-input v-model="preloadDraft.cargoReference" maxlength="128" placeholder="入仓码或业务引用" @input="preloadDraftError = ''" />
|
2026-08-10 15:36:53 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<div class="preload-dialog-grid">
|
|
|
|
|
|
<el-form-item label="箱数" required><el-input-number v-model="preloadDraft.packageCount" :min="1" :step="1" step-strictly controls-position="right" @change="preloadDraftError = ''" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="体积(m³)"><el-input-number v-model="preloadDraft.volumeCbm" :min="0" :precision="3" controls-position="right" @change="preloadDraftError = ''" /></el-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-form-item label="明细备注"><el-input v-model="preloadDraft.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
|
|
|
|
|
|
<el-alert v-if="preloadDraftError" type="error" :closable="false" show-icon>{{ preloadDraftError }}</el-alert>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</AppDialog>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.workflow-panel { display: grid; gap: 14px; }
|
|
|
|
|
|
.workflow-heading, .card-title, .workflow-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
|
|
|
|
.workflow-heading { justify-content: space-between; }
|
2026-08-10 16:24:54 +08:00
|
|
|
|
.workflow-context { display: grid; min-width: 0; gap: 3px; }
|
|
|
|
|
|
.workflow-context span { color: var(--cc-text-secondary, #64748b); font-size: 11px; }
|
|
|
|
|
|
.workflow-context strong { overflow: hidden; color: var(--cc-text, #1f2937); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workflow-card { border-radius: 12px; }
|
|
|
|
|
|
.card-title { justify-content: space-between; }
|
2026-08-10 15:36:53 +08:00
|
|
|
|
.preload-dialog-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workflow-actions { margin-top: 14px; }
|
|
|
|
|
|
.workflow-remark { margin-top: 10px; }
|
|
|
|
|
|
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
2026-08-11 11:15:44 +08:00
|
|
|
|
.evidence-item { display: grid; min-width: 0; gap: 8px; align-content: start; padding: 12px; border: 1px solid var(--cc-border); border-radius: 6px; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.evidence-item strong { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
2026-08-10 12:05:20 +08:00
|
|
|
|
.evidence-control { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
2026-08-11 11:15:44 +08:00
|
|
|
|
.evidence-files { display: grid; gap: 4px; }
|
|
|
|
|
|
.evidence-file { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
|
|
|
|
|
|
.evidence-file span { min-width: 0; overflow-wrap: anywhere; }
|
|
|
|
|
|
.evidence-file__actions { display: flex; flex: 0 0 auto; align-items: center; gap: 4px; }
|
2026-08-10 12:05:20 +08:00
|
|
|
|
.gate-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
|
|
|
|
.gate-grid > div { display: flex; min-height: 34px; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 9px; border: 1px solid var(--cc-border); border-radius: 5px; }
|
|
|
|
|
|
.gate-grid span { color: var(--cc-text-secondary, #64748b); font-size: 12px; }
|
|
|
|
|
|
.gate-grid strong { color: var(--cc-text, #1f2937); font-size: 12px; }
|
|
|
|
|
|
.file-picker { display: inline-flex; align-items: center; gap: 4px; color: var(--cc-primary); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
|
|
|
|
.file-picker input { display: none; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workflow-alert { margin-bottom: 0; }
|
|
|
|
|
|
.workflow-hint { color: var(--cc-text-secondary, #64748b); font-size: 12px; }
|
|
|
|
|
|
@media (max-width: 640px) {
|
2026-08-10 15:36:53 +08:00
|
|
|
|
.preload-dialog-grid, .evidence-grid { grid-template-columns: 1fr; }
|
|
|
|
|
|
.preload-dialog-grid { gap: 0; }
|
2026-08-10 12:05:20 +08:00
|
|
|
|
.evidence-item, .gate-grid { grid-template-columns: 1fr; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workflow-actions > .el-button { flex: 1 1 150px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|