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

+40 -3
View File
@@ -35,8 +35,18 @@
<!-- 联系人列表 -->
<div v-if="customerId" class="panel dialog-list-panel">
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
<el-button class="btn-main" type="primary" @click="handleAddContact">
<el-icon><Plus /></el-icon>新增
</el-button>
</div>
</div>
</div>
<div class="list-body list-wrap dialog-list-body-only">
<el-table
ref="tableRef"
v-loading="loading"
:data="contactList"
class="list-table"
@@ -103,6 +113,8 @@
</el-button>
</div>
</template>
<ContactFormDialog ref="contactFormRef" @success="handleContactFormSuccess" />
</el-dialog>
</template>
@@ -110,9 +122,10 @@
import { useDictStore } from '@/store/modules/dict'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import ContactFormDialog from '@/views/crm/customer/components/ContactFormDialog.vue'
import { Check, Close, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import type { ElFormInstance } from 'element-plus'
import type { ElFormInstance, TableInstance } from 'element-plus'
import { fetchContactList } from '@/api/crm/contact'
import type { ContactQuery, ContactVO } from '@/api/crm/customer'
@@ -167,6 +180,8 @@ const loading = ref(false)
const total = ref(0)
const currentContact = ref<ContactVO | null>(null)
const queryFormRef = ref<ElFormInstance>()
const tableRef = ref<TableInstance>()
const contactFormRef = ref<InstanceType<typeof ContactFormDialog>>()
const resetQueryParams = () => {
queryParams.value.pageNum = 1
@@ -176,7 +191,7 @@ const resetQueryParams = () => {
queryFormRef.value?.resetFields()
}
const getList = async () => {
const getList = async (selectId?: string | number) => {
if (!props.customerId) {
contactList.value = []
total.value = 0
@@ -196,6 +211,9 @@ const getList = async () => {
})
contactList.value = res.list || res.rows || []
total.value = res.total ?? 0
if (selectId) {
selectContactById(selectId)
}
} catch (error) {
console.error('获取联系人列表失败:', error)
} finally {
@@ -203,6 +221,25 @@ const getList = async () => {
}
}
const selectContactById = (id?: string | number) => {
if (!id) return
const row = contactList.value.find((item) => String(item.id) === String(id))
if (!row) return
currentContact.value = row
tableRef.value?.setCurrentRow(row)
}
const handleAddContact = () => {
if (!props.customerId) return
contactFormRef.value?.open(undefined, props.customerId)
}
const handleContactFormSuccess = async (payload?: { id?: string | number }) => {
resetQueryParams()
queryParams.value.pageNum = 1
await getList(payload?.id)
}
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
@@ -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> {
@@ -30,7 +30,8 @@
<ArtIconButton
v-if="isLeftMenu && shouldShowMenuButton"
icon="fold"
class="ml-3 max-sm:ml-[7px]"
class="ml-3 max-sm:ml-[7px] menu-fold-btn"
:class="{ 'is-menu-collapsed': !menuOpen }"
@click="visibleMenu"
/>
@@ -376,6 +377,14 @@
}
}
.menu-fold-btn :deep(.art-svg-icon) {
transition: transform 0.3s ease;
}
.menu-fold-btn.is-menu-collapsed :deep(.art-svg-icon) {
transform: rotate(180deg);
}
/* Hover animation classes */
.refresh-btn:hover :deep(.art-svg-icon) {
animation: rotate180 0.5s;
@@ -11,6 +11,7 @@
<ArtIconButton
icon="fold"
class="portal-header-icon-btn portal-header-fold-btn"
:class="{ 'is-menu-collapsed': !menuOpen }"
@click="toggleSidebar"
/>
<ArtPortalModuleSelect />
@@ -190,6 +191,14 @@
}
}
.portal-header-fold-btn :deep(.art-svg-icon) {
transition: transform 0.3s ease;
}
.portal-header-fold-btn.is-menu-collapsed :deep(.art-svg-icon) {
transform: rotate(180deg);
}
@media only screen and (width <= 800px) {
.portal-header-title {
display: none;