提交
This commit is contained in:
1 parent
6d9354a194
commit
aff71d296b
2759 files changed
+860425
-13211
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user