This commit is contained in:
zlwu committed 2026-07-20 10:50:26 +08:00
1 parent 6d9354a194
commit aff71d296b
2759 files changed
+860425 -13211

No files matched your search

@@ -62,6 +62,7 @@ import {
setupDeliverySlipEditorTypeCheckboxes,
type DeliverySlipRenderContext
} from '@/utils/pl-delivery-slip-template'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
const visible = ref(false)
const slipContent = ref('')
@@ -94,8 +95,7 @@ async function waitForImagesLoaded(container: HTMLElement) {
}
function getPdfCanvasScale() {
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
return Math.min(4, Math.max(3, dpr * 2))
return getPlSlipPdfCanvasScale()
}
async function buildSlipRenderContext(plan: Plan): Promise<DeliverySlipRenderContext> {
@@ -60,6 +60,7 @@ import {
restoreInboundSlipPlaceholders,
type InboundSlipRenderContext
} from '@/utils/pl-inbound-slip-template'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
const visible = ref(false)
const slipContent = ref('')
@@ -92,8 +93,7 @@ async function waitForImagesLoaded(container: HTMLElement) {
}
function getPdfCanvasScale() {
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
return Math.min(4, Math.max(3, dpr * 2))
return getPlSlipPdfCanvasScale()
}
async function buildSlipRenderContext(plan: Plan): Promise<InboundSlipRenderContext> {
@@ -115,11 +115,14 @@
<script setup lang="ts">
import { Check, Close } from '@element-plus/icons-vue'
import {
inboundOrderApi,
outboundOrderApi,
planApi,
transportApi,
type Plan,
type PlanArchiveBody,
type PlanProduct,
type PlIoOrder,
type Transport
} from '@/api/pl'
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
@@ -271,6 +274,27 @@ async function handleOpened() {
const rawProducts = Array.isArray(data.products) ? data.products : []
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
productRows.value = mapPlanProducts(enrichedProducts)
// 入库/出库单实际数量预填归档表单(归档提交时后端也会再同步一次)
if (data.planType === 'inbound' || data.planType === 'outbound') {
try {
const api = data.planType === 'inbound' ? inboundOrderApi : outboundOrderApi
const ioRes = await api.getByPlan(data.id)
const io = pickBxData<PlIoOrder | null>(ioRes)
if (io && io.status !== 'voided' && Array.isArray(io.items)) {
const qtyMap = new Map(
io.items
.filter(it => it.planProductId && it.actualQty != null)
.map(it => [it.planProductId, Number(it.actualQty)])
)
productRows.value = productRows.value.map(row => {
if (!row.id || !qtyMap.has(row.id)) return row
return { ...row, actualQty: qtyMap.get(row.id) }
})
}
} catch {
// 无入库/出库单时保持计划原实际数量
}
}
await applyFreightPrefill(data)
} catch (error) {
innerVisible.value = false
@@ -344,9 +344,6 @@ import { useDictStore } from '@/store/modules/dict'
import { isTonProductUnit } from '@/utils/crm-product-display'
import { formatDate } from '@/utils/format'
import { pickBxData, pickBxList } from '@/utils/http/bx'
import { canPrintInboundSlip } from '@/utils/pl-inbound-slip-template'
import { canPrintDeliverySlip } from '@/utils/pl-delivery-slip-template'
import { canPrintSubpackSlip } from '@/utils/pl-subpack-slip-template'
import { formatPlVehiclePlateDisplay } from '@/utils/pl-display'
const dictStore = useDictStore()
@@ -439,15 +436,10 @@ const dialogTitle = computed(() => {
})
const isRearrange = computed(() => plan.value.transportStatus === 'arranged')
const showInboundSlipPrint = computed(
() => arrangeSuccess.value && canPrintInboundSlip(plan.value as Plan)
)
const showDeliverySlipPrint = computed(
() => arrangeSuccess.value && canPrintDeliverySlip(plan.value as Plan)
)
const showSubpackSlipPrint = computed(
() => arrangeSuccess.value && canPrintSubpackSlip(plan.value as Plan)
)
/** 安排成功后始终展示打印入口,与运输计划列表工具栏一致,便于现场立即开单 */
const showInboundSlipPrint = computed(() => arrangeSuccess.value)
const showDeliverySlipPrint = computed(() => arrangeSuccess.value)
const showSubpackSlipPrint = computed(() => arrangeSuccess.value)
function getUnitLabel(unit: string) {
return dictStore.getLabel('crm_product_unit', unit) || unit
@@ -828,12 +820,14 @@ async function handleSubmit() {
generateDibangPlan: formData.generateDibangPlan
}
const wasRearrange = isRearrange.value
const res = await transportApi.arrange(props.planId, payload)
const result = pickBxData<TransportArrangeResult>(res)
arrangeResult.value = result
plan.value = { ...plan.value, transportStatus: 'arranged' }
applyArrangeResult(result, plan.value, products.value)
arrangeSuccess.value = true
ElMessage.success(isRearrange.value ? '运输调整成功' : '运输安排成功')
ElMessage.success(wasRearrange ? '运输调整成功' : '运输安排成功')
await tabsRef.value?.reload()
tabsRef.value?.setActiveTab('transport')
emit('success')
@@ -224,8 +224,21 @@
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>{{ isEdit ? '保存' : '创建' }}
<el-button
:type="showSaveAndArrange ? 'default' : 'primary'"
:class="showSaveAndArrange ? 'btn-ghost' : undefined"
:loading="submitLoading"
@click="handleSubmit('save')"
>
<el-icon><Check /></el-icon>{{ saveButtonLabel }}
</el-button>
<el-button
v-if="showSaveAndArrange"
type="primary"
:loading="submitLoading"
@click="handleSubmit('saveAndArrange')"
>
<el-icon><Van /></el-icon>{{ saveAndArrangeLabel }}
</el-button>
</div>
</template>
@@ -246,7 +259,7 @@
</template>
<script setup lang="ts">
import { Check, Close, Search, View } from '@element-plus/icons-vue'
import { Check, Close, Search, Van, View } from '@element-plus/icons-vue'
import { getContract } from '@/api/crm/contract'
import type { ContractVO } from '@/api/crm/contract/types'
import { carrierApi, planApi, transportApi, vehicleApi, type Carrier, type Plan, type PlanProduct, type PlanUpdatePayload, type Vehicle, type VehiclePersonnelRow } from '@/api/pl'
@@ -262,13 +275,16 @@ import {
mapContractProducts,
type PlanFormProductRow
} from '@/composables/usePlContractToPlan'
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
import {
buildMissingHazardCertReadyMessage,
enrichPlanProductsHazardFromCrm
} from '@/composables/usePlToxicProductCertCheck'
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
import { useDictStore } from '@/store/modules/dict'
import { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/format'
import { pickBxList } from '@/utils/http/bx'
import { pickBxData, pickBxList } from '@/utils/http/bx'
const ContractDetailDialog = defineAsyncComponent(
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
@@ -284,7 +300,7 @@ const props = defineProps<{
const emit = defineEmits<{
'update:visible': [value: boolean]
success: []
success: [payload?: { planId: string; openArrange: boolean }]
}>()
const userStore = useUserStore()
@@ -315,6 +331,13 @@ function resolveCurrentUserName() {
const isEdit = computed(() => !!props.planId)
const dialogTitle = computed(() => (isEdit.value ? '编辑运输计划' : '新增运输计划'))
const showSaveAndArrange = computed(
() => !isEdit.value || loadedTransportStatus.value === 'pending'
)
const saveButtonLabel = computed(() => (isEdit.value ? '保存' : '创建'))
const saveAndArrangeLabel = computed(() =>
isEdit.value ? '保存并安排运输' : '创建并安排运输'
)
const innerVisible = computed({
get: () => props.visible,
@@ -323,6 +346,7 @@ const innerVisible = computed({
const submitLoading = ref(false)
const detailLoading = ref(false)
const loadedTransportStatus = ref('pending')
const contractSelectorVisible = ref(false)
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
@@ -618,6 +642,7 @@ async function restoreFreightFromTransport(plan: Plan) {
}
function resetForm() {
loadedTransportStatus.value = 'pending'
contractSnapshot.value = null
vehicleBundle.value = null
selectedDriverId.value = ''
@@ -716,6 +741,7 @@ async function loadPlanForEdit(planId: string) {
remark: data.remark || '',
products: (data.products || []).map(mapPlanProductToFormRow)
})
loadedTransportStatus.value = data.transportStatus || 'pending'
if (data.contractId) {
try {
@@ -890,26 +916,55 @@ function validateBusinessRules() {
return true
}
async function handleSubmit() {
async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
const valid = await basicFormRef.value?.validate().catch(() => false)
if (!valid || !validateBusinessRules()) return
submitLoading.value = true
try {
const commonPayload = buildPlanCommonPayload({ allowClearTransport: isEdit.value })
let planId = props.planId || ''
if (isEdit.value) {
if (!props.planId) return
await planApi.update(props.planId, commonPayload)
planId = props.planId
ElMessage.success('保存成功')
} else {
await planApi.createFull({
const res = await planApi.createFull({
...commonPayload,
contractId: formData.contractId
} as Parameters<typeof planApi.createFull>[0])
const created = pickBxData<Plan>(res)
planId = String(created?.id || '')
if (!planId) {
ElMessage.error('创建成功但未返回计划 ID')
return
}
ElMessage.success('创建成功')
}
let openArrange = false
if (mode === 'saveAndArrange' && planId) {
const detailRes = await planApi.get(planId)
const plan = pickBxData<Plan>(detailRes)
const rawProducts = Array.isArray(plan?.products) ? plan.products : []
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
const certMessage = buildMissingHazardCertReadyMessage(plan, enrichedProducts)
if (certMessage) {
await ElMessageBox.alert(
`${certMessage},请至【危化品证上传】页面操作。`,
'无法安排运输',
{ type: 'warning' }
)
openArrange = false
} else {
openArrange = true
}
}
innerVisible.value = false
emit('success')
emit('success', { planId, openArrange })
} finally {
submitLoading.value = false
}
@@ -63,6 +63,7 @@ import {
setupSubpackSlipEditorCheckboxes,
type SubpackSlipRenderContext
} from '@/utils/pl-subpack-slip-template'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
const visible = ref(false)
const slipContent = ref('')
@@ -96,8 +97,7 @@ async function waitForImagesLoaded(container: HTMLElement) {
}
function getPdfCanvasScale() {
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1
return Math.min(4, Math.max(3, dpr * 2))
return getPlSlipPdfCanvasScale()
}
async function buildSlipRenderContext(plan: Plan): Promise<SubpackSlipRenderContext> {