Files
shanxia2.0/web-admin/src/views/db/weighing/index.vue
T
2026-10-05 12:57:51 +08:00

1424 lines
48 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-container">
<section class="panel">
<div class="list-toolbar">
<div class="list-toolbar-row db-list-toolbar-row">
<ListToolbarDateShortcuts
variant="pills"
:active="activeDateShortcut"
aria-label="日期快捷筛选"
@change="handleDateShortcut"
/>
<ListQuickSearchInput
v-model="searchKeyword"
placeholder="车牌号 / 货物 / 计划号 / 出入库单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增地磅单
</el-button>
<ListToolbarOverflow :items="toolbarOverflowItems">
<template #export>
<ListExportDropdown
:export-fn="(payload) => weighingApi.export(payload)"
:get-filter-params="getWeighingExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="weighingExportColumns"
filename="地磅单列表.xlsx"
/>
</template>
</ListToolbarOverflow>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadData"
@reset-columns="handleResetColumns"
/>
</div>
</div>
<div class="list-body list-wrap">
<div ref="weighingTableWrapRef" class="table-scroll bx-list-table-wrap">
<ListTable
ref="weighingTableRef"
table-key="db-weighing"
class="list-table"
:height="tableHeight"
:data="paginatedData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
@header-dragend="() => weighingStatsRowRef?.resync?.()"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('inboundOutboundId')"
prop="inboundOutboundId"
label="出入库单ID"
min-width="100"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.inboundOutboundId || '—' }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('inventoryDate')"
prop="inventoryDate"
label="日期"
min-width="120"
align="center"
header-align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">日期</span>
<ColumnHeaderFilter
prop="inventoryDate"
label="日期"
type="datetime"
:is-active="isFilterActive('inventoryDate')"
:model-value="filters.inventoryDate || null"
@update:model-value="(v) => setFilterState('inventoryDate', v)"
/>
</div>
</template>
<template #default="{ row }">
{{ getRecordDate(row) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planNo')"
prop="planNo"
label="计划单号"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">计划单号</span>
<ColumnHeaderFilter
prop="planNo"
label="计划单号"
type="text"
:is-active="isFilterActive('planNo')"
:model-value="filters.planNo || null"
@update:model-value="(v) => setFilterState('planNo', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
prop="batchNo"
label="出入库单号"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">出入库单号</span>
<ColumnHeaderFilter
prop="batchNo"
label="出入库单号"
type="text"
:is-active="isFilterActive('batchNo')"
:model-value="filters.batchNo || null"
@update:model-value="(v) => setFilterState('batchNo', v)"
/>
</div>
</template>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.batchNo }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('vehicleNo')"
prop="vehicleNo"
label="车牌号"
min-width="120"
align="center"
header-align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">车牌号</span>
<ColumnHeaderFilter
prop="vehicleNo"
label="车牌号"
type="text"
:is-active="isFilterActive('vehicleNo')"
:model-value="filters.vehicleNo || null"
@update:model-value="(v) => setFilterState('vehicleNo', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('cargoName')"
prop="cargoName"
label="货物名称"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">货物名称</span>
<ColumnHeaderFilter
prop="cargoName"
label="货物名称"
type="text"
:is-active="isFilterActive('cargoName')"
:model-value="filters.cargoName || null"
@update:model-value="(v) => setFilterState('cargoName', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('specification')"
prop="specification"
label="规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('businessType')"
prop="businessType"
label="业务类型"
min-width="100"
align="center"
header-align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">业务类型</span>
<ColumnHeaderFilter
prop="businessType"
label="业务类型"
type="enum"
:is-active="isFilterActive('businessType')"
:model-value="filters.businessType || null"
:options="enumOptionsFor('businessType')"
@update:model-value="(v) => setFilterState('businessType', v)"
/>
</div>
</template>
<template #default="{ row }">
<el-tag :type="row.businessType === '入库' ? 'success' : 'warning'" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('docStatus')"
label="单据状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.isVoided === 1 ? 'danger' : 'success'" size="small">
{{ row.isVoided === 1 ? '已作废' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('supplier')"
prop="supplier"
label="供货单位"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">供货单位</span>
<ColumnHeaderFilter
prop="supplier"
label="供货单位"
type="text"
:is-active="isFilterActive('supplier')"
:model-value="filters.supplier || null"
@update:model-value="(v) => setFilterState('supplier', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('consignee')"
prop="consignee"
label="收货单位"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">收货单位</span>
<ColumnHeaderFilter
prop="consignee"
label="收货单位"
type="text"
:is-active="isFilterActive('consignee')"
:model-value="filters.consignee || null"
@update:model-value="(v) => setFilterState('consignee', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('currentQuantity')"
label="本次量(吨)"
min-width="110"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ formatKgToTon(row.currentQuantity) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('netWeight')"
label="净重(吨)"
min-width="110"
align="right"
header-align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">净重(吨)</span>
<ColumnHeaderFilter
prop="netWeight"
label="净重(吨)"
type="number"
:is-active="isFilterActive('netWeight')"
:model-value="filters.netWeight || null"
@update:model-value="(v) => setFilterState('netWeight', v)"
/>
</div>
</template>
<template #default="{ row }">
{{ formatKgToTon(row.netWeight) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('grossWeight')"
prop="grossWeight"
label="毛重(kg)"
min-width="110"
align="right"
header-align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">毛重(kg)</span>
<ColumnHeaderFilter
prop="grossWeight"
label="毛重(kg)"
type="number"
:is-active="isFilterActive('grossWeight')"
:model-value="filters.grossWeight || null"
@update:model-value="(v) => setFilterState('grossWeight', v)"
/>
</div>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('grossWeightTime')"
prop="grossWeightTime"
label="毛重时间"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('tareWeight')"
prop="tareWeight"
label="皮重(kg)"
min-width="110"
align="right"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('tareWeightTime')"
prop="tareWeightTime"
label="皮重时间"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('deduction')"
prop="deduction"
label="扣杂(kg)"
min-width="110"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ row.deduction || 0 }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('createByName')"
prop="createByName"
label="创建人"
min-width="100"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.createByName || row.createBy || '—' }}
</template>
</el-table-column>
</ListTable>
</div>
<div ref="weighingBelowRef">
<ListColumnStatsRow
ref="weighingStatsRowRef"
storage-key="db-weighing-column-stats"
:columns="weighingStatsRowColumns"
:data="paginatedData"
:table-wrap-el="weighingTableWrapRef"
:get-cell-value="getWeighingStatCellValue"
/>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-pagination
v-show="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 100, 500]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</section>
<el-dialog
v-model="modalOpen"
:title="editingRecord ? '修改地磅单' : '新增地磅单'"
width="800px"
align-center
destroy-on-close
:close-on-click-modal="false"
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
@close="handleModalClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="right"
label-width="100px"
class="db-dialog-form form-grid-2"
scroll-to-error
>
<el-form-item label="车牌号" prop="vehicleNo">
<PlVehicleSelect v-model="formData.vehicleNo" />
</el-form-item>
<el-form-item label="产品" prop="productId">
<DbCrmProductSelect
v-model:product-id="formData.productId"
v-model:product-name="formData.cargoName"
v-model:specification="formData.specification"
placeholder="请搜索并选择 CRM 产品"
/>
</el-form-item>
<el-form-item label="规格">
<el-input
v-model="formData.specification"
readonly
placeholder="选择产品后自动带出"
/>
</el-form-item>
<el-form-item label="业务类型" prop="businessType">
<el-radio-group v-model="formData.businessType">
<el-radio value="入库">入库</el-radio>
<el-radio value="出库">出库</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="计划号" prop="planNo">
<el-input v-model="formData.planNo" placeholder="请输入计划号" />
</el-form-item>
<el-form-item label="出入库单号" prop="batchNo">
<el-input v-model="formData.batchNo" placeholder="出入库单号" readonly />
</el-form-item>
<el-form-item label="本次量(吨)">
<el-input :value="currentQuantityInTons" placeholder="本次量(吨)" readonly />
</el-form-item>
<el-form-item label="供货单位" prop="supplier">
<DbCrmCustomerSelect
v-model="formData.supplier"
:allow-create="false"
placeholder="请从客户管理中搜索选择"
/>
</el-form-item>
<el-form-item label="扣杂(kg)" prop="deduction">
<el-input-number
v-model="formData.deduction"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入扣杂"
style="width: 100%"
@change="calculateNetWeight"
/>
</el-form-item>
<el-form-item label="收货单位" prop="consignee">
<DbCrmCustomerSelect
v-model="formData.consignee"
:allow-create="false"
placeholder="请从客户管理中搜索选择"
/>
</el-form-item>
<el-form-item label="毛重(kg)" prop="grossWeight">
<el-input-number
v-model="formData.grossWeight"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入毛重"
style="width: 100%"
@change="handleGrossWeightChange"
/>
</el-form-item>
<el-form-item label="毛重时间" prop="grossWeightTime">
<el-date-picker
v-model="formData.grossWeightTime"
type="datetime"
placeholder="选择时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="皮重(kg)" prop="tareWeight">
<el-input-number
v-model="formData.tareWeight"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入皮重"
style="width: 100%"
@change="handleTareWeightChange"
/>
</el-form-item>
<el-form-item label="皮重时间" prop="tareWeightTime">
<el-date-picker
v-model="formData.tareWeightTime"
type="datetime"
placeholder="选择时间"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="净重(kg)">
<el-input
:value="formData.grossWeight > 0 && formData.tareWeight > 0 ? netWeight.toFixed(2) : '--'"
disabled
/>
<el-alert v-if="isNetInvalid" type="error" :closable="false" style="margin-top: 8px">
净重必须大于0
</el-alert>
</el-form-item>
<el-form-item label="备注" prop="remarks" class="form-col-span-2">
<el-input v-model="formData.remarks" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleModalClose">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button type="primary" :loading="saving" :disabled="isNetInvalid" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<DbRelatedViewDialog
v-model:visible="relatedModalOpen"
:loading="loadingRelated"
:sections="relatedViewSections"
>
<template v-if="viewingRecord" #hero-actions>
<el-button
v-if="viewingRecord.isVoided === 0"
class="btn-main"
type="primary"
@click="handleEdit(viewingRecord)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="viewingRecord.isVoided === 0"
class="btn-ghost"
:disabled="!isWeighingRecordToday(viewingRecord)"
:title="isWeighingRecordToday(viewingRecord) ? '' : '仅支持作废当天的地磅单'"
@click="handleViewVoid"
>
<el-icon><Close /></el-icon>
作废
</el-button>
</template>
</DbRelatedViewDialog>
</div>
</template>
<script setup lang="ts">
/**
* UI 模式 P02 · 复杂业务列表
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
*/
import {
Check,
Close,
Edit,
Plus
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListToolbarOverflow from '@/components/business/list-toolbar-overflow/index.vue'
import type { ListToolbarOverflowItem } from '@/components/business/list-toolbar-overflow/types'
import {
overflowClearFiltersItem,
overflowEditItem,
overflowExportPlaceholder
} from '@/utils/list-toolbar-overflow-items'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import { useColumnSort } from '@/composables/useColumnSort'
import type { DatetimeColumnFilterState } from '@/composables/useClientColumnFilters'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
import DbRelatedViewDialog from '@/components/business/db/DbRelatedViewDialog.vue'
import { buildWeighingRelatedSections } from '@/components/business/db/db-related-view'
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import { $modal } from '@/utils/modal'
import { inventoryApi } from '@/api/db/inventory'
import { planApi } from '@/api/db/plans'
import {
weighingApi,
type WeighingCreate,
type WeighingRecord,
type WeighingRelatedData
} from '@/api/db/weighing'
import { dbWeighingColumnFilterDefs } from './columnFilterDefs'
defineOptions({ name: 'DbWeighing' })
const route = useRoute()
const router = useRouter()
const listColumnDefs = [
{ prop: 'inboundOutboundId', label: '出入库单ID' },
{ prop: 'inventoryDate', label: '日期' },
{ prop: 'planNo', label: '计划单号' },
{ prop: 'vehicleNo', label: '车牌号' },
{ prop: 'cargoName', label: '货物名称' },
{ prop: 'specification', label: '规格' },
{ prop: 'businessType', label: '业务类型' },
{ prop: 'docStatus', label: '单据状态' },
{ prop: 'supplier', label: '供货单位' },
{ prop: 'consignee', label: '收货单位' },
{ prop: 'currentQuantity', label: '本次量(吨)' },
{ prop: 'netWeight', label: '净重(吨)' },
{ prop: 'grossWeight', label: '毛重(kg)' },
{ prop: 'grossWeightTime', label: '毛重时间' },
{ prop: 'tareWeight', label: '皮重(kg)' },
{ prop: 'tareWeightTime', label: '皮重时间' },
{ prop: 'deduction', label: '扣杂(kg)' },
{ prop: 'createByName', label: '创建人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('db-weighing-list-columns-v2', listColumnDefs)
const pad2 = (n: number) => String(n).padStart(2, '0')
const formatYmd = (d: Date) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
const formatYmdHms = (d: Date) =>
`${formatYmd(d)} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
const todayYmd = () => formatYmd(new Date())
const {
filters,
activeFilterProps,
setFilterState,
clearFilter,
isFilterActive,
toQueryParams,
resetToInitial,
watchVisibleColumns
} = useServerColumnFilters(dbWeighingColumnFilterDefs, {
initialFilters: {
inventoryDate: { from: todayYmd(), to: todayYmd() }
},
onChange: () => {
syncActiveShortcutFromDateFilter()
handleSearch()
}
})
function enumOptionsFor(prop: string) {
const def = dbWeighingColumnFilterDefs.find((d) => d.prop === prop)
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
}
const hasClearableColumnFilters = computed(() =>
activeFilterProps.value.some((p) => p !== 'inventoryDate')
)
function handleClearColumnFilters() {
for (const prop of [...activeFilterProps.value]) {
if (prop === 'inventoryDate') continue
clearFilter(prop)
}
}
const toolbarOverflowItems = computed<ListToolbarOverflowItem[]>(() => [
overflowEditItem({
disabled: selectedRows.value.length !== 1 || selectedRows.value[0]?.isVoided === 1,
onClick: () => handleToolbarEdit()
}),
{
key: 'void',
label: '作废',
icon: Close,
disabled:
selectedRows.value.length !== 1 ||
selectedRows.value[0]?.isVoided === 1 ||
!isWeighingRecordToday(selectedRows.value[0]),
onClick: () => handleToolbarVoid()
},
overflowExportPlaceholder(),
overflowClearFiltersItem(hasClearableColumnFilters.value, handleClearColumnFilters)
])
function handleResetColumns() {
resetToDefault()
resetToInitial()
activeDateShortcut.value = 'today'
}
const visibleFilterProps = computed(() => {
const props: string[] = ['batchNo']
for (const def of listColumnDefs) {
if (isColumnVisible(def.prop)) props.push(def.prop)
}
return props
})
watchVisibleColumns(visibleFilterProps)
const weighingExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('planNo')) cols.unshift('planNo')
return cols
})
function getWeighingExportFilterParams() {
return { ...buildListQueryParams() }
}
const weighingTableWrapRef = ref<HTMLElement | null>(null)
const weighingBelowRef = ref<HTMLElement | null>(null)
const weighingTableRef = ref<any>(null)
const weighingStatsRowRef = ref<{ resync?: () => void } | null>(null)
const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
tableEl: weighingTableWrapRef,
pagerEl: weighingBelowRef,
tableRef: weighingTableRef
})
const weighingStatsRowColumns = computed((): StatsRowColumn[] => {
const cols: StatsRowColumn[] = [
{ key: '__selection', interactive: false },
{ key: '__index', interactive: false }
]
if (isColumnVisible('inboundOutboundId')) {
cols.push({ key: 'inboundOutboundId', interactive: true, prop: 'inboundOutboundId' })
}
if (isColumnVisible('inventoryDate')) {
cols.push({ key: 'inventoryDate', interactive: true, prop: 'inventoryDate' })
}
if (isColumnVisible('planNo')) {
cols.push({ key: 'planNo', interactive: true, prop: 'planNo' })
}
cols.push({ key: 'batchNo', interactive: true, prop: 'batchNo' })
for (const def of listColumnDefs) {
if (def.prop === 'inventoryDate' || def.prop === 'planNo' || def.prop === 'inboundOutboundId') {
continue
}
if (!isColumnVisible(def.prop)) continue
cols.push({
key: def.prop,
interactive: true,
prop: def.prop
})
}
return cols
})
const loading = ref(false)
const saving = ref(false)
const allData = ref<WeighingRecord[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const searchKeyword = ref('')
const selectedRows = ref<WeighingRecord[]>([])
const activeDateShortcut = ref<'all' | 'today' | 'tomorrow' | 'yesterday' | 'week' | ''>('today')
const modalOpen = ref(false)
const editingRecord = ref<WeighingRecord | null>(null)
const relatedModalOpen = ref(false)
const loadingRelated = ref(false)
const relatedData = ref<WeighingRelatedData>({ plan: null, inventory: null })
const viewingRecord = ref<WeighingRecord | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, allData.value)
relatedData.value = await weighingApi.getRelated(viewingRecord.value.id)
}
const relatedViewSections = computed(() =>
buildWeighingRelatedSections(relatedData.value, viewingRecord.value)
)
const formRef = ref()
const formData = reactive<
WeighingCreate & {
cargoName: string
specification: string
}
>({
vehicleNo: '',
planNo: '',
productId: '',
cargoName: '',
specification: '',
businessType: '入库',
batchNo: '',
supplier: '',
consignee: '',
grossWeight: 0,
grossWeightTime: '',
tareWeight: 0,
tareWeightTime: '',
deduction: 0,
netWeight: 0,
remarks: '',
purchasePlanId: undefined,
inboundOutboundId: undefined,
weighingPoint: '山峡化工',
plannedQuantity: undefined,
currentQuantity: undefined,
executedQuantity: undefined
})
const formRules = {
vehicleNo: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
productId: [{ required: true, message: '请选择 CRM 产品', trigger: 'change' }],
supplier: [{ required: true, message: '请选择供货单位', trigger: 'change' }],
consignee: [{ required: true, message: '请选择收货单位', trigger: 'change' }]
}
const netWeight = computed(() => {
const gross = formData.grossWeight ?? 0
const tare = formData.tareWeight ?? 0
const deduction = formData.deduction ?? 0
return Number((gross - tare - deduction).toFixed(2))
})
const currentQuantityInTons = computed(() => {
if (formData.currentQuantity !== undefined && formData.currentQuantity !== null) {
return (formData.currentQuantity / 1000).toFixed(2)
}
return '--'
})
const isNetInvalid = computed(() => {
const hasGrossWeight = formData.grossWeight && formData.grossWeight > 0
const hasTareWeight = formData.tareWeight && formData.tareWeight > 0
if (hasGrossWeight && hasTareWeight) {
return netWeight.value <= 0
}
return false
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return allData.value.slice(start, end)
})
const formatKgToTon = (kg: number | undefined | null) => {
if (kg === undefined || kg === null) return '--'
return (kg / 1000).toFixed(2)
}
const getRecordDate = (row: WeighingRecord) =>
row.inventoryDate ||
(row.grossWeightTime ? row.grossWeightTime.split(' ')[0] : '') ||
(row.createdAt ? row.createdAt.split(' ')[0] : '') ||
''
const {
sortField,
sortOrder,
setSort,
clearSort,
isSortActive,
toQueryParams: toSortQueryParams,
watchVisibleColumns: watchSortVisibleColumns
} = useColumnSort({
onChange: () => {
currentPage.value = 1
allData.value = sortWeighingRows(allData.value)
}
})
provide('crmColumnSort', {
setSort,
clearSort,
isSortActive,
sortField,
sortOrder
})
watchSortVisibleColumns(visibleFilterProps)
function sortWeighingRows(rows: WeighingRecord[]) {
const order = sortOrder.value || 'desc'
return [...rows].sort((a, b) => {
const av = getRecordDate(a)
const bv = getRecordDate(b)
if (!av && !bv) return 0
if (!av) return order === 'asc' ? -1 : 1
if (!bv) return order === 'asc' ? 1 : -1
const cmp = av.localeCompare(bv)
return order === 'asc' ? cmp : -cmp
})
}
function getWeighingStatCellValue(row: WeighingRecord, prop: string): unknown {
if (prop === 'inventoryDate') return getRecordDate(row)
if (prop === 'currentQuantity' || prop === 'netWeight') {
return (Number((row as any)[prop]) || 0) / 1000
}
if (prop === 'docStatus') {
return row.isVoided === 1 ? '已作废' : '正常'
}
if (prop === 'createByName') {
return row.createByName || row.createBy || ''
}
return (row as any)?.[prop]
}
const buildListQueryParams = (): Record<string, any> => {
const params: Record<string, any> = { ...toQueryParams(), ...toSortQueryParams() }
const keyword = searchKeyword.value.trim()
if (keyword) params.q = keyword
// UI 净重列单位为吨,库字段为 kg
for (const key of ['netWeight', 'netWeightMin', 'netWeightMax'] as const) {
if (params[key] != null && params[key] !== '') {
const n = Number(params[key])
if (!Number.isNaN(n)) params[key] = String(n * 1000)
}
}
return params
}
function syncActiveShortcutFromDateFilter() {
const s = filters.inventoryDate as DatetimeColumnFilterState | undefined
if (!s?.from || !s?.to) {
activeDateShortcut.value = !s?.from && !s?.to ? 'all' : ''
return
}
const today = new Date()
const todayStr = formatYmd(today)
if (s.from === todayStr && s.to === todayStr) {
activeDateShortcut.value = 'today'
return
}
const tomorrow = new Date(today)
tomorrow.setDate(today.getDate() + 1)
const tomorrowStr = formatYmd(tomorrow)
if (s.from === tomorrowStr && s.to === tomorrowStr) {
activeDateShortcut.value = 'tomorrow'
return
}
const yesterday = new Date(today)
yesterday.setDate(today.getDate() - 1)
const yesterdayStr = formatYmd(yesterday)
if (s.from === yesterdayStr && s.to === yesterdayStr) {
activeDateShortcut.value = 'yesterday'
return
}
const weekday = today.getDay() === 0 ? 7 : today.getDay()
const monday = new Date(today)
monday.setDate(today.getDate() - (weekday - 1))
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
if (s.from === formatYmd(monday) && s.to === formatYmd(sunday)) {
activeDateShortcut.value = 'week'
return
}
activeDateShortcut.value = ''
}
const calculateNetWeight = () => {
formData.netWeight = netWeight.value
}
const handleGrossWeightChange = () => {
calculateNetWeight()
if (formData.grossWeight && formData.grossWeight > 0 && !formData.grossWeightTime) {
formData.grossWeightTime = formatYmdHms(new Date())
}
}
const handleTareWeightChange = () => {
calculateNetWeight()
if (formData.tareWeight && formData.tareWeight > 0 && !formData.tareWeightTime) {
formData.tareWeightTime = formatYmdHms(new Date())
}
}
const loadData = async () => {
loading.value = true
try {
const records = await weighingApi.list(buildListQueryParams())
allData.value = sortWeighingRows(records)
total.value = allData.value.length
const maxPage = Math.ceil(total.value / pageSize.value) || 1
if (currentPage.value > maxPage) {
currentPage.value = 1
}
} catch (error) {
console.error('加载地磅单列表失败', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
currentPage.value = 1
loadData()
}
const handleQuickSearch = () => {
handleSearch()
}
const handleQuickSearchClear = () => {
searchKeyword.value = ''
handleQuickSearch()
}
const handleSelectionChange = (selection: WeighingRecord[]) => {
selectedRows.value = selection
}
const handleDateShortcut = (type: 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week') => {
activeDateShortcut.value = type
const today = new Date()
if (type === 'all') {
setFilterState('inventoryDate', null)
return
}
if (type === 'today') {
const d = formatYmd(today)
setFilterState('inventoryDate', { from: d, to: d })
return
}
if (type === 'tomorrow') {
const t = new Date(today)
t.setDate(t.getDate() + 1)
const d = formatYmd(t)
setFilterState('inventoryDate', { from: d, to: d })
return
}
if (type === 'yesterday') {
const y = new Date(today)
y.setDate(y.getDate() - 1)
const d = formatYmd(y)
setFilterState('inventoryDate', { from: d, to: d })
return
}
if (type === 'week') {
const weekday = today.getDay() === 0 ? 7 : today.getDay()
const monday = new Date(today)
monday.setDate(today.getDate() - (weekday - 1))
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
setFilterState('inventoryDate', { from: formatYmd(monday), to: formatYmd(sunday) })
}
}
const handlePageChange = (page: number) => {
currentPage.value = page
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
}
const resetFormData = () => {
Object.assign(formData, {
vehicleNo: '',
planNo: '',
productId: '',
cargoName: '',
specification: '',
businessType: '入库',
batchNo: '',
supplier: '',
consignee: '',
grossWeight: 0,
grossWeightTime: '',
tareWeight: 0,
tareWeightTime: '',
deduction: 0,
netWeight: 0,
remarks: '',
purchasePlanId: undefined,
inboundOutboundId: undefined,
weighingPoint: '山峡化工',
plannedQuantity: undefined,
currentQuantity: undefined,
executedQuantity: undefined
})
}
const handleAdd = () => {
editingRecord.value = null
resetFormData()
modalOpen.value = true
}
const handleView = async (record: WeighingRecord) => {
viewingRecord.value = record
loadingRelated.value = true
try {
relatedData.value = await weighingApi.getRelated(record.id)
relatedModalOpen.value = true
} catch (error) {
console.error('加载关联数据失败', error)
} finally {
loadingRelated.value = false
}
}
function handleToolbarEdit() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条地磅单')
return
}
handleEdit(selectedRows.value[0])
}
function handleToolbarVoid() {
if (selectedRows.value.length !== 1) {
$modal.msgWarning('请先勾选一条地磅单')
return
}
handleVoid(selectedRows.value[0])
}
async function handleViewVoid() {
if (!viewingRecord.value) return
await handleVoid(viewingRecord.value)
relatedModalOpen.value = false
}
const handleEdit = (record: WeighingRecord) => {
editingRecord.value = record
Object.assign(formData, {
vehicleNo: record.vehicleNo,
planNo: record.planNo || '',
productId: record.productId || '',
cargoName: record.cargoName,
specification: record.specification || '',
businessType: record.businessType,
batchNo: record.batchNo || '',
supplier: record.supplier,
consignee: record.consignee,
grossWeight: record.grossWeight,
grossWeightTime: record.grossWeightTime,
tareWeight: record.tareWeight,
tareWeightTime: record.tareWeightTime,
deduction: record.deduction || 0,
netWeight: record.netWeight,
remarks: record.remarks || '',
purchasePlanId: record.purchasePlanId,
inboundOutboundId: record.inboundOutboundId,
weighingPoint: record.weighingPoint || '山峡化工',
plannedQuantity: record.plannedQuantity,
currentQuantity: record.currentQuantity,
executedQuantity: record.executedQuantity
})
modalOpen.value = true
}
const isWeighingRecordToday = (row: WeighingRecord) => {
return getRecordDate(row) === formatYmd(new Date())
}
const handleVoid = async (row: WeighingRecord) => {
if (!isWeighingRecordToday(row)) {
$modal.msgWarning('仅支持作废当天的地磅单,历史数据不可作废')
return
}
try {
const { promptOptionalCancelReason } = await import('@/utils/prompt-cancel-reason')
const reason = await promptOptionalCancelReason({
title: '作废地磅单',
message: '确定要作废这条地磅单吗?作废原因选填。'
})
if (reason === null) return
await weighingApi.void(row.id, { voidReason: reason || undefined })
$modal.msgSuccess('作废成功')
loadData()
} catch {
// 用户取消或请求失败
}
}
const handleSubmit = async () => {
if (!formRef.value || isNetInvalid.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
saving.value = true
try {
let editedId: string | undefined
const hasGrossWeight = formData.grossWeight && formData.grossWeight > 0
const hasTareWeight = formData.tareWeight && formData.tareWeight > 0
if (hasGrossWeight && hasTareWeight) {
formData.netWeight = netWeight.value
} else {
formData.netWeight = 0
}
const submitData: WeighingCreate = {
vehicleNo: formData.vehicleNo,
planNo: formData.planNo || undefined,
productId: formData.productId,
businessType: formData.businessType,
batchNo: formData.batchNo || undefined,
supplier: formData.supplier,
consignee: formData.consignee,
grossWeight: formData.grossWeight ?? 0,
grossWeightTime: formData.grossWeightTime || '',
tareWeight: formData.tareWeight ?? 0,
tareWeightTime: formData.tareWeightTime || '',
deduction: formData.deduction ?? 0,
netWeight: formData.netWeight,
plannedQuantity: formData.plannedQuantity || undefined,
executedQuantity: formData.executedQuantity || undefined,
purchasePlanId: formData.purchasePlanId || undefined,
inboundOutboundId: formData.inboundOutboundId || undefined,
weighingPoint: formData.weighingPoint || '山峡化工',
remarks: formData.remarks || undefined,
currentQuantity: formData.currentQuantity || undefined
}
if (editingRecord.value) {
editedId = editingRecord.value.id
await weighingApi.update(editingRecord.value.id, {
productId: formData.productId,
vehicleNo: submitData.vehicleNo,
planNo: submitData.planNo,
businessType: submitData.businessType,
batchNo: submitData.batchNo,
supplier: submitData.supplier,
consignee: submitData.consignee,
grossWeight: submitData.grossWeight,
grossWeightTime: submitData.grossWeightTime,
tareWeight: submitData.tareWeight,
tareWeightTime: submitData.tareWeightTime,
deduction: submitData.deduction,
netWeight: submitData.netWeight,
plannedQuantity: submitData.plannedQuantity,
executedQuantity: submitData.executedQuantity,
purchasePlanId: submitData.purchasePlanId,
inboundOutboundId: submitData.inboundOutboundId,
weighingPoint: submitData.weighingPoint,
remarks: submitData.remarks,
currentQuantity: submitData.currentQuantity
})
$modal.msgSuccess('更新成功')
} else {
await weighingApi.create(submitData)
$modal.msgSuccess('保存成功')
}
modalOpen.value = false
await loadData()
if (editedId) await refreshRelatedViewIfOpen(editedId)
} catch (error) {
console.error('保存地磅单失败', error)
} finally {
saving.value = false
}
})
}
const handleModalClose = () => {
modalOpen.value = false
editingRecord.value = null
formRef.value?.resetFields()
}
const clearInventoryRouteQuery = () => {
if (!route.query.inboundOutboundId) return
router.replace({ path: route.path })
}
const openAddFromInventoryQuery = async () => {
const inboundOutboundId = route.query.inboundOutboundId as string
if (!inboundOutboundId) return
const query = { ...route.query }
try {
// 先清空上一张地磅单残留(毛/皮/净重、备注等),再填出入库带入字段
resetFormData()
editingRecord.value = null
const inventory = await inventoryApi.get(inboundOutboundId)
formData.vehicleNo = (query.vehicleNo as string) || inventory.vehicleNo
formData.productId = (query.productId as string) || inventory.productId || ''
formData.cargoName = inventory.product
formData.specification = inventory.specification || ''
const businessType = (query.businessType as '入库' | '出库') || inventory.businessType
formData.businessType = businessType
if (query.supplier) {
formData.supplier = query.supplier as string
} else {
formData.supplier =
businessType === '入库' ? inventory.partnerCompany : '安庆市山峡化工有限公司'
}
if (query.consignee) {
formData.consignee = query.consignee as string
} else {
formData.consignee =
businessType === '入库' ? '安庆市山峡化工有限公司' : inventory.partnerCompany
}
formData.planNo = (query.planNo as string) || inventory.planNo || ''
formData.batchNo = (query.batchNo as string) || inventory.batchNo || ''
formData.inboundOutboundId = inboundOutboundId
formData.weighingPoint = '山峡化工'
const quantityInTons = query.quantity ? Number(query.quantity) : inventory.quantity
formData.currentQuantity = quantityInTons * 1000
const purchasePlanId = (query.purchasePlanId as string) || inventory.purchasePlanId || ''
if (purchasePlanId) {
formData.purchasePlanId = purchasePlanId
try {
const plan = await planApi.get(purchasePlanId)
formData.plannedQuantity = plan.plannedQuantity * 1000
} catch (error) {
console.error('获取计划数据失败', error)
}
}
modalOpen.value = true
clearInventoryRouteQuery()
} catch (error) {
console.error('加载出入库单数据失败', error)
}
}
useDbInstantSearch(
() => ({
q: searchKeyword.value,
columnFilters: { ...toQueryParams() }
}),
handleSearch
)
onMounted(() => {
loadData()
// keepAlive:false 时不会触发 onActivated,须在挂载时处理出入库跳转 query
openAddFromInventoryQuery()
nextTick(() => recalcTableHeight())
})
onActivated(() => {
openAddFromInventoryQuery()
nextTick(() => recalcTableHeight())
})
</script>
<style scoped lang="scss">
.bx-list-table-wrap {
min-width: 0;
}
</style>